Feature/v1.0.0 release (#12)

v1.0.0 - 2025-03-13
EHS-Sentinel has been heavily modified to incorporate the control mechanism
The read-in behavior of the modbus registers has been revised from chunks to single byte
MessageProcessor now runs asynchronously
MessageProducer added which takes over the writing communication with the WP
Configuration of HASS entities has moved from hardcoded to NASA Repository
NASA Repository has been fundamentally changed
All FSV Values, NASA_POWER, VAR_IN_TEMP_WATER_LAW_TARGET_F, NASA_INDOOR_OPMODE are allowed for writing mode
NASA_OUTDOOR_DEFROST_STEP DEFROST STEP 10 (b'0xa') added
ENUM_IN_SG_READY_MODE_STATE ACTIVE (b'0x2') added
New configuration point allowControl to allow control of the Samsung EHS heat pump (deactivated by default).
[!CAUTION]
This functionality requires that EHS-Sentinel actively communicates with the Samsung EHS, so EHS-Sentinel intervenes here in the Modbus data traffic between the components (it sends its own messages). The activation of this functionality is exclusively at your own risk. I assume no liability for any damage caused.

new configuration points in logging
controlMessage (default False) to print out the controlled mesagges
invalidPacket (default False) prints out invalid messages (length not ok, x34 not at end...)
Dashboard template has been split, ressources/dashboard_readonly_template.yaml is for readonly mode and the ressources/dashboard_controlmode_template.yaml for control mode
This commit is contained in:
echoDaveD
2025-03-13 19:57:33 +01:00
committed by GitHub
parent 43de00aacc
commit f0222d750f
26 changed files with 7251 additions and 4316 deletions

1
.gitignore vendored
View File

@@ -174,3 +174,4 @@ prot.csv
helpertils/serial.py helpertils/serial.py
helpertils/test.py helpertils/test.py
helpertils/socker.py helpertils/socker.py
helpertils/messagesFound.txt

View File

@@ -30,29 +30,10 @@ class IndentFormatter(logging.Formatter):
def __init__( self, fmt=None, datefmt=None ): def __init__( self, fmt=None, datefmt=None ):
"""
Initializes the CustomLogger instance.
Args:
fmt (str, optional): The format string for the log messages. Defaults to None.
datefmt (str, optional): The format string for the date in log messages. Defaults to None.
Attributes:
baseline (int): The baseline stack depth when the logger is initialized.
"""
logging.Formatter.__init__(self, fmt, datefmt) logging.Formatter.__init__(self, fmt, datefmt)
self.baseline = len(inspect.stack()) self.baseline = len(inspect.stack())
def format( self, rec ): def format( self, rec ):
"""
Formats the log record by adding indentation and function name.
This method customizes the log record by adding an indentation level
based on the current stack depth and includes the name of the function
from which the log call was made. It then uses the base Formatter class
to format the record and returns the formatted string.
Args:
rec (logging.LogRecord): The log record to be formatted.
Returns:
str: The formatted log record string.
"""
log_fmt = self.FORMATS.get(rec.levelno) log_fmt = self.FORMATS.get(rec.levelno)
formatter = logging.Formatter(log_fmt) formatter = logging.Formatter(log_fmt)
@@ -76,11 +57,5 @@ logger.addHandler(handler)
logger.setLevel(logging.INFO) logger.setLevel(logging.INFO)
def setDebugMode(): def setDebugMode():
"""
Set the logging level to DEBUG and log a message indicating that debug mode is enabled.
This function sets the logging level of the logger to DEBUG, which means that all messages
at the DEBUG level and above will be logged. It also logs a debug message to indicate that
debug mode has been activated.
"""
logger.setLevel(logging.DEBUG) logger.setLevel(logging.DEBUG)
logger.debug("Debug mode is on...") logger.debug("Debug mode is on...")

View File

@@ -24,34 +24,12 @@ class EHSArguments:
_instance = None _instance = None
def __new__(cls, *args, **kwargs): def __new__(cls, *args, **kwargs):
"""
Create and return a new instance of the class, ensuring that only one instance exists (singleton pattern).
This method overrides the default behavior of object creation to implement the singleton pattern.
It checks if an instance of the class already exists; if not, it creates a new instance and marks it as uninitialized.
Args:
*args: Variable length argument list.
**kwargs: Arbitrary keyword arguments.
Returns:
EHSArguments: The singleton instance of the EHSArguments class.
"""
if not cls._instance: if not cls._instance:
cls._instance = super(EHSArguments, cls).__new__(cls, *args, **kwargs) cls._instance = super(EHSArguments, cls).__new__(cls, *args, **kwargs)
cls._instance._initialized = False cls._instance._initialized = False
return cls._instance return cls._instance
def __init__(self): def __init__(self):
"""
Initializes the EHSArguments class, parses command-line arguments, and sets up configuration.
This method performs the following steps:
1. Checks if the class has already been initialized to prevent re-initialization.
2. Sets up an argument parser to handle command-line arguments.
3. Parses the command-line arguments and validates them.
4. Checks if the specified config file and dump file exist.
5. Sets the class attributes based on the parsed arguments.
Raises:
ArgumentException: If the required arguments are not provided or if the specified files do not exist.
"""
if self._initialized: if self._initialized:
return return
self._initialized = True self._initialized = True

View File

@@ -11,15 +11,6 @@ class EHSConfig():
Singleton class to handle the configuration for the EHS Sentinel application. Singleton class to handle the configuration for the EHS Sentinel application.
This class reads configuration parameters from a YAML file and validates them. This class reads configuration parameters from a YAML file and validates them.
It ensures that only one instance of the configuration exists throughout the application. It ensures that only one instance of the configuration exists throughout the application.
Attributes:
MQTT (dict): Configuration parameters for MQTT.
GENERAL (dict): General configuration parameters.
SERIAL (dict): Configuration parameters for serial communication.
NASA_REPO (dict): Configuration parameters for NASA repository.
Methods:
__new__(cls, *args, **kwargs): Ensures only one instance of the class is created.
__init__(self, *args, **kwargs): Initializes the configuration by reading and validating the YAML file.
validate(self): Validates the configuration parameters.
""" """
_instance = None _instance = None
@@ -30,44 +21,15 @@ class EHSConfig():
NASA_REPO = None NASA_REPO = None
LOGGING = {} LOGGING = {}
POLLING = None POLLING = None
NASA_VAL_STORE = {}
def __new__(cls, *args, **kwargs): def __new__(cls, *args, **kwargs):
"""
Create a new instance of the EHSConfig class if one does not already exist.
This method ensures that only one instance of the EHSConfig class is created
(singleton pattern). If an instance already exists, it returns the existing instance.
Args:
cls: The class being instantiated.
*args: Variable length argument list.
**kwargs: Arbitrary keyword arguments.
Returns:
EHSConfig: The single instance of the EHSConfig class.
"""
if not cls._instance: if not cls._instance:
cls._instance = super(EHSConfig, cls).__new__(cls, *args, **kwargs) cls._instance = super(EHSConfig, cls).__new__(cls, *args, **kwargs)
cls._instance._initialized = False cls._instance._initialized = False
return cls._instance return cls._instance
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
"""
Initialize the EHSConfig instance.
This method initializes the EHSConfig instance by loading configuration
settings from a YAML file specified in the EHSArguments. It ensures that
the initialization process is only performed once by checking the
_initialized attribute. If the instance is already initialized, the method
returns immediately. Otherwise, it proceeds to load the configuration and
validate it.
Args:
*args: Variable length argument list.
**kwargs: Arbitrary keyword arguments.
Attributes:
args (EHSArguments): An instance of EHSArguments containing the
configuration file path.
MQTT (dict): MQTT configuration settings loaded from the YAML file.
GENERAL (dict): General configuration settings loaded from the YAML file.
SERIAL (dict): Serial configuration settings loaded from the YAML file.
"""
if self._initialized: if self._initialized:
return return
self._initialized = True self._initialized = True
@@ -101,16 +63,6 @@ class EHSConfig():
self.validate() self.validate()
def parse_time_string(self, time_str: str) -> int: def parse_time_string(self, time_str: str) -> int:
"""
Parses a time string like '10m' or '10s' and converts it to seconds.
Supported formats:
- '10m' for 10 minutes
- '10s' for 10 seconds
Returns:
- Equivalent time in seconds as an integer.
"""
match = re.match(r'^(\d+)([smh])$', time_str.strip(), re.IGNORECASE) match = re.match(r'^(\d+)([smh])$', time_str.strip(), re.IGNORECASE)
if not match: if not match:
raise ValueError("Invalid time format. Use '10s', '10m', or '10h'.") raise ValueError("Invalid time format. Use '10s', '10m', or '10h'.")
@@ -126,15 +78,6 @@ class EHSConfig():
return value * conversion_factors[unit] return value * conversion_factors[unit]
def validate(self): def validate(self):
"""
Validates the configuration parameters for the EHS Sentinel application.
This method checks the presence and validity of various configuration parameters
such as NASA repository file, serial device, baudrate, MQTT broker URL, broker port,
and MQTT credentials. It raises a ConfigException if any required parameter is missing
or invalid. Additionally, it sets default values for optional parameters if they are not provided.
Raises:
ConfigException: If any required configuration parameter is missing or invalid.
"""
if os.path.isfile(self.GENERAL['nasaRepositoryFile']): if os.path.isfile(self.GENERAL['nasaRepositoryFile']):
with open(self.GENERAL['nasaRepositoryFile'], mode='r') as file: with open(self.GENERAL['nasaRepositoryFile'], mode='r') as file:
self.NASA_REPO = yaml.safe_load(file) self.NASA_REPO = yaml.safe_load(file)
@@ -144,6 +87,9 @@ class EHSConfig():
if 'protocolFile' not in self.GENERAL: if 'protocolFile' not in self.GENERAL:
self.GENERAL['protocolFile'] = None self.GENERAL['protocolFile'] = None
if 'allowControl' not in self.GENERAL:
self.GENERAL['allowControl'] = False
if self.SERIAL is None and self.TCP is None: if self.SERIAL is None and self.TCP is None:
raise ConfigException(argument="", message="define tcp or serial config parms") raise ConfigException(argument="", message="define tcp or serial config parms")
@@ -213,8 +159,8 @@ class EHSConfig():
if 'messageNotFound' not in self.LOGGING: if 'messageNotFound' not in self.LOGGING:
self.LOGGING['messageNotFound'] = False self.LOGGING['messageNotFound'] = False
if 'messageNotFound' not in self.LOGGING: if 'invalidPacket' not in self.LOGGING:
self.LOGGING['messageNotFound'] = False self.LOGGING['invalidPacket'] = False
if 'deviceAdded' not in self.LOGGING: if 'deviceAdded' not in self.LOGGING:
self.LOGGING['deviceAdded'] = True self.LOGGING['deviceAdded'] = True
@@ -228,6 +174,9 @@ class EHSConfig():
if 'pollerMessage' not in self.LOGGING: if 'pollerMessage' not in self.LOGGING:
self.LOGGING['pollerMessage'] = False self.LOGGING['pollerMessage'] = False
if 'controlMessage' not in self.LOGGING:
self.LOGGING['controlMessage'] = False
logger.info(f"Logging Config:") logger.info(f"Logging Config:")
for key, value in self.LOGGING.items(): for key, value in self.LOGGING.items():
logger.info(f" {key}: {value}") logger.info(f" {key}: {value}")

View File

@@ -10,6 +10,7 @@ import gmqtt
from CustomLogger import logger from CustomLogger import logger
from EHSArguments import EHSArguments from EHSArguments import EHSArguments
from EHSConfig import EHSConfig from EHSConfig import EHSConfig
from MessageProducer import MessageProducer
class MQTTClient: class MQTTClient:
""" """
@@ -23,18 +24,6 @@ class MQTTClient:
DEVICE_ID = "samsung_ehssentinel" DEVICE_ID = "samsung_ehssentinel"
def __new__(cls, *args, **kwargs): def __new__(cls, *args, **kwargs):
"""
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: The class being instantiated.
*args: Variable length argument list.
**kwargs: Arbitrary keyword arguments.
Returns:
An instance of the class.
"""
if not cls._instance: if not cls._instance:
cls._instance = super(MQTTClient, cls).__new__(cls) cls._instance = super(MQTTClient, cls).__new__(cls)
@@ -42,32 +31,12 @@ class MQTTClient:
return cls._instance return cls._instance
def __init__(self): def __init__(self):
"""
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 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): 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): Topic for storing known devices.
"""
if self._initialized: if self._initialized:
return return
self.config = EHSConfig() self.config = EHSConfig()
self.args = EHSArguments() self.args = EHSArguments()
self.message_producer = None
self._initialized = True self._initialized = True
self.broker = self.config.MQTT['broker-url'] self.broker = self.config.MQTT['broker-url']
self.port = self.config.MQTT['broker-port'] self.port = self.config.MQTT['broker-port']
@@ -88,17 +57,6 @@ class MQTTClient:
self.known_devices_topic = "known/devices" # Dedicated topic for storing known topics self.known_devices_topic = "known/devices" # Dedicated topic for storing known topics
async def connect(self): async def connect(self):
"""
Asynchronously connects to the MQTT broker and optionally clears the known devices topic.
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
"""
logger.info("[MQTT] Connecting to broker...") logger.info("[MQTT] Connecting to broker...")
await self.client.connect(self.broker, self.port, keepalive=60, version=gmqtt.constants.MQTTv311) await self.client.connect(self.broker, self.port, keepalive=60, version=gmqtt.constants.MQTTv311)
@@ -106,53 +64,21 @@ class MQTTClient:
self._publish(f"{self.topicPrefix.replace('/', '')}/{self.known_devices_topic}", " ", retain=True) self._publish(f"{self.topicPrefix.replace('/', '')}/{self.known_devices_topic}", " ", retain=True)
logger.info("Known Devices Topic have been cleared") logger.info("Known Devices Topic have been cleared")
def subscribe_known_topics(self): def subscribe_known_topics(self):
"""
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 info message indicating the subscription to known devices topic.
"""
logger.info("Subscribe to known devices topic") logger.info("Subscribe to known devices topic")
self.client.subscribe( sublist = [
[
gmqtt.Subscription(f"{self.topicPrefix.replace('/', '')}/{self.known_devices_topic}", 1), gmqtt.Subscription(f"{self.topicPrefix.replace('/', '')}/{self.known_devices_topic}", 1),
gmqtt.Subscription(f"{self.homeAssistantAutoDiscoverTopic}/status", 1) gmqtt.Subscription(f"{self.homeAssistantAutoDiscoverTopic}/status", 1)
] ]
) if self.config.GENERAL['allowControl']:
sublist.append(gmqtt.Subscription(f"{self.topicPrefix.replace('/', '')}/entity/+/set", 1))
self.client.subscribe(sublist)
def on_subscribe(self, client, mid, qos, properties): def on_subscribe(self, client, mid, qos, properties):
"""
Callback function that is called when the client subscribes to a topic.
Args:
client (paho.mqtt.client.Client): The client instance for this callback.
mid (int): The message ID for the subscribe request.
qos (int): The Quality of Service level for the subscription.
properties (paho.mqtt.properties.Properties): The properties associated with the subscription.
Returns:
None
"""
logger.debug('SUBSCRIBED') logger.debug('SUBSCRIBED')
def on_message(self, client, topic, payload, qos, properties): def on_message(self, client, topic, payload, qos, properties):
"""
Callback function that is triggered when a message is received on a subscribed topic.
Args:
client (paho.mqtt.client.Client): The MQTT client instance.
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 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: if self.known_devices_topic in topic:
# Update the known devices set with the retained message # Update the known devices set with the retained message
self.known_topics = list(filter(None, [x.strip() for x in payload.decode().split(",")])) self.known_topics = list(filter(None, [x.strip() for x in payload.decode().split(",")]))
@@ -174,19 +100,15 @@ class MQTTClient:
logger.info("Known Devices Topic have been cleared") logger.info("Known Devices Topic have been cleared")
self.clear_hass() self.clear_hass()
logger.info("All configuration from HASS has been resetet") logger.info("All configuration from HASS has been resetet")
if topic.startswith(f"{self.topicPrefix.replace('/', '')}/entity"):
logger.info(f"HASS Set Entity Messages {topic} received: {payload.decode()}")
parts = topic.split("/")
if self.message_producer is None:
self.message_producer = MessageProducer(None)
asyncio.create_task(self.message_producer.write_request(parts[2], payload.decode(), read_request_after=True))
def on_connect(self, client, flags, rc, properties): def on_connect(self, client, flags, rc, properties):
"""
Callback function for when the client receives a CONNACK response from the server.
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: if rc == 0:
logger.info(f"Connected to MQTT with result code {rc}") logger.info(f"Connected to MQTT with result code {rc}")
if len(self.homeAssistantAutoDiscoverTopic) > 0: if len(self.homeAssistantAutoDiscoverTopic) > 0:
@@ -194,18 +116,7 @@ class MQTTClient:
else: else:
logger.error(f"Failed to connect, return code {rc}") logger.error(f"Failed to connect, return code {rc}")
def on_disconnect(self, client, packet, exc=None): 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.packet.Packet): The disconnect packet.
exc (Exception, optional): The exception that caused the disconnection, if any.
"""
logger.info(f"Disconnected with result code ") logger.info(f"Disconnected with result code ")
logger.warning("Unexpected disconnection. Reconnecting...") logger.warning("Unexpected disconnection. Reconnecting...")
while True: while True:
@@ -216,31 +127,12 @@ class MQTTClient:
logger.error(f"Reconnection failed: {e}") logger.error(f"Reconnection failed: {e}")
time.sleep(5) time.sleep(5)
def _publish(self, topic, payload, qos=0, retain=False): def _publish(self, topic, payload, qos=0, retain=False):
"""
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 the message. Defaults to 0.
retain (bool, optional): If True, the message will be retained by the broker. Defaults to False.
Returns:
None
"""
logger.debug(f"MQTT Publish Topic: {topic} payload: {payload}") logger.debug(f"MQTT Publish Topic: {topic} payload: {payload}")
self.client.publish(f"{topic}", payload, qos, retain) self.client.publish(f"{topic}", payload, qos, retain)
#time.sleep(0.1) #time.sleep(0.1)
def refresh_known_devices(self, devname): 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) self.known_topics.append(devname)
if self.config.LOGGING['deviceAdded']: if self.config.LOGGING['deviceAdded']:
logger.info(f"Device added no. {len(self.known_topics):<3}: {devname} ") logger.info(f"Device added no. {len(self.known_topics):<3}: {devname} ")
@@ -248,20 +140,7 @@ class MQTTClient:
logger.debug(f"Device added no. {len(self.known_topics):<3}: {devname} ") 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) self._publish(f"{self.topicPrefix.replace('/', '')}/{self.known_devices_topic}", ",".join(self.known_topics), retain=True)
def publish_message(self, name, value): async def publish_message(self, name, value):
"""
Publishes a message to an MQTT topic.
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.
Raises:
ValueError: If the value type is not supported for publishing.
"""
newname = f"{self._normalize_name(name)}" newname = f"{self._normalize_name(name)}"
if len(self.homeAssistantAutoDiscoverTopic) > 0: if len(self.homeAssistantAutoDiscoverTopic) > 0:
@@ -270,13 +149,10 @@ class MQTTClient:
self.auto_discover_hass(name) self.auto_discover_hass(name)
self.refresh_known_devices(name) self.refresh_known_devices(name)
time.sleep(1) if self.config.NASA_REPO[name]['hass_opts']['writable']:
sensor_type = self.config.NASA_REPO[name]['hass_opts']['platform']['type']
sensor_type = "sensor" else:
if 'enum' in self.config.NASA_REPO[name]: sensor_type = self.config.NASA_REPO[name]['hass_opts']['default_platform']
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" topicname = f"{self.config.MQTT['homeAssistantAutoDiscoverTopic']}/{sensor_type}/{self.DEVICE_ID}_{newname.lower()}/state"
else: else:
topicname = f"{self.topicPrefix.replace('/', '')}/{newname}" topicname = f"{self.topicPrefix.replace('/', '')}/{newname}"
@@ -287,13 +163,13 @@ class MQTTClient:
self._publish(topicname, value, qos=2, retain=False) self._publish(topicname, value, qos=2, retain=False)
def clear_hass(self): def clear_hass(self):
"""
clears all entities/components fpr the HomeAssistant Device
"""
entities = {} entities = {}
for nasa in self.config.NASA_REPO: for nasa in self.config.NASA_REPO:
namenorm = self._normalize_name(nasa) namenorm = self._normalize_name(nasa)
sensor_type = self._get_sensor_type(nasa) if self.config.NASA_REPO[nasa]['hass_opts']['writable']:
sensor_type = self.config.NASA_REPO[nasa]['hass_opts']['platform']['type']
else:
sensor_type = self.config.NASA_REPO[nasa]['hass_opts']['default_platform']
entities[namenorm] = {"platform": sensor_type} entities[namenorm] = {"platform": sensor_type}
device = { device = {
@@ -312,69 +188,47 @@ class MQTTClient:
retain=True) retain=True)
def auto_discover_hass(self, name): 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 = {} entity = {}
namenorm = self._normalize_name(name) namenorm = self._normalize_name(name)
sensor_type = self._get_sensor_type(name)
entity = { entity = {
"name": f"{namenorm}","" "name": f"{namenorm}",
"object_id": f"{self.DEVICE_ID}_{namenorm.lower()}", "object_id": f"{self.DEVICE_ID}_{namenorm.lower()}",
"unique_id": f"{self.DEVICE_ID}_{name.lower()}", "unique_id": f"{self.DEVICE_ID}_{name.lower()}",
"platform": sensor_type, "force_update": True,
#"expire_after": 86400, # 1 day (24h * 60m * 60s) #"expire_after": 86400, # 1 day (24h * 60m * 60s)
"value_template": "{{ value }}", "value_template": "{{ value }}"
#"value_template": "{{ value if value | length > 0 else 'unavailable' }}", #"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 self.config.NASA_REPO[name]['hass_opts']['writable'] and self.config.GENERAL['allowControl']:
if sensor_type == "sensor": sensor_type = self.config.NASA_REPO[name]['hass_opts']['platform']['type']
if len(self.config.NASA_REPO[name]['unit']) > 0: if sensor_type == 'select':
entity['unit_of_measurement'] = self.config.NASA_REPO[name]['unit'] entity['options'] = self.config.NASA_REPO[name]['hass_opts']['platform']['options']
if entity['unit_of_measurement'] == "\u00b0C": if sensor_type == 'number':
entity['device_class'] = "temperature" entity['mode'] = self.config.NASA_REPO[name]['hass_opts']['platform']['mode']
elif entity['unit_of_measurement'] == '%': entity['min'] = self.config.NASA_REPO[name]['hass_opts']['platform']['min']
entity['state_class'] = "measurement" entity['max'] = self.config.NASA_REPO[name]['hass_opts']['platform']['max']
elif entity['unit_of_measurement'] == 'kW': if 'step' in self.config.NASA_REPO[name]['hass_opts']['platform']:
entity['device_class'] = "power" entity['step'] = self.config.NASA_REPO[name]['hass_opts']['platform']['step']
elif entity['unit_of_measurement'] == 'rpm':
entity['state_class'] = "measurement" entity['command_topic'] = f"{self.topicPrefix.replace('/', '')}/entity/{name}/set"
elif entity['unit_of_measurement'] == 'bar': entity['optimistic'] = False
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: else:
entity['payload_on'] = "ON" sensor_type = self.config.NASA_REPO[name]['hass_opts']['default_platform']
entity['payload_off'] = "OFF"
if 'state_class' in self.config.NASA_REPO[name]: if 'unit' in self.config.NASA_REPO[name]['hass_opts']:
entity['state_class'] = self.config.NASA_REPO[name]['state_class'] entity['unit_of_measurement'] = self.config.NASA_REPO[name]['hass_opts']['unit']
if 'device_class' in self.config.NASA_REPO[name]: entity['platform'] = sensor_type
entity['device_class'] = self.config.NASA_REPO[name]['device_class'] entity['state_topic'] = f"{self.config.MQTT['homeAssistantAutoDiscoverTopic']}/{sensor_type}/{self.DEVICE_ID}_{namenorm.lower()}/state"
if 'payload_off' in self.config.NASA_REPO[name]['hass_opts']['platform']:
entity['payload_off'] = "OFF"
if 'payload_on' in self.config.NASA_REPO[name]['hass_opts']['platform']:
entity['payload_on'] = "ON"
if 'state_class' in self.config.NASA_REPO[name]['hass_opts']:
entity['state_class'] = self.config.NASA_REPO[name]['hass_opts']['state_class']
if 'device_class' in self.config.NASA_REPO[name]['hass_opts']:
entity['device_class'] = self.config.NASA_REPO[name]['hass_opts']['device_class']
device = { device = {
"device": self._get_device(), "device": self._get_device(),
@@ -407,18 +261,6 @@ class MQTTClient:
} }
def _normalize_name(self, name): def _normalize_name(self, name):
"""
Normalize the given name based on the specified naming convention.
If `useCamelCaseTopicNames` is True, the function will:
- Remove any of the following prefixes from the name: 'ENUM_', 'LVAR_', 'NASA_', 'VAR_'.
- Convert the name to CamelCase format.
If `useCamelCaseTopicNames` is False, the function will return the name as is.
Args:
name (str): The name to be normalized.
Returns:
str: The normalized name.
"""
if self.useCamelCaseTopicNames: if self.useCamelCaseTopicNames:
prefix_to_remove = ['ENUM_', 'LVAR_', 'NASA_', 'VAR_'] prefix_to_remove = ['ENUM_', 'LVAR_', 'NASA_', 'VAR_']
# remove unnecessary prefixes of name # remove unnecessary prefixes of name
@@ -436,19 +278,3 @@ class MQTTClient:
tmpname = name tmpname = name
return tmpname 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

View File

@@ -14,70 +14,27 @@ from NASAPacket import NASAPacket
class MessageProcessor: class MessageProcessor:
""" """
The MessageProcessor class is responsible for handling and processing incoming messages for the EHS-Sentinel system. The MessageProcessor class is responsible for handling and processing incoming messages for the EHS-Sentinel system.
It follows the singleton pattern to ensure only one instance is created. The class provides methods to process The class provides methods to process messages, extract submessages, search for message definitions in a configuration repository,
messages, extract submessages, search for message definitions in a configuration repository, and determine the and determine the value of message payloads based on predefined rules. It also includes logging for debugging and tracing the
value of message payloads based on predefined rules. It also includes logging for debugging and tracing the
message processing steps. message processing steps.
""" """
_instance = None
tmpdict = {}
def __new__(cls, *args, **kwargs):
"""
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.
Args:
cls (type): The class being instantiated.
*args: Variable length argument list.
**kwargs: Arbitrary keyword arguments.
Returns:
MessageProcessor: The single instance of the MessageProcessor class.
"""
if not cls._instance:
cls._instance = super(MessageProcessor, cls).__new__(cls, *args, **kwargs)
cls._instance._initialized = False
return cls._instance
def __init__(self): def __init__(self):
"""
Initializes the MessageProcessor instance.
This constructor checks if the instance has already been initialized to prevent reinitialization.
If not initialized, it sets the _initialized flag to True, logs the initialization process,
and initializes the configuration and argument handling components.
Attributes:
_initialized (bool): A flag indicating whether the instance has been initialized.
config (EHSConfig): An instance of the EHSConfig class for configuration management.
args (EHSArguments): An instance of the EHSArguments class for argument handling.
"""
if self._initialized:
return
self._initialized = True self._initialized = True
self.config = EHSConfig() self.config = EHSConfig()
self.args = EHSArguments() self.args = EHSArguments()
self.mqtt = MQTTClient() self.mqtt = MQTTClient()
self.NASA_VAL_STORE = {}
def process_message(self, packet: NASAPacket): async def process_message(self, packet: NASAPacket):
"""
Processes an incoming packet .
Args:
message (list): A list of integers representing the message bytes.
Raises:
MessageWarningException: If the message is invalid due to missing end byte, incorrect length, or other processing errors.
Logs:
Various debug and info logs to trace the processing steps, including packet size, raw and hex message content, source address, capacity, and extracted message details.
"""
for msg in packet.packet_messages: for msg in packet.packet_messages:
hexmsg = hex(msg.packet_message) hexmsg = f"0x{msg.packet_message:04x}" #hex(msg.packet_message)
msgname = self.search_nasa_table(hexmsg) msgname = self.search_nasa_table(hexmsg)
if msgname is not None: if msgname is not None:
try: try:
msgvalue = self.determine_value(msg.packet_payload, msgname) msgvalue = self.determine_value(msg.packet_payload, msgname, msg.packet_message_type)
except Exception as e: except Exception as e:
raise MessageWarningException(argument=f"{msg.packet_payload}/{[hex(x) for x in msg.packet_payload]}", message=f"Value of {hexmsg} couldn't be determinate, skip Message {e}") raise MessageWarningException(argument=f"{msg.packet_payload}/{[hex(x) for x in msg.packet_payload]}", message=f"Value of {hexmsg} couldn't be determinate, skip Message {e}")
self.protocolMessage(msg, msgname, msgvalue) await self.protocolMessage(msg, msgname, msgvalue)
else: else:
packedval = int.from_bytes(msg.packet_payload, byteorder='big', signed=True) packedval = int.from_bytes(msg.packet_payload, byteorder='big', signed=True)
if self.config.LOGGING['messageNotFound']: if self.config.LOGGING['messageNotFound']:
@@ -85,25 +42,10 @@ class MessageProcessor:
else: else:
logger.debug(f"Message not Found in NASA repository: {hexmsg:<6} Type: {msg.packet_message_type} Payload: {msg.packet_payload} = {packedval}") logger.debug(f"Message not Found in NASA repository: {hexmsg:<6} Type: {msg.packet_message_type} Payload: {msg.packet_payload} = {packedval}")
def protocolMessage(self, msg: NASAMessage, msgname, msgvalue): async def protocolMessage(self, msg: NASAMessage, msgname, msgvalue):
"""
Processes a protocol message by logging, writing to a protocol file, publishing via MQTT,
and updating internal value store. Additionally, it calculates and processes specific
derived values based on certain message names.
Args:
msg (NASAMessage): The NASA message object containing packet information.
msgname (str): The name of the message.
msgvalue (Any): The value of the message.
Side Effects:
- Logs the message details.
- Appends the message details to a protocol file if configured.
- Publishes the message via MQTT.
- Updates the internal NASA value store with the message value.
- Calculates and processes derived values for specific message names.
"""
if self.config.LOGGING['proccessedMessage']: if self.config.LOGGING['proccessedMessage']:
logger.info(f"Message number: {hex(msg.packet_message):<6} {msgname:<50} Type: {msg.packet_message_type} Payload: {msgvalue}") logger.info(f"Message number: {hex(msg.packet_message):<6} {msgname:<50} Type: {msg.packet_message_type} Payload: {msgvalue} ({msg.packet_payload})")
else: else:
logger.debug(f"Message number: {hex(msg.packet_message):<6} {msgname:<50} Type: {msg.packet_message_type} Payload: {msgvalue}") logger.debug(f"Message number: {hex(msg.packet_message):<6} {msgname:<50} Type: {msg.packet_message_type} Payload: {msgvalue}")
@@ -111,87 +53,94 @@ class MessageProcessor:
with open(self.config.GENERAL['protocolFile'], "a") as protWriter: with open(self.config.GENERAL['protocolFile'], "a") as protWriter:
protWriter.write(f"{hex(msg.packet_message):<6},{msg.packet_message_type},{msgname:<50},{msgvalue}\n") protWriter.write(f"{hex(msg.packet_message):<6},{msg.packet_message_type},{msgname:<50},{msgvalue}\n")
self.mqtt.publish_message(msgname, msgvalue) await self.mqtt.publish_message(msgname, msgvalue)
self.NASA_VAL_STORE[msgname] = msgvalue self.config.NASA_VAL_STORE[msgname] = msgvalue
if msgname in ['NASA_OUTDOOR_TW2_TEMP', 'NASA_OUTDOOR_TW1_TEMP', 'VAR_IN_FLOW_SENSOR_CALC']: if msgname in ['NASA_OUTDOOR_TW2_TEMP', 'NASA_OUTDOOR_TW1_TEMP', 'VAR_IN_FLOW_SENSOR_CALC']:
if all(k in self.NASA_VAL_STORE for k in ['NASA_OUTDOOR_TW2_TEMP', 'NASA_OUTDOOR_TW1_TEMP', 'VAR_IN_FLOW_SENSOR_CALC']): if all(k in self.config.NASA_VAL_STORE for k in ['NASA_OUTDOOR_TW2_TEMP', 'NASA_OUTDOOR_TW1_TEMP', 'VAR_IN_FLOW_SENSOR_CALC']):
value = round( value = round(
abs( abs(
(self.NASA_VAL_STORE['NASA_OUTDOOR_TW2_TEMP'] - self.NASA_VAL_STORE['NASA_OUTDOOR_TW1_TEMP']) * (self.config.NASA_VAL_STORE['NASA_OUTDOOR_TW2_TEMP'] - self.config.NASA_VAL_STORE['NASA_OUTDOOR_TW1_TEMP']) *
(self.NASA_VAL_STORE['VAR_IN_FLOW_SENSOR_CALC']/60) (self.config.NASA_VAL_STORE['VAR_IN_FLOW_SENSOR_CALC']/60)
* 4190 * 4190
) , 4 ) , 4
) )
if (value < 15000 and value > 0): # only if heater output between 0 und 15000 W if (value < 15000 and value > 0): # only if heater output between 0 und 15000 W
self.protocolMessage(NASAMessage(packet_message=0x9999, packet_message_type=1), await self.protocolMessage(NASAMessage(packet_message=0x9999, packet_message_type=1, packet_payload=[0]),
"NASA_EHSSENTINEL_HEAT_OUTPUT", "NASA_EHSSENTINEL_HEAT_OUTPUT",
value value
) )
if msgname in ('NASA_EHSSENTINEL_HEAT_OUTPUT', 'NASA_OUTDOOR_CONTROL_WATTMETER_ALL_UNIT'): if msgname in ('NASA_EHSSENTINEL_HEAT_OUTPUT', 'NASA_OUTDOOR_CONTROL_WATTMETER_ALL_UNIT'):
if all(k in self.NASA_VAL_STORE for k in ['NASA_EHSSENTINEL_HEAT_OUTPUT', 'NASA_OUTDOOR_CONTROL_WATTMETER_ALL_UNIT']): if all(k in self.config.NASA_VAL_STORE for k in ['NASA_EHSSENTINEL_HEAT_OUTPUT', 'NASA_OUTDOOR_CONTROL_WATTMETER_ALL_UNIT']):
if (self.NASA_VAL_STORE['NASA_OUTDOOR_CONTROL_WATTMETER_ALL_UNIT'] > 0): if (self.config.NASA_VAL_STORE['NASA_OUTDOOR_CONTROL_WATTMETER_ALL_UNIT'] > 0):
value = round((self.NASA_VAL_STORE['NASA_EHSSENTINEL_HEAT_OUTPUT'] / self.NASA_VAL_STORE['NASA_OUTDOOR_CONTROL_WATTMETER_ALL_UNIT']/1000.), 3) value = round((self.config.NASA_VAL_STORE['NASA_EHSSENTINEL_HEAT_OUTPUT'] / self.config.NASA_VAL_STORE['NASA_OUTDOOR_CONTROL_WATTMETER_ALL_UNIT']/1000.), 3)
if (value < 20 and value > 0): if (value < 20 and value > 0):
self.protocolMessage(NASAMessage(packet_message=0x9998, packet_message_type=1), await self.protocolMessage(NASAMessage(packet_message=0x9998, packet_message_type=1, packet_payload=[0]),
"NASA_EHSSENTINEL_COP", "NASA_EHSSENTINEL_COP",
value value
) )
if msgname in ('NASA_OUTDOOR_CONTROL_WATTMETER_ALL_UNIT_ACCUM', 'LVAR_IN_TOTAL_GENERATED_POWER'): if msgname in ('NASA_OUTDOOR_CONTROL_WATTMETER_ALL_UNIT_ACCUM', 'LVAR_IN_TOTAL_GENERATED_POWER'):
if all(k in self.NASA_VAL_STORE for k in ['NASA_OUTDOOR_CONTROL_WATTMETER_ALL_UNIT_ACCUM', 'LVAR_IN_TOTAL_GENERATED_POWER']): if all(k in self.config.NASA_VAL_STORE for k in ['NASA_OUTDOOR_CONTROL_WATTMETER_ALL_UNIT_ACCUM', 'LVAR_IN_TOTAL_GENERATED_POWER']):
if (self.NASA_VAL_STORE['NASA_OUTDOOR_CONTROL_WATTMETER_ALL_UNIT_ACCUM'] > 0): if (self.config.NASA_VAL_STORE['NASA_OUTDOOR_CONTROL_WATTMETER_ALL_UNIT_ACCUM'] > 0):
value = round(self.NASA_VAL_STORE['LVAR_IN_TOTAL_GENERATED_POWER'] / self.NASA_VAL_STORE['NASA_OUTDOOR_CONTROL_WATTMETER_ALL_UNIT_ACCUM'], 3) value = round(self.config.NASA_VAL_STORE['LVAR_IN_TOTAL_GENERATED_POWER'] / self.config.NASA_VAL_STORE['NASA_OUTDOOR_CONTROL_WATTMETER_ALL_UNIT_ACCUM'], 3)
if (value < 20 and value > 0): if (value < 20 and value > 0):
self.protocolMessage(NASAMessage(packet_message=0x9997, packet_message_type=1), await self.protocolMessage(NASAMessage(packet_message=0x9997, packet_message_type=1, packet_payload=[0]),
"NASA_EHSSENTINEL_TOTAL_COP", "NASA_EHSSENTINEL_TOTAL_COP",
value value
) )
def search_nasa_table(self, address): def search_nasa_table(self, address):
"""
Searches for a specific address in the NASA_REPO configuration and returns the corresponding key.
Args:
address (str): The address to search for in the NASA_REPO.
Returns:
str: The key associated with the given address if found, otherwise None.
"""
for key, value in self.config.NASA_REPO.items(): for key, value in self.config.NASA_REPO.items():
if value['address'].lower() == address: if value['address'].lower() == address:
return key return key
def determine_value(self, rawvalue, msgname): def is_valid_rawvalue(self, rawvalue: bytes) -> bool:
""" return all(0x20 <= b <= 0x7E or b in (0x00, 0xFF) for b in rawvalue)
Determines the processed value from a raw byte input based on the message name configuration.
Args:
rawvalue (bytes): The raw byte value to be processed.
msgname (str): The name of the message which determines the processing rules.
Returns:
float or str: The processed value, which could be a numerical value or an enumerated string.
Raises:
Warning: Logs a warning if the arithmetic function cannot be applied and uses the raw value instead.
"""
arithmetic = self.config.NASA_REPO[msgname]['arithmetic'].replace("value", 'packed_value')
packed_value = int.from_bytes(rawvalue, byteorder='big', signed=True) def determine_value(self, rawvalue, msgname, packet_message_type):
if packet_message_type == 3:
value = ""
if len(arithmetic) > 0: if self.is_valid_rawvalue(rawvalue[1:-1]):
try: for byte in rawvalue[1:-1]:
value = eval(arithmetic) if byte != 0x00 and byte != 0xFF:
except Exception as e: char = chr(byte) if 32 <= byte <= 126 else f"{byte}"
logger.warning(f"Arithmetic Function couldn't been applied for Message {msgname}, using raw value: arithmetic = {arithmetic} {e}") value += char
value = packed_value else:
else: value += " "
value = packed_value value = value.strip()
if self.config.NASA_REPO[msgname]['type'] == 'ENUM':
if 'enum' in self.config.NASA_REPO[msgname]:
value = self.config.NASA_REPO[msgname]['enum'][int.from_bytes(rawvalue, byteorder='big')].upper()
else: else:
value = f"Unknown enum value: {value}" value = "".join([f"{int(x)}" for x in rawvalue])
#logger.info(f"{msgname} Structure: {rawvalue} type of {value}")
else: else:
if 'arithmetic' in self.config.NASA_REPO[msgname]:
arithmetic = self.config.NASA_REPO[msgname]['arithmetic'].replace("value", 'packed_value')
else:
arithmetic = ''
packed_value = int.from_bytes(rawvalue, byteorder='big', signed=True)
if len(arithmetic) > 0:
try:
value = eval(arithmetic)
except Exception as e:
logger.warning(f"Arithmetic Function couldn't been applied for Message {msgname}, using raw value: arithmetic = {arithmetic} {e} {packed_value} {rawvalue}")
value = packed_value
else:
value = packed_value
value = round(value, 3) value = round(value, 3)
if 'type' in self.config.NASA_REPO[msgname]:
if self.config.NASA_REPO[msgname]['type'] == 'ENUM':
if 'enum' in self.config.NASA_REPO[msgname]:
value = self.config.NASA_REPO[msgname]['enum'][int.from_bytes(rawvalue, byteorder='big')]
else:
value = f"Unknown enum value: {value}"
return value return value

152
MessageProducer.py Normal file
View File

@@ -0,0 +1,152 @@
from CustomLogger import logger
from EHSArguments import EHSArguments
from EHSConfig import EHSConfig
from EHSExceptions import MessageWarningException
import asyncio
from NASAMessage import NASAMessage
from NASAPacket import NASAPacket, AddressClassEnum, PacketType, DataType
class MessageProducer:
"""
The MessageProducer class is responsible for sending messages to the EHS-Sentinel system.
It follows the singleton pattern to ensure only one instance is created. The class provides methods to request and write
messages and transforme the value of message payloads based on predefined rules. It also includes logging for debugging and tracing the
message producing steps.
"""
_instance = None
_CHUNKSIZE = 10 # message requests list will be split into this chunks, experience have shown that more then 10 are too much for an packet
writer = None
def __new__(cls, *args, **kwargs):
if not cls._instance:
cls._instance = super(MessageProducer, cls).__new__(cls)
cls._instance._initialized = False
return cls._instance
def __init__(self, writer: asyncio.StreamWriter):
if self._initialized:
return
self._initialized = True
self.writer = writer
self.config = EHSConfig()
async def read_request(self, list_of_messages: list):
chunks = [list_of_messages[i:i + self._CHUNKSIZE] for i in range(0, len(list_of_messages), self._CHUNKSIZE)]
for chunk in chunks:
await asyncio.sleep(0.5)
nasa_packet = self._build_default_read_packet()
nasa_packet.set_packet_messages([self._build_message(x) for x in chunk])
await self._write_packet_to_serial(nasa_packet)
if self.config.LOGGING['pollerMessage']:
logger.info(f"Polling following NASAPacket: {nasa_packet}")
else:
logger.debug(f"Sent data NASAPacket: {nasa_packet}")
async def write_request(self, message: str, value: str | int, read_request_after=False):
nasa_packet = self._build_default_request_packet()
nasa_packet.set_packet_messages([self._build_message(message.strip(), self._decode_value(message.strip(), value.strip()))])
nasa_packet.to_raw()
if self.config.LOGGING['controlMessage']:
logger.info(f"Write request for {message} with value: {value}")
logger.info(f"Sending NASA packet: {nasa_packet}")
else:
logger.debug(f"Write request for {message} with value: {value}")
logger.debug(f"Sending NASA packet: {nasa_packet}")
await self._write_packet_to_serial(nasa_packet)
await asyncio.sleep(1)
await self.read_request([message])
def _search_nasa_enumkey_for_value(self, message, value):
if 'type' in self.config.NASA_REPO[message] and self.config.NASA_REPO[message]['type'] == 'ENUM':
for key, val in self.config.NASA_REPO[message]['enum'].items():
if val == value:
return key
return None
def is_number(self, s):
return s.replace('+','',1).replace('-','',1).replace('.','',1).isdigit()
def _decode_value(self, message, value) -> int:
enumval = self._search_nasa_enumkey_for_value(message, value)
if enumval is None:
if self.is_number(value):
try:
value = int(value)
except ValueError as e:
value = float(value)
if 'reverse-arithmetic' in self.config.NASA_REPO[message]:
arithmetic = self.config.NASA_REPO[message]['reverse-arithmetic']
else:
arithmetic = ''
if len(arithmetic) > 0:
try:
return int(eval(arithmetic))
except Exception as e:
logger.warning(f"Arithmetic Function couldn't been applied for Message {message}, using raw value: reverse-arithmetic = {arithmetic} {e} {value}")
return value
else:
value = int(enumval)
return value
def _build_message(self, message, value=None) -> NASAMessage:
tmpmsg = NASAMessage()
tmpmsg.set_packet_message(self._extract_address(message))
if value is None:
value = 0
if tmpmsg.packet_message_type == 0:
value_raw = value.to_bytes(1, byteorder='big', signed=True)
elif tmpmsg.packet_message_type == 1:
value_raw = value.to_bytes(2, byteorder='big', signed=True)
elif tmpmsg.packet_message_type == 2:
value_raw = value.to_bytes(4, byteorder='big', signed=True)
else:
raise MessageWarningException(argument=tmpmsg.packet_message_type, message=f"Unknown Type for {message} type:")
tmpmsg.set_packet_payload_raw(value_raw)
return tmpmsg
def _extract_address(self, messagename) -> int:
return int(self.config.NASA_REPO[messagename]['address'], 16)
def _build_default_read_packet(self) -> NASAPacket:
nasa_msg = NASAPacket()
nasa_msg.set_packet_source_address_class(AddressClassEnum.JIGTester)
nasa_msg.set_packet_source_channel(255)
nasa_msg.set_packet_source_address(0)
nasa_msg.set_packet_dest_address_class(AddressClassEnum.BroadcastSetLayer)
nasa_msg.set_packet_dest_channel(0)
nasa_msg.set_packet_dest_address(32)
nasa_msg.set_packet_information(True)
nasa_msg.set_packet_version(2)
nasa_msg.set_packet_retry_count(0)
nasa_msg.set_packet_type(PacketType.Normal)
nasa_msg.set_packet_data_type(DataType.Read)
nasa_msg.set_packet_number(166)
return nasa_msg
def _build_default_request_packet(self) -> NASAPacket:
nasa_msg = NASAPacket()
nasa_msg.set_packet_source_address_class(AddressClassEnum.JIGTester)
nasa_msg.set_packet_source_channel(0)
nasa_msg.set_packet_source_address(255)
nasa_msg.set_packet_dest_address_class(AddressClassEnum.Indoor)
nasa_msg.set_packet_dest_channel(0)
nasa_msg.set_packet_dest_address(0)
nasa_msg.set_packet_information(True)
nasa_msg.set_packet_version(2)
nasa_msg.set_packet_retry_count(0)
nasa_msg.set_packet_type(PacketType.Normal)
nasa_msg.set_packet_data_type(DataType.Request)
nasa_msg.set_packet_number(166)
return nasa_msg
async def _write_packet_to_serial(self, packet: NASAPacket):
final_packet = packet.to_raw()
self.writer.write(final_packet)
await self.writer.drain()

View File

@@ -2,48 +2,8 @@
class NASAMessage: class NASAMessage:
""" """
A class to represent a NASA message. A class to represent a NASA message.
Attributes
----------
packet_message : int
The message packet identifier.
packet_message_type : int
The type of the message packet.
packet_payload : bytes
The payload of the message packet in bytes.
Methods
-------
__str__():
Returns a string representation of the NASAMessage instance.
__repr__():
Returns a string representation of the NASAMessage instance.
""" """
def __init__(self, packet_message=0x000, packet_message_type=0, packet_payload=[0]): def __init__(self, packet_message=0x000, packet_message_type=0, packet_payload=[0]):
"""
Constructs all the necessary attributes for the NASAMessage object.
Parameters
----------
packet_message : int, optional
The message packet identifier (default is 0x000).
packet_message_type : int, optional
The type of the message packet (default is 0).
packet_payload : list, optional
The payload of the message packet as a list of integers (default is [0]).
"""
"""
Returns a string representation of the NASAMessage instance.
Returns
-------
str
A string representation of the NASAMessage instance.
"""
"""
Returns a string representation of the NASAMessage instance.
Returns
-------
str
A string representation of the NASAMessage instance.
"""
self.packet_message: int = packet_message self.packet_message: int = packet_message
self.packet_message_type: int = packet_message_type self.packet_message_type: int = packet_message_type
self.packet_payload: bytes = bytes([int(hex(x), 16) for x in packet_payload]) self.packet_payload: bytes = bytes([int(hex(x), 16) for x in packet_payload])
@@ -51,6 +11,7 @@ class NASAMessage:
def set_packet_message(self, value: int): def set_packet_message(self, value: int):
self.packet_message = value self.packet_message = value
self.packet_message_type = (value & 1536) >> 9
def set_packet_message_type(self, value: int): def set_packet_message_type(self, value: int):
self.packet_message_type = value self.packet_message_type = value
@@ -58,6 +19,9 @@ class NASAMessage:
def set_packet_payload(self, value: list): def set_packet_payload(self, value: list):
self.packet_payload = bytes([int(hex(x), 16) for x in value]) self.packet_payload = bytes([int(hex(x), 16) for x in value])
def set_packet_payload_raw(self, value: bytes):
self.packet_payload = value
def to_raw(self) -> bytearray: def to_raw(self) -> bytearray:
message_number_reconstructed = (self.packet_message_type << 9) | (self.packet_message & 0x1FF) message_number_reconstructed = (self.packet_message_type << 9) | (self.packet_message & 0x1FF)
@@ -88,6 +52,12 @@ class NASAMessage:
(msgpayload >> 8) & 0xFF, (msgpayload >> 8) & 0xFF,
msgpayload & 0xFF msgpayload & 0xFF
] ]
elif self.packet_message_type == 3:
return [
msg_rest_0,
msg_rest_1,
*[(msgpayload >> (8 * i)) & 0xFF for i in reversed(range(len(self.packet_payload)))]
]
def __str__(self): def __str__(self):
return ( return (

View File

@@ -3,38 +3,11 @@ from NASAMessage import NASAMessage
from EHSExceptions import SkipInvalidPacketException from EHSExceptions import SkipInvalidPacketException
import binascii import binascii
import struct import struct
from CustomLogger import logger
class AddressClassEnum(Enum): class AddressClassEnum(Enum):
""" """
Enum class representing various address classes for NASA packets. Enum class representing various address classes for NASA packets.
Attributes:
Outdoor (int): Address class for outdoor units (0x10).
HTU (int): Address class for HTU units (0x11).
Indoor (int): Address class for indoor units (0x20).
ERV (int): Address class for ERV units (0x30).
Diffuser (int): Address class for diffuser units (0x35).
MCU (int): Address class for MCU units (0x38).
RMC (int): Address class for RMC units (0x40).
WiredRemote (int): Address class for wired remote units (0x50).
PIM (int): Address class for PIM units (0x58).
SIM (int): Address class for SIM units (0x59).
Peak (int): Address class for peak units (0x5A).
PowerDivider (int): Address class for power divider units (0x5B).
OnOffController (int): Address class for on/off controller units (0x60).
WiFiKit (int): Address class for WiFi kit units (0x62).
CentralController (int): Address class for central controller units (0x65).
DMS (int): Address class for DMS units (0x6A).
JIGTester (int): Address class for JIG tester units (0x80).
BroadcastSelfLayer (int): Address class for broadcast self layer (0xB0).
BroadcastControlLayer (int): Address class for broadcast control layer (0xB1).
BroadcastSetLayer (int): Address class for broadcast set layer (0xB2).
BroadcastCS (int): Address class for broadcast CS (0xB3).
BroadcastControlAndSetLayer (int): Address class for broadcast control and set layer (0xB3).
BroadcastModuleLayer (int): Address class for broadcast module layer (0xB4).
BroadcastCSM (int): Address class for broadcast CSM (0xB7).
BroadcastLocalLayer (int): Address class for broadcast local layer (0xB8).
BroadcastCSML (int): Address class for broadcast CSML (0xBF).
Undefined (int): Address class for undefined units (0xFF).
""" """
Outdoor = 0x10 Outdoor = 0x10
@@ -68,12 +41,6 @@ class AddressClassEnum(Enum):
class PacketType(Enum): class PacketType(Enum):
""" """
Enum class representing different types of packets in the EHS-Sentinel system. Enum class representing different types of packets in the EHS-Sentinel system.
Attributes:
StandBy (int): Represents a standby packet type with a value of 0.
Normal (int): Represents a normal packet type with a value of 1.
Gathering (int): Represents a gathering packet type with a value of 2.
Install (int): Represents an install packet type with a value of 3.
Download (int): Represents a download packet type with a value of 4.
""" """
StandBy = 0 StandBy = 0
@@ -85,15 +52,6 @@ class PacketType(Enum):
class DataType(Enum): class DataType(Enum):
""" """
Enum representing different types of data operations. Enum representing different types of data operations.
Attributes:
Undefined (int): Represents an undefined data type (0).
Read (int): Represents a read operation (1).
Write (int): Represents a write operation (2).
Request (int): Represents a request operation (3).
Notification (int): Represents a notification operation (4).
Response (int): Represents a response operation (5).
Ack (int): Represents an acknowledgment (6).
Nack (int): Represents a negative acknowledgment (7).
""" """
Undefined = 0 Undefined = 0
@@ -108,56 +66,6 @@ class DataType(Enum):
class NASAPacket: class NASAPacket:
""" """
A class to represent a NASA Packet. A class to represent a NASA Packet.
Attributes
----------
_packet_raw : bytearray
Raw packet data.
packet_start : int
Start byte of the packet.
packet_size : int
Size of the packet.
packet_source_address_class : AddressClassEnum
Source address class of the packet.
packet_source_channel : int
Source channel of the packet.
packet_source_address : int
Source address of the packet.
packet_dest_address_class : AddressClassEnum
Destination address class of the packet.
packet_dest_channel : int
Destination channel of the packet.
packet_dest_address : int
Destination address of the packet.
packet_information : int
Information field of the packet.
packet_version : int
Version of the packet.
packet_retry_count : int
Retry count of the packet.
packet_type : PacketType
Type of the packet.
packet_data_type : DataType
Data type of the packet.
packet_number : int
Number of the packet.
packet_capacity : int
Capacity of the packet.
packet_messages : list[NASAMessage]
List of messages in the packet.
packet_crc16 : int
CRC16 checksum of the packet.
packet_end : int
End byte of the packet.
Methods
-------
parse(packet: bytearray):
Parses the given packet data.
_extract_messages(depth: int, capacity: int, msg_rest: bytearray, return_list: list):
Recursively extracts messages from the packet.
__str__():
Returns a string representation of the NASAPacket.
__repr__():
Returns a string representation of the NASAPacket.
""" """
def __init__(self): def __init__(self):
@@ -182,33 +90,6 @@ class NASAPacket:
self.packet_end: int = None self.packet_end: int = None
def parse(self, packet: bytearray): def parse(self, packet: bytearray):
"""
Parses a given bytearray packet and extracts various fields into the object's attributes.
Args:
packet (bytearray): The packet to be parsed.
Raises:
ValueError: If the packet length is less than 14 bytes.
Attributes:
packet_start (int): The start byte of the packet.
packet_size (int): The size of the packet.
packet_source_address_class (AddressClassEnum): The source address class of the packet.
packet_source_channel (int): The source channel of the packet.
packet_source_address (int): The source address of the packet.
packet_dest_address_class (AddressClassEnum): The destination address class of the packet.
packet_dest_channel (int): The destination channel of the packet.
packet_dest_address (int): The destination address of the packet.
packet_information (bool): Information flag of the packet.
packet_version (int): Version of the packet.
packet_retry_count (int): Retry count of the packet.
packet_type (PacketType): Type of the packet.
packet_data_type (DataType): Data type of the packet.
packet_number (int): Number of the packet.
packet_capacity (int): Capacity of the packet.
packet_crc16 (int): CRC16 checksum of the packet.
packet_end (int): The end byte of the packet.
packet_messages (list): Extracted messages from the packet.
"""
self._packet_raw = packet self._packet_raw = packet
if len(packet) < 14: if len(packet) < 14:
raise ValueError("Data too short to be a valid NASAPacket") raise ValueError("Data too short to be a valid NASAPacket")
@@ -217,6 +98,12 @@ class NASAPacket:
self.packet_start = packet[0] self.packet_start = packet[0]
self.packet_size = ((packet[1] << 8) | packet[2]) self.packet_size = ((packet[1] << 8) | packet[2])
if self.packet_size+2 != len(packet):
logger.info(f"length not correct {self.packet_size+2} -> {len(packet)}")
logger.info(f"{packet.hex()}")
logger.info(f"{hex(packet[self.packet_size+1])}")
try: try:
self.packet_source_address_class = AddressClassEnum(packet[3]) self.packet_source_address_class = AddressClassEnum(packet[3])
except ValueError as e: except ValueError as e:
@@ -244,20 +131,6 @@ class NASAPacket:
raise SkipInvalidPacketException(f"Checksum for package could not be validated. Calculated: {crc_checkusm} in packet: {self.packet_crc16}: packet:{self}") raise SkipInvalidPacketException(f"Checksum for package could not be validated. Calculated: {crc_checkusm} in packet: {self.packet_crc16}: packet:{self}")
def _extract_messages(self, depth: int, capacity: int, msg_rest: bytearray, return_list: list): def _extract_messages(self, depth: int, capacity: int, msg_rest: bytearray, return_list: list):
"""
Recursively extracts messages from a bytearray and appends them to a list.
Args:
depth (int): The current depth of recursion.
capacity (int): The maximum allowed depth of recursion.
msg_rest (bytearray): The remaining bytes to be processed.
return_list (list): The list to which extracted messages are appended.
Returns:
list: The list of extracted messages.
Raises:
ValueError: If the message type is unknown, the capacity is invalid for a structure type message,
or the payload size exceeds 255 bytes.
"""
if depth > capacity or len(msg_rest) <= 2: if depth > capacity or len(msg_rest) <= 2:
return return_list return return_list
@@ -358,11 +231,6 @@ class NASAPacket:
self.packet_messages = value self.packet_messages = value
def to_raw(self) -> bytearray: def to_raw(self) -> bytearray:
"""
Converts the NASAPacket object back to its raw byte representation.
Returns:
bytearray: The raw byte representation of the packet.
"""
self.packet_start = 50 self.packet_start = 50
self.packet_end = 52 self.packet_end = 52

View File

@@ -13,6 +13,15 @@ You need an MQTT Broker.
For Homeassistant you need the MQTT Plugin there with enabled Auto Discovery with Discovery Topic Prefix and Birth-Messages on Discovery Topic Prefix with subtopic "status" with text "online". For Homeassistant you need the MQTT Plugin there with enabled Auto Discovery with Discovery Topic Prefix and Birth-Messages on Discovery Topic Prefix with subtopic "status" with text "online".
EHS-Sentinel subscribes <hass_discovery_prefix>/status Topic and if it receive an "online", then it cleans his intern known-devices topic and send the Auto Discovery Config again for any Measurment for Home Assistant. EHS-Sentinel subscribes <hass_discovery_prefix>/status Topic and if it receive an "online", then it cleans his intern known-devices topic and send the Auto Discovery Config again for any Measurment for Home Assistant.
# Upgrade instructions
1. Stop EHS-Sentinel
2. *Optional* If you are using HASS: Delete the MQTT Device
3. git pull the new release or download and extract the release zip file
4. Look into Release Notes if there are some new configurations and check if you have to ajust your configfile
5. Start EHS-Sentinel (I recommend to use `--clean-known-devices` on the start so EHS-Sentinel will send Configuration messages for HASS Auto Discovery after every startup.)
6. *Optional* If you are using HASS: and not use the `--clean-known-devices` Parm on Startup, send a birthmessage manualy or restart the MQTT Adapter in HASS.
# Installation # Installation
## Simple ## Simple
@@ -23,7 +32,9 @@ EHS-Sentinel subscribes <hass_discovery_prefix>/status Topic and if it receive a
`pip install -r requirements.txt` `pip install -r requirements.txt`
3. Copy the `data/config.yml` and provide your Configuration 3. Copy the `data/config.yml` and provide your Configuration
4. Start the Application: 4. Start the Application:
`python3 startEHSSentinel.py --configfile config.yml` `python3 startEHSSentinel.py --configfile config.yml --clean-known-devices`
I recommend to use `--clean-known-devices` on the start so EHS-Sentinel will send Configuration messages for HASS Autodiscovery after every startup.
## Systemd Service ## Systemd Service
@@ -36,7 +47,9 @@ EHS-Sentinel subscribes <hass_discovery_prefix>/status Topic and if it receive a
`ExecStart = python3 <Path of the script you want to run>` <- provide here to path to your folder where startEHSSentinel.py is `ExecStart = python3 <Path of the script you want to run>` <- provide here to path to your folder where startEHSSentinel.py is
sample: `ExecStart = python3 /root/EHS-Sentinel/startEHSSentinel.py --configfile /root/EHS-Sentinel/config.yml` sample: `ExecStart = python3 /root/EHS-Sentinel/startEHSSentinel.py --configfile /root/EHS-Sentinel/config.yml --clean-known-devices`
I recommend to use `--clean-known-devices` on the start so EHS-Sentinel will send Configuration messages for HASS Autodiscovery after every startup.`
5. Change your `config.yml` to absolut paths: 5. Change your `config.yml` to absolut paths:
`nasaRepositoryFile: /root/EHS-Sentinel/data/NasaRepository.yml` `nasaRepositoryFile: /root/EHS-Sentinel/data/NasaRepository.yml`
@@ -92,7 +105,9 @@ Some Distributions like debian 12 dont allow to use system wide pip package inst
`ExecStart = <path to python3> <Path of the script you want to run>` <- provide here to path to your folder where startEHSSentinel.py is `ExecStart = <path to python3> <Path of the script you want to run>` <- provide here to path to your folder where startEHSSentinel.py is
sample: `ExecStart = /root/EHS-Sentinel/bin/python3 /root/EHS-Sentinel/startEHSSentinel.py --configfile /root/EHS-Sentinel/config.yml` sample: `ExecStart = /root/EHS-Sentinel/bin/python3 /root/EHS-Sentinel/startEHSSentinel.py --configfile /root/EHS-Sentinel/config.yml --clean-known-devices`
I recommend to use `--clean-known-devices` on the start so EHS-Sentinel will send Configuration messages for HASS Autodiscovery after every startup.
10. Change your `config.yml` to absolut paths: 10. Change your `config.yml` to absolut paths:
`nasaRepositoryFile: /root/EHS-Sentinel/data/NasaRepository.yml` `nasaRepositoryFile: /root/EHS-Sentinel/data/NasaRepository.yml`
@@ -121,16 +136,31 @@ Some Distributions like debian 12 dont allow to use system wide pip package inst
# Home Assistant Dashboard # Home Assistant Dashboard
There is a rudimentary dasdboard for Homeassistant, this can be found at: [ressources/dashboard.yaml](ressources/dashboard.yaml) There are two rudimentary dashboard templates for Homeassistant,
Read Only [ressources/dashboard_readonly_template.yaml](ressources/dashboard_readonly_template.yaml)
Control mode [ressources/dashboard_controlmode_template.yaml](ressources/dashboard_controlmode_template.yaml)
If you have good ideas and want to extend this feel free to create an issue or pull request, thanks! If you have good ideas and want to extend this feel free to create an issue or pull request, thanks!
## Read Only Mode
![alt text](ressources/images/dashboard1.png) ![alt text](ressources/images/dashboard1.png)
![alt text](ressources/images/dashboard2.png) ![alt text](ressources/images/dashboard2.png)
![alt text](ressources/images/dashboard3.png) ![alt text](ressources/images/dashboard3.png)
## Control Mode
![alt text](ressources/images/dashboard_cm1.png)
![alt text](ressources/images/dashboard_cm2.png)
![alt text](ressources/images/dashboard_cm3.png)
# Configuration # Configuration
## Command-Line Arguments ## Command-Line Arguments
@@ -182,6 +212,16 @@ The `config.yml` file contains configuration settings for the EHS-Sentinel proje
- Default: `data/NasaRepository.yml` - Default: `data/NasaRepository.yml`
- **protocolFile**: Path to the protocol file. (not set in Sample config.yml) - **protocolFile**: Path to the protocol file. (not set in Sample config.yml)
- Example: `prot.csv` - Example: `prot.csv`
- **allowControl**: Allows EHS-Sentinel to Control the Heatpump. HASS Entities are writable, EHS-Sentinel listents to set Topic and write published values to th Modbus Interface.
The Set Topic have following pattern: <topicPrefix>/entity/<NASA_NAME>/set sample: ehsSentinel/ENUM_IN_SG_READY_MODE_STATE/set
- Default: `False`
> [!CAUTION]
> This functionality requires that EHS-Sentinel actively communicates with
> the Samsung EHS, so EHS-Sentinel intervenes here in the Modbus data
> traffic between the components (it sends its own messages).
> The activation of this functionality is exclusively at your own risk.
> I assume no liability for any damage caused.
### Logging Settings ### Logging Settings
@@ -195,6 +235,10 @@ The `config.yml` file contains configuration settings for the EHS-Sentinel proje
- Default: `False` - Default: `False`
- **pollerMessage**: set to true, prints out detailed poller NASAPackets - **pollerMessage**: set to true, prints out detailed poller NASAPackets
- Default: `False` - Default: `False`
- **controlMessage**: set to true, prints out detailed control Message NASAPackets
- Default: `False`
- **invalidPacket**: set to true, prints out invalid packets like length not ok or end byte not 0x34...
- Default: `False`
### Serial Connection Settings ### Serial Connection Settings
cannot be defined with TCP parm... cannot be defined with TCP parm...
@@ -267,6 +311,7 @@ The data points are defined in the groups section, the group is then enabled in
```yaml ```yaml
general: general:
nasaRepositoryFile: data/NasaRepository.yml nasaRepositoryFile: data/NasaRepository.yml
allowControl: False
# protocolFile: prot.csv # protocolFile: prot.csv
logging: logging:
deviceAdded: True deviceAdded: True
@@ -333,6 +378,30 @@ if you want to see how many uniquie Messages have been collected in the Dumpfile
# Changelog # Changelog
### v1.0.0 - 2025-03-13
- EHS-Sentinel has been heavily modified to incorporate the control mechanism
- The read-in behavior of the modbus registers has been revised from chunks to single byte
- MessageProcessor now runs asynchronously
- MessageProducer added which takes over the writing communication with the WP
- Configuration of HASS entities has moved from hardcoded to NASA Repository
- NASA Repository has been fundamentally changed
- All FSV Values, NASA_POWER, VAR_IN_TEMP_WATER_LAW_TARGET_F, NASA_INDOOR_OPMODE are allowed for writing mode
- NASA_OUTDOOR_DEFROST_STEP DEFROST STEP 10 (b'0xa') added
- ENUM_IN_SG_READY_MODE_STATE ACTIVE (b'0x2') added
- New configuration point allowControl to allow control of the Samsung EHS heat pump (deactivated by default).
> [!CAUTION]
> This functionality requires that EHS-Sentinel actively communicates with
> the Samsung EHS, so EHS-Sentinel intervenes here in the Modbus data
> traffic between the components (it sends its own messages).
> The activation of this functionality is exclusively at your own risk.
> I assume no liability for any damage caused.
- new configuration points in logging
- controlMessage (default False) to print out the controlled mesagges
- invalidPacket (default False) prints out invalid messages (length not ok, x34 not at end...)
- Dashboard template has been split, [ressources/dashboard_readonly_template.yaml](ressources/dashboard_readonly_template.yaml) is for readonly mode and the [ressources/dashboard_controlmode_template.yaml](ressources/dashboard_controlmode_template.yaml) for control mode
### v0.3.0 - 2025-02-27 ### v0.3.0 - 2025-02-27
- Added poller functionality. EHS-Sentinel can now actively request values via Modbus - Added poller functionality. EHS-Sentinel can now actively request values via Modbus
- fetch_intervals and groups can be defined in the config file - fetch_intervals and groups can be defined in the config file

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,14 @@
general: general:
nasaRepositoryFile: data/NasaRepository.yml nasaRepositoryFile: data/NasaRepository.yml
allowControl: False
logging: logging:
deviceAdded: True deviceAdded: True
messageNotFound: False messageNotFound: False
packetNotFromIndoorOutdoor: False packetNotFromIndoorOutdoor: False
proccessedMessage: False proccessedMessage: False
pollerMessage: False pollerMessage: False
controlMessage: False
invalidPacket: False
#serial: #serial:
# device: /dev/ttyUSB0 # device: /dev/ttyUSB0
# baudrate: 9600 # baudrate: 9600

View File

@@ -1,4 +1,13 @@
import json import json
import os
import sys
import inspect
currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
parentdir = os.path.dirname(currentdir)
sys.path.insert(0, parentdir)
import NASAPacket import NASAPacket
import NASAMessage import NASAMessage
@@ -8,6 +17,7 @@ encode_raw = "[50, 0, 60, 16, 0, 0, 176, 0, 255, 192, 20, 196, 13, 2, 2, 255, 25
encode_raw = "[50, 0, 56, 98, 0, 144, 178, 0, 32, 192, 17, 3, 11, 64, 147, 0, 64, 148, 0, 66, 115, 0, 0, 66, 116, 0, 0, 66, 117, 0, 0, 66, 118, 0, 0, 66, 119, 0, 0, 66, 120, 0, 0, 66, 121, 0, 0, 66, 122, 0, 0, 66, 123, 0, 0, 221, 200, 52]" encode_raw = "[50, 0, 56, 98, 0, 144, 178, 0, 32, 192, 17, 3, 11, 64, 147, 0, 64, 148, 0, 66, 115, 0, 0, 66, 116, 0, 0, 66, 117, 0, 0, 66, 118, 0, 0, 66, 119, 0, 0, 66, 120, 0, 0, 66, 121, 0, 0, 66, 122, 0, 0, 66, 123, 0, 0, 221, 200, 52]"
encode_raw = "[50, 0, 56, 98, 0, 144, 178, 0, 32, 192, 17, 240, 11, 64, 147, 0, 64, 148, 0, 66, 115, 0, 0, 66, 116, 0, 0, 66, 117, 0, 0, 66, 118, 0, 0, 66, 119, 0, 0, 66, 120, 0, 0, 66, 121, 0, 0, 66, 122, 0, 0, 66, 123, 0, 0, 76, 33, 52]" encode_raw = "[50, 0, 56, 98, 0, 144, 178, 0, 32, 192, 17, 240, 11, 64, 147, 0, 64, 148, 0, 66, 115, 0, 0, 66, 116, 0, 0, 66, 117, 0, 0, 66, 118, 0, 0, 66, 119, 0, 0, 66, 120, 0, 0, 66, 121, 0, 0, 66, 122, 0, 0, 66, 123, 0, 0, 76, 33, 52]"
#encode_raw ="[50, 0, 48, 98, 0, 144, 178, 0, 32, 192, 17, 240, 11, 64, 147, 0, 64, 148, 0, 66, 115, 0, 0, 66, 116, 0, 66, 117, 0, 66, 118, 0, 66, 119, 0, 66, 120, 0, 66, 121, 0, 66, 122, 0, 66, 123, 0, 7, 180, 52]" #encode_raw ="[50, 0, 48, 98, 0, 144, 178, 0, 32, 192, 17, 240, 11, 64, 147, 0, 64, 148, 0, 66, 115, 0, 0, 66, 116, 0, 66, 117, 0, 66, 118, 0, 66, 119, 0, 66, 120, 0, 66, 121, 0, 66, 122, 0, 66, 123, 0, 7, 180, 52]"
encode_raw = "['0x32', '0x00', '0x1A', '0x80', '0xFF', '0x00', '0x20', '0x00', '0x00', '0xC0', '0x11', '0xB0', '0x01', '0x06', '0x07', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0xBD', '0xBC', '0x34']"
try: try:
encode_bytearray = json.loads(encode_raw.strip()) # for [12, 234, 456 ,67] encode_bytearray = json.loads(encode_raw.strip()) # for [12, 234, 456 ,67]
except: except:
@@ -23,10 +33,10 @@ encoded_nasa = NASAPacket.NASAPacket()
encoded_nasa.parse(encode_bytearray) encoded_nasa.parse(encode_bytearray)
print(f"encode NASA Object: {encoded_nasa}") print(f"encode NASA Object: {encoded_nasa}")
exit()
# time to reverse that thing! # time to reverse that thing!
decoded_nasa = NASAPacket.NASAPacket() decoded_nasa = NASAPacket.NASAPacket()
decoded_nasa.set_packet_source_address_class(NASAPacket.AddressClassEnum.Outdoor) decoded_nasa.set_packet_source_address_class(NASAPacket.AddressClassEnum.JIGTester)
decoded_nasa.set_packet_source_channel(0) decoded_nasa.set_packet_source_channel(0)
decoded_nasa.set_packet_source_address(0) decoded_nasa.set_packet_source_address(0)
decoded_nasa.set_packet_dest_address_class(NASAPacket.AddressClassEnum.BroadcastSelfLayer) decoded_nasa.set_packet_dest_address_class(NASAPacket.AddressClassEnum.BroadcastSelfLayer)

159
helpertils/messageFinder.py Normal file
View File

@@ -0,0 +1,159 @@
import os
import sys
import inspect
import asyncio
import yaml
import traceback
currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
parentdir = os.path.dirname(currentdir)
sys.path.insert(0, parentdir)
from NASAPacket import NASAPacket, AddressClassEnum, DataType, PacketType
from NASAMessage import NASAMessage
# Generate a list of all possible 2-byte hex values, always padded to 4 characters
two_byte_hex_values = [f"0x{i:04X}" for i in range(0x0000, 0xFFFF)]
send_message_list = []
seen_message_list = []
with open('data/NasaRepository.yml', mode='r') as file:
NASA_REPO = yaml.safe_load(file)
async def main():
# load config
with open('config.yml', mode='r') as file:
config = yaml.safe_load(file)
# Print the total count to confirm all values are included
print(f"Total values: {len(two_byte_hex_values)}")
reader, writer = await asyncio.open_connection('172.19.2.240', 4196)
print(" serial_connection fertig")
await asyncio.gather(
serial_read(reader, config),
serial_write(writer, config),
)
async def serial_write(writer: asyncio.StreamWriter, config):
_CHUNKSIZE=10
chunks = [two_byte_hex_values[i:i + _CHUNKSIZE] for i in range(0, len(two_byte_hex_values), _CHUNKSIZE)]
for chunk in chunks:
nasa_msg = NASAPacket()
nasa_msg.set_packet_source_address_class(AddressClassEnum.JIGTester)
nasa_msg.set_packet_source_channel(240)
nasa_msg.set_packet_source_address(0)
nasa_msg.set_packet_dest_address_class(AddressClassEnum.BroadcastSetLayer)
nasa_msg.set_packet_dest_channel(0)
nasa_msg.set_packet_dest_address(32)
nasa_msg.set_packet_information(True)
nasa_msg.set_packet_version(2)
nasa_msg.set_packet_retry_count(0)
nasa_msg.set_packet_type(PacketType.Normal)
nasa_msg.set_packet_data_type(DataType.Read)
nasa_msg.set_packet_number(166)
msglist=[]
for msg in chunk:
if msg not in send_message_list and msg not in seen_message_list:
tmpmsg = NASAMessage()
tmpmsg.set_packet_message(int(msg, 16))
value = 0
if tmpmsg.packet_message_type == 0:
value_raw = value.to_bytes(1, byteorder='big')
elif tmpmsg.packet_message_type == 1:
value_raw = value.to_bytes(2, byteorder='big')
elif tmpmsg.packet_message_type == 2:
value_raw = value.to_bytes(4, byteorder='big')
else:
value_raw = value.to_bytes(1, byteorder='big')
tmpmsg.set_packet_payload_raw(value_raw)
msglist.append(tmpmsg)
nasa_msg.set_packet_messages(msglist)
raw = nasa_msg.to_raw()
writer.write(raw)
await writer.drain()
send_message_list.extend(chunk)
if len(send_message_list) % 100 == 0:
print(f"Sended count: {len(send_message_list)}")
await asyncio.sleep(1)
async def serial_read(reader: asyncio.StreamReader, config):
prev_byte = 0x00
packet_started = False
data = bytearray()
packet_size = 0
while True:
current_byte = await reader.read(1) # read bitewise
#data = await reader.read(1024)
#data = await reader.readuntil(b'\x34fd')
if current_byte:
if packet_started:
data.extend(current_byte)
if len(data) == 3:
packet_size = ((data[1] << 8) | data[2]) + 2
if packet_size <= len(data):
asyncio.create_task(process_packet(data, config))
data = bytearray()
packet_started = False
# identify packet start
if current_byte == b'\x00' and prev_byte == b'\x32':
packet_started = True
data.extend(prev_byte)
data.extend(current_byte)
prev_byte = current_byte
def search_nasa_table(address):
for key in NASA_REPO:
if NASA_REPO[key]['address'].lower() == address.lower():
return key
def is_valid_rawvalue(rawvalue: bytes) -> bool:
return all(0x20 <= b <= 0x7E or b in (0x00, 0xFF) for b in rawvalue)
async def process_packet(buffer, config):
try:
nasa_packet = NASAPacket()
nasa_packet.parse(buffer)
for msg in nasa_packet.packet_messages:
if msg.packet_message not in seen_message_list:
seen_message_list.append(msg.packet_message)
msgkey = search_nasa_table(f"0x{msg.packet_message:04X}")
if msgkey is None:
msgkey = ""
msgvalue = None
if msg.packet_message_type == 3:
msgvalue = ""
if is_valid_rawvalue(msg.packet_payload[1:-1]):
for byte in msg.packet_payload[1:-1]:
if byte != 0x00 and byte != 0xFF:
char = chr(byte) if 32 <= byte <= 126 else f"{byte}"
msgvalue += char
else:
msgvalue += " "
msgvalue = msgvalue.strip()
else:
msgvalue = "".join([f"{int(x)}" for x in msg.packet_payload])
else:
msgvalue = int.from_bytes(msg.packet_payload, byteorder='big', signed=True)
line = f"| {len(seen_message_list):<6} | {hex(msg.packet_message):<6} | {msgkey:<50} | {msg.packet_message_type} | {msgvalue:<20} | {msg.packet_payload} |"
with open('helpertils/messagesFound.txt', "a") as dumpWriter:
dumpWriter.write(f"{line}\n")
except Exception as e:
pass
if __name__ == "__main__":
try:
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
except RuntimeError as e:
print(f"Runtime error: {e}")

View File

@@ -0,0 +1,169 @@
import os
import sys
import inspect
import asyncio
import yaml
import traceback
import pprint
currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
parentdir = os.path.dirname(currentdir)
sys.path.insert(0, parentdir)
from NASAPacket import NASAPacket, AddressClassEnum, DataType, PacketType
from NASAMessage import NASAMessage
# Generate a list of all possible 2-byte hex values, always padded to 4 characters
found_repo = {}
with open('data/NasaRepository.yml', mode='r') as file:
NASA_REPO = yaml.safe_load(file)
async def main():
# load finder file and anylse it
with open('helpertils/messagesFound.txt', mode='r') as file:
lines = file.read()
for line in lines.splitlines():
nix, nr, msgnr, msgname, type, packedval, rawval, nix2 = line.split("|")
if type.strip() != '3':
packedval = int(packedval.strip())
if len(msgname.strip()) == 0 and packedval != -1:
found_repo[msgnr.strip()] = {
"type": type.strip(),
"raw_value": rawval.strip(),
"packed_value": packedval
}
pprint.pprint(found_repo)
# load config
with open('config.yml', mode='r') as file:
config = yaml.safe_load(file)
# Print the total count to confirm all values are included
print(f"Total values: {len(found_repo)}")
reader, writer = await asyncio.open_connection('172.19.2.240', 4196)
print(" serial_connection fertig")
await asyncio.gather(
serial_read(reader, config),
serial_write(writer, config),
)
async def serial_write(writer: asyncio.StreamWriter, config):
_CHUNKSIZE=10
keys = list(found_repo.keys())
chunks = [keys[i:i + _CHUNKSIZE] for i in range(0, len(keys), _CHUNKSIZE)]
while True:
print("Start Writing")
for chunk in chunks:
nasa_msg = NASAPacket()
nasa_msg.set_packet_source_address_class(AddressClassEnum.JIGTester)
nasa_msg.set_packet_source_channel(240)
nasa_msg.set_packet_source_address(0)
nasa_msg.set_packet_dest_address_class(AddressClassEnum.BroadcastSetLayer)
nasa_msg.set_packet_dest_channel(0)
nasa_msg.set_packet_dest_address(32)
nasa_msg.set_packet_information(True)
nasa_msg.set_packet_version(2)
nasa_msg.set_packet_retry_count(0)
nasa_msg.set_packet_type(PacketType.Normal)
nasa_msg.set_packet_data_type(DataType.Read)
nasa_msg.set_packet_number(166)
msglist=[]
for msg in chunk:
tmpmsg = NASAMessage()
tmpmsg.set_packet_message(int(msg, 16))
value = 0
if tmpmsg.packet_message_type == 0:
value_raw = value.to_bytes(1, byteorder='big')
elif tmpmsg.packet_message_type == 1:
value_raw = value.to_bytes(2, byteorder='big')
elif tmpmsg.packet_message_type == 2:
value_raw = value.to_bytes(4, byteorder='big')
else:
value_raw = value.to_bytes(1, byteorder='big')
tmpmsg.set_packet_payload_raw(value_raw)
msglist.append(tmpmsg)
nasa_msg.set_packet_messages(msglist)
raw = nasa_msg.to_raw()
writer.write(raw)
await writer.drain()
await asyncio.sleep(1)
print("End Writing")
await asyncio.sleep(120)
async def serial_read(reader: asyncio.StreamReader, config):
prev_byte = 0x00
packet_started = False
data = bytearray()
packet_size = 0
while True:
current_byte = await reader.read(1) # read bitewise
#data = await reader.read(1024)
#data = await reader.readuntil(b'\x34fd')
if current_byte:
if packet_started:
data.extend(current_byte)
if len(data) == 3:
packet_size = ((data[1] << 8) | data[2]) + 2
if packet_size <= len(data):
asyncio.create_task(process_packet(data, config))
data = bytearray()
packet_started = False
# identify packet start
if current_byte == b'\x00' and prev_byte == b'\x32':
packet_started = True
data.extend(prev_byte)
data.extend(current_byte)
prev_byte = current_byte
def search_nasa_table(address):
for key in NASA_REPO:
if NASA_REPO[key]['address'].lower() == address.lower():
return key
def is_valid_rawvalue(rawvalue: bytes) -> bool:
return all(0x20 <= b <= 0x7E or b in (0x00, 0xFF) for b in rawvalue)
async def process_packet(buffer, config):
try:
nasa_packet = NASAPacket()
nasa_packet.parse(buffer)
for msg in nasa_packet.packet_messages:
if f"0x{msg.packet_message:04X}" in found_repo:
msgvalue = None
if msg.packet_message_type == 3:
msgvalue = ""
if is_valid_rawvalue(msg.packet_payload[1:-1]):
for byte in msg.packet_payload[1:-1]:
if byte != 0x00 and byte != 0xFF:
char = chr(byte) if 32 <= byte <= 126 else f"{byte}"
msgvalue += char
else:
msgvalue += " "
msgvalue = msgvalue.strip()
else:
msgvalue = "".join([f"{int(x)}" for x in msg.packet_payload])
else:
msgvalue = int.from_bytes(msg.packet_payload, byteorder='big', signed=True)
if msgvalue != found_repo[f"0x{msg.packet_message:04X}"]['packed_value']:
line = f" {hex(msg.packet_message):<6} | {msg.packet_message_type} | {found_repo[f"0x{msg.packet_message:04X}"]['packed_value']} -> {msgvalue} |"
print(line)
except Exception as e:
pass
if __name__ == "__main__":
try:
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
except RuntimeError as e:
print(f"Runtime error: {e}")

View File

@@ -0,0 +1,97 @@
import argparse
import yaml
def replace_empty_with_null(d):
if isinstance(d, dict):
return {k: replace_empty_with_null(v) for k, v in d.items()}
elif isinstance(d, list):
return [replace_empty_with_null(v) for v in d]
elif d is None:
return None # Ensure `None` stays as YAML `null`
elif len(d.strip()) == 0:
return None
return d
def main():
with open('data/NasaRepository.yml', 'r') as nasarepo:
old_yaml = yaml.safe_load(nasarepo)
ele = {}
for key, value in old_yaml.items():
print(key)
ele[key] = {}
ele[key]['hass_opts'] = {}
ele[key]['hass_opts']['platform'] = {}
ele[key]['address'] = replace_empty_with_null(old_yaml[key]['address'])
if replace_empty_with_null(old_yaml[key]['arithmetic']) is not None:
ele[key]['arithmetic'] = old_yaml[key]['arithmetic']
if 'description' in old_yaml[key] and replace_empty_with_null(old_yaml[key]['description']) is not None:
ele[key]['description'] = old_yaml[key]['description']
ele[key]['hass_opts']['default_platform'] = "sensor"
if 'writable' in old_yaml[key]:
ele[key]['hass_opts']['writable'] = old_yaml[key]['writable']
else:
ele[key]['hass_opts']['writable'] = False
if 'enum' in old_yaml[key]:
new_values = [x.replace("'", "") for x in old_yaml[key]['enum'].values()]
if all([en.lower() in ['on', 'off'] for en in new_values]):
ele[key]['enum'] = old_yaml[key]['enum']
ele[key]['hass_opts']['default_platform'] = "binary_sensor"
ele[key]['hass_opts']['platform']['payload_off'] = 'OFF'
ele[key]['hass_opts']['platform']['payload_on'] = 'ON'
ele[key]['hass_opts']['platform']['type'] = 'switch'
else:
ele[key]['enum'] = old_yaml[key]['enum']
ele[key]['hass_opts']['platform']['options'] = new_values
ele[key]['hass_opts']['platform']['type'] = 'select'
else:
if 'min' in old_yaml[key]:
ele[key]['hass_opts']['platform']['min'] = old_yaml[key]['min']
if 'max' in old_yaml[key]:
ele[key]['hass_opts']['platform']['max'] = old_yaml[key]['max']
if 'step' in old_yaml[key]:
ele[key]['hass_opts']['platform']['step'] = old_yaml[key]['step']
ele[key]['hass_opts']['platform']['type'] = 'number'
if replace_empty_with_null(old_yaml[key]['remarks']) is not None:
ele[key]['remarks'] = old_yaml[key]['remarks']
if replace_empty_with_null(old_yaml[key]['signed']) is not None:
ele[key]['signed'] = old_yaml[key]['signed']
if replace_empty_with_null(old_yaml[key]['type']) is not None:
ele[key]['type'] = old_yaml[key]['type']
if 'state_class' in old_yaml[key]:
ele[key]['hass_opts']['state_class'] = old_yaml[key]['state_class']
if 'device_class' in old_yaml[key]:
ele[key]['hass_opts']['device_class'] = old_yaml[key]['device_class']
if 'unit' in old_yaml[key]:
if replace_empty_with_null(old_yaml[key]['unit']) is not None:
ele[key]['hass_opts']['unit'] = old_yaml[key]['unit']
if ele[key]['hass_opts']['unit'] == "\u00b0C":
ele[key]['hass_opts']['device_class'] = "temperature"
elif ele[key]['hass_opts']['unit'] == '%':
ele[key]['hass_opts']['state_class'] = "measurement"
elif ele[key]['hass_opts']['unit'] == 'kW':
ele[key]['hass_opts']['device_class'] = "power"
elif ele[key]['hass_opts']['unit'] == 'rpm':
ele[key]['hass_opts']['state_class'] = "measurement"
elif ele[key]['hass_opts']['unit'] == 'bar':
ele[key]['hass_opts']['device_class'] = "pressure"
elif ele[key]['hass_opts']['unit'] == 'HP':
ele[key]['hass_opts']['device_class'] = "power"
elif ele[key]['hass_opts']['unit'] == 'hz':
ele[key]['hass_opts']['device_class'] = "frequency"
elif ele[key]['hass_opts']['unit'] == 'min':
ele[key]['hass_opts']['device_class'] = "duration"
elif ele[key]['hass_opts']['unit'] == 'h':
ele[key]['hass_opts']['device_class'] = "duration"
elif ele[key]['hass_opts']['unit'] == 's':
ele[key]['hass_opts']['device_class'] = "duration"
with open('data/NasaRepository.yml', 'w') as newyaml:
yaml.dump(ele, newyaml, default_flow_style=False)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,16 @@
### list devices
GET https://api.smartthings.com/v1/devices
Authorization: Bearer xx
### "deviceId": "xx" "id": "samsungce.ehsFsvSettings", "version": 1
GET https://api.smartthings.com/v1/capabilities/samsungce.ehsFsvSettings/1
Authorization: Bearer xx
### "locationId": "xx" "name": "Mein Zuhause"
GET https://api.smartthings.com/v1/locations
Authorization: Bearer xx
###
GET https://api.smartthings.com/v1/capabilities/samsungce.ehsFsvSettings/1/i18n/en
Authorization: Bearer xx

View File

@@ -0,0 +1,462 @@
views:
- title: Overview
type: sections
max_columns: 4
subview: false
sections:
- type: grid
cards:
- type: entities
entities:
- entity: select.samsung_ehssentinel_power
name: Heatpump Power
secondary_info: last-updated
icon: mdi:power
- entity: number.samsung_ehssentinel_intempwaterlawtargetf
name: Adjust heating curve
secondary_info: last-updated
- entity: select.samsung_ehssentinel_indooropmode
name: Operation Mode
secondary_info: last-updated
title: Control Heatpump
- type: entities
entities:
- entity: sensor.samsung_ehssentinel_outdooroperationstatus
name: Operation Status
secondary_info: last-updated
- entity: binary_sensor.samsung_ehssentinel_dhwpower
secondary_info: last-updated
name: DHW Power
- entity: sensor.samsung_ehssentinel_outdoordefroststep
name: Defrost Step
secondary_info: last-updated
- entity: binary_sensor.samsung_ehssentinel_controlsilence
name: Silent Mode
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_ehssentinelheatoutput
name: Heat Output
secondary_info: last-updated
icon: mdi:heat-wave
- entity: sensor.samsung_ehssentinel_ingeneratedpowerlastminute
name: Generated Power Last Minute
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_inflowsensorcalc
secondary_info: last-changed
name: Water Flow Speed
- entity: sensor.samsung_ehssentinel_ehssentinelcop
name: COP
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_outdoortw1temp
name: Return Temperature
secondary_info: last-updated
icon: mdi:waves-arrow-left
- entity: sensor.samsung_ehssentinel_outdoortw2temp
name: Flow Temperature
secondary_info: last-updated
icon: mdi:waves-arrow-right
- entity: sensor.samsung_ehssentinel_indoordhwcurrenttemp
name: DHW Tank Temperature
secondary_info: last-updated
icon: mdi:water-boiler
- entity: sensor.samsung_ehssentinel_outdoorouttemp
secondary_info: last-updated
name: Outdoor Temperatur
- entity: sensor.samsung_ehssentinel_outdoorcomp1targethz
name: Compressor Target Frequence
secondary_info: last-updated
icon: mdi:sine-wave
- entity: sensor.samsung_ehssentinel_outdoorcomp1runhz
name: Compressor Run Frequence
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_outdoorcomp1orderhz
name: Compressor Order Frequence
secondary_info: last-updated
title: Current Data
- type: entities
entities:
- entity: sensor.samsung_ehssentinel_intotalgeneratedpower
name: Total Generated Heat Output
secondary_info: last-updated
icon: mdi:heat-wave
- entity: sensor.samsung_ehssentinel_outdoorcontrolwattmeterallunitaccum
name: Total Consumed Power
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_ehssentineltotalcop
name: Total COP
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_inminutessinceinstallation
name: Total Minutes Since Installation
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_inminutesactive
name: Total Minutes Active
secondary_info: last-updated
title: Life Cycle Data
- type: grid
cards:
- type: history-graph
entities:
- entity: sensor.samsung_ehssentinel_outdoorcomp1orderhz
name: Compressor freq.
- entity: sensor.samsung_ehssentinel_outdoorfanrpm1
name: Outdoor FAN Speed
logarithmic_scale: false
title: Outdoor Unit
hours_to_show: 6
grid_options:
columns: full
rows: 10
- type: history-graph
entities:
- entity: sensor.samsung_ehssentinel_outdoortw1temp
name: Return Temperature
- entity: sensor.samsung_ehssentinel_outdoortw2temp
name: Flow Temperature
logarithmic_scale: false
hours_to_show: 6
grid_options:
columns: full
rows: 10
title: Water Law
- type: history-graph
entities:
- entity: sensor.samsung_ehssentinel_ehssentinelheatoutput
name: Heat Output
- entity: sensor.samsung_ehssentinel_outdoorcontrolwattmeterallunit
name: Power Input
- entity: sensor.samsung_ehssentinel_ehssentinelcop
name: COP
logarithmic_scale: false
hours_to_show: 6
grid_options:
columns: full
rows: 16
title: Efficiency
column_span: 3
- type: sections
max_columns: 6
title: Field Setting Value
path: field-setting-value
sections:
- type: grid
cards:
- type: entities
entities:
- entity: number.samsung_ehssentinel_infsv1011
name: Water Out Temp. for Cooling Max.
secondary_info: last-updated
- entity: number.samsung_ehssentinel_infsv1012
name: Water Out Temp. for Cooling Min.
secondary_info: last-updated
- entity: number.samsung_ehssentinel_infsv1021
name: Room Temp. for Cooling Max.
secondary_info: last-updated
- entity: number.samsung_ehssentinel_infsv1022
name: Room Temp. for Cooling Min.
secondary_info: last-updated
- entity: number.samsung_ehssentinel_infsv1031
name: Water Out Temp. for Heating Max.
secondary_info: last-updated
- entity: number.samsung_ehssentinel_infsv1032
name: Water Out Temp. for Heating Min.
secondary_info: last-updated
- entity: number.samsung_ehssentinel_infsv1041
name: Room Temp. for Heating Max.
secondary_info: last-updated
- entity: number.samsung_ehssentinel_infsv1042
name: Room Temp. for Heating Min.
secondary_info: last-updated
- entity: number.samsung_ehssentinel_infsv1051
name: DHW tank Temp. Max.
secondary_info: last-updated
- entity: number.samsung_ehssentinel_infsv1052
name: DHW tank Temp. Min.
secondary_info: last-updated
title: FSV 10** - Remote Controller
show_header_toggle: false
state_color: false
grid_options:
columns: full
column_span: 2
- type: grid
cards:
- type: entities
entities:
- entity: number.samsung_ehssentinel_infsv2011
name: Heating Outdoor Temp. for WL Max.
secondary_info: last-updated
- entity: number.samsung_ehssentinel_infsv2012
name: Heating Outdoor Temp. for WL Min.
secondary_info: last-updated
- entity: number.samsung_ehssentinel_infsv2021
name: Heating Water out Temp. UFH/WL1 Max.
secondary_info: last-updated
- entity: number.samsung_ehssentinel_infsv2022
name: Heating Water out Temp. UFH/WL1 Min.
secondary_info: last-updated
- entity: number.samsung_ehssentinel_infsv2031
name: Heating Water out Temp. FCU/WL2 Max.
secondary_info: last-updated
- entity: number.samsung_ehssentinel_infsv2032
name: Heating Water out Temp. FCU/WL2 Min.
secondary_info: last-updated
- entity: select.samsung_ehssentinel_infsv2041
name: Heating WL Selection
icon: mdi:heating-coil
secondary_info: last-updated
- entity: number.samsung_ehssentinel_infsv2051
name: Cooling Outdoor Temp. for WL Max.
secondary_info: last-updated
- entity: number.samsung_ehssentinel_infsv2052
name: Cooling Outdoor Temp. for WL Min.
secondary_info: last-updated
- entity: number.samsung_ehssentinel_infsv2061
name: Cooling Water out Temp UFH/WL1 Max.
secondary_info: last-updated
- entity: number.samsung_ehssentinel_infsv2062
name: Cooling Water out Temp. UFH/WL1 Min.
secondary_info: last-updated
- entity: number.samsung_ehssentinel_infsv2071
name: Cooling Water out Temp. FCU/WL2 Max.
secondary_info: last-updated
- entity: number.samsung_ehssentinel_infsv2072
name: Cooling Water out Temp. FCU/WL2 Min.
secondary_info: last-updated
- entity: select.samsung_ehssentinel_infsv2081
name: Cooling WL Selection
secondary_info: last-updated
icon: mdi:snowflake
- entity: select.samsung_ehssentinel_infsv2091
name: External Room Thermostat UFH
secondary_info: last-updated
- entity: select.samsung_ehssentinel_infsv2092
name: External Room Thermostat FCU
secondary_info: last-updated
title: FSV 20** - Water Law
grid_options:
columns: full
column_span: 2
- type: grid
cards:
- type: entities
entities:
- entity: select.samsung_ehssentinel_infsv3011
secondary_info: last-updated
name: DHW Application
icon: mdi:water-boiler
- entity: number.samsung_ehssentinel_infsv3021
secondary_info: last-updated
name: Heat Pump Max. Temperature
- entity: number.samsung_ehssentinel_infsv3022
name: Heat Pump Stop
secondary_info: last-updated
- entity: number.samsung_ehssentinel_infsv3023
name: Heat Pump Start
secondary_info: last-updated
- entity: number.samsung_ehssentinel_infsv3024
name: Heat Pump Min. Space heating operation time
secondary_info: last-updated
- entity: number.samsung_ehssentinel_infsv3025
name: Heat Pump Max. DHW operation time
secondary_info: last-updated
- entity: number.samsung_ehssentinel_infsv3026
name: Heat Pump Max. Space heating operation time
secondary_info: last-updated
- entity: number.samsung_ehssentinel_infsv3032
name: Booster Heat Delay Time
secondary_info: last-updated
- entity: switch.samsung_ehssentinel_infsv3041
name: Disinfection Application
secondary_info: last-updated
- entity: select.samsung_ehssentinel_infsv3042
name: Disinfection Interval
secondary_info: last-updated
- entity: number.samsung_ehssentinel_infsv3043
name: Disinfection Start Time
secondary_info: last-updated
- entity: number.samsung_ehssentinel_infsv3044
name: Disinfection Target Temp.
secondary_info: last-updated
- entity: number.samsung_ehssentinel_infsv3045
name: Disinfection Duration
secondary_info: last-updated
- entity: number.samsung_ehssentinel_infsv3046
name: Disinfection Max Time
secondary_info: last-updated
- entity: select.samsung_ehssentinel_infsv3051
name: Forced DHW Operation Time OFF Function
secondary_info: last-updated
- entity: number.samsung_ehssentinel_infsv3052
name: Farced DHW Operation Time Duration
secondary_info: last-updated
- entity: select.samsung_ehssentinel_infsv3061
name: Solar Panel/DHW Thermostat H/P Combination
secondary_info: last-updated
- entity: select.samsung_ehssentinel_infsv3071
name: Direction of 3Way Valve DHW Tank
secondary_info: last-updated
- entity: number.samsung_ehssentinel_infsv3081
name: Energy Metering BUH 1 step capacity
secondary_info: last-updated
- entity: number.samsung_ehssentinel_infsv3082
name: Energy Metering BUH 2 step capacity
secondary_info: last-updated
- entity: number.samsung_ehssentinel_infsv3083
name: Energy Metering BSH capacity
secondary_info: last-updated
title: FSV 30** - DHW code
grid_options:
columns: full
column_span: 2
- type: grid
cards:
- type: entities
entities:
- entity: select.samsung_ehssentinel_infsv4011
secondary_info: last-updated
name: Heat Pump Heating/DHW Priority
- entity: number.samsung_ehssentinel_infsv4012
name: Heat Pump Outdoor Temp. for Priority
secondary_info: last-updated
- entity: number.samsung_ehssentinel_infsv4013
name: Heat Pump Heat OFF
secondary_info: last-updated
- entity: select.samsung_ehssentinel_infsv4021
name: Backup Heater Application
secondary_info: last-updated
- entity: select.samsung_ehssentinel_infsv4022
name: Backup Heater BUH/BSH Priority
secondary_info: last-updated
- entity: select.samsung_ehssentinel_infsv4023
name: Backup Heater Cold Weather Compensation
secondary_info: last-updated
- entity: number.samsung_ehssentinel_infsv4024
name: Backup Heater Threshold Temp.
secondary_info: last-updated
- entity: number.samsung_ehssentinel_infsv4025
name: Backup Heater Defrost Backup Temp.
secondary_info: last-updated
- entity: select.samsung_ehssentinel_infsv4031
name: Backup Boiler Application
secondary_info: last-updated
- entity: select.samsung_ehssentinel_infsv4032
name: Backup Boiler Boiler Priority
secondary_info: last-updated
- entity: number.samsung_ehssentinel_infsv4033
name: Backup Boiler Threshold Power
secondary_info: last-updated
- entity: select.samsung_ehssentinel_infsv4041
name: Mixing Valve Application
secondary_info: last-updated
- entity: number.samsung_ehssentinel_infsv4042
name: Mixing Valve Target △T (Heating)
secondary_info: last-updated
- entity: number.samsung_ehssentinel_infsv4043
secondary_info: last-updated
name: Mixing Valve Target △T (Cooling)
- entity: select.samsung_ehssentinel_infsv4044
name: Mixing Valve Control Factor
secondary_info: last-updated
- entity: number.samsung_ehssentinel_infsv4045
name: Mixing Valve Control Factor
secondary_info: last-updated
- entity: number.samsung_ehssentinel_infsv4046
name: Mixing Valve Running Time
secondary_info: last-updated
- entity: select.samsung_ehssentinel_infsv4051
name: Inverter Pump Application
secondary_info: last-updated
- entity: number.samsung_ehssentinel_infsv4052
name: Inverter Pump Target △T
secondary_info: last-updated
- entity: select.samsung_ehssentinel_infsv4053
name: Inverter Pump Control Factor
secondary_info: last-updated
- entity: select.samsung_ehssentinel_infsv4061
name: Zone Control Application
secondary_info: last-updated
title: FSV 40** - Heating code
state_color: false
grid_options:
columns: full
column_span: 2
- type: grid
cards:
- type: entities
entities:
- entity: number.samsung_ehssentinel_infsv5011
secondary_info: last-updated
name: Outing Mode Water Out Temp. for Cooling
- entity: number.samsung_ehssentinel_infsv5012
name: Outing Mode Room Temp. for Cooling
secondary_info: last-updated
- entity: number.samsung_ehssentinel_infsv5013
name: Outing Mode Water Out Temp. for Heating
secondary_info: last-updated
- entity: number.samsung_ehssentinel_infsv5014
name: Outing Mode Room Temp. for Heating
secondary_info: last-updated
- entity: number.samsung_ehssentinel_infsv5015
name: Outing Mode Auto Cooling WL1 Temp.
secondary_info: last-updated
- entity: number.samsung_ehssentinel_infsv5016
name: Outing Mode Auto Cooling WL2 Temp.
secondary_info: last-updated
- entity: number.samsung_ehssentinel_infsv5017
name: Outing Mode Auto Heating WL1 Temp.
secondary_info: last-updated
- entity: number.samsung_ehssentinel_infsv5018
name: Outing Mode Auto Heating WL2 Temp.
secondary_info: last-updated
- entity: number.samsung_ehssentinel_infsv5019
name: Outing Mode Target Tank Temp.
secondary_info: last-updated
- entity: number.samsung_ehssentinel_infsv5021
name: DHW Saving Temp.
secondary_info: last-updated
- entity: switch.samsung_ehssentinel_infsv5022
name: DHW Saving Mode
secondary_info: last-updated
- entity: number.samsung_ehssentinel_infsv5023
name: DHW Saving Thermo on Temp.
secondary_info: last-updated
- entity: select.samsung_ehssentinel_infsv5041
name: Power Peak Control Application
secondary_info: last-updated
- entity: select.samsung_ehssentinel_infsv5042
name: Power Peak Control Select Forced Off Parts
secondary_info: last-updated
- entity: select.samsung_ehssentinel_infsv5043
name: Power Peak Control Using Input Voltage
secondary_info: last-updated
- entity: select.samsung_ehssentinel_infsv5051
name: Frequency Ratio Control
- entity: select.samsung_ehssentinel_infsv5061
name: Ratio of hot water supply compare to heating
secondary_info: last-updated
- entity: select.samsung_ehssentinel_infsv5081
name: PV Control Application
secondary_info: last-updated
- entity: number.samsung_ehssentinel_infsv5082
name: PV Control Setting Temp. Shift Value (Cool)
secondary_info: last-updated
- entity: number.samsung_ehssentinel_infsv5083
name: PV Control Setting Temp. Shift Value (Heat)
secondary_info: last-updated
- entity: select.samsung_ehssentinel_infsv5091
name: Smart Grid Control Application
secondary_info: last-updated
- entity: number.samsung_ehssentinel_infsv5092
name: Smart Grid Control Setting Temp. Shift Value (Heat)
secondary_info: last-updated
- entity: number.samsung_ehssentinel_infsv5093
name: Smart Grid Control Setting Temp. Shift Value (DHW)
secondary_info: last-updated
- entity: select.samsung_ehssentinel_infsv5094
name: Smart Grid Control DHW Mode
secondary_info: last-updated
title: FSV 50** - Others code
grid_options:
columns: full
column_span: 2
cards: []
dense_section_placement: true

View File

@@ -6,38 +6,23 @@ views:
sections: sections:
- type: grid - type: grid
cards: cards:
- type: tile
name: Operation mode
vertical: true
hide_state: false
show_entity_picture: false
grid_options:
columns: 6
rows: 2
entity: sensor.samsung_ehssentinel_outdooroperationstatus
- type: tile
entity: binary_sensor.samsung_ehssentinel_controlsilence
name: Silent Mode
vertical: true
hide_state: false
show_entity_picture: false
- type: tile
name: DHW Power
vertical: true
hide_state: false
show_entity_picture: false
entity: binary_sensor.samsung_ehssentinel_dhwpower
grid_options:
columns: 6
rows: 2
- type: tile
name: Defrost Status
vertical: true
hide_state: false
show_entity_picture: false
entity: sensor.samsung_ehssentinel_outdoordefroststep
- type: entities - type: entities
entities: entities:
- entity: sensor.samsung_ehssentinel_power
name: Heatpump Power
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_outdooroperationstatus
name: Operation Mode
secondary_info: last-updated
- entity: binary_sensor.samsung_ehssentinel_dhwpower
name: DHW Power
secondary_info: last-updated
- entity: binary_sensor.samsung_ehssentinel_controlsilence
name: Silence Mode
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_outdoordefroststep
name: Defrost Step
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_ehssentinelheatoutput - entity: sensor.samsung_ehssentinel_ehssentinelheatoutput
name: Heat Output name: Heat Output
secondary_info: last-updated secondary_info: last-updated
@@ -45,6 +30,9 @@ views:
- entity: sensor.samsung_ehssentinel_ingeneratedpowerlastminute - entity: sensor.samsung_ehssentinel_ingeneratedpowerlastminute
name: Generated Power Last Minute name: Generated Power Last Minute
secondary_info: last-updated secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_inflowsensorcalc
secondary_info: last-changed
name: Water Flow Speed
- entity: sensor.samsung_ehssentinel_ehssentinelcop - entity: sensor.samsung_ehssentinel_ehssentinelcop
name: COP name: COP
secondary_info: last-updated secondary_info: last-updated
@@ -64,9 +52,15 @@ views:
secondary_info: last-updated secondary_info: last-updated
name: Outdoor Temperatur name: Outdoor Temperatur
- entity: sensor.samsung_ehssentinel_outdoorcomp1targethz - entity: sensor.samsung_ehssentinel_outdoorcomp1targethz
name: Compressor Frequence name: Compressor Target Frequence
secondary_info: last-updated secondary_info: last-updated
icon: mdi:sine-wave icon: mdi:sine-wave
- entity: sensor.samsung_ehssentinel_outdoorcomp1runhz
name: Compressor Run Frequence
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_outdoorcomp1orderhz
name: Compressor Order Frequence
secondary_info: last-updated
title: Current Data title: Current Data
- type: entities - type: entities
entities: entities:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 289 KiB

After

Width:  |  Height:  |  Size: 253 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 227 KiB

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 502 KiB

After

Width:  |  Height:  |  Size: 445 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 293 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

View File

@@ -3,6 +3,7 @@ import serial
import serial_asyncio import serial_asyncio
import traceback import traceback
from MessageProcessor import MessageProcessor from MessageProcessor import MessageProcessor
from MessageProducer import MessageProducer
from EHSArguments import EHSArguments from EHSArguments import EHSArguments
from EHSConfig import EHSConfig from EHSConfig import EHSConfig
from EHSExceptions import MessageWarningException, SkipInvalidPacketException from EHSExceptions import MessageWarningException, SkipInvalidPacketException
@@ -16,7 +17,7 @@ from CustomLogger import logger
from NASAPacket import NASAPacket, AddressClassEnum, PacketType, DataType from NASAPacket import NASAPacket, AddressClassEnum, PacketType, DataType
from NASAMessage import NASAMessage from NASAMessage import NASAMessage
version = "0.3.0 Stable" version = "1.0.0 Stable"
async def main(): async def main():
""" """
@@ -76,28 +77,6 @@ async def main():
await serial_connection(config, args) await serial_connection(config, args)
async def process_buffer(buffer, args, config): async def process_buffer(buffer, args, config):
"""
Processes a buffer of data asynchronously, identifying and handling packets based on specific criteria.
Args:
buffer (list): A list of bytes representing the buffer to be processed.
args (Any): Additional arguments to be passed to the packet processing function.
Notes:
- The function continuously checks the buffer for data.
- If the first byte of the buffer is 0x32, it is considered a start byte.
- The packet size is determined by combining the second and third bytes of the buffer.
- If the buffer contains enough data for a complete packet, the packet is processed.
- If the buffer does not contain enough data, the function waits and checks again.
- Non-start bytes are removed from the buffer.
- The function sleeps for 0.03 seconds between iterations to avoid busy-waiting.
Logging:
- Logs the buffer size when data is present.
- Logs when the start byte is recognized.
- Logs the calculated packet size.
- Logs the complete packet and the last byte read when a packet is processed.
- Logs if the buffer is too small to read a complete packet.
- Logs if a received byte is not a start byte.
"""
if buffer: if buffer:
if (len(buffer) > 14): if (len(buffer) > 14):
for i in range(0, len(buffer)): for i in range(0, len(buffer)):
@@ -111,18 +90,6 @@ async def process_buffer(buffer, args, config):
logger.debug(f"Buffer to short for NASA {len(buffer)}") logger.debug(f"Buffer to short for NASA {len(buffer)}")
async def serial_connection(config, args): async def serial_connection(config, args):
"""
Asynchronously reads data from a serial connection and processes it.
Args:
config (object): Configuration object containing serial or tcp connection parameters.
args (object): Additional arguments for buffer processing.
This function establishes a serial or tcp connection using parameters from the config object,
reads data from the serial port or tcp port until a specified delimiter (0x34) is encountered,
and appends the received data to a buffer. It also starts an asynchronous task to
process the buffer.
The function runs an infinite loop to continuously read data from the serial port/tcp port.
"""
buffer = [] buffer = []
loop = asyncio.get_running_loop() loop = asyncio.get_running_loop()
@@ -139,109 +106,92 @@ async def serial_connection(config, args):
rtscts=True, rtscts=True,
timeout=1 timeout=1
) )
await asyncio.gather( await asyncio.gather(
serial_read(reader, args, config), serial_read(reader, args, config),
serial_write(writer, reader, args, config), serial_write(writer, config),
) )
async def serial_read(reader: asyncio.StreamReader, args, config):
prev_byte = 0x00
packet_started = False
data = bytearray()
packet_size = 0
async def serial_read(reader, args, config):
while True: while True:
data = await reader.readuntil(b'\x34') # Read up to end of next message 0x34 current_byte = await reader.read(1) # read bitewise
if data: #data = await reader.read(1024)
asyncio.create_task(process_buffer(data, args, config)) #data = await reader.readuntil(b'\x34fd')
#buffer.extend(data) if current_byte:
logger.debug(f"Received: {data}") if packet_started:
logger.debug(f"Received: {data!r}") data.extend(current_byte)
logger.debug(f"Received: {[hex(x) for x in data]}") if len(data) == 3:
packet_size = ((data[1] << 8) | data[2]) + 2
if packet_size <= len(data):
if current_byte == b'\x34':
asyncio.create_task(process_buffer(data, args, config))
logger.debug(f"Received int: {data}")
logger.debug(f"Received hex: {[hex(x) for x in data]}")
data = bytearray()
packet_started = False
else:
if config.LOGGING['invalidPacket']:
logger.warning(f"Packet does not end with an x34. Size {packet_size} length {len(data)}")
logger.warning(f"Received hex: {[hex(x) for x in data]}")
logger.warning(f"Received raw: {data}")
else:
logger.debug(f"Packet does not end with an x34. Size {packet_size} length {len(data)}")
logger.debug(f"Received hex: {[hex(x) for x in data]}")
logger.debug(f"Received raw: {data}")
data = bytearray()
packet_started = False
await asyncio.sleep(0.1) # Yield control to other tasks # identify packet start
if current_byte == b'\x00' and prev_byte == b'\x32':
packet_started = True
data.extend(prev_byte)
data.extend(current_byte)
async def serial_write(writer:asyncio.StreamWriter, reader: asyncio.StreamReader, args, config): prev_byte = current_byte
"""
TODO Not used yet, only for future use... #await asyncio.sleep(0.001) # Yield control to other tasks
Asynchronously writes data to the serial port. async def serial_write(writer:asyncio.StreamWriter, config):
This function sends data through the serial port at regular intervals. producer = MessageProducer(writer=writer)
Args:
transport: The serial transport object. # Wait 20s befor initial polling
args: Additional arguments. await asyncio.sleep(20)
Returns:
None
"""
if config.POLLING is not None: if config.POLLING is not None:
for poller in config.POLLING['fetch_interval']: for poller in config.POLLING['fetch_interval']:
if poller['enable']: if poller['enable']:
await asyncio.sleep(3) await asyncio.sleep(1)
asyncio.create_task(make_default_request_packet(writer=writer, config=config, poller=poller)) asyncio.create_task(make_default_request_packet(producer=producer, config=config, poller=poller))
async def make_default_request_packet(writer, config, poller): async def make_default_request_packet(producer: MessageProducer, config: EHSConfig, poller):
logger.info(f"Setting up Poller {poller['name']} every {poller['schedule']} seconds") logger.info(f"Setting up Poller {poller['name']} every {poller['schedule']} seconds")
message_list = [] message_list = []
for message in config.POLLING['groups'][poller['name']]: for message in config.POLLING['groups'][poller['name']]:
tmp_msg = NASAMessage() message_list.append(message)
tmp_msg.set_packet_message(int(config.NASA_REPO[message]['address'], 16))
if config.NASA_REPO[message]['type'] == 'ENUM':
tmp_msg.set_packet_message_type(0)
tmp_msg.set_packet_payload([0])
elif config.NASA_REPO[message]['type'] == 'VAR':
tmp_msg.set_packet_message_type(1)
tmp_msg.set_packet_payload([0, 0])
elif config.NASA_REPO[message]['type'] == 'LVAR':
tmp_msg.set_packet_message_type(2)
tmp_msg.set_packet_payload([0, 0, 0, 0])
else:
logger.warning(f"Unknown Type for {message} type: {config.NASA_REPO[message]['type']}")
break
message_list.append(tmp_msg)
while True: while True:
chunk_size = 10 try:
chunks = [message_list[i:i + chunk_size] for i in range(0, len(message_list), chunk_size)] await producer.read_request(message_list)
for chunk in chunks: except MessageWarningException as e:
await asyncio.sleep(1) logger.warning("Polling Messages was not successfull")
nasa_msg = NASAPacket() logger.warning(f"Error processing message: {e}")
nasa_msg.set_packet_source_address_class(AddressClassEnum.WiFiKit) logger.warning(f"Message List: {message_list}")
nasa_msg.set_packet_source_channel(0) except Exception as e:
nasa_msg.set_packet_source_address(144) logger.error("Error Accured, Polling will be skipped")
nasa_msg.set_packet_dest_address_class(AddressClassEnum.BroadcastSetLayer) logger.error(f"Error processing message: {e}")
nasa_msg.set_packet_dest_channel(0) logger.error(traceback.format_exc())
nasa_msg.set_packet_dest_address(32)
nasa_msg.set_packet_information(True)
nasa_msg.set_packet_version(2)
nasa_msg.set_packet_retry_count(0)
nasa_msg.set_packet_type(PacketType.Normal)
nasa_msg.set_packet_data_type(DataType.Read)
nasa_msg.set_packet_number(len(chunk))
nasa_msg.set_packet_messages(chunk)
final_packet = nasa_msg.to_raw()
writer.write(final_packet)
await writer.drain()
if config.LOGGING['pollerMessage']:
logger.info(f"Polling following raw: {[hex(x) for x in final_packet]}")
logger.info(f"Polling following NASAPacket: {nasa_msg}")
else:
logger.debug(f"Sent data raw: {final_packet}")
logger.debug(f"Sent data raw: {nasa_msg}")
logger.debug(f"Sent data raw: {[hex(x) for x in final_packet]}")
logger.debug(f"Sent data raw: {[x for x in final_packet]}")
await asyncio.sleep(poller['schedule']) await asyncio.sleep(poller['schedule'])
logger.info(f"Refresh Poller {poller['name']}") logger.info(f"Refresh Poller {poller['name']}")
async def process_packet(buffer, args, config): async def process_packet(buffer, args, config):
"""
Asynchronously processes a packet buffer.
If `dumpWriter` is `None`, it attempts to process the packet using `MessageProcessor`.
If a `MessageWarningException` is raised, it logs a warning and skips the packet.
If any other exception is raised, it logs an error, skips the packet, and logs the stack trace.
If `dumpWriter` is not `None`, it writes the buffer to `dumpWriter`.
Args:
buffer (bytes): The packet buffer to be processed.
"""
if args.DUMPFILE and not args.DRYRUN: if args.DUMPFILE and not args.DRYRUN:
async with aiofiles.open(args.DUMPFILE, "a") as dumpWriter: async with aiofiles.open(args.DUMPFILE, "a") as dumpWriter:
await dumpWriter.write(f"{buffer}\n") await dumpWriter.write(f"{buffer}\n")
@@ -254,7 +204,7 @@ async def process_packet(buffer, args, config):
logger.debug(nasa_packet) logger.debug(nasa_packet)
if nasa_packet.packet_source_address_class in (AddressClassEnum.Outdoor, AddressClassEnum.Indoor): if nasa_packet.packet_source_address_class in (AddressClassEnum.Outdoor, AddressClassEnum.Indoor):
messageProcessor = MessageProcessor() messageProcessor = MessageProcessor()
messageProcessor.process_message(nasa_packet) await messageProcessor.process_message(nasa_packet)
elif nasa_packet.packet_source_address_class == AddressClassEnum.WiFiKit and \ elif nasa_packet.packet_source_address_class == AddressClassEnum.WiFiKit and \
nasa_packet.packet_dest_address_class == AddressClassEnum.BroadcastSelfLayer and \ nasa_packet.packet_dest_address_class == AddressClassEnum.BroadcastSelfLayer and \
nasa_packet.packet_data_type == DataType.Notification: nasa_packet.packet_data_type == DataType.Notification:
@@ -274,14 +224,17 @@ async def process_packet(buffer, args, config):
logger.warning("Value Error on parsing Packet, Packet will be skipped") logger.warning("Value Error on parsing Packet, Packet will be skipped")
logger.warning(f"Error processing message: {e}") logger.warning(f"Error processing message: {e}")
logger.warning(f"Complete Packet: {[hex(x) for x in buffer]}") logger.warning(f"Complete Packet: {[hex(x) for x in buffer]}")
logger.warning(traceback.format_exc())
except SkipInvalidPacketException as e: except SkipInvalidPacketException as e:
logger.debug("Warnung accured, Packet will be skipped") logger.debug("Warnung accured, Packet will be skipped")
logger.debug(f"Error processing message: {e}") logger.debug(f"Error processing message: {e}")
logger.debug(f"Complete Packet: {[hex(x) for x in buffer]}") logger.debug(f"Complete Packet: {[hex(x) for x in buffer]}")
logger.debug(traceback.format_exc())
except MessageWarningException as e: except MessageWarningException as e:
logger.warning("Warnung accured, Packet will be skipped") logger.warning("Warnung accured, Packet will be skipped")
logger.warning(f"Error processing message: {e}") logger.warning(f"Error processing message: {e}")
logger.warning(f"Complete Packet: {[hex(x) for x in buffer]}") logger.warning(f"Complete Packet: {[hex(x) for x in buffer]}")
logger.warning(traceback.format_exc())
except Exception as e: except Exception as e:
logger.error("Error Accured, Packet will be skipped") logger.error("Error Accured, Packet will be skipped")
logger.error(f"Error processing message: {e}") logger.error(f"Error processing message: {e}")