Feature/v0.2.0 (#7)

* heateroutput limit 0 - 15000 w

* heatoutput always positive

* ENUM_IN_FSV_2041 value 0c00 unknwon added

* recalculate crc6 checksum and check it

* skip exception to reduce logging

* NASAPacket and NASAMessage prepared for write mode
MQTT Auto Discovery changed single entitity to all entities
NASA Packet crc16 Checksum verificated

* - Changed MQTT Auto Discovery Config Message from single Entitiy to all Entities at ones, known devices are fully configured, not known empty (markt to delete)
- NASAPacket and NASAMessage are now bidirectional, can decode and encode Packets
- Added crc16 Checksum check for any Packet to reduce incorrect value changes
- Folling warnings moved to SkipInvalidPacketException and from warning to debug log level to reduce Logentries
  - Source Adress Class out of enum
  - Destination Adress Class out of enum
  - Checksum for package could not be validatet calculated
  - Message with structure type must have capacity of 1.

* NASA_OUTDOOR_HP as kw unit

* NASA Repository, measurements enums completed

* filter wifikit heartbeat

* process only packets from indoor or outdoor

* correct readme

* remove expire

* device discovery status

* new mqtt hass configuration approach

* added new measurements

* added new logging features from config


* NASA_EHSSENTINEL_TOTAL_COP added

* removed silentMode, added logging proccessedMessage

* loaded devices

* loaded devices counter fix

* only if retain true

* final 0.2.0 commit
This commit is contained in:
echoDaveD
2025-02-22 22:45:18 +01:00
committed by GitHub
parent cce625dabb
commit 48ef003f22
13 changed files with 1069 additions and 386 deletions

View File

@@ -13,25 +13,9 @@ from EHSConfig import EHSConfig
class MQTTClient:
"""
MQTTClient is a singleton class that manages the connection to an MQTT broker and handles
publishing and subscribing to topics. It is designed to work with Home Assistant for
auto-discovery of devices and sensors.
Attributes:
_instance (MQTTClient): The single instance of the MQTTClient class.
STOP (asyncio.Event): Event to signal stopping the MQTT client.
DEVICE_ID (str): The device ID used for MQTT topics.
config (EHSConfig): Configuration object for the MQTT client.
args (EHSArguments): Arguments object for the MQTT client.
broker (str): URL of the MQTT broker.
port (int): Port of the MQTT broker.
client_id (str): Client ID for the MQTT client.
client (gmqtt.Client): MQTT client instance.
topicPrefix (str): Prefix for MQTT topics.
homeAssistantAutoDiscoverTopic (str): Topic for Home Assistant auto-discovery.
useCamelCaseTopicNames (bool): Flag to use camel case for topic names.
known_topics (list): List to keep track of known topics.
known_devices_topic (str): Dedicated topic for storing known topics.
MQTTClient is a singleton class that manages the connection and communication with an MQTT broker.
It handles the initialization, connection, subscription, and message publishing for the MQTT client.
The class also supports Home Assistant auto-discovery and maintains a list of known devices.
"""
_instance = None
STOP = asyncio.Event()
@@ -40,17 +24,16 @@ class MQTTClient:
def __new__(cls, *args, **kwargs):
"""
Create a new instance of the MQTTClient class if one does not already exist.
This method ensures that the MQTTClient class follows the Singleton design pattern,
meaning only one instance of the class can exist at any given time. If an instance
already exists, it returns the existing instance. Otherwise, it creates a new instance
and sets the _initialized attribute to False.
Create a new instance of the class if one does not already exist.
This method ensures that only one instance of the class is created (singleton pattern).
If an instance already exists, it returns the existing instance.
Otherwise, it creates a new instance, marks it as uninitialized, and returns it.
Args:
cls (type): The class being instantiated.
cls: The class being instantiated.
*args: Variable length argument list.
**kwargs: Arbitrary keyword arguments.
Returns:
MQTTClient: The single instance of the MQTTClient class.
An instance of the class.
"""
if not cls._instance:
@@ -60,26 +43,25 @@ class MQTTClient:
def __init__(self):
"""
Initialize the MQTTClient instance.
This constructor initializes the MQTT client with the configuration
provided by the EHSConfig and EHSArguments classes. It sets up the
MQTT broker connection details, client ID, and authentication credentials
if provided. It also assigns callback functions for various MQTT events
such as connect, disconnect, message, and subscribe. Additionally, it
initializes the topic prefix, Home Assistant auto-discover topic, and
topic naming convention.
Initializes the MQTTClient instance.
This constructor sets up the MQTT client with the necessary configuration
parameters, including broker URL, port, client ID, and authentication credentials.
It also assigns callback functions for various MQTT events such as connect,
disconnect, message, and subscribe. Additionally, it initializes topic-related
settings and a list to keep track of known topics.
Attributes:
config (EHSConfig): Configuration object for the MQTT client.
args (EHSArguments): Argument parser object for the MQTT client.
config (EHSConfig): Configuration object for MQTT settings.
args (EHSArguments): Argument parser object.
broker (str): URL of the MQTT broker.
port (int): Port number of the MQTT broker.
client_id (str): Client ID for the MQTT connection.
client (gmqtt.Client): gmqtt client instance.
client (gmqtt.Client): MQTT client instance.
topicPrefix (str): Prefix for MQTT topics.
homeAssistantAutoDiscoverTopic (str): Topic for Home Assistant auto-discovery.
useCamelCaseTopicNames (bool): Flag to use camel case for topic names.
initialized (bool): Flag indicating if the client has been initialized.
known_topics (list): List to keep track of known topics.
known_devices_topic (str): Dedicated topic for storing known topics.
known_devices_topic (str): Topic for storing known devices.
"""
if self._initialized:
@@ -108,17 +90,15 @@ class MQTTClient:
async def connect(self):
"""
Asynchronously connects to the MQTT broker and optionally clears the known devices topic.
This method logs the connection attempt, connects to the MQTT broker using the specified
broker address and port, and sets the keepalive interval. If the CLEAN_KNOWN_DEVICES
argument is set to True, it publishes an empty message to the known devices topic to clear it.
This function logs the connection attempt, connects to the MQTT broker using the specified
broker address and port, and sets the keepalive interval. If the CLEAN_KNOWN_DEVICES argument
is set, it publishes an empty message to the known devices topic to clear it.
Args:
None
Returns:
None
Raises:
Any exceptions raised by the underlying MQTT client library during connection.
"""
logger.info("[MQTT] Connecting to broker...")
await self.client.connect(self.broker, self.port, keepalive=60, version=gmqtt.constants.MQTTv311)
@@ -128,15 +108,13 @@ class MQTTClient:
def subscribe_known_topics(self):
"""
Subscribes the MQTT client to known topics.
This method subscribes the MQTT client to two specific topics:
1. A topic for known devices, constructed using the topic prefix and known devices topic.
2. A status topic for Home Assistant auto-discovery.
The subscription QoS (Quality of Service) level for both topics is set to 1.
Subscribe to predefined MQTT topics.
This method subscribes the MQTT client to a set of known topics, which include:
- A topic for known devices, constructed using the topic prefix and known devices topic.
- A status topic for Home Assistant auto-discovery.
The subscription is done with a QoS level of 1 for both topics.
Logging:
Logs an informational message indicating that the client is subscribing to known devices topic.
Raises:
Any exceptions raised by the gmqtt.Subscription or self.client.subscribe methods.
- Logs an info message indicating the subscription to known devices topic.
"""
logger.info("Subscribe to known devices topic")
@@ -158,7 +136,7 @@ class MQTTClient:
Returns:
None
"""
logger.debug('SUBSCRIBED')
def on_message(self, client, topic, payload, qos, properties):
@@ -169,49 +147,46 @@ class MQTTClient:
topic (str): The topic that the message was received on.
payload (bytes): The message payload.
qos (int): The quality of service level of the message.
properties (paho.mqtt.properties.Properties): The properties of the message.
Behavior:
- If the topic matches the known devices topic, updates the known devices set with the retained message.
- If the topic matches the Home Assistant auto-discover status topic, logs the status message and clears the known devices topic.
properties (paho.mqtt.properties.Properties): The properties associated with the message.
This function performs the following actions:
- If the topic matches the known devices topic, it updates the known topics list with the retained message.
- If the topic matches the Home Assistant auto-discover status topic, it logs the status message and, if the payload indicates that Home Assistant is online, it clears the known devices topic.
"""
if self.known_devices_topic in topic:
# Update the known devices set with the retained message
self.known_topics = list(filter(None, [x.strip() for x in payload.decode().split(",")]))
if properties['retain'] == True:
if self.config.LOGGING['deviceAdded']:
logger.info(f"Loaded devices from known devices Topic:")
for idx, devname in enumerate(self.known_topics, start=1):
logger.info(f"Device no. {idx:<3}: {devname} ")
else:
logger.debug(f"Loaded devices from known devices Topic:")
for idx, devname in enumerate(self.known_topics):
logger.debug(f"Device added no. {idx:<3}: {devname} ")
if f"{self.homeAssistantAutoDiscoverTopic}/status" == topic:
logger.info(f"HASS Status Messages {topic} received: {payload.decode()}")
if payload.decode() == "online":
self._publish(f"{self.topicPrefix.replace('/', '')}/{self.known_devices_topic}", " ", retain=True)
logger.info("Known Devices Topic have been cleared")
def refresh_known_devices(self, devname):
"""
Refreshes the list of known devices by publishing the updated list to the MQTT topic.
Args:
devname (str): The name of the device to be refreshed.
Returns:
None
"""
self._publish(f"{self.topicPrefix.replace('/', '')}/{self.known_devices_topic}", ",".join(self.known_topics), retain=True)
logger.info("Known Devices Topic have been cleared")
self.clear_hass()
logger.info("All configuration from HASS has been resetet")
def on_connect(self, client, flags, rc, properties):
"""
Callback function for when the client receives a CONNACK response from the server.
Args:
client (paho.mqtt.client.Client): The client instance for this callback.
flags (dict): Response flags sent by the broker.
rc (int): The connection result.
properties (paho.mqtt.properties.Properties): The properties associated with the connection.
Returns:
None
Logs:
- Info: When connected successfully with result code 0.
- Error: When failed to connect with a non-zero result code.
Parameters:
client (paho.mqtt.client.Client): The client instance for this callback.
flags (dict): Response flags sent by the broker.
rc (int): The connection result.
properties (paho.mqtt.properties.Properties): The properties associated with the connection.
If the connection is successful (rc == 0), logs a success message and subscribes to known topics if any.
Otherwise, logs an error message with the return code.
"""
if rc == 0:
logger.info(f"Connected to MQTT with result code {rc}")
if len(self.homeAssistantAutoDiscoverTopic) > 0:
@@ -222,14 +197,13 @@ class MQTTClient:
def on_disconnect(self, client, packet, exc=None):
"""
Callback function that is called when the client disconnects from the MQTT broker.
This function logs the disconnection event and attempts to reconnect the client
in case of an unexpected disconnection. It will keep trying to reconnect every
5 seconds until successful.
Args:
client (paho.mqtt.client.Client): The MQTT client instance that disconnected.
packet (paho.mqtt.client.MQTTMessage): The MQTT message packet received during disconnection.
exc (Exception, optional): The exception that caused the disconnection, if any. Defaults to None.
Logs:
Logs an info message indicating disconnection.
Logs a warning message indicating an unexpected disconnection and attempts to reconnect.
Logs an error message if reconnection fails and retries every 5 seconds.
packet (paho.mqtt.packet.Packet): The disconnect packet.
exc (Exception, optional): The exception that caused the disconnection, if any.
"""
logger.info(f"Disconnected with result code ")
@@ -244,11 +218,11 @@ class MQTTClient:
def _publish(self, topic, payload, qos=0, retain=False):
"""
Publish a message to a specified MQTT topic.
Publishes a message to a specified MQTT topic.
Args:
topic (str): The MQTT topic to publish to.
payload (str): The message payload to publish.
qos (int, optional): The Quality of Service level for message delivery. Defaults to 0.
qos (int, optional): The Quality of Service level for the message. Defaults to 0.
retain (bool, optional): If True, the message will be retained by the broker. Defaults to False.
Returns:
None
@@ -257,33 +231,51 @@ class MQTTClient:
logger.debug(f"MQTT Publish Topic: {topic} payload: {payload}")
self.client.publish(f"{topic}", payload, qos, retain)
#time.sleep(0.1)
def refresh_known_devices(self, devname):
"""
Refreshes the list of known devices by publishing the current known topics to the MQTT broker.
Args:
devname (str): The name of the device to refresh.
This function constructs a topic string by replacing '/' with an empty string in the topicPrefix,
then concatenates it with the known_devices_topic. It publishes the known topics as a comma-separated
string to this constructed topic with the retain flag set to True.
"""
self.known_topics.append(devname)
if self.config.LOGGING['deviceAdded']:
logger.info(f"Device added no. {len(self.known_topics):<3}: {devname} ")
else:
logger.debug(f"Device added no. {len(self.known_topics):<3}: {devname} ")
self._publish(f"{self.topicPrefix.replace('/', '')}/{self.known_devices_topic}", ",".join(self.known_topics), retain=True)
def publish_message(self, name, value):
"""
Publishes a message to an MQTT topic.
This method normalizes the given name, determines the appropriate MQTT topic,
and publishes the provided value to that topic. If Home Assistant auto-discovery
is enabled, it will also handle the auto-discovery configuration.
This function normalizes the given name, determines the appropriate MQTT topic,
and publishes the provided value to that topic. It also handles Home Assistant
auto-discovery if configured.
Args:
name (str): The name of the sensor or device.
value (int, float, bool, str): The value to be published. If the value is a float,
it will be rounded to two decimal places.
it will be rounded to two decimal places.
Raises:
KeyError: If the name is not found in the NASA_REPO configuration.
ValueError: If the value type is not supported for publishing.
"""
newname = f"{self._normalize_name(name)}"
if len(self.homeAssistantAutoDiscoverTopic) > 0:
if name not in self.known_topics:
self.auto_discover_hass(name)
self.refresh_known_devices(name)
sensor_type = "sensor"
if 'enum' in self.config.NASA_REPO[name]:
enum = [*self.config.NASA_REPO[name]['enum'].values()]
if all([en.lower() in ['on', 'off'] for en in enum]):
sensor_type = "binary_sensor"
topicname = f"{self.config.MQTT['homeAssistantAutoDiscoverTopic']}/{sensor_type}/{self.DEVICE_ID}_{newname.lower()}/state"
if name not in self.known_topics:
self.auto_discover_hass(topicname, name, newname, sensor_type)
else:
topicname = f"{self.topicPrefix.replace('/', '')}/{newname}"
@@ -292,78 +284,24 @@ class MQTTClient:
self._publish(topicname, value, qos=2, retain=False)
def auto_discover_hass(self, topicname, nameraw, namenorm, sensor_type):
"""
Automatically discovers and configures Home Assistant entities for the MQTT client.
This function creates and publishes a configuration payload for Home Assistant's MQTT discovery.
It supports both sensor and binary sensor types, and sets appropriate attributes based on the
provided sensor type and unit of measurement.
Args:
topicname (str): The MQTT topic name.
nameraw (str): The raw name of the sensor.
namenorm (str): The normalized name of the sensor.
sensor_type (str): The type of the sensor (e.g., "sensor" or "binary_sensor").
Returns:
None
def clear_hass(self):
"""
clears all entities/components fpr the HomeAssistant Device
"""
entities = {}
for nasa in self.config.NASA_REPO:
namenorm = self._normalize_name(nasa)
sensor_type = self._get_sensor_type(nasa)
entities[namenorm] = {"platform": sensor_type}
entity = { namenorm: {
"name": f"{namenorm}",
"object_id": f"{self.DEVICE_ID}_{namenorm.lower()}",
"unique_id": f"{self.DEVICE_ID}_{nameraw.lower()}",
"platform": sensor_type,
"value_template": "{{ value }}",
"state_topic": f"{self.config.MQTT['homeAssistantAutoDiscoverTopic']}/{sensor_type}/{self.DEVICE_ID}_{namenorm.lower()}/state",
}
}
if sensor_type == "sensor":
if len(self.config.NASA_REPO[nameraw]['unit']) > 0:
entity[namenorm]['unit_of_measurement'] = self.config.NASA_REPO[nameraw]['unit']
if entity[namenorm]['unit_of_measurement'] == "\u00b0C":
entity[namenorm]['device_class'] = "temperature"
elif entity[namenorm]['unit_of_measurement'] == '%':
entity[namenorm]['state_class'] = "measurement"
elif entity[namenorm]['unit_of_measurement'] == 'kW':
entity[namenorm]['device_class'] = "power"
elif entity[namenorm]['unit_of_measurement'] == 'rpm':
entity[namenorm]['state_class'] = "measurement"
elif entity[namenorm]['unit_of_measurement'] == 'bar':
entity[namenorm]['device_class'] = "pressure"
elif entity[namenorm]['unit_of_measurement'] == 'HP':
entity[namenorm]['device_class'] = "power"
elif entity[namenorm]['unit_of_measurement'] == 'hz':
entity[namenorm]['device_class'] = "frequency"
else:
entity[namenorm]['device_class'] = None
else:
entity[namenorm]['payload_on'] = "ON"
entity[namenorm]['payload_off'] = "OFF"
if 'state_class' in self.config.NASA_REPO[nameraw]:
entity[namenorm]['state_class'] = self.config.NASA_REPO[nameraw]['state_class']
if 'device_class' in self.config.NASA_REPO[nameraw]:
entity[namenorm]['device_class'] = self.config.NASA_REPO[nameraw]['device_class']
device = {
"device": {
"identifiers": self.DEVICE_ID,
"name": "Samsung EHS",
"manufacturer": "Samsung",
"model": "Mono HQ Quiet",
"sw_version": "1.0.0"
},
"origin": {
"name": "EHS-Sentinel",
"support_url": "https://github.com/echoDaveD/EHS-Sentinel"
},
"components": entity,
"device": self._get_device(),
"origin": self._get_origin(),
"components": entities,
"qos": 2
}
logger.debug(f"Auto Discovery HomeAssistant Message: ")
logger.debug(f"Auto Discovery HomeAssistant Clear Message: ")
logger.debug(f"{device}")
self._publish(f"{self.config.MQTT['homeAssistantAutoDiscoverTopic']}/device/{self.DEVICE_ID}/config",
@@ -371,9 +309,99 @@ class MQTTClient:
qos=2,
retain=True)
self.known_topics.append(nameraw)
self.refresh_known_devices(nameraw)
def auto_discover_hass(self, name):
"""
Automatically discovers and configures Home Assistant entities based on the NASA_REPO configuration.
This function iterates through the NASA_REPO configuration to create and configure entities for Home Assistant.
It determines the type of sensor (binary_sensor or sensor) based on the configuration and sets various attributes
such as unit of measurement, device class, state class, and payloads for binary sensors. It then constructs a device
configuration payload and publishes it to the Home Assistant MQTT discovery topic.
The function performs the following steps:
1. Iterates through the NASA_REPO configuration.
2. Normalizes the name of each NASA_REPO entry.
3. Determines the sensor type (binary_sensor or sensor) based on the 'enum' values.
4. Configures the entity attributes such as unit of measurement, device class, state class, and payloads.
5. Constructs a device configuration payload.
6. Publishes the device configuration to the Home Assistant MQTT discovery topic.
Attributes:
entities (dict): A dictionary to store the configured entities.
device (dict): A dictionary to store the device configuration payload.
Logs:
Logs the constructed device configuration payload for debugging purposes.
Publishes:
Publishes the device configuration payload to the Home Assistant MQTT discovery topic with QoS 2 and retain flag set to True.
"""
entity = {}
namenorm = self._normalize_name(name)
sensor_type = self._get_sensor_type(name)
entity = {
"name": f"{namenorm}",""
"object_id": f"{self.DEVICE_ID}_{namenorm.lower()}",
"unique_id": f"{self.DEVICE_ID}_{name.lower()}",
"platform": sensor_type,
"value_template": "{{ value }}",
#"value_template": "{{ value if value | length > 0 else 'unavailable' }}",
"state_topic": f"{self.config.MQTT['homeAssistantAutoDiscoverTopic']}/{sensor_type}/{self.DEVICE_ID}_{namenorm.lower()}/state",
}
if sensor_type == "sensor":
if len(self.config.NASA_REPO[name]['unit']) > 0:
entity['unit_of_measurement'] = self.config.NASA_REPO[name]['unit']
if entity['unit_of_measurement'] == "\u00b0C":
entity['device_class'] = "temperature"
elif entity['unit_of_measurement'] == '%':
entity['state_class'] = "measurement"
elif entity['unit_of_measurement'] == 'kW':
entity['device_class'] = "power"
elif entity['unit_of_measurement'] == 'rpm':
entity['state_class'] = "measurement"
elif entity['unit_of_measurement'] == 'bar':
entity['device_class'] = "pressure"
elif entity['unit_of_measurement'] == 'HP':
entity['device_class'] = "power"
elif entity['unit_of_measurement'] == 'hz':
entity['device_class'] = "frequency"
else:
entity['device_class'] = None
else:
entity['payload_on'] = "ON"
entity['payload_off'] = "OFF"
if 'state_class' in self.config.NASA_REPO[name]:
entity['state_class'] = self.config.NASA_REPO[name]['state_class']
if 'device_class' in self.config.NASA_REPO[name]:
entity['device_class'] = self.config.NASA_REPO[name]['device_class']
device = {
"device": self._get_device(),
"origin": self._get_origin(),
"qos": 2
}
device.update(entity)
logger.debug(f"Auto Discovery HomeAssistant Message: ")
logger.debug(f"{device}")
self._publish(f"{self.config.MQTT['homeAssistantAutoDiscoverTopic']}/{sensor_type}/{self.DEVICE_ID}_{name.lower()}/config",
json.dumps(device, ensure_ascii=False),
qos=2,
retain=True)
def _get_device(self):
return {
"identifiers": self.DEVICE_ID,
"name": "Samsung EHS",
"manufacturer": "Samsung",
"model": "Mono HQ Quiet",
"sw_version": "1.0.0"
}
def _get_origin(self):
return {
"name": "EHS-Sentinel",
"support_url": "https://github.com/echoDaveD/EHS-Sentinel"
}
def _normalize_name(self, name):
"""
@@ -405,3 +433,19 @@ class MQTTClient:
tmpname = name
return tmpname
def _get_sensor_type(self, name):
"""
return the sensor type of given measurement
Args:
name (str): The name of the measurement.
Returns:
str: The sensor type: sensor or binary_sensor.
"""
sensor_type = "sensor"
if 'enum' in self.config.NASA_REPO[name]:
enum = [*self.config.NASA_REPO[name]['enum'].values()]
if all([en.lower() in ['on', 'off'] for en in enum]):
sensor_type = "binary_sensor"
return sensor_type