forked from jonaz/gombus
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdatetime.go
More file actions
214 lines (197 loc) · 7.64 KB
/
Copy pathdatetime.go
File metadata and controls
214 lines (197 loc) · 7.64 KB
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
package gombus
import (
"errors"
"fmt"
"time"
)
// ErrInvalidDateTime means the meter sent a date it cannot have meant: an unset
// field, an out-of-range component, or its own invalid bit.
//
// ErrUnsupportedDateTime means the date is legal but in an encoding this package
// does not decode. The two are separate so a caller can tell a broken meter from
// a gap in gombus, which are different problems with different fixes.
//
// Both reach the caller wrapped in DecodedDataRecord.ValueErr, never as a frame
// error: an unusable date costs us no sync, so it must not reject the frame.
// Match them with errors.Is.
var (
ErrInvalidDateTime = errors.New("invalid date/time field")
ErrUnsupportedDateTime = errors.New("unsupported date/time encoding")
)
// dateEpochYear is the base of the 7-bit year field. EN 13757-3 counts from
// 2000, matching libmbus (tm_year = 100 + raw, and tm_year is years since
// 1900). There is no 1900/2000 windowing rule. The year field is 7 bits, so it
// runs 2000..2127 and its most significant bit only sets from 2064 on.
const dateEpochYear = 2000
// Widths of the encodings, in bytes. VIF 0x6C is always type G. VIF 0x6D names
// the date-and-time family, and the DIF width picks the member: type J at 3
// bytes (time only), type F at 4, type I at 6.
const (
dateTypeGWidth = 2
dateTypeJWidth = 3
dateTypeFWidth = 4
dateTypeIWidth = 6
)
// TimeOfDay is an EN 13757-3 Type-J CP24 local clock reading. It has no date
// and no zone; both are absent from the meter telegram.
type TimeOfDay struct{ Hour, Minute, Second int }
func (t TimeOfDay) String() string { return fmt.Sprintf("%02d:%02d:%02d", t.Hour, t.Minute, t.Second) }
func decodeTypeJ(b []byte) (TimeOfDay, error) {
if len(b) != dateTypeJWidth {
return TimeOfDay{}, fmt.Errorf("%w: type J needs 3 bytes", ErrInvalidDateTime)
}
if b[0] == 0xFF && b[1] == 0xFF && b[2] == 0xFF {
return TimeOfDay{}, fmt.Errorf("%w: type J invalid value", ErrInvalidDateTime)
}
second, minute, hour := int(b[0]&0x3F), int(b[1]&0x3F), int(b[2]&0x1F)
if b[0]&0xC0 != 0 || b[1]&0xC0 != 0 || b[2]&0xE0 != 0 || second > 59 || minute > 59 || hour > 23 {
return TimeOfDay{}, fmt.Errorf("%w: invalid type J time", ErrInvalidDateTime)
}
return TimeOfDay{Hour: hour, Minute: minute, Second: second}, nil
}
// decodeTypeG decodes an EN 13757-3 type G date: two bytes holding day, month
// and year, with no time of day. The year is split across both bytes.
func decodeTypeG(b []byte) (time.Time, error) {
day := int(b[0] & 0x1F)
month := int(b[1] & 0x0F)
year := int((b[0]&0xE0)>>5 | (b[1]&0xF0)>>1)
return assembleDateTime(year, month, day, 0, 0, 0)
}
// decodeTypeF decodes an EN 13757-3 type F date and time: four bytes holding
// minute through year, plus the invalid (IV) and summer-time (SU) flags. It
// reports the summer-time flag even when the timestamp is unusable, because the
// meter sent it either way.
func decodeTypeF(b []byte) (time.Time, bool, error) {
summerTime := b[1]&0x80 != 0
if b[0]&0x80 != 0 {
// IV: the meter is declaring its own timestamp invalid. Believe it
// rather than decode the bits underneath.
return time.Time{}, summerTime, fmt.Errorf(
"%w: meter set the invalid bit", ErrInvalidDateTime,
)
}
minute := int(b[0] & 0x3F)
hour := int(b[1] & 0x1F)
day := int(b[2] & 0x1F)
month := int(b[3] & 0x0F)
year := int((b[2]&0xE0)>>5 | (b[3]&0xF0)>>1)
t, err := assembleDateTime(year, month, day, hour, minute, 0)
return t, summerTime, err
}
// decodeTypeI decodes an EN 13757-3 type I date and time (compound CP48): six
// bytes holding second through year. It is the same family as type F, selected
// by a 6-byte data field rather than a 4-byte one, and adds seconds.
//
// b[5] carries the day of the week and week number, which are not part of the
// timestamp and are left in RawValue for a caller that wants them.
//
// Verified against libmbus's published decoding of LGB_G350 record 1
// (2016-07-22T08:00:00), not just read off the spec.
func decodeTypeI(b []byte) (time.Time, bool, error) {
summerTime := b[1]&0x40 != 0
second := int(b[0] & 0x3F)
minute := int(b[1] & 0x3F)
hour := int(b[2] & 0x1F)
day := int(b[3] & 0x1F)
month := int(b[4] & 0x0F)
year := int((b[3]&0xE0)>>5 | (b[4]&0xF0)>>1)
t, err := assembleDateTime(year, month, day, hour, minute, second)
return t, summerTime, err
}
// assembleDateTime validates the fields before building a time.Time, because
// time.Date normalises out-of-range input instead of rejecting it: month 0
// becomes the previous December and day 0 the last day of the prior month. An
// unset meter date would silently become a real-looking one, so every field is
// checked first and the result is verified not to have been normalised.
//
// year is the raw 7-bit field, not the calendar year.
func assembleDateTime(year, month, day, hour, minute, second int) (time.Time, error) {
switch {
case month < 1 || month > 12:
return time.Time{}, fmt.Errorf("%w: month %d out of range", ErrInvalidDateTime, month)
case day < 1:
return time.Time{}, fmt.Errorf("%w: day %d out of range", ErrInvalidDateTime, day)
case hour > 23:
return time.Time{}, fmt.Errorf("%w: hour %d out of range", ErrInvalidDateTime, hour)
case minute > 59:
return time.Time{}, fmt.Errorf("%w: minute %d out of range", ErrInvalidDateTime, minute)
case second > 59:
return time.Time{}, fmt.Errorf("%w: second %d out of range", ErrInvalidDateTime, second)
}
t := time.Date(dateEpochYear+year, time.Month(month), day, hour, minute, second, 0, time.UTC)
// The day field holds up to 31, so it can still name a day the month does
// not have. time.Date would roll 31 February forward into March; compare
// the result against the input to catch that.
if t.Day() != day || int(t.Month()) != month {
return time.Time{}, fmt.Errorf(
"%w: %04d-%02d-%02d is not a real date",
ErrInvalidDateTime, dateEpochYear+year, month, day,
)
}
return t, nil
}
// decodeRecordTime fills in Time and SummerTimeFlag for the date and date/time
// VIFs. It is additive: the raw bitfield stays in RawValue, so nothing is lost
// and a caller that wants the bits still has them.
//
// An unusable date sets ValueErr and leaves Time zero rather than reporting a
// date the meter never sent.
func decodeRecordTime(b []byte, record *DecodedDataRecord) {
switch record.Unit.Date {
case DateTypeG:
// 0x6C has one width, so anything else is not a type G field.
if len(b) != dateTypeGWidth {
record.ValueErr = fmt.Errorf(
"%w: %d-byte field with a type G (0x6C) VIF", ErrUnsupportedDateTime, len(b),
)
return
}
t, err := decodeTypeG(b)
if err != nil {
record.ValueErr = err
return
}
record.Time = t
case DateTypeF:
decodeRecordDateTime(b, record)
case DateTypeI, DateNone:
}
}
// decodeRecordDateTime handles the VIF 0x6D family, where the DIF width selects
// which member of it the record carries. Reading a 6-byte type I as the type F
// we expected would silently produce a wrong timestamp, so the width chooses the
// decoder rather than being assumed.
func decodeRecordDateTime(b []byte, record *DecodedDataRecord) {
var (
t time.Time
summerTime bool
err error
)
switch len(b) {
case dateTypeFWidth:
t, summerTime, err = decodeTypeF(b)
case dateTypeIWidth:
t, summerTime, err = decodeTypeI(b)
case dateTypeJWidth:
tod, err := decodeTypeJ(b)
if err != nil {
record.ValueErr = err
return
}
record.TimeOfDay = &tod
return
default:
record.ValueErr = fmt.Errorf(
"%w: %d-byte field with a date and time (0x6D) VIF",
ErrUnsupportedDateTime, len(b),
)
return
}
// The meter sent the flag whether or not the timestamp is usable.
record.SummerTimeFlag = summerTime
if err != nil {
record.ValueErr = err
return
}
record.Time = t
}