Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a010d874ac | ||
|
|
f0222d750f |
15
.github/dependabot.yml
vendored
Normal 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:
|
||||
- "*"
|
||||
1
.gitignore
vendored
@@ -174,3 +174,4 @@ prot.csv
|
||||
helpertils/serial.py
|
||||
helpertils/test.py
|
||||
helpertils/socker.py
|
||||
helpertils/messagesFound.txt
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -76,11 +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...")
|
||||
|
||||
@@ -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
|
||||
|
||||
69
EHSConfig.py
@@ -11,15 +11,6 @@ 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
|
||||
@@ -30,44 +21,15 @@ class EHSConfig():
|
||||
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
|
||||
@@ -101,16 +63,6 @@ class EHSConfig():
|
||||
self.validate()
|
||||
|
||||
def parse_time_string(self, time_str: str) -> int:
|
||||
"""
|
||||
Parses a time string like '10m' or '10s' and converts it to seconds.
|
||||
|
||||
Supported formats:
|
||||
- '10m' for 10 minutes
|
||||
- '10s' for 10 seconds
|
||||
|
||||
Returns:
|
||||
- Equivalent time in seconds as an integer.
|
||||
"""
|
||||
match = re.match(r'^(\d+)([smh])$', time_str.strip(), re.IGNORECASE)
|
||||
if not match:
|
||||
raise ValueError("Invalid time format. Use '10s', '10m', or '10h'.")
|
||||
@@ -126,15 +78,6 @@ class EHSConfig():
|
||||
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)
|
||||
@@ -144,6 +87,9 @@ class EHSConfig():
|
||||
if 'protocolFile' not in self.GENERAL:
|
||||
self.GENERAL['protocolFile'] = None
|
||||
|
||||
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")
|
||||
|
||||
@@ -213,8 +159,8 @@ class EHSConfig():
|
||||
if 'messageNotFound' not in self.LOGGING:
|
||||
self.LOGGING['messageNotFound'] = False
|
||||
|
||||
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
|
||||
@@ -228,6 +174,9 @@ class EHSConfig():
|
||||
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}")
|
||||
|
||||
290
MQTTClient.py
@@ -10,6 +10,7 @@ import gmqtt
|
||||
from CustomLogger import logger
|
||||
from EHSArguments import EHSArguments
|
||||
from EHSConfig import EHSConfig
|
||||
from MessageProducer import MessageProducer
|
||||
|
||||
class MQTTClient:
|
||||
"""
|
||||
@@ -23,18 +24,6 @@ class MQTTClient:
|
||||
DEVICE_ID = "samsung_ehssentinel"
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
"""
|
||||
Create a new instance of the class if one does not already exist.
|
||||
This method ensures that only one instance of the class is created (singleton pattern).
|
||||
If an instance already exists, it returns the existing instance.
|
||||
Otherwise, it creates a new instance, marks it as uninitialized, and returns it.
|
||||
Args:
|
||||
cls: The class being instantiated.
|
||||
*args: Variable length argument list.
|
||||
**kwargs: Arbitrary keyword arguments.
|
||||
Returns:
|
||||
An instance of the class.
|
||||
"""
|
||||
|
||||
if not cls._instance:
|
||||
cls._instance = super(MQTTClient, cls).__new__(cls)
|
||||
@@ -42,32 +31,12 @@ class MQTTClient:
|
||||
return cls._instance
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Initializes the MQTTClient instance.
|
||||
This constructor sets up the MQTT client with the necessary configuration
|
||||
parameters, including broker URL, port, client ID, and authentication credentials.
|
||||
It also assigns callback functions for various MQTT events such as connect,
|
||||
disconnect, message, and subscribe. Additionally, it initializes topic-related
|
||||
settings and a list to keep track of known topics.
|
||||
Attributes:
|
||||
config (EHSConfig): Configuration object for MQTT settings.
|
||||
args (EHSArguments): Argument parser object.
|
||||
broker (str): URL of the MQTT broker.
|
||||
port (int): Port number of the MQTT broker.
|
||||
client_id (str): Client ID for the MQTT connection.
|
||||
client (gmqtt.Client): MQTT client instance.
|
||||
topicPrefix (str): Prefix for MQTT topics.
|
||||
homeAssistantAutoDiscoverTopic (str): Topic for Home Assistant auto-discovery.
|
||||
useCamelCaseTopicNames (bool): Flag to use camel case for topic names.
|
||||
initialized (bool): Flag indicating if the client has been initialized.
|
||||
known_topics (list): List to keep track of known topics.
|
||||
known_devices_topic (str): Topic for storing known devices.
|
||||
"""
|
||||
|
||||
if self._initialized:
|
||||
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']
|
||||
@@ -88,17 +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 function logs the connection attempt, connects to the MQTT broker using the specified
|
||||
broker address and port, and sets the keepalive interval. If the CLEAN_KNOWN_DEVICES argument
|
||||
is set, it publishes an empty message to the known devices topic to clear it.
|
||||
Args:
|
||||
None
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
|
||||
logger.info("[MQTT] Connecting to broker...")
|
||||
await self.client.connect(self.broker, self.port, keepalive=60, version=gmqtt.constants.MQTTv311)
|
||||
|
||||
@@ -106,53 +64,21 @@ class MQTTClient:
|
||||
self._publish(f"{self.topicPrefix.replace('/', '')}/{self.known_devices_topic}", " ", retain=True)
|
||||
logger.info("Known Devices Topic have been cleared")
|
||||
|
||||
def subscribe_known_topics(self):
|
||||
"""
|
||||
Subscribe to predefined MQTT topics.
|
||||
This method subscribes the MQTT client to a set of known topics, which include:
|
||||
- A topic for known devices, constructed using the topic prefix and known devices topic.
|
||||
- A status topic for Home Assistant auto-discovery.
|
||||
The subscription is done with a QoS level of 1 for both topics.
|
||||
Logging:
|
||||
- Logs an info message indicating the subscription to known devices topic.
|
||||
"""
|
||||
|
||||
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 associated with the message.
|
||||
This function performs the following actions:
|
||||
- If the topic matches the known devices topic, it updates the known topics list with the retained message.
|
||||
- If the topic matches the Home Assistant auto-discover status topic, it logs the status message and, if the payload indicates that Home Assistant is online, it clears the known devices topic.
|
||||
"""
|
||||
|
||||
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(",")]))
|
||||
@@ -174,19 +100,15 @@ class MQTTClient:
|
||||
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.
|
||||
Parameters:
|
||||
client (paho.mqtt.client.Client): The client instance for this callback.
|
||||
flags (dict): Response flags sent by the broker.
|
||||
rc (int): The connection result.
|
||||
properties (paho.mqtt.properties.Properties): The properties associated with the connection.
|
||||
If the connection is successful (rc == 0), logs a success message and subscribes to known topics if any.
|
||||
Otherwise, logs an error message with the return code.
|
||||
"""
|
||||
|
||||
if rc == 0:
|
||||
logger.info(f"Connected to MQTT with result code {rc}")
|
||||
if len(self.homeAssistantAutoDiscoverTopic) > 0:
|
||||
@@ -194,18 +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.
|
||||
This function logs the disconnection event and attempts to reconnect the client
|
||||
in case of an unexpected disconnection. It will keep trying to reconnect every
|
||||
5 seconds until successful.
|
||||
Args:
|
||||
client (paho.mqtt.client.Client): The MQTT client instance that disconnected.
|
||||
packet (paho.mqtt.packet.Packet): The disconnect packet.
|
||||
exc (Exception, optional): The exception that caused the disconnection, if any.
|
||||
"""
|
||||
|
||||
def on_disconnect(self, client, packet, exc=None):
|
||||
logger.info(f"Disconnected with result code ")
|
||||
logger.warning("Unexpected disconnection. Reconnecting...")
|
||||
while True:
|
||||
@@ -216,31 +127,12 @@ class MQTTClient:
|
||||
logger.error(f"Reconnection failed: {e}")
|
||||
time.sleep(5)
|
||||
|
||||
def _publish(self, topic, payload, qos=0, retain=False):
|
||||
"""
|
||||
Publishes a message to a specified MQTT topic.
|
||||
Args:
|
||||
topic (str): The MQTT topic to publish to.
|
||||
payload (str): The message payload to publish.
|
||||
qos (int, optional): The Quality of Service level for the message. Defaults to 0.
|
||||
retain (bool, optional): If True, the message will be retained by the broker. Defaults to False.
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
|
||||
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):
|
||||
"""
|
||||
Refreshes the list of known devices by publishing the current known topics to the MQTT broker.
|
||||
Args:
|
||||
devname (str): The name of the device to refresh.
|
||||
This function constructs a topic string by replacing '/' with an empty string in the topicPrefix,
|
||||
then concatenates it with the known_devices_topic. It publishes the known topics as a comma-separated
|
||||
string to this constructed topic with the retain flag set to True.
|
||||
"""
|
||||
self.known_topics.append(devname)
|
||||
if self.config.LOGGING['deviceAdded']:
|
||||
logger.info(f"Device added no. {len(self.known_topics):<3}: {devname} ")
|
||||
@@ -248,20 +140,7 @@ class MQTTClient:
|
||||
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 function normalizes the given name, determines the appropriate MQTT topic,
|
||||
and publishes the provided value to that topic. It also handles Home Assistant
|
||||
auto-discovery if configured.
|
||||
Args:
|
||||
name (str): The name of the sensor or device.
|
||||
value (int, float, bool, str): The value to be published. If the value is a float,
|
||||
it will be rounded to two decimal places.
|
||||
Raises:
|
||||
ValueError: If the value type is not supported for publishing.
|
||||
"""
|
||||
|
||||
async def publish_message(self, name, value):
|
||||
newname = f"{self._normalize_name(name)}"
|
||||
|
||||
if len(self.homeAssistantAutoDiscoverTopic) > 0:
|
||||
@@ -270,13 +149,10 @@ class MQTTClient:
|
||||
self.auto_discover_hass(name)
|
||||
self.refresh_known_devices(name)
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
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"
|
||||
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}"
|
||||
@@ -287,13 +163,13 @@ class MQTTClient:
|
||||
self._publish(topicname, value, qos=2, retain=False)
|
||||
|
||||
def clear_hass(self):
|
||||
"""
|
||||
clears all entities/components fpr the HomeAssistant Device
|
||||
"""
|
||||
entities = {}
|
||||
for nasa in self.config.NASA_REPO:
|
||||
namenorm = self._normalize_name(nasa)
|
||||
sensor_type = self._get_sensor_type(nasa)
|
||||
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}
|
||||
|
||||
device = {
|
||||
@@ -312,69 +188,47 @@ class MQTTClient:
|
||||
retain=True)
|
||||
|
||||
def auto_discover_hass(self, name):
|
||||
"""
|
||||
Automatically discovers and configures Home Assistant entities based on the NASA_REPO configuration.
|
||||
This function iterates through the NASA_REPO configuration to create and configure entities for Home Assistant.
|
||||
It determines the type of sensor (binary_sensor or sensor) based on the configuration and sets various attributes
|
||||
such as unit of measurement, device class, state class, and payloads for binary sensors. It then constructs a device
|
||||
configuration payload and publishes it to the Home Assistant MQTT discovery topic.
|
||||
The function performs the following steps:
|
||||
1. Iterates through the NASA_REPO configuration.
|
||||
2. Normalizes the name of each NASA_REPO entry.
|
||||
3. Determines the sensor type (binary_sensor or sensor) based on the 'enum' values.
|
||||
4. Configures the entity attributes such as unit of measurement, device class, state class, and payloads.
|
||||
5. Constructs a device configuration payload.
|
||||
6. Publishes the device configuration to the Home Assistant MQTT discovery topic.
|
||||
Attributes:
|
||||
entities (dict): A dictionary to store the configured entities.
|
||||
device (dict): A dictionary to store the device configuration payload.
|
||||
Logs:
|
||||
Logs the constructed device configuration payload for debugging purposes.
|
||||
Publishes:
|
||||
Publishes the device configuration payload to the Home Assistant MQTT discovery topic with QoS 2 and retain flag set to True.
|
||||
"""
|
||||
entity = {}
|
||||
namenorm = self._normalize_name(name)
|
||||
sensor_type = self._get_sensor_type(name)
|
||||
entity = {
|
||||
"name": f"{namenorm}",""
|
||||
"name": f"{namenorm}",
|
||||
"object_id": f"{self.DEVICE_ID}_{namenorm.lower()}",
|
||||
"unique_id": f"{self.DEVICE_ID}_{name.lower()}",
|
||||
"platform": sensor_type,
|
||||
"force_update": True,
|
||||
#"expire_after": 86400, # 1 day (24h * 60m * 60s)
|
||||
"value_template": "{{ value }}",
|
||||
"value_template": "{{ value }}"
|
||||
#"value_template": "{{ value if value | length > 0 else 'unavailable' }}",
|
||||
"state_topic": f"{self.config.MQTT['homeAssistantAutoDiscoverTopic']}/{sensor_type}/{self.DEVICE_ID}_{namenorm.lower()}/state",
|
||||
}
|
||||
|
||||
if sensor_type == "sensor":
|
||||
if len(self.config.NASA_REPO[name]['unit']) > 0:
|
||||
entity['unit_of_measurement'] = self.config.NASA_REPO[name]['unit']
|
||||
if entity['unit_of_measurement'] == "\u00b0C":
|
||||
entity['device_class'] = "temperature"
|
||||
elif entity['unit_of_measurement'] == '%':
|
||||
entity['state_class'] = "measurement"
|
||||
elif entity['unit_of_measurement'] == 'kW':
|
||||
entity['device_class'] = "power"
|
||||
elif entity['unit_of_measurement'] == 'rpm':
|
||||
entity['state_class'] = "measurement"
|
||||
elif entity['unit_of_measurement'] == 'bar':
|
||||
entity['device_class'] = "pressure"
|
||||
elif entity['unit_of_measurement'] == 'HP':
|
||||
entity['device_class'] = "power"
|
||||
elif entity['unit_of_measurement'] == 'hz':
|
||||
entity['device_class'] = "frequency"
|
||||
else:
|
||||
entity['device_class'] = None
|
||||
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:
|
||||
entity['payload_on'] = "ON"
|
||||
entity['payload_off'] = "OFF"
|
||||
sensor_type = self.config.NASA_REPO[name]['hass_opts']['default_platform']
|
||||
|
||||
if 'state_class' in self.config.NASA_REPO[name]:
|
||||
entity['state_class'] = self.config.NASA_REPO[name]['state_class']
|
||||
|
||||
if 'device_class' in self.config.NASA_REPO[name]:
|
||||
entity['device_class'] = self.config.NASA_REPO[name]['device_class']
|
||||
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(),
|
||||
@@ -407,18 +261,6 @@ class MQTTClient:
|
||||
}
|
||||
|
||||
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
|
||||
@@ -436,19 +278,3 @@ class MQTTClient:
|
||||
tmpname = name
|
||||
|
||||
return tmpname
|
||||
|
||||
def _get_sensor_type(self, name):
|
||||
"""
|
||||
return the sensor type of given measurement
|
||||
Args:
|
||||
name (str): The name of the measurement.
|
||||
Returns:
|
||||
str: The sensor type: sensor or binary_sensor.
|
||||
"""
|
||||
sensor_type = "sensor"
|
||||
if 'enum' in self.config.NASA_REPO[name]:
|
||||
enum = [*self.config.NASA_REPO[name]['enum'].values()]
|
||||
if all([en.lower() in ['on', 'off'] for en in enum]):
|
||||
sensor_type = "binary_sensor"
|
||||
|
||||
return sensor_type
|
||||
|
||||
@@ -14,70 +14,27 @@ 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
|
||||
tmpdict = {}
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
"""
|
||||
Create a new instance of the class if one does not already exist.
|
||||
This method ensures that only one instance of the class is created (singleton pattern).
|
||||
If an instance already exists, it returns the existing instance.
|
||||
Args:
|
||||
cls (type): The class being instantiated.
|
||||
*args: Variable length argument list.
|
||||
**kwargs: Arbitrary keyword arguments.
|
||||
Returns:
|
||||
MessageProcessor: The single instance of the MessageProcessor class.
|
||||
"""
|
||||
if not cls._instance:
|
||||
cls._instance = super(MessageProcessor, cls).__new__(cls, *args, **kwargs)
|
||||
cls._instance._initialized = False
|
||||
return cls._instance
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Initializes the MessageProcessor instance.
|
||||
This constructor checks if the instance has already been initialized to prevent reinitialization.
|
||||
If not initialized, it sets the _initialized flag to True, logs the initialization process,
|
||||
and initializes the configuration and argument handling components.
|
||||
Attributes:
|
||||
_initialized (bool): A flag indicating whether the instance has been initialized.
|
||||
config (EHSConfig): An instance of the EHSConfig class for configuration management.
|
||||
args (EHSArguments): An instance of the EHSArguments class for argument handling.
|
||||
"""
|
||||
if self._initialized:
|
||||
return
|
||||
self._initialized = True
|
||||
self.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=f"{msg.packet_payload}/{[hex(x) for x in msg.packet_payload]}", message=f"Value of {hexmsg} couldn't be determinate, skip Message {e}")
|
||||
self.protocolMessage(msg, msgname, msgvalue)
|
||||
await self.protocolMessage(msg, msgname, msgvalue)
|
||||
else:
|
||||
packedval = int.from_bytes(msg.packet_payload, byteorder='big', signed=True)
|
||||
if self.config.LOGGING['messageNotFound']:
|
||||
@@ -85,25 +42,10 @@ class MessageProcessor:
|
||||
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):
|
||||
|
||||
if self.config.LOGGING['proccessedMessage']:
|
||||
logger.info(f"Message number: {hex(msg.packet_message):<6} {msgname:<50} Type: {msg.packet_message_type} Payload: {msgvalue}")
|
||||
logger.info(f"Message number: {hex(msg.packet_message):<6} {msgname:<50} Type: {msg.packet_message_type} Payload: {msgvalue} ({msg.packet_payload})")
|
||||
else:
|
||||
logger.debug(f"Message number: {hex(msg.packet_message):<6} {msgname:<50} Type: {msg.packet_message_type} Payload: {msgvalue}")
|
||||
|
||||
@@ -111,87 +53,94 @@ class MessageProcessor:
|
||||
with open(self.config.GENERAL['protocolFile'], "a") as protWriter:
|
||||
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']):
|
||||
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.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)
|
||||
(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
|
||||
self.protocolMessage(NASAMessage(packet_message=0x9999, packet_message_type=1),
|
||||
await self.protocolMessage(NASAMessage(packet_message=0x9999, packet_message_type=1, packet_payload=[0]),
|
||||
"NASA_EHSSENTINEL_HEAT_OUTPUT",
|
||||
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']):
|
||||
if (self.NASA_VAL_STORE['NASA_OUTDOOR_CONTROL_WATTMETER_ALL_UNIT'] > 0):
|
||||
value = round((self.NASA_VAL_STORE['NASA_EHSSENTINEL_HEAT_OUTPUT'] / self.NASA_VAL_STORE['NASA_OUTDOOR_CONTROL_WATTMETER_ALL_UNIT']/1000.), 3)
|
||||
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):
|
||||
self.protocolMessage(NASAMessage(packet_message=0x9998, packet_message_type=1),
|
||||
await self.protocolMessage(NASAMessage(packet_message=0x9998, packet_message_type=1, packet_payload=[0]),
|
||||
"NASA_EHSSENTINEL_COP",
|
||||
value
|
||||
)
|
||||
|
||||
if msgname in ('NASA_OUTDOOR_CONTROL_WATTMETER_ALL_UNIT_ACCUM', 'LVAR_IN_TOTAL_GENERATED_POWER'):
|
||||
if all(k in self.NASA_VAL_STORE for k in ['NASA_OUTDOOR_CONTROL_WATTMETER_ALL_UNIT_ACCUM', 'LVAR_IN_TOTAL_GENERATED_POWER']):
|
||||
if (self.NASA_VAL_STORE['NASA_OUTDOOR_CONTROL_WATTMETER_ALL_UNIT_ACCUM'] > 0):
|
||||
value = round(self.NASA_VAL_STORE['LVAR_IN_TOTAL_GENERATED_POWER'] / self.NASA_VAL_STORE['NASA_OUTDOOR_CONTROL_WATTMETER_ALL_UNIT_ACCUM'], 3)
|
||||
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):
|
||||
self.protocolMessage(NASAMessage(packet_message=0x9997, packet_message_type=1),
|
||||
await self.protocolMessage(NASAMessage(packet_message=0x9997, packet_message_type=1, packet_payload=[0]),
|
||||
"NASA_EHSSENTINEL_TOTAL_COP",
|
||||
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 for Message {msgname}, 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
@@ -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()
|
||||
|
||||
@@ -2,48 +2,8 @@
|
||||
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])
|
||||
@@ -51,6 +11,7 @@ class NASAMessage:
|
||||
|
||||
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
|
||||
@@ -58,6 +19,9 @@ class NASAMessage:
|
||||
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)
|
||||
@@ -88,6 +52,12 @@ class NASAMessage:
|
||||
(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 (
|
||||
|
||||
146
NASAPacket.py
@@ -3,38 +3,11 @@ 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
|
||||
@@ -68,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
|
||||
@@ -85,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
|
||||
@@ -108,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):
|
||||
@@ -182,33 +90,6 @@ 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")
|
||||
@@ -217,6 +98,12 @@ class NASAPacket:
|
||||
|
||||
self.packet_start = packet[0]
|
||||
self.packet_size = ((packet[1] << 8) | packet[2])
|
||||
|
||||
if self.packet_size+2 != len(packet):
|
||||
logger.info(f"length not correct {self.packet_size+2} -> {len(packet)}")
|
||||
logger.info(f"{packet.hex()}")
|
||||
logger.info(f"{hex(packet[self.packet_size+1])}")
|
||||
|
||||
try:
|
||||
self.packet_source_address_class = AddressClassEnum(packet[3])
|
||||
except ValueError as e:
|
||||
@@ -244,20 +131,6 @@ class NASAPacket:
|
||||
raise SkipInvalidPacketException(f"Checksum for package could not be validated. Calculated: {crc_checkusm} in packet: {self.packet_crc16}: packet:{self}")
|
||||
|
||||
def _extract_messages(self, depth: int, capacity: int, msg_rest: bytearray, return_list: list):
|
||||
"""
|
||||
Recursively extracts messages from a bytearray and appends them to a list.
|
||||
Args:
|
||||
depth (int): The current depth of recursion.
|
||||
capacity (int): The maximum allowed depth of recursion.
|
||||
msg_rest (bytearray): The remaining bytes to be processed.
|
||||
return_list (list): The list to which extracted messages are appended.
|
||||
Returns:
|
||||
list: The list of extracted messages.
|
||||
Raises:
|
||||
ValueError: If the message type is unknown, the capacity is invalid for a structure type message,
|
||||
or the payload size exceeds 255 bytes.
|
||||
"""
|
||||
|
||||
if depth > capacity or len(msg_rest) <= 2:
|
||||
return return_list
|
||||
|
||||
@@ -358,11 +231,6 @@ class NASAPacket:
|
||||
self.packet_messages = value
|
||||
|
||||
def to_raw(self) -> bytearray:
|
||||
"""
|
||||
Converts the NASAPacket object back to its raw byte representation.
|
||||
Returns:
|
||||
bytearray: The raw byte representation of the packet.
|
||||
"""
|
||||
self.packet_start = 50
|
||||
self.packet_end = 52
|
||||
|
||||
|
||||
77
README.md
@@ -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`
|
||||
@@ -92,7 +105,9 @@ Some Distributions like debian 12 dont allow to use system wide pip package inst
|
||||
|
||||
`ExecStart = <path to python3> <Path of the script you want to run>` <- provide here to path to your folder where startEHSSentinel.py is
|
||||
|
||||
sample: `ExecStart = /root/EHS-Sentinel/bin/python3 /root/EHS-Sentinel/startEHSSentinel.py --configfile /root/EHS-Sentinel/config.yml`
|
||||
sample: `ExecStart = /root/EHS-Sentinel/bin/python3 /root/EHS-Sentinel/startEHSSentinel.py --configfile /root/EHS-Sentinel/config.yml --clean-known-devices`
|
||||
|
||||
I recommend to use `--clean-known-devices` on the start so EHS-Sentinel will send Configuration messages for HASS Autodiscovery after every startup.
|
||||
|
||||
10. Change your `config.yml` to absolut paths:
|
||||
`nasaRepositoryFile: /root/EHS-Sentinel/data/NasaRepository.yml`
|
||||
@@ -121,16 +136,31 @@ Some Distributions like debian 12 dont allow to use system wide pip package inst
|
||||
|
||||
# Home Assistant Dashboard
|
||||
|
||||
There is a rudimentary dasdboard for Homeassistant, this can be found at: [ressources/dashboard.yaml](ressources/dashboard.yaml)
|
||||
There are two rudimentary dashboard templates for Homeassistant,
|
||||
Read Only [ressources/dashboard_readonly_template.yaml](ressources/dashboard_readonly_template.yaml)
|
||||
|
||||
Control mode [ressources/dashboard_controlmode_template.yaml](ressources/dashboard_controlmode_template.yaml)
|
||||
|
||||
If you have good ideas and want to extend this feel free to create an issue or pull request, thanks!
|
||||
|
||||
## Read Only Mode
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
## Control Mode
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
# Configuration
|
||||
|
||||
## Command-Line Arguments
|
||||
@@ -182,6 +212,16 @@ The `config.yml` file contains configuration settings for the EHS-Sentinel proje
|
||||
- Default: `data/NasaRepository.yml`
|
||||
- **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
|
||||
|
||||
@@ -195,6 +235,10 @@ The `config.yml` file contains configuration settings for the EHS-Sentinel proje
|
||||
- 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...
|
||||
@@ -267,6 +311,7 @@ The data points are defined in the groups section, the group is then enabled in
|
||||
```yaml
|
||||
general:
|
||||
nasaRepositoryFile: data/NasaRepository.yml
|
||||
allowControl: False
|
||||
# protocolFile: prot.csv
|
||||
logging:
|
||||
deviceAdded: True
|
||||
@@ -333,6 +378,30 @@ 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
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
general:
|
||||
nasaRepositoryFile: data/NasaRepository.yml
|
||||
allowControl: False
|
||||
logging:
|
||||
deviceAdded: True
|
||||
messageNotFound: False
|
||||
packetNotFromIndoorOutdoor: False
|
||||
proccessedMessage: False
|
||||
pollerMessage: False
|
||||
controlMessage: False
|
||||
invalidPacket: False
|
||||
#serial:
|
||||
# device: /dev/ttyUSB0
|
||||
# baudrate: 9600
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
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
|
||||
|
||||
@@ -8,6 +17,7 @@ encode_raw = "[50, 0, 60, 16, 0, 0, 176, 0, 255, 192, 20, 196, 13, 2, 2, 255, 25
|
||||
encode_raw = "[50, 0, 56, 98, 0, 144, 178, 0, 32, 192, 17, 3, 11, 64, 147, 0, 64, 148, 0, 66, 115, 0, 0, 66, 116, 0, 0, 66, 117, 0, 0, 66, 118, 0, 0, 66, 119, 0, 0, 66, 120, 0, 0, 66, 121, 0, 0, 66, 122, 0, 0, 66, 123, 0, 0, 221, 200, 52]"
|
||||
encode_raw = "[50, 0, 56, 98, 0, 144, 178, 0, 32, 192, 17, 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:
|
||||
@@ -23,10 +33,10 @@ 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.Outdoor)
|
||||
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)
|
||||
|
||||
159
helpertils/messageFinder.py
Normal 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}")
|
||||
169
helpertils/messageFinderAnalyser.py
Normal 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}")
|
||||
97
helpertils/refreshNasaRepository.py
Normal 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()
|
||||
16
helpertils/smartthings.http
Normal 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
|
||||
462
ressources/dashboard_controlmode_template.yaml
Normal 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
|
||||
@@ -6,38 +6,23 @@ views:
|
||||
sections:
|
||||
- type: grid
|
||||
cards:
|
||||
- type: tile
|
||||
name: Operation mode
|
||||
vertical: true
|
||||
hide_state: false
|
||||
show_entity_picture: false
|
||||
grid_options:
|
||||
columns: 6
|
||||
rows: 2
|
||||
entity: sensor.samsung_ehssentinel_outdooroperationstatus
|
||||
- type: tile
|
||||
entity: binary_sensor.samsung_ehssentinel_controlsilence
|
||||
name: Silent Mode
|
||||
vertical: true
|
||||
hide_state: false
|
||||
show_entity_picture: false
|
||||
- type: tile
|
||||
name: DHW Power
|
||||
vertical: true
|
||||
hide_state: false
|
||||
show_entity_picture: false
|
||||
entity: binary_sensor.samsung_ehssentinel_dhwpower
|
||||
grid_options:
|
||||
columns: 6
|
||||
rows: 2
|
||||
- type: tile
|
||||
name: Defrost Status
|
||||
vertical: true
|
||||
hide_state: false
|
||||
show_entity_picture: false
|
||||
entity: sensor.samsung_ehssentinel_outdoordefroststep
|
||||
- type: entities
|
||||
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
|
||||
@@ -45,6 +30,9 @@ views:
|
||||
- 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
|
||||
@@ -64,9 +52,15 @@ views:
|
||||
secondary_info: last-updated
|
||||
name: Outdoor Temperatur
|
||||
- entity: sensor.samsung_ehssentinel_outdoorcomp1targethz
|
||||
name: Compressor Frequence
|
||||
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:
|
||||
|
Before Width: | Height: | Size: 289 KiB After Width: | Height: | Size: 253 KiB |
|
Before Width: | Height: | Size: 227 KiB After Width: | Height: | Size: 157 KiB |
|
Before Width: | Height: | Size: 502 KiB After Width: | Height: | Size: 445 KiB |
BIN
ressources/images/dashboard_cm1.png
Normal file
|
After Width: | Height: | Size: 208 KiB |
BIN
ressources/images/dashboard_cm2.png
Normal file
|
After Width: | Height: | Size: 293 KiB |
BIN
ressources/images/dashboard_cm3.png
Normal file
|
After Width: | Height: | Size: 254 KiB |
@@ -3,6 +3,7 @@ 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, SkipInvalidPacketException
|
||||
@@ -16,7 +17,7 @@ from CustomLogger import logger
|
||||
from NASAPacket import NASAPacket, AddressClassEnum, PacketType, DataType
|
||||
from NASAMessage import NASAMessage
|
||||
|
||||
version = "0.3.0 Stable"
|
||||
version = "1.0.0 Stable"
|
||||
|
||||
async def main():
|
||||
"""
|
||||
@@ -76,28 +77,6 @@ async def main():
|
||||
await serial_connection(config, args)
|
||||
|
||||
async def process_buffer(buffer, args, config):
|
||||
"""
|
||||
Processes a buffer of data asynchronously, identifying and handling packets based on specific criteria.
|
||||
Args:
|
||||
buffer (list): A list of bytes representing the buffer to be processed.
|
||||
args (Any): Additional arguments to be passed to the packet processing function.
|
||||
Notes:
|
||||
- The function continuously checks the buffer for data.
|
||||
- If the first byte of the buffer is 0x32, it is considered a start byte.
|
||||
- The packet size is determined by combining the second and third bytes of the buffer.
|
||||
- If the buffer contains enough data for a complete packet, the packet is processed.
|
||||
- If the buffer does not contain enough data, the function waits and checks again.
|
||||
- Non-start bytes are removed from the buffer.
|
||||
- The function sleeps for 0.03 seconds between iterations to avoid busy-waiting.
|
||||
Logging:
|
||||
- Logs the buffer size when data is present.
|
||||
- Logs when the start byte is recognized.
|
||||
- Logs the calculated packet size.
|
||||
- Logs the complete packet and the last byte read when a packet is processed.
|
||||
- Logs if the buffer is too small to read a complete packet.
|
||||
- Logs if a received byte is not a start byte.
|
||||
"""
|
||||
|
||||
if buffer:
|
||||
if (len(buffer) > 14):
|
||||
for i in range(0, len(buffer)):
|
||||
@@ -111,18 +90,6 @@ async def process_buffer(buffer, args, config):
|
||||
logger.debug(f"Buffer to short for NASA {len(buffer)}")
|
||||
|
||||
async def serial_connection(config, args):
|
||||
"""
|
||||
Asynchronously reads data from a serial connection and processes it.
|
||||
Args:
|
||||
config (object): Configuration object containing serial or tcp connection parameters.
|
||||
args (object): Additional arguments for buffer processing.
|
||||
This function establishes a serial or tcp connection using parameters from the config object,
|
||||
reads data from the serial port or tcp port until a specified delimiter (0x34) is encountered,
|
||||
and appends the received data to a buffer. It also starts an asynchronous task to
|
||||
process the buffer.
|
||||
The function runs an infinite loop to continuously read data from the serial port/tcp port.
|
||||
"""
|
||||
|
||||
buffer = []
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
@@ -139,109 +106,92 @@ async def serial_connection(config, args):
|
||||
rtscts=True,
|
||||
timeout=1
|
||||
)
|
||||
|
||||
|
||||
await asyncio.gather(
|
||||
serial_read(reader, args, config),
|
||||
serial_write(writer, reader, args, config),
|
||||
serial_write(writer, config),
|
||||
)
|
||||
|
||||
async def serial_read(reader: asyncio.StreamReader, args, config):
|
||||
prev_byte = 0x00
|
||||
packet_started = False
|
||||
data = bytearray()
|
||||
packet_size = 0
|
||||
|
||||
async def serial_read(reader, args, config):
|
||||
while True:
|
||||
data = await reader.readuntil(b'\x34') # Read up to end of next message 0x34
|
||||
if data:
|
||||
asyncio.create_task(process_buffer(data, args, config))
|
||||
#buffer.extend(data)
|
||||
logger.debug(f"Received: {data}")
|
||||
logger.debug(f"Received: {data!r}")
|
||||
logger.debug(f"Received: {[hex(x) for x in data]}")
|
||||
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
|
||||
|
||||
await asyncio.sleep(0.1) # Yield control to other tasks
|
||||
# identify packet start
|
||||
if current_byte == b'\x00' and prev_byte == b'\x32':
|
||||
packet_started = True
|
||||
data.extend(prev_byte)
|
||||
data.extend(current_byte)
|
||||
|
||||
async def serial_write(writer:asyncio.StreamWriter, reader: asyncio.StreamReader, args, config):
|
||||
"""
|
||||
TODO Not used yet, only for future use...
|
||||
prev_byte = current_byte
|
||||
|
||||
#await asyncio.sleep(0.001) # Yield control to other tasks
|
||||
|
||||
|
||||
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
|
||||
"""
|
||||
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(3)
|
||||
asyncio.create_task(make_default_request_packet(writer=writer, config=config, poller=poller))
|
||||
await asyncio.sleep(1)
|
||||
asyncio.create_task(make_default_request_packet(producer=producer, config=config, poller=poller))
|
||||
|
||||
async def make_default_request_packet(writer, config, poller):
|
||||
async def make_default_request_packet(producer: MessageProducer, config: EHSConfig, poller):
|
||||
logger.info(f"Setting up Poller {poller['name']} every {poller['schedule']} seconds")
|
||||
message_list = []
|
||||
for message in config.POLLING['groups'][poller['name']]:
|
||||
tmp_msg = NASAMessage()
|
||||
tmp_msg.set_packet_message(int(config.NASA_REPO[message]['address'], 16))
|
||||
if config.NASA_REPO[message]['type'] == 'ENUM':
|
||||
tmp_msg.set_packet_message_type(0)
|
||||
tmp_msg.set_packet_payload([0])
|
||||
elif config.NASA_REPO[message]['type'] == 'VAR':
|
||||
tmp_msg.set_packet_message_type(1)
|
||||
tmp_msg.set_packet_payload([0, 0])
|
||||
elif config.NASA_REPO[message]['type'] == 'LVAR':
|
||||
tmp_msg.set_packet_message_type(2)
|
||||
tmp_msg.set_packet_payload([0, 0, 0, 0])
|
||||
else:
|
||||
logger.warning(f"Unknown Type for {message} type: {config.NASA_REPO[message]['type']}")
|
||||
break
|
||||
message_list.append(tmp_msg)
|
||||
message_list.append(message)
|
||||
|
||||
while True:
|
||||
chunk_size = 10
|
||||
chunks = [message_list[i:i + chunk_size] for i in range(0, len(message_list), chunk_size)]
|
||||
for chunk in chunks:
|
||||
await asyncio.sleep(1)
|
||||
nasa_msg = NASAPacket()
|
||||
nasa_msg.set_packet_source_address_class(AddressClassEnum.WiFiKit)
|
||||
nasa_msg.set_packet_source_channel(0)
|
||||
nasa_msg.set_packet_source_address(144)
|
||||
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(len(chunk))
|
||||
nasa_msg.set_packet_messages(chunk)
|
||||
final_packet = nasa_msg.to_raw()
|
||||
writer.write(final_packet)
|
||||
await writer.drain()
|
||||
if config.LOGGING['pollerMessage']:
|
||||
logger.info(f"Polling following raw: {[hex(x) for x in final_packet]}")
|
||||
logger.info(f"Polling following NASAPacket: {nasa_msg}")
|
||||
else:
|
||||
logger.debug(f"Sent data raw: {final_packet}")
|
||||
logger.debug(f"Sent data raw: {nasa_msg}")
|
||||
logger.debug(f"Sent data raw: {[hex(x) for x in final_packet]}")
|
||||
logger.debug(f"Sent data raw: {[x for x in final_packet]}")
|
||||
|
||||
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):
|
||||
"""
|
||||
Asynchronously processes a packet buffer.
|
||||
If `dumpWriter` is `None`, it attempts to process the packet using `MessageProcessor`.
|
||||
If a `MessageWarningException` is raised, it logs a warning and skips the packet.
|
||||
If any other exception is raised, it logs an error, skips the packet, and logs the stack trace.
|
||||
If `dumpWriter` is not `None`, it writes the buffer to `dumpWriter`.
|
||||
Args:
|
||||
buffer (bytes): The packet buffer to be processed.
|
||||
"""
|
||||
|
||||
if args.DUMPFILE and not args.DRYRUN:
|
||||
async with aiofiles.open(args.DUMPFILE, "a") as dumpWriter:
|
||||
await dumpWriter.write(f"{buffer}\n")
|
||||
@@ -254,7 +204,7 @@ async def process_packet(buffer, args, config):
|
||||
logger.debug(nasa_packet)
|
||||
if nasa_packet.packet_source_address_class in (AddressClassEnum.Outdoor, AddressClassEnum.Indoor):
|
||||
messageProcessor = MessageProcessor()
|
||||
messageProcessor.process_message(nasa_packet)
|
||||
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:
|
||||
@@ -274,14 +224,17 @@ async def process_packet(buffer, args, config):
|
||||
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}")
|
||||
|
||||