Skip to content

Commit

Permalink
AppInfo Update
Browse files Browse the repository at this point in the history
  • Loading branch information
richibrics committed Mar 9, 2024
1 parent 20836e9 commit 04cea9d
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 38 deletions.
19 changes: 9 additions & 10 deletions IoTuring/Entity/Deployments/AppInfo/AppInfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,23 @@

KEY_NAME = 'name'
KEY_CURRENT_VERSION = 'current_version'
KEY_LATEST_VERSION = 'latest_version'
KEY_UPDATE = 'update'

PYPI_URL = 'https://pypi.org/pypi/ioturing/json'

GET_UPDATE_ERROR_MESSAGE = "Error while checking, try to update to solve this problem. Alert the developers if the problem persists."

EXTRA_ATTRIBUTE_LATEST_VERSION = 'latest_version'
EXTRA_ATTRIBUTE_UPDATE_ERROR = 'Check error'

class AppInfo(Entity):
NAME = "AppInfo"

def Initialize(self):
self.RegisterEntitySensor(EntitySensor(self, KEY_NAME))
self.RegisterEntitySensor(EntitySensor(self, KEY_CURRENT_VERSION, supportsExtraAttributes=True))
self.RegisterEntityCommand(EntityCommand(self, KEY_UPDATE, self.InstallUpdate, KEY_CURRENT_VERSION, self.UpdateCustomDiscoveryPayload()))
self.RegisterEntitySensor(EntitySensor(self, KEY_NAME, supportsExtraAttributes=True))
self.RegisterEntitySensor(EntitySensor(self, KEY_CURRENT_VERSION))
self.RegisterEntitySensor(EntitySensor(self, KEY_LATEST_VERSION, supportsExtraAttributes=True))
self.RegisterEntityCommand(EntityCommand(self, KEY_UPDATE, self.InstallUpdate, KEY_CURRENT_VERSION, [KEY_LATEST_VERSION], self.UpdateCommandCustomPayload()))

self.SetEntitySensorValue(KEY_NAME, App.getName())
self.SetEntitySensorValue(KEY_CURRENT_VERSION, App.getVersion())
Expand All @@ -34,10 +35,7 @@ def Update(self):
try:
new_version = self.GetUpdateInformation()

if not new_version: # signal no update and current version (as its the latest)
self.SetEntitySensorExtraAttribute(KEY_CURRENT_VERSION,EXTRA_ATTRIBUTE_LATEST_VERSION, App.getVersion())
else: # signal update and latest version
self.SetEntitySensorExtraAttribute(KEY_CURRENT_VERSION,EXTRA_ATTRIBUTE_LATEST_VERSION, new_version)
self.SetEntitySensorValue(KEY_LATEST_VERSION, "2025.1.1")
except Exception as e:
# connection error or pypi name changed or something else
# add extra attribute to show error message
Expand Down Expand Up @@ -68,10 +66,11 @@ def GetUpdateInformation(self):
else:
raise UpdateCheckException()

def UpdateCustomDiscoveryPayload(self):
def UpdateCommandCustomPayload(self):
return {
"title": App.getName(),
"name": App.getName(),
"latest_version_topic": self.GetEntitySensorByKey(KEY_CURRENT_VERSION).Get
"release_url": App.getUrlReleases()
}

def versionToInt(version: str):
Expand Down
7 changes: 5 additions & 2 deletions IoTuring/Entity/Entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def CallUpdate(self): # Call the Update method safely
except Exception as exc:
# TODO I need an exception manager
self.Log(self.LOG_ERROR, 'Error occured during update: ' + str(exc))
# self.entityManager.UnloadEntity(self) # TODO Think how to improve this
#  self.entityManager.UnloadEntity(self) # TODO Think how to improve this

def Update(self):
""" Must be implemented in sub-classes """
Expand Down Expand Up @@ -123,9 +123,12 @@ def GetAllEntityData(self) -> list:

