From 875e0abd37fb492ab3da2b97ca4ebd929b07fbfe Mon Sep 17 00:00:00 2001 From: Pierre Gironde Date: Sun, 10 May 2026 21:59:50 +0200 Subject: [PATCH] refactor: remove MQTT discovery feature and streamline entity creation --- smartobject.py | 171 ++++++++++++------------------------------------- 1 file changed, 41 insertions(+), 130 deletions(-) diff --git a/smartobject.py b/smartobject.py index 27d6d5c..deac92c 100644 --- a/smartobject.py +++ b/smartobject.py @@ -1,8 +1,6 @@ import appdaemon.plugins.hass.hassapi as hass import pickle import os -import json -import re from virtualsensors import VirtualSensors from expressionparser import ParsingException from logger_interface import LoggerInterface @@ -20,8 +18,6 @@ from logger_interface import LoggerInterface # - Templates library : shared named-template provider from another app # - Virtual sensors : declarative derived sensors defined in YAML # - Attributes override : map HA sensor states onto entity attributes -# - MQTT discovery : entities created via create_entity() are auto- -# published as HA MQTT discovery sensors # # YAML CONFIGURATION # ------------------ @@ -68,15 +64,6 @@ from logger_interface import LoggerInterface # - A HA entity_id → attribute tracks that entity's state live. # - A static value → attribute is set once at startup. # -# mqtt_device_name: -# Human-readable device name used in HA MQTT discovery payloads. -# Defaults to the AppDaemon app name (self.name) if omitted. -# Requires the MQTT integration to be available in HA. -# Used by the MQTT discovery feature (see create_entity()). -# When you create a new entity with create_entity(), it will be attached to a device through MQTT discovery with this name -# and will appear in HA as a child of that device. -# Since MQTT keep those entity forever, you might have to clean up some entities with MQTT Explorer if you change some entity names or delete some entities from your app. -# # DATASET PERSISTENCE # ------------------- # Subclasses can store arbitrary data in self.dataset (any pickle-able @@ -97,8 +84,8 @@ class SmartObject(hass.Hass,LoggerInterface): # Public API # ------------------------------------------------------------------ - # Create (or update) a HA entity and, when MQTT is available, register - # it with HA via MQTT discovery so it appears as a proper device entity. + # Create (or update) a HA entity and tag it with the originating app + # through the ad_app attribute. # # Parameters # ---------- @@ -110,43 +97,45 @@ class SmartObject(hass.Hass,LoggerInterface): # unit_of_measurement : e.g. "°C", "%", "W" # device_class : HA sensor device class, e.g. "temperature" # state_class : "measurement", "total", "total_increasing" - # - # The entity's live state is kept in sync with MQTT automatically via - # a listen_state subscription set up at registration time. # Returns the AppDaemon entity handle. - def create_entity(self, entity_id, state=None, attributes=None, name=None, icon=None, unit_of_measurement=None, device_class=None, state_class=None): + def create_entity(self, entity_id, state=None, attributes=None, friendly_name=None, icon=None, unit_of_measurement=None, device_class=None, state_class=None): entity = self.get_entity(entity_id, check_existence=False) - if self._mqtt_lazy_init() and entity_id not in self._mqtt['entities']: - node_id = self._mqtt['node_id'] - obj = self._sanitize_for_topic(entity_id.split('.')[-1]) - state_topic = f"appdaemon/{node_id}/{obj}/state" - availability_topic = f"appdaemon/{node_id}/{obj}/availability" - config = { - 'name': name or entity_id, - 'unique_id': f"{node_id}_{obj}", - 'state_topic': state_topic, - 'value_template': '{{ value_json.state }}', - 'availability_topic': availability_topic, - 'payload_available': 'online', - 'payload_not_available': 'offline', - 'device': self._mqtt['device'], - } - if icon: config['icon'] = icon - if unit_of_measurement: config['unit_of_measurement'] = unit_of_measurement - if device_class: config['device_class'] = device_class - if state_class: config['state_class'] = state_class - self._mqtt_publish(f"homeassistant/sensor/{node_id}/{obj}/config", config, retain=True) - self._mqtt_publish(availability_topic, 'online', retain=True) - self._mqtt['entities'].add(entity_id) - self._mqtt['handles'][entity_id] = self.listen_state(self._mqtt_sync_state, entity_id) - if state is not None and attributes is not None: - self.set_state(entity_id, state=state, attributes=attributes) - elif state is not None: - self.set_state(entity_id, state=state) - elif attributes is not None: - self.set_state(entity_id, attributes=attributes) - elif not entity.exists(): - self.set_state(entity_id, state='unknown') + if attributes is not None: + attributes = dict(attributes) + else: + attributes = dict() + + # elif state is not None and entity.exists(): + # state_data = self.get_state(entity_id, attribute='all') or {} + # current_attributes = state_data.get('attributes', {}) + # if current_attributes.get('ad_app') != self.name: + # attributes = dict(current_attributes) + # attributes['ad_app'] = self.name + + attributes['ad_app'] = self.name + + if friendly_name is not None: attributes['friendly_name'] = friendly_name + if icon is not None: attributes['icon'] = icon + if unit_of_measurement is not None: attributes['unit_of_measurement'] = unit_of_measurement + if device_class is not None: attributes['device_class'] = device_class + if state_class is not None: attributes['state_class'] = state_class + + if state is not None: + entity.set_state(state=state, attributes=attributes) + else: + entity.set_state(state='unknown', attributes=attributes) + + # if state is not None and attributes is not None: + # self.set_state(entity_id, state=state, attributes=attributes) + # elif state is not None: + # if entity.exists(): + # self.set_state(entity_id, state=state) + # else: + # self.set_state(entity_id, state=state, attributes={'ad_app': self.name}) + # elif attributes is not None: + # self.set_state(entity_id, attributes=attributes) + # elif not entity.exists(): + # self.set_state(entity_id, state='unknown', attributes={'ad_app': self.name}) return entity # Override this in a subclass to return a default state string when the @@ -162,82 +151,6 @@ class SmartObject(hass.Hass,LoggerInterface): # Called at the end of initialize() after all YAML args are processed. def on_initialize_smart_object(self): pass - # ------------------------------------------------------------------ - # MQTT Discovery (internal) - # ------------------------------------------------------------------ - # Called lazily on the first create_entity() call. Checks whether the - # MQTT publish service is available and, if so, initialises self._mqtt - # with the device metadata used in discovery payloads. - # Returns True when MQTT is ready, False otherwise (MQTT not installed - # or the service is not available). - def _mqtt_lazy_init(self): - if hasattr(self, '_mqtt'): - return self._mqtt is not None - self._mqtt = None - try: - available = any( - i.get('domain') == 'mqtt' and i.get('service') == 'publish' - for i in self.list_services('global') - ) - except Exception: - available = False - if not available: - return False - node_id = self._sanitize_for_topic(self.name) - device_name = self.args.get('mqtt_device_name', self.name) - self._mqtt = { - 'node_id': node_id, - 'device': { - 'identifiers': [node_id], - 'name': device_name, - 'manufacturer': 'AppDaemon', - 'model': self.__class__.__name__, - }, - 'entities': set(), - 'handles': {}, - } - return True - - def _sanitize_for_topic(self, value): - return re.sub(r"[^a-zA-Z0-9_]+", "_", str(value)).strip("_").lower() - - # Serialize payload to JSON if it is not already a string, then call - # the mqtt/publish service. retain=True is used for state and config - # topics so HA picks them up immediately after a broker restart. - def _mqtt_publish(self, topic, payload, retain=False): - if not isinstance(payload, str): - payload = json.dumps(payload) - self.call_service('mqtt/publish', topic=topic, payload=payload, retain=retain) - - # listen_state callback — re-publishes the full state + attributes of - # entity_id to its MQTT state topic whenever the HA state changes. - def _mqtt_sync_state(self, entity_id, attribute, old, new, kwargs): - if not getattr(self, '_mqtt', None) or entity_id not in self._mqtt['entities']: - return - state_data = self.get_state(entity_id, attribute='all') or {} - payload = {'state': state_data.get('state')} - payload.update(state_data.get('attributes', {})) - node_id = self._mqtt['node_id'] - obj = self._sanitize_for_topic(entity_id.split('.')[-1]) - self._mqtt_publish(f"appdaemon/{node_id}/{obj}/state", payload, retain=True) - - - - # Called from terminate(). Publishes "offline" availability for every - # registered entity so HA marks them as unavailable, then cancels all - # listen_state subscriptions created during create_entity(). - def _mqtt_terminate(self): - if not getattr(self, '_mqtt', None): - return - for entity_id in self._mqtt['entities']: - node_id = self._mqtt['node_id'] - obj = self._sanitize_for_topic(entity_id.split('.')[-1]) - try: self._mqtt_publish(f"appdaemon/{node_id}/{obj}/availability", 'offline', retain=True) - except Exception: pass - for handle in self._mqtt['handles'].values(): - try: self.cancel_listen_state(handle, silent=True) - except Exception: pass - # ------------------------------------------------------------------ # Dataset persistence helpers # ------------------------------------------------------------------ @@ -325,7 +238,7 @@ class SmartObject(hass.Hass,LoggerInterface): # virtual_sensors: {...} → create derived sensors from YAML spec if 'virtual_sensors' in self.args: - self.virtual_sensors = VirtualSensors(ad_api = self.get_ad_api(),logger_interface = self,super_entity_id = self.entity_id,yaml_block = self.args['virtual_sensors'],templates_library = self.templates_library,constants = self.constants) + self.virtual_sensors = VirtualSensors(ad_api = self,logger_interface = self,super_entity_id = self.entity_id,yaml_block = self.args['virtual_sensors'],templates_library = self.templates_library,constants = self.constants) # attributes_override: {attr: entity_id | static_value, ...} # Each attribute either mirrors a live HA sensor or holds a @@ -362,12 +275,10 @@ class SmartObject(hass.Hass,LoggerInterface): except ParsingException as e: self.log_error(str(e),stop_app = True) - # AppDaemon shutdown hook. Persists self.dataset to disk (if set), - # publishes MQTT offline availability, and cancels all state listeners. + # AppDaemon shutdown hook. Persists self.dataset to disk (if set). def terminate(self): self.event_dispatchers = None self.virtual_sensors = None - self._mqtt_terminate() try: has_dataset = self.dataset != None except AttributeError: has_dataset = False if has_dataset: