Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions IoTuring/Entity/Entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@

class Entity(ConfiguratorObject, LogObject):

entitySensors: list[EntitySensor]
entityCommands: list[EntityCommand]

def __init__(self, single_configuration: SingleConfiguration) -> None:
super().__init__(single_configuration)

Expand Down Expand Up @@ -128,11 +131,12 @@ def GetAllEntityData(self) -> list:
""" safe - Return list of entity sensors and commands """
return self.entityCommands.copy() + self.entitySensors.copy() # Safe return: nobody outside can change the callback !

def GetAllUnconnectedEntityData(self) -> list[EntityData]:
""" safe - Return All EntityCommands and EntitySensors without connected command """
connected_sensors = [command.GetConnectedEntitySensor()
for command in self.entityCommands
if command.SupportsState()]
def GetAllUnconnectedEntityData(self) -> list[EntityCommand|EntitySensor]:
""" safe - Return All EntityCommands and EntitySensors without connected sensors """
connected_sensors = []
for command in self.entityCommands:
connected_sensors.extend(command.GetConnectedEntitySensors())

unconnected_sensors = [sensor for sensor in self.entitySensors
if sensor not in connected_sensors]
return self.entityCommands.copy() + unconnected_sensors.copy()
Expand Down
39 changes: 25 additions & 14 deletions IoTuring/Entity/EntityData.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Callable
if TYPE_CHECKING:
from IoTuring.Entity.Entity import Entity