def GetAllUnconnectedEntityData(self) -> list[EntityData]:
""" safe - Return All EntityCommands and EntitySensors without connected sensors """
connected_sensors = [command.GetConnectedEntitySensor()
connected_sensors = [command.GetConnectedPrimaryEntitySensor()
for command in self.entityCommands
if command.SupportsState()]
connected_sensors += [secondary
for command in self.entityCommands
for secondary in command.GetConnectedSecondaryEntitySensors()]
unconnected_sensors = [sensor for sensor in self.entitySensors
if sensor not in connected_sensors]
return self.entityCommands.copy() + unconnected_sensors.copy()
Expand Down
24 changes: 16 additions & 8 deletions IoTuring/Entity/EntityData.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,23 +118,31 @@ def SetExtraAttribute(self, attribute_name, attribute_value, valueFormatterOptio
class EntityCommand(EntityData):

def __init__(self, entity, key, callbackFunction,
connectedEntitySensorKey=None, customPayload={}):
primaryConnectedEntitySensorKey=None, secondaryConnectedEntitySensorKeys=[], 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
Better to register the sensor before this command to avoid unexpected behaviours.
CustomPayload overrides HomeAssistant discovery configuration.
Secondary connected sensors can be used for additional data of the commands.
"""
EntityData.__init__(self, entity, key, customPayload)
self.callbackFunction = callbackFunction
self.connectedEntitySensorKey = connectedEntitySensorKey
self.primaryConnectedEntitySensorKey = primaryConnectedEntitySensorKey
self.secondaryConnectedEntitySensorKeys = secondaryConnectedEntitySensorKeys

def SupportsState(self):
return self.connectedEntitySensorKey is not None
""" True if this command supports state (has a primary connected sensor) """
return self.primaryConnectedEntitySensorKey is not None

def GetConnectedEntitySensor(self) -> EntitySensor:
""" Returns the entity sensor connected to this command, if this command supports state.
def GetConnectedPrimaryEntitySensor(self) -> EntitySensor:
""" Returns the entity sensor connected to this command as primary sensor, if this command supports state.
Otherwise returns None. """
return self.GetEntity().GetEntitySensorByKey(self.connectedEntitySensorKey)
return self.GetEntity().GetEntitySensorByKey(self.primaryConnectedEntitySensorKey)

def GetConnectedSecondaryEntitySensors(self) -> list[EntitySensor]:
""" Returns the entity sensors connected to this command as secondary sensors.
If none, returns an empty list. """
return [self.GetEntity().GetEntitySensorByKey(key) for key in self.secondaryConnectedEntitySensorKeys]

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 @@ -46,6 +46,7 @@
PAYLOAD_ON = consts.STATE_ON
PAYLOAD_OFF = consts.STATE_OFF

SECONDARY_SENSOR_STATE_TOPIC_DISCOVERY_CONFIGURATION_KEY = "state_topic_key"

class HomeAssistantEntityBase(LogObject):
""" Base class for all entities in HomeAssistantWarehouse """
Expand Down Expand Up @@ -271,41 +272,58 @@ def __init__(self, entityData: EntityCommand, wh: "HomeAssistantWarehouse") -> N
self.AddTopic("availability_topic")
self.AddTopic("command_topic")

self.connected_sensor = self.GetConnectedSensor()
# Primary sensors are used to retrieve the state of the command and to use it as a switch if present
self.primary_connected_sensor = self.GetPrimaryConnectedSensor()
# Secondary sensors are used to provide additional information to the command and they will be configured
# in the command discovery payload in a custom topic specified in the yaml file (like for Update latest version)
self.secondary_connected_sensors = self.GetSecondaryConnectedSensors()

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

# Get the discovery payload from the secondary connected sensors
for secondary_sensor in self.secondary_connected_sensors:
for payload_key in secondary_sensor.discovery_payload:
if payload_key not in self.discovery_payload:
self.discovery_payload[payload_key] = secondary_sensor.discovery_payload[payload_key]

self.command_callback = self.GenerateCommandCallback()

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

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

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.primary_connected_sensor:
# Only set value if it was already set, to exclude optimistic switches
if self.connected_sensor.entitySensor.HasValue():
if self.primary_connected_sensor.entitySensor.HasValue():
self.Log(self.LOG_DEBUG, "Switch callback: sending state to " +
self.connected_sensor.state_topic)
self.primary_connected_sensor.state_topic)
self.SendTopicData(
self.connected_sensor.state_topic, message.payload.decode('utf-8'))
self.primary_connected_sensor.state_topic, message.payload.decode('utf-8'))
return CommandCallback


