-
-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathtelegram.go
334 lines (295 loc) · 9.53 KB
/
telegram.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
// Copyright (c) 2017, Bas van der Lei
// Modified work Copyright (c) 2019 Roald Nefs
package dsmr
import (
"fmt"
"regexp"
"strings"
"time"
)
// DateTimeFormat used in a telegram in the YYMMDDhhmmssX format. The ASCII
// presentation of timestamp with Year, Month, Day, Hour, Minute, Second,
// and an indication whether DST is active (X=S) or DST is not active (X=W).
const DateTimeFormat = "060102150405"
var (
// OBIS Reduces ID-code and value
objectRegexp = regexp.MustCompile("([0-9]+-[0-9]+:[0-9]+.[0-9]+.[0-9]+)\\((.*)\\)")
// Value and unit
// In the DSMR the unit is optional
valueRegexp = regexp.MustCompile(`((.*)\)\()?(.*)\*(.*)`)
)
// Telegram represent a single DSMR P1 message.
type Telegram struct {
Header string
Version string // Version information for P1 output.
DateTime time.Time // Date-time stamp of the P1 message.
DataObjects map[string]DataObject
}
// MeterReadingElectricityDeliveredToClientTariff1 returns the meter reading
// electricity delivered to client (Tariff 1) in 0,001 kWh.
func (t Telegram) MeterReadingElectricityDeliveredToClientTariff1() (string, bool) {
if do, ok := t.DataObjects["1-0:1.8.1"]; ok {
return do.Value, true
}
return "", false
}
// MeterReadingElectricityDeliveredToClientTariff2 returns the meter reading
// electricity delivered to client (Tariff 2) in 0,001 kWh.
func (t Telegram) MeterReadingElectricityDeliveredToClientTariff2() (string, bool) {
if do, ok := t.DataObjects["1-0:1.8.2"]; ok {
return do.Value, true
}
return "", false
}
// MeterReadingElectricityDeliveredByClientTariff1 returns the meter reading
// electricity delivered by client (Tariff 1) in 0,001 kWh.
func (t Telegram) MeterReadingElectricityDeliveredByClientTariff1() (string, bool) {
if do, ok := t.DataObjects["1-0:2.8.1"]; ok {
return do.Value, true
}
return "", false
}
// MeterReadingElectricityDeliveredByClientTariff2 returns the meter reading
// electricity delivered by client (Tariff 2) in 0,001 kWh.
func (t Telegram) MeterReadingElectricityDeliveredByClientTariff2() (string, bool) {
if do, ok := t.DataObjects["1-0:2.8.2"]; ok {
return do.Value, true
}
return "", false
}
// TariffIndicatorElectricity returns the tariff indicator that can be used to
// switch tariff dependent loads e.g. boilers.
func (t Telegram) TariffIndicatorElectricity() (string, bool) {
if do, ok := t.DataObjects["0-0:96.14.0"]; ok {
return do.Value, true
}
return "", false
}
// ActualElectricityPowerDelivered returns the actual electricity power
// delivered (+P) in 1 Watt resolution.
func (t Telegram) ActualElectricityPowerDelivered() (string, bool) {
if do, ok := t.DataObjects["1-0:1.7.0"]; ok {
return do.Value, true
}
return "", false
}
// ActualElectricityPowerReceived returns the actual electricity power
// received (-P) in 1 Watt resolution.
func (t Telegram) ActualElectricityPowerReceived() (string, bool) {
if do, ok := t.DataObjects["1-0:2.7.0"]; ok {
return do.Value, true
}
return "", false
}
// NumberOfPowerFailuresInAnyPhase returns the number of power failures in any
// phase.
func (t Telegram) NumberOfPowerFailuresInAnyPhase() (string, bool) {
if do, ok := t.DataObjects["0-0:96.7.21"]; ok {
return do.Value, true
}
return "", false
}
// NumberOfLongPowerFailuresInAnyPhase returns the number of power failures in any
// phase.
func (t Telegram) NumberOfLongPowerFailuresInAnyPhase() (string, bool) {
if do, ok := t.DataObjects["0-0:96.7.9"]; ok {
return do.Value, true
}
return "", false
}
// TODO update regex so it will match:
//
// None: 1-0:99.97.0()
// Two: 1-0:99.97.0(2)(0-0:96.7.19)(101208152415W)(0000000240*s)(101208151004W)(0000000301*s)
//
// PowerFailureEventLog returns the power failure event log (long power
// failures).
func (t Telegram) PowerFailureEventLog() (string, bool) {
if do, ok := t.DataObjects["1-0:99.97.0"]; ok {
return do.Value, true
}
return "", false
}
// NumberOfVoltageSagsInPhaseL1 returns the number of voltage sags in phase L1.
func (t Telegram) NumberOfVoltageSagsInPhaseL1() (string, bool) {
if do, ok := t.DataObjects["1-0:32.32.0"]; ok {
return do.Value, true
}
return "", false
}
// NumberOfVoltageSagsInPhaseL2 returns the number of voltage sags in phase L2.
func (t Telegram) NumberOfVoltageSagsInPhaseL2() (string, bool) {
if do, ok := t.DataObjects["1-0:52.32.0"]; ok {
return do.Value, true
}
return "", false
}
// NumberOfVoltageSagsInPhaseL3 returns the number of voltage sags in phase L3.
func (t Telegram) NumberOfVoltageSagsInPhaseL3() (string, bool) {
if do, ok := t.DataObjects["1-0:72.32.0"]; ok {
return do.Value, true
}
return "", false
}
// NumberOfVoltageSwellsInPhaseL1 returns the number of voltage swells in phase L1.
func (t Telegram) NumberOfVoltageSwellsInPhaseL1() (string, bool) {
if do, ok := t.DataObjects["1-0:32.36.0"]; ok {
return do.Value, true
}
return "", false
}
// NumberOfVoltageSwellsInPhaseL2 returns the number of voltage swells in phase L2.
func (t Telegram) NumberOfVoltageSwellsInPhaseL2() (string, bool) {
if do, ok := t.DataObjects["1-0:52.36.0"]; ok {
return do.Value, true
}
return "", false
}
// NumberOfVoltageSwellsInPhaseL3 returns the number of voltage swells in phase L3.
func (t Telegram) NumberOfVoltageSwellsInPhaseL3() (string, bool) {
if do, ok := t.DataObjects["1-0:72.36.0"]; ok {
return do.Value, true
}
return "", false
}
// TextMessage returns text message of max 1024 characters.
func (t Telegram) TextMessage() (string, bool) {
if do, ok := t.DataObjects["0-0:96.13.0"]; ok {
return do.Value, true
}
return "", false
}
// InstantaneousVoltageL1 returns the instantaneous voltage L1 in V resolution.
func (t Telegram) InstantaneousVoltageL1() (string, bool) {
if do, ok := t.DataObjects["1-0:32.7.0"]; ok {
return do.Value, true
}
return "", false
}
// InstantaneousVoltageL2 returns the instantaneous voltage L3 in V resolution.
func (t Telegram) InstantaneousVoltageL2() (string, bool) {
if do, ok := t.DataObjects["1-0:52.7.0"]; ok {
return do.Value, true
}
return "", false
}
// InstantaneousVoltageL3 returns the instantaneous voltage L3 in V resolution.
func (t Telegram) InstantaneousVoltageL3() (string, bool) {
if do, ok := t.DataObjects["1-0:72.7.0"]; ok {
return do.Value, true
}
return "", false
}
// InstantaneousCurrentL1 returns the instantaneous current L1 in A resolution.
func (t Telegram) InstantaneousCurrentL1() (string, bool) {
if do, ok := t.DataObjects["1-0:31.7.0"]; ok {
return do.Value, true
}
return "", false
}
// InstantaneousCurrentL2 returns the instantaneous current L2 in A resolution.
func (t Telegram) InstantaneousCurrentL2() (string, bool) {
if do, ok := t.DataObjects["1-0:51.7.0"]; ok {
return do.Value, true
}
return "", false
}
// InstantaneousCurrentL3 returns the instantaneous current L3 in A resolution.
func (t Telegram) InstantaneousCurrentL3() (string, bool) {
if do, ok := t.DataObjects["1-0:71.7.0"]; ok {
return do.Value, true
}
return "", false
}
// MeterReadingGasDeliveredToClient returns the meter reading gas delivered to
// client in m3 with a mm3 resolution for the gas meter that is installed on
// the given channel.
func (t Telegram) MeterReadingGasDeliveredToClient(channel int) (string, bool) {
identifier := fmt.Sprintf("0-%d:24.2.1", channel)
if do, ok := t.DataObjects[identifier]; ok {
return do.Value, true
}
return "", false
}
// DataObject represent data object and it's reference to the OBIS as defined
// in EN-EN-IEC 62056-61:2002 Electricity metering – Data exchange for meter
// reading, tariff and load control – Part 61: OBIS Object Identification
// System.
type DataObject struct {
OBIS string // OBIS reduced ID-code
Timestamp string
Value string
Unit string
}
// ParseTelegram will parse the DSMR telegram.
func ParseTelegram(telegram string) (t Telegram, err error) {
t.DataObjects = make(map[string]DataObject)
for _, line := range strings.Split(telegram, "\n") {
line = strings.TrimSpace(line)
// Skip empty line or the footer of the telegram
if line == "" || line[0] == '!' {
continue
}
// The header of the telegram
if line[0] == '/' {
t.Header = line
continue
}
do, err := ParseDataObject(line)
if err != nil {
// TODO logging
continue
}
switch do.OBIS {
// Version information for P1 output.
case "1-3:0.2.8":
t.Version = do.Value
// Date time of P1 output.
case "0-0:1.0.0":
if len(do.Value) > 2 {
// Remove the DST indicator from the timestamp
rawDateTime := do.Value[:len(do.Value)-1]
location, err := time.LoadLocation("Europe/Amsterdam")
if err != nil {
// TODO logging
continue
}
dateTime, err := time.ParseInLocation(DateTimeFormat, rawDateTime, location)
if err != nil {
// TODO logging
continue
}
t.DateTime = dateTime
}
default:
t.DataObjects[do.OBIS] = do
}
}
return t, nil
}
// ParseDataObject will parse a single line into a DataObject.
func ParseDataObject(do string) (DataObject, error) {
// Extract the OBIS reduced ID-code and the corresponding value, e.g:
// 1-3:0.2.8(50) --> 1-3:0.2.8 and (50)
match := objectRegexp.FindStringSubmatch(strings.TrimSpace(do))
if match == nil || len(match) < 3 {
return DataObject{}, fmt.Errorf("no valid DSMR object found")
}
obis := match[1]
rawValue := match[2]
// Extract the value and the unit from the raw value, e.g:
// (000099.999*kWh) -> 000099.999 and kWh
match = valueRegexp.FindStringSubmatch(rawValue)
if match == nil {
return DataObject{
OBIS: obis,
Value: rawValue,
}, nil
}
return DataObject{
OBIS: obis,
Timestamp: match[2],
Value: match[3],
Unit: match[4],
}, nil
}