diff options
author | Evgeny Zinoviev <me@ch1p.io> | 2023-06-08 13:32:49 +0300 |
---|---|---|
committer | Evgeny Zinoviev <me@ch1p.io> | 2023-06-08 13:32:49 +0300 |
commit | 27234de929542daaa8a9de9a6a5a3501845e598f (patch) | |
tree | 8b3501a272efe0c2448bd69a27af1fcfe396a457 | |
parent | b8c04cb82e874a7f5ac0ad92f0bc0d110effee97 (diff) | |
parent | 994ae33a811fec7ca11ad049b56b85b4f8c2a553 (diff) |
Merge branch 'mqtt-refactoring' of ch1p.io:homekit into mqtt-refactoring
51 files changed, 332 insertions, 268 deletions
diff --git a/requirements.txt b/requirements.txt index 46f9b8c..e11d512 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,7 +14,7 @@ apscheduler~=3.9.1 psutil~=5.9.1 aioshutil~=1.1 scikit-image~=0.19.3 - +cerberus~=1.3.4 # following can be installed from debian repositories # matplotlib~=3.5.0 diff --git a/src/camera_node.py b/src/camera_node.py index d175e17..3f2c5a4 100755 --- a/src/camera_node.py +++ b/src/camera_node.py @@ -65,7 +65,7 @@ class ESP32CameraNodeServer(MediaNodeServer): if __name__ == '__main__': - config.load('camera_node') + config.load_app('camera_node') recorder_kwargs = {} camera_type = CameraType(config['camera']['type']) diff --git a/src/esp32cam_capture_diff_node.py b/src/esp32cam_capture_diff_node.py index 4363e9e..70ebd47 100755 --- a/src/esp32cam_capture_diff_node.py +++ b/src/esp32cam_capture_diff_node.py @@ -76,7 +76,7 @@ class ESP32CamCaptureDiffNode: if __name__ == '__main__': - config.load('esp32cam_capture_diff_node') + config.load_app('esp32cam_capture_diff_node') loop = asyncio.get_event_loop() ESP32CamCaptureDiffNode() diff --git a/src/gpiorelayd.py b/src/gpiorelayd.py index 85015a7..f1a9e57 100755 --- a/src/gpiorelayd.py +++ b/src/gpiorelayd.py @@ -13,7 +13,7 @@ if __name__ == '__main__': if not os.getegid() == 0: sys.exit('Must be run as root.') - config.load() + config.load_app() try: s = RelayServer(pinname=config.get('relayd.pin'), diff --git a/src/home/config/__init__.py b/src/home/config/__init__.py index cc9c091..10c5bd9 100644 --- a/src/home/config/__init__.py +++ b/src/home/config/__init__.py @@ -1 +1,11 @@ -from .config import ConfigStore, config, is_development_mode, setup_logging +from .config import ( + Config, + config, + is_development_mode, + setup_logging, + app_config +) +from .validators import validate +from ._configs import ( + LinuxBoardsConfig +)
\ No newline at end of file diff --git a/src/home/config/_configs.py b/src/home/config/_configs.py new file mode 100644 index 0000000..2f98d06 --- /dev/null +++ b/src/home/config/_configs.py @@ -0,0 +1,5 @@ +from .config import ConfigUnit + + +class LinuxBoardsConfig(ConfigUnit): + NAME = 'linux_boards' diff --git a/src/home/config/config.py b/src/home/config/config.py index 4681685..2d49524 100644 --- a/src/home/config/config.py +++ b/src/home/config/config.py @@ -3,45 +3,116 @@ import yaml import logging import os +from . import validators from os.path import join, isdir, isfile from typing import Optional, Any, MutableMapping from argparse import ArgumentParser from ..util import parse_addr -def _get_config_path(name: str) -> str: - formats = ['toml', 'yaml'] +_my_validators = {} - dirname = join(os.environ['HOME'], '.config', name) - if isdir(dirname): - for fmt in formats: - filename = join(dirname, f'config.{fmt}') - if isfile(filename): - return filename +def _get_validator(name: str) -> Optional[callable]: + if hasattr(validators, f'{name}_validator'): + return getattr(validators, f'{name}_validator') + if name in _my_validators: + return _my_validators[name] + return None - raise IOError(f'config not found in {dirname}') - else: - filenames = [join(os.environ['HOME'], '.config', f'{name}.{format}') for format in formats] - for file in filenames: - if isfile(file): - return file +def add_validator(name: str, f: callable): + _my_validators[name] = f - raise IOError(f'config not found') +class ConfigUnit: + NAME = 'dumb' -class ConfigStore: data: MutableMapping[str, Any] + + @classmethod + def get_config_path(cls, name=None) -> str: + if name is None: + name = cls.NAME + if name is None: + raise ValueError('get_config_path: name is none') + + dirnames = ( + join(os.environ['HOME'], '.config', 'homekit'), + '/etc/homekit' + ) + + for dirname in dirnames: + if isdir(dirname): + for fmt in ('toml', 'yaml'): + filename = join(dirname, f'{name}.{fmt}') + if isfile(filename): + return filename + + raise IOError(f'config \'{name}\' not found') + + def __init__(self, name=None): + self.data = {} + + if self.NAME != 'dumb': + self.load_from(self.get_config_path()) + self.validate() + + elif name is not None: + self.NAME = name + + def load_from(self, path: str): + if path.endswith('.toml'): + self.data = toml.load(path) + elif path.endswith('.yaml'): + with open(path, 'r') as fd: + self.data = yaml.safe_load(fd) + + def validate(self): + v = _get_validator(self.NAME) + v(self.data) + + def __getitem__(self, key): + return self.data[key] + + def __setitem__(self, key, value): + raise NotImplementedError('overwriting config values is prohibited') + + def __contains__(self, key): + return key in self.data + + def get(self, key: str, default=None): + cur = self.data + pts = key.split('.') + for i in range(len(pts)): + k = pts[i] + if i < len(pts)-1: + if k not in cur: + raise KeyError(f'key {k} not found') + else: + return cur[k] if k in cur else default + cur = self.data[k] + raise KeyError(f'option {key} not found') + + def get_addr(self, key: str): + return parse_addr(self.get(key)) + + def items(self): + return self.data.items() + + +class Config: app_name: Optional[str] + app_config: ConfigUnit def __init__(self): - self.data = {} self.app_name = None + self.app_config = ConfigUnit() - def load(self, name: Optional[str] = None, - use_cli=True, - parser: ArgumentParser = None): + def load_app(self, + name: Optional[str] = None, + use_cli=True, + parser: ArgumentParser = None): self.app_name = name if (name is None) and (not use_cli): @@ -75,65 +146,32 @@ class ConfigStore: log_default_fmt = args.log_default_fmt if not no_config and path is None: - path = _get_config_path(name) - - if no_config: - self.data = {} - else: - if path.endswith('.toml'): - self.data = toml.load(path) - elif path.endswith('.yaml'): - with open(path, 'r') as fd: - self.data = yaml.safe_load(fd) - - if 'logging' in self: - if not log_file and 'file' in self['logging']: - log_file = self['logging']['file'] - if log_default_fmt and 'default_fmt' in self['logging']: - log_default_fmt = self['logging']['default_fmt'] + path = ConfigUnit.get_config_path(name=name) + + if not no_config: + self.app_config.load_from(path) + + if 'logging' in self.app_config: + if not log_file and 'file' in self.app_config['logging']: + log_file = self.app_config['logging']['file'] + if log_default_fmt and 'default_fmt' in self.app_config['logging']: + log_default_fmt = self.app_config['logging']['default_fmt'] setup_logging(log_verbose, log_file, log_default_fmt) if use_cli: return args - def __getitem__(self, key): - return self.data[key] - - def __setitem__(self, key, value): - raise NotImplementedError('overwriting config values is prohibited') - - def __contains__(self, key): - return key in self.data - - def get(self, key: str, default=None): - cur = self.data - pts = key.split('.') - for i in range(len(pts)): - k = pts[i] - if i < len(pts)-1: - if k not in cur: - raise KeyError(f'key {k} not found') - else: - return cur[k] if k in cur else default - cur = self.data[k] - raise KeyError(f'option {key} not found') - - def get_addr(self, key: str): - return parse_addr(self.get(key)) - - def items(self): - return self.data.items() - -config = ConfigStore() +config = Config() +app_config = config.app_config def is_development_mode() -> bool: if 'HK_MODE' in os.environ and os.environ['HK_MODE'] == 'dev': return True - return ('logging' in config) and ('verbose' in config['logging']) and (config['logging']['verbose'] is True) + return ('logging' in config.app_config) and ('verbose' in config.app_config['logging']) and (config.app_config['logging']['verbose'] is True) def setup_logging(verbose=False, log_file=None, default_fmt=False): diff --git a/src/home/config/validators/__init__.py b/src/home/config/validators/__init__.py new file mode 100644 index 0000000..0e75132 --- /dev/null +++ b/src/home/config/validators/__init__.py @@ -0,0 +1,2 @@ +from ._validators import * +from ._util import validate diff --git a/src/home/config/validators/_util.py b/src/home/config/validators/_util.py new file mode 100644 index 0000000..5227c40 --- /dev/null +++ b/src/home/config/validators/_util.py @@ -0,0 +1,11 @@ +import inspect + +from cerberus import Validator, DocumentError + + +def validate(schema, data): + v = Validator(schema) + if not v.validate(data): + frame = inspect.currentframe().f_back + caller_name = frame.f_code.co_name + raise DocumentError(f'{caller_name}: failed to validate data: ' + v.errors) diff --git a/src/home/config/validators/_validators.py b/src/home/config/validators/_validators.py new file mode 100644 index 0000000..cddc1b0 --- /dev/null +++ b/src/home/config/validators/_validators.py @@ -0,0 +1,32 @@ +from ._util import validate + +__all__ = [ + 'linux_boards_validator' +] + + +def linux_boards_validator(data) -> None: + validate({ + 'type': 'dict', + 'valuesrules': { + 'type': 'dict', + 'schema': { + 'mdns': {'type': 'string', 'required': True}, + 'board': {'type': 'string', 'required': True}, + 'network': {'type': 'list', 'required': True, 'empty': False}, + 'ram': {'type': 'integer', 'required': True}, + 'ext_hdd': { + 'type': 'list', + 'schema': { + 'type': 'dict', + 'schema': { + 'mountpoint': {'type': 'string', 'required': True}, + 'size': {'type': 'integer', 'required': True} + } + }, + }, + 'services': {'type': 'list', 'empty': False}, + 'online': {'type': 'boolean', 'required': True} + } + } + }, data) diff --git a/src/home/mqtt/_node.py b/src/home/mqtt/_node.py index ddf5ba2..4e259a4 100644 --- a/src/home/mqtt/_node.py +++ b/src/home/mqtt/_node.py @@ -14,13 +14,17 @@ class MqttNode: _modules: List[MqttModule] _module_subscriptions: dict[str, MqttModule] _node_id: str + _node_secret: str _payload_callbacks: list[callable] _wrapper: Optional[MqttWrapper] - def __init__(self, node_id: str): + def __init__(self, + node_id: str, + node_secret: Optional[str] = None): self._modules = [] self._module_subscriptions = {} self._node_id = node_id + self._node_secret = node_secret self._payload_callbacks = [] self._logger = logging.getLogger(self.__class__.__name__) self._wrapper = None @@ -42,7 +46,7 @@ class MqttNode: payload = self._module_subscriptions[topic].handle_payload(self, topic, payload) if isinstance(payload, MqttPayload): for f in self._payload_callbacks: - f(payload) + f(self, payload) def load_module(self, module_name: str, *args, **kwargs) -> MqttModule: module = importlib.import_module(f'..module.{module_name}', __name__) @@ -78,3 +82,11 @@ class MqttNode: @property def id(self) -> str: return self._node_id + + @property + def secret(self) -> str: + return self._node_secret + + @secret.setter + def secret(self, secret: str) -> None: + self._node_secret = secret diff --git a/src/home/mqtt/_wrapper.py b/src/home/mqtt/_wrapper.py index 41f9d89..0b32197 100644 --- a/src/home/mqtt/_wrapper.py +++ b/src/home/mqtt/_wrapper.py @@ -9,11 +9,15 @@ from ..util import strgen class MqttWrapper(Mqtt): _nodes: list[MqttNode] - def __init__(self, topic_prefix='hk', randomize_client_id=False): + def __init__(self, + topic_prefix='hk', + randomize_client_id=False, + clean_session=True): client_id = config['mqtt']['client_id'] if randomize_client_id: client_id += '_'+strgen(6) - super().__init__(clean_session=True, client_id=client_id) + super().__init__(clean_session=clean_session, + client_id=client_id) self._nodes = [] self._topic_prefix = topic_prefix diff --git a/src/home/mqtt/module/ota.py b/src/home/mqtt/module/ota.py index e71cccc..70c5475 100644 --- a/src/home/mqtt/module/ota.py +++ b/src/home/mqtt/module/ota.py @@ -41,7 +41,7 @@ class OtaPayload(MqttPayload): class MqttOtaModule(MqttModule): - _ota_request: Optional[tuple[str, str, int]] + _ota_request: Optional[tuple[str, int]] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -52,9 +52,9 @@ class MqttOtaModule(MqttModule): mqtt.subscribe_module("otares", self) if self._ota_request is not None: - secret, filename, qos = self._ota_request + filename, qos = self._ota_request self._ota_request = None - self.do_push_ota(secret, filename, qos) + self.do_push_ota(self._mqtt_node_ref.secret, filename, qos) def handle_payload(self, mqtt: MqttNode, topic: str, payload: bytes) -> Optional[MqttPayload]: if topic == 'otares': @@ -69,10 +69,9 @@ class MqttOtaModule(MqttModule): qos=qos) def push_ota(self, - secret: str, filename: str, qos: int): if not self._initialized: - self._ota_request = (secret, filename, qos) + self._ota_request = (filename, qos) else: - self.do_push_ota(secret, filename, qos) + self.do_push_ota(filename, qos) diff --git a/src/home/mqtt/module/relay.py b/src/home/mqtt/module/relay.py index ae88ddb..5383fb6 100644 --- a/src/home/mqtt/module/relay.py +++ b/src/home/mqtt/module/relay.py @@ -64,9 +64,9 @@ class MqttRelayModule(MqttModule): mqtt.subscribe_module('relay/status', self) def switchpower(self, - enable: bool, - secret: str): - payload = MqttPowerSwitchPayload(secret=secret, state=enable) + enable: bool): + payload = MqttPowerSwitchPayload(secret=self._mqtt_node_ref.secret, + state=enable) self._mqtt_node_ref.publish('relay/switch', payload=payload.pack()) def handle_payload(self, mqtt: MqttNode, topic: str, payload: bytes) -> Optional[MqttPayload]: diff --git a/src/home/mqtt/module/temphum.py b/src/home/mqtt/module/temphum.py index fe52523..0e22793 100644 --- a/src/home/mqtt/module/temphum.py +++ b/src/home/mqtt/module/temphum.py @@ -1,8 +1,8 @@ -from enum import auto +# from enum import auto from .._node import MqttNode from .._module import MqttModule from .._payload import MqttPayload -from ...util import HashableEnum +# from ...util import HashableEnum from typing import Optional from ...temphum import BaseSensor @@ -24,30 +24,31 @@ class MqttTemphumDataPayload(MqttPayload): error: int -class MqttTempHumNodes(HashableEnum): - KBN_SH_HALL = auto() - KBN_SH_BATHROOM = auto() - KBN_SH_LIVINGROOM = auto() - KBN_SH_BEDROOM = auto() - - KBN_BH_2FL = auto() - KBN_BH_2FL_STREET = auto() - KBN_BH_1FL_LIVINGROOM = auto() - KBN_BH_1FL_BEDROOM = auto() - KBN_BH_1FL_BATHROOM = auto() - - KBN_NH_1FL_INV = auto() - KBN_NH_1FL_CENTER = auto() - KBN_NH_1LF_KT = auto() - KBN_NH_1FL_DS = auto() - KBN_NH_1FS_EZ = auto() - - SPB_FLAT120_CABINET = auto() +# class MqttTempHumNodes(HashableEnum): +# KBN_SH_HALL = auto() +# KBN_SH_BATHROOM = auto() +# KBN_SH_LIVINGROOM = auto() +# KBN_SH_BEDROOM = auto() +# +# KBN_BH_2FL = auto() +# KBN_BH_2FL_STREET = auto() +# KBN_BH_1FL_LIVINGROOM = auto() +# KBN_BH_1FL_BEDROOM = auto() +# KBN_BH_1FL_BATHROOM = auto() +# +# KBN_NH_1FL_INV = auto() +# KBN_NH_1FL_CENTER = auto() +# KBN_NH_1LF_KT = auto() +# KBN_NH_1FL_DS = auto() +# KBN_NH_1FS_EZ = auto() +# +# SPB_FLAT120_CABINET = auto() class MqttTempHumModule(MqttModule): def __init__(self, sensor: Optional[BaseSensor] = None, + write_to_database=False, *args, **kwargs): if sensor is not None: kwargs['tick_interval'] = 10 diff --git a/src/inverter_bot.py b/src/inverter_bot.py index fd5acf3..194263e 100755 --- a/src/inverter_bot.py +++ b/src/inverter_bot.py @@ -44,7 +44,7 @@ flags_map = { } logger = logging.getLogger(__name__) -config.load('inverter_bot') +config.load_app('inverter_bot') bot.initialize() bot.lang.ru( diff --git a/src/inverter_mqtt_util.py b/src/inverter_mqtt_util.py index edea29a..9db25dc 100755 --- a/src/inverter_mqtt_util.py +++ b/src/inverter_mqtt_util.py @@ -8,10 +8,10 @@ if __name__ == '__main__': parser = ArgumentParser() parser.add_argument('mode', type=str, choices=('sender', 'receiver'), nargs=1) - config.load('inverter_mqtt_util', parser=parser) + config.load_app('inverter_mqtt_util', parser=parser) arg = parser.parse_args() - mqtt = MqttWrapper() + mqtt = MqttWrapper(clean_session=arg.mode[0] != 'receiver') node = MqttNode(node_id='inverter') module_kwargs = {} if arg.mode[0] == 'sender': diff --git a/src/ipcam_server.py b/src/ipcam_server.py index 2c4915d..a54cd35 100755 --- a/src/ipcam_server.py +++ b/src/ipcam_server.py @@ -556,7 +556,7 @@ logger = logging.getLogger(__name__) # -------------------- if __name__ == '__main__': - config.load('ipcam_server') + config.load_app('ipcam_server') open_database() diff --git a/src/mqtt_node_util.py b/src/mqtt_node_util.py index 0af430a..e5744b0 100755 --- a/src/mqtt_node_util.py +++ b/src/mqtt_node_util.py @@ -23,14 +23,14 @@ if __name__ == '__main__': parser.add_argument('--node-secret', type=str, help='node admin password') - config.load('mqtt_util', parser=parser) + config.load_app('mqtt_util', parser=parser) arg = parser.parse_args() if (arg.switch_relay is not None or arg.node_secret is not None) and 'relay' not in arg.modules: raise ArgumentError(None, '--relay is only allowed when \'relay\' module included in --modules') mqtt = MqttWrapper(randomize_client_id=True) - mqtt_node = MqttNode(node_id=arg.node_id) + mqtt_node = MqttNode(node_id=arg.node_id, node_secret=arg.node_secret) mqtt.add_node(mqtt_node) @@ -44,9 +44,7 @@ if __name__ == '__main__': if m == 'relay' and arg.switch_relay is not None: if not arg.node_secret: raise ArgumentError(None, '--switch-relay requires --node-secret') - module_instance.switchpower(mqtt_node, - arg.switch_relay == 1, - arg.node_secret) + module_instance.switchpower(arg.switch_relay == 1) mqtt.configure_tls() try: @@ -58,7 +56,7 @@ if __name__ == '__main__': if not arg.node_secret: raise ArgumentError(None, 'pushing OTA requires --node-secret') - ota_module.push_ota(arg.node_secret, arg.push_ota, 1) + ota_module.push_ota(arg.push_ota, 1) while True: sleep(0.1) diff --git a/src/openwrt_log_analyzer.py b/src/openwrt_log_analyzer.py index d31c3bf..35b755f 100755 --- a/src/openwrt_log_analyzer.py +++ b/src/openwrt_log_analyzer.py @@ -54,7 +54,7 @@ def main(mac: str, if __name__ == '__main__': - config.load('openwrt_log_analyzer') + config.load_app('openwrt_log_analyzer') for ap in config['openwrt_log_analyzer']['aps']: state_file = config['simple_state']['file'] state_file = state_file.replace('.txt', f'-{ap}.txt') diff --git a/src/openwrt_logger.py b/src/openwrt_logger.py index 3b19de2..97fe7a9 100755 --- a/src/openwrt_logger.py +++ b/src/openwrt_logger.py @@ -46,7 +46,7 @@ if __name__ == '__main__': parser.add_argument('--access-point', type=int, required=True, help='access point number') - arg = config.load('openwrt_logger', parser=parser) + arg = config.load_app('openwrt_logger', parser=parser) state = SimpleState(file=config['simple_state']['file'].replace('{ap}', str(arg.access_point)), default={'seek': 0, 'size': 0}) diff --git a/src/polaris_kettle_bot.py b/src/polaris_kettle_bot.py index 8438ab3..80baef3 100755 --- a/src/polaris_kettle_bot.py +++ b/src/polaris_kettle_bot.py @@ -41,7 +41,7 @@ from telegram.ext import ( ) logger = logging.getLogger(__name__) -config.load('polaris_kettle_bot') +config.load_app('polaris_kettle_bot') primary_choices = (70, 80, 90, 100) all_choices = range( diff --git a/src/polaris_kettle_util.py b/src/polaris_kettle_util.py index 816cff7..12c4388 100755 --- a/src/polaris_kettle_util.py +++ b/src/polaris_kettle_util.py @@ -75,7 +75,7 @@ def main(): parser.add_argument('-t', '--temperature', dest='temp', type=int, default=tempmax, choices=range(tempmin, tempmax+tempstep, tempstep)) - arg = config.load('polaris_kettle_util', use_cli=True, parser=parser) + arg = config.load_app('polaris_kettle_util', use_cli=True, parser=parser) if arg.mode == 'mqtt': server = MqttServer() diff --git a/src/pump_bot.py b/src/pump_bot.py index ab73097..172108e 100755 --- a/src/pump_bot.py +++ b/src/pump_bot.py @@ -16,7 +16,7 @@ from home.mqtt.module.temphum import MqttTemphumDataPayload from home.mqtt.module.diagnostics import InitialDiagnosticsPayload, DiagnosticsPayload -config.load('pump_bot') +config.load_app('pump_bot') mqtt: Optional[MqttWrapper] = None mqtt_node: Optional[MqttNode] = None @@ -208,7 +208,7 @@ def markup(ctx: Optional[bot.Context]) -> Optional[ReplyKeyboardMarkup]: return ReplyKeyboardMarkup(buttons, one_time_keyboard=False) -def mqtt_payload_callback(payload: MqttPayload): +def mqtt_payload_callback(mqtt_node: MqttNode, payload: MqttPayload): global watering_mcu_status types_the_node_can_send = ( diff --git a/src/pump_mqtt_bot.py b/src/pump_mqtt_bot.py index 6a63caf..1c52b03 100755 --- a/src/pump_mqtt_bot.py +++ b/src/pump_mqtt_bot.py @@ -13,7 +13,7 @@ from home.mqtt.module.relay import MqttRelayState from home.mqtt.module.diagnostics import InitialDiagnosticsPayload, DiagnosticsPayload -config.load('pump_mqtt_bot') +config.load_app('pump_mqtt_bot') bot.initialize() bot.lang.ru( diff --git a/src/relay_mqtt_bot.py b/src/relay_mqtt_bot.py index ebbff82..e7fa613 100755 --- a/src/relay_mqtt_bot.py +++ b/src/relay_mqtt_bot.py @@ -6,13 +6,12 @@ from functools import partial from home.config import config from home.telegram import bot -from home.mqtt import MqttRelay, MqttRelayState -from home.mqtt.esp import MqttEspDevice -from home.mqtt.payload import MqttPayload -from home.mqtt.payload.relay import InitialDiagnosticsPayload, DiagnosticsPayload +from home.mqtt import MqttPayload, MqttNode, MqttWrapper +from home.mqtt.module.relay import MqttRelayModule, MqttRelayState +from home.mqtt.module.diagnostics import InitialDiagnosticsPayload, DiagnosticsPayload -config.load('relay_mqtt_bot') +config.load_app('relay_mqtt_bot') bot.initialize() bot.lang.ru( @@ -34,7 +33,11 @@ status_emoji = { 'on': '✅', 'off': '❌' } -mqtt_relay: Optional[MqttRelay] = None + + +# mqtt_relay: Optional[MqttRelayModule] = None +mqtt: Optional[MqttWrapper] = None +relay_nodes: dict[str, MqttRelayModule] = {} relay_states: dict[str, MqttRelayState] = {} @@ -43,23 +46,24 @@ class UserAction(Enum): OFF = 'off' -def on_mqtt_message(home_id, message: MqttPayload): +def on_mqtt_message(node: MqttNode, + message: MqttPayload): if isinstance(message, InitialDiagnosticsPayload) or isinstance(message, DiagnosticsPayload): kwargs = dict(rssi=message.rssi, enabled=message.flags.state) if isinstance(message, InitialDiagnosticsPayload): kwargs['fw_version'] = message.fw_version - if home_id not in relay_states: - relay_states[home_id] = MqttRelayState() - relay_states[home_id].update(**kwargs) + if node.id not in relay_states: + relay_states[node.id] = MqttRelayState() + relay_states[node.id].update(**kwargs) -def enable_handler(home_id: str, ctx: bot.Context) -> None: - mqtt_relay.set_power(home_id, True) +def enable_handler(node_id: str, ctx: bot.Context) -> None: + relay_nodes[node_id].switchpower(True) ctx.reply(ctx.lang('done')) -def disable_handler(home_id: str, ctx: bot.Context) -> None: - mqtt_relay.set_power(home_id, False) +def disable_handler(node_id: str, ctx: bot.Context) -> None: + relay_nodes[node_id].switchpower(False) ctx.reply(ctx.lang('done')) @@ -86,9 +90,13 @@ def markup(ctx: Optional[bot.Context]) -> Optional[ReplyKeyboardMarkup]: if __name__ == '__main__': devices = [] + mqtt = MqttWrapper() for device_id, data in config['relays'].items(): - devices.append(MqttEspDevice(id=device_id, - secret=data['secret'])) + mqtt_node = MqttNode(node_id=device_id, node_secret=data['secret']) + relay_nodes[device_id] = mqtt_node.load_module('relay') + mqtt_node.add_payload_callback(on_mqtt_message) + mqtt.add_node(mqtt_node) + labels = data['labels'] bot.lang.ru(**{device_id: labels['ru']}) bot.lang.en(**{device_id: labels['en']}) @@ -101,12 +109,9 @@ if __name__ == '__main__': messages.append(f'{type_emoji}{status_emoji[action.value]} {labels[_lang]}') bot.handler(texts=messages)(partial(enable_handler if action == UserAction.ON else disable_handler, device_id)) - mqtt_relay = MqttRelay(devices=devices) - mqtt_relay.set_message_callback(on_mqtt_message) - mqtt_relay.configure_tls() - mqtt_relay.connect_and_loop(loop_forever=False) + mqtt.configure_tls() + mqtt.connect_and_loop(loop_forever=False) - # bot.enable_logging(BotType.RELAY_MQTT) bot.run(start_handler=start) - mqtt_relay.disconnect() + mqtt.disconnect() diff --git a/src/relay_mqtt_http_proxy.py b/src/relay_mqtt_http_proxy.py index 098facc..50a74a1 100755 --- a/src/relay_mqtt_http_proxy.py +++ b/src/relay_mqtt_http_proxy.py @@ -1,17 +1,19 @@ #!/usr/bin/env python3 from home import http from home.config import config -from home.mqtt import MqttRelay, MqttRelayState -from home.mqtt.esp import MqttEspDevice -from home.mqtt.payload import MqttPayload -from home.mqtt.payload.relay import InitialDiagnosticsPayload, DiagnosticsPayload -from typing import Optional +from home.mqtt import MqttPayload, MqttWrapper, MqttNode, MqttModule +from home.mqtt.module.relay import MqttRelayState, MqttRelayModule +from home.mqtt.module.diagnostics import InitialDiagnosticsPayload, DiagnosticsPayload +from typing import Optional, Union -mqtt_relay: Optional[MqttRelay] = None +mqtt: Optional[MqttWrapper] = None +mqtt_nodes: dict[str, MqttNode] = {} +relay_modules: dict[str, Union[MqttRelayModule, MqttModule]] = {} relay_states: dict[str, MqttRelayState] = {} -def on_mqtt_message(device_id, message: MqttPayload): +def on_mqtt_message(node: MqttNode, + message: MqttPayload): if isinstance(message, InitialDiagnosticsPayload) or isinstance(message, DiagnosticsPayload): kwargs = dict(rssi=message.rssi, enabled=message.flags.state) if device_id not in relay_states: @@ -29,17 +31,22 @@ class RelayMqttHttpProxy(http.HTTPServer): async def _relay_on_off(self, enable: Optional[bool], req: http.Request): - device_id = req.match_info['id'] - device_secret = req.query['secret'] + node_id = req.match_info['id'] + node_secret = req.query['secret'] + + node = mqtt_nodes[node_id] + relay_module = relay_modules[node_id] if enable is None: - if device_id in relay_states and relay_states[device_id].ever_updated: - cur_state = relay_states[device_id].enabled + if node_id in relay_states and relay_states[node_id].ever_updated: + cur_state = relay_states[node_id].enabled else: cur_state = False enable = not cur_state - mqtt_relay.set_power(device_id, enable, device_secret) + if not node.secret: + node.secret = node_secret + relay_module.switchpower(enable) return self.ok() async def relay_on(self, req: http.Request): @@ -53,15 +60,22 @@ class RelayMqttHttpProxy(http.HTTPServer): if __name__ == '__main__': - config.load('relay_mqtt_http_proxy') + config.load_app('relay_mqtt_http_proxy') + + mqtt = MqttWrapper() + for device_id, data in config['relays'].items(): + mqtt_node = MqttNode(node_id=device_id) + relay_modules[device_id] = mqtt_node.load_module('relay') + mqtt_nodes[device_id] = mqtt_node + mqtt_node.add_payload_callback(on_mqtt_message) + mqtt.add_node(mqtt_node) + mqtt_node.add_payload_callback(on_mqtt_message) - mqtt_relay = MqttRelay(devices=[MqttEspDevice(id=device_id) for device_id in config.get('relay.devices')]) - mqtt_relay.configure_tls() - mqtt_relay.set_message_callback(on_mqtt_message) - mqtt_relay.connect_and_loop(loop_forever=False) + mqtt.configure_tls() + mqtt.connect_and_loop(loop_forever=False) proxy = RelayMqttHttpProxy(config.get_addr('server.listen')) try: proxy.run() except KeyboardInterrupt: - mqtt_relay.disconnect() + mqtt.disconnect() diff --git a/src/sensors_bot.py b/src/sensors_bot.py index dc081b0..152dd24 100755 --- a/src/sensors_bot.py +++ b/src/sensors_bot.py @@ -23,7 +23,7 @@ from home.api.types import ( TemperatureSensorLocation ) -config.load('sensors_bot') +config.load_app('sensors_bot') bot.initialize() bot.lang.ru( diff --git a/src/sensors_mqtt_sender.py b/src/sensors_mqtt_sender.py deleted file mode 100755 index 393962a..0000000 --- a/src/sensors_mqtt_sender.py +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env python3 -import time -import json - -from home.util import parse_addr, MySimpleSocketClient -from home.mqtt import Mqtt, poll_tick -from home.mqtt.payload.sensors import Temperature -from home.config import config - - -class MqttClient(Mqtt): - def __init__(self): - super().__init__(self) - self._home_id = config['mqtt']['home_id'] - - def poll(self): - freq = int(config['mqtt']['sensors']['poll_freq']) - self._logger.debug(f'freq={freq}') - - g = poll_tick(freq) - while True: - time.sleep(next(g)) - for k, v in config['mqtt']['sensors']['si7021'].items(): - host, port = parse_addr(v['addr']) - self.publish_si7021(host, port, k) - - def publish_si7021(self, host: str, port: int, name: str): - self._logger.debug(f"publish_si7021/{name}: {host}:{port}") - - try: - now = time.time() - socket = MySimpleSocketClient(host, port) - - socket.write('read') - response = json.loads(socket.read().strip()) - - temp = response['temp'] - humidity = response['humidity'] - - self._logger.debug(f'publish_si7021/{name}: temp={temp} humidity={humidity}') - - pld = Temperature(time=round(now), - temp=temp, - rh=humidity) - self._client.publish(f'hk/{self._home_id}/si7021/{name}', - payload=pld.pack(), - qos=1) - except Exception as e: - self._logger.exception(e) - - -if __name__ == '__main__': - config.load('sensors_mqtt_sender') - - client = MqttClient() - client.configure_tls() - client.connect_and_loop(loop_forever=False) - client.poll() diff --git a/src/sound_bot.py b/src/sound_bot.py index 186337a..a2f8342 100755 --- a/src/sound_bot.py +++ b/src/sound_bot.py @@ -23,7 +23,7 @@ from telegram import ReplyKeyboardMarkup, InlineKeyboardMarkup, InlineKeyboardBu from PIL import Image -config.load('sound_bot') +config.load_app('sound_bot') nodes = {} for nodename, nodecfg in config['nodes'].items(): diff --git a/src/sound_node.py b/src/sound_node.py index 9d53362..b0b4a67 100755 --- a/src/sound_node.py +++ b/src/sound_node.py @@ -77,7 +77,7 @@ if __name__ == '__main__': if not os.getegid() == 0: raise RuntimeError("Must be run as root.") - config.load('sound_node') + config.load_app('sound_node') storage = SoundRecordStorage(config['node']['storage']) diff --git a/src/sound_sensor_node.py b/src/sound_sensor_node.py index d9a8999..e332174 100755 --- a/src/sound_sensor_node.py +++ b/src/sound_sensor_node.py @@ -14,7 +14,7 @@ if __name__ == '__main__': if not os.getegid() == 0: sys.exit('Must be run as root.') - config.load('sound_sensor_node') + config.load_app('sound_sensor_node') kwargs = {} if 'delay' in config['node']: diff --git a/src/sound_sensor_server.py b/src/sound_sensor_server.py index aa62608..3a68a08 100755 --- a/src/sound_sensor_server.py +++ b/src/sound_sensor_server.py @@ -159,7 +159,7 @@ def api_error_handler(exc, name, req: RequestParams): if __name__ == '__main__': - config.load('sound_sensor_server') + config.load_app('sound_sensor_server') hc = HitCounter() api = WebAPIClient(timeout=(10, 60)) diff --git a/src/ssh_tunnels_config_util.py b/src/ssh_tunnels_config_util.py index 3b2ba6e..03a8219 100755 --- a/src/ssh_tunnels_config_util.py +++ b/src/ssh_tunnels_config_util.py @@ -3,7 +3,7 @@ from home.config import config if __name__ == '__main__': - config.load('ssh_tunnels_config_util') + config.load_app('ssh_tunnels_config_util') network_prefix = config['network'] hostnames = [] diff --git a/src/temphum_mqtt_node.py b/src/temphum_mqtt_node.py index bcd72da..c3d1975 100755 --- a/src/temphum_mqtt_node.py +++ b/src/temphum_mqtt_node.py @@ -63,7 +63,7 @@ async def run_server(host, port): if __name__ == '__main__': - config.load() + config.load_app() if 'measure_delay' in config['sensor']: delay = float(config['sensor']['measure_delay']) diff --git a/src/sensors_mqtt_receiver.py b/src/temphum_mqtt_receiver.py index f7cb467..a4b888e 100755 --- a/src/sensors_mqtt_receiver.py +++ b/src/temphum_mqtt_receiver.py @@ -2,18 +2,8 @@ import paho.mqtt.client as mqtt import re -from home.mqtt import Mqtt from home.config import config -from home.mqtt.payload.sensors import Temperature -from home.api.types import TemperatureSensorLocation -from home.database import SensorsDatabase - - -def get_sensor_type(sensor: str) -> TemperatureSensorLocation: - for item in TemperatureSensorLocation: - if sensor == item.name.lower(): - return item - raise ValueError(f'unexpected sensor value: {sensor}') +from home.mqtt import MqttWrapper, MqttNode class MqttServer(Mqtt): @@ -47,7 +37,12 @@ class MqttServer(Mqtt): if __name__ == '__main__': - config.load('sensors_mqtt_receiver') + config.load_app('temphum_mqtt_receiver') + + mqtt = MqttWrapper(clean_session=False) + node = MqttNode(node_id='+') + node.load_module('temphum', write_to_database=True) + mqtt.add_node(node) - server = MqttServer() - server.connect_and_loop() + mqtt.configure_tls() + mqtt.connect_and_loop()
\ No newline at end of file diff --git a/src/temphumd.py b/src/temphumd.py index bcd72da..c3d1975 100755 --- a/src/temphumd.py +++ b/src/temphumd.py @@ -63,7 +63,7 @@ async def run_server(host, port): if __name__ == '__main__': - config.load() + config.load_app() if 'measure_delay' in config['sensor']: delay = float(config['sensor']['measure_delay']) diff --git a/src/test_new_config.py b/src/test_new_config.py new file mode 100644 index 0000000..110e7a1 --- /dev/null +++ b/src/test_new_config.py @@ -0,0 +1,9 @@ +from home.config import config, app_config, LinuxBoardsConfig +from pprint import pprint + + +if __name__ == '__main__': + config.load_app(name=False) + + lbc = LinuxBoardsConfig() + pprint(lbc.data)
\ No newline at end of file diff --git a/src/web_api.py b/src/web_api.py index 0ddc6bd..0aa994a 100755 --- a/src/web_api.py +++ b/src/web_api.py @@ -231,7 +231,7 @@ if __name__ == '__main__': _app_name = 'web_api' if is_development_mode(): _app_name += '_dev' - config.load(_app_name) + config.load_app(_app_name) loop = asyncio.get_event_loop() diff --git a/systemd/sensors_mqtt_receiver.service b/systemd/sensors_mqtt_receiver.service index e67c112..5b9ff6a 100644 --- a/systemd/sensors_mqtt_receiver.service +++ b/systemd/sensors_mqtt_receiver.service @@ -1,12 +1,12 @@ [Unit] -Description=sensors mqtt receiver +Description=temphum mqtt receiver After=network.target [Service] User=user Group=user Restart=on-failure -ExecStart=python3 /home/user/home/src/sensors_mqtt_receiver.py +ExecStart=python3 /home/user/home/src/temphum_mqtt_receiver.py WorkingDirectory=/home/user [Install] diff --git a/systemd/sensors_mqtt_sender.service b/systemd/sensors_mqtt_sender.service deleted file mode 100644 index a271d72..0000000 --- a/systemd/sensors_mqtt_sender.service +++ /dev/null @@ -1,13 +0,0 @@ -[Unit] -Description=Sensors MQTT sender -After=temphumd.service - -[Service] -User=user -Group=user -Restart=on-failure -ExecStart=/home/user/homekit/src/sensors_mqtt_sender.py -WorkingDirectory=/home/user - -[Install] -WantedBy=multi-user.target
\ No newline at end of file diff --git a/test/mqtt_relay_server_util.py b/test/mqtt_relay_server_util.py index e6c5255..35bbf02 100755 --- a/test/mqtt_relay_server_util.py +++ b/test/mqtt_relay_server_util.py @@ -12,7 +12,7 @@ from src.home.mqtt.relay import MQTTRelayClient if __name__ == '__main__': - config.load('test_mqtt_relay_server') + config.load_app('test_mqtt_relay_server') relay = MQTTRelayClient('test') relay.configure_tls() relay.connect_and_loop() diff --git a/test/mqtt_relay_util.py b/test/mqtt_relay_util.py index c1096cc..3634bbe 100755 --- a/test/mqtt_relay_util.py +++ b/test/mqtt_relay_util.py @@ -18,7 +18,7 @@ if __name__ == '__main__': parser.add_argument('--off', action='store_true') parser.add_argument('--stat', action='store_true') - config.load('test_mqtt_relay', parser=parser) + config.load_app('test_mqtt_relay', parser=parser) arg = parser.parse_args() relay = MQTTRelayController('test') diff --git a/test/test_amixer.py b/test/test_amixer.py index c8bd546..464941e 100755 --- a/test/test_amixer.py +++ b/test/test_amixer.py @@ -28,7 +28,7 @@ if __name__ == '__main__': parser.add_argument('--decr', type=str) # parser.add_argument('--dump-config', action='store_true') - args = config.load('test_amixer', parser=parser) + args = config.load_app('test_amixer', parser=parser) # if args.dump_config: # print(config.data) diff --git a/test/test_api.py b/test/test_api.py index 1f6361c..e80eb4c 100755 --- a/test/test_api.py +++ b/test/test_api.py @@ -13,7 +13,7 @@ from src.home.config import config if __name__ == '__main__': - config.load('test_api') + config.load_app('test_api') api = WebAPIClient() print(api.log_bot_request(BotType.ADMIN, 1, "test_api.py")) diff --git a/test/test_esp32_cam.py b/test/test_esp32_cam.py index 27ce379..d743f09 100755 --- a/test/test_esp32_cam.py +++ b/test/test_esp32_cam.py @@ -21,7 +21,7 @@ if __name__ == '__main__': parser.add_argument('--status', action='store_true', help='print status and exit') - arg = config.load(False, parser=parser) + arg = config.load_app(False, parser=parser) cam = esp32.WebClient(addr=parse_addr(arg.addr)) if arg.status: diff --git a/test/test_inverter_monitor.py b/test/test_inverter_monitor.py index 3b1c6b0..621c0e9 100755 --- a/test/test_inverter_monitor.py +++ b/test/test_inverter_monitor.py @@ -372,5 +372,5 @@ def main(): if __name__ == '__main__': - config.load('test_inverter_monitor') + config.load_app('test_inverter_monitor') main() diff --git a/test/test_ipcam_server_cleanup.py b/test/test_ipcam_server_cleanup.py index b7eb23a..5f313a4 100644 --- a/test/test_ipcam_server_cleanup.py +++ b/test/test_ipcam_server_cleanup.py @@ -77,5 +77,5 @@ def cleanup_job(): if __name__ == '__main__': - config.load('ipcam_server') + config.load_app('ipcam_server') cleanup_job() diff --git a/test/test_record_upload.py b/test/test_record_upload.py index cbd3ca2..21e3d68 100755 --- a/test/test_record_upload.py +++ b/test/test_record_upload.py @@ -64,7 +64,7 @@ def api_success_handler(response, name, req: RequestParams): if __name__ == '__main__': - config.load('test_record_upload') + config.load_app('test_record_upload') nodes = {} for name, addr in config['nodes'].items(): diff --git a/test/test_sound_server_api.py b/test/test_sound_server_api.py index e68c6f8..5295a5d 100755 --- a/test/test_sound_server_api.py +++ b/test/test_sound_server_api.py @@ -56,7 +56,7 @@ def hits_sender(): if __name__ == '__main__': - config.load('test_api') + config.load_app('test_api') hc = HitCounter() api = WebAPIClient() diff --git a/test/test_telegram_aio_send_photo.py b/test/test_telegram_aio_send_photo.py index 705e534..4d05c03 100644 --- a/test/test_telegram_aio_send_photo.py +++ b/test/test_telegram_aio_send_photo.py @@ -20,7 +20,7 @@ async def main(): if __name__ == '__main__': - config.load('test_telegram_aio_send_photo') + config.load_app('test_telegram_aio_send_photo') loop = asyncio.get_event_loop() asyncio.ensure_future(main()) |