From 2656353e9ca534f1c51e644c2da1ed3eabb64c68 Mon Sep 17 00:00:00 2001 From: infeeeee Date: Wed, 11 Jan 2023 07:42:35 +0100 Subject: [PATCH 01/10] Monitor as switch, linked command with sensor, battery fix --- .../Entity/Deployments/Battery/Battery.py | 11 ++- .../Entity/Deployments/Monitor/Monitor.py | 74 +++++++++++++------ IoTuring/Logger/consts.py | 4 + .../HomeAssistantWarehouse.py | 44 +++++++++-- .../HomeAssistantWarehouse/entities.yaml | 6 +- 5 files changed, 101 insertions(+), 38 deletions(-) diff --git a/IoTuring/Entity/Deployments/Battery/Battery.py b/IoTuring/Entity/Deployments/Battery/Battery.py index 7da9f72b..bbc167ea 100644 --- a/IoTuring/Entity/Deployments/Battery/Battery.py +++ b/IoTuring/Entity/Deployments/Battery/Battery.py @@ -12,7 +12,11 @@ class Battery(Entity): def Initialize(self): self.RegisterEntitySensor(EntitySensor(self, KEY_PERCENTAGE)) - self.RegisterEntitySensor(EntitySensor(self, KEY_CHARGING_STATUS)) + + # Check if charging state working: + batteryInfo = self.GetBatteryInformation() + if isinstance(batteryInfo['charging'], bool): + self.RegisterEntitySensor(EntitySensor(self, KEY_CHARGING_STATUS)) def PostInitialize(self): # Check if battery infomration are present @@ -23,8 +27,9 @@ def Update(self): batteryInfo = self.GetBatteryInformation() self.SetEntitySensorValue(KEY_PERCENTAGE, int( batteryInfo['level']), ValueFormatter.Options(ValueFormatter.TYPE_PERCENTAGE)) - self.SetEntitySensorValue( - KEY_CHARGING_STATUS, str(batteryInfo['charging'])) + if isinstance(batteryInfo['charging'], bool): + self.SetEntitySensorValue( + KEY_CHARGING_STATUS, str(batteryInfo['charging'])) def GetBatteryInformation(self): battery = psutil.sensors_battery() diff --git a/IoTuring/Entity/Deployments/Monitor/Monitor.py b/IoTuring/Entity/Deployments/Monitor/Monitor.py index 254a3a77..a829117d 100644 --- a/IoTuring/Entity/Deployments/Monitor/Monitor.py +++ b/IoTuring/Entity/Deployments/Monitor/Monitor.py @@ -1,13 +1,15 @@ import subprocess import ctypes -import os as sys_os +import os +import re + from IoTuring.Entity.Entity import Entity -from ctypes import * +from IoTuring.Entity.EntityData import EntityCommand, EntitySensor +from IoTuring.Entity import consts +from IoTuring.Logger.consts import STATE_OFF, STATE_ON -from IoTuring.Entity.EntityData import EntityCommand -KEY_TURN_ALL_OFF = 'turn_all_off' -KEY_TURN_ALL_ON = 'turn_all_on' +KEY = 'monitor' class Monitor(Entity): @@ -20,26 +22,52 @@ def Initialize(self): def PostInitialize(self): self.os = self.GetDependentEntitySensorValue('Os', "operating_system") - if self.os == 'Windows' or (self.os == 'Linux' and sys_os.environ.get('DISPLAY')): + if self.os == consts.OS_FIXED_VALUE_LINUX: + # Check if xset is working: + p = subprocess.run( + ['xset', 'dpms'], capture_output=True, shell=False) + if p.stderr: + raise Exception(f"Xset dpms error: {p.stderr.decode()}") + elif not os.getenv('DISPLAY'): + raise Exception('No $DISPLAY environment variable!') + else: + supports_linux = True + + if self.os == consts.OS_FIXED_VALUE_WINDOWS: self.RegisterEntityCommand(EntityCommand( - self, KEY_TURN_ALL_OFF, self.CallbackTurnAllOff)) + self, KEY, self.Callback)) + elif supports_linux: + # Support for sending state on linux self.RegisterEntityCommand(EntityCommand( - self, KEY_TURN_ALL_ON, self.CallbackTurnAllOn)) - - def CallbackTurnAllOff(self, message): - if self.os == 'Windows': - ctypes.windll.user32.SendMessageA(0xFFFF, 0x0112, 0xF170, 2) - elif self.os == 'Linux': - # Check if X11 or something else - if sys_os.environ.get('DISPLAY'): - command = 'xset dpms force off' - subprocess.Popen(command.split(), stdout=subprocess.PIPE) + self, KEY, self.Callback)) + self.RegisterEntitySensor(EntitySensor(self, KEY)) + + def Callback(self, message): + payloadString = message.payload.decode('utf-8') - def CallbackTurnAllOn(self, message): - if self.os == 'Windows': - ctypes.windll.user32.SendMessageA(0xFFFF, 0x0112, 0xF170, -1) - elif self.os == 'Linux': - # Check if X11 or something else - if sys_os.environ.get('DISPLAY'): + if payloadString == STATE_ON: + if self.os == consts.OS_FIXED_VALUE_WINDOWS: + ctypes.windll.user32.SendMessageA(0xFFFF, 0x0112, 0xF170, -1) + elif self.os == consts.OS_FIXED_VALUE_LINUX: command = 'xset dpms force on' subprocess.Popen(command.split(), stdout=subprocess.PIPE) + + elif payloadString == STATE_OFF: + if self.os == consts.OS_FIXED_VALUE_WINDOWS: + ctypes.windll.user32.SendMessageA(0xFFFF, 0x0112, 0xF170, 2) + elif self.os == consts.OS_FIXED_VALUE_LINUX: + command = 'xset dpms force off' + subprocess.Popen(command.split(), stdout=subprocess.PIPE) + else: + raise Exception('Incorrect payload!') + + def Update(self): + if self.os == consts.OS_FIXED_VALUE_LINUX: + p = subprocess.run(['xset', 'q'], capture_output=True, shell=False) + outputString = p.stdout.decode() + monitorState = re.findall( + 'Monitor is (.{2,3})', outputString)[0].upper() + if monitorState in [STATE_OFF, STATE_ON]: + self.SetEntitySensorValue(KEY, monitorState) + else: + raise Exception(f'Incorrect monitor state: {monitorState}') diff --git a/IoTuring/Logger/consts.py b/IoTuring/Logger/consts.py index 5081b970..947e736c 100644 --- a/IoTuring/Logger/consts.py +++ b/IoTuring/Logger/consts.py @@ -11,6 +11,10 @@ LOG_DEVELOPMENT = 5 +# On/off states as strings: +STATE_ON = "ON" +STATE_OFF = "OFF" + # Fill start of string with spaces to jusitfy the message (0: no padding) # First for type, second for source STRINGS_LENGTH = [8, 30] diff --git a/IoTuring/Warehouse/Deployments/HomeAssistantWarehouse/HomeAssistantWarehouse.py b/IoTuring/Warehouse/Deployments/HomeAssistantWarehouse/HomeAssistantWarehouse.py index 63357730..d704e0a6 100644 --- a/IoTuring/Warehouse/Deployments/HomeAssistantWarehouse/HomeAssistantWarehouse.py +++ b/IoTuring/Warehouse/Deployments/HomeAssistantWarehouse/HomeAssistantWarehouse.py @@ -4,6 +4,8 @@ from IoTuring.Protocols.MQTTClient.MQTTClient import MQTTClient from IoTuring.Warehouse.Warehouse import Warehouse from IoTuring.MyApp.App import App +from IoTuring.Logger import consts + import json import yaml import re @@ -31,8 +33,10 @@ EXTERNAL_ENTITY_DATA_CONFIGURATION_KEY_CUSTOM_TYPE = "custom_type" LWT_TOPIC_SUFFIX = "LWT" -LWT_PAYLOAD_ONLINE = "ON" -LWT_PAYLOAD_OFFLINE = "OFF" +LWT_PAYLOAD_ONLINE = "ONLINE" +LWT_PAYLOAD_OFFLINE = "OFFLINE" +PAYLOAD_ON = consts.STATE_ON +PAYLOAD_OFF = consts.STATE_OFF class HomeAssistantWarehouse(Warehouse): @@ -103,7 +107,20 @@ def SendSensorsValues(self): def SendEntityDataConfigurations(self): self.SendLwtSensorConfiguration() for entity in self.GetEntities(): + + # Get entity data keys for checking links: + entityDataKeys = [ed.GetKey() for ed in entity.GetAllEntityData()] + for entityData in entity.GetAllEntityData(): + + # Check if it has linked command: + has_linked_command = False + if entityDataKeys.count(entityData.GetKey()) > 1: + if entityData in entity.GetEntitySensors(): + has_linked_command = True + else: # it's a linked command, so skip + continue + data_type = "" autoDiscoverySendTopic = "" payload = {} @@ -111,7 +128,9 @@ def SendEntityDataConfigurations(self): entityData.GetKey() # check the data type: can be edited from custom configurations - if entityData in entity.GetEntitySensors(): # it's an EntitySensorData + if has_linked_command: # it's a sensor with a linked command + data_type = "switch" + elif entityData in entity.GetEntitySensors(): # it's an EntitySensorData data_type = "sensor" else: # it's a EntityCommandData data_type = "button" @@ -142,13 +161,22 @@ def SendEntityDataConfigurations(self): payload['expire_after'] = 600 # TODO Improve payload['state_topic'] = self.MakeEntityDataTopic( entityData) - autoDiscoverySendTopic = TOPIC_AUTODISCOVERY_FORMAT.format( - data_type, App.getName(), payload['unique_id'].replace(".", "_")) - else: # it's a EntityCommandData + + # Add default payloads + if data_type in ["binary_sensor", "switch"]: + if not 'payload_on' in payload: + payload['payload_on'] = PAYLOAD_ON + if not 'payload_off' in payload: + payload['payload_off'] = PAYLOAD_OFF + + + if entityData in entity.GetEntityCommands() \ + or has_linked_command: # it's a EntityCommandData or a linked sensor payload['command_topic'] = self.MakeEntityDataTopic( entityData) - autoDiscoverySendTopic = TOPIC_AUTODISCOVERY_FORMAT.format( - data_type, App.getName(), payload['unique_id'].replace(".", "_")) + + autoDiscoverySendTopic = TOPIC_AUTODISCOVERY_FORMAT.format( + data_type, App.getName(), payload['unique_id'].replace(".", "_")) # Add availability configuration payload["availability_topic"] = self.MakeValuesTopic( diff --git a/IoTuring/Warehouse/Deployments/HomeAssistantWarehouse/entities.yaml b/IoTuring/Warehouse/Deployments/HomeAssistantWarehouse/entities.yaml index c9448050..b1d7ab45 100644 --- a/IoTuring/Warehouse/Deployments/HomeAssistantWarehouse/entities.yaml +++ b/IoTuring/Warehouse/Deployments/HomeAssistantWarehouse/entities.yaml @@ -50,10 +50,8 @@ Cpu: # if no matches with above CPU patterns, set this icon: "mdi:calculator-variant" Time: icon: "mdi:clock" -Monitor - turn_all_on: - icon: "mdi:monitor-star" -Monitor - turn_all_off: - icon: "mdi:monitor-off" +Monitor: + icon: "mdi:monitor-shimmer" AppInfo: icon: "mdi:information-outline" Temperature: From 063ae561b68cbfe549d53a12aeaf0803fba4bb8f Mon Sep 17 00:00:00 2001 From: Riccardo Briccola Date: Thu, 12 Jan 2023 18:17:54 +0100 Subject: [PATCH 02/10] Linked sensor to command manually, not with same KEY --- .../Entity/Deployments/Monitor/Monitor.py | 9 ++-- IoTuring/Entity/EntityData.py | 18 ++++++- .../HomeAssistantWarehouse.py | 51 ++++++++++--------- 3 files changed, 49 insertions(+), 29 deletions(-) diff --git a/IoTuring/Entity/Deployments/Monitor/Monitor.py b/IoTuring/Entity/Deployments/Monitor/Monitor.py index a829117d..bd71fafd 100644 --- a/IoTuring/Entity/Deployments/Monitor/Monitor.py +++ b/IoTuring/Entity/Deployments/Monitor/Monitor.py @@ -9,7 +9,8 @@ from IoTuring.Logger.consts import STATE_OFF, STATE_ON -KEY = 'monitor' +KEY_STATE = 'monitor_state' +KEY_CMD = 'monitor' class Monitor(Entity): @@ -35,12 +36,12 @@ def PostInitialize(self): if self.os == consts.OS_FIXED_VALUE_WINDOWS: self.RegisterEntityCommand(EntityCommand( - self, KEY, self.Callback)) + self, KEY_CMD, self.Callback)) elif supports_linux: # Support for sending state on linux + self.RegisterEntitySensor(EntitySensor(self, KEY_STATE)) self.RegisterEntityCommand(EntityCommand( - self, KEY, self.Callback)) - self.RegisterEntitySensor(EntitySensor(self, KEY)) + self, KEY_CMD, self.Callback, KEY_STATE)) def Callback(self, message): payloadString = message.payload.decode('utf-8') diff --git a/IoTuring/Entity/EntityData.py b/IoTuring/Entity/EntityData.py index 39ee3517..6d65a42d 100644 --- a/IoTuring/Entity/EntityData.py +++ b/IoTuring/Entity/EntityData.py @@ -62,9 +62,25 @@ def SetExtraAttributes(self, _dict): class EntityCommand(EntityData): - def __init__(self, entity, key, callbackFunction): + def __init__(self, entity, key, callbackFunction, connectedEntitySensorKey = None): + """ + If a key for the entity sensor is passed, warehouses that support it use this command as a switch with state. + Better to register the sensor before this command to avoud unexpected behaviours. + """ EntityData.__init__(self, entity, key) self.callbackFunction = callbackFunction + self.connectedEntitySensorKey = connectedEntitySensorKey + + def SupportsState(self): + return self.connectedEntitySensorKey is not None + + def GetConnectedEntitySensorKey(self): + # if this support state, return the key of the entity sensor that is connected to this command + # otherwise return None + if self.SupportsState(): + return self.connectedEntitySensorKey + else: + return None def CallCallback(self, message): """ Safely run callback for this command, passing the message (a paho.mqtt.client.MQTTMessage) """ diff --git a/IoTuring/Warehouse/Deployments/HomeAssistantWarehouse/HomeAssistantWarehouse.py b/IoTuring/Warehouse/Deployments/HomeAssistantWarehouse/HomeAssistantWarehouse.py index d704e0a6..4a680440 100644 --- a/IoTuring/Warehouse/Deployments/HomeAssistantWarehouse/HomeAssistantWarehouse.py +++ b/IoTuring/Warehouse/Deployments/HomeAssistantWarehouse/HomeAssistantWarehouse.py @@ -108,18 +108,16 @@ def SendEntityDataConfigurations(self): self.SendLwtSensorConfiguration() for entity in self.GetEntities(): - # Get entity data keys for checking links: - entityDataKeys = [ed.GetKey() for ed in entity.GetAllEntityData()] - + # Get sensors entity data linked to commands so they are not configured as sensor but + # will be set in command state which will be a switch + keys_of_sensors_connected_to_commands = [command.GetConnectedEntitySensorKey() for command in entity.GetEntityCommands() if command.SupportsState()] + for entityData in entity.GetAllEntityData(): - # Check if it has linked command: - has_linked_command = False - if entityDataKeys.count(entityData.GetKey()) > 1: - if entityData in entity.GetEntitySensors(): - has_linked_command = True - else: # it's a linked command, so skip - continue + # if I found a sensor data that will be configured only after, together with + # its command (as a switch) + if entityData.GetKey() in keys_of_sensors_connected_to_commands: + continue # it's a sensor linked to a command, so skip data_type = "" autoDiscoverySendTopic = "" @@ -127,12 +125,11 @@ def SendEntityDataConfigurations(self): payload['name'] = entity.GetEntityNameWithTag() + " - " + \ entityData.GetKey() - # check the data type: can be edited from custom configurations - if has_linked_command: # it's a sensor with a linked command - data_type = "switch" - elif entityData in entity.GetEntitySensors(): # it's an EntitySensorData + if entityData in entity.GetEntitySensors(): # it's an EntitySensorData data_type = "sensor" - else: # it's a EntityCommandData + elif entityData.SupportsState(): # it's a EntityCommandData: has it a state ? + data_type = "switch" + else: data_type = "button" # add custom info to the entity data, reading it from external file and accessing the information using the entity data name @@ -151,27 +148,33 @@ def SendEntityDataConfigurations(self): payload['unique_id'] = self.clientName + \ "." + entityData.GetId() - if entityData in entity.GetEntitySensors(): # it's an EntitySensorData + # add configurations about sensors or switches (both have a state) + if entityData in entity.GetEntitySensors() or data_type=='switch': # it's an EntitySensorData # If the sensor supports extra attributes, send them as JSON. - # So here I have to specify also the topic for those attrbitues - if entityData.DoesSupportExtraAttributes(): + # So here I have to specify also the topic for those attributes + # (not supported by switches) + if data_type!='switch' and entityData.DoesSupportExtraAttributes(): payload["json_attributes_topic"] = self.MakeEntityDataExtraAttributesTopic( entityData) payload['expire_after'] = 600 # TODO Improve - payload['state_topic'] = self.MakeEntityDataTopic( - entityData) + + + if data_type!='switch': + payload['state_topic'] = self.MakeEntityDataTopic( + entityData) + else: # it's a switch, so the state is the state of the connected sensor + payload['state_topic'] = self.MakeEntityDataTopic(entity.GetEntitySensorByKey(entityData.GetConnectedEntitySensorKey())) - # Add default payloads + # Add default payloads (only for ON/OFF entities) if data_type in ["binary_sensor", "switch"]: if not 'payload_on' in payload: payload['payload_on'] = PAYLOAD_ON if not 'payload_off' in payload: payload['payload_off'] = PAYLOAD_OFF - - if entityData in entity.GetEntityCommands() \ - or has_linked_command: # it's a EntityCommandData or a linked sensor + # if it's a command (so button or switch), configure the topic where the command will be called + if entityData in entity.GetEntityCommands(): payload['command_topic'] = self.MakeEntityDataTopic( entityData) From 7a05d830cefa594669f22a70dc53ee9474b2a2ef Mon Sep 17 00:00:00 2001 From: Riccardo Briccola Date: Thu, 12 Jan 2023 18:18:09 +0100 Subject: [PATCH 03/10] Add missing variable to monitor --- IoTuring/Entity/Deployments/Monitor/Monitor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/IoTuring/Entity/Deployments/Monitor/Monitor.py b/IoTuring/Entity/Deployments/Monitor/Monitor.py index bd71fafd..cc4b22b4 100644 --- a/IoTuring/Entity/Deployments/Monitor/Monitor.py +++ b/IoTuring/Entity/Deployments/Monitor/Monitor.py @@ -22,7 +22,8 @@ def Initialize(self): def PostInitialize(self): self.os = self.GetDependentEntitySensorValue('Os', "operating_system") - + + supports_linux = False if self.os == consts.OS_FIXED_VALUE_LINUX: # Check if xset is working: p = subprocess.run( From c0d7d072466ed838637c2d6617ad488f916cbb9c Mon Sep 17 00:00:00 2001 From: infeeeee Date: Thu, 12 Jan 2023 20:52:36 +0100 Subject: [PATCH 04/10] FileSwitch entity --- .../Deployments/FileSwitch/FileSwitch.py | 54 +++++++++++++++++++ .../HomeAssistantWarehouse/entities.yaml | 4 ++ 2 files changed, 58 insertions(+) create mode 100644 IoTuring/Entity/Deployments/FileSwitch/FileSwitch.py diff --git a/IoTuring/Entity/Deployments/FileSwitch/FileSwitch.py b/IoTuring/Entity/Deployments/FileSwitch/FileSwitch.py new file mode 100644 index 00000000..caf91911 --- /dev/null +++ b/IoTuring/Entity/Deployments/FileSwitch/FileSwitch.py @@ -0,0 +1,54 @@ +from pathlib import Path + +from IoTuring.Entity.Entity import Entity +from IoTuring.Entity.EntityData import EntityCommand, EntitySensor +from IoTuring.Entity import consts +from IoTuring.Logger.consts import STATE_OFF, STATE_ON +from IoTuring.Configurator.MenuPreset import MenuPreset + +KEY_STATE = 'fileswitch_state' +KEY_CMD = 'fileswitch' + +CONFIG_KEY_PATH = 'path' + + +class FileSwitch(Entity): + NAME = "FileSwitch" + ALLOW_MULTI_INSTANCE = True + + def Initialize(self): + + try: + self.config_path = self.GetConfigurations()[CONFIG_KEY_PATH] + except Exception as e: + raise Exception("Configuration error: " + str(e)) + + self.RegisterEntitySensor(EntitySensor(self, KEY_STATE)) + self.RegisterEntityCommand(EntityCommand( + self, KEY_CMD, self.Callback, KEY_STATE)) + + def PostInitialize(self): + pass + + def Callback(self, message): + payloadString = message.payload.decode('utf-8') + + if payloadString == "True": + Path(self.config_path).touch() + + elif payloadString == "False": + Path(self.config_path).unlink(missing_ok=True) + + else: + raise Exception('Incorrect payload!') + + def Update(self): + + self.SetEntitySensorValue(KEY_STATE, + str(Path(self.config_path).exists())) + + @classmethod + def ConfigurationPreset(self): + preset = MenuPreset() + preset.AddEntry("Path to file?", CONFIG_KEY_PATH, mandatory=True) + return preset diff --git a/IoTuring/Warehouse/Deployments/HomeAssistantWarehouse/entities.yaml b/IoTuring/Warehouse/Deployments/HomeAssistantWarehouse/entities.yaml index b1d7ab45..5717e83e 100644 --- a/IoTuring/Warehouse/Deployments/HomeAssistantWarehouse/entities.yaml +++ b/IoTuring/Warehouse/Deployments/HomeAssistantWarehouse/entities.yaml @@ -57,3 +57,7 @@ AppInfo: Temperature: icon: "mdi:thermometer-lines" unit_of_measurement: "°C" +FileSwitch: + icon: mdi:file-star + payload_on: "True" + payload_off: "False" \ No newline at end of file From 4bc67c898cb06350a02b3f1205f0bd1e9c1aae33 Mon Sep 17 00:00:00 2001 From: infeeeee Date: Thu, 12 Jan 2023 21:06:09 +0100 Subject: [PATCH 05/10] Remove unused imports, fix key on Monitor update --- IoTuring/Entity/Deployments/FileSwitch/FileSwitch.py | 3 --- IoTuring/Entity/Deployments/Monitor/Monitor.py | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/IoTuring/Entity/Deployments/FileSwitch/FileSwitch.py b/IoTuring/Entity/Deployments/FileSwitch/FileSwitch.py index caf91911..14b72066 100644 --- a/IoTuring/Entity/Deployments/FileSwitch/FileSwitch.py +++ b/IoTuring/Entity/Deployments/FileSwitch/FileSwitch.py @@ -2,8 +2,6 @@ from IoTuring.Entity.Entity import Entity from IoTuring.Entity.EntityData import EntityCommand, EntitySensor -from IoTuring.Entity import consts -from IoTuring.Logger.consts import STATE_OFF, STATE_ON from IoTuring.Configurator.MenuPreset import MenuPreset KEY_STATE = 'fileswitch_state' @@ -43,7 +41,6 @@ def Callback(self, message): raise Exception('Incorrect payload!') def Update(self): - self.SetEntitySensorValue(KEY_STATE, str(Path(self.config_path).exists())) diff --git a/IoTuring/Entity/Deployments/Monitor/Monitor.py b/IoTuring/Entity/Deployments/Monitor/Monitor.py index cc4b22b4..3df0930d 100644 --- a/IoTuring/Entity/Deployments/Monitor/Monitor.py +++ b/IoTuring/Entity/Deployments/Monitor/Monitor.py @@ -70,6 +70,6 @@ def Update(self): monitorState = re.findall( 'Monitor is (.{2,3})', outputString)[0].upper() if monitorState in [STATE_OFF, STATE_ON]: - self.SetEntitySensorValue(KEY, monitorState) + self.SetEntitySensorValue(KEY_STATE, monitorState) else: raise Exception(f'Incorrect monitor state: {monitorState}') From ad49e95d24be1a2aa1b646a2ac95f05050fe7bc4 Mon Sep 17 00:00:00 2001 From: Riccardo Briccola Date: Sun, 15 Jan 2023 01:21:11 +0100 Subject: [PATCH 06/10] HA wh: send sensor to command topic if switch entity --- .../HomeAssistantWarehouse.py | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/IoTuring/Warehouse/Deployments/HomeAssistantWarehouse/HomeAssistantWarehouse.py b/IoTuring/Warehouse/Deployments/HomeAssistantWarehouse/HomeAssistantWarehouse.py index 4a680440..95c6faa5 100644 --- a/IoTuring/Warehouse/Deployments/HomeAssistantWarehouse/HomeAssistantWarehouse.py +++ b/IoTuring/Warehouse/Deployments/HomeAssistantWarehouse/HomeAssistantWarehouse.py @@ -95,11 +95,26 @@ def Loop(self): def SendSensorsValues(self): """ Here I send sensor's data (command callbacks are not managed here) """ + for entity in self.GetEntities(): + + # switches are commands with state, their state comes from a sensor. + # To work correctly we need to send sensor data on command topic (to avoid status blinking on HA) + # so we get sensors that will be switches (and their commands to get the correct + # topic where to send data) + switch_sensors = {} # key: sensor key, value: command (for topic) + for possible_switch in entity.GetEntityCommands(): + if possible_switch.SupportsState(): + switch_sensors[possible_switch.GetConnectedEntitySensorKey()] = possible_switch + for entitySensor in entity.GetEntitySensors(): if (entitySensor.HasValue()): - self.client.SendTopicData(self.MakeEntityDataTopic( - entitySensor), entitySensor.GetValue()) + # topic: get from sensor data if sensor, get from its command data if sensor belongs to a switch + if not entitySensor.GetKey() in switch_sensors: + topic = self.MakeEntityDataTopic(entitySensor) + else: + topic = self.MakeEntityDataTopic(switch_sensors[entitySensor.GetKey()]) + self.client.SendTopicData(topic, entitySensor.GetValue()) # send if (entitySensor.HasExtraAttributes()): # send as json self.client.SendTopicData(self.MakeEntityDataExtraAttributesTopic(entitySensor), json.dumps(entitySensor.GetExtraAttributes())) @@ -163,8 +178,8 @@ def SendEntityDataConfigurations(self): if data_type!='switch': payload['state_topic'] = self.MakeEntityDataTopic( entityData) - else: # it's a switch, so the state is the state of the connected sensor - payload['state_topic'] = self.MakeEntityDataTopic(entity.GetEntitySensorByKey(entityData.GetConnectedEntitySensorKey())) + else: # it's a switch, so the state is at the same topic of the command + payload['state_topic'] = self.MakeEntityDataTopic(entityData) # Add default payloads (only for ON/OFF entities) if data_type in ["binary_sensor", "switch"]: From 4a8ce94a1ddadda47340a891a43525343c5d5bc8 Mon Sep 17 00:00:00 2001 From: Riccardo Briccola Date: Sun, 15 Jan 2023 01:40:46 +0100 Subject: [PATCH 07/10] Add extra attributes support for switches --- IoTuring/Entity/Deployments/FileSwitch/FileSwitch.py | 6 +++++- .../HomeAssistantWarehouse/HomeAssistantWarehouse.py | 8 ++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/IoTuring/Entity/Deployments/FileSwitch/FileSwitch.py b/IoTuring/Entity/Deployments/FileSwitch/FileSwitch.py index 14b72066..f7253010 100644 --- a/IoTuring/Entity/Deployments/FileSwitch/FileSwitch.py +++ b/IoTuring/Entity/Deployments/FileSwitch/FileSwitch.py @@ -21,7 +21,7 @@ def Initialize(self): except Exception as e: raise Exception("Configuration error: " + str(e)) - self.RegisterEntitySensor(EntitySensor(self, KEY_STATE)) + self.RegisterEntitySensor(EntitySensor(self, KEY_STATE, True)) self.RegisterEntityCommand(EntityCommand( self, KEY_CMD, self.Callback, KEY_STATE)) @@ -43,6 +43,10 @@ def Callback(self, message): def Update(self): self.SetEntitySensorValue(KEY_STATE, str(Path(self.config_path).exists())) + + extra = {} + extra["exists"] = str(Path(self.config_path).exists()) + self.SetEntitySensorExtraAttributes(KEY_STATE, extra) @classmethod def ConfigurationPreset(self): diff --git a/IoTuring/Warehouse/Deployments/HomeAssistantWarehouse/HomeAssistantWarehouse.py b/IoTuring/Warehouse/Deployments/HomeAssistantWarehouse/HomeAssistantWarehouse.py index 95c6faa5..2d4d13d0 100644 --- a/IoTuring/Warehouse/Deployments/HomeAssistantWarehouse/HomeAssistantWarehouse.py +++ b/IoTuring/Warehouse/Deployments/HomeAssistantWarehouse/HomeAssistantWarehouse.py @@ -115,7 +115,7 @@ def SendSensorsValues(self): else: topic = self.MakeEntityDataTopic(switch_sensors[entitySensor.GetKey()]) self.client.SendTopicData(topic, entitySensor.GetValue()) # send - if (entitySensor.HasExtraAttributes()): # send as json + if (entitySensor.HasExtraAttributes()): # send as json - even if it's a switch, that topic is okay self.client.SendTopicData(self.MakeEntityDataExtraAttributesTopic(entitySensor), json.dumps(entitySensor.GetExtraAttributes())) @@ -167,10 +167,14 @@ def SendEntityDataConfigurations(self): if entityData in entity.GetEntitySensors() or data_type=='switch': # it's an EntitySensorData # If the sensor supports extra attributes, send them as JSON. # So here I have to specify also the topic for those attributes - # (not supported by switches) + # - for real sensors - if data_type!='switch' and entityData.DoesSupportExtraAttributes(): payload["json_attributes_topic"] = self.MakeEntityDataExtraAttributesTopic( entityData) + # - for sensors that became switches: entityData is the command so I need to retrieve the sensor and do upper operations - + elif data_type=='switch' and entity.GetEntitySensorByKey(entityData.GetConnectedEntitySensorKey()).DoesSupportExtraAttributes(): + payload["json_attributes_topic"] = self.MakeEntityDataExtraAttributesTopic( + entity.GetEntitySensorByKey(entityData.GetConnectedEntitySensorKey())) payload['expire_after'] = 600 # TODO Improve From f73a8e9a396e9bc2c310181fb19cb0caaa702eae Mon Sep 17 00:00:00 2001 From: Riccardo Briccola Date: Mon, 23 Jan 2023 20:35:51 +0100 Subject: [PATCH 08/10] Send callback received state back to HA via sensor topic --- IoTuring/Entity/EntityData.py | 5 ++++- .../HomeAssistantWarehouse.py | 16 +++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/IoTuring/Entity/EntityData.py b/IoTuring/Entity/EntityData.py index 6d65a42d..d611001a 100644 --- a/IoTuring/Entity/EntityData.py +++ b/IoTuring/Entity/EntityData.py @@ -83,12 +83,15 @@ def GetConnectedEntitySensorKey(self): return None def CallCallback(self, message): - """ Safely run callback for this command, passing the message (a paho.mqtt.client.MQTTMessage) """ + """ Safely run callback for this command, passing the message (a paho.mqtt.client.MQTTMessage). + Reutrns True if callback was run correctly, False if an error occurred.""" self.Log(self.LOG_DEBUG, "Callback") try: self.RunCallback(message) + return True except Exception as e: self.Log(self.LOG_ERROR, "Error while running callback: " + str(e)) + return False def RunCallback(self, message): """ Called only by CallCallback. diff --git a/IoTuring/Warehouse/Deployments/HomeAssistantWarehouse/HomeAssistantWarehouse.py b/IoTuring/Warehouse/Deployments/HomeAssistantWarehouse/HomeAssistantWarehouse.py index 2d4d13d0..0514641b 100644 --- a/IoTuring/Warehouse/Deployments/HomeAssistantWarehouse/HomeAssistantWarehouse.py +++ b/IoTuring/Warehouse/Deployments/HomeAssistantWarehouse/HomeAssistantWarehouse.py @@ -70,10 +70,24 @@ def RegisterEntityCommands(self): for entity in self.GetEntities(): for entityCommand in entity.GetEntityCommands(): self.client.AddNewTopicToSubscribeTo( - self.MakeEntityDataTopic(entityCommand), entityCommand.CallCallback) + self.MakeEntityDataTopic(entityCommand), self.GenerateCommandCallback(entityCommand)) self.Log(self.LOG_DEBUG, entityCommand.GetId( ) + " subscribed to " + self.MakeEntityDataTopic(entityCommand)) + + def GenerateCommandCallback(self, entityCommand): + """ Generates a lambda function that will become the command callback. + Obviously this lamda will call the default callback of the command. + But will also send the state to the state topic of the sensor relative to the command, + in case the command has a sensor connected to it (= is a switch). + This is needed to avoid status blinking on HA.""" + def CommandCallback(message): + status = entityCommand.CallCallback(message) # True: success, False: error + if status and self.client.IsConnected(): + if entityCommand.SupportsState(): + self.client.SendTopicData(entityCommand.GetConnectedEntitySensorKey(), message.payload.decode('utf-8')) + return CommandCallback + def Loop(self): # Send online state self.client.SendTopicData(self.MakeValuesTopic( From 487592d5fefead94be9bbeee1c4dbcaaf5ced2e0 Mon Sep 17 00:00:00 2001 From: Riccardo Briccola Date: Tue, 24 Jan 2023 11:12:35 +0100 Subject: [PATCH 09/10] Fix callback for HA, better state manage for cmds --- IoTuring/Entity/EntityData.py | 15 ++-- .../HomeAssistantWarehouse.py | 74 +++++++++---------- 2 files changed, 44 insertions(+), 45 deletions(-) diff --git a/IoTuring/Entity/EntityData.py b/IoTuring/Entity/EntityData.py index d611001a..65636e65 100644 --- a/IoTuring/Entity/EntityData.py +++ b/IoTuring/Entity/EntityData.py @@ -8,6 +8,10 @@ def __init__(self, entity, key): self.entityId = entity.GetEntityId() self.id = self.entityId + "." + key self.key = key + self.entity = entity + + def GetEntity(self): + return self.entity def GetId(self): return self.id @@ -74,13 +78,10 @@ def __init__(self, entity, key, callbackFunction, connectedEntitySensorKey = Non def SupportsState(self): return self.connectedEntitySensorKey is not None - def GetConnectedEntitySensorKey(self): - # if this support state, return the key of the entity sensor that is connected to this command - # otherwise return None - if self.SupportsState(): - return self.connectedEntitySensorKey - else: - return None + def GetConnectedEntitySensor(self): + """ Returns the entity sensor connected to this command, if this command supports state. + Otherwise returns None. """ + return self.GetEntity().GetEntitySensorByKey(self.connectedEntitySensorKey) def CallCallback(self, message): """ Safely run callback for this command, passing the message (a paho.mqtt.client.MQTTMessage). diff --git a/IoTuring/Warehouse/Deployments/HomeAssistantWarehouse/HomeAssistantWarehouse.py b/IoTuring/Warehouse/Deployments/HomeAssistantWarehouse/HomeAssistantWarehouse.py index 0514641b..b4b82425 100644 --- a/IoTuring/Warehouse/Deployments/HomeAssistantWarehouse/HomeAssistantWarehouse.py +++ b/IoTuring/Warehouse/Deployments/HomeAssistantWarehouse/HomeAssistantWarehouse.py @@ -9,7 +9,7 @@ import json import yaml import re -import time +import time SLEEP_TIME_NOT_CONNECTED_WHILE = 1 @@ -74,7 +74,6 @@ def RegisterEntityCommands(self): self.Log(self.LOG_DEBUG, entityCommand.GetId( ) + " subscribed to " + self.MakeEntityDataTopic(entityCommand)) - def GenerateCommandCallback(self, entityCommand): """ Generates a lambda function that will become the command callback. Obviously this lamda will call the default callback of the command. @@ -82,10 +81,14 @@ def GenerateCommandCallback(self, entityCommand): in case the command has a sensor connected to it (= is a switch). This is needed to avoid status blinking on HA.""" def CommandCallback(message): - status = entityCommand.CallCallback(message) # True: success, False: error + status = entityCommand.CallCallback( + message) # True: success, False: error if status and self.client.IsConnected(): if entityCommand.SupportsState(): - self.client.SendTopicData(entityCommand.GetConnectedEntitySensorKey(), message.payload.decode('utf-8')) + self.Log(self.LOG_DEBUG, "Switch callback: sending state to " + + self.MakeEntityDataTopicForSensorByCommandIfSwitch(entityCommand)) + self.client.SendTopicData(self.MakeEntityDataTopicForSensorByCommandIfSwitch( + entityCommand), message.payload.decode('utf-8')) return CommandCallback def Loop(self): @@ -109,44 +112,31 @@ def Loop(self): def SendSensorsValues(self): """ Here I send sensor's data (command callbacks are not managed here) """ - for entity in self.GetEntities(): - - # switches are commands with state, their state comes from a sensor. - # To work correctly we need to send sensor data on command topic (to avoid status blinking on HA) - # so we get sensors that will be switches (and their commands to get the correct - # topic where to send data) - switch_sensors = {} # key: sensor key, value: command (for topic) - for possible_switch in entity.GetEntityCommands(): - if possible_switch.SupportsState(): - switch_sensors[possible_switch.GetConnectedEntitySensorKey()] = possible_switch - for entitySensor in entity.GetEntitySensors(): if (entitySensor.HasValue()): - # topic: get from sensor data if sensor, get from its command data if sensor belongs to a switch - if not entitySensor.GetKey() in switch_sensors: - topic = self.MakeEntityDataTopic(entitySensor) - else: - topic = self.MakeEntityDataTopic(switch_sensors[entitySensor.GetKey()]) - self.client.SendTopicData(topic, entitySensor.GetValue()) # send - if (entitySensor.HasExtraAttributes()): # send as json - even if it's a switch, that topic is okay + topic = self.MakeEntityDataTopic(entitySensor) + self.client.SendTopicData( + topic, entitySensor.GetValue()) # send + if (entitySensor.HasExtraAttributes()): self.client.SendTopicData(self.MakeEntityDataExtraAttributesTopic(entitySensor), json.dumps(entitySensor.GetExtraAttributes())) def SendEntityDataConfigurations(self): self.SendLwtSensorConfiguration() for entity in self.GetEntities(): - + # Get sensors entity data linked to commands so they are not configured as sensor but # will be set in command state which will be a switch - keys_of_sensors_connected_to_commands = [command.GetConnectedEntitySensorKey() for command in entity.GetEntityCommands() if command.SupportsState()] - + keys_of_sensors_connected_to_commands = [command.GetConnectedEntitySensor( + ).GetKey() for command in entity.GetEntityCommands() if command.SupportsState()] + for entityData in entity.GetAllEntityData(): # if I found a sensor data that will be configured only after, together with # its command (as a switch) if entityData.GetKey() in keys_of_sensors_connected_to_commands: - continue # it's a sensor linked to a command, so skip + continue # it's a sensor linked to a command, so skip data_type = "" autoDiscoverySendTopic = "" @@ -178,26 +168,26 @@ def SendEntityDataConfigurations(self): "." + entityData.GetId() # add configurations about sensors or switches (both have a state) - if entityData in entity.GetEntitySensors() or data_type=='switch': # it's an EntitySensorData + if entityData in entity.GetEntitySensors() or data_type == 'switch': # it's an EntitySensorData # If the sensor supports extra attributes, send them as JSON. # So here I have to specify also the topic for those attributes - # - for real sensors - - if data_type!='switch' and entityData.DoesSupportExtraAttributes(): + # - for real sensors - + if data_type != 'switch' and entityData.DoesSupportExtraAttributes(): payload["json_attributes_topic"] = self.MakeEntityDataExtraAttributesTopic( entityData) - # - for sensors that became switches: entityData is the command so I need to retrieve the sensor and do upper operations - - elif data_type=='switch' and entity.GetEntitySensorByKey(entityData.GetConnectedEntitySensorKey()).DoesSupportExtraAttributes(): + # - for sensors that became switches: entityData is the command so I need to retrieve the sensor and check there if supports estra attributes and get from there the topic + elif data_type == 'switch' and entityData.GetConnectedEntitySensor().DoesSupportExtraAttributes(): payload["json_attributes_topic"] = self.MakeEntityDataExtraAttributesTopic( - entity.GetEntitySensorByKey(entityData.GetConnectedEntitySensorKey())) + entityData.GetConnectedEntitySensor()) payload['expire_after'] = 600 # TODO Improve - - - if data_type!='switch': + + if data_type != 'switch': payload['state_topic'] = self.MakeEntityDataTopic( entityData) - else: # it's a switch, so the state is at the same topic of the command - payload['state_topic'] = self.MakeEntityDataTopic(entityData) + else: # it's a switch, so the key of the sensor to generate the topic is found in the command + payload['state_topic'] = self.MakeEntityDataTopicForSensorByCommandIfSwitch( + entityData) # Add default payloads (only for ON/OFF entities) if data_type in ["binary_sensor", "switch"]: @@ -210,7 +200,7 @@ def SendEntityDataConfigurations(self): if entityData in entity.GetEntityCommands(): payload['command_topic'] = self.MakeEntityDataTopic( entityData) - + autoDiscoverySendTopic = TOPIC_AUTODISCOVERY_FORMAT.format( data_type, App.getName(), payload['unique_id'].replace(".", "_")) @@ -260,6 +250,14 @@ def AddEntityDataCustomConfigurations(self, entityDataName, payload): return {**payload, **entityDataConfiguration} return payload # if nothing found + def MakeEntityDataTopicForSensorByCommandIfSwitch(self, entityData): + """ If the entityData is a command, returns the topic of the sensor connected to it """ + if entityData.SupportsState(): + return self.MakeEntityDataTopic(entityData.GetConnectedEntitySensor()) + else: + raise Exception(entityData.GetID() + + " is not a switch, can't get its sensor topic") + def MakeEntityDataTopic(self, entityData): """ Uses MakeValuesTopic but receives an EntityData to manage itself its id""" return self.MakeValuesTopic(entityData.GetId()) From b88c0404a055a439d13a0528906c86603155579c Mon Sep 17 00:00:00 2001 From: infeeeee Date: Thu, 26 Jan 2023 01:24:17 +0100 Subject: [PATCH 10/10] Support for switches without state, Fileswitch extra attribute --- .../Deployments/FileSwitch/FileSwitch.py | 2 +- .../HomeAssistantWarehouse.py | 43 +++++++++++-------- 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/IoTuring/Entity/Deployments/FileSwitch/FileSwitch.py b/IoTuring/Entity/Deployments/FileSwitch/FileSwitch.py index f7253010..42aa12f1 100644 --- a/IoTuring/Entity/Deployments/FileSwitch/FileSwitch.py +++ b/IoTuring/Entity/Deployments/FileSwitch/FileSwitch.py @@ -45,7 +45,7 @@ def Update(self): str(Path(self.config_path).exists())) extra = {} - extra["exists"] = str(Path(self.config_path).exists()) + extra["Path"] = str(self.config_path) self.SetEntitySensorExtraAttributes(KEY_STATE, extra) @classmethod diff --git a/IoTuring/Warehouse/Deployments/HomeAssistantWarehouse/HomeAssistantWarehouse.py b/IoTuring/Warehouse/Deployments/HomeAssistantWarehouse/HomeAssistantWarehouse.py index b4b82425..dceb85a5 100644 --- a/IoTuring/Warehouse/Deployments/HomeAssistantWarehouse/HomeAssistantWarehouse.py +++ b/IoTuring/Warehouse/Deployments/HomeAssistantWarehouse/HomeAssistantWarehouse.py @@ -128,8 +128,10 @@ def SendEntityDataConfigurations(self): # Get sensors entity data linked to commands so they are not configured as sensor but # will be set in command state which will be a switch - keys_of_sensors_connected_to_commands = [command.GetConnectedEntitySensor( - ).GetKey() for command in entity.GetEntityCommands() if command.SupportsState()] + keys_of_sensors_connected_to_commands = \ + [command.GetConnectedEntitySensor().GetKey() + for command in entity.GetEntityCommands() + if command.SupportsState()] for entityData in entity.GetAllEntityData(): @@ -138,6 +140,7 @@ def SendEntityDataConfigurations(self): if entityData.GetKey() in keys_of_sensors_connected_to_commands: continue # it's a sensor linked to a command, so skip + entitycommand_supports_state = False data_type = "" autoDiscoverySendTopic = "" payload = {} @@ -148,6 +151,7 @@ def SendEntityDataConfigurations(self): data_type = "sensor" elif entityData.SupportsState(): # it's a EntityCommandData: has it a state ? data_type = "switch" + entitycommand_supports_state = True else: data_type = "button" @@ -168,33 +172,36 @@ def SendEntityDataConfigurations(self): "." + entityData.GetId() # add configurations about sensors or switches (both have a state) - if entityData in entity.GetEntitySensors() or data_type == 'switch': # it's an EntitySensorData + if entityData in entity.GetEntitySensors() or entitycommand_supports_state: # it's an EntitySensorData # If the sensor supports extra attributes, send them as JSON. # So here I have to specify also the topic for those attributes + # - for sensors that became switches: entityData is the command + # so I need to retrieve the sensor and check there if supports extra attributes + # and get from there the topic + if entitycommand_supports_state and entityData.GetConnectedEntitySensor().DoesSupportExtraAttributes(): + payload["json_attributes_topic"] = self.MakeEntityDataExtraAttributesTopic( + entityData.GetConnectedEntitySensor()) + # - for real sensors - - if data_type != 'switch' and entityData.DoesSupportExtraAttributes(): + elif entityData.DoesSupportExtraAttributes(): payload["json_attributes_topic"] = self.MakeEntityDataExtraAttributesTopic( entityData) - # - for sensors that became switches: entityData is the command so I need to retrieve the sensor and check there if supports estra attributes and get from there the topic - elif data_type == 'switch' and entityData.GetConnectedEntitySensor().DoesSupportExtraAttributes(): - payload["json_attributes_topic"] = self.MakeEntityDataExtraAttributesTopic( - entityData.GetConnectedEntitySensor()) payload['expire_after'] = 600 # TODO Improve - if data_type != 'switch': - payload['state_topic'] = self.MakeEntityDataTopic( - entityData) - else: # it's a switch, so the key of the sensor to generate the topic is found in the command + if entitycommand_supports_state: # it has a state, so the key of the sensor to generate the topic is found in the sensor payload['state_topic'] = self.MakeEntityDataTopicForSensorByCommandIfSwitch( entityData) + else: # it's a real sensor: + payload['state_topic'] = self.MakeEntityDataTopic( + entityData) - # Add default payloads (only for ON/OFF entities) - if data_type in ["binary_sensor", "switch"]: - if not 'payload_on' in payload: - payload['payload_on'] = PAYLOAD_ON - if not 'payload_off' in payload: - payload['payload_off'] = PAYLOAD_OFF + # Add default payloads (only for ON/OFF entities) + if data_type in ["binary_sensor", "switch"]: + if not 'payload_on' in payload: + payload['payload_on'] = PAYLOAD_ON + if not 'payload_off' in payload: + payload['payload_off'] = PAYLOAD_OFF # if it's a command (so button or switch), configure the topic where the command will be called if entityData in entity.GetEntityCommands():