Skip to content

Instantly share code, notes, and snippets.

@Beormund
Last active November 12, 2023 09:16
Show Gist options
  • Save Beormund/f0f39c72e066da497f6308d1964c9627 to your computer and use it in GitHub Desktop.
Save Beormund/f0f39c72e066da497f6308d1964c9627 to your computer and use it in GitHub Desktop.
Micropython module for converting UTC to local time using daylight saving policies.
import utime
# hemisphere [0 = Northern, 1 = Southern]
# week [0 = last week of month, 1..4 = first..fourth]
# month [1 = January; 12 = December]
# weekday [0 = Monday; 6 = Sunday] (day of week)
# hour (hour at which dst/std changes)
# timezone [-780..780] (offset from UTC in MINUTES - 780min / 60min=13hrs)
class Policy:
def __init__(self, hemisphere, week, month, weekday, hour, timezone):
if hemisphere not in [0,1]: raise ValueError('hemisphere must be 0..1')
if week not in range (0,5): raise ValueError('week must be 0 or 1..4')
if month not in range (1,13): raise ValueError('month must be 1..12')
if weekday not in range(0,7): raise ValueError('weekday must be 0..6')
if hour not in range(0,24): raise ValueError('hour must be 0..23')
if timezone not in range(-780,781): raise ValueError('timezone must be -780..780')
self.hemisphere = hemisphere
self.week = week
self.month = month
self.weekday = weekday
self.hour = hour
self.timezone = timezone
def __str__(self):
self.days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sum']
self.abbr = ['last', 'first', 'second', 'third', 'fourth']
self.months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
return ('Daylight Saving Policy: daylight saving {} on the {} {} of {} at {:02}:00 hrs (UTC{})').format(
self.prefix,
self.abbr[self.week],
self.days[self.weekday],
self.months[self.month-1],
self.hour,
'' if not self.timezone else '{:+1}'.format(self.timezone/60)
)
class StandardTimePolicy(Policy):
def __init__(self, hemisphere, week, month, weekday, hour, timezone):
self.prefix = 'ends'
super(StandardTimePolicy, self).__init__(hemisphere, week, month, weekday, hour, timezone)
class DaylightSavingPolicy(Policy):
def __init__(self, hemisphere, week, month, weekday, hour, timezone):
self.prefix = 'starts'
super(DaylightSavingPolicy, self).__init__(hemisphere, week, month, weekday, hour, timezone)
class DaylightSaving:
def __init__(self, dstp, stdp):
self.dstp = dstp
self.stdp = stdp
print(self.dstp)
print(self.stdp)
def isleapyear(self, year):
return ( year % 4 == 0 and year % 100 != 0) or year % 400 == 0
def dayofmonth(self, week, month, weekday, day, year):
# Get the first or last day of the month
t = utime.mktime((year, month, day, 0, 0, 0, 0, 0))
# Get the weekday of the first or last day of the month
d = utime.localtime(t)[6]
increment = lambda d: d + 1 if d < 6 else 0
decrement = lambda d: d - 1 if d > 0 else 6
while d != weekday:
# Increment if start of month else decrement
day = day + 1 if week else day - 1
d = increment(d) if week else decrement(d)
# Increment day of month by number of weeks
return day + (week - 1) * 7 if week else day
def nthweekday(self, week, month, weekday, hour, year):
monthlength = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
if self.isleapyear(year):
monthlength[1] = 29
day = 1 if week else monthlength[month-1]
day = self.dayofmonth(week, month, weekday, day, year)
return utime.mktime((year, month, day, hour, 0, 0, 0, 0))
def gettfromp(self, p, year):
return self.nthweekday(p.week, p.month, p.weekday, p.hour, year)
def localtime(self, utc):
print(f'UTC: {self.ftime(utc)}')
year = utime.gmtime(utc)[0]
dst = self.gettfromp(self.dstp, year)
print(f'Daylight Saving Starts: {self.ftime(dst)}')
std = self.gettfromp(self.stdp, year)
print(f'Daylight Saving Ends: {self.ftime(std)}')
saving = False
if self.stdp.hemisphere:
# Sourthern hemisphere
saving = utc > dst or utc < std
else:
# Northern hemisphere
saving = utc > dst and utc < std
offset = self.dstp.timezone if saving else self.stdp.timezone
local = utc + (offset * 60)
print(f'Local: {self.ftime(local)}')
return utc + (offset * 60)
def ftime(self, t):
year, month, day, hour, minute, second, ms, dayinyear = utime.localtime(t)
return "{:4}-{:02}-{:02}T{:02}:{:02}:{:02}".format(year, month, day, hour, minute, second)
@Beormund
Copy link
Author