Expand All @@ -329,6 +347,41 @@ def SendValues(self):
self.SendTopicData(self.state_topic, LWT_PAYLOAD_ONLINE)


class SecondarySensor(HomeAssistantEntity):
# All secondary sensors won't have a discovery payload that will be sent alone
# but joined with the one of the command they are connected to.
# The state topic is set to a topic, but published in discovery payload not as state topic
# but with that MUST be present in the YAML file: look at the Update latest version sensor
def __init__(self, entityData: EntitySensor, wh: "HomeAssistantWarehouse") -> None:
super().__init__(entityData=entityData, wh=wh)

self.entitySensor = entityData

# Default data type:
self.SetDefaultDataType("sensor")

self.AddTopic("state_topic")

# The state topic to use is in the discovery payload as SECONDARY_SENSOR_STATE_TOPIC_DISCOVERY_CONFIGURATION_KEY
if not SECONDARY_SENSOR_STATE_TOPIC_DISCOVERY_CONFIGURATION_KEY in self.discovery_payload:
raise Exception(f"Secondary sensor {self.id} must define the discovery state topic key in entity data configuration")

self.key_for_state_topic = self.discovery_payload.pop(SECONDARY_SENSOR_STATE_TOPIC_DISCOVERY_CONFIGURATION_KEY)

self.discovery_payload[self.key_for_state_topic] = self.state_topic


def SendValues(self):
""" Send values of the sensor to the custom topic """
if self.entitySensor.HasValue():
sensor_value = ValueFormatter.FormatValue(
self.entitySensor.GetValue(),
self.entitySensor.GetValueFormatterOptions(),
INCLUDE_UNITS_IN_SENSORS)

self.SendTopicData(self.state_topic, sensor_value)


class HomeAssistantWarehouse(Warehouse):
NAME = "HomeAssistant"

Expand Down Expand Up @@ -368,21 +421,23 @@ 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))

# Add real entities:
for entity in self.GetEntities():
for entityData in entity.GetAllUnconnectedEntityData():

# It's a command:
if isinstance(entityData, EntityCommand):
hasscommand = HomeAssistantCommand(entityData, self)
if hasscommand.connected_sensor:
if hasscommand.primary_connected_sensor:
self.homeAssistantEntities["connected_sensors"].append(
hasscommand.primary_connected_sensor)
for secondary in hasscommand.secondary_connected_sensors:
self.homeAssistantEntities["connected_sensors"].append(
hasscommand.connected_sensor)
secondary)
self.homeAssistantEntities["commands"].append(hasscommand)

# It's a sensor:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,17 @@ Time:
Monitor:
icon: mdi:monitor-shimmer
custom_type: switch
AppInfo:
AppInfo - name:
icon: mdi:information-outline
AppInfo - update:
custom_type: update
device_class: firmware
latest_version_topic: IoTuring/LenovoYogaHomeAssistant/Entity/AppInfo/current_version_extraattributes
latest_version_template: value_json['update'].latest_version
payload_install: ""
AppInfo - latest_version:
# This sensor is set as secondary sensor of the update command
# A secondary sensor will not provide its value as state_topic in discovery of the command
# but in another key, so we specify that key here
state_topic_key: latest_version_topic
Temperature:
icon: mdi:thermometer-lines
unit_of_measurement: °C
Expand Down

0 comments on commit 04cea9d

Please sign in to comment.