diff --git a/enocean/__init__.py b/enocean/__init__.py index dcbfb629d..2a0f9c241 100755 --- a/enocean/__init__.py +++ b/enocean/__init__.py @@ -21,172 +21,45 @@ ######################################################################### import serial -import os -import sys -import struct -import time +import logging import threading -from lib.item import Items #what for? -from . import eep_parser -from . import prepare_packet_data -from lib.model.smartplugin import * +from time import sleep + +from lib.model.smartplugin import SmartPlugin +from lib.item import Items + +from .protocol import CRC +from .protocol.eep_parser import EEP_Parser +from .protocol.packet_data import Packet_Data +from .protocol.constants import ( + PACKET, PACKET_TYPE, COMMON_COMMAND, SMART_ACK, EVENT, RETURN_CODE, RORG +) from .webif import WebInterface -FCSTAB = [ - 0x00, 0x07, 0x0e, 0x09, 0x1c, 0x1b, 0x12, 0x15, - 0x38, 0x3f, 0x36, 0x31, 0x24, 0x23, 0x2a, 0x2d, - 0x70, 0x77, 0x7e, 0x79, 0x6c, 0x6b, 0x62, 0x65, - 0x48, 0x4f, 0x46, 0x41, 0x54, 0x53, 0x5a, 0x5d, - 0xe0, 0xe7, 0xee, 0xe9, 0xfc, 0xfb, 0xf2, 0xf5, - 0xd8, 0xdf, 0xd6, 0xd1, 0xc4, 0xc3, 0xca, 0xcd, - 0x90, 0x97, 0x9e, 0x99, 0x8c, 0x8b, 0x82, 0x85, - 0xa8, 0xaf, 0xa6, 0xa1, 0xb4, 0xb3, 0xba, 0xbd, - 0xc7, 0xc0, 0xc9, 0xce, 0xdb, 0xdc, 0xd5, 0xd2, - 0xff, 0xf8, 0xf1, 0xf6, 0xe3, 0xe4, 0xed, 0xea, - 0xb7, 0xb0, 0xb9, 0xbe, 0xab, 0xac, 0xa5, 0xa2, - 0x8f, 0x88, 0x81, 0x86, 0x93, 0x94, 0x9d, 0x9a, - 0x27, 0x20, 0x29, 0x2e, 0x3b, 0x3c, 0x35, 0x32, - 0x1f, 0x18, 0x11, 0x16, 0x03, 0x04, 0x0d, 0x0a, - 0x57, 0x50, 0x59, 0x5e, 0x4b, 0x4c, 0x45, 0x42, - 0x6f, 0x68, 0x61, 0x66, 0x73, 0x74, 0x7d, 0x7a, - 0x89, 0x8e, 0x87, 0x80, 0x95, 0x92, 0x9b, 0x9c, - 0xb1, 0xb6, 0xbf, 0xb8, 0xad, 0xaa, 0xa3, 0xa4, - 0xf9, 0xfe, 0xf7, 0xf0, 0xe5, 0xe2, 0xeb, 0xec, - 0xc1, 0xc6, 0xcf, 0xc8, 0xdd, 0xda, 0xd3, 0xd4, - 0x69, 0x6e, 0x67, 0x60, 0x75, 0x72, 0x7b, 0x7c, - 0x51, 0x56, 0x5f, 0x58, 0x4d, 0x4a, 0x43, 0x44, - 0x19, 0x1e, 0x17, 0x10, 0x05, 0x02, 0x0b, 0x0c, - 0x21, 0x26, 0x2f, 0x28, 0x3d, 0x3a, 0x33, 0x34, - 0x4e, 0x49, 0x40, 0x47, 0x52, 0x55, 0x5c, 0x5b, - 0x76, 0x71, 0x78, 0x7f, 0x6A, 0x6d, 0x64, 0x63, - 0x3e, 0x39, 0x30, 0x37, 0x22, 0x25, 0x2c, 0x2b, - 0x06, 0x01, 0x08, 0x0f, 0x1a, 0x1d, 0x14, 0x13, - 0xae, 0xa9, 0xa0, 0xa7, 0xb2, 0xb5, 0xbc, 0xbb, - 0x96, 0x91, 0x98, 0x9f, 0x8a, 0x8D, 0x84, 0x83, - 0xde, 0xd9, 0xd0, 0xd7, 0xc2, 0xc5, 0xcc, 0xcb, - 0xe6, 0xe1, 0xe8, 0xef, 0xfa, 0xfd, 0xf4, 0xf3 - ] - -################################ -### --- Packet Sync Byte --- ### -################################ -PACKET_SYNC_BYTE = 0x55 # PACKET SYNC BYTE - - -############################ -### --- Packet Types --- ### -############################ - -PACKET_TYPE_RADIO = 0x01 # RADIO ERP1 -PACKET_TYPE_RESPONSE = 0x02 # RESPONSE -PACKET_TYPE_RADIO_SUB_TEL = 0x03 # RADIO_SUB_TEL -PACKET_TYPE_EVENT = 0x04 # EVENT -PACKET_TYPE_COMMON_COMMAND = 0x05 # COMMON COMMAND -PACKET_TYPE_SMART_ACK_COMMAND = 0x06 # SMART ACK COMMAND -PACKET_REMOTE_MAN_COMMAND = 0x07 # REMOTE MANAGEMENT COMMAND -PACKET_TYPE_RADIO_MESSAGE = 0x09 # RADIO MESSAGE -PACKET_TYPE_RADIO_ERP2 = 0x0A # RADIO ERP2 -PACKET_TYPE_RADIO_802_15_4 = 0x10 # RADIO_802_15_4 -PACKET_TYPE_COMMAND_2_4 = 0x11 # COMMAND_2_4 - - -############################################ -### --- List of Common Command Codes --- ### -############################################ - -CO_WR_SLEEP = 0x01 # Order to enter in energy saving mode -CO_WR_RESET = 0x02 # Order to reset the device -CO_RD_VERSION = 0x03 # Read the device (SW) version /(HW) version, chip ID etc. -CO_RD_SYS_LOG = 0x04 # Read system log from device databank -CO_WR_SYS_LOG = 0x05 # Reset System log from device databank -CO_WR_BIST = 0x06 # Perform built in self test -CO_WR_IDBASE = 0x07 # Write ID range base number -CO_RD_IDBASE = 0x08 # Read ID range base number -CO_WR_REPEATER = 0x09 # Write Repeater Level off,1,2 -CO_RD_REPEATER = 0x0A # Read Repeater Level off,1,2 -CO_WR_FILTER_ADD = 0x0B # Add filter to filter list -CO_WR_FILTER_DEL = 0x0C # Delete filter from filter list -CO_WR_FILTER_DEL_ALL = 0x0D # Delete all filter -CO_WR_FILTER_ENABLE = 0x0E # Enable/Disable supplied filters -CO_RD_FILTER = 0x0F # Read supplied filters -CO_WR_WAIT_MATURITY = 0x10 # Waiting till end of maturity time before received radio telegrams will transmitted -CO_WR_SUBTEL = 0x11 # Enable/Disable transmitting additional subtelegram info -CO_WR_MEM = 0x12 # Write x bytes of the Flash, XRAM, RAM0 … -CO_RD_MEM = 0x13 # Read x bytes of the Flash, XRAM, RAM0 …. -CO_RD_MEM_ADDRESS = 0x14 # Feedback about the used address and length of the configarea and the Smart Ack Table -CO_RD_SECURITY = 0x15 # Read own security information (level, key) -CO_WR_SECURITY = 0x16 # Write own security information (level, key) -CO_WR_LEARNMODE = 0x17 # Function: Enables or disables learn mode of Controller. -CO_RD_LEARNMODE = 0x18 # Function: Reads the learn-mode state of Controller. -CO_WR_SECUREDEVICE_ADD = 0x19 # Add a secure device -CO_WR_SECUREDEVICE_DEL = 0x1A # Delete a secure device -CO_RD_SECUREDEVICE_BY_INDEX = 0x1B # Read secure device by index -CO_WR_MODE = 0x1C # Sets the gateway transceiver mode -CO_RD_NUMSECUREDEVICES = 0x1D # Read number of taught in secure devices -CO_RD_SECUREDEVICE_BY_ID = 0x1E # Read secure device by ID -CO_WR_SECUREDEVICE_ADD_PSK = 0x1F # Add Pre-shared key for inbound secure device -CO_WR_SECUREDEVICE_SENDTEACHIN = 0x20 # Send secure Teach-In message -CO_WR_TEMPORARY_RLC_WINDOW = 0x21 # Set the temporary rolling-code window for every taught-in devic -CO_RD_SECUREDEVICE_PSK = 0x22 # Read PSK -CO_RD_DUTYCYCLE_LIMIT = 0x23 # Read parameters of actual duty cycle limit -CO_SET_BAUDRATE = 0x24 # Modifies the baud rate of the EnOcean device -CO_GET_FREQUENCY_INFO = 0x25 # Reads Frequency and protocol of the Device -CO_GET_STEPCODE = 0x27 # Reads Hardware Step code and Revision of the Device - - -################################### -### --- List of Event Codes --- ### -################################### - -SA_RECLAIM_NOT_SUCCESSFUL = 0x01 # Informs the backbone of a Smart Ack Client to not successful reclaim. -SA_CONFIRM_LEARN = 0x02 # Used for SMACK to confirm/discard learn in/out -SA_LEARN_ACK = 0x03 # Inform backbone about result of learn request -CO_READY = 0x04 # Inform backbone about the readiness for operation -CO_EVENT_SECUREDEVICES = 0x05 # Informs about a secure device -CO_DUTYCYCLE_LIMIT = 0x06 # Informs about duty cycle limit -CO_TRANSMIT_FAILED = 0x07 # Informs that the device was not able to send a telegram. - - -########################################### -### --- Smart Acknowledge Defines: --- ### -########################################### - -SA_WR_LEARNMODE = 0x01 # Set/Reset Smart Ack learn mode -SA_RD_LEARNMODE = 0x02 # Get Smart Ack learn mode state -SA_WR_LEARNCONFIRM = 0x03 # Used for Smart Ack to add or delete a mailbox of a client -SA_WR_CLIENTLEARNRQ = 0x04 # Send Smart Ack Learn request (Client) -SA_WR_RESET = 0x05 # Send reset command to a Smart Ack client -SA_RD_LEARNEDCLIENTS = 0x06 # Get Smart Ack learned sensors / mailboxes -SA_WR_RECLAIMS = 0x07 # Set number of reclaim attempts -SA_WR_POSTMASTER = 0x08 # Activate/Deactivate Post master functionality - -SENT_RADIO_PACKET = 0xFF -SENT_ENCAPSULATED_RADIO_PACKET = 0xA6 class EnOcean(SmartPlugin): ALLOW_MULTIINSTANCE = False - PLUGIN_VERSION = "1.4.0" + PLUGIN_VERSION = "1.4.2" - def __init__(self, sh): - """ - Initializes the plugin. - - """ + """ Initializes the plugin. """ # Call init code of parent class (SmartPlugin) super().__init__() - self._sh = sh self.port = self.get_parameter_value("serialport") + self._log_unknown_msg = self.get_parameter_value("log_unknown_messages") + self._connect_retries = self.get_parameter_value("retry") + self._retry_cycle = self.get_parameter_value("retry_cycle") tx_id = self.get_parameter_value("tx_id") - if (len(tx_id) < 8): + if len(tx_id) < 8: self.tx_id = 0 self.logger.warning('No valid enocean stick ID configured. Transmitting is not supported') else: self.tx_id = int(tx_id, 16) self.logger.info(f"Stick TX ID configured via plugin.conf to: {tx_id}") - self._log_unknown_msg = self.get_parameter_value("log_unknown_messages") +# + self._items = [] self._tcm = None self._cmd_lock = threading.Lock() self._response_lock = threading.Condition() @@ -196,421 +69,168 @@ def __init__(self, sh): self.UTE_listen = False self.unknown_sender_id = 'None' self._block_ext_out_msg = False - # call init of eep_parser - self.eep_parser = eep_parser.EEP_Parser() - # call init of prepare_packet_data - self.prepare_packet_data = prepare_packet_data.Prepare_Packet_Data(self) - - self.init_webinterface(WebInterface) - - - def eval_telegram(self, sender_id, data, opt): - logger_debug = self.logger.isEnabledFor(logging.DEBUG) - if logger_debug: - self.logger.debug("Call function << eval_telegram >>") - for item in self._items: - # validate id for item id: - if item.conf['enocean_id'] == sender_id: - #print ("validated {0} for {1}".format(sender_id,item)) - #print ("try to get value for: {0} and {1}".format(item.conf['enocean_rorg'][0],item.conf['enocean_rorg'][1])) - rorg = item.conf['enocean_rorg'] - eval_value = item.conf['enocean_value'] - if rorg in RADIO_PAYLOAD_VALUE: # check if RORG exists - pl = eval(RADIO_PAYLOAD_VALUE[rorg]['payload_idx']) - #could be nicer - for entity in RADIO_PAYLOAD_VALUE: - if (rorg == entity) and (eval_value in RADIO_PAYLOAD_VALUE[rorg]['entities']): - value_dict = RADIO_PAYLOAD_VALUE[rorg]['entities'] - value = eval(RADIO_PAYLOAD_VALUE[rorg]['entities'][eval_value]) - if logger_debug: - self.logger.debug(f"Resulting value: {value} for {item}") - if value: # not sure about this - item(value, self.get_shortname(), 'RADIO') - - def _process_packet_type_event(self, data, optional): - logger_debug = self.logger.isEnabledFor(logging.DEBUG) - if logger_debug: - self.logger.debug("call function << _process_packet_type_event >>") - event_code = data[0] - if(event_code == SA_RECLAIM_NOT_SUCCESSFUL): - self.logger.error("SA reclaim was not successful") - elif(event_code == SA_CONFIRM_LEARN): - self.logger.info("Requesting how to handle confirm/discard learn in/out") - elif(event_code == SA_LEARN_ACK): - self.logger.info("SA lern acknowledged") - elif(event_code == CO_READY): - self.logger.info("Controller is ready for operation") - elif(event_code == CO_TRANSMIT_FAILED): - self.logger.error("Telegram transmission failed") - elif(event_code == CO_DUTYCYCLE_LIMIT): - self.logger.warning("Duty cycle limit reached") - elif(event_code == CO_EVENT_SECUREDEVICES): - self.logger.info("Secure device event packet received") - else: - self.logger.warning("Unknown event packet received") - - def _rocker_sequence(self, item, sender_id, sequence): - logger_debug = self.logger.isEnabledFor(logging.DEBUG) - if logger_debug: - self.logger.debug("Call function << _rocker_sequence >>") - try: - for step in sequence: - event, relation, delay = step.split() - #self.logger.debug("waiting for {} {} {}".format(event, relation, delay)) - if item._enocean_rs_events[event.upper()].wait(float(delay)) != (relation.upper() == "WITHIN"): - if logger_debug: - self.logger.debug(f"NOT {step} - aborting sequence!") - return - else: - if logger_debug: - self.logger.debug(f"{step}") - item._enocean_rs_events[event.upper()].clear() - continue - value = True - if 'enocean_rocker_action' in item.conf: - if item.conf['enocean_rocker_action'].upper() == "UNSET": - value = False - elif item.conf['enocean_rocker_action'].upper() == "TOGGLE": - value = not item() - item(value, self.get_shortname(), "{:08X}".format(sender_id)) - except Exception as e: - self.logger.error(f'Error handling enocean_rocker_sequence \"{sequence}\" - {e}'.format(sequence, e)) - - def _process_packet_type_radio(self, data, optional): - logger_debug = self.logger.isEnabledFor(logging.DEBUG) - if logger_debug: - self.logger.debug("Call function << _process_packet_type_radio >>") - #self.logger.warning("Processing radio message with data = [{}] / optional = [{}]".format(', '.join(['0x%02x' % b for b in data]), ', '.join(['0x%02x' % b for b in optional]))) - - choice = data[0] - payload = data[1:-5] - sender_id = int.from_bytes(data[-5:-1], byteorder='big', signed=False) - status = data[-1] - repeater_cnt = status & 0x0F - self.logger.info("Radio message: choice = {:02x} / payload = [{}] / sender_id = {:08X} / status = {} / repeat = {}".format(choice, ', '.join(['0x%02x' % b for b in payload]), sender_id, status, repeater_cnt)) - - if (len(optional) == 7): - subtelnum = optional[0] - dest_id = int.from_bytes(optional[1:5], byteorder='big', signed=False) - dBm = -optional[5] - SecurityLevel = optional[6] - if logger_debug: - self.logger.debug("Radio message with additional info: subtelnum = {} / dest_id = {:08X} / signal = {}dBm / SecurityLevel = {}".format(subtelnum, dest_id, dBm, SecurityLevel)) - if (choice == 0xD4) and (self.UTE_listen == True): - self.logger.info("Call send_UTE_response") - self._send_UTE_response(data, optional) - if sender_id in self._rx_items: - if logger_debug: - self.logger.debug("Sender ID found in item list") - # iterate over all eep known for this id and get list of associated items - for eep,items in self._rx_items[sender_id].items(): - # check if choice matches first byte in eep (this seems to be the only way to find right eep for this particular packet) - if eep.startswith("{:02X}".format(choice)): - # call parser for particular eep - returns dictionary with key-value pairs - results = self.eep_parser.Parse(eep, payload, status) - if logger_debug: - self.logger.debug(f"Radio message results = {results}") - if 'DEBUG' in results: - self.logger.warning("DEBUG Info: processing radio message with data = [{}] / optional = [{}]".format(', '.join(['0x%02x' % b for b in data]), ', '.join(['0x%02x' % b for b in optional]))) - self.logger.warning(f"Radio message results = {results}") - self.logger.warning("Radio message: choice = {:02x} / payload = [{}] / sender_id = {:08X} / status = {} / repeat = {}".format(choice, ', '.join(['0x%02x' % b for b in payload]), sender_id, status, repeater_cnt)) + self.log_for_debug = self.logger.isEnabledFor(logging.DEBUG) - for item in items: - rx_key = item.conf['enocean_rx_key'].upper() - if rx_key in results: - if 'enocean_rocker_sequence' in item.conf: - try: - if hasattr(item, '_enocean_rs_thread') and item._enocean_rs_thread.is_alive(): - if results[rx_key]: - if logger_debug: - self.logger.debug("Sending pressed event") - item._enocean_rs_events["PRESSED"].set() - else: - if logger_debug: - self.logger.debug("Sending released event") - item._enocean_rs_events["RELEASED"].set() - elif results[rx_key]: - item._enocean_rs_events = {'PRESSED': threading.Event(), 'RELEASED': threading.Event()} - item._enocean_rs_thread = threading.Thread(target=self._rocker_sequence, name="enocean-rs", args=(item, sender_id, item.conf['enocean_rocker_sequence'].split(','), )) - #self.logger.info("starting enocean_rocker_sequence thread") - item._enocean_rs_thread.daemon = True - item._enocean_rs_thread.start() - except Exception as e: - self.logger.error(f"Error handling enocean_rocker_sequence: {e}") - else: - item(results[rx_key], self.get_shortname(), "{:08X}".format(sender_id)) - elif (sender_id <= self.tx_id + 127) and (sender_id >= self.tx_id): - if logger_debug: - self.logger.debug("Received repeated enocean stick message") - else: - self.unknown_sender_id = "{:08X}".format(sender_id) - if self._log_unknown_msg: - self.logger.info("Unknown ID = {:08X}".format(sender_id)) - self.logger.warning("Unknown device sent radio message: choice = {:02x} / payload = [{}] / sender_id = {:08X} / status = {} / repeat = {}".format(choice, ', '.join(['0x%02x' % b for b in payload]), sender_id, status, repeater_cnt)) - - - def _process_packet_type_smart_ack_command(self, data, optional): - self.logger.warning("Smart acknowledge command 0x06 received but not supported at the moment") + # init eep_parser + self.eep_parser = EEP_Parser(self.logger) + # init prepare_packet_data + self.prepare_packet_data = Packet_Data(self) - def _process_packet_type_response(self, data, optional): - logger_debug = self.logger.isEnabledFor(logging.DEBUG) - if logger_debug: - self.logger.debug("Call function << _process_packet_type_response >>") - RETURN_CODES = ['OK', 'ERROR', 'NOT SUPPORTED', 'WRONG PARAM', 'OPERATION DENIED'] - if (self._last_cmd_code == SENT_RADIO_PACKET) and (len(data) == 1): - if logger_debug: - self.logger.debug(f"Sending command returned code = {RETURN_CODES[data[0]]}") - elif (self._last_packet_type == PACKET_TYPE_COMMON_COMMAND) and (self._last_cmd_code == CO_WR_RESET) and (len(data) == 1): - self.logger.info(f"Reset returned code = {RETURN_CODES[data[0]]}") - elif (self._last_packet_type == PACKET_TYPE_COMMON_COMMAND) and (self._last_cmd_code == CO_WR_LEARNMODE) and (len(data) == 1): - self.logger.info(f"Write LearnMode returned code = {RETURN_CODES[data[0]]}") - elif (self._last_packet_type == PACKET_TYPE_COMMON_COMMAND) and (self._last_cmd_code == CO_RD_VERSION): - if (data[0] == 0) and (len(data) == 33): - self.logger.info("Chip ID = 0x{} / Chip Version = 0x{}".format(''.join(['%02x' % b for b in data[9:13]]), ''.join(['%02x' % b for b in data[13:17]]))) - self.logger.info("APP version = {} / API version = {} / App description = {}".format('.'.join(['%d' % b for b in data[1:5]]), '.'.join(['%d' % b for b in data[5:9]]), ''.join(['%c' % b for b in data[17:33]]))) - elif (data[0] == 0) and (len(data) == 0): - self.logger.error("Reading version: No answer") - else: - self.logger.error(f"Reading version returned code = {RETURN_CODES[data[0]]}, length = {len(data)}") - elif (self._last_packet_type == PACKET_TYPE_COMMON_COMMAND) and (self._last_cmd_code == CO_RD_IDBASE): - if (data[0] == 0) and (len(data) == 5): - self.logger.info("Base ID = 0x{}".format(''.join(['%02x' % b for b in data[1:5]]))) - if (self.tx_id == 0): - self.tx_id = int.from_bytes(data[1:5], byteorder='big', signed=False) - self.logger.info("Transmit ID set set automatically by reading chips BaseID") - if (len(optional) == 1): - self.logger.info(f"Remaining write cycles for Base ID = {optional[0]}") - elif (data[0] == 0) and (len(data) == 0): - self.logger.error("Reading Base ID: No answer") - else: - self.logger.error(f"Reading Base ID returned code = {RETURN_CODES[data[0]]} and {len(data)} bytes") - elif (self._last_packet_type == PACKET_TYPE_COMMON_COMMAND) and (self._last_cmd_code == CO_WR_BIST): - if (data[0] == 0) and (len(data) == 2): - if (data[1] == 0): - self.logger.info("Built in self test result: All OK") - else: - self.logger.info(f"Built in self test result: Problem, code = {data[1]}") - elif (data[0] == 0) and (len(data) == 0): - self.logger.error("Doing built in self test: No answer") - else: - self.logger.error(f"Doing built in self test returned code = {RETURN_CODES[data[0]]}") - elif (self._last_packet_type == PACKET_TYPE_COMMON_COMMAND) and (self._last_cmd_code == CO_RD_LEARNMODE): - if (data[0] == 0) and (len(data) == 2): - self.logger.info("Reading LearnMode = 0x{}".format(''.join(['%02x' % b for b in data[1]]))) - if (len(optional) == 1): - self.logger.info("Learn channel = {}".format(optional[0])) - elif (data[0] == 0) and (len(data) == 0): - self.logger.error("Reading LearnMode: No answer") - elif (self._last_packet_type == PACKET_TYPE_COMMON_COMMAND) and (self._last_cmd_code == CO_RD_NUMSECUREDEVICES): - if (data[0] == 0) and (len(data) == 2): - self.logger.info("Number of taught in devices = 0x{}".format(''.join(['%02x' % b for b in data[1]]))) - elif (data[0] == 0) and (len(data) == 0): - self.logger.error("Reading NUMSECUREDEVICES: No answer") - elif (data[0] == 2) and (len(data) == 1): - self.logger.error("Reading NUMSECUREDEVICES: Command not supported") - else: - self.logger.error("Reading NUMSECUREDEVICES: Unknown error") - elif (self._last_packet_type == PACKET_TYPE_SMART_ACK_COMMAND) and (self._last_cmd_code == SA_WR_LEARNMODE): - self.logger.info(f"Setting SmartAck mode returned code = {RETURN_CODES[data[0]]}") - elif (self._last_packet_type == PACKET_TYPE_SMART_ACK_COMMAND) and (self._last_cmd_code == SA_RD_LEARNEDCLIENTS): - if (data[0] == 0): - self.logger.info(f"Number of smart acknowledge mailboxes = {int((len(data)-1)/9)}") - else: - self.logger.error(f"Requesting SmartAck mailboxes returned code = {RETURN_CODES[data[0]]}") - else: - self.logger.error("Processing unexpected response with return code = {} / data = [{}] / optional = [{}]".format(RETURN_CODES[data[0]], ', '.join(['0x%02x' % b for b in data]), ', '.join(['0x%02x' % b for b in optional]))) - self._response_lock.acquire() - self._response_lock.notify() - self._response_lock.release() + # init crc parser + self.crc = CRC() - def _startup(self): - self.logger.debug("Call function << _startup >>") - # request one time information - self.logger.info("Resetting device") - self._send_common_command(CO_WR_RESET) - self.logger.info("Requesting id-base") - self._send_common_command(CO_RD_IDBASE) - self.logger.info("Requesting version information") - self._send_common_command(CO_RD_VERSION) - self.logger.debug("Ending connect-thread") + self.init_webinterface(WebInterface) def run(self): - logger_debug = self.logger.isEnabledFor(logging.DEBUG) - if logger_debug: - self.logger.debug("Call function << run >>") + if self.log_for_debug: + self.logger.debug("Run method called") + self.alive = True self.UTE_listen = False - - # open serial or serial2TCP device: - try: - self._tcm = serial.serial_for_url(self.port, 57600, timeout=1.5) - except Exception as e: - self._tcm = None - self._init_complete = False - self.logger.error(f"Exception occurred during serial open: {e}") - return - else: - self.logger.info(f"Serial port successfully opened at port {self.port}") - - t = threading.Thread(target=self._startup, name="enocean-startup") - # if you need to create child threads, do not make them daemon = True! - # They will not shutdown properly. (It's a python bug) - t.daemon = False - t.start() msg = [] + while self.alive: + + # just try connecting anytime the serial object is not initialized + connect_count = 0 + while self._tcm is None and self.alive: + if self._connect_retries > 0 and connect_count >= self._connect_retries: + self.alive = False + break + if not self.connect(): + connect_count += 1 + self.logger.info(f'connecting failed {connect_count} times. Retrying after 5 seconds...') + sleep(self._retry_cycle) + + # main loop, read from device + readin = None try: readin = self._tcm.read(1000) except Exception as e: - self.logger.error(f"Exception during tcm read occurred: {e}") - break - else: - if readin: - msg += readin - if logger_debug: - self.logger.debug("Data received") - # check if header is complete (6bytes including sync) - # 0x55 (SYNC) + 4bytes (HEADER) + 1byte(HEADER-CRC) - while (len(msg) >= 6): - #check header for CRC - if (msg[0] == PACKET_SYNC_BYTE) and (self._calc_crc8(msg[1:5]) == msg[5]): - # header bytes: sync; length of data (2); optional length; packet type; crc - data_length = (msg[1] << 8) + msg[2] - opt_length = msg[3] - packet_type = msg[4] - msg_length = data_length + opt_length + 7 - if logger_debug: - self.logger.debug("Received header with data_length = {} / opt_length = 0x{:02x} / type = {}".format(data_length, opt_length, packet_type)) - - # break if msg is not yet complete: - if (len(msg) < msg_length): - break - - # msg complete - if (self._calc_crc8(msg[6:msg_length - 1]) == msg[msg_length - 1]): - if logger_debug: - self.logger.debug("Accepted package with type = 0x{:02x} / len = {} / data = [{}]!".format(packet_type, msg_length, ', '.join(['0x%02x' % b for b in msg]))) - data = msg[6:msg_length - (opt_length + 1)] - optional = msg[(6 + data_length):msg_length - 1] - if (packet_type == PACKET_TYPE_RADIO): - self._process_packet_type_radio(data, optional) - elif (packet_type == PACKET_TYPE_SMART_ACK_COMMAND): - self._process_packet_type_smart_ack_command(data, optional) - elif (packet_type == PACKET_TYPE_RESPONSE): - self._process_packet_type_response(data, optional) - elif (packet_type == PACKET_TYPE_EVENT): - self._process_packet_type_event(data, optional) - else: - self.logger.error("Received packet with unknown type = 0x{:02x} - len = {} / data = [{}]".format(packet_type, msg_length, ', '.join(['0x%02x' % b for b in msg]))) + if self.alive: + self.logger.error(f"Exception during tcm read occurred: {e}") + # reset serial device + try: + self._tcm.close() + except Exception: + pass + self._tcm = None + continue + + if readin: + msg += readin + if self.log_for_debug: + self.logger.debug(f"Data received: {readin}") + # check if header is complete (6bytes including sync) + # 0x55 (SYNC) + 4bytes (HEADER) + 1byte(HEADER-CRC) + while len(msg) >= 6: + # check header for CRC + if msg[0] == PACKET.SYNC_BYTE and msg[5] == self.crc(msg[1:5]): + # header bytes: sync; length of data (2); optional length; packet type; crc + data_length = (msg[1] << 8) + msg[2] + opt_length = msg[3] + packet_type = msg[4] + msg_length = data_length + opt_length + 7 + if self.log_for_debug: + self.logger.debug(f"Received header with data_length = {data_length} / opt_length = 0x{opt_length:02x} / type = {packet_type}") + + # break if msg is not yet complete: + if len(msg) < msg_length: + break + + # msg complete + if self.crc(msg[6:msg_length - 1]) == msg[msg_length - 1]: + if self.log_for_debug: + self.logger.debug("Accepted package with type = 0x{:02x} / len = {} / data = [{}]!".format(packet_type, msg_length, ', '.join(['0x%02x' % b for b in msg]))) + data = msg[6:msg_length - (opt_length + 1)] + optional = msg[data_length + 6:msg_length - 1] + if packet_type == PACKET_TYPE.RADIO: + self._process_packet_type_radio(data, optional) + elif packet_type == PACKET_TYPE.SMART_ACK_COMMAND: + self._process_packet_type_smart_ack_command(data, optional) + elif packet_type == PACKET_TYPE.RESPONSE: + self._process_packet_type_response(data, optional) + elif packet_type == PACKET_TYPE.EVENT: + self._process_packet_type_event(data, optional) else: - self.logger.error("Crc error - dumping packet with type = 0x{:02x} / len = {} / data = [{}]!".format(packet_type, msg_length, ', '.join(['0x%02x' % b for b in msg]))) - msg = msg[msg_length:] + self.logger.error("Received packet with unknown type = 0x{:02x} - len = {} / data = [{}]".format(packet_type, msg_length, ', '.join(['0x%02x' % b for b in msg]))) else: - #self.logger.warning("Consuming [0x{:02x}] from input buffer!".format(msg[0])) - msg.pop(0) - try: - self._tcm.close() - except Exception as e: - self.logger.error(f"Exception during tcm close occured: {e}") - else: - self.logger.info(f"Enocean serial device closed") - self.logger.info("Run method stopped") - - def stop(self): - self.logger.debug("Call function << stop >>") - self.alive = False - - def get_tx_id_as_hex(self): - hexstring = "{:08X}".format(self.tx_id) - return hexstring - - def get_serial_status_as_string(self): - if (self._tcm and self._tcm.is_open): - return "open" - else: - return "not connected" - - def get_log_unknown_msg(self): - return self._log_unknown_msg + self.logger.error("Crc error - dumping packet with type = 0x{:02x} / len = {} / data = [{}]!".format(packet_type, msg_length, ', '.join(['0x%02x' % b for b in msg]))) + msg = msg[msg_length:] + else: + # self.logger.warning("Consuming [0x{:02x}] from input buffer!".format(msg[0])) + msg.pop(0) - def toggle_log_unknown_msg(self): - self._log_unknown_msg = not self._log_unknown_msg - - def _send_UTE_response(self, data, optional): - self.logger.debug("Call function << _send_UTE_response >>") - choice = data[0] - payload = data[1:-5] - #sender_id = int.from_bytes(data[-5:-1], byteorder='big', signed=False) - #status = data[-1] - #repeater_cnt = status & 0x0F - SubTel = 0x03 - db = 0xFF - Secu = 0x0 + # self.alive is False or connect error caused loop exit + self.stop() - self._send_radio_packet(self.learn_id, choice, [0x91, payload[1], payload[2], payload[3], payload[4], payload[5], payload[6]],[SubTel, data[-5], data[-4], data[-3], data[-2], db, Secu] )#payload[0] = 0x91: EEP Teach-in response, Request accepted, teach-in successful, bidirectional - self.UTE_listen = False - self.logger.info("Sending UTE response and end listening") + def stop(self): + self.logger.debug("Stop method called") + self.alive = False + self.disconnect() def parse_item(self, item): - self.logger.debug("Call function << parse_item >>") + self.logger.debug("parse_item method called") if 'enocean_rx_key' in item.conf: # look for info from the most specific info to the broadest (key->eep->id) - one id might use multiple eep might define multiple keys eep_item = item found_eep = True - while (not 'enocean_rx_eep' in eep_item.conf): + while 'enocean_rx_eep' not in eep_item.conf: eep_item = eep_item.return_parent() - if (eep_item is self._sh): + if eep_item is Items.get_instance(): self.logger.error(f"Could not find enocean_rx_eep for item {item}") found_eep = False + break id_item = eep_item found_rx_id = True - while (not 'enocean_rx_id' in id_item.conf): + while 'enocean_rx_id' not in id_item.conf: id_item = id_item.return_parent() - if (id_item is self._sh): + if id_item is Items.get_instance(): self.logger.error(f"Could not find enocean_rx_id for item {item}") found_rx_id = False + break # Only proceed, if valid rx_id and eep could be found: if found_rx_id and found_eep: rx_key = item.conf['enocean_rx_key'].upper() rx_eep = eep_item.conf['enocean_rx_eep'].upper() - rx_id = int(id_item.conf['enocean_rx_id'],16) + rx_id = int(id_item.conf['enocean_rx_id'], 16) # check if there is a function to parse payload if self.eep_parser.CanParse(rx_eep): - + if (rx_key in ['A0', 'A1', 'B0', 'B1']): - self.logger.warning(f"Key \"{rx_key}\" does not match EEP - \"0\" (Zero, number) should be \"O\" (letter) (same for \"1\" and \"I\") - will be accepted for now") + self.logger.warning(f'Key "{rx_key}" does not match EEP - "0" (Zero, number) should be "O" (letter) (same for "1" and "I") - will be accepted for now') rx_key = rx_key.replace('0', 'O').replace("1", 'I') - if (not rx_id in self._rx_items): + if rx_id not in self._rx_items: self._rx_items[rx_id] = {rx_eep: [item]} - elif (not rx_eep in self._rx_items[rx_id]): + elif rx_eep not in self._rx_items[rx_id]: self._rx_items[rx_id][rx_eep] = [item] - elif (not item in self._rx_items[rx_id][rx_eep]): + elif item not in self._rx_items[rx_id][rx_eep]: self._rx_items[rx_id][rx_eep].append(item) - self.logger.info("Item {} listens to id {:08X} with eep {} key {}".format(item, rx_id, rx_eep, rx_key)) - #self.logger.info(f"self._rx_items = {self._rx_items}") - + self.logger.info(f"Item {item} listens to id {rx_id:08X} with eep {rx_eep} key {rx_key}") + # self.logger.info(f"self._rx_items = {self._rx_items}") + if 'enocean_tx_eep' in item.conf: self.logger.debug(f"TX eep found in item {item._name}") - - if not 'enocean_tx_id_offset' in item.conf: + + if 'enocean_tx_id_offset' not in item.conf: self.logger.error(f"TX eep found for item {item._name} but no tx id offset specified.") return tx_offset = item.conf['enocean_tx_id_offset'] - if not (tx_offset in self._used_tx_offsets): + if tx_offset not in self._used_tx_offsets: self._used_tx_offsets.append(tx_offset) self._used_tx_offsets.sort() self.logger.debug(f"Debug offset list: {self._used_tx_offsets}") for x in range(1, 127): - if not x in self._used_tx_offsets: + if x not in self._used_tx_offsets: self._unused_tx_offset = x self.logger.debug(f"Next free offset set to {self._unused_tx_offset}") break @@ -620,59 +240,101 @@ def parse_item(self, item): # register item for event handling via smarthomeNG core. Needed for sending control actions: return self.update_item - - def update_item(self, item, caller=None, source=None, dest=None): - logger_debug = self.logger.isEnabledFor(logging.DEBUG) - if logger_debug: - self.logger.debug("Call function << update_item >>") + if self.log_for_debug: + self.logger.debug("update_item method called") - #self.logger.warning(f"Debug: update item: caller: {caller}, shortname: {self.get_shortname()}, item: {item.id()}") + # self.logger.warning(f"Debug: update item: caller: {caller}, shortname: {self.get_shortname()}, item: {item.id()}") if caller != self.get_shortname(): - if logger_debug: - self.logger.debug(f'Item << {item} >> updated externally.') + if self.log_for_debug: + self.logger.debug(f'Item {item} updated externally.') if self._block_ext_out_msg: - self.logger.warning('Sending manually blocked by user. Aborting') - return None + self.logger.warning('Transmitting manually blocked by user. Aborting') + return if 'enocean_tx_eep' in item.conf: if isinstance(item.conf['enocean_tx_eep'], str): tx_eep = item.conf['enocean_tx_eep'] - if logger_debug: + if self.log_for_debug: self.logger.debug(f'item << {item} >> has tx_eep') # check if Data can be Prepared - if not self.prepare_packet_data.CanDataPrepare(tx_eep): + if not self.prepare_packet_data.CanPrepareData(tx_eep): self.logger.error(f'enocean-update_item: method missing for prepare telegram data for {tx_eep}') else: # call method prepare_packet_data(item, tx_eep) id_offset, rorg, payload, optional = self.prepare_packet_data.PrepareData(item, tx_eep) self._send_radio_packet(id_offset, rorg, payload, optional) else: - self.logger.error(f'tx_eep {tx_eep} is not a string value') + self.logger.error('tx_eep is not a string value') else: - if logger_debug: - self.logger.debug(f'Item << {item} >>has no tx_eep value') + if self.log_for_debug: + self.logger.debug(f'Item {item} has no tx_eep value') - def read_num_securedivices(self): - self.logger.debug("Call function << read_num_securedivices >>") - self._send_common_command(CO_RD_NUMSECUREDEVICES) - self.logger.info("Read number of secured devices") + def connect(self, startup=True): + """ open serial or serial2TCP device """ + self.logger.debug(f'trying to connect to device at {self.port}') + try: + self._tcm = serial.serial_for_url(self.port, 57600, timeout=1.5) + except Exception as e: + self._tcm = None + self.logger.error(f"Exception occurred during serial open: {e}") + return False + else: + self.logger.info(f"Serial port successfully opened at port {self.port}") + +# why startup in separate thread? time to startup? collision with receiving? + if startup: + t = threading.Thread(target=self._startup, name="enocean-startup") + t.daemon = False + t.start() + return True + + def disconnect(self): + """ close serial or serial2TCP device """ + try: + self._tcm.close() + except Exception: + pass + self.logger.info("Enocean serial device closed") + + def _startup(self): + """ send startup sequence to device """ + self.logger.debug("_startup method called") + + # request one time information + self.logger.info("Resetting device") + self._send_common_command(COMMON_COMMAND.WR_RESET) + self.logger.info("Requesting id-base") + self._send_common_command(COMMON_COMMAND.RD_IDBASE) + self.logger.info("Requesting version information") + self._send_common_command(COMMON_COMMAND.RD_VERSION) + self.logger.debug("Ending startup-thread") + +# +# public EnOcean interface methods +# + + def read_num_securedevices(self): + """ read number of secure devices """ + self.logger.debug("read_num_securedevices method called") + self._send_common_command(COMMON_COMMAND.RD_NUMSECUREDEVICES) + self.logger.info("Read number of secured devices") - # Request all taught in smart acknowledge devices that have a mailbox def get_smart_ack_devices(self): - self.logger.debug("Call function << get_smart_ack_devices >>") - self._send_smart_ack_command(SA_RD_LEARNEDCLIENTS) + """ request all smart acknowledge devices """ + self.logger.debug("get_smart_ack_devices method called") + self._send_smart_ack_command(SMART_ACK.RD_LEARNEDCLIENTS) self.logger.info("Requesting all available smart acknowledge mailboxes") - def reset_stick(self): - self.logger.debug("Call function << reset_stick >>") + """ reset EnOcean transmitter """ + self.logger.debug("reset_stick method called") self.logger.info("Resetting device") - self._send_common_command(CO_WR_RESET) + self._send_common_command(COMMON_COMMAND.WR_RESET) def block_external_out_messages(self, block=True): - self.logger.debug("Call function << block_external_out_messages >>") + self.logger.debug("block_external_out_messages method called") if block: self.logger.info("Blocking of external out messages activated") self._block_ext_out_msg = True @@ -683,213 +345,435 @@ def block_external_out_messages(self, block=True): self.logger.error("Invalid argument. Must be True/False") def toggle_block_external_out_messages(self): - self.logger.debug("Call function << toggle block_external_out_messages >>") - if self._block_ext_out_msg == False: + self.logger.debug("toggle block_external_out_messages method called") + if not self._block_ext_out_msg: self.logger.info("Blocking of external out messages activated") self._block_ext_out_msg = True else: self.logger.info("Blocking of external out messages deactivated") self._block_ext_out_msg = False - def toggle_UTE_mode(self,id_offset=0): + def toggle_UTE_mode(self, id_offset=0): self.logger.debug("Toggle UTE mode") - if self.UTE_listen == True: + if self.UTE_listen: self.logger.info("UTE mode deactivated") self.UTE_listen = False - elif (id_offset is not None) and not (id_offset == 0): + elif id_offset: self.start_UTE_learnmode(id_offset) - self.logger.info("UTE mode activated for ID offset") + self.logger.info(f"UTE mode activated for ID offset {id_offset}") def send_bit(self): self.logger.info("Trigger Built-In Self Test telegram") - self._send_common_command(CO_WR_BIST) + self._send_common_command(COMMON_COMMAND.WR_BIST) def version(self): self.logger.info("Request stick version") - self._send_common_command(CO_RD_VERSION) + self._send_common_command(COMMON_COMMAND.RD_VERSION) - def _send_packet(self, packet_type, data=[], optional=[]): - #self.logger.debug("Call function << _send_packet >>") - length_optional = len(optional) - if length_optional > 255: - self.logger.error(f"Optional too long ({length_optional} bytes, 255 allowed)") - return None - length_data = len(data) - if length_data > 65535: - self.logger.error(f"Data too long ({length_data} bytes, 65535 allowed)") - return None +# +# Utility methods +# - packet = bytearray([PACKET_SYNC_BYTE]) - packet += length_data.to_bytes(2, byteorder='big') + bytes([length_optional, packet_type]) - packet += bytes([self._calc_crc8(packet[1:5])]) - packet += bytes(data + optional) - packet += bytes([self._calc_crc8(packet[6:])]) - self.logger.info("Sending packet with len = {} / data = [{}]!".format(len(packet), ', '.join(['0x%02x' % b for b in packet]))) - - # Send out serial data: - if not (self._tcm and self._tcm.is_open): - self.logger.debug("Trying serial reinit") - try: - self._tcm = serial.serial_for_url(self.port, 57600, timeout=1.5) - except Exception as e: - self._tcm = None - self.logger.error(f"Exception occurred during serial reinit: {e}") - else: - self.logger.debug("Serial reinit successful") - if self._tcm: - try: - self._tcm.write(packet) - except Exception as e: - self.logger.error(f"Exception during tcm write occurred: {e}") - self.logger.debug("Trying serial reinit after failed write") - try: - self._tcm = serial.serial_for_url(self.port, 57600, timeout=1.5) - except Exception as e: - self._tcm = None - self.logger.error(f"Exception occurred during serial reinit after failed write: {e}") - else: - self.logger.debug("Serial reinit successful after failed write") - try: - self._tcm.write(packet) - except Exception as e: - self.logger.error(f"Exception occurred during tcm write after successful serial reinit: {e}") - - def _send_smart_ack_command(self, _code, data=[]): - #self.logger.debug("Call function << _send_smart_ack_command >>") + def get_tx_id_as_hex(self): + hexstring = "{:08X}".format(self.tx_id) + return hexstring + + def is_connected(self): + return self._tcm and self._tcm.is_open + + def get_serial_status_as_string(self): + return "open" if self.is_connected() else "not connected" + + def get_log_unknown_msg(self): + return self._log_unknown_msg + + def toggle_log_unknown_msg(self): + self._log_unknown_msg = not self._log_unknown_msg + +# +# (private) packet / protocol methods +# + + def _send_smart_ack_command(self, code, data=[]): + # self.logger.debug("_send_smart_ack_command method called") self._cmd_lock.acquire() - self._last_cmd_code = _code - self._last_packet_type = PACKET_TYPE_SMART_ACK_COMMAND - self._send_packet(PACKET_TYPE_SMART_ACK_COMMAND, [_code] + data) + self._last_cmd_code = code + self._last_packet_type = PACKET_TYPE.SMART_ACK_COMMAND + self._send_packet(PACKET_TYPE.SMART_ACK_COMMAND, [code] + data) self._response_lock.acquire() # wait 5sec for response self._response_lock.wait(5) self._response_lock.release() self._cmd_lock.release() - def _send_common_command(self, _code, data=[], optional=[]): - #self.logger.debug("Call function << _send_common_command >>") + def _send_common_command(self, code, data=[], optional=[]): + # self.logger.debug("_send_common_command method called") self._cmd_lock.acquire() - self._last_cmd_code = _code - self._last_packet_type = PACKET_TYPE_COMMON_COMMAND - self._send_packet(PACKET_TYPE_COMMON_COMMAND, [_code] + data, optional) + self._last_cmd_code = code + self._last_packet_type = PACKET_TYPE.COMMON_COMMAND + self._send_packet(PACKET_TYPE.COMMON_COMMAND, [code] + data, optional) self._response_lock.acquire() # wait 5sec for response self._response_lock.wait(5) self._response_lock.release() self._cmd_lock.release() - def _send_radio_packet(self, id_offset, _code, data=[], optional=[]): - #self.logger.debug("Call function << _send_radio_packet >>") + def _send_radio_packet(self, id_offset, code, data=[], optional=[]): + # self.logger.debug("_send_radio_packet method called") if (id_offset < 0) or (id_offset > 127): self.logger.error(f"Invalid base ID offset range. (Is {id_offset}, must be [0 127])") return self._cmd_lock.acquire() - self._last_cmd_code = SENT_RADIO_PACKET - self._send_packet(PACKET_TYPE_RADIO, [_code] + data + list((self.tx_id + id_offset).to_bytes(4, byteorder='big')) + [0x00], optional) + self._last_cmd_code = PACKET.SENT_RADIO + self._send_packet(PACKET_TYPE.RADIO, [code] + data + list((self.tx_id + id_offset).to_bytes(4, byteorder='big')) + [0x00], optional) self._response_lock.acquire() # wait 1sec for response self._response_lock.wait(1) self._response_lock.release() self._cmd_lock.release() - - + def _send_UTE_response(self, data, optional): + self.logger.debug("_send_UTE_response method called") + choice = data[0] + payload = data[1:-5] + # sender_id = int.from_bytes(data[-5:-1], byteorder='big', signed=False) + # status = data[-1] + # repeater_cnt = status & 0x0F + db = 0xFF + Secu = 0x0 + # payload[0] = 0x91: EEP Teach-in response, Request accepted, teach-in successful, bidirectional + self._send_radio_packet(self.learn_id, choice, [0x91, payload[1], payload[2], payload[3], payload[4], payload[5], payload[6]], [PACKET_TYPE.RADIO_SUB_TEL, data[-5], data[-4], data[-3], data[-2], db, Secu] ) + self.UTE_listen = False + self.logger.info("Sent UTE response and ended listening") + + def _rocker_sequence(self, item, sender_id, sequence): + if self.log_for_debug: + self.logger.debug("_rocker_sequence method called") + try: + for step in sequence: + event, relation, delay = step.split() + # self.logger.debug("waiting for {} {} {}".format(event, relation, delay)) + if item._enocean_rs_events[event.upper()].wait(float(delay)) != (relation.upper() == "WITHIN"): + if self.log_for_debug: + self.logger.debug(f"NOT {step} - aborting sequence!") + return + else: + if self.log_for_debug: + self.logger.debug(f"{step}") + item._enocean_rs_events[event.upper()].clear() + continue + value = True + if 'enocean_rocker_action' in item.conf: + if item.conf['enocean_rocker_action'].upper() == "UNSET": + value = False + elif item.conf['enocean_rocker_action'].upper() == "TOGGLE": + value = not item() + item(value, self.get_shortname(), "{:08X}".format(sender_id)) + except Exception as e: + self.logger.error(f'Error handling enocean_rocker_sequence \"{sequence}\" - {e}') + + def _send_packet(self, packet_type, data=[], optional=[]): + # self.logger.debug("_send_packet method called") + length_optional = len(optional) + if length_optional > 255: + self.logger.error(f"Optional too long ({length_optional} bytes, 255 allowed)") + return + length_data = len(data) + if length_data > 65535: + self.logger.error(f"Data too long ({length_data} bytes, 65535 allowed)") + return + + packet = bytearray([PACKET.SYNC_BYTE]) + packet += length_data.to_bytes(2, byteorder='big') + bytes([length_optional, packet_type]) + packet += bytes([self.crc(packet[1:5])]) + packet += bytes(data + optional) + packet += bytes([self.crc(packet[6:])]) + self.logger.info("Sending packet with len = {} / data = [{}]!".format(len(packet), ', '.join(['0x%02x' % b for b in packet]))) + + # check connection, reconnect + if not self.is_connected(): + self.logger.debug("Trying serial reinit") + if not self.connect(startup=False): + self.logger.error('Connection failed, not sending.') + return + try: + self._tcm.write(packet) + return + except Exception as e: + self.logger.error(f"Exception during tcm write occurred: {e}") + self.logger.debug("Trying serial reinit after failed write") + + if not self.connect(startup=False): + self.logger.error('Connection failed again, not sending. Giving up.') + return + + try: + self._tcm.write(packet) + except Exception as e: + self.logger.error(f"Writing failed twice, giving up: {e}") + + def _process_packet_type_event(self, data, optional): + if self.log_for_debug: + self.logger.debug("_process_packet_type_event method called") + event_code = data[0] + if event_code == EVENT.RECLAIM_NOT_SUCCESSFUL: + self.logger.error("SA reclaim was not successful") + elif event_code == EVENT.CONFIRM_LEARN: + self.logger.info("Requesting how to handle confirm/discard learn in/out") + elif event_code == EVENT.LEARN_ACK: + self.logger.info("SA lern acknowledged") + elif event_code == EVENT.READY: + self.logger.info("Controller is ready for operation") + elif event_code == EVENT.TRANSMIT_FAILED: + self.logger.error("Telegram transmission failed") + elif event_code == EVENT.DUTYCYCLE_LIMIT: + self.logger.warning("Duty cycle limit reached") + elif event_code == EVENT.EVENT_SECUREDEVICES: + self.logger.info("Secure device event packet received") + else: + self.logger.warning("Unknown event packet received") + + def _process_packet_type_radio(self, data, optional): + if self.log_for_debug: + self.logger.debug("_process_packet_type_radio method called") + # self.logger.warning("Processing radio message with data = [{}] / optional = [{}]".format(', '.join(['0x%02x' % b for b in data]), ', '.join(['0x%02x' % b for b in optional]))) + + choice = data[0] + payload = data[1:-5] + sender_id = int.from_bytes(data[-5:-1], byteorder='big', signed=False) + status = data[-1] + repeater_cnt = status & 0x0F + self.logger.info("Radio message: choice = {:02x} / payload = [{}] / sender_id = {:08X} / status = {} / repeat = {}".format(choice, ', '.join(['0x%02x' % b for b in payload]), sender_id, status, repeater_cnt)) + + if len(optional) == 7: + subtelnum = optional[0] + dest_id = int.from_bytes(optional[1:5], byteorder='big', signed=False) + dBm = -optional[5] + SecurityLevel = optional[6] + if self.log_for_debug: + self.logger.debug(f"Radio message with additional info: subtelnum = {subtelnum} / dest_id = {dest_id:08X} / signal = {dBm} dBm / SecurityLevel = {SecurityLevel}") + if choice == 0xD4 and self.UTE_listen: + self.logger.info("Call send_UTE_response") + self._send_UTE_response(data, optional) + + if sender_id in self._rx_items: + if self.log_for_debug: + self.logger.debug("Sender ID found in item list") + # iterate over all eep known for this id and get list of associated items + for eep, items in self._rx_items[sender_id].items(): + # check if choice matches first byte in eep (this seems to be the only way to find right eep for this particular packet) + if eep.startswith("{:02X}".format(choice)): + # call parser for particular eep - returns dictionary with key-value pairs + results = self.eep_parser(eep, payload, status) + if self.log_for_debug: + self.logger.debug(f"Radio message results = {results}") + if 'DEBUG' in results: + self.logger.warning("DEBUG Info: processing radio message with data = [{}] / optional = [{}]".format(', '.join(['0x%02x' % b for b in data]), ', '.join(['0x%02x' % b for b in optional]))) + self.logger.warning(f"Radio message results = {results}") + self.logger.warning("Radio message: choice = {:02x} / payload = [{}] / sender_id = {:08X} / status = {} / repeat = {}".format(choice, ', '.join(['0x%02x' % b for b in payload]), sender_id, status, repeater_cnt)) + + for item in items: + rx_key = item.conf['enocean_rx_key'].upper() + if rx_key in results: + if 'enocean_rocker_sequence' in item.conf: + try: + if hasattr(item, '_enocean_rs_thread') and item._enocean_rs_thread.is_alive(): + if results[rx_key]: + if self.log_for_debug: + self.logger.debug("Sending pressed event") + item._enocean_rs_events["PRESSED"].set() + else: + if self.log_for_debug: + self.logger.debug("Sending released event") + item._enocean_rs_events["RELEASED"].set() + elif results[rx_key]: + item._enocean_rs_events = {'PRESSED': threading.Event(), 'RELEASED': threading.Event()} + item._enocean_rs_thread = threading.Thread(target=self._rocker_sequence, name="enocean-rs", args=(item, sender_id, item.conf['enocean_rocker_sequence'].split(','), )) + # self.logger.info("starting enocean_rocker_sequence thread") + item._enocean_rs_thread.start() + except Exception as e: + self.logger.error(f"Error handling enocean_rocker_sequence: {e}") + else: + item(results[rx_key], self.get_shortname(), f"{sender_id:08X}") + elif sender_id <= self.tx_id + 127 and sender_id >= self.tx_id: + if self.log_for_debug: + self.logger.debug("Received repeated enocean stick message") + else: + self.unknown_sender_id = f"{sender_id:08X}" + if self._log_unknown_msg: + self.logger.info(f"Unknown ID = {sender_id:08X}") + self.logger.warning("Unknown device sent radio message: choice = {:02x} / payload = [{}] / sender_id = {:08X} / status = {} / repeat = {}".format(choice, ', '.join(['0x%02x' % b for b in payload]), sender_id, status, repeater_cnt)) + + def _process_packet_type_smart_ack_command(self, data, optional): + self.logger.warning("Smart acknowledge command 0x06 received but not supported at the moment") + + def _process_packet_type_response(self, data, optional): + if self.log_for_debug: + self.logger.debug("_process_packet_type_response method called") + + # handle sent packet + if self._last_cmd_code == PACKET.SENT_RADIO and len(data) == 1: + + if self.log_for_debug: + self.logger.debug(f"Sending command returned code = {RETURN_CODE(data[0])}") + + # handle common commands + elif self._last_packet_type == PACKET_TYPE.COMMON_COMMAND: + + if self._last_cmd_code == COMMON_COMMAND.WR_RESET and len(data) == 1: + self.logger.info(f"Reset returned code = {RETURN_CODE(data[0])}") + + elif self._last_cmd_code == COMMON_COMMAND.WR_LEARNMODE and len(data) == 1: + self.logger.info(f"Write LearnMode returned code = {RETURN_CODE(data[0])}") + + elif self._last_cmd_code == COMMON_COMMAND.RD_VERSION: + if data[0] == 0 and len(data) == 33: + self.logger.info("Chip ID = 0x{} / Chip Version = 0x{}".format(''.join(['%02x' % b for b in data[9:13]]), ''.join(['%02x' % b for b in data[13:17]]))) + self.logger.info("APP version = {} / API version = {} / App description = {}".format('.'.join(['%d' % b for b in data[1:5]]), '.'.join(['%d' % b for b in data[5:9]]), ''.join(['%c' % b for b in data[17:33]]))) + elif data[0] == 0 and len(data) == 0: + self.logger.error("Reading version: No answer") + else: + self.logger.error(f"Reading version returned code = {RETURN_CODE(data[0])}, length = {len(data)}") + + elif self._last_cmd_code == COMMON_COMMAND.RD_IDBASE: + if data[0] == 0 and len(data) == 5: + self.logger.info("Base ID = 0x{}".format(''.join(['%02x' % b for b in data[1:5]]))) + if self.tx_id == 0: + self.tx_id = int.from_bytes(data[1:5], byteorder='big', signed=False) + self.logger.info("Transmit ID set set automatically by reading chips BaseID") + if len(optional) == 1: + self.logger.info(f"Remaining write cycles for Base ID = {optional[0]}") + elif data[0] == 0 and len(data) == 0: + self.logger.error("Reading Base ID: No answer") + else: + self.logger.error(f"Reading Base ID returned code = {RETURN_CODE(data[0])} and {len(data)} bytes") + + elif self._last_cmd_code == COMMON_COMMAND.WR_BIST: + if data[0] == 0 and len(data) == 2: + if data[1] == 0: + self.logger.info("Built in self test result: All OK") + else: + self.logger.info(f"Built in self test result: Problem, code = {data[1]}") + elif data[0] == 0 and len(data) == 0: + self.logger.error("Doing built in self test: No answer") + else: + self.logger.error(f"Doing built in self test returned code = {RETURN_CODE(data[0])}") + + elif self._last_cmd_code == COMMON_COMMAND.RD_LEARNMODE: + if data[0] == 0 and len(data) == 2: + self.logger.info("Reading LearnMode = 0x{}".format(''.join(['%02x' % b for b in data[1]]))) + if len(optional) == 1: + self.logger.info("Learn channel = {}".format(optional[0])) + elif data[0] == 0 and len(data) == 0: + self.logger.error("Reading LearnMode: No answer") + + elif self._last_cmd_code == COMMON_COMMAND.RD_NUMSECUREDEVICES: + if data[0] == 0 and len(data) == 2: + self.logger.info("Number of taught in devices = 0x{}".format(''.join(['%02x' % b for b in data[1]]))) + elif data[0] == 0 and len(data) == 0: + self.logger.error("Reading NUMSECUREDEVICES: No answer") + elif data[0] == 2 and len(data) == 1: + self.logger.error("Reading NUMSECUREDEVICES: Command not supported") + else: + self.logger.error("Reading NUMSECUREDEVICES: Unknown error") + elif self._last_packet_type == PACKET_TYPE.SMART_ACK_COMMAND: + + # handle SmartAck commands + if self._last_cmd_code == SMART_ACK.WR_LEARNMODE: + self.logger.info(f"Setting SmartAck mode returned code = {RETURN_CODE(data[0])}") + + elif self._last_cmd_code == SMART_ACK.RD_LEARNEDCLIENTS: + if data[0] == 0: + self.logger.info(f"Number of smart acknowledge mailboxes = {int((len(data)-1)/9)}") + else: + self.logger.error(f"Requesting SmartAck mailboxes returned code = {RETURN_CODE(data[0])}") + else: + self.logger.error("Processing unexpected response with return code = {} / data = [{}] / optional = [{}]".format(RETURN_CODE(data[0]), ', '.join(['0x%02x' % b for b in data]), ', '.join(['0x%02x' % b for b in optional]))) + + self._response_lock.acquire() + self._response_lock.notify() + self._response_lock.release() + +# +# Definitions of Learn Methods +# -#################################################### -### --- START - Definitions of Learn Methods --- ### -#################################################### def send_learn_protocol(self, id_offset=0, device=10): - self.logger.debug("Call function << send_learn_protocol >>") + self.logger.debug("send_learn_protocol method called") # define RORG - rorg = 0xA5 - + rorg = RORG.BS4 + # check offset range between 0 and 127 - if (id_offset < 0) or (id_offset > 127): + if not 0 <= id_offset <= 127: self.logger.error(f'ID offset with value = {id_offset} out of range (0-127). Aborting.') return False + # device range 10 - 19 --> Learn protocol for switch actuators - elif (device == 10): + if device == 10: + # Prepare Data for Eltako switch FSR61, Eltako FSVA-230V payload = [0xE0, 0x40, 0x0D, 0x80] self.logger.info('Sending learn telegram for switch command with [Device], [ID-Offset], [RORG], [payload] / [{}], [{:#04x}], [{:#04x}], [{}]'.format(device, id_offset, rorg, ', '.join('{:#04x}'.format(x) for x in payload))) + # device range 20 - 29 --> Learn protocol for dim actuators - elif (device == 20): + elif device == 20: + # Only for Eltako FSUD-230V payload = [0x02, 0x00, 0x00, 0x00] self.logger.info('Sending learn telegram for dim command with [Device], [ID-Offset], [RORG], [payload] / [{}], [{:#04x}], [{:#04x}], [{}]'.format(device, id_offset, rorg, ', '.join('{:#04x}'.format(x) for x in payload))) - elif (device == 21): + elif device == 21: + # For Eltako FHK61SSR dim device (EEP A5-38-08) payload = [0xE0, 0x40, 0x0D, 0x80] self.logger.info('Sending learn telegram for dim command with [Device], [ID-Offset], [RORG], [payload] / [{}], [{:#04x}], [{:#04x}], [{}]'.format(device, id_offset, rorg, ', '.join('{:#04x}'.format(x) for x in payload))) - elif (device == 22): + elif device == 22: + # For Eltako FRGBW71L RGB dim devices (EEP 07-3F-7F) payload = [0xFF, 0xF8, 0x0D, 0x87] self.logger.info('Sending learn telegram for rgbw dim command with [Device], [ID-Offset], [RORG], [payload] / [{}], [{:#04x}], [{:#04x}], [{}]'.format(device, id_offset, rorg, ', '.join('{:#04x}'.format(x) for x in payload))) + # device range 30 - 39 --> Learn protocol for radiator valves - elif (device == 30): + elif device == 30: + # Radiator Valve payload = [0x00, 0x00, 0x00, 0x00] self.logger.info('Sending learn telegram for radiator valve with [Device], [ID-Offset], [RORG], [payload] / [{}], [{:#04x}], [{:#04x}], [{}]'.format(device, id_offset, rorg, ', '.join('{:#04x}'.format(x) for x in payload))) + # device range 40 - 49 --> Learn protocol for other actuators - elif (device == 40): + elif device == 40: + # Eltako shutter actor FSB14, FSB61, FSB71 payload = [0xFF, 0xF8, 0x0D, 0x80] self.logger.info('Sending learn telegram for actuator with [Device], [ID-Offset], [RORG], [payload] / [{}], [{:#04x}], [{:#04x}], [{}]'.format(device, id_offset, rorg, ', '.join('{:#04x}'.format(x) for x in payload))) else: - self.logger.error(f'Sending learn telegram with invalid device! Device {device} actually not defined!') + self.logger.error(f'Sending learn telegram with invalid device! Device {device} currently not defined!') return False + # Send radio package self._send_radio_packet(id_offset, rorg, payload) return True - def start_UTE_learnmode(self, id_offset=0): - self.logger.debug("Call function << start_UTE_learnmode >>") + self.logger.debug("start_UTE_learnmode method called") self.UTE_listen = True self.learn_id = id_offset self.logger.info("Listening for UTE package ('D4')") - - + def enter_learn_mode(self, onoff=1): - self.logger.debug("Call function << enter_learn_mode >>") - if (onoff == 1): - self._send_common_command(CO_WR_LEARNMODE, [0x01, 0x00, 0x00, 0x00, 0x00],[0xFF]) + self.logger.debug("enter_learn_mode method called") + if onoff == 1: + self._send_common_command(COMMON_COMMAND.WR_LEARNMODE, [0x01, 0x00, 0x00, 0x00, 0x00], [0xFF]) self.logger.info("Entering learning mode") - return None else: - self._send_common_command(CO_WR_LEARNMODE, [0x00, 0x00, 0x00, 0x00, 0x00],[0xFF]) + self._send_common_command(COMMON_COMMAND.WR_LEARNMODE, [0x00, 0x00, 0x00, 0x00, 0x00], [0xFF]) self.logger.info("Leaving learning mode") - return None - # This function enables/disables the controller's smart acknowledge mode def set_smart_ack_learn_mode(self, onoff=1): - self.logger.debug("Call function << set_smart_ack_learn_mode >>") - if (onoff == 1): - self._send_smart_ack_command(SA_WR_LEARNMODE, [0x01, 0x00, 0x00, 0x00, 0x00, 0x00]) + self.logger.debug("set_smart_ack_learn_mode method called") + if onoff == 1: + self._send_smart_ack_command(SMART_ACK.WR_LEARNMODE, [0x01, 0x00, 0x00, 0x00, 0x00, 0x00]) self.logger.info("Enabling smart acknowledge learning mode") - return None else: - self._send_smart_ack_command(SA_WR_LEARNMODE, [0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) + self._send_smart_ack_command(SMART_ACK.WR_LEARNMODE, [0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) self.logger.info("Disabling smart acknowledge learning mode") - return None - -################################################## -### --- END - Definitions of Learn Methods --- ### -################################################## - - -################################# -### --- START - Calc CRC8 --- ### -################################# - def _calc_crc8(self, msg, crc=0): - #self.logger.debug("Call function << _calc_crc8 >>") - for i in msg: - crc = FCSTAB[crc ^ i] - return crc - -############################### -### --- END - Calc CRC8 --- ### -############################### - - diff --git a/enocean/plugin.yaml b/enocean/plugin.yaml index 1d677fb49..443a10827 100755 --- a/enocean/plugin.yaml +++ b/enocean/plugin.yaml @@ -16,11 +16,11 @@ plugin: # url of the support thread support: https://knx-user-forum.de/forum/supportforen/smarthome-py/26542-featurewunsch-enocean-plugin/page13 - version: 1.4.0 # Plugin version - sh_minversion: '1.3' # minimum shNG version to use this plugin + version: 1.4.2 # Plugin version + sh_minversion: '1.9' # minimum shNG version to use this plugin #sh_maxversion: # maximum shNG version to use this plugin (leave empty if latest) - multi_instance: False # plugin supports multi instance - restartable: unknown + multi_instance: false # plugin supports multi instance + restartable: true classname: EnOcean # class containing the plugin parameters: @@ -46,6 +46,19 @@ parameters: en: 'Log messages from unknown devices to logfile' default: 'False' + retry: + type: int + description: + de: 'Anzahl der Verbindungsversuche (0 = kein Limit)' + en: 'Number of connect retries (0 = no limit)' + default: 10 + + retry_cycle: + type: int + description: + de: 'Pause zwischen Verbindungsversuchen (in Sekunden)' + en: 'pause interval between connect retries (in seconds)' + default: 5 item_attributes: # Definition of item attributes defined by this plugin diff --git a/enocean/protocol/__init__.py b/enocean/protocol/__init__.py new file mode 100644 index 000000000..c54f0df93 --- /dev/null +++ b/enocean/protocol/__init__.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +# vim: set encoding=utf-8 tabstop=4 softtabstop=4 shiftwidth=4 expandtab +######################################################################### +######################################################################### +# Enocean plugin for SmartHomeNG. https://github.com/smarthomeNG// +# +# This plugin is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This plugin is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this plugin. If not, see . +######################################################################### + +# this module contains EnOcean protocol routines + + +class CRC(): + """ provides CRC calculations """ + + CRC_TABLE = ( + 0x00, 0x07, 0x0e, 0x09, 0x1c, 0x1b, 0x12, 0x15, + 0x38, 0x3f, 0x36, 0x31, 0x24, 0x23, 0x2a, 0x2d, + 0x70, 0x77, 0x7e, 0x79, 0x6c, 0x6b, 0x62, 0x65, + 0x48, 0x4f, 0x46, 0x41, 0x54, 0x53, 0x5a, 0x5d, + 0xe0, 0xe7, 0xee, 0xe9, 0xfc, 0xfb, 0xf2, 0xf5, + 0xd8, 0xdf, 0xd6, 0xd1, 0xc4, 0xc3, 0xca, 0xcd, + 0x90, 0x97, 0x9e, 0x99, 0x8c, 0x8b, 0x82, 0x85, + 0xa8, 0xaf, 0xa6, 0xa1, 0xb4, 0xb3, 0xba, 0xbd, + 0xc7, 0xc0, 0xc9, 0xce, 0xdb, 0xdc, 0xd5, 0xd2, + 0xff, 0xf8, 0xf1, 0xf6, 0xe3, 0xe4, 0xed, 0xea, + 0xb7, 0xb0, 0xb9, 0xbe, 0xab, 0xac, 0xa5, 0xa2, + 0x8f, 0x88, 0x81, 0x86, 0x93, 0x94, 0x9d, 0x9a, + 0x27, 0x20, 0x29, 0x2e, 0x3b, 0x3c, 0x35, 0x32, + 0x1f, 0x18, 0x11, 0x16, 0x03, 0x04, 0x0d, 0x0a, + 0x57, 0x50, 0x59, 0x5e, 0x4b, 0x4c, 0x45, 0x42, + 0x6f, 0x68, 0x61, 0x66, 0x73, 0x74, 0x7d, 0x7a, + 0x89, 0x8e, 0x87, 0x80, 0x95, 0x92, 0x9b, 0x9c, + 0xb1, 0xb6, 0xbf, 0xb8, 0xad, 0xaa, 0xa3, 0xa4, + 0xf9, 0xfe, 0xf7, 0xf0, 0xe5, 0xe2, 0xeb, 0xec, + 0xc1, 0xc6, 0xcf, 0xc8, 0xdd, 0xda, 0xd3, 0xd4, + 0x69, 0x6e, 0x67, 0x60, 0x75, 0x72, 0x7b, 0x7c, + 0x51, 0x56, 0x5f, 0x58, 0x4d, 0x4a, 0x43, 0x44, + 0x19, 0x1e, 0x17, 0x10, 0x05, 0x02, 0x0b, 0x0c, + 0x21, 0x26, 0x2f, 0x28, 0x3d, 0x3a, 0x33, 0x34, + 0x4e, 0x49, 0x40, 0x47, 0x52, 0x55, 0x5c, 0x5b, + 0x76, 0x71, 0x78, 0x7f, 0x6A, 0x6d, 0x64, 0x63, + 0x3e, 0x39, 0x30, 0x37, 0x22, 0x25, 0x2c, 0x2b, + 0x06, 0x01, 0x08, 0x0f, 0x1a, 0x1d, 0x14, 0x13, + 0xae, 0xa9, 0xa0, 0xa7, 0xb2, 0xb5, 0xbc, 0xbb, + 0x96, 0x91, 0x98, 0x9f, 0x8a, 0x8D, 0x84, 0x83, + 0xde, 0xd9, 0xd0, 0xd7, 0xc2, 0xc5, 0xcc, 0xcb, + 0xe6, 0xe1, 0xe8, 0xef, 0xfa, 0xfd, 0xf4, 0xf3 + ) + + def __call__(self, msg, crc=0): + for i in msg: + crc = self.CRC_TABLE[crc ^ i] + return crc diff --git a/enocean/protocol/constants.py b/enocean/protocol/constants.py new file mode 100644 index 000000000..6f74c9c65 --- /dev/null +++ b/enocean/protocol/constants.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python3 +# vim: set encoding=utf-8 tabstop=4 softtabstop=4 shiftwidth=4 expandtab +######################################################################### +######################################################################### +# Enocean plugin for SmartHomeNG. https://github.com/smarthomeNG// +# +# This plugin is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This plugin is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this plugin. If not, see . +######################################################################### + +from enum import IntEnum + + +class PACKET(IntEnum): + """ generic packet identifiers """ + SYNC_BYTE = 0x55 + SENT_RADIO = 0xFF + SENT_ENCAPSULATED_RADIO = 0xA6 + + +class RORG(IntEnum): + """ encapsulates EEP types from EnOcean Equipment Profiles v2.61 """ + UNDEFINED = 0x00 + RPS = 0xF6 + BS1 = 0xD5 + BS4 = 0xA5 + VLD = 0xD2 + MSC = 0xD1 + ADT = 0xA6 + SM_LRN_REQ = 0xC6 + SM_LRN_ANS = 0xC7 + SM_REC = 0xA7 + SYS_EX = 0xC5 + SEC = 0x30 + SEC_ENCAPS = 0x31 + UTE = 0xD4 + + +class PACKET_TYPE(IntEnum): + """ encapsulates packet types """ + RESERVED = 0x00 + RADIO = 0x01 # RADIO ERP1 + RADIO_ERP1 = 0x01 # RADIO ERP1 => Kept for backwards compatibility reasons, for example custom packet. Generation shouldn't be affected... + RESPONSE = 0x02 # RESPONSE + RADIO_SUB_TEL = 0x03 # RADIO_SUB_TEL + EVENT = 0x04 # EVENT + COMMON_COMMAND = 0x05 # COMMON COMMAND + SMART_ACK_COMMAND = 0x06 # SMART ACK COMMAND + REMOTE_MAN_COMMAND = 0x07 # REMOTE MANAGEMENT COMMAND + RADIO_MESSAGE = 0x09 # RADIO MESSAGE + RADIO_ERP2 = 0x0A # RADIO ERP2 + RADIO_802_15_4 = 0x10 # RADIO_802_15_4_RAW_Packet + COMMAND_2_4 = 0x11 # COMMAND 2.4 GHz + + +class EVENT(IntEnum): + """ encapsulates Event Codes """ + RECLAIM_NOT_SUCCESSFUL = 0x01 # Informs the backbone of a Smart Ack Client to not successful reclaim. + CONFIRM_LEARN = 0x02 # Used for SMACK to confirm/discard learn in/out + LEARN_ACK = 0x03 # Inform backbone about result of learn request + READY = 0x04 # Inform backbone about the readiness for operation + EVENT_SECUREDEVICES = 0x05 # Informs about a secure device + DUTYCYCLE_LIMIT = 0x06 # Informs about duty cycle limit + TRANSMIT_FAILED = 0x07 # Informs that the device was not able to send a telegram. + TX_DONE = 0x08 # Informs the external host that the device has finished all transmissions. + LRN_MODE_DISABLED = 0x09 # Informs the external host that the learn mode has been disabled due to timeout. + + +class COMMON_COMMAND(IntEnum): + """ encapsulates Common Command Codes """ + WR_SLEEP = 0x01 # Enter in energy saving mode + WR_RESET = 0x02 # Reset the device + RD_VERSION = 0x03 # Read the device (SW) version /(HW) version, chip ID etc. + RD_SYS_LOG = 0x04 # Read system log from device databank + WR_SYS_LOG = 0x05 # Reset System log from device databank + WR_BIST = 0x06 # Perform built in self test + WR_IDBASE = 0x07 # Write ID range base number + RD_IDBASE = 0x08 # Read ID range base number + WR_REPEATER = 0x09 # Write Repeater Level off,1,2 + RD_REPEATER = 0x0A # Read Repeater Level off,1,2 + WR_FILTER_ADD = 0x0B # Add filter to filter list + WR_FILTER_DEL = 0x0C # Delete filter from filter list + WR_FILTER_DEL_ALL = 0x0D # Delete all filter + WR_FILTER_ENABLE = 0x0E # Enable/Disable supplied filters + RD_FILTER = 0x0F # Read supplied filters + WR_WAIT_MATURITY = 0x10 # Waiting till end of maturity time before received radio telegrams will transmitted + WR_SUBTEL = 0x11 # Enable/Disable transmitting additional subtelegram info + WR_MEM = 0x12 # Write x bytes of the Flash, XRAM, RAM0 … + RD_MEM = 0x13 # Read x bytes of the Flash, XRAM, RAM0 …. + RD_MEM_ADDRESS = 0x14 # Feedback about the used address and length of the configarea and the Smart Ack Table + RD_SECURITY = 0x15 # Read own security information (level, key) + WR_SECURITY = 0x16 # Write own security information (level, key) + WR_LEARNMODE = 0x17 # Function: Enables or disables learn mode of Controller. + RD_LEARNMODE = 0x18 # Function: Reads the learn-mode state of Controller. + WR_SECUREDEVICE_ADD = 0x19 # Add a secure device + WR_SECUREDEVICE_DEL = 0x1A # Delete a secure device + RD_SECUREDEVICE_BY_INDEX = 0x1B # Read secure device by index + WR_MODE = 0x1C # Sets the gateway transceiver mode + RD_NUMSECUREDEVICES = 0x1D # Read number of taught in secure devices + RD_SECUREDEVICE_BY_ID = 0x1E # Read secure device by ID + WR_SECUREDEVICE_ADD_PSK = 0x1F # Add Pre-shared key for inbound secure device + WR_SECUREDEVICE_SENDTEACHIN = 0x20 # Send secure Teach-In message + WR_TEMPORARY_RLC_WINDOW = 0x21 # Set the temporary rolling-code window for every taught-in devic + RD_SECUREDEVICE_PSK = 0x22 # Read PSK + RD_DUTYCYCLE_LIMIT = 0x23 # Read parameters of actual duty cycle limit + SET_BAUDRATE = 0x24 # Modifies the baud rate of the EnOcean device + GET_FREQUENCY_INFO = 0x25 # Reads Frequency and protocol of the Device + GET_STEPCODE = 0x27 # Reads Hardware Step code and Revision of the Device + WR_REMAN_CODE = 0x2E # Set the security code to unlock Remote Management functionality via radio + WR_STARTUP_DELAY = 0x2F # Set the startup delay (time from power up until start of operation) + WR_REMAN_REPEATING = 0x30 # Select if REMAN telegrams originating from this module can be repeated + RD_REMAN_REPEATING = 0x31 # Check if REMAN telegrams originating from this module can be repeated + SET_NOISETHRESHOLD = 0x32 # Set the RSSI noise threshold level for telegram reception + GET_NOISETHRESHOLD = 0x33 # Read the RSSI noise threshold level for telegram reception + WR_RLC_SAVE_PERIOD = 0x36 # Set the period in which outgoing RLCs are saved to the EEPROM + WR_RLC_LEGACY_MODE = 0x37 # Activate the legacy RLC security mode allowing roll-over and using the RLC acceptance window for 24bit explicit RLC + WR_SECUREDEVICEV2_ADD = 0x38 # Add secure device to secure link table + RD_SECUREDEVICEV2_BY_INDEX = 0x39 # Read secure device from secure link table using the table index + WR_RSSITEST_MODE = 0x3A # Control the state of the RSSI-Test mode + RD_RSSITEST_MODE = 0x3B # Read the state of the RSSI-Test mode + WR_SECUREDEVICE_MAINTENANCEKEY = 0x3C # Add the maintenance key information into the secure link table + RD_SECUREDEVICE_MAINTENANCEKEY = 0x3D # Read by index the maintenance key information from the secure link table + WR_TRANSPARENT_MODE = 0x3E # Control the state of the transparent mode + RD_TRANSPARENT_MODE = 0x3F # Read the state of the transparent mode + WR_TX_ONLY_MODE = 0x40 # Control the state of the TX only mode + RD_TX_ONLY_MODE = 0x41 # Read the state of the TX only mode + + +class SMART_ACK(IntEnum): + """ encapsulates Smart Acknowledge codes """ + WR_LEARNMODE = 0x01 # Set/Reset Smart Ack learn mode + RD_LEARNMODE = 0x02 # Get Smart Ack learn mode state + WR_LEARNCONFIRM = 0x03 # Used for Smart Ack to add or delete a mailbox of a client + WR_CLIENTLEARNRQ = 0x04 # Send Smart Ack Learn request (Client) + WR_RESET = 0x05 # Send reset command to a Smart Ack client + RD_LEARNEDCLIENTS = 0x06 # Get Smart Ack learned sensors / mailboxes + WR_RECLAIMS = 0x07 # Set number of reclaim attempts + WR_POSTMASTER = 0x08 # Activate/Deactivate Post master functionality + + +class RETURN_CODE(IntEnum): + """ encapsulates return codes """ + OK = 0x00 + ERROR = 0x01 + NOT_SUPPORTED = 0x02 + WRONG_PARAM = 0x03 + OPERATION_DENIED = 0x04 + + +class PARSE_RESULT(IntEnum): + """ encapsulates parsing return codes """ + OK = 0x00 + INCOMPLETE = 0x01 + CRC_MISMATCH = 0x03 diff --git a/enocean/eep_parser.py b/enocean/protocol/eep_parser.py similarity index 71% rename from enocean/eep_parser.py rename to enocean/protocol/eep_parser.py index e017f7014..d9d90def3 100755 --- a/enocean/eep_parser.py +++ b/enocean/protocol/eep_parser.py @@ -1,26 +1,58 @@ +#!/usr/bin/env python3 +# vim: set encoding=utf-8 tabstop=4 softtabstop=4 shiftwidth=4 expandtab +######################################################################### +# Copyright 2013-2014 Robert Budde robert@ing-budde.de +# Copyright 2014 Alexander Schwithal aschwith +######################################################################### +# Enocean plugin for SmartHomeNG. https://github.com/smarthomeNG// +# +# This plugin is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This plugin is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this plugin. If not, see . +######################################################################### + import logging + class EEP_Parser(): - def __init__(self): + def __init__(self, plg_logger=None): self.logger = logging.getLogger(__name__) - self.logger.info('Eep-parser instantiated') + self.logger.info('EEP-parser instantiated') + + # create plugin logger for errors + self.plg_logger = plg_logger + if not self.plg_logger: + self.plg_logger = self.logger def CanParse(self, eep): found = callable(getattr(self, "_parse_eep_" + eep, None)) - if (not found): + if not found: self.logger.error(f"eep-parser: missing parser for eep {eep} - there should be a _parse_eep_{eep}-function!") return found - def Parse(self, eep, payload, status): - #self.logger.debug('Parser called with eep = {} / payload = {} / status = {}'.format(eep, ', '.join(hex(x) for x in payload), hex(status))) - results = getattr(self, "_parse_eep_" + eep)(payload, status) - #self.logger.info('Parser returns {results}') + def __call__(self, eep, payload, status): + # self.logger.debug('Parser called with eep = {} / payload = {} / status = {}'.format(eep, ', '.join(hex(x) for x in payload), hex(status))) + try: + results = getattr(self, "_parse_eep_" + eep)(payload, status) + except Exception as e: + self.plg_logger.warning(f'EEP-Parser: error on parsing eep {eep}: {e}') + return + + # self.logger.info('Parser returns {results}') return results -##################################################### -### --- Definitions for RORG = A5 / ORG = 07 --- ### -##################################################### +# Definitions for RORG = A5 / ORG = 07 + def _parse_eep_A5_02_01(self, payload, status): return {'TMP': (0 - (payload[2] * 40 / 255))} @@ -112,23 +144,23 @@ def _parse_eep_A5_04_02(self, payload, status): # temperature in degree Celsius from -20.0 degC - 60degC result['TMP'] = -20.0 + (payload[2] / 250.0 * 80.0) return result - + def _parse_eep_A5_06_01(self, payload, status): # Brightness sensor, for example Eltako FAH60 self.logger.debug('Parsing A5_06_01: Brightness sensor') result = {} # Calculation of brightness in lux - if (payload[3] == 0x0F) and (payload[1] > 0x00) and (payload[1] <= 0xFF): + if payload[3] == 0x0F and payload[1] > 0x00 and payload[1] <= 0xFF: # If Data-Messege AND DataByte 2 is between: 0x00 = 300 lux and 0xFF = 30.000 lux result['BRI'] = round(((payload[1] / 255.0 * (30000 - 300)) + 300), 2) - elif (payload[3] == 0x0F) and (payload[1] == 0x00): + elif payload[3] == 0x0F and payload[1] == 0x00: # If Data-Messege AND DataByte 2: 0x00 then read DataByte 3 - result['BRI'] = (payload[0]) + result['BRI'] = payload[0] else: # No Data Message - result['BRI'] = (-1) + result['BRI'] = -1 # only trigger the logger info when 'BRI' > 0 - if (result['BRI'] > 0): + if result['BRI'] > 0: self.logger.info(f"Brightness: {result['BRI']}") return result @@ -136,7 +168,7 @@ def _parse_eep_A5_07_03(self, payload, status): # Occupancy sensor with supply voltage monitor, NodOne self.logger.debug("Parsing A5_07_03: Occupancy sensor") result = {} - is_data = ((payload[3] & 0x08) == 0x08) # learn or data telegeram: 1:data, 0:learn + is_data = (payload[3] & 0x08) == 0x08 # learn or data telegeram: 1:data, 0:learn if not is_data: self.logger.info("Occupancy sensor: Received learn telegram.") return result @@ -144,9 +176,9 @@ def _parse_eep_A5_07_03(self, payload, status): if payload[0] > 250: self.logger.error(f"Occupancy sensor issued error code: {payload[0]}") else: - result['SVC'] = (payload[0] / 255.0 * 5.0) # supply voltage in volts - result['ILL'] = (payload[1] << 2) + ((payload[2] & 0xC0) >> 6) # 10 bit illumination in lux - result['PIR'] = ((payload[3] & 0x80) == 0x80) # Movement flag, 1:motion detected + result['SVC'] = payload[0] / 255.0 * 5.0 # supply voltage in volts + result['ILL'] = payload[1] << 2 + (payload[2] & 0xC0) >> 6 # 10 bit illumination in lux + result['PIR'] = (payload[3] & 0x80) == 0x80 # Movement flag, 1:motion detected self.logger.debug(f"Occupancy: PIR:{result['PIR']} illumination: {result['ILL']}lx, voltage: {result['SVC']}V") return result @@ -154,9 +186,9 @@ def _parse_eep_A5_08_01(self, payload, status): # Brightness and movement sensor, for example eltako FBH65TFB self.logger.debug("Parsing A5_08_01: Movement sensor") result = {} - result['BRI'] = (payload[1] / 255.0 * 2048) # brightness in lux - result['MOV'] = not ((payload[3] & 0x02) == 0x02) # movement - #self.logger.debug(f"Movement: {result['MOV']}, brightness: {result['BRI']}") + result['BRI'] = payload[1] / 255.0 * 2048 # brightness in lux + result['MOV'] = not (payload[3] & 0x02) == 0x02 # movement + # self.logger.debug(f"Movement: {result['MOV']}, brightness: {result['BRI']}") return result def _parse_eep_A5_11_04(self, payload, status): @@ -168,14 +200,14 @@ def _parse_eep_A5_11_04(self, payload, status): # Data_byte0 = 0x08 = Dimmer aus, 0x09 = Dimmer an self.logger.debug("Processing A5_11_04: Dimmer Status on/off") results = {} - # if !( (payload[0] == 0x02) and (payload[2] == 0x00)): + # if !( (payload[0] == 0x02 and payload[2] == 0x00)): # self.logger.error("Error in processing A5_11_04: static byte missmatch") # return results results['D'] = payload[1] - if (payload[3] == 0x08): + if payload[3] == 0x08: # Dimmer is off results['STAT'] = 0 - elif (payload[3] == 0x09): + elif payload[3] == 0x09: # Dimmer is on results['STAT'] = 1 return results @@ -184,40 +216,39 @@ def _parse_eep_A5_12_01(self, payload, status): # Status command from switch actor with powermeter, for example Eltako FSVA-230 results = {} status_byte = payload[3] - is_data = (status_byte & 0x08) == 0x08 - if(is_data == False): + is_data = status_byte & 0x08 == 0x08 + if is_data is False: self.logger.debug("Processing A5_12_01: powermeter: is learn telegram. Aborting.") return results - is_power = (status_byte & 0x04) == 0x04 - div_enum = (status_byte & 0x03) + is_power = status_byte & 0x04 == 0x04 + div_enum = status_byte & 0x03 divisor = 1.0 - if(div_enum == 0): + if div_enum == 0: divisor = 1.0 - elif(div_enum == 1): + elif div_enum == 1: divisor = 10.0 - elif(div_enum == 2): + elif div_enum == 2: divisor = 100.0 - elif(div_enum == 3): + elif div_enum == 3: divisor = 1000.0 - else: + else: self.logger.warning(f"Processing A5_12_01: Unknown enum ({div_enum}) for divisor") self.logger.debug(f"Processing A5_12_01: divisor is {divisor}") - if(is_power): + if is_power: self.logger.debug("Processing A5_12_01: powermeter: Unit is Watts") else: self.logger.debug("Processing A5_12_01: powermeter: Unit is kWh") - value = (payload[0] << 16) + (payload[1] << 8) + payload[2] - value = value / divisor + value = (payload[0] << 16 + payload[1] << 8 + payload[2]) / divisor self.logger.debug(f"Processing A5_12_01: powermeter: {value} W") # It is confirmed by Eltako that with the use of multiple repeaters in an Eltako network, values can be corrupted in random cases. # Catching these random errors via plausibility check: if value > 2300: self.logger.warning(f"A5_12_01 plausibility error: power value {value} is greater than 2300W, which is not plausible. Skipping.") - #self.logger.warning(f"A5_12_01 exception: value {value}, divisor {divisor}, divenum {div_enum}, statusPayload {status_byte}, header status {status}") - #self.logger.warning(f"A5_12_01 exception: payloads 0-3: {payload[0]},{payload[1]},{payload[2]},{payload[3]}") + # self.logger.warning(f"A5_12_01 exception: value {value}, divisor {divisor}, divenum {div_enum}, statusPayload {status_byte}, header status {status}") + # self.logger.warning(f"A5_12_01 exception: payloads 0-3: {payload[0]},{payload[1]},{payload[2]},{payload[3]}") results['DEBUG'] = 1 return results @@ -229,24 +260,24 @@ def _parse_eep_A5_20_04(self, payload, status): self.logger.debug("Processing A5_20_04") results = {} status_byte = payload[3] - #1: temperature setpoint, 0: feed temperature - TS = ((status_byte & 1 << 6) == 1 << 6) - #1: failure, 0: normal - FL = ((status_byte & 1 << 7) == 1 << 7) - #1: locked, 0: unlocked - BLS= ((status_byte& 1 << 5) == 1 << 5) + # 1: temperature setpoint, 0: feed temperature + TS = status_byte & 1 << 6 == 1 << 6 + # 1: failure, 0: normal + FL = status_byte & 1 << 7 == 1 << 7 + # 1: locked, 0: unlocked + BLS = status_byte & 1 << 5 == 1 << 5 results['BLS'] = BLS # current valve position 0-100% results['CP'] = payload[0] # Current feet temperature or setpoint - if(TS == 1): - results['TS'] = 10 + (payload[1]/255*20) + if TS == 1: + results['TS'] = 10 + payload[1] / 255 * 20 else: - results['FT'] = 20 + (payload[1]/255*60) + results['FT'] = 20 + payload[1] / 255 * 60 # Current room temperature or failure code - if (FL == 0): - results['TMP'] = 10 + (payload[2]/255*20) - else: + if FL == 0: + results['TMP'] = 10 + payload[2] / 255 * 20 + else: results['FC'] = payload[2] results['STATUS'] = status_byte return results @@ -259,7 +290,7 @@ def _parse_eep_A5_30_01(self, payload, status): self.logger.warning("A5_30_03 is learn telegram") return results # Data_byte1 = 0x00 / 0xFF - results['ALARM'] = (payload[2] == 0x00) + results['ALARM'] = payload[2] == 0x00 # Battery linear: 0-120 (bat low), 121-255(bat high) results['BAT'] = payload[1] return results @@ -276,27 +307,26 @@ def _parse_eep_A5_30_03(self, payload, status): self.logger.error("EEP A5_30_03 not according to spec.") return results # Data_byte2 = Temperatur 0...40 °C (255...0) - results['TEMP'] = 40 - (payload[1]/255*40) + results['TEMP'] = 40 - payload[1] / 255 * 40 # Data_byte1 = 0x0F = Alarm, 0x1F = kein Alarm - results['ALARM'] = (payload[2] == 0x0F) + results['ALARM'] = payload[2] == 0x0F return results - def _parse_eep_A5_38_08(self, payload, status): results = {} - if (payload[1] == 2): # Dimming + if payload[1] == 2: # Dimming results['EDIM'] = payload[2] results['RMP'] = payload[3] - results['LRNB'] = ((payload[4] & 1 << 3) == 1 << 3) - results['EDIM_R'] = ((payload[4] & 1 << 2) == 1 << 2) - results['STR'] = ((payload[4] & 1 << 1) == 1 << 1) - results['SW'] = ((payload[4] & 1 << 0) == 1 << 0) + results['LRNB'] = payload[4] & 1 << 3 == 1 << 3 + results['EDIM_R'] = payload[4] & 1 << 2 == 1 << 2 + results['STR'] = payload[4] & 1 << 1 == 1 << 1 + results['SW'] = payload[4] & 1 << 0 == 1 << 0 return results def _parse_eep_A5_3F_7F(self, payload, status): self.logger.debug("Processing A5_3F_7F") results = {'DI_3': (payload[3] & 1 << 3) == 1 << 3, 'DI_2': (payload[3] & 1 << 2) == 1 << 2, 'DI_1': (payload[3] & 1 << 1) == 1 << 1, 'DI_0': (payload[3] & 1 << 0) == 1 << 0} - results['AD_0'] = (((payload[1] & 0x03) << 8) + payload[2]) * 1.8 / pow(2, 10) + results['AD_0'] = ((payload[1] & 0x03) << 8 + payload[2]) * 1.8 / pow(2, 10) results['AD_1'] = (payload[1] >> 2) * 1.8 / pow(2, 6) results['AD_2'] = payload[0] * 1.8 / pow(2, 8) return results @@ -316,26 +346,25 @@ def _parse_eep_A5_0G_03(self, payload, status): self.logger.debug(f"eep-parser input status = {status}") results = {} runtime_s = ((payload[0] << 8) + payload[1]) / 10 - if (payload[2] == 1): + if payload[2] == 1: self.logger.debug(f"Shutter moved {runtime_s} s 'upwards'") results['MOVE'] = runtime_s * -1 - elif (payload[2] == 2): + elif payload[2] == 2: self.logger.debug(f"Shutter moved {runtime_s} s 'downwards'") results['MOVE'] = runtime_s return results -##################################################### -### --- Definitions for RORG = D2 / ORG = D2 --- ### -##################################################### +# Definitions for RORG = D2 / ORG = D2 + def _parse_eep_D2_01_07(self, payload, status): # self.logger.debug("Processing D2_01_07: VLD Switch") results = {} # self.logger.info(f'D2 Switch Feedback 0:{payload[0]} 1:{payload[1]} 2:{payload[2]}') - if (payload[2] == 0x80): + if payload[2] == 0x80: # Switch is off results['STAT'] = 0 self.logger.debug('D2 Switch off') - elif (payload[2] == 0xe4): + elif payload[2] == 0xe4: # Switch is on results['STAT'] = 1 self.logger.debug('D2 Switch on') @@ -345,55 +374,50 @@ def _parse_eep_D2_01_12(self, payload, status): # self.logger.debug("Processing D2_01_12: VLD Switch") results = {} # self.logger.info(f'D2 Switch Feedback 0:{payload[0]} 1:{payload[1]} 2:{payload[2]}') - if (payload[1] == 0x60) and (payload[2] == 0x80): + if payload[1] == 0x60 and payload[2] == 0x80: # Switch is off results['STAT_A'] = 0 self.logger.debug('D2 Switch Channel A: off') - elif (payload[1] == 0x60) and (payload[2] == 0xe4): + elif payload[1] == 0x60 and payload[2] == 0xe4: # Switch is on results['STAT_A'] = 1 self.logger.debug('D2 Channel A: Switch on') - elif (payload[1] == 0x61) and (payload[2] == 0x80): + elif payload[1] == 0x61 and payload[2] == 0x80: # Switch is off results['STAT_B'] = 0 self.logger.debug('D2 SwitchChannel A: off') - elif (payload[1] == 0x61) and (payload[2] == 0xe4): + elif payload[1] == 0x61 and payload[2] == 0xe4: # Switch is on results['STAT_B'] = 1 self.logger.debug('D2 Switch Channel B: on') return results -#################################################### -### --- Definitions for RORG = D5 / ORG = 06 --- ### -#################################################### +# Definitions for RORG = D5 / ORG = 06 + def _parse_eep_D5_00_01(self, payload, status): # Window/Door Contact Sensor, for example Eltako FTK, FTKB self.logger.debug("Processing D5_00_01: Door contact") - return {'STATUS': (payload[0] & 0x01) == 0x01} + return {'STATUS': payload[0] & 0x01 == 0x01} +# Definitions for RORG = F6 / ORG = 05 -#################################################### -### --- Definitions for RORG = F6 / ORG = 05 --- ### -#################################################### def _parse_eep_F6_02_01(self, payload, status): self.logger.debug("Processing F6_02_01: Rocker Switch, 2 Rocker, Light and Blind Control - Application Style 1") results = {} R1 = (payload[0] & 0xE0) >> 5 - EB = (payload[0] & (1<<4) == (1<<4)) R2 = (payload[0] & 0x0E) >> 1 - SA = (payload[0] & (1<<0) == (1<<0)) - NU = (status & (1<<4) == (1<<4)) + SA = payload[0] & 1 == 1 + NU = status & (1 << 4) == (1 << 4) - if (NU): + if NU: results['AI'] = (R1 == 0) or (SA and (R2 == 0)) results['AO'] = (R1 == 1) or (SA and (R2 == 1)) results['BI'] = (R1 == 2) or (SA and (R2 == 2)) results['BO'] = (R1 == 3) or (SA and (R2 == 3)) - elif (not NU) and (payload[0] == 0x00): + elif not NU and payload[0] == 0x00: results = {'AI': False, 'AO': False, 'BI': False, 'BO': False} else: self.logger.error("Parser detected invalid state encoding - check your switch!") - pass return results def _parse_eep_F6_02_02(self, payload, status): @@ -408,34 +432,34 @@ def _parse_eep_F6_02_03(self, payload, status): self.logger.debug("Processing F6_02_03: Rocker Switch, 2 Rocker") results = {} # Button A1: Dimm light down - results['AI'] = (payload[0]) == 0x10 + results['AI'] = payload[0] == 0x10 # Button A0: Dimm light up - results['AO'] = (payload[0]) == 0x30 + results['AO'] = payload[0] == 0x30 # Button B1: Dimm light down - results['BI'] = (payload[0]) == 0x50 + results['BI'] = payload[0] == 0x50 # Button B0: Dimm light up - results['BO'] = (payload[0]) == 0x70 - if (payload[0] == 0x70): + results['BO'] = payload[0] == 0x70 + if payload[0] == 0x70: results['B'] = True - elif (payload[0] == 0x50): + elif payload[0] == 0x50: results['B'] = False - elif (payload[0] == 0x30): + elif payload[0] == 0x30: results['A'] = True - elif (payload[0] == 0x10): + elif payload[0] == 0x10: results['A'] = False - return results + return results def _parse_eep_F6_10_00(self, payload, status): self.logger.debug(f"Processing F6_10_00: Mechanical Handle sends payload {payload[0]}") results = {} # Eltako defines 0xF0 for closed status. Enocean spec defines masking of lower 4 bit: - if (payload[0] & 0b11110000) == 0b11110000: + if payload[0] & 0b11110000 == 0b11110000: results['STATUS'] = 0 # Eltako defines 0xE0 for window open (horizontal) up status. Enocean spec defines the following masking: - elif (payload[0] & 0b11010000) == 0b11000000: + elif payload[0] & 0b11010000 == 0b11000000: results['STATUS'] = 1 # Eltako defines 0xD0 for open/right up status. Enocean spec defines masking of lower 4 bit: - elif (payload[0] & 0b11110000) == 0b11010000: + elif payload[0] & 0b11110000 == 0b11010000: results['STATUS'] = 2 else: self.logger.error(f"Error in F6_10_00 handle status, payload: {payload[0]} unknown") @@ -453,19 +477,19 @@ def _parse_eep_F6_0G_03(self, payload, status): ''' self.logger.debug("Processing F6_0G_03: shutter actor") self.logger.debug("payload = [{}]".format(', '.join(['0x%02X' % b for b in payload]))) - self.logger.debug("status: {}".format(status)) + self.logger.debug(f"status: {status}") results = {} - if (payload[0] == 0x70): + if payload[0] == 0x70: results['POSITION'] = 0 results['B'] = 0 - elif (payload[0] == 0x50): + elif payload[0] == 0x50: results['POSITION'] = 255 results['B'] = 0 - elif (payload[0] == 0x01): + elif payload[0] == 0x01: results['STATUS'] = 'Start moving up' results['B'] = 1 - elif (payload[0] == 0x02): + elif payload[0] == 0x02: results['STATUS'] = 'Start moving down' results['B'] = 2 - self.logger.debug('parse_eep_F6_0G_03 returns: {}'.format(results)) + self.logger.debug(f'parse_eep_F6_0G_03 returns: {results}') return results diff --git a/enocean/prepare_packet_data.py b/enocean/protocol/packet_data.py similarity index 80% rename from enocean/prepare_packet_data.py rename to enocean/protocol/packet_data.py index 79f9f76e6..e4683bdb6 100755 --- a/enocean/prepare_packet_data.py +++ b/enocean/protocol/packet_data.py @@ -22,9 +22,10 @@ import logging from lib.utils import Utils +from .constants import RORG, PACKET_TYPE -class Prepare_Packet_Data(): +class Packet_Data(): def __init__(self, plugin_instance): """ @@ -35,13 +36,13 @@ def __init__(self, plugin_instance): # Get the plugin instance from encocean class self._plugin_instance = plugin_instance - def CanDataPrepare(self, tx_eep): + def CanPrepareData(self, tx_eep): """ This Method checks if there is an available Prepare Data Method for the tx_eep """ found = callable(getattr(self, '_prepare_data_for_tx_eep_' + tx_eep, None)) - if (not found): - self.logger.error(f"enocean-CanDataPrepare: missing tx_eep for pepare send data {tx_eep} - there should be a _prepare_data_for_tx_eep_{tx_eep}-function!") + if not found: + self.logger.error(f"enocean-CanPrepareData: missing tx_eep for pepare send data {tx_eep} - there should be a _prepare_data_for_tx_eep_{tx_eep}-function!") return found def PrepareData(self, item, tx_eep): @@ -54,40 +55,34 @@ def PrepareData(self, item, tx_eep): if self._plugin_instance.has_iattr(item.conf, 'enocean_tx_id_offset'): self.logger.debug("enocean-PrepareData: item has valid enocean_tx_id_offset") id_offset = int(self._plugin_instance.get_iattr_value(item.conf, 'enocean_tx_id_offset')) - if (id_offset < 0) or (id_offset > 127): + if id_offset < 0 or id_offset > 127: self.logger.error('enocean-PrepareData: ID offset out of range (0-127). Aborting.') - return None + return else: self.logger.info(f"enocean-PrepareData: {tx_eep} item has no attribute ''enocean_tx_id_offset''! Set to default = 0") id_offset = 0 - # start prepare data + # start prepare data rorg, payload, optional = getattr(self, '_prepare_data_for_tx_eep_' + tx_eep)(item, tx_eep) - #self.logger.info('enocean-PrepareData: {} returns [{:#04x}], [{}], [{}]'.format(tx_eep, rorg, ', '.join('{:#04x}'.format(x) for x in payload), ', '.join('{:#04x}'.format(x) for x in optional))) + # self.logger.info('enocean-PrepareData: {} returns [{:#04x}], [{}], [{}]'.format(tx_eep, rorg, ', '.join('{:#04x}'.format(x) for x in payload), ', '.join('{:#04x}'.format(x) for x in optional))) return id_offset, rorg, payload, optional +# Definitions for RORG = A5 / ORG = 07 -##################################################### -### --- Definitions for RORG = A5 / ORG = 07 --- ### -### --> Definition of 4BS Telegrams ### -##################################################### - - def _prepare_data_for_tx_eep_A5_20_04(self, item, tx_eep): """ ### --- Data for radiator valve command --- ### """ self.logger.debug(f'enocean-PrepareData: prepare data for tx_eep {tx_eep}') - rorg = 0xa5 temperature = item() # define default values: - MC = 1 # off - WUC = 3 # 120 seconds - BLC = 0 # unlocked - LRNB = 1 # data - DSO = 0 # 0 degree + MC = 1 # off + WUC = 3 # 120 seconds + BLC = 0 # unlocked + LRNB = 1 # data + DSO = 0 # 0 degree valve_position = 50 - for sibling in get_children(item.parent): + for sibling in item.return_parent().get_children(): if hasattr(sibling, 'MC'): MC = sibling() if hasattr(sibling, 'WUC'): @@ -100,24 +95,22 @@ def _prepare_data_for_tx_eep_A5_20_04(self, item, tx_eep): DSO = sibling() if hasattr(sibling, 'VALVE_POSITION'): valve_position = sibling() - TSP = int((temperature -10)*255/30) - status = 0 + (MC << 1) + (WUC << 2) + TSP = int((temperature - 10) * 255 / 30) + status = 0 + (MC << 1) + (WUC << 2) status2 = (BLC << 5) + (LRNB << 4) + (DSO << 2) - payload = [valve_position, TSP, status , status2] + payload = [valve_position, TSP, status, status2] optional = [] - return rorg, payload, optional - - - def _prepare_data_for_tx_eep_A5_38_08_01(self, item, tx_eep): + return RORG.BS4, payload, optional + + def _prepare_data_for_tx_eep_A5_38_08_01(self, item, tx_eep): """ ### --- Data for A5-38_08 command 1 --- ### Eltako Devices: - FSR14-2x, FSR14-4x, FSR14SSR, FSR71 + FSR14-2x, FSR14-4x, FSR14SSR, FSR71 FSR61, FSR61NP, FSR61G, FSR61LN, FLC61NP This method has the function to prepare the packet data in case of switching device on or off """ self.logger.debug(f'enocean-PrepareData: prepare data for tx_eep {tx_eep}') - rorg = 0xa5 block = 0 # check if item has attribute block_switch if self._plugin_instance.has_iattr(item.conf, 'block_switch'): @@ -133,10 +126,9 @@ def _prepare_data_for_tx_eep_A5_38_08_01(self, item, tx_eep): payload = [0x01, 0x00, 0x00, int(9 + block)] self.logger.debug(f'enocean-PrepareData: {tx_eep} prepare data to switch on') optional = [] - return rorg, payload, optional - - - def _prepare_data_for_tx_eep_A5_38_08_02(self, item, tx_eep): + return RORG.BS4, payload, optional + + def _prepare_data_for_tx_eep_A5_38_08_02(self, item, tx_eep): """ ### --- Data for A5-38_08 command 2 --- ### Eltako Devices: @@ -145,8 +137,7 @@ def _prepare_data_for_tx_eep_A5_38_08_02(self, item, tx_eep): This method has the function to prepare the packet data in case of switching the dimmer device on or off, but calculate also the correct data of dim_speed and dim_value for further solutions. """ - #self.logger.debug(f'enocean-PrepareData: prepare data for tx_eep {tx_eep}') - rorg = 0xa5 + # self.logger.debug(f'enocean-PrepareData: prepare data for tx_eep {tx_eep}') block = 0 # check if item has attribute block_dim_value if self._plugin_instance.has_iattr(item.level.conf, 'block_dim_value'): @@ -158,7 +149,7 @@ def _prepare_data_for_tx_eep_A5_38_08_02(self, item, tx_eep): dim_speed = self._plugin_instance.get_iattr_value(item.level.conf, 'dim_speed') # bound dim_speed values to [0 - 100] % dim_speed = max(0, min(100, int(dim_speed))) - #self.logger.debug(f'enocean-PrepareData: {tx_eep} use dim_speed = {dim_speed} %') + # self.logger.debug(f'enocean-PrepareData: {tx_eep} use dim_speed = {dim_speed} %') # calculate dimspeed from percent into integer # 0x01 --> fastest speed --> 100 % # 0xFF --> slowest speed --> 0 % @@ -166,29 +157,28 @@ def _prepare_data_for_tx_eep_A5_38_08_02(self, item, tx_eep): else: # use intern dim_speed of the dim device dim_speed = 0 - #self.logger.debug('enocean-PrepareData: no attribute dim_speed --> use intern dim speed') + # self.logger.debug('enocean-PrepareData: no attribute dim_speed --> use intern dim speed') if not item(): # if value is False --> Switch off dim_value = 0 payload = [0x02, int(dim_value), int(dim_speed), int(8 + block)] - #self.logger.debug('enocean-PrepareData: prepare data to switch off for command for A5_38_08_02') + # self.logger.debug('enocean-PrepareData: prepare data to switch off for command for A5_38_08_02') else: # check if reference dim value exists if 'ref_level' in item.level.conf: dim_value = int(item.level.conf['ref_level']) # check range of dim_value [0 - 100] % dim_value = max(0, min(100, int(dim_value))) - #self.logger.debug(f'enocean-PrepareData: {tx_eep} ref_level {dim_value} % found for A5_38_08_02') + # self.logger.debug(f'enocean-PrepareData: {tx_eep} ref_level {dim_value} % found for A5_38_08_02') else: # set dim_value on 100 % == 0x64 dim_value = 0x64 self.logger.debug(f'enocean-PrepareData: {tx_eep} no ref_level found! Setting to default 100 %') payload = [0x02, int(dim_value), int(dim_speed), int(9 + block)] optional = [] - return rorg, payload, optional - - - def _prepare_data_for_tx_eep_A5_38_08_03(self, item, tx_eep): + return RORG.BS4, payload, optional + + def _prepare_data_for_tx_eep_A5_38_08_03(self, item, tx_eep): """ ### --- Data for A5-38_08 command 3--- ### Eltako Devices: @@ -198,14 +188,13 @@ def _prepare_data_for_tx_eep_A5_38_08_03(self, item, tx_eep): In case of dim_value == 0 the dimmer is switched off. """ self.logger.debug(f'enocean-PrepareData: prepare data for tx_eep {tx_eep}') - rorg = 0xa5 block = 0 # check if item has attribute block_dim_value if self._plugin_instance.has_iattr(item.conf, 'block_dim_value'): block_value = self._plugin_instance.get_iattr_value(item.conf, 'block_dim_value') if Utils.to_bool(block_value): block = 4 - # check if item has attribite dim_speed + # check if item has attribite dim_speed if self._plugin_instance.has_iattr(item.conf, 'dim_speed'): dim_speed = self._plugin_instance.get_iattr_value(item.conf, 'dim_speed') # bound dim_speed values to [0 - 100] % @@ -214,7 +203,7 @@ def _prepare_data_for_tx_eep_A5_38_08_03(self, item, tx_eep): # calculate dimspeed from percent into hex # 0x01 --> fastest speed --> 100 % # 0xFF --> slowest speed --> 0 % - dim_speed = (255 - (254 * dim_speed/100)) + dim_speed = (255 - (254 * dim_speed / 100)) else: # use intern dim_speed of the dim device dim_speed = 0x00 @@ -232,10 +221,9 @@ def _prepare_data_for_tx_eep_A5_38_08_03(self, item, tx_eep): dim_value = dim_value payload = [0x02, int(dim_value), int(dim_speed), int(9 + block)] optional = [] - return rorg, payload, optional - - - def _prepare_data_for_tx_eep_A5_3F_7F(self, item, tx_eep): + return RORG.BS4, payload, optional + + def _prepare_data_for_tx_eep_A5_3F_7F(self, item, tx_eep): """ ### --- Data for A5-3F-7F - Universal Actuator Command --- ### Eltako Devices: @@ -244,14 +232,13 @@ def _prepare_data_for_tx_eep_A5_3F_7F(self, item, tx_eep): The Runtime is set in [0 - 255] s """ self.logger.debug(f'enocean-PrepareData: prepare data for tx_eep {tx_eep}') - rorg = 0xa5 block = 0 # check if item has attribute block_switch if self._plugin_instance.has_iattr(item.conf, 'block_switch'): block_value = self._plugin_instance.get_iattr_value(item.conf, 'block_switch') if Utils.to_bool(block_value): block = 4 - # check if item has attribite enocean_rtime + # check if item has attribite enocean_rtime if self._plugin_instance.has_iattr(item.conf, 'enocean_rtime'): rtime = self._plugin_instance.get_iattr_value(item.conf, 'enocean_rtime') # rtime [0 - 255] s @@ -263,25 +250,24 @@ def _prepare_data_for_tx_eep_A5_3F_7F(self, item, tx_eep): self.logger.debug(f'enocean-PrepareData: {tx_eep} actuator runtime not specified set to {rtime} s.') # check command (up, stop, or down) command = int(item()) - if(command == 0): + if command == 0: # Stopp moving command_hex_code = 0x00 - elif(command == 1): + elif command == 1: # moving up command_hex_code = 0x01 - elif(command == 2): + elif command == 2: # moving down command_hex_code = 0x02 else: self.logger.error(f'enocean-PrepareData: {tx_eep} sending actuator command failed: invalid command {command}') - return None + return # define payload payload = [0x00, rtime, command_hex_code, int(8 + block)] optional = [] - return rorg, payload, optional - - - def _prepare_data_for_tx_eep_07_3F_7F(self, item, tx_eep): + return RORG.BS4, payload, optional + + def _prepare_data_for_tx_eep_07_3F_7F(self, item, tx_eep): """ ### --- Data for 07-3F-7F Command --- ### Eltako Devices: @@ -294,8 +280,9 @@ def _prepare_data_for_tx_eep_07_3F_7F(self, item, tx_eep): Color: bit0 = red, bit1= green, bit2 = blue, bit3 = white """ self.logger.debug(f'enocean-PrepareData: prepare data for tx_eep {tx_eep}') + # NOTE: not an official RORG value! rorg = 0x07 - # check if item has attribite dim_speed + # check if item has attribite dim_speed if self._plugin_instance.has_iattr(item.conf, 'dim_speed'): dim_speed = self._plugin_instance.get_iattr_value(item.conf, 'dim_speed') dim_speed = max(0, min(100, int(dim_speed))) @@ -303,89 +290,87 @@ def _prepare_data_for_tx_eep_07_3F_7F(self, item, tx_eep): # calculate dimspeed from percent into hex # 0x01 --> fastest speed --> 100 % # 0xFF --> slowest speed --> 0 % - dim_speed = (255 - (254 * dim_speed/100)) + dim_speed = (255 - (254 * dim_speed / 100)) else: # use intern dim_speed of the dim device dim_speed = 0x00 self.logger.debug(f'enocean-PrepareData: {tx_eep} no attribute dim_speed --> use intern dim speed') + # check the color of the item if self._plugin_instance.has_iattr(item.conf, 'color'): color = self._plugin_instance.get_iattr_value(item.conf, 'color') - if (color == 'red'): + color_hex = '' + if color == 'red': color_hex = 0x01 - elif (color == 'green'): + elif color == 'green': color_hex = 0x02 - elif (color == 'blue'): + elif color == 'blue': color_hex = 0x04 - elif (color == 'white'): + elif color == 'white': color_hex = 0x08 else: self.logger.error(f'enocean-PrepareData: {item} has no attribute color --> please specify color!') - return None + return + # Aufdimmen: [dim_speed, color_hex, 0x30, 0x0F] # Abdimmen: [dim_speed, color_hex, 0x31, 0x0F] # Dimmstop: [dim_speed, color_hex, 0x32, 0x0F] # check command (up, stop, or down) command = int(item()) - if(command == 0): + if command == 0: # dim up command_hex_code = 0x30 - elif(command == 1): + elif command == 1: # dim down command_hex_code = 0x31 - elif(command == 2): + elif command == 2: # stop dim command_hex_code = 0x32 else: self.logger.error(f'enocean-PrepareData: {tx_eep} sending actuator command failed: invalid command {command}') - return None + return # define payload payload = [int(dim_speed), color_hex, command_hex_code, 0x0F] optional = [] return rorg, payload, optional - - -############################################################# -### --- Definitions for RORG = D2 --- ### -### --> Definition EnOcean Variable Length Telegram (VLD) ### -############################################################# - def _prepare_data_for_tx_eep_D2_01_07(self, item, tx_eep): +# Definitions for RORG = D2, EnOcean Variable Length Telegram (VLD) + + def _prepare_data_for_tx_eep_D2_01_07(self, item, tx_eep): """ ### --- Data for D2_01_07 (VLD) --- ### Prepare data for Devices with Varable Length Telegram. - There is currently no device information available. - Optional 'pulsewidth' - Attribute was removed, it can be realized with the smarthomeNG + There is currently no device information available. + Optional 'pulsewidth' - Attribute was removed, it can be realized with the smarthomeNG build in function autotimer! """ self.logger.debug(f'enocean-PrepareData: prepare data for tx_eep {tx_eep}') - rorg = 0xD2 - SubTel = 0x03 db = 0xFF Secu = 0x0 if self._plugin_instance.has_iattr(item.conf, 'enocean_rx_id'): rx_id = int(self._plugin_instance.get_iattr_value(item.conf, 'enocean_rx_id'), 16) - if (rx_id < 0) or (rx_id > 0xFFFFFFFF): + if rx_id < 0 or rx_id > 0xFFFFFFFF: self.logger.error(f'enocean-PrepareData: {tx_eep} rx-ID-Offset out of range (0-127). Aborting.') - return None + return self.logger.debug(f'enocean-PrepareData: {tx_eep} enocean_rx_id found.') else: rx_id = 0 self.logger.debug(f'enocean-PrepareData: {tx_eep} no enocean_rx_id found!') # Prepare Data Packet - if (item() == 0): + if item() == 0: payload = [0x01, 0x1E, 0x00] - optional = [SubTel, rx_id, db, Secu] - elif (item() == 1): + optional = [PACKET_TYPE.RADIO_SUB_TEL, rx_id, db, Secu] + elif item() == 1: payload = [0x01, 0x1E, 0x01] - optional = [SubTel, rx_id, db, Secu] + optional = [PACKET_TYPE.RADIO_SUB_TEL, rx_id, db, Secu] else: self.logger.error(f'enocean-PrepareData: {tx_eep} undefined Value. Error!') - return None + return # packet_data_prepared = (id_offset, 0xD2, payload, [0x03, 0xFF, 0xBA, 0xD0, 0x00, 0xFF, 0x0]) self.logger.info(f'enocean-PrepareData: {tx_eep} Packet Data Prepared for {tx_eep} (VLD)') - optional = [SubTel, rx_id, db, Secu] - return rorg, payload, optional + optional = [PACKET_TYPE.RADIO_SUB_TEL, rx_id, db, Secu] + + return RORG.VLD, payload, optional def _prepare_data_for_tx_eep_D2_01_12(self, item, tx_eep): """ @@ -396,24 +381,22 @@ def _prepare_data_for_tx_eep_D2_01_12(self, item, tx_eep): build in function autotimer! """ self.logger.debug(f'enocean-PrepareData: prepare data for tx_eep {tx_eep}') - rorg = 0xD2 - SubTel = 0x03 db = 0xFF Secu = 0x0 if self._plugin_instance.has_iattr(item.conf, 'enocean_rx_id'): rx_id = int(self._plugin_instance.get_iattr_value(item.conf, 'enocean_rx_id'), 16) - if (rx_id < 0) or (rx_id > 0xFFFFFFFF): + if rx_id < 0 or rx_id > 0xFFFFFFFF: self.logger.error(f'enocean-PrepareData: {tx_eep} rx-ID-Offset out of range (0-127). Aborting.') - return None + return self.logger.debug(f'enocean-PrepareData: {tx_eep} enocean_rx_id found.') else: rx_id = 0 self.logger.debug(f'enocean-PrepareData: {tx_eep} no enocean_rx_id found!') if self._plugin_instance.has_iattr(item.conf, 'enocean_channel'): schannel = self._plugin_instance.get_iattr_value(item.conf, 'enocean_channel') - if (schannel == "A"): + if schannel == "A": channel = 0x00 - elif (schannel == "B"): + elif schannel == "B": channel = 0x01 else: channel = 0x1E @@ -422,16 +405,17 @@ def _prepare_data_for_tx_eep_D2_01_12(self, item, tx_eep): channel = 0x1E self.logger.debug(f'enocean-PrepareData: {tx_eep} no enocean_channel found!') # Prepare Data Packet - if (item() == 0): + if item() == 0: payload = [0x01, channel, 0x00] - optional = [SubTel, rx_id, db, Secu] - elif (item() == 1): + optional = [PACKET_TYPE.RADIO_SUB_TEL, rx_id, db, Secu] + elif item() == 1: payload = [0x01, channel, 0x01] - optional = [SubTel, rx_id, db, Secu] + optional = [PACKET_TYPE.RADIO_SUB_TEL, rx_id, db, Secu] else: self.logger.error(f'enocean-PrepareData: {tx_eep} undefined Value. Error!') - return None + return # packet_data_prepared = (id_offset, 0xD2, payload, [0x03, 0xFF, 0xBA, 0xD0, 0x00, 0xFF, 0x0]) self.logger.info(f'enocean-PrepareData: {tx_eep} Packet Data Prepared for {tx_eep} (VLD)') - optional = [SubTel, rx_id, db, Secu] - return rorg, payload, optional + optional = [PACKET_TYPE.RADIO_SUB_TEL, rx_id, db, Secu] + + return RORG.VLD, payload, optional