wwvb 4.1.0a0__py3-none-any.whl → 5.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
uwwvb.py ADDED
@@ -0,0 +1,193 @@
1
+ # SPDX-FileCopyrightText: 2021-2024 Jeff Epler
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-only
4
+
5
+ # ruff: noqa: C405 PYI024 PLR2004 FBT001 FBT002
6
+
7
+ """Implementation of a WWVB state machine & decoder for resource-constrained systems"""
8
+
9
+ from __future__ import annotations
10
+
11
+ from collections import namedtuple
12
+
13
+ import adafruit_datetime as datetime
14
+
15
+ ZERO, ONE, MARK = range(3)
16
+
17
+ always_mark = set((0, 9, 19, 29, 39, 49, 59))
18
+ always_zero = set((4, 10, 11, 14, 20, 21, 34, 35, 44, 54))
19
+ bcd_weights = (1, 2, 4, 8, 10, 20, 40, 80, 100, 200, 400, 800)
20
+
21
+ WWVBMinute = namedtuple("WWVBMinute", ["year", "days", "hour", "minute", "dst", "ut1", "ls", "ly"])
22
+
23
+
24
+ class WWVBDecoder:
25
+ """A state machine for receiving WWVB timecodes."""
26
+
27
+ def __init__(self) -> None:
28
+ """Construct a WWVBDecoder"""
29
+ self.minute: list[int] = []
30
+ self.state = 1
31
+
32
+ def update(self, value: int) -> list[int] | None:
33
+ """Update the _state machine when a new symbol is received.
34
+
35
+ If a possible complete _minute is received, return it; otherwise, return None
36
+ """
37
+ result = None
38
+ if self.state == 1:
39
+ self.minute = []
40
+ if value == MARK:
41
+ self.state = 2
42
+
43
+ elif self.state == 2:
44
+ if value == MARK:
45
+ self.state = 3
46
+ else:
47
+ self.state = 1
48
+
49
+ elif self.state == 3:
50
+ if value != MARK:
51
+ self.minute = [MARK, value]
52
+ self.state = 4
53
+
54
+ else: # self.state == 4:
55
+ idx = len(self.minute)
56
+ self.minute.append(value)
57
+ if (idx in always_mark) != (value == MARK):
58
+ self.state = 3 if self.minute[-2] == MARK else 2
59
+ elif idx in always_zero and value != ZERO:
60
+ self.state = 1
61
+
62
+ elif idx == 59:
63
+ result = self.minute
64
+ self.minute = []
65
+ self.state = 2
66
+
67
+ return result
68
+
69
+ def __str__(self) -> str:
70
+ """Return a string representation of self"""
71
+ return f"<WWVBDecoder {self.state} {self.minute}>"
72
+
73
+
74
+ def get_am_bcd(seq: list[int], *poslist: int) -> int | None:
75
+ """Convert the bits seq[positions[0]], ... seq[positions[len(positions-1)]] [in MSB order] from BCD to decimal"""
76
+ pos = list(poslist)[::-1]
77
+ val = [int(seq[p]) for p in pos]
78
+ while len(val) % 4 != 0:
79
+ val.append(0)
80
+ result = 0
81
+ base = 1
82
+ for i in range(0, len(val), 4):
83
+ digit = 0
84
+ for j in range(4):
85
+ digit += 1 << j if val[i + j] else 0
86
+ if digit > 9:
87
+ return None
88
+ result += digit * base
89
+ base *= 10
90
+ return result
91
+
92
+
93
+ def decode_wwvb(
94
+ t: list[int] | None,
95
+ ) -> WWVBMinute | None:
96
+ """Convert a received minute of wwvb symbols to a WWVBMinute. Returns None if any error is detected."""
97
+ if not t:
98
+ return None
99
+ if not all(t[i] == MARK for i in always_mark):
100
+ return None
101
+ if not all(t[i] == ZERO for i in always_zero):
102
+ return None
103
+ # Checking redundant DUT1 sign bits
104
+ if t[36] == t[37]:
105
+ return None
106
+ if t[36] != t[38]:
107
+ return None
108
+ minute = get_am_bcd(t, 1, 2, 3, 5, 6, 7, 8)
109
+ if minute is None:
110
+ return None
111
+
112
+ hour = get_am_bcd(t, 12, 13, 15, 16, 17, 18)
113
+ if hour is None:
114
+ return None
115
+
116
+ days = get_am_bcd(t, 22, 23, 25, 26, 27, 28, 30, 31, 32, 33)
117
+ if days is None:
118
+ return None
119
+
120
+ abs_ut1 = get_am_bcd(t, 40, 41, 42, 43)
121
+ if abs_ut1 is None:
122
+ return None
123
+
124
+ abs_ut1 *= 100
125
+ ut1_sign = t[38]
126
+ ut1 = abs_ut1 if ut1_sign else -abs_ut1
127
+ year = get_am_bcd(t, 45, 46, 47, 48, 50, 51, 52, 53)
128
+ if year is None:
129
+ return None
130
+
131
+ is_ly = t[55]
132
+ if days > 366 or (not is_ly and days > 365):
133
+ return None
134
+ ls = t[56]
135
+ dst = get_am_bcd(t, 57, 58)
136
+ assert dst is not None # No possibility of BCD decode error in 2 bits
137
+
138
+ return WWVBMinute(year, days, hour, minute, dst, ut1, ls, is_ly)
139
+
140
+
141
+ def as_datetime_utc(decoded_timestamp: WWVBMinute) -> datetime.datetime:
142
+ """Convert a WWVBMinute to a UTC datetime"""
143
+ d = datetime.datetime(decoded_timestamp.year + 2000, 1, 1)
144
+ d += datetime.timedelta(
145
+ decoded_timestamp.days - 1,
146
+ decoded_timestamp.hour * 3600 + decoded_timestamp.minute * 60,
147
+ )
148
+ return d
149
+
150
+
151
+ def is_dst(
152
+ dt: datetime.datetime,
153
+ dst_bits: int,
154
+ standard_time_offset: int = 7 * 3600,
155
+ dst_observed: bool = True,
156
+ ) -> bool:
157
+ """Return True iff DST is observed at the given moment"""
158
+ d = dt - datetime.timedelta(seconds=standard_time_offset)
159
+ if not dst_observed:
160
+ return False
161
+ if dst_bits == 0b10:
162
+ transition_time = dt.replace(hour=2)
163
+ return d >= transition_time
164
+ if dst_bits == 0b11:
165
+ return True
166
+ if dst_bits == 0b01:
167
+ # DST ends at 2AM *DST* which is 1AM *standard*
168
+ transition_time = dt.replace(hour=1)
169
+ return d < transition_time
170
+ return False
171
+
172
+
173
+ def apply_dst(
174
+ dt: datetime.datetime,
175
+ dst_bits: int,
176
+ standard_time_offset: int = 7 * 3600,
177
+ dst_observed: bool = True,
178
+ ) -> datetime.datetime:
179
+ """Apply time zone and DST (if applicable) to the given moment"""
180
+ d = dt - datetime.timedelta(seconds=standard_time_offset)
181
+ if is_dst(dt, dst_bits, standard_time_offset, dst_observed):
182
+ d += datetime.timedelta(seconds=3600)
183
+ return d
184
+
185
+
186
+ def as_datetime_local(
187
+ decoded_timestamp: WWVBMinute,
188
+ standard_time_offset: int = 7 * 3600,
189
+ dst_observed: bool = True,
190
+ ) -> datetime.datetime:
191
+ """Convert a WWVBMinute to a local datetime with tzinfo=None"""
192
+ dt = as_datetime_utc(decoded_timestamp)
193
+ return apply_dst(dt, decoded_timestamp.dst, standard_time_offset, dst_observed)