From 82384b2a33781c97e8311a053e6de42eb76d64dc Mon Sep 17 00:00:00 2001 From: Riccardo Briccola Date: Sun, 9 Nov 2025 17:55:49 +0100 Subject: [PATCH 1/7] Allow tagged entities to have multiple entity data with name --- .../HomeAssistantWarehouse/HomeAssistantWarehouse.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/IoTuring/Warehouse/Deployments/HomeAssistantWarehouse/HomeAssistantWarehouse.py b/IoTuring/Warehouse/Deployments/HomeAssistantWarehouse/HomeAssistantWarehouse.py index 353d78f12..c190e4739 100644 --- a/IoTuring/Warehouse/Deployments/HomeAssistantWarehouse/HomeAssistantWarehouse.py +++ b/IoTuring/Warehouse/Deployments/HomeAssistantWarehouse/HomeAssistantWarehouse.py @@ -209,15 +209,14 @@ def SetDiscoveryPayloadName(self) -> None: else: # Add key only if more than one entityData, and it doesn't have a tag: - if not self.entity.GetEntityTag() and \ - len(self.entity.GetAllUnconnectedEntityData()) > 1: + if len(self.entity.GetAllUnconnectedEntityData()) > 1: formatted_key = self.entityData.GetKey().capitalize().replace("_", " ") - payload_name = f"{self.entity.GetEntityName()} - {formatted_key}" + payload_name = f"{self.entity.GetEntityNameWithTag()} - {formatted_key}" else: - # Default name: + # Default name since just one entity data exists for this entity payload_name = self.entity.GetEntityNameWithTag() self.SetDefaultDiscoveryPayload("name", payload_name) From 565378bd201e4d7f8119273aa168400df0d92e29 Mon Sep 17 00:00:00 2001 From: Riccardo Briccola Date: Sun, 9 Nov 2025 17:56:11 +0100 Subject: [PATCH 2/7] Safer configuration load for entities --- IoTuring/Configurator/ConfiguratorLoader.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/IoTuring/Configurator/ConfiguratorLoader.py b/IoTuring/Configurator/ConfiguratorLoader.py index bd79502c1..578f5c160 100644 --- a/IoTuring/Configurator/ConfiguratorLoader.py +++ b/IoTuring/Configurator/ConfiguratorLoader.py @@ -54,10 +54,13 @@ def LoadEntities(self) -> list[Entity]: self.Log( self.LOG_ERROR, f"Can't find {entityConfig.GetType()} entity, check your configurations.") else: - ec = entityClass(entityConfig) - self.Log( - self.LOG_DEBUG, f"Full configuration with defaults: {ec.configurations.ToDict()}") - entities.append(ec) # Entity instance + try: + ec = entityClass(entityConfig) + self.Log( + self.LOG_DEBUG, f"Full configuration with defaults: {ec.configurations.ToDict()}") + entities.append(ec) # Entity instance + except Exception as e: + self.Log(self.LOG_ERROR, f"Error initializing entity {entityConfig.GetType()}: {e}") return entities # How Warehouse configurations works: From 9135b22fa92dcb8eee442b25bdb47fbdf61e8781 Mon Sep 17 00:00:00 2001 From: Riccardo Briccola Date: Sun, 9 Nov 2025 17:56:28 +0100 Subject: [PATCH 3/7] Network entity --- .../Entity/Deployments/Network/Network.py | 152 ++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 IoTuring/Entity/Deployments/Network/Network.py diff --git a/IoTuring/Entity/Deployments/Network/Network.py b/IoTuring/Entity/Deployments/Network/Network.py new file mode 100644 index 000000000..c5a9460ad --- /dev/null +++ b/IoTuring/Entity/Deployments/Network/Network.py @@ -0,0 +1,152 @@ +from IoTuring.Entity.Entity import Entity +from IoTuring.Entity.EntityData import EntitySensor +from IoTuring.Configurator.MenuPreset import MenuPreset +from IoTuring.MyApp.SystemConsts import OperatingSystemDetection as OsD # don't name Os as could be a problem with old configurations that used the Os entity + +# Use cross-platform netifaces library for network interface details +try: + import netifaces as ni + library_available_netifaces = True +except Exception as e: + library_available_netifaces = False + +CONFIG_KEY_TITLE = "inet_name" + +KEY_IPV4_ADDRESS = "IPv4_address" +KEY_IPV6_ADDRESS = "IPv6_address" + +EXTRA_KEY_IP_ADDRESS = "IP Address" +EXTRA_KEY_MAC_ADDRESS = "MAC Address" +EXTRA_KEY_NETMASK = "Netmask" + +EXTRA_KEY_BROADCAST = "Broadcast Address" +EXTRA_KEY_GATEWAY = "Gateway Address" + +class Network(Entity): + NAME = "Network" + ALLOW_MULTI_INSTANCE = True + + def Initialize(self): + self.configured_inet = self.GetFromConfigurations(CONFIG_KEY_TITLE) + + self.RegisterEntitySensor( + EntitySensor( + self, + KEY_IPV4_ADDRESS, + supportsExtraAttributes=True + ) + ) + + self.RegisterEntitySensor( + EntitySensor( + self, + KEY_IPV6_ADDRESS, + supportsExtraAttributes=True + ) + ) + + def Update(self): + try: + inet_addresses = ni.ifaddresses(self.configured_inet) + + ipv4 = { + 'ip_address': None, + 'netmask': None, + 'broadcast_address': None + } + ipv6 = { + 'ip_address': None, + 'netmask': None, + 'broadcast_address': None + } + mac_address = None + + if ni.AF_INET in inet_addresses: + if len(inet_addresses[ni.AF_INET]) > 0: + # IPv4 + if 'addr' in inet_addresses[ni.AF_INET][0]: + ipv4['ip_address'] = inet_addresses[ni.AF_INET][0]['addr'] + if 'netmask' in inet_addresses[ni.AF_INET][0]: + ipv4['netmask'] = inet_addresses[ni.AF_INET][0]['netmask'] + if 'broadcast' in inet_addresses[ni.AF_INET][0]: + ipv4['broadcast_address'] = inet_addresses[ni.AF_INET][0]['broadcast'] + + if ni.AF_INET6 in inet_addresses: + if len(inet_addresses[ni.AF_INET6]) > 0: + # IPv6 + if 'addr' in inet_addresses[ni.AF_INET6][0]: + ipv6['ip_address'] = inet_addresses[ni.AF_INET6][0]['addr'] + if 'netmask' in inet_addresses[ni.AF_INET6][0]: + ipv6['netmask'] = inet_addresses[ni.AF_INET6][0]['netmask'] + if 'broadcast' in inet_addresses[ni.AF_INET6][0]: + ipv6['broadcast_address'] = inet_addresses[ni.AF_INET6][0]['broadcast'] + + if ni.AF_LINK in inet_addresses: + if len(inet_addresses[ni.AF_LINK]) > 0: + # Link + mac_address = inet_addresses[ni.AF_LINK][0]['addr'] + + self.SetEntitySensorValue( + key = KEY_IPV4_ADDRESS, + value = ipv4['ip_address'] if ipv4['ip_address'] is not None else "N/A" + ) + self.SetEntitySensorValue( + key = KEY_IPV6_ADDRESS, + value = ipv6['ip_address'] if ipv4['ip_address'] is not None else "N/A" + ) + + for key, value in ipv4.items(): + if value is not None: + self.SetEntitySensorExtraAttribute( + sensorDataKey = KEY_IPV4_ADDRESS, + attributeKey = key, + attributeValue = str(value) + ) + + for key, value in ipv4.items(): + if value is not None: + self.SetEntitySensorExtraAttribute( + sensorDataKey = KEY_IPV4_ADDRESS, + attributeKey = key, + attributeValue = str(value) + ) + + if mac_address is not None: + self.SetEntitySensorExtraAttribute( + sensorDataKey = KEY_IPV4_ADDRESS, + attributeKey = "mac_address", + attributeValue = str(mac_address) + ) + + if mac_address is not None: + self.SetEntitySensorExtraAttribute( + sensorDataKey = KEY_IPV6_ADDRESS, + attributeKey = "mac_address", + attributeValue = str(mac_address) + ) + + except Exception as e: + raise Exception(f"Error retrieving network information for interface '{self.configured_inet}': {str(e)}") + + @classmethod + def ConfigurationPreset(cls) -> MenuPreset: + # Get the choices for menu: + INET_AVAILABLE_INTERFACES = [] + + for iface in ni.interfaces(): + INET_AVAILABLE_INTERFACES.append( + {"name": iface, "value": iface} + ) + + preset = MenuPreset() + preset.AddEntry(name="Network interface", + key=CONFIG_KEY_TITLE, mandatory=False, + question_type="select", choices=INET_AVAILABLE_INTERFACES) + return preset + + @classmethod + def CheckSystemSupport(cls): + if (not OsD.IsLinux() and not OsD.IsMacos() and not OsD.IsWindows()): + raise cls.UnsupportedOsException() + if (not library_available_netifaces): + raise Exception("Error while importing netifaces library") \ No newline at end of file From 7b1f14c9d886161ac893675a11ce4eb101ad217d Mon Sep 17 00:00:00 2001 From: Riccardo Briccola Date: Sun, 9 Nov 2025 18:04:04 +0100 Subject: [PATCH 4/7] Fix typo --- IoTuring/Configurator/ConfiguratorObject.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IoTuring/Configurator/ConfiguratorObject.py b/IoTuring/Configurator/ConfiguratorObject.py index 31b00e2db..4f66eede4 100644 --- a/IoTuring/Configurator/ConfiguratorObject.py +++ b/IoTuring/Configurator/ConfiguratorObject.py @@ -35,7 +35,7 @@ def GetFromConfigurations(self, key): raise UnknownConfigKeyException(key) def GetTrueOrFalseFromConfigurations(self, key) -> bool: - """ Get boolean value from confiugurations with key (if not present raise Exception) """ + """ Get boolean value from configurations with key (if not present raise Exception) """ value = self.GetFromConfigurations(key).lower() if value in BooleanAnswers.TRUE_ANSWERS: return True From c35c6cf64f8439f6e1dffffbe68a1f34c7b15c69 Mon Sep 17 00:00:00 2001 From: Riccardo Briccola Date: Sun, 9 Nov 2025 18:04:35 +0100 Subject: [PATCH 5/7] Safer handling of entity config load in configurator --- IoTuring/Configurator/Configurator.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/IoTuring/Configurator/Configurator.py b/IoTuring/Configurator/Configurator.py index da778e2fd..36760f685 100644 --- a/IoTuring/Configurator/Configurator.py +++ b/IoTuring/Configurator/Configurator.py @@ -373,7 +373,12 @@ def AddNewConfiguration(self, typeClass) -> None: def EditActiveConfiguration(self, typeClass, single_config: SingleConfiguration) -> None: """ UI for changing settings """ - preset = typeClass.ConfigurationPreset() + try: + preset = typeClass.ConfigurationPreset() + except Exception as e: + self.DisplayMessage( + f"Error during {typeClass.GetClassKey()} preset loading: {str(e)}") + return if preset.HasQuestions(): From 697b78fd51eb7a6e40ff12e885bfbad99c765ea3 Mon Sep 17 00:00:00 2001 From: Riccardo Briccola Date: Sun, 9 Nov 2025 18:05:58 +0100 Subject: [PATCH 6/7] Add option to enable or disable ipv6 info --- .../Entity/Deployments/Network/Network.py | 78 +++++++++++-------- 1 file changed, 44 insertions(+), 34 deletions(-) diff --git a/IoTuring/Entity/Deployments/Network/Network.py b/IoTuring/Entity/Deployments/Network/Network.py index c5a9460ad..f6c82651e 100644 --- a/IoTuring/Entity/Deployments/Network/Network.py +++ b/IoTuring/Entity/Deployments/Network/Network.py @@ -11,6 +11,7 @@ library_available_netifaces = False CONFIG_KEY_TITLE = "inet_name" +CONFIG_KEY_INCLUDE_IPV6 = "include_ipv6" KEY_IPV4_ADDRESS = "IPv4_address" KEY_IPV6_ADDRESS = "IPv6_address" @@ -28,6 +29,7 @@ class Network(Entity): def Initialize(self): self.configured_inet = self.GetFromConfigurations(CONFIG_KEY_TITLE) + self.include_ipv6 = self.GetTrueOrFalseFromConfigurations(CONFIG_KEY_INCLUDE_IPV6) self.RegisterEntitySensor( EntitySensor( @@ -37,13 +39,14 @@ def Initialize(self): ) ) - self.RegisterEntitySensor( - EntitySensor( - self, - KEY_IPV6_ADDRESS, - supportsExtraAttributes=True + if self.include_ipv6: + self.RegisterEntitySensor( + EntitySensor( + self, + KEY_IPV6_ADDRESS, + supportsExtraAttributes=True + ) ) - ) def Update(self): try: @@ -71,15 +74,16 @@ def Update(self): if 'broadcast' in inet_addresses[ni.AF_INET][0]: ipv4['broadcast_address'] = inet_addresses[ni.AF_INET][0]['broadcast'] - if ni.AF_INET6 in inet_addresses: - if len(inet_addresses[ni.AF_INET6]) > 0: - # IPv6 - if 'addr' in inet_addresses[ni.AF_INET6][0]: - ipv6['ip_address'] = inet_addresses[ni.AF_INET6][0]['addr'] - if 'netmask' in inet_addresses[ni.AF_INET6][0]: - ipv6['netmask'] = inet_addresses[ni.AF_INET6][0]['netmask'] - if 'broadcast' in inet_addresses[ni.AF_INET6][0]: - ipv6['broadcast_address'] = inet_addresses[ni.AF_INET6][0]['broadcast'] + if self.include_ipv6: + if ni.AF_INET6 in inet_addresses: + if len(inet_addresses[ni.AF_INET6]) > 0: + # IPv6 + if 'addr' in inet_addresses[ni.AF_INET6][0]: + ipv6['ip_address'] = inet_addresses[ni.AF_INET6][0]['addr'] + if 'netmask' in inet_addresses[ni.AF_INET6][0]: + ipv6['netmask'] = inet_addresses[ni.AF_INET6][0]['netmask'] + if 'broadcast' in inet_addresses[ni.AF_INET6][0]: + ipv6['broadcast_address'] = inet_addresses[ni.AF_INET6][0]['broadcast'] if ni.AF_LINK in inet_addresses: if len(inet_addresses[ni.AF_LINK]) > 0: @@ -90,18 +94,12 @@ def Update(self): key = KEY_IPV4_ADDRESS, value = ipv4['ip_address'] if ipv4['ip_address'] is not None else "N/A" ) - self.SetEntitySensorValue( - key = KEY_IPV6_ADDRESS, - value = ipv6['ip_address'] if ipv4['ip_address'] is not None else "N/A" - ) - for key, value in ipv4.items(): - if value is not None: - self.SetEntitySensorExtraAttribute( - sensorDataKey = KEY_IPV4_ADDRESS, - attributeKey = key, - attributeValue = str(value) - ) + if self.include_ipv6: + self.SetEntitySensorValue( + key = KEY_IPV6_ADDRESS, + value = ipv6['ip_address'] if ipv4['ip_address'] is not None else "N/A" + ) for key, value in ipv4.items(): if value is not None: @@ -111,19 +109,28 @@ def Update(self): attributeValue = str(value) ) - if mac_address is not None: - self.SetEntitySensorExtraAttribute( - sensorDataKey = KEY_IPV4_ADDRESS, - attributeKey = "mac_address", - attributeValue = str(mac_address) - ) + if self.include_ipv6: + for key, value in ipv4.items(): + if value is not None: + self.SetEntitySensorExtraAttribute( + sensorDataKey = KEY_IPV4_ADDRESS, + attributeKey = key, + attributeValue = str(value) + ) if mac_address is not None: self.SetEntitySensorExtraAttribute( - sensorDataKey = KEY_IPV6_ADDRESS, + sensorDataKey = KEY_IPV4_ADDRESS, attributeKey = "mac_address", attributeValue = str(mac_address) ) + + if self.include_ipv6: + self.SetEntitySensorExtraAttribute( + sensorDataKey = KEY_IPV6_ADDRESS, + attributeKey = "mac_address", + attributeValue = str(mac_address) + ) except Exception as e: raise Exception(f"Error retrieving network information for interface '{self.configured_inet}': {str(e)}") @@ -140,8 +147,11 @@ def ConfigurationPreset(cls) -> MenuPreset: preset = MenuPreset() preset.AddEntry(name="Network interface", - key=CONFIG_KEY_TITLE, mandatory=False, + key=CONFIG_KEY_TITLE, mandatory=True, question_type="select", choices=INET_AVAILABLE_INTERFACES) + preset.AddEntry(name="Include IPv6 information", + key=CONFIG_KEY_INCLUDE_IPV6, mandatory=False, + question_type="yesno") return preset @classmethod From 8278ec15873e41f460798e7db9fe76d83170541f Mon Sep 17 00:00:00 2001 From: Riccardo Briccola Date: Sun, 9 Nov 2025 18:29:33 +0100 Subject: [PATCH 7/7] Network entity HA icon --- .../Warehouse/Deployments/HomeAssistantWarehouse/entities.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/IoTuring/Warehouse/Deployments/HomeAssistantWarehouse/entities.yaml b/IoTuring/Warehouse/Deployments/HomeAssistantWarehouse/entities.yaml index f00f615f0..d19076665 100644 --- a/IoTuring/Warehouse/Deployments/HomeAssistantWarehouse/entities.yaml +++ b/IoTuring/Warehouse/Deployments/HomeAssistantWarehouse/entities.yaml @@ -31,6 +31,8 @@ Username: icon: mdi:account-supervisor-circle Lock: icon: mdi:lock +Network: + icon: mdi:network-outline NotifyPayload: icon: mdi:forum custom_type: text