Expand Down Expand Up @@ -117,24 +117,35 @@ def SetExtraAttribute(self, attribute_name, attribute_value, valueFormatterOptio

class EntityCommand(EntityData):

def __init__(self, entity, key, callbackFunction,
connectedEntitySensorKey=None, customPayload={}):
"""
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.
CustomPayload overrides HomeAssistant discovery configuration
def __init__(self, entity: Entity, key: str, callbackFunction: Callable,
connectedEntitySensorKeys: str | list = [],
customPayload={}):
"""Create a new EntityCommand.

If key or keys for the entity sensor is passed, warehouses that support it can use this command as a switch with state.
Order of sensors matter, first sensors state topic will be used.
Better to register the sensors before this command to avoid unexpected behaviours.

Args:
entity (Entity): The entity this command belongs to.
key (str): The KEY of this command
callbackFunction (Callable): Function to be called
connectedEntitySensorKeys (str | list, optional): A key to a sensor or a list of keys. Defaults to [].
customPayload (dict, optional): Overrides HomeAssistant discovery configuration. Defaults to {}.
"""

EntityData.__init__(self, entity, key, customPayload)
self.callbackFunction = callbackFunction
self.connectedEntitySensorKey = connectedEntitySensorKey
self.connectedEntitySensorKeys = connectedEntitySensorKeys if isinstance(
connectedEntitySensorKeys, list) else [connectedEntitySensorKeys]

def SupportsState(self):
return self.connectedEntitySensorKey is not None
def SupportsState(self) -> bool:
""" True if this command supports state (has a connected sensors) """
return bool(self.connectedEntitySensorKeys)

def GetConnectedEntitySensor(self) -> EntitySensor:
""" Returns the entity sensor connected to this command, if this command supports state.
Otherwise returns None. """
return self.GetEntity().GetEntitySensorByKey(self.connectedEntitySensorKey)
def GetConnectedEntitySensors(self) -> list[EntitySensor]:
""" Returns the entity sensors connected to this command. Returns empty list if none found. """
return [self.GetEntity().GetEntitySensorByKey(key) for key in self.connectedEntitySensorKeys]

def CallCallback(self, message):
""" Safely run callback for this command, passing the message (a paho.mqtt.client.MQTTMessage).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,19 @@

CONFIGURATION_SEND_LOOP_SKIP_NUMBER = 10

EXTERNAL_ENTITY_DATA_CONFIGURATION_FILE_FILENAME = "entities.yaml"

LWT_TOPIC_SUFFIX = "LWT"
LWT_PAYLOAD_ONLINE = "ONLINE"
LWT_PAYLOAD_OFFLINE = "OFFLINE"
PAYLOAD_ON = consts.STATE_ON
PAYLOAD_OFF = consts.STATE_OFF

# Entity configuration file for HAWH:
EXTERNAL_ENTITY_DATA_CONFIGURATION_FILE_FILENAME = "entities.yaml"
# Set HA entity type, e.g. number, light, switch:
ENTITY_CONFIG_CUSTOM_TYPE_KEY = "custom_type"
# Custom topic keys for discovery. Use list for multiple topics:
ENTITY_CONFIG_CUSTOM_TOPIC_SUFFIX = "_key"

class HomeAssistantEntityBase(LogObject):
""" Base class for all entities in HomeAssistantWarehouse """
Expand Down Expand Up @@ -76,10 +81,7 @@ def __init__(self,
self.GetEntityDataCustomConfigurations(self.name)

# Get data type:
if "custom_type" in self.discovery_payload:
self.data_type = self.discovery_payload.pop("custom_type")
else:
self.data_type = ""
self.data_type = self.discovery_payload.pop(ENTITY_CONFIG_CUSTOM_TYPE_KEY, "")

# Set name:
self.SetDiscoveryPayloadName()
Expand Down Expand Up @@ -125,8 +127,16 @@ def AddTopic(self, topic_name: str, topic_path: str = ""):

# Add as an attribute:
setattr(self, topic_name, topic_path)
# Add to the discovery payload:
self.discovery_payload[topic_name] = topic_path

# Check for custom topic:
discovery_keys = self.discovery_payload.pop(topic_name + ENTITY_CONFIG_CUSTOM_TOPIC_SUFFIX, topic_name)
if not isinstance(discovery_keys, list):
discovery_keys = [discovery_keys]

# Add to discovery payload:
for discovery_key in discovery_keys:
self.discovery_payload[discovery_key] = topic_path


def SendTopicData(self, topic, data) -> None:
self.wh.client.SendTopicData(topic, data)
Expand Down Expand Up @@ -181,7 +191,7 @@ def __init__(self, entityData: EntityData, wh: "HomeAssistantWarehouse") -> None
self.default_topics = {
"availability_topic": self.wh.MakeValuesTopic(LWT_TOPIC_SUFFIX),
"state_topic": self.MakeEntityDataTopic(self.entityData),
"json_attributes_topic": self.MakeEntityDataExtraAttributesTopic(self.entityData),
"json_attributes_topic": self.MakeEntityDataTopic(self.entityData, TOPIC_DATA_EXTRA_ATTRIBUTES_SUFFIX),
"command_topic": self.MakeEntityDataTopic(self.entityData)
}

Expand Down Expand Up @@ -214,14 +224,10 @@ def SetDiscoveryPayloadName(self) -> None:

return super().SetDiscoveryPayloadName()

def MakeEntityDataTopic(self, entityData: EntityData) -> str:
def MakeEntityDataTopic(self, entityData: EntityData, suffix:str = "") -> str:
""" Uses MakeValuesTopic but receives an EntityData to manage itself its id"""
return self.wh.MakeValuesTopic(entityData.GetId())
return self.wh.MakeValuesTopic(entityData.GetId() + suffix)

def MakeEntityDataExtraAttributesTopic(self, entityData: EntityData) -> str:
""" Uses MakeValuesTopic but receives an EntityData to manage itself its id, appending a suffix to distinguish
the extra attrbiutes from the original value """
return self.wh.MakeValuesTopic(entityData.GetId() + TOPIC_DATA_EXTRA_ATTRIBUTES_SUFFIX)


class HomeAssistantSensor(HomeAssistantEntity):
Expand Down Expand Up @@ -254,11 +260,13 @@ def SendValues(self, callback_value:str|None= None):
"""

if self.entitySensor.HasValue():
if callback_value is not None:
sensor_value = callback_value
if callback_value is None:
value = self.entitySensor.GetValue()
else:
sensor_value = ValueFormatter.FormatValue(
self.entitySensor.GetValue(),
value = callback_value

sensor_value = ValueFormatter.FormatValue(
value,
self.entitySensor.GetValueFormatterOptions(),
INCLUDE_UNITS_IN_SENSORS)

Expand All @@ -283,47 +291,44 @@ def __init__(self, entityData: EntityCommand, wh: "HomeAssistantWarehouse") -> N

self.entityCommand = entityData

self.AddTopic("availability_topic")
self.AddTopic("command_topic")

self.connected_sensor = self.GetConnectedSensor()
self.connected_sensors = self.GetConnectedSensors()

if self.connected_sensor:
if self.connected_sensors:
self.SetDefaultDataType("switch")
# Get discovery payload from connected sensor?
for payload_key in self.connected_sensor.discovery_payload:
if payload_key not in self.discovery_payload:
self.discovery_payload[payload_key] = self.connected_sensor.discovery_payload[payload_key]
# Get discovery payload from connected sensors
for sensor in self.connected_sensors:
for payload_key in sensor.discovery_payload:
if payload_key not in self.discovery_payload:
self.discovery_payload[payload_key] = sensor.discovery_payload[payload_key]
else:
# Button as default data type:
self.SetDefaultDataType("button")

self.command_callback = self.GenerateCommandCallback()

def GetConnectedSensor(self) -> HomeAssistantSensor | None:
""" Get the connected sensor of this command """
if self.entityCommand.SupportsState():
return HomeAssistantSensor(
entityData=self.entityCommand.GetConnectedEntitySensor(),
wh=self.wh)
else:
return None

def GetConnectedSensors(self) -> list[HomeAssistantSensor]:
""" Get the connected sensors of this command """
return [HomeAssistantSensor(entityData=sensor, wh=self.wh)
for sensor in self.entityCommand.GetConnectedEntitySensors()]


def GenerateCommandCallback(self) -> Callable:
""" Generate the callback function """
def CommandCallback(message):
status = self.entityCommand.CallCallback(message)
if status and self.wh.client.IsConnected():
if self.connected_sensor:
if self.connected_sensors:
# Only set value if it was already set, to exclude optimistic switches
if self.connected_sensor.entitySensor.HasValue():
self.Log(self.LOG_DEBUG, "Switch callback: sending state to " +
self.connected_sensor.state_topic)
self.connected_sensor.SendValues(callback_value = message.payload.decode('utf-8'))
for sensor in self.connected_sensors:
if sensor.entitySensor.HasValue():
sensor.SendValues(callback_value = message.payload.decode('utf-8'))

# Optimistic switches with extra attributes:
elif self.connected_sensor.supports_extra_attributes:
self.connected_sensor.SendExtraAttributes()
# Optimistic switches with extra attributes:
elif sensor.supports_extra_attributes:
sensor.SendExtraAttributes()

return CommandCallback

Expand Down Expand Up @@ -387,7 +392,7 @@ def Start(self):
super().Start() # Then run other inits (start the Loop method for example)

def CollectEntityData(self) -> None:
""" Collect entities and save them ass hass entities """
""" Collect entities and save them as hass entities """

# Add the Lwt sensor:
self.homeAssistantEntities["sensors"].append(LwtSensor(self))
Expand All @@ -399,9 +404,8 @@ def CollectEntityData(self) -> None:
# It's a command:
if isinstance(entityData, EntityCommand):
hasscommand = HomeAssistantCommand(entityData, self)
if hasscommand.connected_sensor:
self.homeAssistantEntities["connected_sensors"].append(
hasscommand.connected_sensor)
if hasscommand.connected_sensors:
self.homeAssistantEntities["connected_sensors"].extend(hasscommand.connected_sensors)
self.homeAssistantEntities["commands"].append(hasscommand)

# It's a sensor:
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
[project]
name = "IoTuring"
version = "2024.6.1"
description = "Simple and powerful cross-platform script to control your pc and share statistics using communication protocols like MQTT and home control hubs like HomeAssistant."
description = "Your Windows, Linux, macOS computer as MQTT and HomeAssistant integration."
readme = "README.md"
requires-python = ">=3.8"
license = {file = "COPYING"}
keywords = ["iot","mqtt","monitor"]
keywords = ["iot","mqtt","monitor","homeassistant"]
authors = [
{name = "richibrics", email = "riccardo.briccola.dev@gmail.com"},
{name = "infeeeee", email = "gyetpet@mailbox.org"}
Expand Down