From ba2764e20d179a8f6651380a8855d623489cf5a9 Mon Sep 17 00:00:00 2001 From: spacemanspiff2007 Date: Mon, 30 Sep 2019 06:28:47 +0200 Subject: [PATCH] Version 0.9.0 - Renamed functions and variables for item: - item.state -> item.value - item.set_state -> item.set_value - item.post_state -> item.post_value - item.get_state -> item.get_value - Moved configuration to EasyCo - MQTT configuration has listen_only switch, too - Added NumberItem to openhab.items - Updated lots documentation - Added functions with corresponding commands for OpenhabItems - Added command processing for OpenhabItems - Prepared for Openhab 2.5 - Use relative names in testing configuration - Counter is thread safe - renamed ColorItem.value to ColorItem.brightness - MultiModeValue has function calculate_lower_priority_value which returns the low prio value --- HABApp/__version__.py | 2 +- HABApp/config/_conf_mqtt.py | 72 +++++------- HABApp/config/_conf_openhab.py | 46 ++++---- HABApp/config/config.py | 103 ++++++------------ HABApp/config/configentry.py | 97 ----------------- HABApp/core/EventBus.py | 13 +++ HABApp/core/Items.py | 4 +- HABApp/core/items/item.py | 100 +++++++++++------ HABApp/core/items/item_color.py | 65 ++++++++--- HABApp/mqtt/items/mqtt_item.py | 4 +- HABApp/mqtt/mqtt_connection.py | 2 +- HABApp/mqtt/mqtt_interface.py | 2 + HABApp/openhab/definitions/__init__.py | 4 +- HABApp/openhab/definitions/values.py | 60 ++++++++++ HABApp/openhab/events/map_events.py | 16 ++- HABApp/openhab/items/__init__.py | 2 + HABApp/openhab/items/color_item.py | 20 ++++ HABApp/openhab/items/commands.py | 39 +++++++ HABApp/openhab/items/contact_item.py | 18 +-- HABApp/openhab/items/dimmer_item.py | 44 ++++---- HABApp/openhab/items/map_items.py | 10 +- HABApp/openhab/items/number_item.py | 12 ++ HABApp/openhab/items/rollershutter_item.py | 36 ++---- HABApp/openhab/items/switch_item.py | 37 +++---- HABApp/openhab/oh_connection.py | 4 +- HABApp/openhab/oh_interface.py | 21 +++- HABApp/rule/rule.py | 6 +- HABApp/rule/watched_item.py | 2 +- HABApp/util/__init__.py | 2 +- HABApp/util/counter.py | 69 ------------ HABApp/util/counter_item.py | 57 ++++++++++ HABApp/util/multimode_item.py | 44 +++++--- HABApp/util/statistics.py | 23 ++-- _doc/conf.py | 19 +++- _doc/configuration.rst | 9 +- _doc/interface_mqtt.rst | 39 ++++--- _doc/interface_openhab.rst | 53 +++++++-- _doc/rule.rst | 34 ++++-- _doc/util.rst | 19 ++-- conf/rules/mqtt_rule.py | 3 - conf_testing/config.yml | 8 +- conf_testing/lib/HABAppTests/item_waiter.py | 4 +- .../lib/HABAppTests/openhab_tmp_item.py | 9 +- conf_testing/lib/HABAppTests/test_base.py | 2 + conf_testing/lib/HABAppTests/test_data.py | 4 +- conf_testing/logging.yml | 6 +- conf_testing/rules/test_openhab_interface.py | 2 +- conf_testing/rules/test_openhab_item_funcs.py | 9 +- conf_testing/rules/test_parameter_files.py | 5 +- setup.py | 2 +- tests/test_core/__init__.py | 1 + tests/test_core/test_items/__init__.py | 1 + tests/test_core/test_items/test_item.py | 15 ++- tests/test_core/test_items/test_item_color.py | 18 +-- tests/test_core/test_items/tests_all_items.py | 59 ++++++++++ tests/test_openhab/test_items/__init__.py | 0 .../test_openhab/test_items/test_commands.py | 40 +++++++ tests/test_openhab/test_openhab_events.py | 2 +- tests/test_utils/test_counter.py | 17 +++ tests/test_utils/test_multivalue.py | 27 ++++- tox.ini | 2 +- 61 files changed, 872 insertions(+), 573 deletions(-) delete mode 100644 HABApp/config/configentry.py create mode 100644 HABApp/openhab/definitions/values.py create mode 100644 HABApp/openhab/items/color_item.py create mode 100644 HABApp/openhab/items/commands.py create mode 100644 HABApp/openhab/items/number_item.py delete mode 100644 HABApp/util/counter.py create mode 100644 HABApp/util/counter_item.py create mode 100644 tests/test_core/test_items/tests_all_items.py create mode 100644 tests/test_openhab/test_items/__init__.py create mode 100644 tests/test_openhab/test_items/test_commands.py create mode 100644 tests/test_utils/test_counter.py diff --git a/HABApp/__version__.py b/HABApp/__version__.py index 702b5789..7bf10157 100644 --- a/HABApp/__version__.py +++ b/HABApp/__version__.py @@ -1 +1 @@ -__VERSION__ = '0.8.0' +__VERSION__ = '0.9.0' diff --git a/HABApp/config/_conf_mqtt.py b/HABApp/config/_conf_mqtt.py index 35815324..a7c6e9f9 100644 --- a/HABApp/config/_conf_mqtt.py +++ b/HABApp/config/_conf_mqtt.py @@ -1,6 +1,7 @@ -from voluptuous import All, Invalid, Length +import typing -from .configentry import ConfigEntry, ConfigEntryContainer +from EasyCo import ConfigContainer, ConfigEntry +from voluptuous import Invalid def MqttTopicValidator(msg=None): @@ -32,54 +33,37 @@ def f(v): return f -class MqttConnection(ConfigEntry): - def __init__(self): - super().__init__() - self._entry_name = 'connection' - self.client_id = 'HABApp' - self.host = '' - self.port = 8883 - self.user = '' - self.password = '' - self.tls = True - self.tls_insecure = False +class Connection(ConfigContainer): + client_id: str = 'HABApp' + host: str = '' + port: int = 8883 + user: str = '' + password: str = '' + tls: bool = True + tls_insecure: bool = False - self._entry_validators['client_id'] = All(str, Length(min=1)) - self._entry_kwargs['password'] = {'default': ''} - self._entry_kwargs['tls_insecure'] = {'default': False} +class Subscribe(ConfigContainer): + qos: int = ConfigEntry(default=0, description='Default QoS for subscribing') + topics: typing.List[typing.Union[str, int]] = ConfigEntry( + default_factory=lambda: list(('#', 0)), validator=MqttTopicValidator + ) -class Subscribe(ConfigEntry): - def __init__(self): - super().__init__() - self.qos = 0 - self.topics = ['#', 0] - self._entry_validators['topics'] = MqttTopicValidator() - self._entry_kwargs['topics'] = {'default': [('#', 0)]} - self._entry_kwargs['default_qos'] = {'default': 0} +class Publish(ConfigContainer): + qos: int = ConfigEntry(default=0, description='Default QoS when publishing values') + retain: bool = ConfigEntry(default=False, description='Default retain flag when publishing values') -class Publish(ConfigEntry): - def __init__(self): - super().__init__() - self.qos = 0 - self.retain = False +class General(ConfigContainer): + listen_only: bool = ConfigEntry( + False, description='If True HABApp will not publish any value to the broker' + ) -class mqtt(ConfigEntry): - def __init__(self): - super().__init__() - self.client_id = '' - self.tls = True - self.tls_insecure = False - - self._entry_kwargs['tls_insecure'] = {'default': False} - - -class Mqtt(ConfigEntryContainer): - def __init__(self): - self.connection = MqttConnection() - self.subscribe = Subscribe() - self.publish = Publish() +class Mqtt(ConfigContainer): + connection = Connection() + subscribe = Subscribe() + publish = Publish() + general = General() diff --git a/HABApp/config/_conf_openhab.py b/HABApp/config/_conf_openhab.py index dd28e801..f07688c6 100644 --- a/HABApp/config/_conf_openhab.py +++ b/HABApp/config/_conf_openhab.py @@ -1,34 +1,28 @@ -from .configentry import ConfigEntry, ConfigEntryContainer +from EasyCo import ConfigContainer, ConfigEntry -class Ping(ConfigEntry): - def __init__(self): - super().__init__() - self.enabled = False - self.item = '' - self.interval = 10 +class Ping(ConfigContainer): + enabled: bool = ConfigEntry(True, description='If enabled the configured item will show how long it takes to send ' + 'an update from HABApp and get the updated value back from openhab' + 'in milliseconds') + item: str = ConfigEntry('HABApp_Ping', description='Name of the item') + interval: int = ConfigEntry(10, description='Seconds between two pings') -class General(ConfigEntry): - def __init__(self): - super().__init__() - self.listen_only = False +class General(ConfigContainer): + listen_only: bool = ConfigEntry( + False, description='If True HABApp will not change anything on the openhab instance.' + ) -class Connection(ConfigEntry): - def __init__(self): - super().__init__() - self.host = 'localhost' - self.port = 8080 - self.user = '' - self.password = '' +class Connection(ConfigContainer): + host: str = 'localhost' + port: int = 8080 + user: str = '' + password: str = '' - self._entry_kwargs['user'] = {'default': ''} - self._entry_kwargs['password'] = {'default': ''} - -class Openhab(ConfigEntryContainer): - def __init__(self): - self.ping = Ping() - self.connection = Connection() - self.general = General() +class Openhab(ConfigContainer): + ping = Ping() + connection = Connection() + general = General() diff --git a/HABApp/config/config.py b/HABApp/config/config.py index c7a2bfdb..99ca72fa 100644 --- a/HABApp/config/config.py +++ b/HABApp/config/config.py @@ -1,21 +1,19 @@ import codecs import logging import logging.config +import sys import time from pathlib import Path -import sys import ruamel.yaml -from voluptuous import MultipleInvalid, Schema +from EasyCo import ConfigFile, PathContainer from HABApp.__version__ import __VERSION__ +from HABApp.runtime import FileEventTarget from ._conf_mqtt import Mqtt from ._conf_openhab import Openhab -from .configentry import ConfigEntry from .default_logfile import get_default_logfile -from HABApp.runtime import FileEventTarget - _yaml_param = ruamel.yaml.YAML(typ='safe') _yaml_param.default_flow_style = False _yaml_param.default_style = False @@ -35,13 +33,26 @@ class InvalidConfigException(Exception): pass -class Directories(ConfigEntry): - def __init__(self): - super().__init__() - self.logging: Path = 'log' - self.rules: Path = 'rules' - self.lib: Path = 'lib' - self.param: Path = 'param' +class Directories(PathContainer): + logging: Path = 'log' + rules: Path = 'rules' + lib: Path = 'lib' + param: Path = 'param' + + def on_all_values_set(self): + try: + if not self.rules.is_dir(): + self.rules.mkdir() + if not self.logging.is_dir(): + self.logging.mkdir() + except Exception as e: + log.error(e) + + +class HABAppConfigFile(ConfigFile): + directories = Directories() + mqtt = Mqtt() + openhab = Openhab() class Config(FileEventTarget): @@ -59,12 +70,12 @@ def __init__(self, runtime, config_folder : Path): self.file_conf_logging = self.folder_conf / 'logging.yml' # these are the accessible config entries - self.directories = Directories() - self.mqtt = Mqtt() - self.openhab = Openhab() + self.config = HABAppConfigFile() + self.directories = self.config.directories + self.mqtt = self.config.mqtt + self.openhab = self.config.openhab # if the config does not exist it will be created - self.__check_create_config() self.__check_create_logging() # folder watcher @@ -97,31 +108,6 @@ def reload_file(self, path: Path): def remove_file(self, path: Path): pass - def __check_create_config(self): - if self.file_conf_habapp.is_file(): - return None - - cfg = {} - self.directories.insert_data(cfg) - self.openhab.insert_data(cfg) - self.mqtt.insert_data(cfg) - - print( f'Creating {self.file_conf_habapp.name} in {self.file_conf_habapp.parent}') - with self.file_conf_habapp.open('w', encoding='utf-8') as file: - _yaml_param.dump(cfg, file) - - # Create default folder for rules, too - # Logging directories will get created elsewhere - # Param files are optional - if isinstance(self.directories.rules, str): - (self.file_conf_habapp.parent / self.directories.rules).resolve().mkdir() - if isinstance(self.directories.logging, str): - (self.file_conf_habapp.parent / self.directories.logging).resolve().mkdir() - - time.sleep(0.1) - return None - - def __check_create_logging(self): if self.file_conf_logging.is_file(): return None @@ -134,37 +120,12 @@ def __check_create_logging(self): return None def load_cfg(self): - # File has to exist - check because we also get FileDelete events - if not self.file_conf_habapp.is_file(): - return + # We always try to create the dummy config + # # File has to exist - check because we also get FileDelete events + # if not self.file_conf_habapp.is_file(): + # return - with self.file_conf_habapp.open('r', encoding='utf-8') as file: - cfg = _yaml_param.load(file) - try: - _s = {} - self.directories.update_schema(_s) - self.openhab.update_schema(_s) - self.mqtt.update_schema(_s) - cfg = Schema(_s)(cfg) - except MultipleInvalid as e: - log.error(f'Error while loading {self.file_conf_habapp.name}:') - log.error(e) - raise InvalidConfigException() - - self.directories.load_data(cfg) - self.openhab.load_data(cfg) - self.mqtt.load_data(cfg) - - # make Path absolute for all directory entries - for k, v in self.directories.iter_entry(): - __entry = Path(v) - if not __entry.is_absolute(): - __entry = self.folder_conf / __entry - self.directories.__dict__[k] = __entry.resolve() - - if not self.directories.logging.is_dir(): - print( f'Creating log-dir: {self.directories.logging}') - self.directories.logging.mkdir() + self.config.load(self.file_conf_habapp) # Set path for libraries if self.directories.lib.is_dir(): diff --git a/HABApp/config/configentry.py b/HABApp/config/configentry.py deleted file mode 100644 index b9ef730b..00000000 --- a/HABApp/config/configentry.py +++ /dev/null @@ -1,97 +0,0 @@ -import pathlib - -from voluptuous import Required, Coerce, Optional - - -class ConfigEntry: - def __init__(self): - self._entry_is_required = True - self._entry_name = self.__class__.__name__.lower() - self._entry_kwargs = {} - self._entry_validators = {} - self._entry_notify_on_change = [] - - def subscribe_for_changes(self, callback): - assert callback not in self._entry_notify_on_change - self._entry_notify_on_change.append(callback) - - def iter_entry(self): - for name, value in self.__dict__.items(): - assert name.islower(), f'variable name must be lower case! "{name}"' - if name.startswith('_entry_'): - continue - yield name, value - - def update_schema(self, _dict): - - val = {} - for name, value in self.iter_entry(): - - # datatype - if name in self._entry_validators: - _type = self._entry_validators[name] - else: - _type = type(value) - if _type is int or _type is float: - _type = Coerce(_type) - - # we do not load Path objects, we load the strings - if isinstance(value, pathlib.Path): - _type = str - - # name - __name = {'schema' : name} - __name.update(self._entry_kwargs.get(name, {})) - val[Required(**__name)] = _type - - _dict[ Required(self._entry_name) if self._entry_is_required else Optional(self._entry_name)] = val - return _dict - - def insert_data(self, _dict): - _insert = {} - for name, value in self.iter_entry(): - _insert[name] = value - _dict[self._entry_name] = _insert - - def load_data(self, _dict): - if self._entry_name not in _dict: - return None - - notify = False - _dict = _dict[self._entry_name] - for name, value in self.iter_entry(): - cur = getattr(self, name) - new = _dict[name] - if cur != new: - notify = True - setattr(self, name, new) - - if notify: - for cb in self._entry_notify_on_change: - cb() - - -class ConfigEntryContainer: - - def iter_entriy(self): - for name, value in self.__dict__.items(): - if not isinstance(value, ConfigEntry): - continue - yield name, value - - def insert_data(self, _dict): - _insert = {} - for name, value in self.iter_entriy(): - value.insert_data(_insert) - _dict[self.__class__.__name__.lower()] = _insert - - def load_data(self, _dict): - _load = _dict[self.__class__.__name__.lower()] - for name, value in self.iter_entriy(): - value.load_data(_load) - - def update_schema(self, _dict): - schema = {} - for name, value in self.iter_entriy(): - value.update_schema(schema) - _dict[Required(self.__class__.__name__.lower())] = schema diff --git a/HABApp/core/EventBus.py b/HABApp/core/EventBus.py index 931f128d..0fea9a11 100644 --- a/HABApp/core/EventBus.py +++ b/HABApp/core/EventBus.py @@ -4,6 +4,7 @@ from HABApp.util import PrintException from . import EventBusListener +from .events import ValueChangeEvent, ValueUpdateEvent _event_log = logging.getLogger('HABApp.EventBus') _habapp_log = logging.getLogger('HABApp') @@ -20,11 +21,23 @@ def __get_listener_description(listener: EventBusListener) -> str: return f'"{listener.name}" (type {listener.event_filter})' +class ComplexEventValue: + def __init__(self, value): + self.value: typing.Any = value + + @PrintException def post_event(name, event): _event_log.info(event) + # Sometimes we have nested data structues which we need to set the value. + # Once the value in the item registry is set the data structures provide no benefit thus + # we unpack the corresponding value + if isinstance(event, (ValueUpdateEvent, ValueChangeEvent)): + if isinstance(event.value, ComplexEventValue): + event.value = event.value.value + # Notify all listeners for listener in itertools.chain(_EVENT_LISTENER.get(name, []), _EVENT_LISTENER_ALL_EVENTS): listener.notify_listeners(event) diff --git a/HABApp/core/Items.py b/HABApp/core/Items.py index 946bd32d..625503d3 100644 --- a/HABApp/core/Items.py +++ b/HABApp/core/Items.py @@ -29,9 +29,9 @@ def get_all_item_names() -> typing.List[str]: return list(_ALL_ITEMS.keys()) -def create_item(name: str, item_factory, item_state=None) -> __Item: +def create_item(name: str, item_factory, initial_value=None) -> __Item: assert issubclass(item_factory, __Item), item_factory - _ALL_ITEMS[name] = new_item = item_factory(name, state=item_state) + _ALL_ITEMS[name] = new_item = item_factory(name, initial_value=initial_value) return new_item diff --git a/HABApp/core/items/item.py b/HABApp/core/items/item.py index ba664a6d..333871cc 100644 --- a/HABApp/core/items/item.py +++ b/HABApp/core/items/item.py @@ -1,9 +1,17 @@ import datetime import typing import HABApp +import warnings class Item: + """Simple item + + :ivar str ~.name: Name of the item + :ivar ~.value: Value of the item, can be anything + :ivar ~.datetime.datetime last_change: Timestamp of the last time when the item has changed the value + :ivar ~.datetime.datetime last_update: Timestamp of the last time when the item has updated the value + """ @classmethod def get_item(cls, name: str): @@ -17,11 +25,11 @@ def get_item(cls, name: str): return item @classmethod - def get_create_item(cls, name: str, default_state=None): + def get_create_item(cls, name: str, initial_value=None): """Creates a new item in HABApp and returns it or returns the already existing one with the given name :param name: item name - :param default_state: state the item will have if it gets created + :param initial_value: state the item will have if it gets created :return: item """ assert isinstance(name, str), type(name) @@ -29,95 +37,117 @@ def get_create_item(cls, name: str, default_state=None): try: item = HABApp.core.Items.get_item(name) except HABApp.core.Items.ItemNotFoundException: - item = cls(name, default_state) + item = cls(name, initial_value) HABApp.core.Items.set_item(item) assert isinstance(item, cls), f'{cls} != {type(item)}' return item - def __init__(self, name: str, state=None): + def __init__(self, name: str, initial_value=None): + super().__init__() assert isinstance(name, str), type(name) self.name: str = name - self.state: typing.Any = state + self.value: typing.Any = initial_value _now = datetime.datetime.now() self.last_change: datetime.datetime = _now self.last_update: datetime.datetime = _now - def set_state(self, new_state) -> bool: - """Set a new state without creating events on the event bus + def set_value(self, new_value) -> bool: + """Set a new value without creating events on the event bus - :param new_state: new state + :param new_value: new value of the item :return: True if state has changed """ - state_changed = self.state != new_state + state_changed = self.value != new_value _now = datetime.datetime.now() if state_changed: self.last_change = _now self.last_update = _now - self.state = new_state + self.value = new_value return state_changed - def post_state(self, new_state): - """Set a new state and post appropriate events on the event bus (``ValueUpdateEvent``, ``ValueChangeEvent``) + def post_value(self, new_value): + """Set a new value and post appropriate events on the HABApp event bus + (``ValueUpdateEvent``, ``ValueChangeEvent``) - :param new_state: new state + :param new_value: new value of the item """ - old_state = self.state - self.set_state(new_state) + old_value = self.value + self.set_value(new_value) # create events - HABApp.core.EventBus.post_event(self.name, HABApp.core.events.ValueUpdateEvent(self.name, new_state)) - if old_state != new_state: + HABApp.core.EventBus.post_event(self.name, HABApp.core.events.ValueUpdateEvent(self.name, self.value)) + if old_value != self.value: HABApp.core.EventBus.post_event( - self.name, HABApp.core.events.ValueChangeEvent(self.name, value=new_state, old_value=old_state) + self.name, HABApp.core.events.ValueChangeEvent(self.name, value=self.value, old_value=old_value) ) return None - def get_state(self, default_value=None) -> typing.Any: - """Return the state of the item. + def get_value(self, default_value=None) -> typing.Any: + """Return the value of the item. - :param default_value: Return this value if the item state is None - :return: State of the item + :param default_value: Return this value if the item value is None + :return: value of the item """ - if self.state is None: + if self.value is None: return default_value - return self.state + return self.value def __repr__(self): ret = '' - for k in ['name', 'state', 'last_change', 'last_update']: + for k in ['name', 'value', 'last_change', 'last_update']: ret += f'{", " if ret else ""}{k}: {getattr(self, k)}' return f'<{self.__class__.__name__} {ret:s}>' # only support == and != operators by default # __ne__ delegates to __eq__ and inverts the result so this is not overloaded separately def __eq__(self, other): - return self.state == other + return self.value == other def __bool__(self): - return bool(self.state) + return bool(self.value) # rich comparisons only for numeric types (int and float) def __lt__(self, other): - if not isinstance(self.state, (int, float)): + if not isinstance(self.value, (int, float)): return NotImplemented - return self.state < other + return self.value < other def __le__(self, other): - if not isinstance(self.state, (int, float)): + if not isinstance(self.value, (int, float)): return NotImplemented - return self.state <= other + return self.value <= other def __ge__(self, other): - if not isinstance(self.state, (int, float)): + if not isinstance(self.value, (int, float)): return NotImplemented - return self.state >= other + return self.value >= other def __gt__(self, other): - if not isinstance(self.state, (int, float)): + if not isinstance(self.value, (int, float)): return NotImplemented - return self.state > other + return self.value > other + + # ------------------------------------------------------------------------------------------------------------------ + # Deprecated functions. Created 30.09.2019, Keep this around for some time so this doesn't brake anything + # ------------------------------------------------------------------------------------------------------------------ + @property + def state(self): + warnings.warn("'state' is deprecated, use 'value' instead", DeprecationWarning, 2) + return self.value + + def set_state(self, new_state) -> bool: + warnings.warn("'set_state' is deprecated, use 'set_value' instead", DeprecationWarning, 2) + return self.set_value(new_state) + + def post_state(self, new_state): + warnings.warn("'post_state' is deprecated, use 'post_value' instead", DeprecationWarning, 2) + self.post_value(new_state) + + def get_state(self, default_value=None) -> typing.Any: + warnings.warn("'get_state' is deprecated, use 'get_value' instead", DeprecationWarning, 2) + return self.get_value(default_value) diff --git a/HABApp/core/items/item_color.py b/HABApp/core/items/item_color.py index 76f64e10..32112f98 100644 --- a/HABApp/core/items/item_color.py +++ b/HABApp/core/items/item_color.py @@ -8,42 +8,77 @@ class ColorItem(Item): - def __init__(self, name: str, h=0.0, s=0.0, v=0.0): - super().__init__(name=name, state=(h, s, v)) + def __init__(self, name: str, h=0.0, s=0.0, b=0.0): + super().__init__(name=name, initial_value=(h, s, b)) self.hue: float = min(max(0.0, h), HUE_FACTOR) self.saturation: float = min(max(0.0, s), PERCENT_FACTOR) - self.value: float = min(max(0.0, v), PERCENT_FACTOR) + self.brightness: float = min(max(0.0, b), PERCENT_FACTOR) - def set_state(self, hue=0.0, saturation=0.0, value=0.0): + def set_value(self, hue=0.0, saturation=0.0, brightness=0.0): + """Set the color value + + :param hue: hue (in °) + :param saturation: saturation (in %) + :param brightness: brightness (in %) + """ # map tuples to variables # when processing events instead of three values we get the tuple if isinstance(hue, tuple): - value = hue[2] - saturation = hue[1] - hue = hue[0] + hue, saturation, brightness = hue + + # with None we use the already set value + self.hue = min(max(0.0, hue), HUE_FACTOR) if hue is not None else self.hue + self.saturation = min(max(0.0, saturation), PERCENT_FACTOR) if saturation is not None else self.saturation + self.brightness = min(max(0.0, brightness), PERCENT_FACTOR) if brightness is not None else self.brightness - self.hue = min(max(0.0, hue), HUE_FACTOR) - self.saturation = min(max(0.0, saturation), PERCENT_FACTOR) - self.value = min(max(0.0, value), PERCENT_FACTOR) + return super().set_value(new_value=(hue, saturation, brightness)) - return super().set_state(new_state=(hue, saturation, value)) + def post_value(self, hue=0.0, saturation=0.0, brightness=0.0): + """Set a new value and post appropriate events on the event bus (``ValueUpdateEvent``, ``ValueChangeEvent``) + + :param hue: hue (in °) + :param saturation: saturation (in %) + :param brightness: brightness (in %) + """ + super().post_value((hue, saturation, brightness)) def get_rgb(self, max_rgb_value=255) -> typing.Tuple[int, int, int]: + """Return a rgb equivalent of the color + + :param max_rgb_value: the max value for rgb, typically 255 (default) or 65.536 + :return: rgb tuple + """ r, g, b = colorsys.hsv_to_rgb( self.hue / HUE_FACTOR, self.saturation / PERCENT_FACTOR, - self.value / PERCENT_FACTOR + self.brightness / PERCENT_FACTOR ) return int(r * max_rgb_value), int(g * max_rgb_value), int(b * max_rgb_value) - def set_rgb(self, r, g, b, max_rgb_value=255): + def set_rgb(self, r, g, b, max_rgb_value=255) -> 'ColorItem': + """Set a rgb value + + :param r: red value + :param g: green value + :param b: blue value + :param max_rgb_value: the max value for rgb, typically 255 (default) or 65.536 + :return: self + """ h, s, v = colorsys.rgb_to_hsv(r / max_rgb_value, g / max_rgb_value, b / max_rgb_value) self.hue = h * HUE_FACTOR self.saturation = s * PERCENT_FACTOR - self.value = v * PERCENT_FACTOR + self.brightness = v * PERCENT_FACTOR return self + def is_on(self) -> bool: + """Return true if item is on""" + return self.brightness > 0 + + def is_off(self) -> bool: + """Return true if item is off""" + return self.brightness <= 0 + def __repr__(self): - return f'' + return f'' diff --git a/HABApp/mqtt/items/mqtt_item.py b/HABApp/mqtt/items/mqtt_item.py index 557fc744..a78b0bc6 100644 --- a/HABApp/mqtt/items/mqtt_item.py +++ b/HABApp/mqtt/items/mqtt_item.py @@ -4,12 +4,12 @@ class MqttItem(Item): - def publish(self, payload, qos=None, retain=None): + def publish(self, payload, qos: int = None, retain: bool = None): """ Publish the payload under the topic from the item. :param payload: MQTT Payload - :param qos: QoS, can be 0, 1 or 2. If not specified value from configuration file will be used. + :param qos: QoS, can be ``0``, ``1`` or ``2``. If not specified value from configuration file will be used. :param retain: retain message. If not specified value from configuration file will be used. :return: 0 if successful """ diff --git a/HABApp/mqtt/mqtt_connection.py b/HABApp/mqtt/mqtt_connection.py index eed926c2..18d82753 100644 --- a/HABApp/mqtt/mqtt_connection.py +++ b/HABApp/mqtt/mqtt_connection.py @@ -149,7 +149,7 @@ def process_msg(self, client, userdata, message: mqtt.MQTTMessage): # remeber state and update item before doing callbacks _old_state = _item.state - _item.set_state(payload) + _item.set_value(payload) # Post events HABApp.core.EventBus.post_event(topic, MqttValueUpdateEvent(topic, payload)) diff --git a/HABApp/mqtt/mqtt_interface.py b/HABApp/mqtt/mqtt_interface.py index d18ce94d..38fca901 100644 --- a/HABApp/mqtt/mqtt_interface.py +++ b/HABApp/mqtt/mqtt_interface.py @@ -45,6 +45,8 @@ def publish(self, topic: str, payload: typing.Any, qos: int = None, retain: bool if not self.__is_connected(): return mqtt.MQTT_ERR_NO_CONN + if self.__config.general.listen_only: + return 100 if qos is None: qos = self.__config.publish.qos diff --git a/HABApp/openhab/definitions/__init__.py b/HABApp/openhab/definitions/__init__.py index c91cfa5b..d013600a 100644 --- a/HABApp/openhab/definitions/__init__.py +++ b/HABApp/openhab/definitions/__init__.py @@ -2,4 +2,6 @@ 'String', 'Number', 'Switch', 'Contact', 'Dimmer', 'Rollershutter', 'Color', 'Contact', 'DateTime', 'Location', 'Player', 'Group'] -GROUP_FUNCTIONS = ['AND', 'OR', 'NAND', 'NOR', 'AVG', 'MAX', 'MIN', 'SUM'] \ No newline at end of file +GROUP_FUNCTIONS = ['AND', 'OR', 'NAND', 'NOR', 'AVG', 'MAX', 'MIN', 'SUM'] + +from .values import OnOffValue, PercentValue, UpDownValue, HSBValue, QuantityValue \ No newline at end of file diff --git a/HABApp/openhab/definitions/values.py b/HABApp/openhab/definitions/values.py new file mode 100644 index 00000000..5290a463 --- /dev/null +++ b/HABApp/openhab/definitions/values.py @@ -0,0 +1,60 @@ +from HABApp.core.EventBus import ComplexEventValue + + +class OnOffValue(ComplexEventValue): + ON = 'ON' + OFF = 'OFF' + + def __init__(self, value): + super().__init__(value) + assert value == OnOffValue.ON or value == OnOffValue.OFF, f'{value} ({type(value)})' + self.on = value == 'ON' + + def __str__(self): + return self.value + + +class PercentValue(ComplexEventValue): + def __init__(self, value: str): + value = float(value) + assert 0 <= value <= 100, f'{value} ({type(value)})' + super().__init__(value) + + def __str__(self): + return f'{self.value}%' + + +class UpDownValue(ComplexEventValue): + UP = 'UP' + DOWN = 'DOWN' + + def __init__(self, value): + super().__init__(value) + assert value == UpDownValue.UP or value == UpDownValue.DOWN, f'{value} ({type(value)})' + self.up = value == UpDownValue.UP + + def __str__(self): + return self.value + + +class HSBValue(ComplexEventValue): + def __init__(self, value: str): + super().__init__(tuple(float(k) for k in value.split(','))) + + def __str__(self): + return f'{self.value[0]}°,{self.value[1]}%,{self.value[2]}%' + + +class QuantityValue(ComplexEventValue): + def __init__(self, value: str): + val, unit = value.split(' ') + try: + val = int(val) + except ValueError: + val = float(val) + + super().__init__(val) + self.unit = unit + + def __str__(self): + return f'{self.value} {self.unit}' diff --git a/HABApp/openhab/events/map_events.py b/HABApp/openhab/events/map_events.py index 56251423..d96cd05a 100644 --- a/HABApp/openhab/events/map_events.py +++ b/HABApp/openhab/events/map_events.py @@ -1,5 +1,7 @@ import datetime +from ..definitions import PercentValue, UpDownValue, OnOffValue, HSBValue + def map_event_types(openhab_type: str, openhab_value: str): assert isinstance(openhab_type, str), type(openhab_type) @@ -11,9 +13,6 @@ def map_event_types(openhab_type: str, openhab_value: str): if openhab_type == "Number": return int(openhab_value) - if openhab_type == 'Percent': - return float(openhab_value) - if openhab_type == "Decimal": try: return int(openhab_value) @@ -25,6 +24,15 @@ def map_event_types(openhab_type: str, openhab_value: str): return datetime.datetime.strptime(openhab_value.replace('+', '000+'), '%Y-%m-%dT%H:%M:%S.%f%z') if openhab_type == "HSB": - return tuple(float(k) for k in openhab_value.split(',')) + return HSBValue(openhab_value) + + if openhab_type == 'OnOff': + return OnOffValue(openhab_value) + + if openhab_type == 'UpDown': + return UpDownValue(openhab_value) + + if openhab_type == 'Percent': + return PercentValue(openhab_value) return openhab_value diff --git a/HABApp/openhab/items/__init__.py b/HABApp/openhab/items/__init__.py index a3fedb8c..b5cfb22e 100644 --- a/HABApp/openhab/items/__init__.py +++ b/HABApp/openhab/items/__init__.py @@ -2,3 +2,5 @@ from .dimmer_item import DimmerItem from .rollershutter_item import RollershutterItem from .switch_item import SwitchItem +from .color_item import ColorItem +from .number_item import NumberItem \ No newline at end of file diff --git a/HABApp/openhab/items/color_item.py b/HABApp/openhab/items/color_item.py new file mode 100644 index 00000000..516fd931 --- /dev/null +++ b/HABApp/openhab/items/color_item.py @@ -0,0 +1,20 @@ +from HABApp.core.items import ColorItem as ColorItemCore +from .commands import OnOffCommand, PercentCommand +from ..definitions import OnOffValue, PercentValue, HSBValue + + +class ColorItem(ColorItemCore, OnOffCommand, PercentCommand): + + def set_value(self, hue=0.0, saturation=0.0, brightness=0.0): + + if isinstance(hue, OnOffValue): + return super().set_value(hue=None, saturation=None, brightness=100 if hue.on else 0) + elif isinstance(hue, PercentValue): + return super().set_value(hue=None, saturation=None, brightness=hue.value) + elif isinstance(hue, HSBValue): + return super().set_value(hue=hue.value) + + return super().set_value(hue=hue, saturation=saturation, brightness=brightness) + + def __str__(self): + return self.value diff --git a/HABApp/openhab/items/commands.py b/HABApp/openhab/items/commands.py new file mode 100644 index 00000000..c5ab28b0 --- /dev/null +++ b/HABApp/openhab/items/commands.py @@ -0,0 +1,39 @@ +from .. import get_openhab_interface +from ..definitions import OnOffValue, UpDownValue + + +class OnOffCommand: + + def is_on(self) -> bool: + """Test value against on-value""" + raise NotImplementedError() + + def is_off(self) -> bool: + """Test value against off-value""" + raise NotImplementedError() + + def on(self): + """Command item on""" + get_openhab_interface().send_command(self, OnOffValue.ON) + + def off(self): + """Command item off""" + get_openhab_interface().send_command(self, OnOffValue.OFF) + + + +class PercentCommand: + def percent(self, value: float): + """Command to value (in percent)""" + assert 0 <= value <= 100, value + get_openhab_interface().send_command(self, str(value)) + + +class UpDownCommand: + def up(self): + """Command up""" + get_openhab_interface().send_command(self, UpDownValue.UP) + + def down(self): + """Command down""" + get_openhab_interface().send_command(self, UpDownValue.DOWN) diff --git a/HABApp/openhab/items/contact_item.py b/HABApp/openhab/items/contact_item.py index 7aa4ae28..08de2500 100644 --- a/HABApp/openhab/items/contact_item.py +++ b/HABApp/openhab/items/contact_item.py @@ -5,27 +5,27 @@ class ContactItem(Item): OPEN = 'OPEN' CLOSED = 'CLOSED' - def set_state(self, new_state) -> bool: - if new_state is not None and new_state != ContactItem.OPEN and new_state != ContactItem.CLOSED: - raise ValueError(f'Invalid value for ContactItem: {new_state}') - return super().set_state(new_state) + def set_value(self, new_value) -> bool: + if new_value is not None and new_value != ContactItem.OPEN and new_value != ContactItem.CLOSED: + raise ValueError(f'Invalid value for ContactItem: {new_value}') + return super().set_value(new_value) def is_open(self) -> bool: """Test value against open-value""" - return True if self.state == ContactItem.OPEN else False + return self.value == ContactItem.OPEN def is_closed(self) -> bool: """Test value against closed-value""" - return True if self.state == ContactItem.CLOSED else False + return self.value == ContactItem.CLOSED def __str__(self): - return self.state + return self.value def __eq__(self, other): if isinstance(other, ContactItem): - return self.state == other.state + return self.value == other.value elif isinstance(other, str): - return self.state == other + return self.value == other elif isinstance(other, int): if other and self.is_open(): return True diff --git a/HABApp/openhab/items/dimmer_item.py b/HABApp/openhab/items/dimmer_item.py index 746fdff6..996d22dc 100644 --- a/HABApp/openhab/items/dimmer_item.py +++ b/HABApp/openhab/items/dimmer_item.py @@ -1,30 +1,32 @@ from HABApp.core.items import Item -from .. import get_openhab_interface +from .commands import OnOffCommand, PercentCommand +from ..definitions import OnOffValue, PercentValue -class DimmerItem(Item): +class DimmerItem(Item, OnOffCommand, PercentCommand): - def set_state(self, new_state) -> bool: - if new_state == 'ON': - new_state = 100 - if new_state == 'OFF': - new_state = 0 + def set_value(self, new_value) -> bool: - assert isinstance(new_state, (int, float)) or new_state is None, new_state - return super().set_state(new_state) + if isinstance(new_value, OnOffValue): + new_value = 100 if new_value.on else 0 + elif isinstance(new_value, PercentValue): + new_value = new_value.value - def on(self): - """Switch on""" - get_openhab_interface().send_command(self.name, 'ON') + # Percent is 0 ... 100 + if isinstance(new_value, (int, float)): + assert 0 <= new_value <= 100, new_value + else: + assert new_value is None, new_value - def off(self): - """Switch off""" - get_openhab_interface().send_command(self.name, 'OFF') - - def percent(self, value: float): - """Command dimmer to value (in percent)""" - assert 0 <= value <= 100 - get_openhab_interface().send_command(self.name, str(value)) + return super().set_value(new_value) def __str__(self): - return self.state + return self.value + + def is_on(self) -> bool: + """Test value against on-value""" + return bool(self.value) + + def is_off(self) -> bool: + """Test value against off-value""" + return not bool(self.value) diff --git a/HABApp/openhab/items/map_items.py b/HABApp/openhab/items/map_items.py index 1c6ce15e..30101425 100644 --- a/HABApp/openhab/items/map_items.py +++ b/HABApp/openhab/items/map_items.py @@ -1,7 +1,7 @@ import datetime -from HABApp.core.items import Item, ColorItem -from . import SwitchItem, ContactItem, RollershutterItem, DimmerItem +from HABApp.core.items import Item +from . import SwitchItem, ContactItem, RollershutterItem, DimmerItem, ColorItem, NumberItem def map_items(name, openhab_type : str, openhab_value : str): @@ -29,13 +29,13 @@ def map_items(name, openhab_type : str, openhab_value : str): if openhab_type == "Number": if value is None: - return Item(name, value) + return NumberItem(name, value) # Number items can be int or float try: - return Item(name, int(value)) + return NumberItem(name, int(value)) except ValueError: - return Item(name, float(value)) + return NumberItem(name, float(value)) if openhab_type == "DateTime": if value is None: diff --git a/HABApp/openhab/items/number_item.py b/HABApp/openhab/items/number_item.py new file mode 100644 index 00000000..66fadcf4 --- /dev/null +++ b/HABApp/openhab/items/number_item.py @@ -0,0 +1,12 @@ +from HABApp.core.items import Item +from ..definitions import QuantityValue + + +class NumberItem(Item): + + def set_value(self, new_value) -> bool: + + if isinstance(new_value, QuantityValue): + return super().set_value(new_value.value) + + return super().set_value(new_value) diff --git a/HABApp/openhab/items/rollershutter_item.py b/HABApp/openhab/items/rollershutter_item.py index 8613639a..102c37d9 100644 --- a/HABApp/openhab/items/rollershutter_item.py +++ b/HABApp/openhab/items/rollershutter_item.py @@ -1,33 +1,19 @@ from HABApp.core.items import Item -from .. import get_openhab_interface +from .commands import UpDownCommand, PercentCommand +from ..definitions import UpDownValue, PercentValue -class RollershutterItem(Item): +class RollershutterItem(Item, UpDownCommand, PercentCommand): - def set_state(self, new_state) -> bool: - if new_state == 'UP': - new_state = 0.0 - if new_state == 'DOWN': - new_state = 100.0 - assert isinstance(new_state, (int, float)) or new_state is None, new_state - return super().set_state(new_state) + def set_value(self, new_value) -> bool: - def up(self): - """Move shutter up""" - get_openhab_interface().send_command(self.name, 'UP') + if isinstance(new_value, UpDownValue): + new_value = 0 if new_value.up else 100 + elif isinstance(new_value, PercentValue): + new_value = new_value.value - def down(self): - """Move shutter down""" - get_openhab_interface().send_command(self.name, 'DOWN') - - def percent(self, percent: float): - """Command shutter to value (in percent) - - :param percent: target position in percent - :return: - """ - assert 0 <= percent <= 100 - get_openhab_interface().send_command(self.name, str(percent)) + assert isinstance(new_value, (int, float)) or new_value is None, new_value + return super().set_value(new_value) def __str__(self): - return self.state + return self.value diff --git a/HABApp/openhab/items/switch_item.py b/HABApp/openhab/items/switch_item.py index 729d37d0..ff2a59f2 100644 --- a/HABApp/openhab/items/switch_item.py +++ b/HABApp/openhab/items/switch_item.py @@ -1,40 +1,35 @@ from HABApp.core.items import Item -from .. import get_openhab_interface +from .commands import OnOffCommand +from ..definitions import OnOffValue -class SwitchItem(Item): - ON = 'ON' - OFF = 'OFF' +class SwitchItem(Item, OnOffCommand): - def set_state(self, new_state) -> bool: - if new_state is not None and new_state != SwitchItem.ON and new_state != SwitchItem.OFF: - raise ValueError(f'Invalid value for SwitchItem {self.name}: {new_state}') - return super().set_state(new_state) + def set_value(self, new_value) -> bool: + + if isinstance(new_value, OnOffValue): + new_value = new_value.value + + if new_value is not None and new_value != OnOffValue.ON and new_value != OnOffValue.OFF: + raise ValueError(f'Invalid value for SwitchItem {self.name}: {new_value}') + return super().set_value(new_value) def is_on(self) -> bool: """Test value against on-value""" - return True if self.state == SwitchItem.ON else False + return True if self.value == OnOffValue.ON else False def is_off(self) -> bool: """Test value against off-value""" - return True if self.state == SwitchItem.OFF else False - - def on(self): - """Command item on""" - get_openhab_interface().send_command(self.name, SwitchItem.ON) - - def off(self): - """Command item off""" - get_openhab_interface().send_command(self.name, SwitchItem.OFF) + return True if self.value == OnOffValue.OFF else False def __str__(self): - return self.state + return self.value def __eq__(self, other): if isinstance(other, SwitchItem): - return self.state == other.state + return self.value == other.value elif isinstance(other, str): - return self.state == other + return self.value == other elif isinstance(other, int): if other and self.is_on(): return True diff --git a/HABApp/openhab/oh_connection.py b/HABApp/openhab/oh_connection.py index 87de3da4..0af141be 100644 --- a/HABApp/openhab/oh_connection.py +++ b/HABApp/openhab/oh_connection.py @@ -117,7 +117,7 @@ def on_sse_event(self, event: dict): # Update Item in registry BEFORE posting to the event bus if isinstance(event, HABApp.core.events.ValueUpdateEvent): try: - HABApp.core.Items.get_item(event.name).set_state(event.value) + HABApp.core.Items.get_item(event.name).set_value(event.value) except HABApp.core.Items.ItemNotFoundException: pass @@ -148,7 +148,7 @@ async def update_all_items(self) -> int: # Since we load the items before we load the rules this should actually never happen existing_item = HABApp.core.Items.get_item(item_name) if isinstance(existing_item, new_item.__class__): - existing_item.set_state(_dict['state']) + existing_item.set_value(_dict['state']) except HABApp.core.Items.ItemNotFoundException: pass diff --git a/HABApp/openhab/oh_interface.py b/HABApp/openhab/oh_interface.py index 4ae06980..513c8057 100644 --- a/HABApp/openhab/oh_interface.py +++ b/HABApp/openhab/oh_interface.py @@ -14,14 +14,14 @@ log = logging.getLogger('HABApp.openhab.Connection') -@dataclasses.dataclass() +@dataclasses.dataclass class OpenhabItemDefinition: type: str name: str state: typing.Any - editable: bool - label = '' - category = '' + label: str = '' + category: str = '' + editable: bool = True tags: typing.List[str] = dataclasses.field(default_factory=list) groups: typing.List[str] = dataclasses.field(default_factory=list) members: 'typing.List[OpenhabItemDefinition]' = dataclasses.field(default_factory=list) @@ -72,10 +72,10 @@ def __convert_to_oh_type(self, _in): if isinstance(_in, datetime.datetime): return _in.strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + self._timezone elif isinstance(_in, HABApp.core.items.Item): - return str(_in.state) + return str(_in.value) elif isinstance(_in, HABApp.core.items.ColorItem): return f'{_in.hue:.1f},{_in.saturation:.1f},{_in.value:.1f}' - elif isinstance(_in, (set, list, tuple)): + elif isinstance(_in, (set, list, tuple, frozenset)): return ','.join(str(k) for k in _in) return str(_in) @@ -212,6 +212,9 @@ def item_exists(self, item_name: str): :param item_name: name """ + if not self.__connection.is_online: + return None + assert isinstance(item_name, str), type(item_name) fut = asyncio.run_coroutine_threadsafe( self.__connection.async_item_exists(item_name), @@ -229,6 +232,9 @@ def set_metadata(self, item_name: str, namespace: str, value: str, config: dict) :param config: configuration :return: """ + if not self.__connection.is_online or self.__connection.is_read_only: + return None + if isinstance(item_name, HABApp.core.items.Item): item_name = item_name.name assert isinstance(item_name, str), type(item_name) @@ -250,6 +256,9 @@ def remove_metadata(self, item_name: str, namespace: str): :param namespace: namespace :return: """ + if not self.__connection.is_online or self.__connection.is_read_only: + return None + if isinstance(item_name, HABApp.core.items.Item): item_name = item_name.name assert isinstance(item_name, str), type(item_name) diff --git a/HABApp/rule/rule.py b/HABApp/rule/rule.py index 552195a3..4c597e5a 100644 --- a/HABApp/rule/rule.py +++ b/HABApp/rule/rule.py @@ -119,10 +119,10 @@ def get_item_state(self, name: str, default=None) -> typing.Any: :return: state of the specified item """ if default is None: - return HABApp.core.Items.get_item(name).state + return HABApp.core.Items.get_item(name).value try: - state = HABApp.core.Items.get_item(name).state + state = HABApp.core.Items.get_item(name).value except HABApp.core.Items.ItemNotFoundException: return default @@ -149,7 +149,7 @@ def set_item_state(self, name: str, value: typing.Any): assert isinstance(name, HABApp.core.items.Item) item = name - item.post_state(value) + item.post_value(value) return None def item_watch(self, name: typing.Union[str, HABApp.core.items.Item], diff --git a/HABApp/rule/watched_item.py b/HABApp/rule/watched_item.py index d2f2bc03..d7c4c7f2 100644 --- a/HABApp/rule/watched_item.py +++ b/HABApp/rule/watched_item.py @@ -39,7 +39,7 @@ def check(self, now): EventBus.post_event( self.name, (ValueNoChangeEvent if self.__watch_only_changes else ValueNoUpdateEvent)( - self.name, item.state, int(duration.total_seconds()) + self.name, item.value, int(duration.total_seconds()) ) ) self.executed = True diff --git a/HABApp/util/__init__.py b/HABApp/util/__init__.py index 968f24ea..903fbeee 100644 --- a/HABApp/util/__init__.py +++ b/HABApp/util/__init__.py @@ -2,7 +2,7 @@ from .timeframe import TimeFrame -from .counter import Counter +from .counter_item import CounterItem from .period_counter import PeriodCounter from .threshold import Threshold from .statistics import Statistics diff --git a/HABApp/util/counter.py b/HABApp/util/counter.py deleted file mode 100644 index 2553d193..00000000 --- a/HABApp/util/counter.py +++ /dev/null @@ -1,69 +0,0 @@ -import threading - - -class Counter: - """Implements a thread safe counter""" - - def __init__(self, initial_value: int = 0, on_change = None): - """ - - :param initial_value: Initial value of the counter - :param on_change: Function which will be called when the counter changes. - First argument will be the new counter value. - Function will also be called when the Counter gets created. - """ - assert isinstance(initial_value, int) - - self.__lock = threading.Lock() - - self.__initial_value = initial_value - self.__value = self.__initial_value - - # func which gets called when the counter changes - self.on_change = on_change - if self.on_change: - self.on_change(self.__value) - - @property - def value(self) -> int: - """Current value""" - return self.__value - - def reset(self): - """Reset value to initial value""" - with self.__lock: - changed = self.__value != self.__initial_value - self.__value = self.__initial_value - value = self.__value - - if changed and self.on_change: - self.on_change(value) - - def increase(self, step=1) -> int: - """Increase value - - :param step: increase by this value, default = 1 - :return: value of the counter - """ - with self.__lock: - self.__value += step - value = self.__value - - if self.on_change: - self.on_change(value) - return value - - def decrease(self, step=1) -> int: - """Decrease value - - :param step: decrease by this value, default = 1 - :return: value of the counter - """ - with self.__lock: - self.__value -= step - value = self.__value - - if self.on_change: - self.on_change(value) - - return value diff --git a/HABApp/util/counter_item.py b/HABApp/util/counter_item.py new file mode 100644 index 00000000..ceeecf12 --- /dev/null +++ b/HABApp/util/counter_item.py @@ -0,0 +1,57 @@ +from HABApp.core.items import Item +from threading import Lock + + +class CounterItem(Item): + """Implements a simple thread safe counter""" + + # todo: Max Value and events when counter is 0 or has max value + + def __init__(self, name: str, initial_value: int = 0): + """ + :param initial_value: Initial value of the counter + """ + + self.value: int = initial_value # this gets overwritten but we provide a type hint anyway + + super().__init__(name=name, initial_value=initial_value) + assert isinstance(initial_value, int), type(initial_value) + + self.__lock: Lock = Lock() + self.__initial_value = initial_value + + def set_value(self, new_value) -> bool: + assert isinstance(new_value, int), type(new_value) + return super().set_value(new_value) + + def post_value(self, new_value): + assert isinstance(new_value, int), type(new_value) + super().post_value(new_value) + + def reset(self): + """Reset value to initial value""" + with self.__lock: + self.post_value(self.__initial_value) + return self.__initial_value + + def increase(self, step=1) -> int: + """Increase value + + :param step: increase by this value, default = 1 + :return: value of the counter + """ + assert isinstance(step, int), type(step) + with self.__lock: + self.post_value(self.value + step) + return self.value + + def decrease(self, step=1) -> int: + """Decrease value + + :param step: decrease by this value, default = 1 + :return: value of the counter + """ + assert isinstance(step, int), type(step) + with self.__lock: + self.post_value(self.value - step) + return self.value diff --git a/HABApp/util/multimode_item.py b/HABApp/util/multimode_item.py index f89dbf40..1aedb5dc 100644 --- a/HABApp/util/multimode_item.py +++ b/HABApp/util/multimode_item.py @@ -29,6 +29,7 @@ def __init__(self, parent, name: str, initial_value=None, auto_disable_on=None, assert isinstance(parent, MultiModeItem), type(parent) assert isinstance(name, str), type(name) + self.__lower_priority_mode: typing.Optional[MultiModeValue] = None self.__parent: MultiModeItem = parent self.__name = name @@ -50,6 +51,10 @@ def __init__(self, parent, name: str, initial_value=None, auto_disable_on=None, self.calc_value_func: typing.Optional[typing.Callable[[typing.Any, typing.Any], typing.Any]] = calc_value_func + def _set_lower_priority_mode(self, value): + assert isinstance(value, MultiModeValue) or value is None, type(value) + self.__lower_priority_mode = value + @property def value(self): """Returns the current value""" @@ -96,7 +101,18 @@ def __operator_on_value(self, operator_str: str, low_prio_value): f'{self.__value}({type(self.__value)})') return False - def calculate_value(self, value_with_lower_priority): + def calculate_lower_priority_value(self) -> typing.Any: + + # go down to the first item an trigger recalculation, this way we keep the output of MultiModeValue synchronized + if self.__lower_priority_mode is None: + self.__parent.calculate_value() + return None + + lower_mode = self.__lower_priority_mode + low_prio_value = lower_mode.calculate_lower_priority_value() + return lower_mode.calculate_value(low_prio_value) + + def calculate_value(self, value_with_lower_priority: typing.Any) -> typing.Any: # so we don't spam the log if we are already disabled if not self.__enabled: @@ -140,8 +156,8 @@ def get_create_item(cls, name: str, logger: logging.getLoggerClass() = None): item.logger = logger return item - def __init__(self, name: str, state=None): - super().__init__(name=name, state=state) + def __init__(self, name: str, initial_value=None): + super().__init__(name=name, initial_value=initial_value) self.__values_by_prio: typing.Dict[int, MultiModeValue] = {} self.__values_by_name: typing.Dict[str, MultiModeValue] = {} @@ -185,6 +201,13 @@ def create_mode( ) self.__values_by_prio[priority] = ret self.__values_by_name[name.lower()] = ret + + # make the lower priority known to the mode + low = None + for priority, child in sorted(self.__values_by_prio.items()): # type: int, MultiModeValue + child._set_lower_priority_mode(low) + low = child + return ret def get_mode(self, name: str) -> MultiModeValue: @@ -195,18 +218,6 @@ def get_mode(self, name: str) -> MultiModeValue: """ return self.__values_by_name[name.lower()] - def get_value_until(self, mode_to_stop): - assert isinstance(mode_to_stop, MultiModeValue), type(mode_to_stop) - new_value = None - with self.__lock: - for priority, child in sorted(self.__values_by_prio.items()): - if child is mode_to_stop: - return new_value - - assert isinstance(child, MultiModeValue) - new_value = child.calculate_value(new_value) - raise ValueError() - def calculate_value(self) -> typing.Any: """Recalculate the output value and post the state to the event bus (if it is not None) @@ -220,7 +231,6 @@ def calculate_value(self) -> typing.Any: assert isinstance(child, MultiModeValue) new_value = child.calculate_value(new_value) - # Notify that the value has changed if new_value is not None: - self.post_state(new_value) + self.post_value(new_value) return new_value diff --git a/HABApp/util/statistics.py b/HABApp/util/statistics.py index 3458760c..95e8749b 100644 --- a/HABApp/util/statistics.py +++ b/HABApp/util/statistics.py @@ -4,19 +4,20 @@ class Statistics: + """Calculate mathematical statistics of numerical values. + + :ivar sum: sum of all values + :ivar min: minimum of all values + :ivar max: maximum of all values + :ivar mean: mean of all values + :ivar median: median of all values + :ivar last_value: last added value + :ivar last_change: timestamp the last time a value was added + """ def __init__(self, max_age=None, max_samples=None): - """Calculate mathematical statistics of numerical values. - + """ :param max_age: Maximum age of values in seconds :param max_samples: Maximum amount of samples which will be kept - - :ivar sum: sum of all values - :ivar min: minimum of all values - :ivar max: maximum of all values - :ivar mean: mean of all values - :ivar median: median of all values - :ivar last_value: last added value - :ivar last_change: timestamp the last time a value was added """ if max_age is None and max_samples is None: @@ -76,7 +77,7 @@ def update(self): def add_value(self, value): """Add a new value and recalculate statistical values - :param value: + :param value: new value """ assert isinstance(value, (int, float)), type(value) diff --git a/_doc/conf.py b/_doc/conf.py index ad6cee42..272ec550 100644 --- a/_doc/conf.py +++ b/_doc/conf.py @@ -213,7 +213,12 @@ execute_code_working_dir = pathlib.Path(__file__).parent.parent # Skip documentation for overloaded .set_state functions -RE_SKIP = re.compile(r'\w+Item.set_state', re.IGNORECASE) +RE_SKIP = ( + re.compile(r'(\w+Item).(?:set|post)_value', re.IGNORECASE), +) +IGNORE_SKIP = ( + 'ColorItem', +) def skip_member(app, what, name, obj, skip, options): @@ -225,9 +230,15 @@ def skip_member(app, what, name, obj, skip, options): if skip: return skip - if RE_SKIP.search(str(obj)): - print( f'Skipping autodoc for {str(obj).split(" ")[1]}') - return True + for regex in RE_SKIP: + m = regex.search(str(obj)) + if m: + # make it possible to ignore skipping + if m.group(1) in IGNORE_SKIP: + continue + + print(f'Skipping autodoc for {str(obj).split(" ")[1]}') + return True return skip diff --git a/_doc/configuration.rst b/_doc/configuration.rst index 6e6286fe..542e7430 100644 --- a/_doc/configuration.rst +++ b/_doc/configuration.rst @@ -28,15 +28,18 @@ Configuration contents # and get the updated value back in milliseconds item: 'HABApp_Ping' # Name of the item interval: 10 # Seconds between two pings + connection: host: localhost port: 8080 user: '' password: '' + general: listen_only: False # If True HABApp will not change any value on the openhab instance. # Useful for testing rules from another machine. - + + mqtt: connection: client_id: HABApp @@ -56,3 +59,7 @@ Configuration contents publish: qos: 0 # Default QoS when publishing values retain: false # Default retain flag when publishing values + + general: + listen_only: False # If True HABApp will not publish any value to the broker. + # Useful for testing rules from another machine. diff --git a/_doc/interface_mqtt.rst b/_doc/interface_mqtt.rst index 191b85b7..8914d3a6 100644 --- a/_doc/interface_mqtt.rst +++ b/_doc/interface_mqtt.rst @@ -3,15 +3,17 @@ MQTT ================================== -Interaction with a MQTT broker ------------------------------- -Interaction with the MQTT broker is done through the ``self.mqtt`` object in the rule. +Interaction with the MQTT broker +--------------------------------- +Interaction with the MQTT broker is done through the ``self.mqtt`` object in the rule or through +the :class:`~HABApp.mqtt.items.MqttItem`. When receiving a topic for the first time a new :class:`~HABApp.mqtt.items.MqttItem` +will automatically be created. .. image:: /gifs/mqtt.gif -Function parameters +Rule Interface ------------------------------ .. py:class:: mqtt @@ -42,27 +44,38 @@ Function parameters :return: 0 if successful -MQTT item types +MqttItem ------------------------------ -Mqtt items have a publish method which make interaction with the mqtt broker easier. +Mqtt items have an additional publish method which make interaction with the mqtt broker easier. + +.. execute_code:: + :hide_output: + + # hide + import HABApp + from unittest.mock import MagicMock + HABApp.mqtt.mqtt_interface.MQTT_INTERFACE = MagicMock() + # hide -Example:: + from HABApp.mqtt.items import MqttItem - # items can be created manually or will be automatically created when the first mqtt message is received - my_mqtt_item = self.create_item('test/topic', HABApp.mqtt.items.MqttItem) - assert isinstance(my_mqtt_item, HABApp.mqtt.items.MqttItem) + # items can be created manually or will be automatically + # created when the first mqtt message is received + my_mqtt_item = MqttItem.get_create_item('test/topic') - # easy publish + # easy to publish values my_mqtt_item.publish('new_value') # comparing the item to get the state works, too if my_mqtt_item == 'test': - # do something + pass # do something + .. autoclass:: HABApp.mqtt.items.MqttItem :members: - + :inherited-members: + :member-order: groupwise Example MQTT rule diff --git a/_doc/interface_openhab.rst b/_doc/interface_openhab.rst index 196ff980..ab9f94c0 100644 --- a/_doc/interface_openhab.rst +++ b/_doc/interface_openhab.rst @@ -21,31 +21,70 @@ Function parameters Openhab item types ------------------------------ -Openhab items are mapped to special classes and provide convenience functions +Openhab items are mapped to special classes and provide convenience functions. -Examples:: +Example: - my_contact = self.get_item('MyContact') +.. execute_code:: + + # hide + import HABApp + from HABApp.openhab.items import ContactItem, SwitchItem + ContactItem.get_create_item('MyContact', initial_value='OPEN') + SwitchItem.get_create_item('MySwitch', initial_value='OFF') + # hide + + my_contact = ContactItem.get_item('MyContact') if my_contact.is_open(): - pass + print('Contact is open!') - my_switch = self.get_item('MySwitch') + my_switch = SwitchItem.get_item('MySwitch') if my_switch.is_on(): my_switch.off() - +.. list-table:: + :widths: auto + :header-rows: 1 + + * - Openhab type + - HABApp class + * - ``Contact`` + - :class:`~HABApp.openhab.items.ContactItem` + * - ``Switch`` + - :class:`~HABApp.openhab.items.SwitchItem` + * - ``Dimmer`` + - :class:`~HABApp.openhab.items.DimmerItem` + * - ``Rollershutter`` + - :class:`~HABApp.openhab.items.RollershutterItem` + * - ``Color`` + - :class:`~HABApp.openhab.items.ColorItem` .. autoclass:: HABApp.openhab.items.ContactItem :members: - + :inherited-members: + :member-order: groupwise + + .. autoclass:: HABApp.openhab.items.SwitchItem :members: + :inherited-members: + :member-order: groupwise .. autoclass:: HABApp.openhab.items.DimmerItem :members: + :inherited-members: + :member-order: groupwise .. autoclass:: HABApp.openhab.items.RollershutterItem :members: + :inherited-members: + :member-order: groupwise + +.. autoclass:: HABApp.openhab.items.ColorItem + :members: + :inherited-members: + :member-order: groupwise + diff --git a/_doc/rule.rst b/_doc/rule.rst index e77e9838..72f73c8a 100644 --- a/_doc/rule.rst +++ b/_doc/rule.rst @@ -21,10 +21,18 @@ Items from MQTT use the topic as item name and get created as soon as a message Some item types provide convenience functions, so it is advised to always set the correct item type. -The preferred way to interact with items is through the class factory `get_create_item` since this provides type hints:: +The preferred way to get and create items is through the class factories :class:`~HABApp.core.items.Item.get_item` +and :class:`~HABApp.core.items.Item.get_create_item` since this ensures the proper item class and provides type hints when +using an IDE: + +.. execute_code:: + :hide_output: from HABApp.core.items import Item - my_item = Item.get_create_item('MyItem') + my_item = Item.get_create_item('MyItem', initial_value=5) + my_item = Item.get_item('MyItem') + print(my_item) + If an item value gets set there will be a :class:`~HABApp.core.ValueUpdateEvent` on the event bus. If it changes there will be additionally a :class:`~HABApp.core.ValueChangeEvent`, too. @@ -59,17 +67,27 @@ If it changes there will be additionally a :class:`~HABApp.core.ValueChangeEvent * - :meth:`~HABApp.Rule.item_watch_and_listen` - Convenience function which combines :class:`~HABApp.Rule.item_watch` and :class:`~HABApp.Rule.listen_event` -It is possible to check the item value by comparing it:: +It is possible to check the item value by comparing it - my_item = self.get_item('MyItem') +.. execute_code:: + :hide_output: + + # hide + from HABApp.core.items import Item + Item.get_create_item('MyItem', initial_value=5) + # hide + + from HABApp.core.items import Item + my_item = Item.get_item('MyItem') # this works if my_item == 5: - # do sth + pass # do something + + # and is the same as this + if my_item.value == 5: + pass # do something - # and is the same as - if my_item.state == 5: - # do sth .. autoclass:: HABApp.core.items.Item :members: diff --git a/_doc/util.rst b/_doc/util.rst index 62ed1236..3bda6bb5 100644 --- a/_doc/util.rst +++ b/_doc/util.rst @@ -7,27 +7,22 @@ util - helpers and utilities The util package contains useful classes which make rule creation easier. -Counter +CounterItem ------------------------------ Example ^^^^^^^^^^^^^^^^^^ .. execute_code:: - # hide - from HABApp.util import Counter - # hide - - def print_value(val): - print( f'Counter is {val}') - - c = Counter( initial_value=5, on_change=print_value) - c.increase() - c.decrease() + from HABApp.util import CounterItem + c = CounterItem.get_create_item('MyCounter', initial_value=5) + print(c.increase()) + print(c.decrease()) + print(c.reset()) Documentation ^^^^^^^^^^^^^^^^^^ -.. autoclass:: Counter +.. autoclass:: CounterItem :members: .. automethod:: __init__ diff --git a/conf/rules/mqtt_rule.py b/conf/rules/mqtt_rule.py index d372f67c..0ac95022 100644 --- a/conf/rules/mqtt_rule.py +++ b/conf/rules/mqtt_rule.py @@ -15,9 +15,6 @@ def __init__(self): callback=self.publish_rand_value ) - # these two methods have the same effect. - # If item_factory is omitted self.get_item will raise an error if the item does not exist instead of creating it - self.my_mqtt_item: MqttItem = self.get_item('test/test', item_factory=MqttItem) self.my_mqtt_item = MqttItem.get_create_item('test/test') self.listen_event('test/test', self.topic_updated, ValueUpdateEvent) diff --git a/conf_testing/config.yml b/conf_testing/config.yml index b6ed1f0c..33324927 100644 --- a/conf_testing/config.yml +++ b/conf_testing/config.yml @@ -2,11 +2,11 @@ directories: logging: ../conf/log rules: rules lib: lib - param: param + param: parameters mqtt: connection: client_id: HABApp - host: 'localhost' + host: localhost password: '' port: 1883 tls: false @@ -20,6 +20,8 @@ mqtt: topics: - '#' - 0 + general: + listen_only: false openhab: connection: host: localhost @@ -31,4 +33,4 @@ openhab: ping: enabled: true interval: 10 - item: 'Ping' + item: Ping diff --git a/conf_testing/lib/HABAppTests/item_waiter.py b/conf_testing/lib/HABAppTests/item_waiter.py index c64b4ce5..ce797f17 100644 --- a/conf_testing/lib/HABAppTests/item_waiter.py +++ b/conf_testing/lib/HABAppTests/item_waiter.py @@ -24,12 +24,12 @@ def wait_for_state(self, state=None): while True: time.sleep(0.01) - if (self.item if self.item_compare else self.item.state) == state: + if (self.item if self.item_compare else self.item.value) == state: return True if time.time() > start + self.timeout: self.states_ok = False - log.error(f'Timeout waiting for {self.item.name} {get_equal_text(state, self.item.state)}') + log.error(f'Timeout waiting for {self.item.name} {get_equal_text(state, self.item.value)}') return False raise ValueError() diff --git a/conf_testing/lib/HABAppTests/openhab_tmp_item.py b/conf_testing/lib/HABAppTests/openhab_tmp_item.py index bd7cbd72..bbe72380 100644 --- a/conf_testing/lib/HABAppTests/openhab_tmp_item.py +++ b/conf_testing/lib/HABAppTests/openhab_tmp_item.py @@ -1,10 +1,10 @@ import random import string +import time import HABApp - class OpenhabTmpItem: def __init__(self, item_name, item_type): self.item_name = item_name @@ -19,6 +19,13 @@ def __enter__(self) -> HABApp.core.items.Item: if not interface.item_exists(self.item_name): interface.create_item(self.item_type, self.item_name) + # wait max 1 sec for the item to be created + stop = time.time() + 1 + while not HABApp.core.Items.item_exists(self.item_name): + time.sleep(0.01) + if time.time() > stop: + break + return HABApp.core.Items.get_item(self.item_name) def __exit__(self, exc_type, exc_val, exc_tb): diff --git a/conf_testing/lib/HABAppTests/test_base.py b/conf_testing/lib/HABAppTests/test_base.py index 4ff87576..5e6ccc53 100644 --- a/conf_testing/lib/HABAppTests/test_base.py +++ b/conf_testing/lib/HABAppTests/test_base.py @@ -63,6 +63,8 @@ def add_test(self, name, func, *args, **kwargs): def run_tests(self, result: TestResult): test_count = len(self.__tests_funcs) + log.info(f'Running {test_count} tests for {self.rule_name}') + width = test_count // 10 + 1 test_current = 0 for name, test_data in self.__tests_funcs.items(): diff --git a/conf_testing/lib/HABAppTests/test_data.py b/conf_testing/lib/HABAppTests/test_data.py index fdd8f354..14813c5d 100644 --- a/conf_testing/lib/HABAppTests/test_data.py +++ b/conf_testing/lib/HABAppTests/test_data.py @@ -7,7 +7,7 @@ now = now.replace(microsecond=now.microsecond // 1000 * 1000) ITEM_STATE = { - 'Color': [(1, 2, 3)], + 'Color': [(1, 2, 3), (2.0, 3.0, 4.0)], 'Contact': ['OPEN', 'CLOSED'], 'DateTime': [ now, @@ -24,7 +24,9 @@ } ITEM_EVENTS = { + 'Switch': ['ON', 'OFF'], 'Dimmer': ['ON', 'OFF'], + 'Color': ['ON', 'OFF'], 'Rollershutter': ['UP', 'DOWN'], } diff --git a/conf_testing/logging.yml b/conf_testing/logging.yml index 69105526..d5804d70 100644 --- a/conf_testing/logging.yml +++ b/conf_testing/logging.yml @@ -9,7 +9,7 @@ formatters: handlers: HABApp_default: class: logging.handlers.RotatingFileHandler - filename: 'z:/Python/HABApp/conf/log/HABApp.log' + filename: 'HABApp.log' maxBytes: 10_000_000 backupCount: '3' @@ -18,7 +18,7 @@ handlers: EventFile: class: logging.handlers.RotatingFileHandler - filename: 'z:/Python/HABApp/conf/log/events.log' + filename: 'events.log' maxBytes: 10_485_760 backupCount: 3 @@ -27,7 +27,7 @@ handlers: BufferEventFile: class: logging.handlers.MemoryHandler - capacity: 10 + capacity: 0 formatter: HABApp_format target: EventFile level: DEBUG diff --git a/conf_testing/rules/test_openhab_interface.py b/conf_testing/rules/test_openhab_interface.py index 0f2b78d1..a7860ebb 100644 --- a/conf_testing/rules/test_openhab_interface.py +++ b/conf_testing/rules/test_openhab_interface.py @@ -79,7 +79,7 @@ def test_umlaute(self): self.openhab.create_item('String', NAME, label=LABEL) ret = self.openhab.get_item(NAME) - assert ret.label == LABEL + assert ret.label == LABEL, f'"{LABEL}" != "{ret.label}"' def test_openhab_item_not_found(self): test_item = ''.join(random.choice(string.ascii_letters) for _ in range(20)) diff --git a/conf_testing/rules/test_openhab_item_funcs.py b/conf_testing/rules/test_openhab_item_funcs.py index 891163c1..06edd7e2 100644 --- a/conf_testing/rules/test_openhab_item_funcs.py +++ b/conf_testing/rules/test_openhab_item_funcs.py @@ -2,7 +2,7 @@ import logging import typing -from HABApp.openhab.items import SwitchItem, RollershutterItem, DimmerItem +from HABApp.openhab.items import SwitchItem, RollershutterItem, DimmerItem, ColorItem from HABAppTests import TestBaseRule, ItemWaiter, OpenhabTmpItem log = logging.getLogger('HABApp.Test') @@ -28,6 +28,11 @@ def __init__(self): [TestParam('percent', 55.5, 55.5), TestParam('up', 0), TestParam('down', 100)]) self.add_test('DimmerItem', self.test_item, DimmerItem, [TestParam('percent', 55.5, 55.5), TestParam('on', 100), TestParam('off', 0)]) + self.add_test('ColorItem', self.test_item, ColorItem, [ + TestParam('on', (None, None, 100)), + TestParam('off', (None, None, 0)), + TestParam('percent', (None, None, 55.5), 55.5) + ]) def test_item(self, item_type, test_params): @@ -47,7 +52,7 @@ def test_item(self, item_type, test_params): log.info(f'{item_type}.{test_param.func_name}() is ok!') # reset state so we don't get false positives - item.set_state(None) + item.set_value(None) test_ok = waiter.states_ok diff --git a/conf_testing/rules/test_parameter_files.py b/conf_testing/rules/test_parameter_files.py index b1ac4829..8066004f 100644 --- a/conf_testing/rules/test_parameter_files.py +++ b/conf_testing/rules/test_parameter_files.py @@ -7,10 +7,11 @@ # User Parameter files to create rules dynamically try: - assert HABApp.parameters.get_parameter_value('param_file', 'key') != 10, \ + assert HABApp.Parameter('param_file', 'key') == 10, \ f'Loading of Parameters does not work properly' except Exception as e: log.error(e) + log.error(HABApp.Parameter('param_file', 'key')) class TestParamFile(TestBaseRule): @@ -22,7 +23,7 @@ def __init__(self): self.add_test('ParamFile', self.test_param_file) def test_param_file(self): - p = HABApp.parameters.Parameter('param_file', 'key') + p = HABApp.Parameter('param_file', 'key') assert p < 11 assert p.value == 10 return True diff --git a/setup.py b/setup.py index 07f4b06d..74a67a6e 100644 --- a/setup.py +++ b/setup.py @@ -39,7 +39,7 @@ }, packages=setuptools.find_packages(exclude=['tests*']), install_requires=[ - 'ruamel.yaml>=0.16.1', + 'easyco>=0.2', 'aiohttp>=3.5.4', 'voluptuous>=0.11.7', 'aiohttp-sse-client', diff --git a/tests/test_core/__init__.py b/tests/test_core/__init__.py index e69de29b..e29320c9 100644 --- a/tests/test_core/__init__.py +++ b/tests/test_core/__init__.py @@ -0,0 +1 @@ +from .test_items import ItemTests \ No newline at end of file diff --git a/tests/test_core/test_items/__init__.py b/tests/test_core/test_items/__init__.py index e69de29b..1b1a5ae1 100644 --- a/tests/test_core/test_items/__init__.py +++ b/tests/test_core/test_items/__init__.py @@ -0,0 +1 @@ +from .tests_all_items import ItemTests \ No newline at end of file diff --git a/tests/test_core/test_items/test_item.py b/tests/test_core/test_items/test_item.py index 101176ba..c56a22a3 100644 --- a/tests/test_core/test_items/test_item.py +++ b/tests/test_core/test_items/test_item.py @@ -2,6 +2,13 @@ from datetime import datetime, timedelta from HABApp.core.items import Item +from . import ItemTests + + +class TestItem(ItemTests): + CLS = Item + TEST_VALUES = [0, 'str', (1, 2, 3)] + TEST_CREATE_ITEM = {'initial_value': 0} class TestCasesItem(unittest.TestCase): @@ -12,20 +19,20 @@ def test_repr(self): def test_time_update(self): i = Item('test') - i.set_state('test') + i.set_value('test') i.last_change = datetime.now() - timedelta(seconds=5) i.last_update = datetime.now() - timedelta(seconds=5) - i.set_state('test') + i.set_value('test') self.assertGreater(i.last_update, datetime.now() - timedelta(milliseconds=100)) self.assertLess(i.last_change, datetime.now() - timedelta(milliseconds=100)) def test_time_change(self): i = Item('test') - i.set_state('test') + i.set_value('test') i.last_change = datetime.now() - timedelta(seconds=5) i.last_update = datetime.now() - timedelta(seconds=5) - i.set_state('test1') + i.set_value('test1') self.assertGreater(i.last_update, datetime.now() - timedelta(milliseconds=100)) self.assertGreater(i.last_change, datetime.now() - timedelta(milliseconds=100)) diff --git a/tests/test_core/test_items/test_item_color.py b/tests/test_core/test_items/test_item_color.py index e0c539cd..db772de9 100644 --- a/tests/test_core/test_items/test_item_color.py +++ b/tests/test_core/test_items/test_item_color.py @@ -12,29 +12,29 @@ def test_set_func(self): i = ColorItem('test') self.assertEqual(i.hue, 0) self.assertEqual(i.saturation, 0) - self.assertEqual(i.value, 0) + self.assertEqual(i.brightness, 0) - i.set_state(30, 50, 70) + i.set_value(30, 50, 70) self.assertEqual(i.hue, 30) self.assertEqual(i.saturation, 50) - self.assertEqual(i.value, 70) + self.assertEqual(i.brightness, 70) + self.assertEqual(i.value, (30, 50, 70)) - self.assertEqual(i.state, (30, 50, 70)) def test_set_func_touple(self): i = ColorItem('test') self.assertEqual(i.hue, 0) self.assertEqual(i.saturation, 0) - self.assertEqual(i.value, 0) + self.assertEqual(i.brightness, 0) - i.set_state((22, 33.3, 77), None) + i.set_value((22, 33.3, 77), None) self.assertEqual(i.hue, 22) self.assertEqual(i.saturation, 33.3) - self.assertEqual(i.value, 77) + self.assertEqual(i.brightness, 77) + self.assertEqual(i.value, (22, 33.3, 77)) - self.assertEqual(i.state, (22, 33.3, 77)) def test_rgb_to_hsv(self): i = ColorItem('test') @@ -42,7 +42,7 @@ def test_rgb_to_hsv(self): self.assertEqual(int(i.hue), 333) self.assertEqual(int(i.saturation), 87) - self.assertEqual(int(i.value), 75) + self.assertEqual(int(i.brightness), 75) def test_hsv_to_rgb(self): i = ColorItem('test', 23, 44, 66) diff --git a/tests/test_core/test_items/tests_all_items.py b/tests/test_core/test_items/tests_all_items.py new file mode 100644 index 00000000..b4e29aed --- /dev/null +++ b/tests/test_core/test_items/tests_all_items.py @@ -0,0 +1,59 @@ +import typing +from datetime import datetime, timedelta + +from HABApp.core import Items +from HABApp.core.items import Item + + +class ItemTests: + CLS: typing.Type[Item] = None + TEST_VALUES = [] + TEST_CREATE_ITEM = {} + + def test_test_params(self): + assert self.CLS is not None + assert self.TEST_VALUES, type(self) + + def test_factories(self): + cls = self.CLS + + ITEM_NAME = 'testitem' + if Items.item_exists(ITEM_NAME): + Items.pop_item(ITEM_NAME) + + c = cls.get_create_item(name=ITEM_NAME, **self.TEST_CREATE_ITEM) + assert isinstance(c, cls) + + assert isinstance(cls.get_item(name=ITEM_NAME), cls) + + + def test_var_names(self): + item = self.CLS('test') + # assert item.value is None, f'{item.value} ({type(item.value)})' + + item.set_value(self.TEST_VALUES[0]) + assert item.value == self.TEST_VALUES[0] + + item.post_value(self.TEST_VALUES[0]) + item.get_value(default_value='asdf') + + def test_time_value_update(self): + for value in self.TEST_VALUES: + i = self.CLS('test') + i.set_value(value) + i.last_change = datetime.now() - timedelta(seconds=5) + i.last_update = datetime.now() - timedelta(seconds=5) + i.set_value(value) + + assert i.last_update > datetime.now() - timedelta(milliseconds=100) + assert i.last_change < datetime.now() - timedelta(milliseconds=100) + + def test_time_value_change(self): + i = self.CLS('test') + for value in self.TEST_VALUES: + i.last_change = datetime.now() - timedelta(seconds=5) + i.last_update = datetime.now() - timedelta(seconds=5) + i.set_value(value) + + assert i.last_update > datetime.now() - timedelta(milliseconds=100) + assert i.last_change > datetime.now() - timedelta(milliseconds=100) diff --git a/tests/test_openhab/test_items/__init__.py b/tests/test_openhab/test_items/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_openhab/test_items/test_commands.py b/tests/test_openhab/test_items/test_commands.py new file mode 100644 index 00000000..fbfea0fc --- /dev/null +++ b/tests/test_openhab/test_items/test_commands.py @@ -0,0 +1,40 @@ +import pytest +import typing + +from HABApp.openhab.items import ContactItem, DimmerItem, RollershutterItem, SwitchItem, ColorItem +from HABApp.openhab.definitions import OnOffValue, UpDownValue + + +@pytest.mark.parametrize("cls", (SwitchItem, DimmerItem, ColorItem)) +def test_onoff(cls: typing.Type[SwitchItem]): + c = cls('item_name') + c.set_value(OnOffValue('ON')) + assert c.is_on() + assert not c.is_off() + + c = cls('item_name') + c.set_value(OnOffValue('OFF')) + assert c.is_off() + assert not c.is_on() + + +@pytest.mark.parametrize("cls", (RollershutterItem, )) +def test_UpDown(cls: typing.Type[RollershutterItem]): + c = cls('item_name') + c.set_value(UpDownValue('UP')) + + c = cls('item_name') + c.set_value(UpDownValue('DOWN')) + + +@pytest.mark.parametrize("cls", (ContactItem, )) +def test_OpenClosed(cls: typing.Type[ContactItem]): + c = cls('item_name') + c.set_value(ContactItem.OPEN) + assert c.is_open() + assert not c.is_closed() + + c = cls('item_name') + c.set_value(ContactItem.CLOSED) + assert c.is_closed() + assert not c.is_open() diff --git a/tests/test_openhab/test_openhab_events.py b/tests/test_openhab/test_openhab_events.py index c6a9cca9..b800abba 100644 --- a/tests/test_openhab/test_openhab_events.py +++ b/tests/test_openhab/test_openhab_events.py @@ -68,7 +68,7 @@ def test_ItemStatePredictedEvent(self): 'type': 'ItemStatePredictedEvent'}) self.assertIsInstance(event, ItemStatePredictedEvent) self.assertEqual(event.name, 'Buero_Lampe_Vorne_W') - self.assertEqual(event.value, 10.0) + self.assertEqual(event.value.value, 10.0) def test_ItemStateChangedEvent2(self): d = { diff --git a/tests/test_utils/test_counter.py b/tests/test_utils/test_counter.py new file mode 100644 index 00000000..63ab53a6 --- /dev/null +++ b/tests/test_utils/test_counter.py @@ -0,0 +1,17 @@ +from HABApp.util import CounterItem + +from ..test_core import ItemTests + + +class TestCounterItem(ItemTests): + CLS = CounterItem + TEST_VALUES = [5, -10, 10] + TEST_CREATE_ITEM = {'initial_value': 10} + + +def test_reset(): + c = CounterItem('TestItem', initial_value=10) + c.increase() + assert c.value == 11 + c.reset() + assert c.value == 10 diff --git a/tests/test_utils/test_multivalue.py b/tests/test_utils/test_multivalue.py index 171a8b57..6e80cbc2 100644 --- a/tests/test_utils/test_multivalue.py +++ b/tests/test_utils/test_multivalue.py @@ -1,5 +1,12 @@ from HABApp.util import MultiModeItem +from ..test_core import ItemTests + + +class TestMultiModeItem(ItemTests): + CLS = MultiModeItem + TEST_VALUES = [0, 'str', (1, 2, 3)] + def test_diff_prio(): p = MultiModeItem('TestItem') @@ -10,14 +17,26 @@ def test_diff_prio(): p2 = p.get_mode('modeb') p1.set_value(5) - assert p.state == '4567' + assert p.value == '4567' p2.set_enabled(False) - assert p.state == 5 + assert p.value == 5 p2.set_enabled(True) - assert p.state == '4567' + assert p.value == '4567' p2.set_enabled(False) p2.set_value(8888) - assert p.state == 8888 + assert p.value == 8888 + + +def test_calculate_lower_priority_value(): + p = MultiModeItem('TestItem') + m1 = p.create_mode('modea', 1, '1234') + m2 = p.create_mode('modeb', 2, '4567') + + assert m1.calculate_lower_priority_value() is None + assert m2.calculate_lower_priority_value() == '1234' + + m1.set_value('asdf') + assert m2.calculate_lower_priority_value() == 'asdf' diff --git a/tox.ini b/tox.ini index 13bf402d..9ed2e386 100644 --- a/tox.ini +++ b/tox.ini @@ -8,7 +8,7 @@ envlist = [testenv] deps = - ruamel.yaml + easyco watchdog voluptuous aiohttp