Source code for htheatpump.httimeprog

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

#  htheatpump - Serial communication module for Heliotherm heat pumps
#  Copyright (C) 2023  Daniel Strigl

#  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 3 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, see <http://www.gnu.org/licenses/>.

""" Classes representing the time programs of the Heliotherm heat pump. """

import copy
import re
from itertools import chain
from typing import Final, Any, Dict, List, Optional, Tuple, Type, TypeVar

# ------------------------------------------------------------------------------------------------------------------- #
# TimeProgPeriod class
# ------------------------------------------------------------------------------------------------------------------- #

TimeProgPeriodT = TypeVar("TimeProgPeriodT", bound="TimeProgPeriod")


[docs]class TimeProgPeriod: """Representation of a time program period defined by start- and end-time (``HH:MM``). :param start_hour: The hour value of the start-time (``HH``). :type start_hour: int :param start_minute: The minute value of the start-time (``MM``). :type start_minute: int :param end_hour: The hour value of the end-time (``HH``). :type end_hour: int :param end_minute: The minute value of the end-time (``MM``). :type end_minute: int :raises ValueError: Will be raised for any invalid argument. """ TIME_PATTERN: Final = r"^(\d?\d):(\d?\d)$" # e.g. '23:45' or '2:5' HOURS_RANGE: Final = range(0, 25) # 0..24 MINUTES_RANGE: Final = range(0, 60) # 0..59 def __init__(self, start_hour: int, start_minute: int, end_hour: int, end_minute: int) -> None: # verify the passed time values self._verify(start_hour, start_minute, end_hour, end_minute) # ... and store it self._start_hour, self._start_minute = start_hour, start_minute self._end_hour, self._end_minute = end_hour, end_minute @classmethod def _is_time_valid(cls: Type[TimeProgPeriodT], hour: int, minute: int) -> bool: if (hour not in cls.HOURS_RANGE) or (minute not in cls.MINUTES_RANGE): return False if (hour * 60 + minute) > (24 * 60 + 0): # e.g. '24:15' -> not valid! return False return True @classmethod def _verify( cls: Type[TimeProgPeriodT], start_hour: int, start_minute: int, end_hour: int, end_minute: int, ) -> None: if not cls._is_time_valid(start_hour, start_minute): raise ValueError( "the provided start time does not represent a valid time value [{:02d}:{:02d}]".format( start_hour, start_minute ) ) if not cls._is_time_valid(end_hour, end_minute): raise ValueError( "the provided end time does not represent a valid time value [{:02d}:{:02d}]".format( end_hour, end_minute ) ) if (start_hour * 60 + start_minute) > (end_hour * 60 + end_minute): raise ValueError( "the provided start time must be lesser or equal to the end time [{:02d}:{:02d}-{:02d}:{:02d}]".format( start_hour, start_minute, end_hour, end_minute ) )
[docs] @classmethod def from_str(cls: Type[TimeProgPeriodT], start_str: str, end_str: str) -> TimeProgPeriodT: """Create a :class:`~TimeProgPeriod` instance from string representations of the start- and end-time. :param start_str: The start-time of the time program entry as :obj:`str`. :type start_str: str :param end_str: The end-time of the time program entry as :obj:`str`. :type end_str: str :returns: A :class:`~TimeProgPeriod` instance with the given properties. :rtype: ``TimeProgPeriod`` :raises ValueError: Will be raised for any invalid argument. """ m_start = re.match(cls.TIME_PATTERN, start_str) if not m_start: raise ValueError("the provided 'start_str' does not represent a valid time value [{!r}]".format(start_str)) m_end = re.match(cls.TIME_PATTERN, end_str) if not m_end: raise ValueError("the provided 'end_str' does not represent a valid time value [{!r}]".format(end_str)) start_hour, start_minute = [int(v) for v in m_start.group(1, 2)] end_hour, end_minute = [int(v) for v in m_end.group(1, 2)] return cls(start_hour, start_minute, end_hour, end_minute)
[docs] @classmethod def from_json(cls: Type[TimeProgPeriodT], json_dict: Dict[str, str]) -> TimeProgPeriodT: """Create a :class:`~TimeProgPeriod` instance from a JSON representation. :param json_dict: The JSON representation of the time program period as :obj:`dict`. :type json_dict: dict :rtype: ``TimeProgPeriod`` :raises ValueError: Will be raised for any invalid argument. """ return cls.from_str(json_dict["start"], json_dict["end"])
[docs] def set(self, start_hour: int, start_minute: int, end_hour: int, end_minute: int) -> None: """Set the start- and end-time of this time program period. :param start_hour: The hour value of the start-time. :type start_hour: int :param start_minute: The minute value of the start-time. :type start_minute: int :param end_hour: The hour value of the end-time. :type end_hour: int :param end_minute: The minute value of the end-time. :type end_minute: int :raises ValueError: Will be raised for any invalid argument. """ # verify the passed time values self._verify(start_hour, start_minute, end_hour, end_minute) # ... and store it self._start_hour, self._start_minute = start_hour, start_minute self._end_hour, self._end_minute = end_hour, end_minute
def __str__(self) -> str: """Return a string representation of this time program period. :returns: A string representation of this time program period. :rtype: ``str`` """ return "{:02d}:{:02d}-{:02d}:{:02d}".format( self._start_hour, self.start_minute, self.end_hour, self._end_minute ) def __eq__(self, other: object) -> bool: """Implement the equal operator. :param other: Another instance of :class:`~TimeProgPeriod` to check against. :returns: :const:`True` if we check against the same subclass and the raw values matches, :const:`False` otherwise. :rtype: ``bool`` """ if other is None: return False if not isinstance(other, self.__class__): raise TypeError() return ( self._start_hour == other.start_hour and self._start_minute == other.start_minute and self._end_hour == other.end_hour and self._end_minute == other.end_minute )
[docs] def as_dict(self) -> Dict[str, Tuple[int, int]]: """Create a dict representation of this time program period. :returns: A dict representing this time program period. :rtype: ``dict`` """ return { "start": (self._start_hour, self._start_minute), "end": (self._end_hour, self._end_minute), }
[docs] def as_json(self) -> Dict[str, str]: """Create a json-readable dict representation of this time program period. :returns: A json-readable dict representing this time program period. :rtype: ``dict`` """ return { "start": self.start_str, "end": self.end_str, }
@property def start_str(self) -> str: """Return the start-time of this time program period as :obj:`str`. :returns: The start-time of this time program period as :obj:`str`. For example: :: '11:00' :rtype: ``str`` """ return "{:02d}:{:02d}".format(self._start_hour, self.start_minute) @property def end_str(self) -> str: """Return the end-time of this time program period as :obj:`str`. :returns: The end-time of this time program period as :obj:`str`. For example: :: '16:45' :rtype: ``str`` """ return "{:02d}:{:02d}".format(self._end_hour, self.end_minute) @property def start_hour(self) -> int: """Return the hour value of the start-time of this time program period. :returns: The hour value of the start-time of this time program period. :rtype: ``int`` """ return self._start_hour @property def start_minute(self) -> int: """Return the minute value of the start-time of this time program period. :returns: The minute value of the start-time of this time program period. :rtype: ``int`` """ return self._start_minute @property def end_hour(self) -> int: """Return the hour value of the end-time of this time program period. :returns: The hour value of the end-time of this time program period. :rtype: ``int`` """ return self._end_hour @property def end_minute(self) -> int: """Return the minute value of the end-time of this time program period. :returns: The minute value of the end-time of this time program period. :rtype: ``int`` """ return self._end_minute @property def start(self) -> Tuple[int, int]: """Return the start-time of this time program period as a tuple with 2 elements, where the first element represents the hours and the second one the minutes. :returns: The start-time of this time program period as tuple. For example: :: ( 11, 0 ) # -> 11:00 :rtype: ``tuple`` ( int, int ) """ return self._start_hour, self._start_minute @property def end(self) -> Tuple[int, int]: """Return the end-time of this time program period as a tuple with 2 elements, where the first element represents the hours and the second one the minutes. :returns: The end-time of this time program period as tuple. For example: :: ( 16, 45 ) # -> 16:45 :rtype: ``tuple`` ( int, int ) """ return self._end_hour, self._end_minute
# ------------------------------------------------------------------------------------------------------------------- # # TimeProgEntry class # ------------------------------------------------------------------------------------------------------------------- # TimeProgEntryT = TypeVar("TimeProgEntryT", bound="TimeProgEntry")
[docs]class TimeProgEntry: """Representation of a single time program entry. :param state: The state of the time program entry. :type state: int :param period: The period of the time program entry. :type period: TimeProgPeriod """ def __init__(self, state: int, period: TimeProgPeriod) -> None: self._state = state self._period = copy.deepcopy(period)
[docs] @classmethod def from_str(cls: Type[TimeProgEntryT], state: str, start_str: str, end_str: str) -> TimeProgEntryT: """Create a :class:`~TimeProgEntry` instance from string representations of the state, start- and end-time. :param state: The state of the time program entry as :obj:`str`. :type state: str :param start_str: The start-time of the time program entry as :obj:`str`. :type start_str: str :param end_str: The end-time of the time program entry as :obj:`str`. :type end_str: str :returns: A :class:`~TimeProgEntry` instance with the given properties. :rtype: ``TimeProgEntry`` """ return cls(int(state), TimeProgPeriod.from_str(start_str, end_str))
[docs] @classmethod def from_json(cls: Type[TimeProgEntryT], json_dict: Dict[str, Any]) -> TimeProgEntryT: """Create a :class:`~TimeProgEntry` instance from a JSON representation. :param json_dict: The JSON representation of the time program entry as :obj:`dict`. :type json_dict: dict :rtype: ``TimeProgEntry`` :raises ValueError: Will be raised for any invalid argument. """ return cls( int(json_dict["state"]), TimeProgPeriod.from_str(json_dict["start"], json_dict["end"]), )
[docs] def set(self, state: int, period: TimeProgPeriod) -> None: """Set the state and period of this time program entry. :param state: The state of the time program entry. :type state: int :param period: The period of the time program entry. :type period: TimeProgPeriod """ self._state = state self._period = copy.deepcopy(period)
def __str__(self) -> str: """Return a string representation of this time program entry. :returns: A string representation of this time program entry. :rtype: ``str`` """ return "state={:d}, time={!s}".format(self._state, self._period) def __eq__(self, other: object) -> bool: """Implement the equal operator. :param other: Another instance of :class:`~TimeProgEntry` to check against. :returns: :const:`True` if we check against the same subclass and the raw values matches, :const:`False` otherwise. :rtype: ``bool`` """ if other is None: return False if not isinstance(other, self.__class__): raise TypeError() return self._state == other.state and self._period == other.period
[docs] def as_dict(self) -> Dict[str, Any]: """Create a dict representation of this time program entry. :returns: A dict representing this time program entry. :rtype: ``dict`` """ ret: Dict[str, Any] = {"state": self._state} ret.update(self._period.as_dict()) return ret
[docs] def as_json(self) -> Dict[str, Any]: """Create a json-readable dict representation of this time program entry. :returns: A json-readable dict representing this time program entry. :rtype: ``dict`` """ ret: Dict[str, Any] = {"state": self._state} ret.update(self._period.as_json()) return ret
@property def state(self) -> int: """Property to get or set the state of this time program entry. :param: The new state of the time program entry. :returns: The current state of the time program entry. :rtype: ``int`` """ return self._state @state.setter def state(self, val: int) -> None: self._state = val @property def period(self) -> TimeProgPeriod: """Property to get or set the period of this time program entry. :param: The new period of the time program entry as :class:`~TimeProgPeriod`. :returns: A copy of the current period of the time program entry as :class:`~TimeProgPeriod`. :rtype: ``TimeProgPeriod`` """ return copy.deepcopy(self._period) @period.setter def period(self, val: TimeProgPeriod) -> None: self._period = copy.deepcopy(val)
# ------------------------------------------------------------------------------------------------------------------- # # TimeProgram class # ------------------------------------------------------------------------------------------------------------------- # TimeProgramT = TypeVar("TimeProgramT", bound="TimeProgram")
[docs]class TimeProgram: """Representation of a time program of the Heliotherm heat pump. :param idx: The time program index. :type idx: int :param name: The name of the time program (e.g. "Warmwasser"). :type name: str :param ead: The number of entries a day of the time program. :type ead: int :param nos: The number of states of the time program. :type nos: int :param ste: The step-size (in minutes) of the start- and end-times of the time program entries. :type ste: int :param nod: The number of days of the time program. :type nod: int """ def __init__(self, idx: int, name: str, ead: int, nos: int, ste: int, nod: int) -> None: self._index = idx self._name = name self._entries_a_day = ead self._number_of_states = nos self._step_size = ste self._number_of_days = nod self._entries: List[List[Optional[TimeProgEntry]]] = [ [None for _ in range(self._entries_a_day)] for _ in range(self._number_of_days) ] # TODO verify args?! def _verify_entry(self, entry: TimeProgEntry) -> None: if entry.state not in range(0, self._number_of_states): raise ValueError( "the state of the provided entry is outside the allowed range [{:d}, 0..{:d}]".format( entry.state, self._number_of_states ) ) if entry.period.start_minute % self._step_size != 0: raise ValueError( "the provided start time must be a multiple of the given step size [{}, {:d}]".format( entry.period.start_str, self._step_size ) ) if entry.period.end_minute % self._step_size != 0: raise ValueError( "the provided end time must be a multiple of the given step size [{}, {:d}]".format( entry.period.end_str, self._step_size ) )
[docs] @classmethod def from_json(cls: Type[TimeProgramT], json_dict: Dict[str, Any]) -> TimeProgramT: """Create a :class:`~TimeProgram` instance from a JSON representation. :param json_dict: The JSON representation of the time program as :obj:`dict`. :type json_dict: dict :rtype: ``TimeProgram`` :raises ValueError: Will be raised for any invalid argument. """ idx = int(json_dict["index"]) name = str(json_dict["name"]) ead = int(json_dict["ead"]) nos = int(json_dict["nos"]) ste = int(json_dict["ste"]) nod = int(json_dict["nod"]) time_prog = cls(idx, name, ead, nos, ste, nod) entries = json_dict.get("entries") if entries is not None: for day_num, day_entries in enumerate(entries): for entry_num, entry in enumerate(day_entries): if entry is not None: time_prog.set_entry(day_num, entry_num, TimeProgEntry.from_json(entry)) return time_prog
def __str__(self) -> str: """Return a string representation of this time program. :returns: A string representation of this time program. :rtype: ``str`` """ any_entries = sum([1 for entry in chain.from_iterable(self._entries) if entry is not None]) > 0 return "idx={:d}, name={!r}, ead={:d}, nos={:d}, ste={:d}, nod={:d}, entries=[{}]".format( self._index, self._name, self._entries_a_day, self._number_of_states, self._step_size, self._number_of_days, "..." if any_entries else "", )
[docs] def as_dict(self, with_entries: bool = True) -> Dict[str, Any]: """Create a dict representation of this time program. :param with_entries: Determines whether the single time program entries should be included or not. Default is :const:`True`. :type with_entries: bool :returns: A dict representing this time program. :rtype: ``dict`` """ ret = { "index": self._index, # index of the time program "name": self._name, # name of the time program "ead": self._entries_a_day, # entries-a-day (?) "nos": self._number_of_states, # number-of-states (?) "ste": self._step_size, # step-size [in minutes] (?) "nod": self._number_of_days, # number-of-days (?) } if with_entries: # the time program entries itself ret.update({"entries": self._entries}) return ret
[docs] def as_json(self, with_entries: bool = True) -> Dict[str, Any]: """Create a json-readable dict representation of this time program. :param with_entries: Determines whether the single time program entries should be included or not. Default is :const:`True`. :type with_entries: bool :returns: A json-readable dict representing this time program. :rtype: ``dict`` """ ret = { "index": self._index, # index of the time program "name": self._name, # name of the time program "ead": self._entries_a_day, # entries-a-day (?) "nos": self._number_of_states, # number-of-states (?) "ste": self._step_size, # step-size [in minutes] (?) "nod": self._number_of_days, # number-of-days (?) } if with_entries: # the time program entries itself ret.update( { "entries": [ [entry.as_json() if entry is not None else None for entry in day_entries] for day_entries in self._entries ] } ) return ret
@property def index(self) -> int: """Return the index of this time program. :returns: The index of this time program. :rtype: ``int`` """ return self._index @property def name(self) -> str: """Return the name of this time program. :returns: The name of this time program. :rtype: ``int`` """ return self._name @property def entries_a_day(self) -> int: """Return the number of entries a day of this time program. :returns: The number of entries a day of this time program. :rtype: ``int`` """ return self._entries_a_day @property def number_of_states(self) -> int: """Return the number of states of this time program. :returns: The number of states of this time program. :rtype: ``int`` """ return self._number_of_states @property def step_size(self) -> int: """Return the step-size (in minutes) of the start- and end-times of this time program entries. :returns: The step-size (in minutes) of the start- and end-times of this time program entries. :rtype: ``int`` """ return self._step_size @property def number_of_days(self) -> int: """Return the number of days of this time program. :returns: The number of days of this time program. :rtype: ``int`` """ return self._number_of_days
[docs] def entry(self, day: int, num: int) -> Optional[TimeProgEntry]: """Return a copy of a specific time program entry. :param day: The day of the time program entry. :type day: int :param num: The number of the time program entry. :type num: int :returns: The time program entry as instance of :class:`~TimeProgEntry` or :const:`None` if not set. :rtype: ``TimeProgEntry`` """ return copy.deepcopy(self._entries[day][num])
[docs] def entries_of_day(self, day: int) -> List[Optional[TimeProgEntry]]: """Return a list of copies of time program entries of a specific day. :param day: The day of the time program entries. :type day: int :returns: A list of :class:`~TimeProgEntry` instances or :const:`None` if not set. :rtype: ``list`` (TimeProgEntry) """ return copy.deepcopy(self._entries[day])
[docs] def set_entry(self, day: int, num: int, entry: TimeProgEntry) -> None: """Set the properties of a given time program entry of the heat pump. :param day: The day of the time program entry. :type day: int :param num: The number of the time program entry. :type num: int :param entry: The time program entry itself. :type entry: TimeProgEntry :raises ValueError: Will be raised if any property of the given entry is out of the specification of this time program. """ self._verify_entry(entry) self._entries[day][num] = copy.deepcopy(entry)
# ------------------------------------------------------------------------------------------------------------------- # # Exported symbols # ------------------------------------------------------------------------------------------------------------------- # __all__ = ["TimeProgPeriod", "TimeProgEntry", "TimeProgram"]