Source code for pywws.timezone

#!/usr/bin/env python

# pywws - Python software for USB Wireless Weather Stations
# http://github.com/jim-easterbrook/pywws
# Copyright (C) 2008-21  pywws contributors

# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

"""Provide a couple of :py:class:`datetime.tzinfo` compatible objects
representing local time and UTC.

Introduction
------------

This module provides two :py:class:`datetime.tzinfo` compatible objects
representing UTC and local time zones. These are used to convert
timestamps to and from UTC and local time. The weather station software
stores data with UTC timestamps, to avoid problems with daylight savings
time, but the template and plot programs output data with local times.

Detailed API
------------

"""

from __future__ import print_function

import datetime
import logging
import sys

import tzlocal

from pywws.constants import DAY, HOUR

logger = logging.getLogger(__name__)


class _TimeZone(object):
    class _UTC(datetime.tzinfo):
        _offset = datetime.timedelta(0)

        def utcoffset(self, dt):
            return self._offset

        def dst(self, dt):
            return self._offset

        def tzname(self, dt):
            return 'UTC'


    def __init__(self, tz_name=None):
        if tz_name:
            # use a named time zone instead of system default, for testing
            import pytz
            self.local = pytz.timezone(tz_name)
        else:
            self.local = tzlocal.get_localzone()
        logger.info('Using timezone "{!s}"'.format(self.local))
        self._using_pytz = hasattr(self.local, 'localize')
        if sys.version_info >= (3, 2):
            self.utc = datetime.timezone.utc
        else:
            self.utc = self._UTC()

    def local_to_utc(self, dt):
        """Convert a local time (with or without tzinfo) to UTC without
        tzinfo, as used for pywws timestamps."""
        if dt.tzinfo is None:
            if self._using_pytz:
                dt = self.local.localize(dt)
            else:
                dt = dt.replace(tzinfo=self.local)
        return dt.astimezone(self.utc).replace(tzinfo=None)

    def utc_to_local(self, dt):
        """Convert a pywws time to local time, with tzinfo."""
        return dt.replace(tzinfo=self.utc).astimezone(self.local)

    def utc_to_nodst(self, dt):
        """Convert a pywws time to local time, without DST.

        This could be an invalid time, so no tzinfo is included."""
        dt = self.utc_to_local(dt)
        return dt.replace(tzinfo=None) - dt.dst()

    def hour_start(self, dt):
        """Return UTC time before dt whose local time is on the hour."""
        local = self.utc_to_local(dt)
        diff = local - local.replace(minute=0, second=0)
        return dt - diff

    def day_start(self, dt, day_end_hour, use_dst=True):
        """Return UTC time before dt whose local time is day_end_hour.

        If use_dst is False then DST is ignored, so a day ending at 9am
        (local time) in winter would end at 10am in summer."""
        # get UTC and local equivalent
        dt = dt.replace(tzinfo=self.utc)
        local = dt.astimezone(self.local)
        if not use_dst:
            local = local.replace(tzinfo=None) - local.dst()
        # truncate to the hour
        diff = local - local.replace(minute=0, second=0)
        dt -= diff
        local -= diff
        # decrement UTC until equivalent local time has correct hour
        for h in range(4):
            hours = local.hour - day_end_hour
            if hours == 0:
                break
            elif hours > 1:
                hours -= 1
            elif hours < 0:
                hours += 23
            dt -= HOUR * hours
            local = dt.astimezone(self.local)
            if not use_dst:
                local = local.replace(tzinfo=None) - local.dst()
        return dt.replace(tzinfo=None)

    def _dst_transitions(self):
        """Find local times near DST transitions in the last year.

        This is only intended to be used for testing."""
        day = datetime.datetime.now(tz=self.local).replace(
            minute=15, second=0, microsecond=0)
        dst = day.dst()
        for d in range(365):
            day -= DAY
            if self._using_pytz:
                day = self.local.normalize(day)
            if day.dst() == dst:
                continue
            hour = day
            for h in range(25):
                hour += HOUR
                if self._using_pytz:
                    hour = self.local.normalize(hour)
                if hour.dst() == dst:
                    yield hour
                    break
            dst = day.dst()


time_zone = _TimeZone()


[docs]def main(): global time_zone if len(sys.argv) > 1: time_zone = _TimeZone(sys.argv[1]) print('Local time zone:', time_zone.local) now = datetime.datetime.utcnow().replace(microsecond=0) print('UTC time:', now) lcl = time_zone.utc_to_local(now) print('Local time:', lcl, lcl.strftime('(%Z)')) print() print('most recent 9am', time_zone.day_start(now, 9), 'UTC') print('most recent 9pm', time_zone.day_start(now, 21), 'UTC') for dt in time_zone._dst_transitions(): print() print('Convert local to UTC around DST transition') lcl = dt.replace(tzinfo=None) lcl -= HOUR * 2 for h in range(4): utc = time_zone.local_to_utc(lcl) print(lcl, '->', utc) lcl += HOUR print() print('Convert UTC to local around DST transition') utc -= HOUR * 3 for h in range(4): lcl = time_zone.utc_to_local(utc) print(utc, '->', lcl, lcl.strftime('(%Z)')) utc += HOUR return 0
if __name__ == "__main__": sys.exit(main())