forked from penguintutor/picow-railway-departure
-
Notifications
You must be signed in to change notification settings - Fork 0
/
datetime_utils.py
executable file
·259 lines (222 loc) · 7.77 KB
/
datetime_utils.py
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
"""
Author: Adam Knowles
Version: 0.1
Name: datetime_utils.py
Description: Utils that operate on datetime
GitHub Repository: https://github.com/Pharkie/AdamGalactic/
License: GNU General Public License (GPL)
"""
import random
import utime
import ntptime
import machine
import utils
from utils_logger import log_message
from config import OFFLINE_MODE
def format_date(dt):
"""
Formats a date as 'DD MMM YYYY'.
Parameters:
dt (tuple): A tuple containing three integers representing the year, month,
and day, respectively.
Returns:
str: The formatted date string.
"""
# Format the date as 'DD MMM YYYY'.
months = [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
]
day = f"{dt[2]:02d}"
month = months[dt[1] - 1]
year = f"{dt[0]:04d}"
return f"{day} {month} {year}"
def last_sunday(year, month):
"""
Calculates the date of the last Sunday of the specified month and year.
Parameters:
year (int): The year for which to find the last Sunday.
month (int): The month for which to find the last Sunday.
Returns:
int: The Unix timestamp of the last Sunday of the specified month and year.
"""
# Calculate the date of the last Sunday of the specified month and year.
# Find the date of the last Sunday in a given month
last_day = (
utime.mktime((year, month + 1, 1, 0, 0, 0, 0, 0)) # type: ignore
- 86400 # pylint: disable=no-value-for-parameter
) # Set to the last day of the previous month
weekday = utime.localtime(last_day)[6] # Get the weekday for the last day
while weekday != 6: # Sunday has index 6
last_day -= 86400 # Subtract a day in seconds
weekday = (weekday - 1) % 7
return int(last_day)
def is_dst(timestamp):
"""
Checks if the given timestamp is in Daylight Saving Time (DST), considering the 1 am transition.
Parameters:
timestamp (int): The Unix timestamp to check.
Returns:
bool: True if the timestamp is in DST, False otherwise.
"""
# Check if the given timestamp is in DST (BST) considering the 1 am transition
time_tuple = utime.localtime(timestamp)
dst_start = last_sunday(time_tuple[0], 3) # Last Sunday of March
dst_end = last_sunday(time_tuple[0], 10) # Last Sunday of October
# Check if the current time is within DST dates
if dst_start <= timestamp < dst_end:
# Check if it's after 1 am on the last Sunday of March and before 2 am on the
# last Sunday of October
if (
time_tuple[1] == 3
and time_tuple[2] == (dst_start // 86400) + 1
and time_tuple[3] < 1
) or (
time_tuple[1] == 10
and time_tuple[2] == (dst_end // 86400)
and time_tuple[3] < 2
):
return False # Not in DST during the 1 am transition
else:
return True # In DST during other times
else:
return False
def get_time_values(current_time_tuple=None):
"""
Splits a time into individual digits, defaulting to the current, real time if no time is
provided.
Parameters:
current_time_tuple (tuple, optional): A tuple representing the time to split. If None, the
current time is used.
Returns:
tuple: A tuple containing the tens and ones places of the hours, minutes, and seconds, as
well as the day of the month, month name, and year.
"""
# Split a time into individual digits, defaulting to current, real time.
if current_time_tuple is None:
current_time_tuple = utime.localtime()
# Extract digits
month_names = [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
]
day_of_month = current_time_tuple[2]
month_name = month_names[current_time_tuple[1] - 1]
year = current_time_tuple[0]
hours_tens, hours_ones = divmod(current_time_tuple[3], 10)
minutes_tens, minutes_ones = divmod(current_time_tuple[4], 10)
seconds_tens, seconds_ones = divmod(current_time_tuple[5], 10)
# Return the extracted digits
return (
hours_tens,
hours_ones,
minutes_tens,
minutes_ones,
seconds_tens,
seconds_ones,
day_of_month,
month_name,
year,
)
def sync_ntp():
"""
Syncs the Real Time Clock (RTC) with NTP, or sets a random time if in offline mode.
Raises:
OSError: If not in offline mode and WiFi is not connected.
Side Effects:
Sets the RTC to the current NTP time, or a random time if in offline mode.
"""
if OFFLINE_MODE:
# Generate random values for hours, minutes, and seconds
hours = random.randint(0, 23)
minutes = random.randint(0, 59)
seconds = random.randint(0, 59)
log_message(
f"Offline mode: setting a random time {hours:02d}:{minutes:02d}:{seconds:02d}"
)
# Set the RTC to the random time
machine.RTC().datetime((2023, 1, 1, 0, hours, minutes, seconds, 0))
elif not utils.is_wifi_connected():
raise OSError("Wifi not connected")
try:
ntptime.settime()
log_message("RTC set from NTP", level="INFO")
except (OSError, ValueError) as error:
log_message(f"Failed to set RTC from NTP: {error}")
async def check_dst():
"""
Checks if Daylight Saving Time (DST) is in effect and updates the RTC if necessary.
Raises:
Exception: If there's an error while checking DST or updating the RTC.
Side Effects:
Updates the RTC time if it differs from the current time by more than a minute.
"""
try:
# Get the current time from utime
current_timestamp = utime.time()
# Get the current time from the RTC
rtc_timestamp = machine.RTC().datetime()
# Rearrange the rtc_timestamp to match the format expected by utime.mktime()
rtc_timestamp_rearranged = (
rtc_timestamp[0],
rtc_timestamp[1],
rtc_timestamp[2],
rtc_timestamp[4],
rtc_timestamp[5],
rtc_timestamp[6],
rtc_timestamp[3],
0,
)
# Convert the RTC timestamp to seconds since the Epoch
rtc_timestamp_seconds = utime.mktime(rtc_timestamp_rearranged) # type: ignore
# Check if DST is in effect
is_dst_flag = is_dst(current_timestamp)
# If DST is in effect, add an hour to the current timestamp
if is_dst_flag:
current_timestamp += 3600
# If the current timestamp and the RTC timestamp differ by more than a minute,
# update the RTC
if abs(current_timestamp - rtc_timestamp_seconds) > 60:
# rtc.datetime() param is a different format of tuple to utime.localtime()
# so below converts it
machine.RTC().datetime((
utime.localtime(current_timestamp)[0],
utime.localtime(current_timestamp)[1],
utime.localtime(current_timestamp)[2],
utime.localtime(current_timestamp)[6],
utime.localtime(current_timestamp)[3],
utime.localtime(current_timestamp)[4],
utime.localtime(current_timestamp)[5],
0,
))
log_message(
f"RTC time updated for DST: {is_dst_flag}",
level="INFO",
)
else:
log_message(
f"RTC time not updated for DST, no change from: {is_dst_flag}",
level="INFO",
)
except Exception as error: # pylint: disable=broad-except
log_message(f"check_dst() error, will try to ignore: {error}")