From 44df59e0ba36797bbce1e4d11386c5b5b3e1843a Mon Sep 17 00:00:00 2001 From: Andreas Hochsteger Date: Wed, 8 Jun 2022 00:00:24 +0200 Subject: [PATCH 1/3] refactor: enum-based event handling * lib: refactor to use enum class for events * lib: pass auto-reconnect behavior on disconnect * shell: split wp initialization from connecting * pass wp, wpdef into WattpilotShell BREAKING CHANGEs: * rename cmd get/set to propget/set (conflict with built-in cmd) --- README.md | 14 +-- ShellCommands.md | 36 +++++--- ShellEnvVariables.md | 2 + src/wattpilot/__init__.py | 101 +++++++++++----------- src/wattpilot/wattpilotshell.py | 149 ++++++++++++++------------------ 5 files changed, 151 insertions(+), 151 deletions(-) diff --git a/README.md b/README.md index 173a911..145b9a6 100644 --- a/README.md +++ b/README.md @@ -33,10 +33,12 @@ Welcome to the Wattpilot Shell 0.2. Type help or ? to list commands. wattpilot> help -Documented commands (type help ): -======================================== -EOF exit ha info properties server unwatch watch -connect get help mqtt rawvalues set values +Documented commands (use 'help -v' for verbose/'help ' for details): +=========================================================================== +alias EOF help mqtt rawvalues set values +connect exit history properties run_pyscript shell watch +disconnect get info propset run_script shortcuts +edit ha macro quit server unwatch ``` The shell supports TAB-completion for all commands and their arguments. @@ -49,8 +51,8 @@ It's also possible to pass a single command to the shell to integrate it into sc wattpilotshell " " # Examples: -wattpilotshell "get amp" -wattpilotshell "set amp 6" +wattpilotshell "propget amp" +wattpilotshell "propset amp 6" ``` ## MQTT Bridge Support diff --git a/ShellCommands.md b/ShellCommands.md index 47eea12..41b208c 100644 --- a/ShellCommands.md +++ b/ShellCommands.md @@ -26,7 +26,7 @@ See also: ## connect ```bash -Connect to Wattpilot (using WATTPILOT_* env variables) +Connect to Wattpilot Usage: connect ``` @@ -63,13 +63,6 @@ Exit the shell Usage: exit ``` -## get - -```bash -Get a property value -Usage: get -``` - ## ha ```bash @@ -219,6 +212,20 @@ List property definitions and values Usage: properties [propRegex] ``` +## propget + +```bash +Get a property value +Usage: propget +``` + +## propset + +```bash +Set a property value +Usage: propset +``` + ## quit ```bash @@ -287,8 +294,17 @@ Usage: server ## set ```bash -Set a property value -Usage: set +Usage: set [-h] [param] [value] + +Set a settable parameter or show current settings of parameters + +positional arguments: + param parameter to set or view + value new value for settable + +optional arguments: + -h, --help show this help message and exit + ``` ## shell diff --git a/ShellEnvVariables.md b/ShellEnvVariables.md index 928a448..7174de5 100644 --- a/ShellEnvVariables.md +++ b/ShellEnvVariables.md @@ -14,6 +14,7 @@ | `MQTT_HOST` | | | `MQTT_MESSAGES` | | | `MQTT_NOT_AVAILABLE_PAYLOAD` | `offline` | +| `MQTT_PASSWORD` | | | `MQTT_PORT` | `1883` | | `MQTT_PROPERTIES` | | | `MQTT_PUBLISH_MESSAGES` | `false` | @@ -24,6 +25,7 @@ | `MQTT_TOPIC_PROPERTY_BASE` | `{baseTopic}/properties/{propName}` | | `MQTT_TOPIC_PROPERTY_SET` | `~/set` | | `MQTT_TOPIC_PROPERTY_STATE` | `~/state` | +| `MQTT_USERNAME` | | | `WATTPILOT_AUTOCONNECT` | `true` | | `WATTPILOT_AUTO_RECONNECT` | `true` | | `WATTPILOT_CONNECT_TIMEOUT` | `30` | diff --git a/src/wattpilot/__init__.py b/src/wattpilot/__init__.py index 42c21a8..3af5f1f 100644 --- a/src/wattpilot/__init__.py +++ b/src/wattpilot/__init__.py @@ -7,6 +7,7 @@ import logging import base64 +from enum import Enum, auto from time import sleep from types import SimpleNamespace @@ -19,30 +20,29 @@ class LoadMode(): NEXTTRIP=5 -class Wattpilot(object): +class Event(Enum): + # Wattpilot events: + WP_AUTH = auto(), + WP_AUTH_ERROR = auto(), + WP_AUTH_SUCCESS = auto(), + WP_CLEAR_INVERTERS = auto(), + WP_CONNECT = auto(), + WP_DELTA_STATUS = auto(), + WP_DISCONNECT = auto(), + WP_FULL_STATUS = auto(), + WP_FULL_STATUS_FINISHED = auto(), + WP_HELLO = auto(), + WP_INIT = auto(), + WP_PROPERTY = auto(), + WP_RESPONSE = auto(), + WP_UPDATE_INVERTER = auto(), + # WebSocketApp events: + WS_CLOSE = auto(), + WS_ERROR = auto(), + WS_MESSAGE = auto(), + WS_OPEN = auto(), - supported_events = [ - # Wattpilot events: - "wp_auth", - "wp_authError", - "wp_authSuccess", - "wp_clearInverters", - "wp_connect", - "wp_deltaStatus", - "wp_disconnect", - "wp_fullStatus", - "wp_fullStatus_finished", - "wp_hello", - "wp_init", - "wp_property", - "wp_response", - "wp_updateInverter", - # WebSocketApp events: - "ws_close", - "ws_error", - "ws_message", - "ws_open", - ] +class Wattpilot(object): carValues = {} alwValues = {} @@ -304,18 +304,24 @@ def connect(self): self._wst = threading.Thread(target=self._wsapp.run_forever) self._wst.daemon = True self._wst.start() - self.__call_event_handler("wp_connect") + self.__call_event_handler(Event.WP_CONNECT) _LOGGER.info("Wattpilot connected") - def disconnect(self): + def disconnect(self, auto_reconnect=False): self._wsapp.close() self._connected=False - self._auto_reconnect=False # Do not reconnect on explicit disconnect - self.__call_event_handler("wp_disconnect") + self._auto_reconnect = auto_reconnect + self.__call_event_handler(Event.WP_DISCONNECT) _LOGGER.info("Wattpilot disconnected") # Wattpilot Event Handling + # def __init_event_handler(): + # eh = {} + # for event_type in list(Event): + # eh[event_type.value] = [] + # return eh + def add_event_handler(self,event_type,callback_fn): if event_type not in self._event_handler: self._event_handler[event_type] = [] @@ -327,6 +333,8 @@ def remove_event_handler(self,event_type,callback_fn): def __call_event_handler(self, event_type, *args): _LOGGER.debug(f"Calling event handler for event type '{event_type} ...") + if event_type not in self._event_handler: + return for callback_fn in self._event_handler[event_type]: event = { "type": event_type, @@ -422,7 +430,7 @@ def __update_property(self,name,value): self._updateAvailable = False else: self._updateAvailable = True - self.__call_event_handler("wp_property",name,value) + self.__call_event_handler(Event.WP_PROPERTY, name, value) def __on_hello(self,message): _LOGGER.info("Connected to WattPilot Serial %s",message.serial) @@ -438,7 +446,7 @@ def __on_hello(self,message): self._protocol=message.protocol if hasattr(message,"secured"): self._secured=message.secured - self.__call_event_handler("wp_hello",message) + self.__call_event_handler(Event.WP_HELLO, message) def __on_auth(self,wsapp,message): ran = random.randrange(10**80) @@ -451,7 +459,7 @@ def __on_auth(self,wsapp,message): response["token3"] = self._token3 response["hash"] = hash self.__send(response) - self.__call_event_handler("wp_auth",message) + self.__call_event_handler(Event.WP_AUTH, message) def __send(self,message,secure=False): # If the connection to wattpilot is over a unsecure channel (http) all send messages are wrapped in @@ -472,36 +480,35 @@ def __send(self,message,secure=False): def __on_AuthSuccess(self,message): self._connected = True - self.__call_event_handler("wp_authSuccess",message) + self.__call_event_handler(Event.WP_AUTH_SUCCESS, message) _LOGGER.info("Authentication successful") def __on_FullStatus(self,message): props = message.status.__dict__ for key in props: self.__update_property(key,props[key]) - self.__call_event_handler("wp_fullStatus",message) + self.__call_event_handler(Event.WP_FULL_STATUS, message) self._allPropsInitialized = not message.partial if message.partial == False: - self.__call_event_handler("wp_fullStatus_finished",message) + self.__call_event_handler(Event.WP_FULL_STATUS_FINISHED, message) def __on_AuthError(self,message): if message.message=="Wrong password": self._wsapp.close() - _LOGGER.error("Authentication failed: %s" , message.message) - self.__call_event_handler("wp_authError",message) + _LOGGER.error("Authentication failed: %s", message.message) + self.__call_event_handler(Event.WP_AUTH_ERROR, message) def __on_DeltaStatus(self,message): props = message.status.__dict__ for key in props: self.__update_property(key,props[key]) - self.__call_event_handler("wp_deltaStatus",message) - + self.__call_event_handler(Event.WP_DELTA_STATUS, message) def __on_clearInverters(self,message): - self.__call_event_handler("wp_clearInverters",message) + self.__call_event_handler(Event.WP_CLEAR_INVERTERS, message) def __on_updateInverter(self,message): - self.__call_event_handler("wp_updateInverter",message) + self.__call_event_handler(Event.WP_UPDATE_INVERTER, message) def __on_response(self,message): if message.success: @@ -510,18 +517,18 @@ def __on_response(self,message): self.__update_property(key,props[key]) else: _LOGGER.error("Error Sending Request %s. Message: %s" ,message.requestId,message.message) - self.__call_event_handler("wp_response",message) + self.__call_event_handler(Event.WP_RESPONSE, message) def __on_open(self,wsapp): - self.__call_event_handler("ws_open",wsapp) + self.__call_event_handler(Event.WS_OPEN, wsapp) def __on_error(self,wsapp,err): - self.__call_event_handler("ws_error",wsapp,err) + self.__call_event_handler(Event.WS_ERROR, wsapp, err) _LOGGER.error(f"Error received from WebSocketApp: {err}") def __on_close(self,wsapp,code,msg): self._connected=False - self.__call_event_handler("ws_close",wsapp,code,msg) + self.__call_event_handler(Event.WS_CLOSE, wsapp, code, msg) if (self._auto_reconnect): sleep(self._reconnect_interval) self._wsapp.run_forever() @@ -530,7 +537,7 @@ def __on_message(self, wsapp, message): ## called whenever a message through websocket is received _LOGGER.debug("Message received: %s", message) msg=json.loads(message, object_hook=lambda d: SimpleNamespace(**d)) - self.__call_event_handler("ws_message",message) + self.__call_event_handler(Event.WS_MESSAGE, message) if (msg.type == 'hello'): # Hello Message -> Received upon connection before auth self.__on_hello(msg) if (msg.type == 'authRequired'): # Auth Required -> Received after hello @@ -550,7 +557,6 @@ def __on_message(self, wsapp, message): if (msg.type == 'updateInverter'): # Contains information of connected Photovoltaik inverter / powermeter self.__on_updateInverter(msg) - def __init__(self, ip ,password,serial=None,cloud=False): self._auto_reconnect = True self._reconnect_interval = 30 @@ -598,10 +604,7 @@ def __init__(self, ip ,password,serial=None,cloud=False): self._carConnected=None self._cae=None self._cak=None - # Initialize callback lists: self._event_handler = {} - for event_type in self.supported_events: - self._event_handler[event_type] = [] self._wst=threading.Thread() @@ -613,6 +616,6 @@ def __init__(self, ip ,password,serial=None,cloud=False): on_message=self.__on_message, on_open=self.__on_open, ) - self.__call_event_handler("wp_init") + self.__call_event_handler(Event.WP_INIT) _LOGGER.info ("Wattpilot %s initialized",self.serial) diff --git a/src/wattpilot/wattpilotshell.py b/src/wattpilot/wattpilotshell.py index a6175c8..8fae55f 100644 --- a/src/wattpilot/wattpilotshell.py +++ b/src/wattpilot/wattpilotshell.py @@ -105,14 +105,18 @@ def wp_initialize(host, password): wp = wattpilot.Wattpilot(host, password) wp._auto_reconnect = WATTPILOT_AUTO_RECONNECT wp._reconnect_interval = WATTPILOT_RECONNECT_INTERVAL + wp.add_event_handler(wattpilot.Event.WS_CLOSE, wp_handle_events) + wp.add_event_handler(wattpilot.Event.WS_OPEN, wp_handle_events) + return wp + +def wp_connect(wp, wait_for_timeouts=True): wp.connect() - wp.add_event_handler("ws_close", wp_handle_events) - wp.add_event_handler("ws_open", wp_handle_events) # Wait for connection and initialization - TODO: Use event handler instead to make it more responsive! - utils_wait_timeout(lambda: wp.connected, WATTPILOT_CONNECT_TIMEOUT) or exit( - "ERROR: Timeout while connecting to Wattpilot!") - utils_wait_timeout(lambda: wp.allPropsInitialized, WATTPILOT_INIT_TIMEOUT) or exit( - "ERROR: Timeout while waiting for property initialization!") + if wait_for_timeouts: + utils_wait_timeout(lambda: wp.connected, WATTPILOT_CONNECT_TIMEOUT) or exit( + "ERROR: Timeout while connecting to Wattpilot!") + utils_wait_timeout(lambda: wp.allPropsInitialized, WATTPILOT_INIT_TIMEOUT) or exit( + "ERROR: Timeout while waiting for property initialization!") return wp @@ -185,6 +189,11 @@ class WattpilotShell(cmd2.Cmd): watching_messages = [] watching_properties = [] + def __init__(self, wp, wpdef): + super().__init__() + self.wp = wp + self.wpdef = wpdef + def postloop(self) -> None: print() return super().postloop() @@ -196,19 +205,16 @@ def _complete_list(self, clist, text): return [x for x in clist if x.startswith(text)] def _complete_message(self, text, sender=None): - global wpdef - return [md["key"] for md in wpdef["messages"].values() if (not sender or md["sender"] == sender) and md["key"].startswith(text)] + return [md["key"] for md in self.wpdef["messages"].values() if (not sender or md["sender"] == sender) and md["key"].startswith(text)] def _complete_propname(self, text, rw=False, available_only=True): - global wpdef - return [k for k in wp_get_all_props(available_only).keys() if (not rw or ("rw" in wpdef["properties"][k] and wpdef["properties"][k]["rw"] == "R/W")) and k.startswith(text)] + return [k for k in wp_get_all_props(available_only).keys() if (not rw or ("rw" in self.wpdef["properties"][k] and self.wpdef["properties"][k]["rw"] == "R/W")) and k.startswith(text)] def _complete_values(self, text, line): - global wpdef token = line.split(' ') if len(token) == 2: return self._complete_propname(text, rw=False, available_only=True) + [''] - elif len(token) == 3 and text in wpdef["properties"]: + elif len(token) == 3 and text in self.wpdef["properties"]: return ['', ''] return [] @@ -217,12 +223,9 @@ def do_EOF(self, arg: str) -> bool | None: return True def do_connect(self, arg: str) -> bool | None: - """Connect to Wattpilot (using WATTPILOT_* env variables) + """Connect to Wattpilot Usage: connect""" - global WATTPILOT_HOST - global WATTPILOT_PASSWORD - global wp - wp = wp_initialize(WATTPILOT_HOST, WATTPILOT_PASSWORD) + wp_connect(self.wp) def do_disconnect(self, arg: str) -> bool | None: """Disconnect from Wattpilot @@ -234,27 +237,25 @@ def do_exit(self, arg: str) -> bool | None: Usage: exit""" return True - def do_get(self, arg: str) -> bool | None: + def do_propget(self, arg: str) -> bool | None: """Get a property value -Usage: get """ - global wp - global wpdef +Usage: propget """ args = arg.split(' ') if not self._ensure_connected(): return if len(args) < 1 or arg == '': print(f"ERROR: Wrong number of arguments!") - elif args[0] in wp.allProps: - pd = wpdef["properties"][args[0]] - print(mqtt_get_encoded_property(pd, wp.allProps[args[0]])) - elif args[0] in wpdef["splitProperties"]: - pd = wpdef["properties"][args[0]] + elif args[0] in self.wp.allProps: + pd = self.wpdef["properties"][args[0]] + print(mqtt_get_encoded_property(pd, self.wp.allProps[args[0]])) + elif args[0] in self.wpdef["splitProperties"]: + pd = self.wpdef["properties"][args[0]] print(mqtt_get_encoded_property( pd, wp_get_child_prop_value(pd["key"]))) else: print(f"ERROR: Unknown property: {args[0]}") - def complete_get(self, text, line, begidx, endidx): + def complete_propget(self, text, line, begidx, endidx): return self._complete_propname(text, rw=False, available_only=True) def do_ha(self, arg: str) -> bool | None: @@ -311,34 +312,32 @@ def _ha_prop_cmds(self, cmd, prop_name): global HA_PROPERTIES global MQTT_PROPERTIES global mqtt_client - global wp - global wpdef if prop_name not in wpdef["properties"]: print(f"ERROR: Unknown property '{prop_name}!") elif cmd == "enable": if prop_name not in MQTT_PROPERTIES: MQTT_PROPERTIES.append(prop_name) ha_discover_property( - wp, mqtt_client, wpdef["properties"][prop_name], disable_discovery=False, force_enablement=True) + self.wp, mqtt_client, self.wpdef["properties"][prop_name], disable_discovery=False, force_enablement=True) elif cmd == "disable": if prop_name in MQTT_PROPERTIES: MQTT_PROPERTIES.remove(prop_name) ha_discover_property( - wp, mqtt_client, wpdef["properties"][prop_name], disable_discovery=False, force_enablement=False) + self.wp, mqtt_client, self.wpdef["properties"][prop_name], disable_discovery=False, force_enablement=False) elif cmd == "discover": if prop_name not in HA_PROPERTIES: HA_PROPERTIES.append(prop_name) if prop_name not in MQTT_PROPERTIES: MQTT_PROPERTIES.append(prop_name) ha_discover_property( - wp, mqtt_client, wpdef["properties"][prop_name], disable_discovery=False, force_enablement=True) + self.wp, mqtt_client, self.wpdef["properties"][prop_name], disable_discovery=False, force_enablement=True) elif cmd == "undiscover": if prop_name in HA_PROPERTIES: HA_PROPERTIES.remove(prop_name) if prop_name in MQTT_PROPERTIES: MQTT_PROPERTIES.remove(prop_name) ha_discover_property( - wp, mqtt_client, wpdef["properties"][prop_name], disable_discovery=True, force_enablement=False) + self.wp, mqtt_client, self.wpdef["properties"][prop_name], disable_discovery=True, force_enablement=False) def complete_ha(self, text, line, begidx, endidx): token = line.split(' ') @@ -353,10 +352,9 @@ def complete_ha(self, text, line, begidx, endidx): def do_info(self, arg: str) -> bool | None: """Print device infos Usage: info""" - global wp if not self._ensure_connected(): return - print(wp) + print(self.wp) def do_mqtt(self, arg: str) -> bool | None: """Control the MQTT bridge @@ -386,7 +384,6 @@ def do_mqtt(self, arg: str) -> bool | None: """ global MQTT_ENABLED global mqtt_client - global wp args = arg.split(' ') if not self._ensure_connected(): return @@ -398,7 +395,7 @@ def do_mqtt(self, arg: str) -> bool | None: f"List of properties activated for MQTT publishing: {MQTT_PROPERTIES}") elif args[0] == "start": MQTT_ENABLED = 'true' - mqtt_client = mqtt_setup(wp) + mqtt_client = mqtt_setup(self.wp) elif args[0] == "stop": mqtt_stop(mqtt_client) MQTT_ENABLED = 'false' @@ -413,9 +410,7 @@ def do_mqtt(self, arg: str) -> bool | None: def _mqtt_prop_cmds(self, cmd, prop_name): global MQTT_PROPERTIES global mqtt_client - global wp - global wpdef - if prop_name not in wpdef["properties"]: + if prop_name not in self.wpdef["properties"]: print(f"ERROR: Undefined property '{prop_name}'!") elif cmd == "publish" and prop_name not in MQTT_PROPERTIES: MQTT_PROPERTIES.append(prop_name) @@ -435,7 +430,6 @@ def complete_mqtt(self, text, line, begidx, endidx): def do_properties(self, arg: str) -> bool | None: """List property definitions and values Usage: properties [propRegex]""" - global wpdef if not self._ensure_connected(): return props = self._get_props_matching_regex(arg, available_only=False) @@ -444,7 +438,7 @@ def do_properties(self, arg: str) -> bool | None: return print(f"Properties:") for prop_name, value in sorted(props.items()): - self._print_prop_info(wpdef["properties"][prop_name], value) + self._print_prop_info(self.wpdef["properties"][prop_name], value) print() def complete_properties(self, text, line, begidx, endidx): @@ -453,7 +447,6 @@ def complete_properties(self, text, line, begidx, endidx): def do_rawvalues(self, arg: str) -> bool | None: """List raw values of properties (without value mapping) Usage: rawvalues [propRegex] [valueRegex]""" - global wp if not self._ensure_connected(): return print(f"List raw values of properties (without value mapping):") @@ -477,11 +470,9 @@ def do_server(self, arg: str) -> bool | None: _LOGGER.info("Server shutting down.") return True - def do_set(self, arg: str) -> bool | None: + def do_propset(self, arg: str) -> bool | None: """Set a property value -Usage: set """ - global wp - global wpdef +Usage: propset """ args = arg.split(' ') if not self._ensure_connected(): return @@ -499,15 +490,14 @@ def do_set(self, arg: str) -> bool | None: else: v = str(args[1]) wp.send_update(args[0], mqtt_get_decoded_property( - wpdef["properties"][args[0]], v)) + self.wpdef["properties"][args[0]], v)) - def complete_set(self, text, line, begidx, endidx): - global wpdef + def complete_propset(self, text, line, begidx, endidx): token = line.split(' ') if len(token) == 2: return self._complete_propname(text, rw=True, available_only=True) - elif len(token) == 3 and token[1] in wpdef["properties"]: - pd = wpdef["properties"][token[1]] + elif len(token) == 3 and token[1] in self.wpdef["properties"]: + pd = self.wpdef["properties"][token[1]] if "jsonType" in pd and pd["jsonType"] == 'boolean': return [v for v in ['false', 'true'] if v.startswith(text)] elif "valueMap" in pd: @@ -519,28 +509,25 @@ def complete_set(self, text, line, begidx, endidx): def do_unwatch(self, arg: str) -> bool | None: """Unwatch a message or property Usage: unwatch """ - global wp args = arg.split(' ') - if not self._ensure_connected(): - return if len(args) < 2 or arg == '': print(f"ERROR: Wrong number of arguments!") - elif args[0] == 'event' and args[1] not in wp.supported_events: + elif args[0] == 'event' and args[1] not in [e.name for e in list(wattpilot.Event)]: print(f"ERROR: Event of type '{args[1]}' is not watched") elif args[0] == 'event': - wp.remove_event_handler(args[1], self._watched_event_received) + self.wp.remove_event_handler(wattpilot.Event[args[1]], self._watched_event_received) elif args[0] == 'message' and args[1] not in self.watching_messages: print(f"ERROR: Message of type '{args[1]}' is not watched") elif args[0] == 'message': self.watching_messages.remove(args[1]) if len(self.watching_messages) == 0: - wp.remove_event_handler("ws_message",self._watched_message_received) + self.wp.remove_event_handler(wattpilot.Event.WS_MESSAGE,self._watched_message_received) elif args[0] == 'property' and args[1] not in self.watching_properties: print(f"ERROR: Property with name '{args[1]}' is not watched") elif args[0] == 'property': self.watching_properties.remove(args[1]) if len(self.watching_properties) == 0: - wp.remove_event_handler("on_property",self._watched_property_changed) + self.wp.remove_event_handler(wattpilot.Event.WP_PROPERTY,self._watched_property_changed) else: print(f"ERROR: Unknown watch type: {args[0]}") @@ -549,7 +536,7 @@ def complete_unwatch(self, text, line, begidx, endidx): if len(token) == 2: return self._complete_list(['event', 'message', 'property'], text) elif len(token) == 3 and token[1] == 'event': - return self._complete_list([e for e in wp.supported_events if len(wp._event_handler[e])>0], text) + return self._complete_list([e.name for e in list(wattpilot.Event) if len(wp._event_handler[e])>0], text) elif len(token) == 3 and token[1] == 'message': return self._complete_list(self.watching_messages, text) elif len(token) == 3 and token[1] == 'property': @@ -559,15 +546,13 @@ def complete_unwatch(self, text, line, begidx, endidx): def do_values(self, arg: str) -> bool | None: """List values of properties (with value mapping enabled) Usage: values [propRegex] [valueRegex]""" - global wp - global wpdef if not self._ensure_connected(): return print(f"List values of properties (with value mapping):") props = self._get_props_matching_regex(arg) for pd, value in sorted(props.items()): print( - f"- {pd}: {mqtt_get_encoded_property(wpdef['properties'][pd],value)}") + f"- {pd}: {mqtt_get_encoded_property(self.wpdef['properties'][pd],value)}") print() def complete_values(self, text, line, begidx, endidx): @@ -576,23 +561,19 @@ def complete_values(self, text, line, begidx, endidx): def do_watch(self, arg: str) -> bool | None: """Watch an event, a message or a property Usage: watch """ - global wp - global wpdef args = arg.split(' ') - if not self._ensure_connected(): - return if len(args) < 2 or arg == '': print(f"ERROR: Wrong number of arguments!") - elif args[0] == 'event' and args[1] not in wp.supported_events: + elif args[0] == 'event' and args[1] not in [e.name for e in list(wattpilot.Event)]: print(f"ERROR: Unknown event type: {args[1]}") elif args[0] == 'event': - wp.add_event_handler(args[1], self._watched_event_received) - elif args[0] == 'message' and args[1] not in wpdef['messages']: + self.wp.add_event_handler(wattpilot.Event[args[1]], self._watched_event_received) + elif args[0] == 'message' and args[1] not in self.wpdef['messages']: print(f"ERROR: Unknown message type: {args[1]}") elif args[0] == 'message': msg_type = args[1] if len(self.watching_messages) == 0: - wp.add_event_handler("ws_message",self._watched_message_received) + self.wp.add_event_handler(wattpilot.Event.WS_MESSAGE,self._watched_message_received) if msg_type not in self.watching_messages: self.watching_messages.append(msg_type) elif args[0] == 'property' and args[1] not in wp.allProps: @@ -600,19 +581,18 @@ def do_watch(self, arg: str) -> bool | None: elif args[0] == 'property': prop_name = args[1] if len(self.watching_properties) == 0: - wp.add_event_handler("wp_property",self._watched_property_changed) + wp.add_event_handler(wattpilot.Event.WP_PROPERTY,self._watched_property_changed) if prop_name not in self.watching_properties: self.watching_properties.append(prop_name) else: print(f"ERROR: Unknown watch type: {args[0]}") def complete_watch(self, text, line, begidx, endidx): - global wpdef token = line.split(' ') if len(token) == 2: return self._complete_list(['event', 'message', 'property'], text) elif len(token) == 3 and token[1] == 'event': - return self._complete_list(wp.supported_events, text) + return self._complete_list([e.name for e in list(wattpilot.Event)], text) elif len(token) == 3 and token[1] == 'message': return self._complete_message(text, 'server') elif len(token) == 3 and token[1] == 'property': @@ -620,7 +600,6 @@ def complete_watch(self, text, line, begidx, endidx): return [] def _print_prop_info(self, pd, value): - global wp _LOGGER.debug(f"Property definition: {pd}") title = "" desc = "" @@ -637,7 +616,7 @@ def _print_prop_info(self, pd, value): print(f"- {pd['key']} ({pd['jsonType']}{alias}{rw}): {title}") if desc: print(f" Description: {desc}") - if pd['key'] in wp.allProps.keys(): + if pd['key'] in self.wp.allProps.keys(): print( f" Value: {mqtt_get_encoded_property(pd,value)}{' (raw:' + utils_value2json(value) + ')' if 'valueMap' in pd else ''}") else: @@ -648,9 +627,8 @@ def _watched_event_received(self, event, *args): print(f"Event of type '{event['type']}' with args '{args}' received!") def _watched_property_changed(self, wp, name, value): - global wpdef if name in self.watching_properties: - pd = wpdef["properties"][name] + pd = self.wpdef["properties"][name] _LOGGER.info( f"Property {name} changed to {mqtt_get_encoded_property(pd,value)}") @@ -660,15 +638,12 @@ def _watched_message_received(self, event, message): _LOGGER.info(f"Message of type {msg_dict['type']} received: {message}") def _ensure_connected(self): - global wp - if not wp: + if not self.wp or not self.wp._connected: print('Not connected to wattpilot!') return False return True def _get_props_matching_regex(self, arg, available_only=True): - global wp - global wpdef args = arg.split(' ') prop_regex = '.*' if len(args) > 0 and args[0] != '': @@ -679,7 +654,7 @@ def _get_props_matching_regex(self, arg, available_only=True): if len(args) > 1: value_regex = args[1] props = {k: v for k, v in props.items() if re.match(r'^'+value_regex+'$', - str(mqtt_get_encoded_property(wpdef["properties"][k], v)), flags=re.IGNORECASE)} + str(mqtt_get_encoded_property(self.wpdef["properties"][k], v)), flags=re.IGNORECASE)} return props @@ -850,7 +825,7 @@ def mqtt_setup(wp): MQTT_PROPERTIES = mqtt_get_watched_properties(wp) _LOGGER.info( f"Registering message callback to publish updates to the following properties to MQTT: {MQTT_PROPERTIES}") - wp.add_event_handler("ws_message", mqtt_publish_message) + wp.add_event_handler(wattpilot.Event.WS_MESSAGE, mqtt_publish_message) return mqtt_client @@ -1192,7 +1167,9 @@ def main_setup_env(): def main(): global MQTT_ENABLED global WATTPILOT_AUTOCONNECT + global WATTPILOT_HOST global WATTPILOT_LOGLEVEL + global WATTPILOT_PASSWORD global mqtt_client global wp global wpdef @@ -1205,11 +1182,11 @@ def main(): # Initialize globals: mqtt_client = None - wp = None + wp = wp_initialize(WATTPILOT_HOST, WATTPILOT_PASSWORD) wpdef = wp_read_apidef() # Initialize shell: - wpsh = WattpilotShell() + wpsh = WattpilotShell(wp, wpdef) if WATTPILOT_AUTOCONNECT == 'true': _LOGGER.info("Automatically connecting to Wattpilot ...") wpsh.do_connect("") From ac6b431c898bca3e8eb3be1b6519725d95d30b4d Mon Sep 17 00:00:00 2001 From: Andreas Hochsteger Date: Fri, 10 Jun 2022 08:45:52 +0200 Subject: [PATCH 2/3] refactor: full self-contained cfg handling w/o globals * no more globals for configs that clutter the code * refactoring-safe config parameter references * new shell commands 'config' and 'docs' * move env var docs to separate auto-generated file * update env var docs using './test-shell.sh update-docs' --- README.md | 43 +--- ShellCommands.md | 14 ++ ShellEnvVariables.md | 70 +++--- src/wattpilot/wattpilotshell.py | 392 +++++++++++++++----------------- test-shell.sh | 16 +- 5 files changed, 244 insertions(+), 291 deletions(-) diff --git a/README.md b/README.md index 145b9a6..8138996 100644 --- a/README.md +++ b/README.md @@ -35,10 +35,10 @@ wattpilot> help Documented commands (use 'help -v' for verbose/'help ' for details): =========================================================================== -alias EOF help mqtt rawvalues set values -connect exit history properties run_pyscript shell watch -disconnect get info propset run_script shortcuts -edit ha macro quit server unwatch +alias docs ha macro propset run_script shortcuts +config edit help mqtt quit server unwatch +connect EOF history properties rawvalues set values +disconnect exit info propget run_pyscript shell watch ``` The shell supports TAB-completion for all commands and their arguments. @@ -144,40 +144,7 @@ docker-compose run wattpilot shell ## Environment Variables -| Environment Variable | Description | Default Value | -| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- | -| `HA_ENABLED` | Enable Home Assistant Discovery | `false` | -| `HA_PROPERTIES` | Only discover given properties (leave unset for all properties having `homeAssistant` set in [wattpilot.yaml](src/wattpilot/ressources/wattpilot.yaml)) | | -| `HA_TOPIC_CONFIG` | Topic pattern for HA discovery config | `homeassistant/{component}/{uniqueId}/config` | -| `HA_WAIT_INIT_S` | Wait initial number of seconds after starting discovery (in addition to wait time depending on the number of properties). May be increased, if entities in HA are not populated with values. | `5` | -| `HA_WAIT_PROPS_MS` | Wait milliseconds per property after discovery before publishing property values. May be increased, if entities in HA are not populated with values. | `50` | -| `MQTT_AVAILABLE_PAYLOAD` | Payload for the availability topic in case the MQTT bridge is online | `online` | -| `MQTT_CLIENT_ID` | MQTT client ID | `wattpilot2mqtt` | -| `MQTT_ENABLED` | Enable MQTT | `false` | -| `MQTT_HOST` | MQTT host to connect to | | -| `MQTT_MESSAGES` | List of space-separated message types to be published to MQTT (leave unset for all messages) | | -| `MQTT_NOT_AVAILABLE_PAYLOAD` | Payload for the availability topic in case the MQTT bridge is offline (last will message) | `offline` | -| `MQTT_PASSWORD` | Password for connecting to MQTT | | -| `MQTT_PORT` | Port of the MQTT host to connect to | `1883` | -| `MQTT_PROPERTIES` | List of space-separated property names to publish changes for (leave unset for all properties) | | -| `MQTT_PUBLISH_MESSAGES` | Publish received Wattpilot messages to MQTT | `false` | -| `MQTT_PUBLISH_PROPERTIES` | Publish received property values to MQTT | `true` | -| `MQTT_TOPIC_AVAILABLE` | Topic pattern to publish Wattpilot availability status to | `{baseTopic}/available` | -| `MQTT_TOPIC_BASE` | Base topic for MQTT | `wattpilot` | -| `MQTT_TOPIC_MESSAGES` | Topic pattern to publish Wattpilot messages to | `{baseTopic}/messages/{messageType}` | -| `MQTT_TOPIC_PROPERTY_BASE` | Base topic for properties | `{baseTopic}/properties/{propName}` | -| `MQTT_TOPIC_PROPERTY_SET` | Topic pattern to listen for property value changes for | `~/set` | -| `MQTT_TOPIC_PROPERTY_STATE` | Topic pattern to publish property values to | `~/state` | -| `MQTT_USERNAME` | Username for connecting to MQTT | | -| `WATTPILOT_AUTOCONNECT` | Automatically connect to Wattpilot on startup | `true` | -| `WATTPILOT_AUTO_RECONNECT` | Automatically re-connect to Wattpilot on lost connections | `true` | -| `WATTPILOT_CONNECT_TIMEOUT` | Connect timeout for Wattpilot connection | `30` | -| `WATTPILOT_HOST` | IP address of the Wattpilot device to connect to | | -| `WATTPILOT_INIT_TIMEOUT` | Wait timeout for property initialization | `30` | -| `WATTPILOT_LOGLEVEL` | Log level (CRITICAL,ERROR,WARNING,INFO,DEBUG) | `INFO` | -| `WATTPILOT_PASSWORD` | Password for connecting to the Wattpilot device | | -| `WATTPILOT_RECONNECT_INTERVAL` | Waiting time in seconds before a lost connection is re-connected | `30` | -| `WATTPILOT_SPLIT_PROPERTIES` | Whether compound properties (e.g. JSON arrays or objects) should be decomposed into separate properties | `true` | +For a complete list of supported environment variables see [ShellEnvVariables.md](ShellEnvVariables.md). ## HELP improving API definition in wattpilot.yaml diff --git a/ShellCommands.md b/ShellCommands.md index 41b208c..0b100d4 100644 --- a/ShellCommands.md +++ b/ShellCommands.md @@ -23,6 +23,13 @@ See also: ``` +## config + +```bash +Show configuration values +Usage: config +``` + ## connect ```bash @@ -37,6 +44,13 @@ Disconnect from Wattpilot Usage: disconnect ``` +## docs + +```bash +Show markdown documentation for environment variables +Usage: docs +``` + ## edit ```bash diff --git a/ShellEnvVariables.md b/ShellEnvVariables.md index 7174de5..7aa0efa 100644 --- a/ShellEnvVariables.md +++ b/ShellEnvVariables.md @@ -1,37 +1,37 @@ # Wattpilot Shell Environment Variables -| Environment Variable | Default Value | -|----------------------|---------------| -| `HA_DISABLED_ENTITIES` | `false` | -| `HA_ENABLED` | `false` | -| `HA_PROPERTIES` | | -| `HA_TOPIC_CONFIG` | `homeassistant/{component}/{uniqueId}/config` | -| `HA_WAIT_INIT_S` | `0` | -| `HA_WAIT_PROPS_MS` | `0` | -| `MQTT_AVAILABLE_PAYLOAD` | `online` | -| `MQTT_CLIENT_ID` | `wattpilot2mqtt` | -| `MQTT_ENABLED` | `false` | -| `MQTT_HOST` | | -| `MQTT_MESSAGES` | | -| `MQTT_NOT_AVAILABLE_PAYLOAD` | `offline` | -| `MQTT_PASSWORD` | | -| `MQTT_PORT` | `1883` | -| `MQTT_PROPERTIES` | | -| `MQTT_PUBLISH_MESSAGES` | `false` | -| `MQTT_PUBLISH_PROPERTIES` | `true` | -| `MQTT_TOPIC_AVAILABLE` | `{baseTopic}/available` | -| `MQTT_TOPIC_BASE` | `wattpilot` | -| `MQTT_TOPIC_MESSAGES` | `{baseTopic}/messages/{messageType}` | -| `MQTT_TOPIC_PROPERTY_BASE` | `{baseTopic}/properties/{propName}` | -| `MQTT_TOPIC_PROPERTY_SET` | `~/set` | -| `MQTT_TOPIC_PROPERTY_STATE` | `~/state` | -| `MQTT_USERNAME` | | -| `WATTPILOT_AUTOCONNECT` | `true` | -| `WATTPILOT_AUTO_RECONNECT` | `true` | -| `WATTPILOT_CONNECT_TIMEOUT` | `30` | -| `WATTPILOT_HOST` | | -| `WATTPILOT_INIT_TIMEOUT` | `30` | -| `WATTPILOT_LOGLEVEL` | `INFO` | -| `WATTPILOT_PASSWORD` | | -| `WATTPILOT_RECONNECT_INTERVAL` | `30` | -| `WATTPILOT_SPLIT_PROPERTIES` | `true` | +|Environment Variable|Type|Default Value|Description| +|--------------------|----|-------------|-----------| +|`HA_DISABLED_ENTITIES`|`boolean`|`false`|Create disabled entities in Home Assistant| +|`HA_ENABLED`|`boolean`|`false`|Enable Home Assistant Discovery| +|`HA_PROPERTIES`|`list`||List of space-separated properties that should be discovered by Home Assistant (leave unset for all properties having `homeAssistant` set in [wattpilot.yaml](src/wattpilot/ressources/wattpilot.yaml)| +|`HA_TOPIC_CONFIG`|`string`|`homeassistant/{component}/{uniqueId}/config`|Topic pattern for HA discovery config| +|`HA_WAIT_INIT_S`|`integer`|`0`|Wait initial number of seconds after starting discovery (in addition to wait time depending on the number of properties). May be increased, if entities in HA are not populated with values.| +|`HA_WAIT_PROPS_MS`|`integer`|`0`|Wait milliseconds per property after discovery before publishing property values. May be increased, if entities in HA are not populated with values.| +|`MQTT_AVAILABLE_PAYLOAD`|`string`|`online`|Payload for the availability topic in case the MQTT bridge is online| +|`MQTT_CLIENT_ID`|`string`|`wattpilot2mqtt`|MQTT client ID| +|`MQTT_ENABLED`|`boolean`|`false`|Enable MQTT| +|`MQTT_HOST`|`string`||MQTT host to connect to| +|`MQTT_MESSAGES`|`list`||List of space-separated message types to be published to MQTT (leave unset for all messages)| +|`MQTT_NOT_AVAILABLE_PAYLOAD`|`string`|`offline`|Payload for the availability topic in case the MQTT bridge is offline (last will message)| +|`MQTT_PASSWORD`|`password`||Password for connecting to MQTT| +|`MQTT_PORT`|`integer`|`1883`|Port of the MQTT host to connect to| +|`MQTT_PROPERTIES`|`list`||List of space-separated property names to publish changes for (leave unset for all properties)| +|`MQTT_PUBLISH_MESSAGES`|`boolean`|`false`|Publish received Wattpilot messages to MQTT| +|`MQTT_PUBLISH_PROPERTIES`|`boolean`|`true`|Publish received property values to MQTT| +|`MQTT_TOPIC_AVAILABLE`|`string`|`{baseTopic}/available`|Topic pattern to publish Wattpilot availability status to| +|`MQTT_TOPIC_BASE`|`string`|`wattpilot`|Base topic for MQTT| +|`MQTT_TOPIC_MESSAGES`|`string`|`{baseTopic}/messages/{messageType}`|Topic pattern to publish Wattpilot messages to| +|`MQTT_TOPIC_PROPERTY_BASE`|`string`|`{baseTopic}/properties/{propName}`|Base topic for properties| +|`MQTT_TOPIC_PROPERTY_SET`|`string`|`~/set`|Topic pattern to listen for property value changes for| +|`MQTT_TOPIC_PROPERTY_STATE`|`string`|`~/state`|Topic pattern to publish property values to| +|`MQTT_USERNAME`|`string`||Username for connecting to MQTT| +|`WATTPILOT_AUTOCONNECT`|`boolean`|`true`|Automatically connect to Wattpilot on startup| +|`WATTPILOT_AUTO_RECONNECT`|`boolean`|`true`|Automatically re-connect to Wattpilot on lost connections| +|`WATTPILOT_CONNECT_TIMEOUT`|`integer`|`30`|Connect timeout for Wattpilot connection| +|`WATTPILOT_HOST`|`string`||IP address of the Wattpilot device to connect to| +|`WATTPILOT_INIT_TIMEOUT`|`integer`|`30`|Wait timeout for property initialization| +|`WATTPILOT_LOGLEVEL`|`string`|`INFO`|Log level (CRITICAL,ERROR,WARNING,INFO,DEBUG)| +|`WATTPILOT_PASSWORD`|`password`||Password for connecting to the Wattpilot device| +|`WATTPILOT_RECONNECT_INTERVAL`|`integer`|`30`|Waiting time in seconds before a lost connection is re-connected| +|`WATTPILOT_SPLIT_PROPERTIES`|`boolean`|`true`|Whether compound properties (e.g. JSON arrays or objects) should be decomposed into separate properties| diff --git a/src/wattpilot/wattpilotshell.py b/src/wattpilot/wattpilotshell.py index 8fae55f..d680cf0 100644 --- a/src/wattpilot/wattpilotshell.py +++ b/src/wattpilot/wattpilotshell.py @@ -10,6 +10,7 @@ import yaml import pkgutil +from enum import Enum, auto from importlib.metadata import version from time import sleep from threading import Event @@ -56,8 +57,6 @@ def utils_value2json(value): #### Wattpilot Functions #### def wp_read_apidef(): - global WATTPILOT_SPLIT_PROPERTIES - api_definition = pkgutil.get_data(__name__, "ressources/wattpilot.yaml") wpdef = { "config": {}, @@ -75,7 +74,7 @@ def wp_read_apidef(): for p in wpdef["config"]["properties"]: wpdef["properties"] = utils_add_to_dict_unique( wpdef["properties"], p["key"], p) - if "childProps" in p and WATTPILOT_SPLIT_PROPERTIES: + if "childProps" in p and Cfg.WATTPILOT_SPLIT_PROPERTIES.val: for cp in p["childProps"]: cp = { # Defaults for split properties: @@ -103,8 +102,8 @@ def wp_initialize(host, password): # Connect to Wattpilot: _LOGGER.debug(f"wp_initialize()") wp = wattpilot.Wattpilot(host, password) - wp._auto_reconnect = WATTPILOT_AUTO_RECONNECT - wp._reconnect_interval = WATTPILOT_RECONNECT_INTERVAL + wp._auto_reconnect = Cfg.WATTPILOT_AUTO_RECONNECT.val + wp._reconnect_interval = Cfg.WATTPILOT_RECONNECT_INTERVAL.val wp.add_event_handler(wattpilot.Event.WS_CLOSE, wp_handle_events) wp.add_event_handler(wattpilot.Event.WS_OPEN, wp_handle_events) return wp @@ -113,9 +112,9 @@ def wp_connect(wp, wait_for_timeouts=True): wp.connect() # Wait for connection and initialization - TODO: Use event handler instead to make it more responsive! if wait_for_timeouts: - utils_wait_timeout(lambda: wp.connected, WATTPILOT_CONNECT_TIMEOUT) or exit( + utils_wait_timeout(lambda: wp.connected, Cfg.WATTPILOT_CONNECT_TIMEOUT.val) or exit( "ERROR: Timeout while connecting to Wattpilot!") - utils_wait_timeout(lambda: wp.allPropsInitialized, WATTPILOT_INIT_TIMEOUT) or exit( + utils_wait_timeout(lambda: wp.allPropsInitialized, Cfg.WATTPILOT_INIT_TIMEOUT.val) or exit( "ERROR: Timeout while waiting for property initialization!") return wp @@ -126,7 +125,7 @@ def wp_handle_events(event, *args): if not mqtt_client: _LOGGER.debug(f"wp_handle_events(): MQTT client not yet initialized - status publishing skipped.") return - available_topic = mqtt_subst_topic(MQTT_TOPIC_AVAILABLE, {}) + available_topic = mqtt_subst_topic(Cfg.MQTT_TOPIC_AVAILABLE.val, {}) if event['type'] == 'on_close': mqtt_client.publish(available_topic, payload="offline", qos=0, retain=True) elif event['type'] == 'on_open': @@ -166,12 +165,11 @@ def wp_get_child_prop_value(cp): def wp_get_all_props(available_only=True): - global WATTPILOT_SPLIT_PROPERTIES global wp global wpdef if available_only: props = {k: v for k, v in wp.allProps.items()} - if WATTPILOT_SPLIT_PROPERTIES: + if Cfg.WATTPILOT_SPLIT_PROPERTIES.val: for cp_key in wpdef["splitProperties"]: props[cp_key] = wp_get_child_prop_value(cp_key) else: @@ -232,6 +230,18 @@ def do_disconnect(self, arg: str) -> bool | None: Usage: disconnect""" wp.disconnect() + def do_docs(self, arg: str) -> bool | None: + """Show markdown documentation for environment variables +Usage: docs""" + Cfg.docs_markdown() + + def do_config(self, arg: str) -> bool | None: + """Show configuration values +Usage: config""" + for e in list(Cfg): + #print(f"{e.name}={os.environ.get(e.name,'')} (-> {e.val})") + print(e.value.format()) + def do_exit(self, arg: str) -> bool | None: """Exit the shell Usage: exit""" @@ -282,8 +292,6 @@ def do_ha(self, arg: str) -> bool | None: Let HA remove a discovered entity representing the property NOTE: Removing of disabled entities may still be broken in HA and require a restart of HA. """ - global HA_ENABLED - global HA_PROPERTIES global mqtt_client args = arg.split(' ') if not self._ensure_connected(): @@ -293,49 +301,47 @@ def do_ha(self, arg: str) -> bool | None: return if args[0] == "properties": print( - f"List of properties activated for discovery: {HA_PROPERTIES}") + f"List of properties activated for discovery: {Cfg.HA_PROPERTIES.val}") elif args[0] == "start": - HA_ENABLED = 'true' + Cfg.HA_ENABLED.val = True mqtt_client = ha_setup(wp) elif args[0] == "stop": ha_stop(mqtt_client) - HA_ENABLED = 'false' + Cfg.HA_ENABLED.val = False elif args[0] == "status": print( - f"HA discovery is {'enabled' if HA_ENABLED == 'true' else 'disabled'}.") + f"HA discovery is {'enabled' if Cfg.HA_ENABLED.val else 'disabled'}.") elif len(args) > 1 and args[0] in ['enable', 'disable', 'discover', 'undiscover']: self._ha_prop_cmds(args[0], args[1]) else: print(f"ERROR: Unsupported argument: {args[0]}") def _ha_prop_cmds(self, cmd, prop_name): - global HA_PROPERTIES - global MQTT_PROPERTIES global mqtt_client if prop_name not in wpdef["properties"]: print(f"ERROR: Unknown property '{prop_name}!") elif cmd == "enable": - if prop_name not in MQTT_PROPERTIES: - MQTT_PROPERTIES.append(prop_name) + if prop_name not in Cfg.MQTT_PROPERTIES.val: + Cfg.MQTT_PROPERTIES.val.append(prop_name) ha_discover_property( self.wp, mqtt_client, self.wpdef["properties"][prop_name], disable_discovery=False, force_enablement=True) elif cmd == "disable": - if prop_name in MQTT_PROPERTIES: - MQTT_PROPERTIES.remove(prop_name) + if prop_name in Cfg.MQTT_PROPERTIES.val: + Cfg.MQTT_PROPERTIES.val.remove(prop_name) ha_discover_property( self.wp, mqtt_client, self.wpdef["properties"][prop_name], disable_discovery=False, force_enablement=False) elif cmd == "discover": - if prop_name not in HA_PROPERTIES: - HA_PROPERTIES.append(prop_name) - if prop_name not in MQTT_PROPERTIES: - MQTT_PROPERTIES.append(prop_name) + if prop_name not in Cfg.HA_PROPERTIES.val: + Cfg.HA_PROPERTIES.val.append(prop_name) + if prop_name not in Cfg.MQTT_PROPERTIES.val: + Cfg.MQTT_PROPERTIES.val.append(prop_name) ha_discover_property( self.wp, mqtt_client, self.wpdef["properties"][prop_name], disable_discovery=False, force_enablement=True) elif cmd == "undiscover": - if prop_name in HA_PROPERTIES: - HA_PROPERTIES.remove(prop_name) - if prop_name in MQTT_PROPERTIES: - MQTT_PROPERTIES.remove(prop_name) + if prop_name in Cfg.HA_PROPERTIES.val: + Cfg.HA_PROPERTIES.val.remove(prop_name) + if prop_name in Cfg.MQTT_PROPERTIES.val: + Cfg.MQTT_PROPERTIES.val.remove(prop_name) ha_discover_property( self.wp, mqtt_client, self.wpdef["properties"][prop_name], disable_discovery=True, force_enablement=False) @@ -344,9 +350,9 @@ def complete_ha(self, text, line, begidx, endidx): if len(token) == 2: return self._complete_list(['enable', 'disable', 'discover', 'properties', 'start', 'status', 'stop', 'undiscover'], text) elif len(token) == 3 and token[1] == 'discover': - return self._complete_list([p for p in self._complete_propname(text, available_only=True) if p not in HA_PROPERTIES], text) + return self._complete_list([p for p in self._complete_propname(text, available_only=True) if p not in Cfg.HA_PROPERTIES.val], text) elif len(token) == 3 and token[1] in ['enable', 'disable', 'undiscover']: - return self._complete_list(HA_PROPERTIES, text) + return self._complete_list(Cfg.HA_PROPERTIES.val, text) return [] def do_info(self, arg: str) -> bool | None: @@ -382,7 +388,6 @@ def do_mqtt(self, arg: str) -> bool | None: unpublish Disable publishing of a certain property """ - global MQTT_ENABLED global mqtt_client args = arg.split(' ') if not self._ensure_connected(): @@ -392,39 +397,38 @@ def do_mqtt(self, arg: str) -> bool | None: return if args[0] == "properties": print( - f"List of properties activated for MQTT publishing: {MQTT_PROPERTIES}") + f"List of properties activated for MQTT publishing: {Cfg.MQTT_PROPERTIES.val}") elif args[0] == "start": - MQTT_ENABLED = 'true' + Cfg.MQTT_ENABLED.val = True mqtt_client = mqtt_setup(self.wp) elif args[0] == "stop": mqtt_stop(mqtt_client) - MQTT_ENABLED = 'false' + Cfg.MQTT_ENABLED.val = False elif args[0] == "status": print( - f"MQTT client is {'enabled' if MQTT_ENABLED == 'true' else 'disabled'}.") + f"MQTT client is {'enabled' if Cfg.MQTT_ENABLED.val else 'disabled'}.") elif len(args) > 1 and args[0] in ['publish', 'unpublish']: self._mqtt_prop_cmds(args[0], args[1]) else: print(f"ERROR: Unsupported argument: {args[0]}") def _mqtt_prop_cmds(self, cmd, prop_name): - global MQTT_PROPERTIES global mqtt_client if prop_name not in self.wpdef["properties"]: print(f"ERROR: Undefined property '{prop_name}'!") - elif cmd == "publish" and prop_name not in MQTT_PROPERTIES: - MQTT_PROPERTIES.append(prop_name) - elif cmd == "unpublish" and prop_name in MQTT_PROPERTIES: - MQTT_PROPERTIES.remove(prop_name) + elif cmd == "publish" and prop_name not in Cfg.MQTT_PROPERTIES.val: + Cfg.MQTT_PROPERTIES.val.append(prop_name) + elif cmd == "unpublish" and prop_name in Cfg.MQTT_PROPERTIES.val: + Cfg.MQTT_PROPERTIES.val.remove(prop_name) def complete_mqtt(self, text, line, begidx, endidx): token = line.split(' ') if len(token) == 2: return self._complete_list(['properties', 'publish', 'start', 'status', 'stop', 'unpublish'], text) elif len(token) == 3 and token[1] == 'publish': - return self._complete_list([p for p in self._complete_propname(text, available_only=True) if p not in MQTT_PROPERTIES], text) + return self._complete_list([p for p in self._complete_propname(text, available_only=True) if p not in Cfg.MQTT_PROPERTIES.val], text) elif len(token) == 3 and token[1] == 'unpublish': - return self._complete_list(MQTT_PROPERTIES, text) + return self._complete_list(Cfg.MQTT_PROPERTIES.val, text) return [] def do_properties(self, arg: str) -> bool | None: @@ -726,11 +730,11 @@ def mqtt_get_decoded_property(pd, value): def mqtt_publish_property(wp, mqtt_client, pd, value, force_publish=False): prop_name = pd["key"] - if not (force_publish or MQTT_PROPERTIES == [''] or prop_name in MQTT_PROPERTIES): + if not (force_publish or not Cfg.MQTT_PROPERTIES.val or prop_name in Cfg.MQTT_PROPERTIES.val): _LOGGER.debug(f"Skipping publishing of property '{prop_name}' ...") return - property_topic = mqtt_subst_topic(MQTT_TOPIC_PROPERTY_STATE, { - "baseTopic": MQTT_TOPIC_BASE, + property_topic = mqtt_subst_topic(Cfg.MQTT_TOPIC_PROPERTY_STATE.val, { + "baseTopic": Cfg.MQTT_TOPIC_BASE.val, "serialNumber": wp.serial, "propName": prop_name, }) @@ -738,7 +742,7 @@ def mqtt_publish_property(wp, mqtt_client, pd, value, force_publish=False): _LOGGER.debug( f"Publishing property '{prop_name}' with value '{encoded_value}' to MQTT ...") mqtt_client.publish(property_topic, encoded_value, retain=True) - if WATTPILOT_SPLIT_PROPERTIES and "childProps" in pd: + if Cfg.WATTPILOT_SPLIT_PROPERTIES.val and "childProps" in pd: _LOGGER.debug( f"Splitting child props of property {prop_name} as {pd['jsonType']} for value {value} ...") for cpd in pd["childProps"]: @@ -752,24 +756,20 @@ def mqtt_publish_property(wp, mqtt_client, pd, value, force_publish=False): def mqtt_publish_message(event, message): _LOGGER.debug(f"""mqtt_publish_message(event={event},message={message})""") global mqtt_client - global MQTT_PUBLISH_MESSAGES - global MQTT_TOPIC_BASE - global MQTT_PUBLISH_PROPERTIES - global MQTT_TOPIC_MESSAGES global wpdef wp = event['wp'] if mqtt_client == None: _LOGGER.debug(f"Skipping MQTT message publishing.") return msg_dict = json.loads(message) - if MQTT_PUBLISH_MESSAGES == "true" and (MQTT_MESSAGES == [] or MQTT_MESSAGES == [''] or msg_dict["type"] in MQTT_MESSAGES): - message_topic = mqtt_subst_topic(MQTT_TOPIC_MESSAGES, { - "baseTopic": MQTT_TOPIC_BASE, + if Cfg.MQTT_PUBLISH_MESSAGES.val and (not Cfg.MQTT_MESSAGES.val or msg_dict["type"] in Cfg.MQTT_MESSAGES.val): + message_topic = mqtt_subst_topic(Cfg.MQTT_TOPIC_MESSAGES.val, { + "baseTopic": Cfg.MQTT_TOPIC_BASE.val, "serialNumber": wp.serial, "messageType": msg_dict["type"], }) mqtt_client.publish(message_topic, message) - if MQTT_PUBLISH_PROPERTIES == "true" and msg_dict["type"] in ["fullStatus", "deltaStatus"]: + if Cfg.MQTT_PUBLISH_PROPERTIES.val and msg_dict["type"] in ["fullStatus", "deltaStatus"]: for prop_name, value in msg_dict["status"].items(): pd = wpdef["properties"][prop_name] mqtt_publish_property(wp, mqtt_client, pd, value) @@ -779,9 +779,9 @@ def mqtt_publish_message(event, message): def mqtt_subst_topic(s, values, expand=True): if expand: - s = re.sub(r'^~', MQTT_TOPIC_PROPERTY_BASE, s) + s = re.sub(r'^~', Cfg.MQTT_TOPIC_PROPERTY_BASE.val, s) all_values = { - "baseTopic": MQTT_TOPIC_BASE, + "baseTopic": Cfg.MQTT_TOPIC_BASE.val, } | values return s.format(**all_values) @@ -804,27 +804,19 @@ def mqtt_setup_client(host, port, client_id, available_topic, command_topic, use def mqtt_setup(wp): - global MQTT_CLIENT_ID - global MQTT_HOST - global MQTT_PORT - global MQTT_PASSWORD - global MQTT_PROPERTIES - global MQTT_TOPIC_AVAILABLE - global MQTT_TOPIC_PROPERTY_SET _LOGGER.debug(f"mqtt_setup(wp)") - global MQTT_USERNAME # Connect to MQTT server: - mqtt_client = mqtt_setup_client(MQTT_HOST, MQTT_PORT, MQTT_CLIENT_ID, - mqtt_subst_topic(MQTT_TOPIC_AVAILABLE, {}), - mqtt_subst_topic(MQTT_TOPIC_PROPERTY_SET, { + mqtt_client = mqtt_setup_client(Cfg.MQTT_HOST.val, Cfg.MQTT_PORT.val, Cfg.MQTT_CLIENT_ID.val, + mqtt_subst_topic(Cfg.MQTT_TOPIC_AVAILABLE.val, {}), + mqtt_subst_topic(Cfg.MQTT_TOPIC_PROPERTY_SET.val, { "propName": "+"}), - MQTT_USERNAME, - MQTT_PASSWORD, + Cfg.MQTT_USERNAME.val, + Cfg.MQTT_PASSWORD.val, ) - MQTT_PROPERTIES = mqtt_get_watched_properties(wp) + Cfg.MQTT_PROPERTIES.val = mqtt_get_watched_properties(wp) _LOGGER.info( - f"Registering message callback to publish updates to the following properties to MQTT: {MQTT_PROPERTIES}") + f"Registering message callback to publish updates to the following properties to MQTT: {Cfg.MQTT_PROPERTIES.val}") wp.add_event_handler(wattpilot.Event.WS_MESSAGE, mqtt_publish_message) return mqtt_client @@ -840,7 +832,7 @@ def mqtt_stop(mqtt_client): def mqtt_set_value(client, userdata, message): global wpdef topic_regex = mqtt_subst_topic( - MQTT_TOPIC_PROPERTY_SET, {"propName": "([^/]+)"}) + Cfg.MQTT_TOPIC_PROPERTY_SET.val, {"propName": "([^/]+)"}) name = re.sub(topic_regex, r'\1', message.topic) if not name or name == "" or not wpdef["properties"][name]: _LOGGER.warning(f"Unknown property '{name}'!") @@ -855,11 +847,10 @@ def mqtt_set_value(client, userdata, message): def mqtt_get_watched_properties(wp): - global MQTT_PROPERTIES - if MQTT_PROPERTIES == [] or MQTT_PROPERTIES == ['']: + if not Cfg.MQTT_PROPERTIES.val: return list(wp.allProps.keys()) else: - return MQTT_PROPERTIES + return Cfg.MQTT_PROPERTIES.val #### Home Assistant Functions #### @@ -929,11 +920,6 @@ def ha_get_template_filter_from_json_type(json_type): def ha_discover_property(wp, mqtt_client, pd, disable_discovery=False, force_enablement=None): - global HA_TOPIC_CONFIG - global WATTPILOT_SPLIT_PROPERTIES - global MQTT_TOPIC_PROPERTY_BASE - global MQTT_TOPIC_PROPERTY_SET - global MQTT_TOPIC_PROPERTY_STATE name = pd["key"] ha_info = {} if "homeAssistant" in pd: @@ -957,14 +943,14 @@ def ha_discover_property(wp, mqtt_client, pd, disable_discovery=False, force_ena } ha_device = ha_get_device_info(wp) base_topic = mqtt_subst_topic( - MQTT_TOPIC_PROPERTY_BASE, topic_subst_map, False) + Cfg.MQTT_TOPIC_PROPERTY_BASE.val, topic_subst_map, False) ha_discovery_config = ha_get_default_config_for_prop(pd) | { "~": base_topic, "name": title, "object_id": object_id, "unique_id": unique_id, - "state_topic": mqtt_subst_topic(MQTT_TOPIC_PROPERTY_STATE, topic_subst_map, False), - "availability_topic": mqtt_subst_topic(MQTT_TOPIC_AVAILABLE, {}), + "state_topic": mqtt_subst_topic(Cfg.MQTT_TOPIC_PROPERTY_STATE.val, topic_subst_map, False), + "availability_topic": mqtt_subst_topic(Cfg.MQTT_TOPIC_AVAILABLE.val, {}), "payload_available": "online", "payload_not_available": "offline", "device": ha_device, @@ -973,14 +959,14 @@ def ha_discover_property(wp, mqtt_client, pd, disable_discovery=False, force_ena ha_discovery_config["options"] = list(pd["valueMap"].values()) if pd.get("rw", "") == "R/W": ha_discovery_config["command_topic"] = mqtt_subst_topic( - MQTT_TOPIC_PROPERTY_SET, topic_subst_map, False) + Cfg.MQTT_TOPIC_PROPERTY_SET.val, topic_subst_map, False) ha_discovery_config = dict( list(ha_discovery_config.items()) + list(ha_config.items()) ) if force_enablement != None: ha_discovery_config["enabled_by_default"] = force_enablement - topic_cfg = mqtt_subst_topic(HA_TOPIC_CONFIG, topic_subst_map) + topic_cfg = mqtt_subst_topic(Cfg.HA_TOPIC_CONFIG.val, topic_subst_map) if disable_discovery: payload = '' else: @@ -993,29 +979,27 @@ def ha_discover_property(wp, mqtt_client, pd, disable_discovery=False, force_ena if payload != "": del ha_discovery_config["command_topic"] payload = utils_value2json(ha_discovery_config) - mqtt_client.publish(mqtt_subst_topic(HA_TOPIC_CONFIG, topic_subst_map | { + mqtt_client.publish(mqtt_subst_topic(Cfg.HA_TOPIC_CONFIG.val, topic_subst_map | { "component": "sensor"}), payload, retain=True) - if WATTPILOT_SPLIT_PROPERTIES and "childProps" in pd: + if Cfg.WATTPILOT_SPLIT_PROPERTIES.val and "childProps" in pd: for p in pd["childProps"]: ha_discover_property(wp, mqtt_client, p, disable_discovery, force_enablement) def ha_is_default_prop(pd): - global HA_DISABLED_ENTITIES v = "homeAssistant" in pd - if HA_DISABLED_ENTITIES != 'true': + if not Cfg.HA_DISABLED_ENTITIES.val: ha = pd.get("homeAssistant", {}) if pd.get("homeAssistant", {}) else {} v = v and ha.get("config", {}).get("enabled_by_default", True) return v def ha_get_discovery_properties(): - global HA_PROPERTIES global wpdef _LOGGER.debug( - f"get_ha_discovery_properties(): HA_PROPERTIES='{HA_PROPERTIES}', propdef size='{len(wpdef['properties'])}'") - ha_properties = HA_PROPERTIES + f"get_ha_discovery_properties(): HA_PROPERTIES='{Cfg.HA_PROPERTIES.val}', propdef size='{len(wpdef['properties'])}'") + ha_properties = Cfg.HA_PROPERTIES.val if ha_properties == [''] or ha_properties == []: ha_properties = [p["key"] for p in wpdef["properties"].values() if ha_is_default_prop(p)] @@ -1034,11 +1018,10 @@ def ha_discover_properties(mqtt_client, ha_properties, disable_discovery=True): def ha_publish_initial_properties(wp, mqtt_client): - global HA_PROPERTIES global wpdef _LOGGER.info( f"Publishing all initial property values to MQTT to populate the entity values ...") - for prop_name in HA_PROPERTIES: + for prop_name in Cfg.HA_PROPERTIES.val: if prop_name in wp.allProps: value = wp.allProps[prop_name] pd = wpdef["properties"][prop_name] @@ -1046,22 +1029,18 @@ def ha_publish_initial_properties(wp, mqtt_client): def ha_setup(wp): - global HA_PROPERTIES - global HA_WAIT_INIT_S - global HA_WAIT_PROPS_MS - global MQTT_PROPERTIES global wpdef # Configure list of relevant properties: - HA_PROPERTIES = ha_get_discovery_properties() - if MQTT_PROPERTIES == [] or MQTT_PROPERTIES == ['']: - MQTT_PROPERTIES = HA_PROPERTIES + Cfg.HA_PROPERTIES.val = ha_get_discovery_properties() + if Cfg.MQTT_PROPERTIES.val == [] or Cfg.MQTT_PROPERTIES.val == ['']: + Cfg.MQTT_PROPERTIES.val = Cfg.HA_PROPERTIES.val # Setup MQTT client: mqtt_client = mqtt_setup(wp) # Publish HA discovery config: - ha_discover_properties(mqtt_client, HA_PROPERTIES, False) + ha_discover_properties(mqtt_client, Cfg.HA_PROPERTIES.val, False) # Wait a bit for HA to catch up: wait_time = math.ceil( - HA_WAIT_INIT_S + len(HA_PROPERTIES)*HA_WAIT_PROPS_MS*0.001) + Cfg.HA_WAIT_INIT_S.val + len(Cfg.HA_PROPERTIES.val)*Cfg.HA_WAIT_PROPS_MS.val*0.001) if wait_time > 0: _LOGGER.info( f"Waiting {wait_time}s to allow Home Assistant to discovery entities and subscribe MQTT topics before publishing initial values ...") @@ -1073,127 +1052,130 @@ def ha_setup(wp): def ha_stop(mqtt_client): - global HA_PROPERTIES - ha_discover_properties(mqtt_client, HA_PROPERTIES, True) + ha_discover_properties(mqtt_client, Cfg.HA_PROPERTIES.val, True) mqtt_stop(mqtt_client) +class Env(): + def __init__(self, datatype, default, description, name="", val="", required=False, requiredIf=None): + self.datatype = datatype + self.default = default + self.description = description + self.name = name + self.val = val + self.required = required + self.requiredIf = requiredIf + def format(self): + val = self.val + if self.datatype == "password" and self.val != "": + val = "********" + return f"{self.name}={val}" + + +# Wattpilot Configuration +class Cfg(Enum): + HA_DISABLED_ENTITIES = Env("boolean", "false", "Create disabled entities in Home Assistant") + HA_ENABLED = Env("boolean", "false", "Enable Home Assistant Discovery") + HA_PROPERTIES = Env("list", "", "List of space-separated properties that should be discovered by Home Assistant (leave unset for all properties having `homeAssistant` set in [wattpilot.yaml](src/wattpilot/ressources/wattpilot.yaml)") + HA_TOPIC_CONFIG = Env("string", "homeassistant/{component}/{uniqueId}/config", "Topic pattern for HA discovery config") + HA_WAIT_INIT_S = Env("integer", "0", "Wait initial number of seconds after starting discovery (in addition to wait time depending on the number of properties). May be increased, if entities in HA are not populated with values.") + HA_WAIT_PROPS_MS = Env("integer", "0", "Wait milliseconds per property after discovery before publishing property values. May be increased, if entities in HA are not populated with values.") + MQTT_AVAILABLE_PAYLOAD = Env("string", "online", "Payload for the availability topic in case the MQTT bridge is online") + MQTT_CLIENT_ID = Env("string", "wattpilot2mqtt", "MQTT client ID") + MQTT_ENABLED = Env("boolean", "false", "Enable MQTT") + MQTT_HOST = Env("string", "", "MQTT host to connect to", requiredIf='MQTT_ENABLED') + MQTT_MESSAGES = Env("list", "", "List of space-separated message types to be published to MQTT (leave unset for all messages)") + MQTT_NOT_AVAILABLE_PAYLOAD = Env("string", "offline", "Payload for the availability topic in case the MQTT bridge is offline (last will message)") + MQTT_PASSWORD = Env("password", "", "Password for connecting to MQTT") + MQTT_PORT = Env("integer", "1883", "Port of the MQTT host to connect to") + MQTT_PROPERTIES = Env("list", "", "List of space-separated property names to publish changes for (leave unset for all properties)") + MQTT_PUBLISH_MESSAGES = Env("boolean", "false", "Publish received Wattpilot messages to MQTT") + MQTT_PUBLISH_PROPERTIES = Env("boolean", "true", "Publish received property values to MQTT") + MQTT_TOPIC_AVAILABLE = Env("string", "{baseTopic}/available", "Topic pattern to publish Wattpilot availability status to") + MQTT_TOPIC_BASE = Env("string", "wattpilot", "Base topic for MQTT") + MQTT_TOPIC_MESSAGES = Env("string", "{baseTopic}/messages/{messageType}", "Topic pattern to publish Wattpilot messages to") + MQTT_TOPIC_PROPERTY_BASE = Env("string", "{baseTopic}/properties/{propName}", "Base topic for properties") + MQTT_TOPIC_PROPERTY_SET = Env("string", "~/set", "Topic pattern to listen for property value changes for") + MQTT_TOPIC_PROPERTY_STATE = Env("string", "~/state", "Topic pattern to publish property values to") + MQTT_USERNAME = Env("string", "", "Username for connecting to MQTT") + WATTPILOT_AUTOCONNECT = Env("boolean", "true", "Automatically connect to Wattpilot on startup") + WATTPILOT_AUTO_RECONNECT = Env("boolean", "true", "Automatically re-connect to Wattpilot on lost connections") + WATTPILOT_CONNECT_TIMEOUT = Env("integer", "30", "Connect timeout for Wattpilot connection") + WATTPILOT_HOST = Env("string", "", "IP address of the Wattpilot device to connect to", required=True) + WATTPILOT_INIT_TIMEOUT = Env("integer", "30", "Wait timeout for property initialization") + WATTPILOT_LOGLEVEL = Env("string", "INFO", "Log level (CRITICAL,ERROR,WARNING,INFO,DEBUG)") + WATTPILOT_PASSWORD = Env("password", "", "Password for connecting to the Wattpilot device", required=True) + WATTPILOT_RECONNECT_INTERVAL = Env("integer", "30", "Waiting time in seconds before a lost connection is re-connected") + WATTPILOT_SPLIT_PROPERTIES = Env("boolean", "true", "Whether compound properties (e.g. JSON arrays or objects) should be decomposed into separate properties") + + @classmethod + def set(cls, env: dict): + for var in list(cls): + #print(f"Setting parameter {var.name} ...") + d = var.value + d.name = var.name + strval = env.get(var.name, d.default) + if d.datatype == "boolean": + d.val = (strval == "true") + elif d.datatype == "integer": + d.val = int(strval) + elif d.datatype == "list": + d.val = strval.split(sep=' ') if strval else [] + elif d.datatype == "password": + d.val = strval + if strval != "": + strval = "********" + elif d.datatype == "string": + d.val = strval + _LOGGER.debug(f"{d.format()} (from '{strval}')") + assert not d.required or d.val, f"{var.name} is not set!" + for var in [e for e in list(cls) if e.value.requiredIf]: + d = var.value + assert not Cfg[d.requiredIf].value.val or d.val, f"{var.name} is not set (required for '{d.requiredIf}')!" + + @classmethod + def docs_markdown(cls): + print("|" + "|".join(["Environment Variable", "Type", "Default Value", "Description"]) + "|") + print("|" + "|".join(["--------------------", "----", "-------------", "-----------"]) + "|") + for e in list(cls): + d = e.value + print("|" + "|".join([f"`{e.name}`", f"`{d.datatype}`", f"{'`'+d.default+'`' if d.default else ''}", d.description]) + "|") + + @property + def val(self): + return self.value.val + + @val.setter + def val(self,value): + self.value.val = value -#### Main Program #### - -def main_setup_env(): - global HA_DISABLED_ENTITIES - global HA_ENABLED - global HA_PROPERTIES - global HA_TOPIC_CONFIG - global HA_WAIT_INIT_S - global HA_WAIT_PROPS_MS - global MQTT_AVAILABLE_PAYLOAD - global MQTT_CLIENT_ID - global MQTT_ENABLED - global MQTT_HOST - global MQTT_MESSAGES - global MQTT_NOT_AVAILABLE_PAYLOAD - global MQTT_PASSWORD - global MQTT_PORT - global MQTT_PROPERTIES - global MQTT_PUBLISH_MESSAGES - global MQTT_PUBLISH_PROPERTIES - global MQTT_TOPIC_AVAILABLE - global MQTT_TOPIC_BASE - global MQTT_TOPIC_MESSAGES - global MQTT_TOPIC_PROPERTY_BASE - global MQTT_TOPIC_PROPERTY_SET - global MQTT_TOPIC_PROPERTY_STATE - global MQTT_USERNAME - global WATTPILOT_AUTOCONNECT - global WATTPILOT_AUTO_RECONNECT - global WATTPILOT_CONNECT_TIMEOUT - global WATTPILOT_LOGLEVEL - global WATTPILOT_HOST - global WATTPILOT_INIT_TIMEOUT - global WATTPILOT_PASSWORD - global WATTPILOT_RECONNECT_INTERVAL - global WATTPILOT_SPLIT_PROPERTIES - HA_DISABLED_ENTITIES = os.environ.get('HA_DISABLED_ENTITIES', 'false') - HA_ENABLED = os.environ.get('HA_ENABLED', 'false') - HA_PROPERTIES = os.environ.get('HA_PROPERTIES', '').split(sep=' ') - HA_TOPIC_CONFIG = os.environ.get( - 'HA_TOPIC_CONFIG', 'homeassistant/{component}/{uniqueId}/config') - HA_WAIT_INIT_S = int(os.environ.get('HA_WAIT_INIT_S', '0')) - HA_WAIT_PROPS_MS = int(os.environ.get('HA_WAIT_PROPS_MS', '0')) - MQTT_AVAILABLE_PAYLOAD = os.environ.get('MQTT_AVAILABLE_PAYLOAD', 'online') - MQTT_CLIENT_ID = os.environ.get('MQTT_CLIENT_ID', 'wattpilot2mqtt') - MQTT_ENABLED = os.environ.get('MQTT_ENABLED', 'false') - MQTT_HOST = os.environ.get('MQTT_HOST', '') - MQTT_MESSAGES = os.environ.get('MQTT_MESSAGES', '').split(sep=' ') - MQTT_NOT_AVAILABLE_PAYLOAD = os.environ.get( - 'MQTT_NOT_AVAILABLE_PAYLOAD', 'offline') - MQTT_PASSWORD = os.environ.get('MQTT_PASSWORD', '') - MQTT_PORT = int(os.environ.get('MQTT_PORT', '1883')) - MQTT_PROPERTIES = os.environ.get('MQTT_PROPERTIES', '').split(sep=' ') - MQTT_PUBLISH_MESSAGES = os.environ.get('MQTT_PUBLISH_MESSAGES', 'false') - MQTT_PUBLISH_PROPERTIES = os.environ.get('MQTT_PUBLISH_PROPERTIES', 'true') - MQTT_TOPIC_AVAILABLE = os.environ.get( - 'MQTT_TOPIC_AVAILABLE', '{baseTopic}/available') - MQTT_TOPIC_BASE = os.environ.get('MQTT_TOPIC_BASE', 'wattpilot') - MQTT_TOPIC_MESSAGES = os.environ.get( - 'MQTT_TOPIC_MESSAGES', '{baseTopic}/messages/{messageType}') - MQTT_TOPIC_PROPERTY_BASE = os.environ.get( - 'MQTT_TOPIC_PROPERTY_BASE', '{baseTopic}/properties/{propName}') - MQTT_TOPIC_PROPERTY_SET = os.environ.get( - 'MQTT_TOPIC_PROPERTY_SET', '~/set') - MQTT_TOPIC_PROPERTY_STATE = os.environ.get( - 'MQTT_TOPIC_PROPERTY_STATE', '~/state') - MQTT_USERNAME = os.environ.get('MQTT_USERNAME', '') - WATTPILOT_AUTOCONNECT = os.environ.get('WATTPILOT_AUTOCONNECT', 'true') - WATTPILOT_AUTO_RECONNECT = os.environ.get('WATTPILOT_AUTO_RECONNECT', 'true') - WATTPILOT_CONNECT_TIMEOUT = int( - os.environ.get('WATTPILOT_CONNECT_TIMEOUT', '30')) - WATTPILOT_LOGLEVEL = os.environ.get('WATTPILOT_LOGLEVEL', 'INFO') - WATTPILOT_HOST = os.environ.get('WATTPILOT_HOST', '') - WATTPILOT_INIT_TIMEOUT = int( - os.environ.get('WATTPILOT_INIT_TIMEOUT', '30')) - WATTPILOT_PASSWORD = os.environ.get('WATTPILOT_PASSWORD', '') - WATTPILOT_RECONNECT_INTERVAL = int(os.environ.get('WATTPILOT_RECONNECT_INTERVAL', '30')) - WATTPILOT_SPLIT_PROPERTIES = bool( - os.environ.get('WATTPILOT_SPLIT_PROPERTIES', 'true')) - - # Ensure wattpilot host an password are set: - assert WATTPILOT_HOST != '', "WATTPILOT_HOST not set!" - assert WATTPILOT_PASSWORD != '', "WATTPILOT_PASSWORD not set!" - assert MQTT_ENABLED == 'false' or MQTT_HOST != '', 'MQTT_HOST not set!' +#### Main Program #### def main(): - global MQTT_ENABLED - global WATTPILOT_AUTOCONNECT - global WATTPILOT_HOST - global WATTPILOT_LOGLEVEL - global WATTPILOT_PASSWORD global mqtt_client global wp global wpdef - # Setup environment variables: - main_setup_env() - # Set debug level: - logging.basicConfig(level=WATTPILOT_LOGLEVEL) + logging.basicConfig(level=os.environ.get('WATTPILOT_LOGLEVEL','INFO').upper()) + + # Setup environment variables: + Cfg.set(os.environ) # Initialize globals: mqtt_client = None - wp = wp_initialize(WATTPILOT_HOST, WATTPILOT_PASSWORD) - wpdef = wp_read_apidef() + wp = wp_initialize(Cfg.WATTPILOT_HOST.val, Cfg.WATTPILOT_PASSWORD.val) + wpdef = wp_read_apidef() # TODO: Should be part of the wattpilot core library! # Initialize shell: wpsh = WattpilotShell(wp, wpdef) - if WATTPILOT_AUTOCONNECT == 'true': + if Cfg.WATTPILOT_AUTOCONNECT.val: _LOGGER.info("Automatically connecting to Wattpilot ...") wpsh.do_connect("") # Enable MQTT and/or HA integration: - if MQTT_ENABLED == "true" and HA_ENABLED == "false": + if Cfg.MQTT_ENABLED.val and not Cfg.HA_ENABLED.val: wpsh.do_mqtt("start") - elif MQTT_ENABLED == "true" and HA_ENABLED == "true": + elif Cfg.MQTT_ENABLED.val and Cfg.HA_ENABLED.val: wpsh.do_ha("start") wpsh.do_info("") if len(sys.argv) < 2: diff --git a/test-shell.sh b/test-shell.sh index 73594d7..cfb9de1 100755 --- a/test-shell.sh +++ b/test-shell.sh @@ -18,7 +18,7 @@ function runShell() { } function runShellOnly() { - WATTPILOT_AUTOCONNECT=false MQTT_ENABLED=false HA_ENABLED=false runShell "${@}" + WATTPILOT_AUTOCONNECT=false MQTT_ENABLED=false HA_ENABLED=false WATTPILOT_LOGLEVEL=WARN runShell "${@}" } function runShellWithProps() { @@ -84,7 +84,7 @@ case "${cmd}" in echo "# Wattpilot Shell Commands" for cmd in $( runShellOnly "help" \ - | awk 'BEGIN {p=0} {if(p) print $0} /^==/ {p=1}' \ + | grep -v -E '^(===+|.*:)$' \ | xargs -n 1 echo \ | grep -E -v '^EOF$' \ | sort \ @@ -98,19 +98,9 @@ case "${cmd}" in done ) >ShellCommands.md ( - # NOTE: The file cannot yet fully replace the table in README.md since the description is missing echo "# Wattpilot Shell Environment Variables" echo "" - echo "| Environment Variable | Default Value |" - echo "|----------------------|---------------|" - cat src/wattpilot/wattpilotshell.py \ - | awk 'BEGIN {p=0} /^ +/ {if(p) print $0} /^def / {p=0} /^def main_setup_env\(\):/ {p=1}' \ - | grep -E -v '\b(global|assert)\b' \ - | sed -re 's/#.*//g;s/\n//g' \ - | tr '\n' ' ' \ - | sed -re "s/os\\.environ\\.get\\(\\s*'([A-Z_]+)'\\s*,\\s*'([^']*)'\\s*\\)/\n| \`\1\` | \`\2\` |\n/g; s/\`\`//g" \ - | grep -E '^\|' \ - | sort + runShellOnly docs ) >ShellEnvVariables.md ;; validate-yaml) From fd65048cb4ae36c6e4e2dba414b2d85ac976cdfb Mon Sep 17 00:00:00 2001 From: Andreas Hochsteger Date: Fri, 10 Jun 2022 08:54:29 +0200 Subject: [PATCH 3/3] docs: update docs after merge --- ShellCommands.md | 24 ++++++++---------------- src/wattpilot/wattpilotshell.py | 3 ++- 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/ShellCommands.md b/ShellCommands.md index 5e910a1..f6bcb3c 100644 --- a/ShellCommands.md +++ b/ShellCommands.md @@ -77,22 +77,6 @@ Exit the shell Usage: exit ``` -## get - -```bash -Get a property value -Usage: get -``` - -## updateInverter - -```bash -(un)pairs a connected inverter -Usage: updateInverter (pair|unpair) - - is normally in the form 123.456789 -``` - ## ha ```bash @@ -372,6 +356,14 @@ Unwatch a message or property Usage: unwatch ``` +## UpdateInverter + +```bash +Performs an Inverter Operation +Usage: updateInverter pair|unpair + is normally in the form 123.456789 +``` + ## values ```bash diff --git a/src/wattpilot/wattpilotshell.py b/src/wattpilot/wattpilotshell.py index d36f2ab..c5b64c2 100644 --- a/src/wattpilot/wattpilotshell.py +++ b/src/wattpilot/wattpilotshell.py @@ -498,7 +498,8 @@ def do_propset(self, arg: str) -> bool | None: def do_UpdateInverter(self, arg: str) -> bool | None: """Performs an Inverter Operation -Usage: updateInverter pair|unpair """ +Usage: updateInverter pair|unpair + is normally in the form 123.456789""" global wp global wpdef args = arg.split(' ')