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/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/oh_interface.py b/HABApp/openhab/oh_interface.py index 6737d65d..513c8057 100644 --- a/HABApp/openhab/oh_interface.py +++ b/HABApp/openhab/oh_interface.py @@ -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/_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/conf_testing/config.yml b/conf_testing/config.yml index 50fda40a..33324927 100644 --- a/conf_testing/config.yml +++ b/conf_testing/config.yml @@ -6,7 +6,7 @@ directories: 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/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/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