Files
MicroPythonOS/internal_filesystem/lib/localPTZtime.py
T
Thomas Farstrike 2dfeb381e4 Add timezone setting
2025-07-02 12:43:01 +02:00

320 lines
8.4 KiB
Python

#! /usr/bin/env python3
"""
Method to convert time - in seconds passed since Unix epoch - from GMT time zone to given time zone expressed in Posix Time Zone format.
:author: Roberto Bellingeri
:copyright: Copyright 2023 - NetGuru
:license: GPL
"""
"""
Changelog:
0.0.1
initial release
0.0.2
changed returned format
0.0.3
fixed bug in leap year calculation
"""
__version__ = "0.0.3"
import time
import re
def checkptz(ptz_string: str):
"""
Check if the format of the string complies with what is described here: https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html
Only for testing purposes: on MicroPython always return 'None'.
Parameters:
ptz_string (str): String in Posix Time Zone format
Returns:
bool: Test result
"""
ptz_string = _normalize(ptz_string)
result = None
if hasattr(re, 'fullmatch'): # re.fullmatch() is not defined in MicroPython
check_re = r"^"
check_re += r"[^:\d+-]{3,}" # std name
check_re += r"[+-]?\d{1,2}(?::\d{1,2}){0,2}" # std offset
check_re += r"(?:" # dst zone begin
check_re += r"[^:\d+-,]{3,}" # dst name
check_re += r"(?:" # dst offset begin
check_re += r"(?:[+-]?\d{1,2}(?::\d{1,2}){0,2})?" # dst offset time, can be omitted
check_re += r"(?:,(?:J\d{1,3}|\d{1,3}|M(?:[1-9]|1[0-2])\.[1-5]\.[0-6])(?:\/\d{1,2}(?::\d{1,2}){0,2})?){2}" #dst start/end date - time can be omitted
check_re += r")" # dst offset end
check_re += r")?" # dst zone finish, can be omitted
check_re += r"$"
#print(check_re)
if (re.fullmatch(check_re,ptz_string) == None): # type: ignore
result = False
else:
result = True
return result
def tztime(timestamp: float, ptz_string: str):
"""
Converts a time expressed in seconds in struct_time style 9-tuple according to the time zone provided in Posix Time Zone format
Parameters:
timestamp (float): Time in second
ptz_string (str): Time zone in Posix format
Returns:
struct_time style tuple:
* ``year``
* ``month``
* ``mday``
* ``hour``
* ``minute``
* ``second``
* ``weekday``
* ``yearday``
* ``isdst``
"""
return _timecalc(timestamp, ptz_string)[:9]
def tziso(timestamp: float, ptz_string: str, zone_designator = True):
"""
Return an ISO 8601 date and time string according to the time zone provided in Posix Time Zone format
Parameters:
timestamp (float): Time in second
ptz_string (str): Time Zone in Posix format
zone_designator (bool): Insert zone designator in returned string - default: True
Returns:
string: ISO 8601 date and time string
"""
tx = _timecalc(timestamp, ptz_string)
stx = f"{tx[0]}-{tx[1]:02d}-{tx[2]:02d}T{tx[3]:02d}:{tx[4]:02d}:{tx[5]:02d}"
if (zone_designator == True):
if (tx[9] != 0):
stx += f"{(tx[9] // 3600):+03d}"
mins = abs(tx[9]) % 3600
if (mins != 0):
stx += ":" + f"{(mins // 60):02d}"
else:
stx += "Z"
return stx
def _timecalc(timestamp: float, ptz_string: str):
"""
Converts a time expressed in seconds in 10-tuple according to the time zone provided in Posix Time Zone format
Parameters:
timestamp (float): Time in second
ptz_string (str): Time zone in Posix format
zone_designator (bool): Insert zone designator in returned string - default: True
Returns:
tuple:
* ``year``
* ``month``
* ``mday``
* ``hour``
* ``minute``
* ``second``
* ``weekday``
* ``yearday``
* ``isdst``
* ``utcoffset``
"""
ptz_string = _normalize(ptz_string)
std_offset_seconds = 0
dst_offset_seconds = 0
tot_offset_seconds = 0
is_dst = False
ptz_parts = ptz_string.split(",")
#offsetHours = re.split(r"[^\d\+\-\:]+", ptz_parts[0])
offsetHours = re.compile(r"[^\d\+\-\:]+").split(ptz_parts[0]) # re.compile() is used for MicroPython compatibility
offsetHours = list(filter(None, offsetHours))
#print(offsetHours)
if (len(offsetHours) > 0):
std_offset_seconds = - _hours2secs(offsetHours[0])
if (len(offsetHours)>1):
dst_offset_seconds = _hours2secs(offsetHours[1])
else:
dst_offset_seconds = 3600
#print("timestamp:\t" + str(int(timestamp)))
if (len(ptz_parts)==3):
year = time.gmtime(int(timestamp))[0]
dst_start = _parseposixtransition(ptz_parts[1], year)
dst_end = _parseposixtransition(ptz_parts[2], year)
if (dst_start < dst_end): #northern hemisphere
if ((timestamp + std_offset_seconds) < dst_start):
is_dst = False
tot_offset_seconds = std_offset_seconds
elif ((timestamp + std_offset_seconds + dst_offset_seconds) < dst_end):
is_dst = True
tot_offset_seconds = std_offset_seconds + dst_offset_seconds
else:
is_dst = False
tot_offset_seconds = std_offset_seconds
else: # southern hemisphere
if ((timestamp + std_offset_seconds + dst_offset_seconds) < dst_end):
is_dst = True
tot_offset_seconds = std_offset_seconds + dst_offset_seconds
elif ((timestamp + std_offset_seconds) < dst_start):
is_dst = False
tot_offset_seconds = std_offset_seconds
else:
is_dst = True
tot_offset_seconds = std_offset_seconds + dst_offset_seconds
#print("dstOffset:\t" + str(dst_offset_seconds))
#print("dstStart:\t" + str(dst_start) + "\t" + str(time.gmtime(dst_start)))
#print("dstEnd: \t" + str(dst_end) + "\t" + str(time.gmtime(dst_end)))
else:
tot_offset_seconds = std_offset_seconds
timemod = timestamp + tot_offset_seconds
t = time.gmtime(int(timemod))
tx = (t[0], t[1], t[2], t[3], t[4], t[5], t[6], t[7], int(is_dst), tot_offset_seconds)
return tx
def _normalize(ptz_string: str):
"""
Return simple normalization of PTZ string
Parameters:
ptz_string (str): PTZ string
Returns:
str: Normalized PTZ string
"""
ptz_string = ptz_string.upper()
ptz_string = re.compile(r"\<[^\>]*\>").sub("DUMMY",ptz_string) # For what appear to be non-standard strings like "<+11>-11<+12>,M10.1.0,M4.1.0/3"
return ptz_string
def _parseposixtransition(transition: str, year: int):
"""
Returns the moment of the transition from std to dst and vice-versa
Parameters:
transition (str): Part of Posix Time Zone string related to the transition
year (int): The year
Returns:
float: Time adjusted
"""
parts = transition.split('/')
seconds = 0
tr = 0
if (len(parts) == 2):
seconds = _hours2secs(parts[1])
else:
seconds = 2 * 3600
if (transition[0] == "M"):
# 'Mm.n.d' format.
date_parts = parts[0][1:].split('.')
if (len(date_parts)==3):
month = int(date_parts[0]) # month from '1' to '12'
week_of_month = int(date_parts[1]) # week number from '1' to '5'. '5' always the last.
day_of_week = int(date_parts[2]) # day of week - 0:Sunday 1:Monday 2:Tuesday 3:Wednesday 4:Thursday 5:Friday 6:Saturday
base_year = 1970
base_year_1st_day = 4 # the first day of the year 1970 was Thursday
month_days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
if ((((year % 4) == 0) and ((year % 100) != 0)) or (year % 400) == 0):
month_days[1] = 29
# calculate the number of days since 1/1/base_year
days_since_base_date = (year - base_year) * 365
for y in range(base_year, year):
if ((((y % 4) == 0) and ((y % 100) != 0)) or (y % 400) == 0):
days_since_base_date += 1
days_since_base_date += sum(month_days[:month - 1])
# calculate the day of the week for the first day of month
first_day_of_month = (days_since_base_date + base_year_1st_day) % 7
# calculate the day of the month
day_of_month = 1 + (week_of_month - 1) * 7 + (day_of_week - first_day_of_month) % 7
if day_of_month > month_days[month - 1]:
day_of_month -= 7
tr = time.mktime((year, month, day_of_month, 0, 0, 0, 0, 0, 0))
elif (transition[0] == "J"):
# 'Jn' format. Counting from 1 to 365, and February 29 is never counted.
day_num = int(parts[0][1:])
if (((((year % 4) == 0) and ((year % 100) != 0)) or (year % 400) == 0) and (day_num > (31 + 28))): # after February 28 in leap years
day_num += 1
tr = time.mktime((year,1,1,0,0,0,0,0,0)) + ((day_num - 1) * 86400)
else:
# 'n' format. Counting from zero to 364, or to 365 in leap years.
day_num = int(parts[0])
tr = time.mktime((year,1,1,0,0,0,0,0,0)) + (day_num * 86400)
return tr + seconds
def _hours2secs(hours: str):
"""
Convert hours string in seconds
Parameters:
hours (str): Hours in format 00[:00][:00]
Returns:
int: seconds
"""
seconds = 0
hours_parts = hours.split(':')
if (len(hours_parts)>0):
seconds = int(hours_parts[0]) * 3600
if (len(hours_parts)>1):
seconds += int(hours_parts[1]) * 60
if (len(hours_parts)>2):
seconds += int(hours_parts[2])
return seconds