Feature/v1.0.0 release (#12)
v1.0.0 - 2025-03-13 EHS-Sentinel has been heavily modified to incorporate the control mechanism The read-in behavior of the modbus registers has been revised from chunks to single byte MessageProcessor now runs asynchronously MessageProducer added which takes over the writing communication with the WP Configuration of HASS entities has moved from hardcoded to NASA Repository NASA Repository has been fundamentally changed All FSV Values, NASA_POWER, VAR_IN_TEMP_WATER_LAW_TARGET_F, NASA_INDOOR_OPMODE are allowed for writing mode NASA_OUTDOOR_DEFROST_STEP DEFROST STEP 10 (b'0xa') added ENUM_IN_SG_READY_MODE_STATE ACTIVE (b'0x2') added New configuration point allowControl to allow control of the Samsung EHS heat pump (deactivated by default). [!CAUTION] This functionality requires that EHS-Sentinel actively communicates with the Samsung EHS, so EHS-Sentinel intervenes here in the Modbus data traffic between the components (it sends its own messages). The activation of this functionality is exclusively at your own risk. I assume no liability for any damage caused. new configuration points in logging controlMessage (default False) to print out the controlled mesagges invalidPacket (default False) prints out invalid messages (length not ok, x34 not at end...) Dashboard template has been split, ressources/dashboard_readonly_template.yaml is for readonly mode and the ressources/dashboard_controlmode_template.yaml for control mode
This commit is contained in:
@@ -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}")
|
||||
|
||||
Reference in New Issue
Block a user