13 Commits

Author SHA1 Message Date
echoDaveD
a010d874ac Create dependabot.yml 2025-03-15 20:28:39 +01:00
echoDaveD
f0222d750f 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
2025-03-13 19:57:33 +01:00
echoDaveD
43de00aacc fix struct 2025-02-27 22:44:35 +01:00
echoDaveD
0e3067dc26 Update dashboard.yaml 2025-02-27 22:08:05 +01:00
echoDaveD
86ef22006d Feature/v0.3.0 (#11)
v0.3.0 - 2025-02-27
Added poller functionality. EHS-Sentinel can now actively request values via Modbus

fetch_intervals and groups can be defined in the config file
default group and pollers are in the sampelconfig
[!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.

added a homeassistant dashboard.yaml with default Dashboard

edited Measurement

ENUM_IN_FSV_5061 add enums
ENUM_IN_FSV_5094 correct enum values
ENUM_IN_PV_CONTACT_STATE correct enum values
added units for multiple Measurements
Rename some Measurements:

NASA_INDOOR_COOL_MAX_SETTEMP_WATEROUT -> VAR_IN_FSV_1011
NASA_INDOOR_COOL_MIN_SETTEMP_WATEROUT -> VAR_IN_FSV_1012
NASA_INDOOR_COOL_MAX_SETTEMP_ROOM -> VAR_IN_FSV_1021
NASA_INDOOR_COOL_MIN_SETTEMP_ROOM -> VAR_IN_FSV_1022
NASA_INDOOR_HEAT_MAX_SETTEMP_WATEROUT -> VAR_IN_FSV_1031
NASA_INDOOR_HEAT_MIN_SETTEMP_WATEROUT -> VAR_IN_FSV_1032
NASA_INDOOR_HEAT_MAX_SETTEMP_ROOM -> VAR_IN_FSV_1041
NASA_INDOOR_HEAT_MIN_SETTEMP_ROOM -> VAR_IN_FSV_1042
NASA_DHW_MAX_SETTEMPLIMIT -> VAR_IN_FSV_1051
NASA_DHW_MIN_SETTEMPLIMIT -> VAR_IN_FSV_1052
NASA_USE_DHW_THERMOSTAT -> ENUM_IN_FSV_3061
NASA_USE_BOOSTER_HEATER -> ENUM_IN_FSV_3031
NASA_ENABLE_DHW -> ENUM_IN_FSV_3011
NASA_USE_THERMOSTAT!1 -> ENUM_IN_FSV_2091
NASA_USE_THERMOSTAT2 -> ENUM_IN_FSV_2092
Fixes #9
2025-02-27 22:04:27 +01:00
echoDaveD
1278f8e3e7 version vergessen 2025-02-25 00:07:14 +01:00
echoDaveD
e2685d76ba Feature/v0.2.2 (#10)
* init v.0.2.2

* v0.2.2 added support for rs485 to tcp adapter
2025-02-25 00:00:20 +01:00
echoDaveD
81ec2f755c limit NASA_EHSSENTINEL_COP and NASA_EHSSENTINEL_TOTAL_COP to values between 0 and 20 (#8) 2025-02-22 23:54:23 +01:00
echoDaveD
4272dc62fe Update README.md 2025-02-22 23:10:15 +01:00
echoDaveD
2bb38b9ccc Update README.md 2025-02-22 23:08:48 +01:00
echoDaveD
48ef003f22 Feature/v0.2.0 (#7)
* heateroutput limit 0 - 15000 w

* heatoutput always positive

* ENUM_IN_FSV_2041 value 0c00 unknwon added

* recalculate crc6 checksum and check it

* skip exception to reduce logging

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

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

* NASA_OUTDOOR_HP as kw unit

* NASA Repository, measurements enums completed

* filter wifikit heartbeat

* process only packets from indoor or outdoor

* correct readme

* remove expire

* device discovery status

* new mqtt hass configuration approach

* added new measurements

* added new logging features from config


* NASA_EHSSENTINEL_TOTAL_COP added

* removed silentMode, added logging proccessedMessage

* loaded devices

* loaded devices counter fix

* only if retain true

* final 0.2.0 commit
2025-02-22 22:45:18 +01:00
echoDaveD
cce625dabb Feature/0.1.0 releasebranch (#6)
* device_class: measurement for NASA_EHSSENTINEL_COP and NASA_EHSSENTINEL_HEAT_OUTPUT

* state_class: measurement for NASA_EHSSENTINEL_COP and NASA_EHSSENTINEL_HEAT_OUTPUT

* ENUM_IN_WATERPUMP_PWM_VALUE as var not enum
Unit % as  = measurement

* NASA_OUTDOOR_CONTROL_WATTMETER_ALL_UNIT_ACCUM state_class: total_increasing

* NASA_OUTDOOR_CONTROL_WATTMETER_ALL_UNIT_ACCUM device_class and unit

* NASA_OUTDOOR_CONTROL_WATTMETER_ALL_UNIT_ACCUM device_class energy

* fixing ValueError and better logging for determine_value try catch

* update reqierments and rreadme for venv

* ENUM_IN_FSV_2041 enum value fixed

* large buffer log

* prevent buffer overloading

* remove await

* a

* Feature/test without buffer (#5)

* test

* test

* test

* devision by 0 error fixed
remove task print

* logger

* topic clear only on online message

* expand logging

* reduce logging
2025-02-14 18:27:15 +01:00
echoDaveD
ef1e0a0f79 Feature/v0.0.2 (#4)
* device_class: measurement for NASA_EHSSENTINEL_COP and NASA_EHSSENTINEL_HEAT_OUTPUT

* state_class: measurement for NASA_EHSSENTINEL_COP and NASA_EHSSENTINEL_HEAT_OUTPUT

* ENUM_IN_WATERPUMP_PWM_VALUE as var not enum
Unit % as  = measurement

* NASA_OUTDOOR_CONTROL_WATTMETER_ALL_UNIT_ACCUM state_class: total_increasing

* NASA_OUTDOOR_CONTROL_WATTMETER_ALL_UNIT_ACCUM device_class and unit

* NASA_OUTDOOR_CONTROL_WATTMETER_ALL_UNIT_ACCUM device_class energy

* fixing ValueError and better logging for determine_value try catch

* update reqierments and rreadme for venv
2025-02-12 21:26:45 +01:00
30 changed files with 8875 additions and 4422 deletions

15
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,15 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: pip
directory: "/"
schedule:
interval: weekly
groups:
python-packages:
patterns:
- "*"

2
.gitignore vendored
View File

@@ -173,3 +173,5 @@ cython_debug/
prot.csv
helpertils/serial.py
helpertils/test.py
helpertils/socker.py
helpertils/messagesFound.txt

View File

@@ -30,29 +30,10 @@ class IndentFormatter(logging.Formatter):
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)
self.baseline = len(inspect.stack())
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)
formatter = logging.Formatter(log_fmt)
@@ -63,13 +44,6 @@ class IndentFormatter(logging.Formatter):
del rec.indent; del rec.function
return out
class MessageProcessorFilter(logging.Filter):
def filter(self, record):
# Suppress INFO level messages from MessageProcessor.py
if record.levelno == logging.INFO and record.pathname.endswith("MessageProcessor.py"):
return False
return True
# The following code sets up a custom logger with indentation support.
# It creates a custom formatter, a logger instance, and a stream handler.
# The custom formatter is set to the handler, which is then added to the logger.
@@ -83,23 +57,5 @@ logger.addHandler(handler)
logger.setLevel(logging.INFO)
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.debug("Debug mode is on...")
def setSilent():
"""
Sets the logger to silent mode, where only messages at the ERROR level or higher are displayed.
If the current logging level is not 'DEBUG', this function will log an informational message
indicating that silent mode is being activated and then set the logging level to ERROR.
"""
if logger.level != logging.DEBUG:
logger.info("Silent Mode is turning on, only Messages at Level ERROR or higher are displayed")
#logger.setLevel(logging.ERROR)
# Add the filter to suppress INFO level messages from MessageProcessor.py
logger.addFilter(MessageProcessorFilter())

View File

@@ -24,34 +24,12 @@ class EHSArguments:
_instance = None
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:
cls._instance = super(EHSArguments, cls).__new__(cls, *args, **kwargs)
cls._instance._initialized = False
return cls._instance
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:
return
self._initialized = True

View File

@@ -2,6 +2,7 @@ from EHSExceptions import ConfigException
from EHSArguments import EHSArguments
import yaml
import os
import re
from CustomLogger import logger
@@ -10,60 +11,25 @@ class EHSConfig():
Singleton class to handle the configuration for the EHS Sentinel application.
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.
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
MQTT = None
GENERAL = None
SERIAL = None
TCP = None
NASA_REPO = None
LOGGING = {}
POLLING = None
NASA_VAL_STORE = {}
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:
cls._instance = super(EHSConfig, cls).__new__(cls, *args, **kwargs)
cls._instance._initialized = False
return cls._instance
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:
return
self._initialized = True
@@ -76,40 +42,96 @@ class EHSConfig():
config = yaml.safe_load(file)
self.MQTT = config.get('mqtt')
self.GENERAL = config.get('general')
self.SERIAL = config.get('serial')
if 'tcp' in config:
self.TCP = config.get('tcp')
if 'serial' in config:
self.SERIAL = config.get('serial')
if 'logging' in config:
self.LOGGING = config.get('logging')
else:
self.LOGGING = {}
if 'polling' in config:
self.POLLING = config.get('polling')
logger.debug(f"Configuration loaded: {config}")
self.validate()
def parse_time_string(self, time_str: str) -> int:
match = re.match(r'^(\d+)([smh])$', time_str.strip(), re.IGNORECASE)
if not match:
raise ValueError("Invalid time format. Use '10s', '10m', or '10h'.")
value, unit = int(match.group(1)), match.group(2).lower()
conversion_factors = {
's': 1, # seconds
'm': 60, # minutes
'h': 3600 # hours
}
return value * conversion_factors[unit]
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']):
with open(self.GENERAL['nasaRepositoryFile'], mode='r') as file:
self.NASA_REPO = yaml.safe_load(file)
else:
raise ConfigException(argument=self.GENERAL['nasaRepositoryFile'], message="NASA Respository File is missing")
if 'silentMode' not in self.GENERAL:
self.GENERAL['silentMode'] = True
if 'protocolFile' not in self.GENERAL:
self.GENERAL['protocolFile'] = None
if 'device' not in self.SERIAL:
raise ConfigException(argument=self.SERIAL['device'], message="serial device config parameter is missing")
if 'baudrate' not in self.SERIAL:
raise ConfigException(argument=self.SERIAL['baudrate'], message="serial baudrate config parameter is missing")
if 'allowControl' not in self.GENERAL:
self.GENERAL['allowControl'] = False
if self.SERIAL is None and self.TCP is None:
raise ConfigException(argument="", message="define tcp or serial config parms")
if self.SERIAL is not None and self.TCP is not None:
raise ConfigException(argument="", message="you cannot define tcp and serial please define only one")
if self.SERIAL is not None:
if 'device' not in self.SERIAL:
raise ConfigException(argument=self.SERIAL['device'], message="serial device config parameter is missing")
if 'baudrate' not in self.SERIAL:
raise ConfigException(argument=self.SERIAL['baudrate'], message="serial baudrate config parameter is missing")
if self.TCP is not None:
if 'ip' not in self.TCP:
raise ConfigException(argument=self.TCP['ip'], message="tcp ip config parameter is missing")
if 'port' not in self.TCP:
raise ConfigException(argument=self.TCP['port'], message="tcp port config parameter is missing")
if self.POLLING is not None:
if 'fetch_interval' not in self.POLLING:
raise ConfigException(argument='', message="fetch_interval in polling parameter is missing")
if 'groups' not in self.POLLING:
raise ConfigException(argument='', message="groups in polling parameter is missing")
if 'fetch_interval' in self.POLLING and 'groups' in self.POLLING:
for poller in self.POLLING['fetch_interval']:
if poller['name'] not in self.POLLING['groups']:
raise ConfigException(argument=poller['name'], message="Groupname from fetch_interval not defined in groups: ")
if 'schedule' in poller:
try:
poller['schedule'] = self.parse_time_string(poller['schedule'])
except ValueError as e:
raise ConfigException(argument=poller['schedule'], message="schedule value from fetch_interval couldn't be validated, use format 10s, 10m or 10h")
for group in self.POLLING['groups']:
for ele in self.POLLING['groups'][group]:
if ele not in self.NASA_REPO:
raise ConfigException(argument=ele, message="Element from group not in NASA Repository")
if 'broker-url' not in self.MQTT:
raise ConfigException(argument=self.MQTT['broker-url'], message="mqtt broker-url config parameter is missing")
@@ -132,4 +154,30 @@ class EHSConfig():
raise ConfigException(argument=self.SERIAL['device'], message="mqtt user parameter is missing")
if 'password' not in self.MQTT and 'user' in self.MQTT:
raise ConfigException(argument=self.SERIAL['device'], message="mqtt password parameter is missing")
raise ConfigException(argument=self.SERIAL['device'], message="mqtt password parameter is missing")
if 'messageNotFound' not in self.LOGGING:
self.LOGGING['messageNotFound'] = False
if 'invalidPacket' not in self.LOGGING:
self.LOGGING['invalidPacket'] = False
if 'deviceAdded' not in self.LOGGING:
self.LOGGING['deviceAdded'] = True
if 'packetNotFromIndoorOutdoor' not in self.LOGGING:
self.LOGGING['packetNotFromIndoorOutdoor'] = False
if 'proccessedMessage' not in self.LOGGING:
self.LOGGING['proccessedMessage'] = False
if 'pollerMessage' not in self.LOGGING:
self.LOGGING['pollerMessage'] = False
if 'controlMessage' not in self.LOGGING:
self.LOGGING['controlMessage'] = False
logger.info(f"Logging Config:")
for key, value in self.LOGGING.items():
logger.info(f" {key}: {value}")

View File

@@ -46,8 +46,8 @@ class ArgumentException(EHSException):
def __str__(self):
return f'{self.argument} -> {self.message}'
class InvalidMessageTypeException(EHSException):
class SkipInvalidPacketException(EHSException):
"""Exception raised for invalid message types.
Attributes:
@@ -55,10 +55,9 @@ class InvalidMessageTypeException(EHSException):
message -- explanation of the error
"""
def __init__(self, message_type, message="Invalid message type provided"):
self.message_type = message_type
def __init__(self, message="Invalid message type provided"):
self.message = message
super().__init__(self.message)
def __str__(self):
return f'{self.message_type} -> {self.message}'
return f'{self.message}'

View File

@@ -10,28 +10,13 @@ import gmqtt
from CustomLogger import logger
from EHSArguments import EHSArguments
from EHSConfig import EHSConfig
from MessageProducer import MessageProducer
class MQTTClient:
"""
MQTTClient is a singleton class that manages the connection to an MQTT broker and handles
publishing and subscribing to topics. It is designed to work with Home Assistant for
auto-discovery of devices and sensors.
Attributes:
_instance (MQTTClient): The single instance of the MQTTClient class.
STOP (asyncio.Event): Event to signal stopping the MQTT client.
DEVICE_ID (str): The device ID used for MQTT topics.
config (EHSConfig): Configuration object for the MQTT client.
args (EHSArguments): Arguments object for the MQTT client.
broker (str): URL of the MQTT broker.
port (int): Port of the MQTT broker.
client_id (str): Client ID for the MQTT client.
client (gmqtt.Client): MQTT client instance.
topicPrefix (str): Prefix for MQTT topics.
homeAssistantAutoDiscoverTopic (str): Topic for Home Assistant auto-discovery.
useCamelCaseTopicNames (bool): Flag to use camel case for topic names.
known_topics (list): List to keep track of known topics.
known_devices_topic (str): Dedicated topic for storing known topics.
MQTTClient is a singleton class that manages the connection and communication with an MQTT broker.
It handles the initialization, connection, subscription, and message publishing for the MQTT client.
The class also supports Home Assistant auto-discovery and maintains a list of known devices.
"""
_instance = None
STOP = asyncio.Event()
@@ -39,19 +24,6 @@ class MQTTClient:
DEVICE_ID = "samsung_ehssentinel"
def __new__(cls, *args, **kwargs):
"""
Create a new instance of the MQTTClient class if one does not already exist.
This method ensures that the MQTTClient class follows the Singleton design pattern,
meaning only one instance of the class can exist at any given time. If an instance
already exists, it returns the existing instance. Otherwise, it creates a new instance
and sets the _initialized attribute to False.
Args:
cls (type): The class being instantiated.
*args: Variable length argument list.
**kwargs: Arbitrary keyword arguments.
Returns:
MQTTClient: The single instance of the MQTTClient class.
"""
if not cls._instance:
cls._instance = super(MQTTClient, cls).__new__(cls)
@@ -59,38 +31,17 @@ class MQTTClient:
return cls._instance
def __init__(self):
"""
Initialize the MQTTClient instance.
This constructor initializes the MQTT client with the configuration
provided by the EHSConfig and EHSArguments classes. It sets up the
MQTT broker connection details, client ID, and authentication credentials
if provided. It also assigns callback functions for various MQTT events
such as connect, disconnect, message, and subscribe. Additionally, it
initializes the topic prefix, Home Assistant auto-discover topic, and
topic naming convention.
Attributes:
config (EHSConfig): Configuration object for the MQTT client.
args (EHSArguments): Argument parser object for the MQTT client.
broker (str): URL of the MQTT broker.
port (int): Port number of the MQTT broker.
client_id (str): Client ID for the MQTT connection.
client (gmqtt.Client): gmqtt client instance.
topicPrefix (str): Prefix for MQTT topics.
homeAssistantAutoDiscoverTopic (str): Topic for Home Assistant auto-discovery.
useCamelCaseTopicNames (bool): Flag to use camel case for topic names.
known_topics (list): List to keep track of known topics.
known_devices_topic (str): Dedicated topic for storing known topics.
"""
if self._initialized:
return
self.config = EHSConfig()
self.args = EHSArguments()
self.message_producer = None
self._initialized = True
self.broker = self.config.MQTT['broker-url']
self.port = self.config.MQTT['broker-port']
self.client_id = self.config.MQTT['client-id']
self.client = gmqtt.Client(self.client_id, logger=logger)
self.client = gmqtt.Client(self.client_id)
self.client.on_connect = self.on_connect
self.client.on_disconnect = self.on_disconnect
self.client.on_message = self.on_message
@@ -106,19 +57,6 @@ class MQTTClient:
self.known_devices_topic = "known/devices" # Dedicated topic for storing known topics
async def connect(self):
"""
Asynchronously connects to the MQTT broker and optionally clears the known devices topic.
This method logs the connection attempt, connects to the MQTT broker using the specified
broker address and port, and sets the keepalive interval. If the CLEAN_KNOWN_DEVICES
argument is set to True, it publishes an empty message to the known devices topic to clear it.
Args:
None
Returns:
None
Raises:
Any exceptions raised by the underlying MQTT client library during connection.
"""
logger.info("[MQTT] Connecting to broker...")
await self.client.connect(self.broker, self.port, keepalive=60, version=gmqtt.constants.MQTTv311)
@@ -126,91 +64,51 @@ class MQTTClient:
self._publish(f"{self.topicPrefix.replace('/', '')}/{self.known_devices_topic}", " ", retain=True)
logger.info("Known Devices Topic have been cleared")
def subscribe_known_topics(self):
"""
Subscribes the MQTT client to known topics.
This method subscribes the MQTT client to two specific topics:
1. A topic for known devices, constructed using the topic prefix and known devices topic.
2. A status topic for Home Assistant auto-discovery.
The subscription QoS (Quality of Service) level for both topics is set to 1.
Logging:
Logs an informational message indicating that the client is subscribing to known devices topic.
Raises:
Any exceptions raised by the gmqtt.Subscription or self.client.subscribe methods.
"""
def subscribe_known_topics(self):
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.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):
"""
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')
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 of the message.
Behavior:
- If the topic matches the known devices topic, updates the known devices set with the retained message.
- If the topic matches the Home Assistant auto-discover status topic, logs the status message and clears the known devices topic.
"""
def on_message(self, client, topic, payload, qos, properties):
if self.known_devices_topic in topic:
# Update the known devices set with the retained message
self.known_topics = list(filter(None, [x.strip() for x in payload.decode().split(",")]))
if properties['retain'] == True:
if self.config.LOGGING['deviceAdded']:
logger.info(f"Loaded devices from known devices Topic:")
for idx, devname in enumerate(self.known_topics, start=1):
logger.info(f"Device no. {idx:<3}: {devname} ")
else:
logger.debug(f"Loaded devices from known devices Topic:")
for idx, devname in enumerate(self.known_topics):
logger.debug(f"Device added no. {idx:<3}: {devname} ")
if f"{self.homeAssistantAutoDiscoverTopic}/status" == topic:
logger.info(f"HASS Status Messages {topic} received: {payload.decode()}")
self._publish(f"{self.topicPrefix.replace('/', '')}/{self.known_devices_topic}", " ", retain=True)
logger.info("Known Devices Topic have been cleared")
def refresh_known_devices(self, devname):
"""
Refreshes the list of known devices by publishing the updated list to the MQTT topic.
Args:
devname (str): The name of the device to be refreshed.
Returns:
None
"""
self._publish(f"{self.topicPrefix.replace('/', '')}/{self.known_devices_topic}", ",".join(self.known_topics), retain=True)
if payload.decode() == "online":
self._publish(f"{self.topicPrefix.replace('/', '')}/{self.known_devices_topic}", " ", retain=True)
logger.info("Known Devices Topic have been cleared")
self.clear_hass()
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):
"""
Callback function for when the client receives a CONNACK response from the server.
Args:
client (paho.mqtt.client.Client): The client instance for this callback.
flags (dict): Response flags sent by the broker.
rc (int): The connection result.
properties (paho.mqtt.properties.Properties): The properties associated with the connection.
Returns:
None
Logs:
- Info: When connected successfully with result code 0.
- Error: When failed to connect with a non-zero result code.
"""
if rc == 0:
logger.info(f"Connected to MQTT with result code {rc}")
if len(self.homeAssistantAutoDiscoverTopic) > 0:
@@ -218,19 +116,7 @@ class MQTTClient:
else:
logger.error(f"Failed to connect, return code {rc}")
def on_disconnect(self, client, packet, exc=None):
"""
Callback function that is called when the client disconnects from the MQTT broker.
Args:
client (paho.mqtt.client.Client): The MQTT client instance that disconnected.
packet (paho.mqtt.client.MQTTMessage): The MQTT message packet received during disconnection.
exc (Exception, optional): The exception that caused the disconnection, if any. Defaults to None.
Logs:
Logs an info message indicating disconnection.
Logs a warning message indicating an unexpected disconnection and attempts to reconnect.
Logs an error message if reconnection fails and retries every 5 seconds.
"""
def on_disconnect(self, client, packet, exc=None):
logger.info(f"Disconnected with result code ")
logger.warning("Unexpected disconnection. Reconnecting...")
while True:
@@ -241,48 +127,33 @@ class MQTTClient:
logger.error(f"Reconnection failed: {e}")
time.sleep(5)
def _publish(self, topic, payload, qos=0, retain=False):
"""
Publish a message to a specified MQTT topic.
Args:
topic (str): The MQTT topic to publish to.
payload (str): The message payload to publish.
qos (int, optional): The Quality of Service level for message delivery. Defaults to 0.
retain (bool, optional): If True, the message will be retained by the broker. Defaults to False.
Returns:
None
"""
def _publish(self, topic, payload, qos=0, retain=False):
logger.debug(f"MQTT Publish Topic: {topic} payload: {payload}")
self.client.publish(f"{topic}", payload, qos, retain)
#time.sleep(0.1)
def refresh_known_devices(self, devname):
self.known_topics.append(devname)
if self.config.LOGGING['deviceAdded']:
logger.info(f"Device added no. {len(self.known_topics):<3}: {devname} ")
else:
logger.debug(f"Device added no. {len(self.known_topics):<3}: {devname} ")
self._publish(f"{self.topicPrefix.replace('/', '')}/{self.known_devices_topic}", ",".join(self.known_topics), retain=True)
def publish_message(self, name, value):
"""
Publishes a message to an MQTT topic.
This method normalizes the given name, determines the appropriate MQTT topic,
and publishes the provided value to that topic. If Home Assistant auto-discovery
is enabled, it will also handle the auto-discovery configuration.
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:
KeyError: If the name is not found in the NASA_REPO configuration.
"""
async def publish_message(self, name, value):
newname = f"{self._normalize_name(name)}"
if len(self.homeAssistantAutoDiscoverTopic) > 0:
sensor_type = "sensor"
if 'enum' in self.config.NASA_REPO[name]:
enum = [*self.config.NASA_REPO[name]['enum'].values()]
if all([en.lower() in ['on', 'off'] for en in enum]):
sensor_type = "binary_sensor"
topicname = f"{self.config.MQTT['homeAssistantAutoDiscoverTopic']}/{sensor_type}/{self.DEVICE_ID}_{newname.lower()}/state"
if name not in self.known_topics:
self.auto_discover_hass(topicname, name, newname, sensor_type)
if name not in self.known_topics:
self.auto_discover_hass(name)
self.refresh_known_devices(name)
if self.config.NASA_REPO[name]['hass_opts']['writable']:
sensor_type = self.config.NASA_REPO[name]['hass_opts']['platform']['type']
else:
sensor_type = self.config.NASA_REPO[name]['hass_opts']['default_platform']
topicname = f"{self.config.MQTT['homeAssistantAutoDiscoverTopic']}/{sensor_type}/{self.DEVICE_ID}_{newname.lower()}/state"
else:
topicname = f"{self.topicPrefix.replace('/', '')}/{newname}"
@@ -291,78 +162,24 @@ class MQTTClient:
self._publish(topicname, value, qos=2, retain=False)
def auto_discover_hass(self, topicname, nameraw, namenorm, sensor_type):
"""
Automatically discovers and configures Home Assistant entities for the MQTT client.
This function creates and publishes a configuration payload for Home Assistant's MQTT discovery.
It supports both sensor and binary sensor types, and sets appropriate attributes based on the
provided sensor type and unit of measurement.
Args:
topicname (str): The MQTT topic name.
nameraw (str): The raw name of the sensor.
namenorm (str): The normalized name of the sensor.
sensor_type (str): The type of the sensor (e.g., "sensor" or "binary_sensor").
Returns:
None
"""
def clear_hass(self):
entities = {}
for nasa in self.config.NASA_REPO:
namenorm = self._normalize_name(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}
entity = { namenorm: {
"name": f"{namenorm}",
"object_id": f"{self.DEVICE_ID}_{namenorm.lower()}",
"unique_id": f"{self.DEVICE_ID}_{nameraw.lower()}",
"platform": sensor_type,
"value_template": "{{ value }}",
"state_topic": f"{self.config.MQTT['homeAssistantAutoDiscoverTopic']}/{sensor_type}/{self.DEVICE_ID}_{namenorm.lower()}/state",
}
}
if sensor_type == "sensor":
if len(self.config.NASA_REPO[nameraw]['unit']) > 0:
entity[namenorm]['unit_of_measurement'] = self.config.NASA_REPO[nameraw]['unit']
if entity[namenorm]['unit_of_measurement'] == "\u00b0C":
entity[namenorm]['device_class'] = "temperature"
elif entity[namenorm]['unit_of_measurement'] == '%':
entity[namenorm]['device_class'] = "power_factor"
elif entity[namenorm]['unit_of_measurement'] == 'kW':
entity[namenorm]['device_class'] = "power"
elif entity[namenorm]['unit_of_measurement'] == 'rpm':
entity[namenorm]['state_class'] = "measurement"
elif entity[namenorm]['unit_of_measurement'] == 'bar':
entity[namenorm]['device_class'] = "pressure"
elif entity[namenorm]['unit_of_measurement'] == 'HP':
entity[namenorm]['device_class'] = "power"
elif entity[namenorm]['unit_of_measurement'] == 'hz':
entity[namenorm]['device_class'] = "frequency"
else:
entity[namenorm]['device_class'] = None
else:
entity[namenorm]['payload_on'] = "ON"
entity[namenorm]['payload_off'] = "OFF"
if 'state_class' in self.config.NASA_REPO[nameraw]:
entity[namenorm]['state_class'] = self.config.NASA_REPO[nameraw]['state_class']
if 'device_class' in self.config.NASA_REPO[nameraw]:
entity[namenorm]['device_class'] = self.config.NASA_REPO[nameraw]['device_class']
device = {
"device": {
"identifiers": self.DEVICE_ID,
"name": "Samsung EHS",
"manufacturer": "Samsung",
"model": "Mono HQ Quiet",
"sw_version": "1.0.0"
},
"origin": {
"name": "EHS-Sentinel",
"support_url": "https://github.com/echoDaveD/EHS-Sentinel"
},
"components": entity,
"device": self._get_device(),
"origin": self._get_origin(),
"components": entities,
"qos": 2
}
logger.debug(f"Auto Discovery HomeAssistant Message: ")
logger.debug(f"Auto Discovery HomeAssistant Clear Message: ")
logger.debug(f"{device}")
self._publish(f"{self.config.MQTT['homeAssistantAutoDiscoverTopic']}/device/{self.DEVICE_ID}/config",
@@ -370,23 +187,80 @@ class MQTTClient:
qos=2,
retain=True)
self.known_topics.append(nameraw)
self.refresh_known_devices(nameraw)
def auto_discover_hass(self, name):
entity = {}
namenorm = self._normalize_name(name)
entity = {
"name": f"{namenorm}",
"object_id": f"{self.DEVICE_ID}_{namenorm.lower()}",
"unique_id": f"{self.DEVICE_ID}_{name.lower()}",
"force_update": True,
#"expire_after": 86400, # 1 day (24h * 60m * 60s)
"value_template": "{{ value }}"
#"value_template": "{{ value if value | length > 0 else 'unavailable' }}",
}
if self.config.NASA_REPO[name]['hass_opts']['writable'] and self.config.GENERAL['allowControl']:
sensor_type = self.config.NASA_REPO[name]['hass_opts']['platform']['type']
if sensor_type == 'select':
entity['options'] = self.config.NASA_REPO[name]['hass_opts']['platform']['options']
if sensor_type == 'number':
entity['mode'] = self.config.NASA_REPO[name]['hass_opts']['platform']['mode']
entity['min'] = self.config.NASA_REPO[name]['hass_opts']['platform']['min']
entity['max'] = self.config.NASA_REPO[name]['hass_opts']['platform']['max']
if 'step' in self.config.NASA_REPO[name]['hass_opts']['platform']:
entity['step'] = self.config.NASA_REPO[name]['hass_opts']['platform']['step']
entity['command_topic'] = f"{self.topicPrefix.replace('/', '')}/entity/{name}/set"
entity['optimistic'] = False
else:
sensor_type = self.config.NASA_REPO[name]['hass_opts']['default_platform']
if 'unit' in self.config.NASA_REPO[name]['hass_opts']:
entity['unit_of_measurement'] = self.config.NASA_REPO[name]['hass_opts']['unit']
entity['platform'] = sensor_type
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": self._get_device(),
"origin": self._get_origin(),
"qos": 2
}
device.update(entity)
logger.debug(f"Auto Discovery HomeAssistant Message: ")
logger.debug(f"{device}")
self._publish(f"{self.config.MQTT['homeAssistantAutoDiscoverTopic']}/{sensor_type}/{self.DEVICE_ID}_{name.lower()}/config",
json.dumps(device, ensure_ascii=False),
qos=2,
retain=True)
def _get_device(self):
return {
"identifiers": self.DEVICE_ID,
"name": "Samsung EHS",
"manufacturer": "Samsung",
"model": "Mono HQ Quiet",
"sw_version": "1.0.0"
}
def _get_origin(self):
return {
"name": "EHS-Sentinel",
"support_url": "https://github.com/echoDaveD/EHS-Sentinel"
}
def _normalize_name(self, name):
"""
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:
prefix_to_remove = ['ENUM_', 'LVAR_', 'NASA_', 'VAR_']
# remove unnecessary prefixes of name

View File

@@ -2,7 +2,7 @@ import asyncio
import logging
import traceback
import yaml
from CustomLogger import logger, setSilent
from CustomLogger import logger
from EHSArguments import EHSArguments
from EHSConfig import EHSConfig
from EHSExceptions import MessageWarningException
@@ -14,158 +14,133 @@ from NASAPacket import NASAPacket
class MessageProcessor:
"""
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
messages, extract submessages, search for message definitions in a configuration repository, and determine the
value of message payloads based on predefined rules. It also includes logging for debugging and tracing the
The class provides methods to process messages, extract submessages, search for message definitions in a configuration repository,
and determine the value of message payloads based on predefined rules. It also includes logging for debugging and tracing the
message processing steps.
"""
_instance = None
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):
"""
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
logger.debug("init MessageProcessor")
self.config = EHSConfig()
self.args = EHSArguments()
self.mqtt = MQTTClient()
self.NASA_VAL_STORE = {}
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.
"""
async def process_message(self, packet: NASAPacket):
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)
if msgname is not None:
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:
raise MessageWarningException(argument=msg['payload'], message=f"Value of {hexmsg:<6} couldn't be determinate, skip Message {e}")
self.protocolMessage(msg, msgname, msgvalue)
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}")
await self.protocolMessage(msg, msgname, msgvalue)
else:
logger.debug(f"Message not Found in NASA repository: {hexmsg:<6} Type: {msg.packet_message_type} Payload: {msg.packet_payload}")
packedval = int.from_bytes(msg.packet_payload, byteorder='big', signed=True)
if self.config.LOGGING['messageNotFound']:
logger.info(f"Message not Found in NASA repository: {hexmsg:<6} Type: {msg.packet_message_type} Payload: {msg.packet_payload} = {packedval}")
else:
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):
"""
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.
"""
async def protocolMessage(self, msg: NASAMessage, msgname, msgvalue):
logger.info(f"Message number: {hex(msg.packet_message):<6} {msgname:<50} Type: {msg.packet_message_type} Payload: {msgvalue}")
if self.config.LOGGING['proccessedMessage']:
logger.info(f"Message number: {hex(msg.packet_message):<6} {msgname:<50} Type: {msg.packet_message_type} Payload: {msgvalue} ({msg.packet_payload})")
else:
logger.debug(f"Message number: {hex(msg.packet_message):<6} {msgname:<50} Type: {msg.packet_message_type} Payload: {msgvalue}")
if self.config.GENERAL['protocolFile'] is not None:
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")
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 all(k in self.NASA_VAL_STORE for k in ['NASA_OUTDOOR_TW2_TEMP', 'NASA_OUTDOOR_TW1_TEMP', 'VAR_IN_FLOW_SENSOR_CALC']):
self.protocolMessage(NASAMessage(packet_message=0x9999, packet_message_type=1),
"NASA_EHSSENTINEL_HEAT_OUTPUT",
round(
(
(self.NASA_VAL_STORE['NASA_OUTDOOR_TW2_TEMP'] - self.NASA_VAL_STORE['NASA_OUTDOOR_TW1_TEMP']) *
(self.NASA_VAL_STORE['VAR_IN_FLOW_SENSOR_CALC']/60)
* 4190
), 4))
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(
abs(
(self.config.NASA_VAL_STORE['NASA_OUTDOOR_TW2_TEMP'] - self.config.NASA_VAL_STORE['NASA_OUTDOOR_TW1_TEMP']) *
(self.config.NASA_VAL_STORE['VAR_IN_FLOW_SENSOR_CALC']/60)
* 4190
) , 4
)
if (value < 15000 and value > 0): # only if heater output between 0 und 15000 W
await self.protocolMessage(NASAMessage(packet_message=0x9999, packet_message_type=1, packet_payload=[0]),
"NASA_EHSSENTINEL_HEAT_OUTPUT",
value
)
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']):
self.protocolMessage(NASAMessage(packet_message=0x9998, packet_message_type=1),
"NASA_EHSSENTINEL_COP",
round((self.NASA_VAL_STORE['NASA_EHSSENTINEL_HEAT_OUTPUT'] / self.NASA_VAL_STORE['NASA_OUTDOOR_CONTROL_WATTMETER_ALL_UNIT']/1000.), 3))
if all(k in self.config.NASA_VAL_STORE for k in ['NASA_EHSSENTINEL_HEAT_OUTPUT', 'NASA_OUTDOOR_CONTROL_WATTMETER_ALL_UNIT']):
if (self.config.NASA_VAL_STORE['NASA_OUTDOOR_CONTROL_WATTMETER_ALL_UNIT'] > 0):
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):
await self.protocolMessage(NASAMessage(packet_message=0x9998, packet_message_type=1, packet_payload=[0]),
"NASA_EHSSENTINEL_COP",
value
)
if msgname 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.config.NASA_VAL_STORE['NASA_OUTDOOR_CONTROL_WATTMETER_ALL_UNIT_ACCUM'] > 0):
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):
await self.protocolMessage(NASAMessage(packet_message=0x9997, packet_message_type=1, packet_payload=[0]),
"NASA_EHSSENTINEL_TOTAL_COP",
value
)
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():
if value['address'].lower() == address:
return key
def determine_value(self, rawvalue, msgname):
"""
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')
def is_valid_rawvalue(self, rawvalue: bytes) -> bool:
return all(0x20 <= b <= 0x7E or b in (0x00, 0xFF) for b in rawvalue)
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:
try:
value = eval(arithmetic)
except Exception as e:
logger.warning(f"Arithmetic Function couldn't been applied, using raw value: arithmetic = {arithmetic} {e}")
value = packed_value
else:
value = packed_value
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()
if self.is_valid_rawvalue(rawvalue[1:-1]):
for byte in rawvalue[1:-1]:
if byte != 0x00 and byte != 0xFF:
char = chr(byte) if 32 <= byte <= 126 else f"{byte}"
value += char
else:
value += " "
value = value.strip()
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:
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)
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

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,58 +2,69 @@
class NASAMessage:
"""
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]):
"""
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.
"""
def __init__(self, packet_message=0x000, packet_message_type=0, packet_payload=[0]):
self.packet_message: int = packet_message
self.packet_message_type: int = packet_message_type
self.packet_payload: bytes = bytes([int(hex(x), 16) for x in packet_payload])
def set_packet_message(self, value: int):
self.packet_message = value
self.packet_message_type = (value & 1536) >> 9
def set_packet_message_type(self, value: int):
self.packet_message_type = value
def set_packet_payload(self, value: list):
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:
message_number_reconstructed = (self.packet_message_type << 9) | (self.packet_message & 0x1FF)
# Extract the original bytes from message_number
msg_rest_0 = (self.packet_message >> 8) & 0xFF # Upper 8 bits
msg_rest_1 = self.packet_message & 0xFF # Lower 8 bits
msgpayload = int.from_bytes(self.packet_payload, byteorder='big', signed=True)
if self.packet_message_type == 0:
return [
msg_rest_0,
msg_rest_1,
msgpayload & 0xFF
]
elif self.packet_message_type == 1:
return [
msg_rest_0,
msg_rest_1,
(msgpayload >> 8) & 0xFF,
msgpayload & 0xFF
]
elif self.packet_message_type == 2:
return [
msg_rest_0,
msg_rest_1,
(msgpayload >> 24) & 0xFF,
(msgpayload >> 16) & 0xFF,
(msgpayload >> 8) & 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):
return (
f"NASAMessage(\n"
f" packet_message={self.packet_message} ({hex(self.packet_message)}),\n"
f" packet_message={self.packet_message} ({hex(self.packet_message)}) ({[x for x in bytearray(self.packet_message.to_bytes(2))]}),\n"
f" packet_message_type={self.packet_message_type} ({hex(self.packet_message_type)}),\n"
f" packet_payload={self.packet_payload} ({self.packet_payload.hex()})\n"
f" packet_payload={self.packet_payload} ({self.packet_payload.hex()}) ({[int(x) for x in self.packet_payload]})\n"
f")"
)

View File

@@ -1,37 +1,13 @@
from enum import Enum
from NASAMessage import NASAMessage
from EHSExceptions import SkipInvalidPacketException
import binascii
import struct
from CustomLogger import logger
class AddressClassEnum(Enum):
"""
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
@@ -65,12 +41,6 @@ class AddressClassEnum(Enum):
class PacketType(Enum):
"""
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
@@ -82,15 +52,6 @@ class PacketType(Enum):
class DataType(Enum):
"""
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
@@ -105,56 +66,6 @@ class DataType(Enum):
class NASAPacket:
"""
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):
@@ -179,43 +90,30 @@ class NASAPacket:
self.packet_end: int = None
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
if len(packet) < 14:
raise ValueError("Data too short to be a valid NASAPacket")
crc_checkusm=binascii.crc_hqx(bytearray(packet[3:-3]), 0)
self.packet_start = packet[0]
self.packet_size = ((packet[1] << 8) | packet[2])
self.packet_source_address_class = AddressClassEnum(packet[3])
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:
self.packet_source_address_class = AddressClassEnum(packet[3])
except ValueError as e:
raise SkipInvalidPacketException(f"Source Adress Class out of enum {packet[3]}")
self.packet_source_channel = packet[4]
self.packet_source_address = packet[5]
self.packet_dest_address_class = AddressClassEnum(packet[6])
try:
self.packet_dest_address_class = AddressClassEnum(packet[6])
except ValueError as e:
raise SkipInvalidPacketException(f"Destination Adress Class out of enum {packet[6]}")
self.packet_dest_channel = packet[7]
self.packet_dest_address = packet[8]
self.packet_information = (int(packet[9]) & 128) >> 7 == 1
@@ -225,25 +123,14 @@ class NASAPacket:
self.packet_data_type = DataType(int(packet[10]) & 15)
self.packet_number = packet[11]
self.packet_capacity = packet[12]
self.packet_crc16 = ((packet[-3] << 8) | packet[-2]) + 2
self.packet_crc16 = ((packet[-3] << 8) | packet[-2]) # + 2
self.packet_end = packet[-1]
self.packet_messages = self._extract_messages(0, self.packet_capacity, packet[13:-3], [])
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 crc_checkusm != self.packet_crc16:
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):
if depth > capacity or len(msg_rest) <= 2:
return return_list
@@ -259,9 +146,9 @@ class NASAPacket:
elif message_type == 3:
payload_size = len(msg_rest)
if capacity != 1:
raise ValueError(message="Message with structure type must have capacity of 1.")
raise SkipInvalidPacketException("Message with structure type must have capacity of 1.")
else:
raise ValueError(message=f"Mssage type unknown: {message_type}")
raise ValueError(f"Mssage type unknown: {message_type}")
payload = msg_rest[2:2 + payload_size]
if len(payload) > 255:
@@ -302,6 +189,78 @@ class NASAPacket:
def __repr__(self):
return self.__str__()
# Setter methods
def set_packet_source_address_class(self, value: AddressClassEnum):
self.packet_source_address_class = value
def set_packet_source_channel(self, value: int):
self.packet_source_channel = value
def set_packet_source_address(self, value: int):
self.packet_source_address = value
def set_packet_dest_address_class(self, value: AddressClassEnum):
self.packet_dest_address_class = value
def set_packet_dest_channel(self, value: int):
self.packet_dest_channel = value
def set_packet_dest_address(self, value: int):
self.packet_dest_address = value
def set_packet_information(self, value: bool):
self.packet_information = value
def set_packet_version(self, value: int):
self.packet_version = value
def set_packet_retry_count(self, value: int):
self.packet_retry_count = value
def set_packet_type(self, value: PacketType):
self.packet_type = value
def set_packet_data_type(self, value: DataType):
self.packet_data_type = value
def set_packet_number(self, value: int):
self.packet_number = value
def set_packet_messages(self, value: list[NASAMessage]):
self.packet_messages = value
def to_raw(self) -> bytearray:
self.packet_start = 50
self.packet_end = 52
packet = bytearray()
packet.append(int(self.packet_start))
packet.append(0)
packet.append(0)
packet.append(self.packet_source_address_class.value)
packet.append(self.packet_source_channel)
packet.append(self.packet_source_address)
packet.append(self.packet_dest_address_class.value)
packet.append(self.packet_dest_channel)
packet.append(self.packet_dest_address)
packet.append((self.packet_information << 7) | (self.packet_version << 5) | (self.packet_retry_count << 3))
packet.append((self.packet_type.value << 4) | self.packet_data_type.value)
packet.append(self.packet_number)
packet.append(len(self.packet_messages))
# Add messages to the packet
for msg in self.packet_messages:
for msg_pack in msg.to_raw():
packet.append(msg_pack)
self.packet_capacity = len(self.packet_messages)
self.packet_size = len(packet)+2+2
packet[1] = (self.packet_size >> 8) & 0xFF
packet[2] = self.packet_size & 0xFF
self.packet_crc16=binascii.crc_hqx(packet[3:], 0)
final_packet = struct.pack(">BH", packet[0], len(packet[1:])+2) + packet[3:] + struct.pack(">HB", self.packet_crc16, 0x34)
return final_packet
# Example usage:
# packet = NASAPacket()
# packet.parse(bytearray([0x01, 0x02, 0x03, 0x04, 0x05, 0x06]))

386
README.md
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".
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
## Simple
@@ -23,7 +32,9 @@ EHS-Sentinel subscribes <hass_discovery_prefix>/status Topic and if it receive a
`pip install -r requirements.txt`
3. Copy the `data/config.yml` and provide your Configuration
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
@@ -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
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:
`nasaRepositoryFile: /root/EHS-Sentinel/data/NasaRepository.yml`
@@ -60,8 +73,95 @@ EHS-Sentinel subscribes <hass_discovery_prefix>/status Topic and if it receive a
`journalctl | grep ehsSentinel`
# Configuration
## Venv Installation (recommendet)
In general, it is recommended to work with a virtual environment (venvs) in python to be independent of other projects.
Some Distributions like debian 12 dont allow to use system wide pip package installation, so you have to use venv.
1. Just clone the repository
`git clone https://github.com/echoDaveD/EHS-Sentinel`
2. Install python venv
`apt install python3.11-venv` <- modify your python verison here
3. Create Python venv
`python3 -m venv EHS-Sentinel`
4. change diractory
`cd EHS-Sentinel`
5. activate venv
`source bin/activate`
6. Install the requierments
`pip install -r requirements.txt`
7. Copy the `data/config.yml` and provide your Configuration
8. get path of venv python executable
`which python3` <- copy the output
9. Change to ehs-sentinel.service file as followed:
`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 --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:
`nasaRepositoryFile: /root/EHS-Sentinel/data/NasaRepository.yml`
11. Deactivate venv
`dactivate`
12. Copy the service File to your systemd folder:
`cp ehs-sentinel.service /etc/systemd/system`
13. Enable the new service
`systemctl enable ehs-sentinel`
14. Reload deamon
`systemctl daemon-reload`
15. Start the Service
`systemctl start ehs-sentinel`
16. check if anything is fine
`systemctl status ehs-sentinel`
17. If your want to check the journal logs
`journalctl | grep ehsSentinel`
# Home Assistant Dashboard
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!
## Read Only Mode
![alt text](ressources/images/dashboard1.png)
![alt text](ressources/images/dashboard2.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
## Command-Line Arguments
@@ -110,18 +210,52 @@ The `config.yml` file contains configuration settings for the EHS-Sentinel proje
- **nasaRepositoryFile**: Path to the NASA repository file.
- Default: `data/NasaRepository.yml`
- **silentMode**: Boolean flag to enable or disable silent mode. In Silent Mode only Logmessages above WARNING are printed out (for production use to not spam your systemlog)
- Default: `True`
- **protocolFile**: Path to the protocol file. (not set in Sample config.yml)
- 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
- **deviceAdded**: Set to true will log when new device is added to known Devices (and discover to HASS).
- Default: `True`
- **messageNotFound**: Set to true will log when a received message was not found in NasaRepository
- Default: `False`
- **packetNotFromIndoorOutdoor**: Set to true will log when a message not from Indoor/Outdoor unit was received
- Default: `False`
- **proccessedMessage**: set to true, prints out a summary of which massage was processed and its value
- Default: `False`
- **pollerMessage**: set to true, prints out detailed poller NASAPackets
- 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
cannot be defined with TCP parm...
- **device**: The serial device URL.
- Example: `/dev/ttyUSB0`
- **baudrate**: The baud rate for the serial connection.
- Example: `9600`
### TCP Settings
cannot be defined with SERIAL parm...
- **ip**: The ip of rs485 to ETH Adapter.
- Example: `168.192.2.200`
- **port**: The port of rs485 to ETH Adapter.
- Example: `4196`
### MQTT Broker Settings
- **broker-url**: The URL of the MQTT broker.
@@ -141,16 +275,56 @@ The `config.yml` file contains configuration settings for the EHS-Sentinel proje
- **topicPrefix**: The prefix to use for MQTT topics. (Is used when homeassistant is not set or empty)
- Example: `ehsSentinel`
### Poller Configuration
> [!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.
Experience has shown that the write function (required for poller) only works with a rts486 to ETH adapter, with a USB adapter no value could be written successfully so far.
With the Poller Configuration, values can be actively polled cyclically from the Samsung. All FSV values are already predefined in the sample Config. The pollers only need to be enabled.
The data points are defined in the groups section, the group is then enabled in the fetch_interval and the schedule is entered (10h, 10m, 10s are valid units).
- **fetch_interval**: The ip of rs485 to ETH Adapter.
- Example: `168.192.2.200`
***name***: Name of the Group from groups section
- Example: `fsv10xx`
***enabled***: True or False, true to enable this poller
- Example: `True`
***schedule***: Time of often teh Values should be polled, be carefully do not poll to often. Valid units are `h` for hours, `m` for minutes and `s` for seconds
- Example: `10h`
- **groups**: A list of groups, the with the Measurements to be polled, name can be freely assigned.
- Example: `fsv10xx`
***fsv10xx***: A list wiht Measurements name, can be taken from the NASARepository
### Example Configuration
```yaml
general:
nasaRepositoryFile: data/NasaRepository.yml
silentMode: False
protocolFile: prot.csv
serial:
device: /dev/ttyUSB0
baudrate: 9600
allowControl: False
# protocolFile: prot.csv
logging:
deviceAdded: True
messageNotFound: False
packetNotFromIndoorOutdoor: False
proccessedMessage: False
pollerMessage: False
#serial:
# device: /dev/ttyUSB0
# baudrate: 9600
tcp:
ip: 168.192.2.200
port: 4196
mqtt:
broker-url: 123.45.6.69
broker-port: 1883
@@ -160,6 +334,15 @@ mqtt:
homeAssistantAutoDiscoverTopic: "homeassistant"
useCamelCaseTopicNames: True
topicPrefix: ehsSentinel
polling:
fetch_interval:
- name: fsv10xx
enable: false
schedule: 30m
groups:
fsv10xx:
- VAR_IN_FSV_1011
- VAR_IN_FSV_1012
```
# Debugging
@@ -195,5 +378,186 @@ if you want to see how many uniquie Messages have been collected in the Dumpfile
# 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
- Added poller functionality. EHS-Sentinel can now actively request values via Modbus
- fetch_intervals and groups can be defined in the config file
- default group and pollers are in the sampelconfig
> [!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.
- added a homeassistant dashboard.yaml with default Dashboard
- edited Measurement
- ENUM_IN_FSV_5061 add enums
- ENUM_IN_FSV_5094 correct enum values
- ENUM_IN_PV_CONTACT_STATE correct enum values
- added units for multiple Measurements
- Rename some Measurements:
- NASA_INDOOR_COOL_MAX_SETTEMP_WATEROUT -> VAR_IN_FSV_1011
- NASA_INDOOR_COOL_MIN_SETTEMP_WATEROUT -> VAR_IN_FSV_1012
- NASA_INDOOR_COOL_MAX_SETTEMP_ROOM -> VAR_IN_FSV_1021
- NASA_INDOOR_COOL_MIN_SETTEMP_ROOM -> VAR_IN_FSV_1022
- NASA_INDOOR_HEAT_MAX_SETTEMP_WATEROUT -> VAR_IN_FSV_1031
- NASA_INDOOR_HEAT_MIN_SETTEMP_WATEROUT -> VAR_IN_FSV_1032
- NASA_INDOOR_HEAT_MAX_SETTEMP_ROOM -> VAR_IN_FSV_1041
- NASA_INDOOR_HEAT_MIN_SETTEMP_ROOM -> VAR_IN_FSV_1042
- NASA_DHW_MAX_SETTEMPLIMIT -> VAR_IN_FSV_1051
- NASA_DHW_MIN_SETTEMPLIMIT -> VAR_IN_FSV_1052
- NASA_USE_DHW_THERMOSTAT -> ENUM_IN_FSV_3061
- NASA_USE_BOOSTER_HEATER -> ENUM_IN_FSV_3031
- NASA_ENABLE_DHW -> ENUM_IN_FSV_3011
- NASA_USE_THERMOSTAT!1 -> ENUM_IN_FSV_2091
- NASA_USE_THERMOSTAT2 -> ENUM_IN_FSV_2092
### v0.2.2 - 2025-02-24
- Support for rs485 to ETH Adapter, tcp options instead of serial port are possible now
### v0.2.1 - 2025-02-22
- limit NASA_EHSSENTINEL_COP and NASA_EHSSENTINEL_TOTAL_COP to values between 0 and 20
### v0.2.0 - 2025-02-22
- improved MQTT Auto Discovery Config Messages
- NASAPacket and NASAMessage are now bidirectional, can decode and encode Packets
- Improved data quality
- Added crc16 Checksum check for any Packet to reduce incorrect value changes
- Only Packets from outdoor/Indoor Units are processed
- Following warnings moved to SkipInvalidPacketException and from warning to debug log level to reduce log entries
- Source Adress Class out of enum
- Destination Adress Class out of enum
- Checksum for package could not be validatet calculated
- Message with structure type must have capacity of 1.
- removed silentNode config property (new logging section is replacing its functionality but more granular)
- added new logging config property to allow to turn on/off additional info log entries
- deviceAdded set to true (default) will log when new device is added to known Devices (and discover to HASS)
- messageNotFound set to true (false is default) will log when a received message was not found in NasaRepository
- packetNotFromIndoorOutdoor set to true (false is default) will log when a message not from Indoor/Outdoor unit was received
- proccessedMessage set to true(false is default) prints out a summary of which massage was processed and its value
- NASA_EHSSENTINEL_HEAT_OUTPUT limited to values between 0 and 15000 to reduce false values between temp statsu changes
- Added new Measurements
- 0x4423 LVAR_IN_MINUTES_SINCE_INSTALLATION
- 0x4424 LVAR_IN_MINUTES_ACTIVE
- 0x4426 LVAR_IN_GENERATED_POWER_LAST_MINUTE
- 0x4427 LVAR_IN_TOTAL_GENERATED_POWER
- 0x0997 NASA_EHSSENTINEL_TOTAL_COP = LVAR_IN_TOTAL_GENERATED_POWER / NASA_OUTDOOR_CONTROL_WATTMETER_ALL_UNIT_ACCUM
- NASA Repository, measurements enums completed
- ENUM_IN_FSV_3041: enum edited
- ENUM_IN_FSV_3071: enum edited
- ENUM_IN_FSV_4021: enum edited
- ENUM_IN_FSV_4041: enum edited
- ENUM_IN_FSV_4051: enum edited
- ENUM_IN_FSV_4053: enum edited
- ENUM_IN_FSV_5022: enum edited
- ENUM_IN_FSV_5042: enum edited
- ENUM_IN_FSV_5081: enum edited
- ENUM_IN_FSV_5091: enum edited
- ENUM_IN_FSV_2093: enum edited
- ENUM_IN_FSV_2094: enum edited
- VAR_IN_FSV_2011: desc edited
- VAR_IN_FSV_2012: desc edited
- VAR_IN_FSV_2021: desc edited
- VAR_IN_FSV_2022: desc edited
- VAR_IN_FSV_2031: desc edited
- VAR_IN_FSV_2032: desc edited
- ENUM_IN_FSV_2041: desc edited
- VAR_IN_FSV_2051: desc edited
- VAR_IN_FSV_2052: desc edited
- VAR_IN_FSV_2061: desc edited
- VAR_IN_FSV_2062: desc edited
- VAR_IN_FSV_2071: desc edited
- VAR_IN_FSV_2072: desc edited
- ENUM_IN_FSV_2093: desc edited
- VAR_IN_FSV_3021: desc edited
- VAR_IN_FSV_3022: desc edited
- VAR_IN_FSV_3023: desc edited
- VAR_IN_FSV_3024: desc edited
- VAR_IN_FSV_3025: desc edited
- VAR_IN_FSV_3026: desc edited
- VAR_IN_FSV_3032: desc edited
- VAR_IN_FSV_3033: desc edited
- VAR_IN_FSV_3041: desc edited
- VAR_IN_FSV_3042: desc edited
- VAR_IN_FSV_3043: desc edited
- VAR_IN_FSV_3044: desc edited
- VAR_IN_FSV_3045: desc edited
- VAR_IN_FSV_3046: desc edited
- ENUM_IN_FSV_3051: desc edited
- ENUM_IN_FSV_3052: desc edited
- ENUM_IN_FSV_3071: desc edited
- ENUM_IN_FSV_3081: desc edited
- ENUM_IN_FSV_3082: desc edited
- ENUM_IN_FSV_3083: desc edited
- VAR_IN_FSV_4011: desc edited
- VAR_IN_FSV_4012: desc edited
- VAR_IN_FSV_4013: desc edited
- VAR_IN_FSV_4021: desc edited
- VAR_IN_FSV_4022: desc edited
- VAR_IN_FSV_4023: desc edited
- VAR_IN_FSV_4024: desc edited
- VAR_IN_FSV_4025: desc edited
- VAR_IN_FSV_4031: desc edited
- VAR_IN_FSV_4032: desc edited
- VAR_IN_FSV_4033: desc edited
- VAR_IN_FSV_4041: desc edited
- VAR_IN_FSV_4042: desc edited
- VAR_IN_FSV_4043: desc edited
- VAR_IN_FSV_4044: desc edited
- VAR_IN_FSV_4045: desc edited
- VAR_IN_FSV_4046: desc edited
- VAR_IN_FSV_4051: desc edited
- VAR_IN_FSV_4052: desc edited
- VAR_IN_FSV_4053: desc edited
- VAR_IN_FSV_4061: desc edited
- VAR_IN_FSV_5011: desc edited
- VAR_IN_FSV_5012: desc edited
- VAR_IN_FSV_5013: desc edited
- VAR_IN_FSV_5014: desc edited
- VAR_IN_FSV_5015: desc edited
- VAR_IN_FSV_5016: desc edited
- VAR_IN_FSV_5017: desc edited
- VAR_IN_FSV_5018: desc edited
- VAR_IN_FSV_5019: desc edited
- VAR_IN_FSV_5021: desc edited
- VAR_IN_FSV_5023: desc edited
- ENUM_IN_FSV_5022: desc edited
- ENUM_IN_FSV_5041: desc edited
- ENUM_IN_FSV_5042: desc edited
- ENUM_IN_FSV_5043: desc edited
- ENUM_IN_FSV_5051: desc edited
- VAR_IN_FSV_5083: desc edited
- VAR_IN_FSV_5082: desc edited
- ENUM_IN_FSV_5081: desc edited
- ENUM_IN_FSV_5091: desc edited
- ENUM_IN_FSV_5094: desc edited
- VAR_IN_FSV_5092: desc edited
- VAR_IN_FSV_5093: desc edited
### v0.1.0Beta - 2025-02-08
- Initial Commit
- Initial Commit

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,20 @@
general:
nasaRepositoryFile: data/NasaRepository.yml
silentMode: True
serial:
device: /dev/ttyUSB0
baudrate: 9600
allowControl: False
logging:
deviceAdded: True
messageNotFound: False
packetNotFromIndoorOutdoor: False
proccessedMessage: False
pollerMessage: False
controlMessage: False
invalidPacket: False
#serial:
# device: /dev/ttyUSB0
# baudrate: 9600
tcp:
ip: 168.192.2.200
port: 4196
mqtt:
broker-url: 111.111.11.1
broker-port: 1883
@@ -13,3 +24,123 @@ mqtt:
homeAssistantAutoDiscoverTopic: "hass"
useCamelCaseTopicNames: True
topicPrefix: ehsSentinel
polling:
fetch_interval:
- name: fsv10xx
enable: false
schedule: 30m
- name: fsv20xx
enable: false
schedule: 30m
- name: fsv30xx
enable: false
schedule: 30m
- name: fsv40xx
enable: false
schedule: 30m
- name: fsv50xx
enable: false
schedule: 30m
groups:
fsv10xx:
- VAR_IN_FSV_1011
- VAR_IN_FSV_1012
- VAR_IN_FSV_1021
- VAR_IN_FSV_1022
- VAR_IN_FSV_1031
- VAR_IN_FSV_1032
- VAR_IN_FSV_1041
- VAR_IN_FSV_1042
- VAR_IN_FSV_1051
- VAR_IN_FSV_1052
fsv20xx:
- VAR_IN_FSV_2011
- VAR_IN_FSV_2012
- VAR_IN_FSV_2021
- VAR_IN_FSV_2022
- VAR_IN_FSV_2031
- VAR_IN_FSV_2032
- ENUM_IN_FSV_2041
- VAR_IN_FSV_2051
- VAR_IN_FSV_2052
- VAR_IN_FSV_2061
- VAR_IN_FSV_2062
- VAR_IN_FSV_2071
- VAR_IN_FSV_2072
- ENUM_IN_FSV_2081
- ENUM_IN_FSV_2091
- ENUM_IN_FSV_2092
- ENUM_IN_FSV_2093
- ENUM_IN_FSV_2094
fsv30xx:
- ENUM_IN_FSV_3011
- VAR_IN_FSV_3021
- VAR_IN_FSV_3022
- VAR_IN_FSV_3023
- VAR_IN_FSV_3024
- VAR_IN_FSV_3025
- VAR_IN_FSV_3026
- ENUM_IN_FSV_3031
- VAR_IN_FSV_3032
- VAR_IN_FSV_3033
- ENUM_IN_FSV_3041
- ENUM_IN_FSV_3042
- VAR_IN_FSV_3043
- VAR_IN_FSV_3044
- VAR_IN_FSV_3045
- VAR_IN_FSV_3046
- ENUM_IN_FSV_3051
- VAR_IN_FSV_3052
- ENUM_IN_FSV_3061
- ENUM_IN_FSV_3071
- VAR_IN_FSV_3081
- VAR_IN_FSV_3082
- VAR_IN_FSV_3083
fsv40xx:
- ENUM_IN_FSV_4011
- VAR_IN_FSV_4012
- VAR_IN_FSV_4013
- ENUM_IN_FSV_4021
- ENUM_IN_FSV_4022
- ENUM_IN_FSV_4023
- VAR_IN_FSV_4024
- VAR_IN_FSV_4025
- ENUM_IN_FSV_4031
- ENUM_IN_FSV_4032
- VAR_IN_FSV_4033
- ENUM_IN_FSV_4041
- VAR_IN_FSV_4042
- VAR_IN_FSV_4043
- ENUM_IN_FSV_4044
- VAR_IN_FSV_4045
- VAR_IN_FSV_4046
- ENUM_IN_FSV_4051
- VAR_IN_FSV_4052
- ENUM_IN_FSV_4053
- ENUM_IN_FSV_4061
fsv50xx:
- VAR_IN_FSV_5011
- VAR_IN_FSV_5012
- VAR_IN_FSV_5013
- VAR_IN_FSV_5014
- VAR_IN_FSV_5015
- VAR_IN_FSV_5016
- VAR_IN_FSV_5017
- VAR_IN_FSV_5018
- VAR_IN_FSV_5019
- VAR_IN_FSV_5021
- VAR_IN_FSV_5031
- ENUM_IN_FSV_5022
- VAR_IN_FSV_5023
- ENUM_IN_FSV_5041
- ENUM_IN_FSV_5042
- ENUM_IN_FSV_5043
- ENUM_IN_FSV_5051
- ENUM_IN_FSV_5061
- ENUM_IN_FSV_5081
- VAR_IN_FSV_5082
- VAR_IN_FSV_5083
- ENUM_IN_FSV_5091
- VAR_IN_FSV_5092
- VAR_IN_FSV_5093
- ENUM_IN_FSV_5094

View File

@@ -1,2 +1,2 @@
[50, 0, 60, 16, 0, 0, 176, 0, 255, 192, 20, 27, 13, 2, 2, 255, 255, 4, 16, 0, 0, 0, 0, 4, 27, 0, 32, 255, 255, 128, 0, 0, 128, 5, 255, 128, 23, 0, 128, 25, 0, 128, 26, 0, 128, 33, 1, 128, 50, 1, 128, 51, 2, 128, 60, 0, 128, 69, 0, 63, 195, 52]
[50, 0, 62, 16, 0, 0, 176, 0, 255, 192, 20, 28, 16, 128, 99, 1, 128, 102, 0, 128, 119, 0, 128, 120, 2, 128, 121, 0, 128, 122, 0, 128, 123, 0, 128, 124, 0, 128, 125, 0, 128, 126, 0, 128, 127, 255, 128, 131, 0, 128, 142, 255, 128, 169, 0, 128, 175, 0, 128, 177, 0, 145, 101, 52]
['0x32', '0x0', '0x3e', '0x10', '0x0', '0x0', '0xb0', '0x0', '0xff', '0xc0', '0x14', '0xc7', '0xc', '0x82', '0x18', '0xff', '0xdb', '0x82', '0x1a', '0xff', '0xd5', '0x82', '0x1e', '0x0', '0x8b', '0x82', '0x24', '0x0', '0xd6', '0x82', '0x25', '0x1', '0x5c', '0x82', '0x26', '0x0', '0x1b', '0x82', '0x29', '0x0', '0xe9', '0x82', '0x2e', '0x0', '0x50', '0x82', '0x31', '0x0', '0x64', '0x82', '0x34']
['0x32', '0x0', '0x12', '0x10', '0x0', '0x0', '0xb0', '0x0', '0xff', '0xc0', '0x14', '0x5a', '0x1', '0x82', '0x1c', '0x1', '0x38', '0x1f', '0x34']

View File

@@ -0,0 +1,240 @@
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 NASAMessage
encode_raw = "[50, 0, 17, 16, 0, 0, 176, 0, 255, 192, 20, 143, 1, 128, 49, 0, 157, 7, 52]"
encode_raw = "[50, 0, 60, 16, 0, 0, 176, 0, 255, 192, 20, 196, 13, 2, 2, 255, 255, 4, 16, 0, 0, 0, 0, 4, 27, 0, 32, 255, 255, 128, 0, 0, 128, 5, 255, 128, 23, 0, 128, 25, 0, 128, 26, 0, 128, 33, 1, 128, 50, 255, 128, 51, 0, 128, 60, 0, 128, 69, 0, 240, 94, 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, 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:
encode_bytearray = json.loads(encode_raw.strip()) # for [12, 234, 456 ,67]
except:
encode_tmp = encode_raw.strip().replace("'", "").replace("[", "").replace("]", "").split(", ") # for ['0x1', '0x2' ..]
encode_bytearray = [int(value, 16) for value in encode_tmp]
print(f"encode raw: {bytearray(encode_bytearray)}")
print(f"encode bytearray: {encode_bytearray}")
print(f"encode bytearray length: {len(encode_bytearray)}")
encoded_nasa = NASAPacket.NASAPacket()
encoded_nasa.parse(encode_bytearray)
print(f"encode NASA Object: {encoded_nasa}")
exit()
# time to reverse that thing!
decoded_nasa = NASAPacket.NASAPacket()
decoded_nasa.set_packet_source_address_class(NASAPacket.AddressClassEnum.JIGTester)
decoded_nasa.set_packet_source_channel(0)
decoded_nasa.set_packet_source_address(0)
decoded_nasa.set_packet_dest_address_class(NASAPacket.AddressClassEnum.BroadcastSelfLayer)
decoded_nasa.set_packet_dest_channel(0)
decoded_nasa.set_packet_dest_address(255)
decoded_nasa.set_packet_information(True)
decoded_nasa.set_packet_version(2)
decoded_nasa.set_packet_retry_count(0)
decoded_nasa.set_packet_type(NASAPacket.PacketType.Normal)
decoded_nasa.set_packet_data_type(NASAPacket.DataType.Notification)
decoded_nasa.set_packet_number(143)
tmp_msg = NASAMessage.NASAMessage()
tmp_msg.set_packet_message(0x8031)
tmp_msg.set_packet_message_type(0)
tmp_msg.set_packet_payload([0])
decoded_nasa.set_packet_messages([tmp_msg])
decoded_nasa = NASAPacket.NASAPacket()
decoded_nasa.set_packet_source_address_class(NASAPacket.AddressClassEnum.Outdoor)
decoded_nasa.set_packet_source_channel(0)
decoded_nasa.set_packet_source_address(0)
decoded_nasa.set_packet_dest_address_class(NASAPacket.AddressClassEnum.BroadcastSelfLayer)
decoded_nasa.set_packet_dest_channel(0)
decoded_nasa.set_packet_dest_address(255)
decoded_nasa.set_packet_information(True)
decoded_nasa.set_packet_version(2)
decoded_nasa.set_packet_retry_count(0)
decoded_nasa.set_packet_type(NASAPacket.PacketType.Normal)
decoded_nasa.set_packet_data_type(NASAPacket.DataType.Notification)
decoded_nasa.set_packet_number(196)
lst = []
tmp_msg = NASAMessage.NASAMessage()
tmp_msg.set_packet_message(0x0202)
tmp_msg.set_packet_message_type(1)
tmp_msg.set_packet_payload([255, 255])
lst.append(tmp_msg)
tmp_msg = NASAMessage.NASAMessage()
tmp_msg.set_packet_message(0x0410)
tmp_msg.set_packet_message_type(2)
tmp_msg.set_packet_payload([0, 0, 0, 0])
lst.append(tmp_msg)
tmp_msg = NASAMessage.NASAMessage()
tmp_msg.set_packet_message(0x41b)
tmp_msg.set_packet_message_type(2)
tmp_msg.set_packet_payload([0, 32, 255, 255])
lst.append(tmp_msg)
tmp_msg = NASAMessage.NASAMessage()
tmp_msg.set_packet_message(0x8000)
tmp_msg.set_packet_message_type(0)
tmp_msg.set_packet_payload([0])
lst.append(tmp_msg)
tmp_msg = NASAMessage.NASAMessage()
tmp_msg.set_packet_message(0x8005)
tmp_msg.set_packet_message_type(0)
tmp_msg.set_packet_payload([255])
lst.append(tmp_msg)
tmp_msg = NASAMessage.NASAMessage()
tmp_msg.set_packet_message(0x8017)
tmp_msg.set_packet_message_type(0)
tmp_msg.set_packet_payload([0])
lst.append(tmp_msg)
tmp_msg = NASAMessage.NASAMessage()
tmp_msg.set_packet_message(0x8019)
tmp_msg.set_packet_message_type(0)
tmp_msg.set_packet_payload([0])
lst.append(tmp_msg)
tmp_msg = NASAMessage.NASAMessage()
tmp_msg.set_packet_message(0x801a)
tmp_msg.set_packet_message_type(0)
tmp_msg.set_packet_payload([0])
lst.append(tmp_msg)
tmp_msg = NASAMessage.NASAMessage()
tmp_msg.set_packet_message(0x8021)
tmp_msg.set_packet_message_type(0)
tmp_msg.set_packet_payload([1])
lst.append(tmp_msg)
tmp_msg = NASAMessage.NASAMessage()
tmp_msg.set_packet_message(0x8032)
tmp_msg.set_packet_message_type(0)
tmp_msg.set_packet_payload([255])
lst.append(tmp_msg)
tmp_msg = NASAMessage.NASAMessage()
tmp_msg.set_packet_message(0x8033)
tmp_msg.set_packet_message_type(0)
tmp_msg.set_packet_payload([0])
lst.append(tmp_msg)
tmp_msg = NASAMessage.NASAMessage()
tmp_msg.set_packet_message(0x803c)
tmp_msg.set_packet_message_type(0)
tmp_msg.set_packet_payload([0])
lst.append(tmp_msg)
tmp_msg = NASAMessage.NASAMessage()
tmp_msg.set_packet_message(0x8045)
tmp_msg.set_packet_message_type(0)
tmp_msg.set_packet_payload([0])
lst.append(tmp_msg)
decoded_nasa.set_packet_messages(lst)
#rasw = decoded_nasa.to_raw()
decoded_nasa = NASAPacket.NASAPacket()
decoded_nasa.set_packet_source_address_class(NASAPacket.AddressClassEnum.WiFiKit)
decoded_nasa.set_packet_source_channel(0)
decoded_nasa.set_packet_source_address(144)
decoded_nasa.set_packet_dest_address_class(NASAPacket.AddressClassEnum.BroadcastSetLayer)
decoded_nasa.set_packet_dest_channel(0)
decoded_nasa.set_packet_dest_address(32)
decoded_nasa.set_packet_information(True)
decoded_nasa.set_packet_version(2)
decoded_nasa.set_packet_retry_count(0)
decoded_nasa.set_packet_type(NASAPacket.PacketType.Normal)
decoded_nasa.set_packet_data_type(NASAPacket.DataType.Read)
decoded_nasa.set_packet_number(240)
lst = []
tmp_msg = NASAMessage.NASAMessage()
tmp_msg.set_packet_message(0x4093)
tmp_msg.set_packet_message_type(0)
tmp_msg.set_packet_payload([0])
lst.append(tmp_msg)
tmp_msg = NASAMessage.NASAMessage()
tmp_msg.set_packet_message(0x4094)
tmp_msg.set_packet_message_type(0)
tmp_msg.set_packet_payload([0])
lst.append(tmp_msg)
tmp_msg = NASAMessage.NASAMessage()
tmp_msg.set_packet_message(0x4273)
tmp_msg.set_packet_message_type(1)
tmp_msg.set_packet_payload([0, 0])
lst.append(tmp_msg)
tmp_msg = NASAMessage.NASAMessage()
tmp_msg.set_packet_message(0x4274)
tmp_msg.set_packet_message_type(1)
tmp_msg.set_packet_payload([0, 0])
lst.append(tmp_msg)
tmp_msg = NASAMessage.NASAMessage()
tmp_msg.set_packet_message(0x4275)
tmp_msg.set_packet_message_type(1)
tmp_msg.set_packet_payload([0, 0])
lst.append(tmp_msg)
tmp_msg = NASAMessage.NASAMessage()
tmp_msg.set_packet_message(0x4276)
tmp_msg.set_packet_message_type(1)
tmp_msg.set_packet_payload([0, 0])
lst.append(tmp_msg)
tmp_msg = NASAMessage.NASAMessage()
tmp_msg.set_packet_message(0x4277)
tmp_msg.set_packet_message_type(1)
tmp_msg.set_packet_payload([0, 0])
lst.append(tmp_msg)
tmp_msg = NASAMessage.NASAMessage()
tmp_msg.set_packet_message(0x4278)
tmp_msg.set_packet_message_type(1)
tmp_msg.set_packet_payload([0, 0])
lst.append(tmp_msg)
tmp_msg = NASAMessage.NASAMessage()
tmp_msg.set_packet_message(0x4279)
tmp_msg.set_packet_message_type(1)
tmp_msg.set_packet_payload([0, 0])
lst.append(tmp_msg)
tmp_msg = NASAMessage.NASAMessage()
tmp_msg.set_packet_message(0x427a)
tmp_msg.set_packet_message_type(1)
tmp_msg.set_packet_payload([0, 0])
lst.append(tmp_msg)
tmp_msg = NASAMessage.NASAMessage()
tmp_msg.set_packet_message(0x427b)
tmp_msg.set_packet_message_type(1)
tmp_msg.set_packet_payload([0, 0])
lst.append(tmp_msg)
decoded_nasa.set_packet_messages(lst)
decoded_nasa = NASAPacket.NASAPacket()
decoded_nasa.set_packet_source_address_class(NASAPacket.AddressClassEnum.WiFiKit)
decoded_nasa.set_packet_source_channel(0)
decoded_nasa.set_packet_source_address(144)
decoded_nasa.set_packet_dest_address_class(NASAPacket.AddressClassEnum.BroadcastSelfLayer)
decoded_nasa.set_packet_dest_channel(255)
decoded_nasa.set_packet_dest_address(255)
decoded_nasa.set_packet_information(True)
decoded_nasa.set_packet_version(2)
decoded_nasa.set_packet_retry_count(0)
decoded_nasa.set_packet_type(NASAPacket.PacketType.Normal)
decoded_nasa.set_packet_data_type(NASAPacket.DataType.Notification)
decoded_nasa.set_packet_number(168)
tmp_msg = NASAMessage.NASAMessage()
tmp_msg.set_packet_message(0x0000)
tmp_msg.set_packet_message_type(0)
tmp_msg.set_packet_payload([2])
decoded_nasa.set_packet_messages([tmp_msg])
rasw = decoded_nasa.to_raw()
print(f"decoded bytearray: {rasw}")
print(f"decoded NASA Object: {decoded_nasa}")
print(f"decoded bytearray: {[int(value) for value in rasw]}")
print("Reverse Check:")
checkback = NASAPacket.NASAPacket()
checkback.parse(rasw)
print(checkback)

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

@@ -1,6 +1,7 @@
aiofiles>=24.1.0
future>=1.0.0
gmqtt>=0.7.0
iso8601>=2.1.0
pyserial>=3.5
pyserial-asyncio>=0.6
PyYAML>=6.0.2
serial>=0.0.97
gmqtt>=0.7.0
PyYAML>=6.0.2

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

@@ -0,0 +1,438 @@
views:
- title: Overview
type: sections
max_columns: 4
subview: false
sections:
- type: grid
cards:
- type: 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
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: 5
title: Field Setting Value
path: field-setting-value
sections:
- type: grid
cards:
- type: entities
entities:
- entity: sensor.samsung_ehssentinel_infsv1011
name: Water Out Temp. for Cooling Max.
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv1012
name: Water Out Temp. for Cooling Min.
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv1021
name: Room Temp. for Cooling Max.
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv1022
name: Room Temp. for Cooling Min.
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv1031
name: Water Out Temp. for Heating Max.
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv1032
name: Water Out Temp. for Heating Min.
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv1041
name: Room Temp. for Heating Max.
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv1042
name: Room Temp. for Heating Min.
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv1051
name: DHW tank Temp. Max.
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv1052
name: DHW tank Temp. Min.
secondary_info: last-updated
title: FSV 10** - Remote Controller
show_header_toggle: false
state_color: false
column_span: 1
- type: grid
cards:
- type: entities
entities:
- entity: sensor.samsung_ehssentinel_infsv2011
name: Heating Outdoor Temp. for WL Max.
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv2012
name: Heating Outdoor Temp. for WL Min.
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv2021
name: Heating Water out Temp. UFH/WL1 Max.
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv2022
name: Heating Water out Temp. UFH/WL1 Min.
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv2031
name: Heating Water out Temp. FCU/WL2 Max.
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv2032
name: Heating Water out Temp. FCU/WL2 Min.
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv2041
name: Heating WL Selection
icon: mdi:heating-coil
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv2051
name: Cooling Outdoor Temp. for WL Max.
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv2052
name: Cooling Outdoor Temp. for WL Min.
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv2061
name: Cooling Water out Temp UFH/WL1 Max.
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv2062
name: Cooling Water out Temp. UFH/WL1 Min.
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv2071
name: Cooling Water out Temp. FCU/WL2 Max.
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv2072
name: Cooling Water out Temp. FCU/WL2 Min.
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv2081
name: Cooling WL Selection
secondary_info: last-updated
icon: mdi:snowflake
- entity: sensor.samsung_ehssentinel_infsv2091
name: External Room Thermostat UFH
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv2092
name: External Room Thermostat FCU
secondary_info: last-updated
title: FSV 20** - Water Law
- type: grid
cards:
- type: entities
entities:
- entity: sensor.samsung_ehssentinel_infsv3011
secondary_info: last-updated
name: DHW Application
icon: mdi:water-boiler
- entity: sensor.samsung_ehssentinel_infsv3021
secondary_info: last-updated
name: Heat Pump Max. Temperature
- entity: sensor.samsung_ehssentinel_infsv3022
name: Heat Pump Stop
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv3023
name: Heat Pump Start
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv3024
name: Heat Pump Min. Space heating operation time
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv3025
name: Heat Pump Max. DHW operation time
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv3026
name: Heat Pump Max. Space heating operation time
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv3032
name: Booster Heat Delay Time
secondary_info: last-updated
- entity: binary_sensor.samsung_ehssentinel_infsv3041
name: Disinfection Application
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv3042
name: Disinfection Interval
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv3043
name: Disinfection Start Time
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv3044
name: Disinfection Target Temp.
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv3045
name: Disinfection Duration
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv3046
name: Disinfection Max Time
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv3051
name: Forced DHW Operation Time OFF Function
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv3052
name: Farced DHW Operation Time Duration
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv3061
name: Solar Panel/DHW Thermostat H/P Combination
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv3071
name: Direction of 3Way Valve DHW Tank
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv3081
name: Energy Metering BUH 1 step capacity
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv3082
name: Energy Metering BUH 2 step capacity
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv3083
name: Energy Metering BSH capacity
secondary_info: last-updated
title: FSV 30** - DHW code
- type: grid
cards:
- type: entities
entities:
- entity: sensor.samsung_ehssentinel_infsv4011
secondary_info: last-updated
name: Heat Pump Heating/DHW Priority
- entity: sensor.samsung_ehssentinel_infsv4012
name: Heat Pump Outdoor Temp. for Priority
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv4013
name: Heat Pump Heat OFF
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv4021
name: Backup Heater Application
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv4022
name: Backup Heater BUH/BSH Priority
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv4023
name: Backup Heater Cold Weather Compensation
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv4024
name: Backup Heater Threshold Temp.
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv4025
name: Backup Heater Defrost Backup Temp.
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv4031
name: Backup Boiler Application
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv4032
name: Backup Boiler Boiler Priority
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv4033
name: Backup Boiler Threshold Power
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv4041
name: Mixing Valve Application
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv4042
name: Mixing Valve Target △T (Heating)
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv4043
secondary_info: last-updated
name: Mixing Valve Target △T (Cooling)
- entity: sensor.samsung_ehssentinel_infsv4044
name: Mixing Valve Control Factor
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv4045
name: Mixing Valve Control Factor
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv4046
name: Mixing Valve Running Time
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv4051
name: Inverter Pump Application
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv4052
name: Inverter Pump Target △T
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv4053
name: Inverter Pump Control Factor
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv4061
name: Zone Control Application
secondary_info: last-updated
title: FSV 40** - Heating code
state_color: false
- type: grid
cards:
- type: entities
entities:
- entity: sensor.samsung_ehssentinel_infsv5011
secondary_info: last-updated
name: Outing Mode Water Out Temp. for Cooling
- entity: sensor.samsung_ehssentinel_infsv5012
name: Outing Mode Room Temp. for Cooling
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv5013
name: Outing Mode Water Out Temp. for Heating
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv5014
name: Outing Mode Room Temp. for Heating
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv5015
name: Outing Mode Auto Cooling WL1 Temp.
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv5016
name: Outing Mode Auto Cooling WL2 Temp.
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv5017
name: Outing Mode Auto Heating WL1 Temp.
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv5018
name: Outing Mode Auto Heating WL2 Temp.
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv5019
name: Outing Mode Target Tank Temp.
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv5021
name: DHW Saving Temp.
secondary_info: last-updated
- entity: binary_sensor.samsung_ehssentinel_infsv5022
name: DHW Saving Mode
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv5023
name: DHW Saving Thermo on Temp.
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv5041
name: Power Peak Control Application
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv5042
name: Power Peak Control Select Forced Off Parts
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv5043
name: Power Peak Control Using Input Voltage
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv5051
name: Frequency Ratio Control
- entity: sensor.samsung_ehssentinel_infsv5061
name: Ratio of hot water supply compare to heating
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv5081
name: PV Control Application
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv5082
name: PV Control Setting Temp. Shift Value (Cool)
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv5083
name: PV Control Setting Temp. Shift Value (Heat)
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv5091
name: Smart Grid Control Application
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv5092
name: Smart Grid Control Setting Temp. Shift Value (Heat)
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv5093
name: Smart Grid Control Setting Temp. Shift Value (DHW)
secondary_info: last-updated
- entity: sensor.samsung_ehssentinel_infsv5094
name: Smart Grid Control DHW Mode
secondary_info: last-updated
title: FSV 50** - Others code
cards: []
dense_section_placement: true

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

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,20 +3,21 @@ import serial
import serial_asyncio
import traceback
from MessageProcessor import MessageProcessor
from MessageProducer import MessageProducer
from EHSArguments import EHSArguments
from EHSConfig import EHSConfig
from EHSExceptions import MessageWarningException
from EHSExceptions import MessageWarningException, SkipInvalidPacketException
from MQTTClient import MQTTClient
import aiofiles
import json
import struct
import binascii
import random
# Get the logger
from CustomLogger import logger, setSilent
from NASAPacket import NASAPacket
from CustomLogger import logger
from NASAPacket import NASAPacket, AddressClassEnum, PacketType, DataType
from NASAMessage import NASAMessage
version = "0.0.1Beta"
version = "1.0.0 Stable"
async def main():
"""
@@ -26,9 +27,8 @@ async def main():
2. Reads command-line arguments.
3. Reads configuration settings.
4. Connects to the MQTT broker.
5. Sets silent mode if specified in the configuration.
6. If dry run mode is enabled, reads data from a dump file and processes it.
7. If not in dry run mode, reads data from a serial port and processes it.
5. If dry run mode is enabled, reads data from a dump file and processes it.
6. If not in dry run mode, reads data from a serial port and processes it.
Args:
None
Returns:
@@ -61,176 +61,137 @@ async def main():
await asyncio.sleep(1)
# if Silent is true, set Silent Mode
if config.GENERAL['silentMode']:
setSilent()
# if dryrun then we read from dumpfile
if args.DRYRUN:
logger.info(f"DRYRUN detected, reading from dumpfile {args.DUMPFILE}")
async with aiofiles.open(args.DUMPFILE, mode='r') as file:
async for line in file:
line = json.loads(line.strip())
await process_packet(line, args)
try:
line = json.loads(line.strip()) # for [12, 234, 456 ,67]
except:
line = line.strip().replace("'", "").replace("[", "").replace("]", "").split(", ") # for ['0x1', '0x2' ..]
line = [int(value, 16) for value in line]
await process_packet(line, args, config)
else:
# we are not in dryrun mode, so we need to read from Serial Pimort
await serial_read(config, args)
await serial_connection(config, args)
async def process_buffer(buffer, args):
"""
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.
"""
while True:
if buffer:
logger.debug(f"Buffersize: {len(buffer)}")
if buffer[0] == 0x32:
logger.debug("Start Byte recognized")
packet_size = ((buffer[1] << 8) | buffer[2]) +2
logger.debug(f"Readed packet size: {packet_size-1}")
if len(buffer) > packet_size-1:
packet = []
for i in range(0, len(buffer)):
packet.append(buffer[i])
if i == packet_size-1: #buffer[i] == 0x34 or
logger.debug(f"Complete Packet: {i}/{packet_size-1}")
logger.debug(f"Last Byte readed: {hex(buffer[i])}")
await process_packet(packet, args)
del buffer[0:i]
break
else:
logger.debug(f"Buffer to small to read hole packet, wait... buffer size {len(buffer)} packet size {packet_size}")
else:
logger.debug(f"Received byte not a startbyte 0x32 {buffer[0]} / {hex(buffer[0])}")
buffer.pop(0)
await asyncio.sleep(0.03)
async def serial_read(config, args):
"""
Asynchronously reads data from a serial connection and processes it.
Args:
config (object): Configuration object containing serial connection parameters.
args (object): Additional arguments for buffer processing.
This function establishes a serial connection using parameters from the config object,
reads data from the serial 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 serial connection is configured with the following parameters:
- Device URL: config.SERIAL['device']
- Baudrate: config.SERIAL['baudrate']
- Parity: Even
- Stopbits: One
- Bytesize: Eight
- RTS/CTS flow control: Enabled
- Timeout: 0
The function runs an infinite loop to continuously read data from the serial port.
"""
async def process_buffer(buffer, args, config):
if buffer:
if (len(buffer) > 14):
for i in range(0, len(buffer)):
if buffer[i] == 0x32:
if (len(buffer[i:]) > 14):
asyncio.create_task(process_packet(buffer[i:], args, config))
else:
logger.debug(f"Buffermessages to short for NASA {len(buffer)}")
break
else:
logger.debug(f"Buffer to short for NASA {len(buffer)}")
async def serial_connection(config, args):
buffer = []
loop = asyncio.get_running_loop()
reader, writer = await serial_asyncio.open_serial_connection(
loop=loop,
url=config.SERIAL['device'],
baudrate=config.SERIAL['baudrate'],
parity=serial.PARITY_EVEN,
stopbits=serial.STOPBITS_ONE,
bytesize=serial.EIGHTBITS,
rtscts=True,
timeout=0
)
# start the async buffer process
asyncio.create_task(process_buffer(buffer, args))# start the async buffer process
# TODO have to be tested and verified, please do not try it yet
# start the async writer process
#asyncio.create_task(serial_write(writer, reader))
# Read loop
while True:
data = await reader.readuntil(b'\x34') # Read up to end of next message 0x34
if data:
buffer.extend(data)
logger.debug(f"Received: {[hex(x) for x in data]}")
async def serial_write(writer, reader):
"""
TODO Not used yet, only for future use...
Asynchronously writes data to the serial port.
This function sends data through the serial port at regular intervals.
Args:
transport: The serial transport object.
args: Additional arguments.
Returns:
None
"""
while True:
await asyncio.sleep(5)
# Example data to write
if config.TCP is not None:
reader, writer = await asyncio.open_connection(config.TCP['ip'], config.TCP['port'])
else:
reader, writer = await serial_asyncio.open_serial_connection(
loop=loop,
url=config.SERIAL['device'],
baudrate=config.SERIAL['baudrate'],
parity=serial.PARITY_EVEN,
stopbits=serial.STOPBITS_ONE,
bytesize=serial.EIGHTBITS,
rtscts=True,
timeout=1
)
packet = bytearray([
#0x32, # Packet Start Byte
#0x00, 0x12, # Packet Size
0x80, # Source Address Class JIGTester
0xFF, # Source Channel
0x00, # Source Address
0x20, # Destination Address Class Indoor
0x00, # Destination Channel
0x00, # Destination Address
0xC0, # Packet Information + Protocol Version + Retry Count
0x11, # Packet Type [Normal = 1] + Data Type [Read = 1]
0xF0, # Packet Number
0x01, # Capacity (Number of Messages)
0x42, 0x56, # NASA Message Number
0x00, 0x00 # Message Payload (placeholder for return value)
])
await asyncio.gather(
serial_read(reader, args, config),
serial_write(writer, config),
)
crc=binascii.crc_hqx(packet, 0)
# NOTE: include length of CRC(2) and length of length field(2) in the
# total length, exclude SF/TF of total length
final_packet = struct.pack(">BH", 0x32, len(packet)+2+2) + packet + struct.pack(">HB", crc, 0x34)
# ['0x32', '0x0', '0x12', '0x80', '0xff', '0x0', '0x20', '0x0', '0x0', '0xc0', '0x11', '0xf0', '0x1', '0x42', '0x56', '0x0', '0x0', '0xf9', '0x65', '0x34']
# ['0x32', '0x0', '0x12', '0x80', '0xff', '0x0', '0x20', '0x0', '0x0', '0xc0', '0x11', '0xf0', '0x1', '0x42', '0x56', '0x0', '0x0', '0x38', '0xc6', '0x34']
writer.write(final_packet)
await writer.drain()
logger.debug(f"Sent data raw: {final_packet}")
logger.debug(f"Sent data raw: {[hex(x) for x in final_packet]}")
await asyncio.sleep(1) # Adjust the interval as needed
async def serial_read(reader: asyncio.StreamReader, args, config):
prev_byte = 0x00
packet_started = False
data = bytearray()
packet_size = 0
async def process_packet(buffer, args):
"""
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.
"""
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):
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
# 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
#await asyncio.sleep(0.001) # Yield control to other tasks
async def serial_write(writer:asyncio.StreamWriter, config):
producer = MessageProducer(writer=writer)
# Wait 20s befor initial polling
await asyncio.sleep(20)
if config.POLLING is not None:
for poller in config.POLLING['fetch_interval']:
if poller['enable']:
await asyncio.sleep(1)
asyncio.create_task(make_default_request_packet(producer=producer, config=config, poller=poller))
async def make_default_request_packet(producer: MessageProducer, config: EHSConfig, poller):
logger.info(f"Setting up Poller {poller['name']} every {poller['schedule']} seconds")
message_list = []
for message in config.POLLING['groups'][poller['name']]:
message_list.append(message)
while True:
try:
await producer.read_request(message_list)
except MessageWarningException as e:
logger.warning("Polling Messages was not successfull")
logger.warning(f"Error processing message: {e}")
logger.warning(f"Message List: {message_list}")
except Exception as e:
logger.error("Error Accured, Polling will be skipped")
logger.error(f"Error processing message: {e}")
logger.error(traceback.format_exc())
await asyncio.sleep(poller['schedule'])
logger.info(f"Refresh Poller {poller['name']}")
async def process_packet(buffer, args, config):
if args.DUMPFILE and not args.DRYRUN:
async with aiofiles.open(args.DUMPFILE, "a") as dumpWriter:
await dumpWriter.write(f"{buffer}\n")
@@ -241,18 +202,47 @@ async def process_packet(buffer, args):
logger.debug("Packet processed: ")
logger.debug(f"Packet raw: {[hex(x) for x in buffer]}")
logger.debug(nasa_packet)
messageProcessor = MessageProcessor()
messageProcessor.process_message(nasa_packet)
if nasa_packet.packet_source_address_class in (AddressClassEnum.Outdoor, AddressClassEnum.Indoor):
messageProcessor = MessageProcessor()
await messageProcessor.process_message(nasa_packet)
elif nasa_packet.packet_source_address_class == AddressClassEnum.WiFiKit and \
nasa_packet.packet_dest_address_class == AddressClassEnum.BroadcastSelfLayer and \
nasa_packet.packet_data_type == DataType.Notification:
pass
else:
if config.LOGGING['packetNotFromIndoorOutdoor']:
logger.info("Message not From Indoor or Outdoor")
logger.info(nasa_packet)
logger.info(f"Packet int: {[x for x in buffer]}")
logger.info(f"Packet hex: {[hex(x) for x in buffer]}")
else:
logger.debug("Message not From Indoor or Outdoor")
logger.debug(nasa_packet)
logger.debug(f"Packet int: {[x for x in buffer]}")
logger.debug(f"Packet hex: {[hex(x) for x in buffer]}")
except ValueError as e:
logger.warning("Value Error on parsing Packet, Packet will be skipped")
logger.warning(f"Error processing message: {e}")
logger.warning(f"Complete Packet: {[hex(x) for x in buffer]}")
logger.warning(traceback.format_exc())
except SkipInvalidPacketException as e:
logger.debug("Warnung accured, Packet will be skipped")
logger.debug(f"Error processing message: {e}")
logger.debug(f"Complete Packet: {[hex(x) for x in buffer]}")
logger.debug(traceback.format_exc())
except MessageWarningException as e:
logger.warning("Warnung accured, Packet will be skipped")
logger.warning(f"Error processing message: {e}")
logger.warning(f"Complete Packet: {[hex(x) for x in buffer]}")
logger.warning(traceback.format_exc())
except Exception as e:
logger.error("Error Accured, Packet will be skipped")
logger.error(f"Error processing message: {e}")
logger.error(traceback.format_exc())
if __name__ == "__main__":
asyncio.run(main())
try:
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
except RuntimeError as e:
logger.error(f"Runtime error: {e}")