Beormund commented Jul 27, 2022

GB Example

import daylightsaving
import utime

# Daylight Saving starts on last Sunday of March at 1AM
# 0 = Northern hemisphere
# 0 = Last week of the month
# 3 = March
# 6 = Sunday
# 1 = 1AM
# Time offset = 60 mins (GMT+1)
dst = DaylightSavingPolicy(0, 0, 3, 6, 1, 60)

# Daylight Saving ends on last Snday of October at 2AM
# 0 = Northern hemisphere
# 0 = Last week of the month
# 10 = October
# 6 = Sunday
# 2 = 2AM
# Time offset = 0 mins (GMT/UTC)
std = StandardTimePolicy(0, 0, 10, 6, 2, 0)

# Create a DaylightSaving object passing in policies
ds = DaylightSaving(dst, std)

# Obtain UTC time in seconds since epoch (from NTP Server or RTC etc)
utc = utime.mktime((2022, 7, 27, 13, 30, 0, 0, 0))

# Calculate local time passing in UTC time
local = ds.localtime(utc)

Output:

Daylight Saving Policy: daylight saving starts on the last Sum of Mar at 01:00 hrs (UTC+1)
Daylight Saving Policy: daylight saving ends on the last Sum of Oct at 02:00 hrs (UTC)
UTC: 2022-07-27T22:00:16
Daylight Saving Starts: 2022-03-27T01:00:00
Daylight Saving Ends: 2022-10-30T02:00:00
Local: 2022-07-27T23:00:16

@Beormund
Copy link
Author

Beormund commented Jul 27, 2022

This can then be used in ntptime.py like so:

import socket
import struct
from utime import gmtime
import daylightsaving as dls

# (date(2000, 1, 1) - date(1900, 1, 1)).days * 24*60*60
# (date(1970, 1, 1) - date(1900, 1, 1)).days * 24*60*60
NTP_DELTA = 3155673600 if gmtime(0)[0] == 2000 else 2208988800

DS = dls.DaylightSaving(
        dls.DaylightSavingPolicy(0, 0, 3, 6, 1, 60),
        dls.StandardTimePolicy(0, 0, 10, 6, 2, 0)
    )

# The NTP host can be configured at runtime by doing: ntptime.host = 'myhost.org'
host = "pool.ntp.org"

def time(hrs_offset=0):  # Local time offset in hrs relative to UTC
    NTP_QUERY = bytearray(48)
    NTP_QUERY[0] = 0x1B
    addr = socket.getaddrinfo(host, 123)[0][-1]
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    try:
        s.settimeout(1)
        res = s.sendto(NTP_QUERY, addr)
        msg = s.recv(48)
    finally:
        s.close()
    val = struct.unpack("!I", msg[40:44])[0]
    return val - NTP_DELTA + hrs_offset * 3600


# There's currently no timezone support in MicroPython, and the RTC is normally set in UTC time.
# Use daylightsaving module to calculate local time.
def settime():
    import machine    
    t = time()
    tm = gmtime(DS.localtime(t))
    machine.RTC().datetime((tm[0], tm[1], tm[2], tm[6] + 1, tm[3], tm[4], tm[5], 0))

@davefes
Copy link

davefes commented Apr 15, 2023

Thank you for providing this code. Is not hrs_offset redundant now?

Would suggest submitting this to https://awesome-micropython.com/#ntp as I am sure many people would find this useful.

@stritti
Copy link

stritti commented May 24, 2023

Thanks for this great gist! The text of error should be corrected in line: https://gist.github.com/Beormund/f0f39c72e066da497f6308d1964c9627#file-daylightsaving-py-L16

@Beormund
Copy link
Author

Thanks for this great gist! The text of error should be corrected in line: https://gist.github.com/Beormund/f0f39c72e066da497f6308d1964c9627#file-daylightsaving-py-L16

Done - thanks for spotting.

@gustavolaureano
Copy link

Hi @Beormund
I found your post about the UDP issue on the micropython forum and it looks like you were also trying to get the fauxmo / echo integration to work on the Pico W, this is exactly what I am trying to accomplish in a hobby project now, did you get it to work on the Pico W ? if yes, do you mind in sharing how you did it? I am stuck at this right now :(
Would you mind sharing your code?? Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment