From 98871634887a70c30952835d19f8fd96d7bb7f69 Mon Sep 17 00:00:00 2001 From: davesmeghead Date: Sat, 4 Jan 2025 13:53:54 +0000 Subject: [PATCH] 0.10.3.0 - Changes per description below 1. Removed lastevent attribute from Alarm Entity. I did say I'd do this eventually! Please use lasteventname and lasteventaction instead. 2. Sensors have 2 Zone Name attributes. The name translated by the HA language translation files and the zone name that is reported by the Panel itself. 3. Renamed mode attribute of the Alarm Entity. This seemed to be in conflict with HA somehow so it is now called emulationmode. 4. Lots mode decoding of B0 message data 5. Instead of terminating the Integration on a disconnection, the Integration with stop and then attempt to connect again. I struggle to test this as my system works so please let me know of issues. --- custom_components/visonic/__init__.py | 6 +- custom_components/visonic/binary_sensor.py | 6 +- custom_components/visonic/client.py | 18 +- .../visonic/examples/complete_example.py | 2 +- custom_components/visonic/pyconst.py | 2 +- custom_components/visonic/pyhelper.py | 68 +- custom_components/visonic/pyvisonic.py | 628 +++++++++++------- custom_components/visonic/sensor.py | 7 +- custom_components/visonic/services.yaml | 6 +- .../visonic/translations/en.json | 7 +- .../visonic/translations/fr.json | 7 +- .../visonic/translations/it.json | 7 +- .../visonic/translations/nl.json | 7 +- 13 files changed, 458 insertions(+), 313 deletions(-) diff --git a/custom_components/visonic/__init__.py b/custom_components/visonic/__init__.py index 2b1a94b..0b53635 100644 --- a/custom_components/visonic/__init__.py +++ b/custom_components/visonic/__init__.py @@ -320,12 +320,12 @@ async def service_panel_eventlog(call): else: sendHANotification(f"Event log failed - Panel not found") - async def service_panel_reconnect(call): + async def async_service_panel_reconnect(call): """Handler for panel reconnect service""" _LOGGER.info("Service Panel reconnect called") client, panel = getClient(call) if client is not None: - await client.service_panel_reconnect(call) + await client.async_service_panel_reconnect(call) elif panel is not None: sendHANotification(f"Service Panel reconnect failed - Panel {panel} not found") else: @@ -398,7 +398,7 @@ async def handle_reload(call) -> None: hass.services.async_register( DOMAIN, ALARM_PANEL_RECONNECT, - service_panel_reconnect, + async_service_panel_reconnect, schema=ALARM_SCHEMA_RECONNECT, ) hass.services.async_register( diff --git a/custom_components/visonic/binary_sensor.py b/custom_components/visonic/binary_sensor.py index 3a8c943..b0aa014 100644 --- a/custom_components/visonic/binary_sensor.py +++ b/custom_components/visonic/binary_sensor.py @@ -263,7 +263,11 @@ def extra_state_attributes(self): attr["zone_tamper"] = t #attr["zone type"] = self.ztype - attr["zone_name"] = self._visonic_device.getZoneLocation() + zn = self._visonic_device.getZoneLocation() + if len(zn) == 2: + attr["zone_name"] = zn[0] + attr["zone_name_panel"] = "Unknown" if zn[1] is None else zn[1] + attr["zone_type"] = self._visonic_device.getZoneType() attr["zone_chime"] = self._visonic_device.getChimeType() attr["zone_trouble"] = self._visonic_device.getProblem() diff --git a/custom_components/visonic/client.py b/custom_components/visonic/client.py index 1f4c2fe..3a7b9e9 100644 --- a/custom_components/visonic/client.py +++ b/custom_components/visonic/client.py @@ -105,7 +105,7 @@ PIN_REGEX, ) -CLIENT_VERSION = "0.10.2.0" +CLIENT_VERSION = "0.10.3.0" MAX_CLIENT_LOG_ENTRIES = 300 @@ -590,7 +590,7 @@ def getPanelStatusDict(self, partition : int | None = None, include_extended_sta pd = self.visonicProtocol.getPanelStatusDict(partition, include_extended_status) if partition is None: #self.logstate_debug(f"Client Dict {pd}") - pd["lastevent"] = self.PanelLastEventName + "/" + self.PanelLastEventAction + #pd["lastevent"] = self.PanelLastEventName + "/" + self.PanelLastEventAction pd["lasteventname"] = self.PanelLastEventName pd["lasteventaction"] = self.PanelLastEventAction pd["lasteventtime"] = self.PanelLastEventTime @@ -1110,6 +1110,9 @@ def onPanelChangeHandler(self, event_id: AlCondition | PanelCondition, data : di if event_id == AlCondition.PANEL_UPDATE: if data is not None and len(data) == 4 and PE_NAME in data and data[PE_NAME] >= 0 and PE_PARTITION in data: # The panel has partitions + + self.logstate_debug(f"[onPanelChangeHandler] {type(self.myPanelEventCoordinator)} set to {self.myPanelEventCoordinator}") + partition = data[PE_PARTITION] if self.myPanelEventCoordinator is None: # initialise as a dict, the partition is the key @@ -1249,7 +1252,7 @@ def onDisconnect(self, termination : AlTerminationType): #self.panel_exception_counter = self.panel_exception_counter + 1 self.panel_disconnection_counter = self.panel_disconnection_counter + 1 - asyncio.ensure_future(self.async_service_panel_stop(), loop=self.hass.loop) + asyncio.ensure_future(self.async_service_panel_reconnect(), loop=self.hass.loop) # pmGetPin: Convert a PIN given as 4 digit string in the PIN PDU format as used in messages to powermax def pmGetPin(self, code: str, forcedKeypad: bool, partition : int): @@ -1713,7 +1716,7 @@ async def service_panel_x10(self, call): def _createSocketConnection(self, address, port): try: #self.logstate_debug(f"Setting TCP socket Options {address} {port}") - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP) sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) sock.setblocking(1) # Set blocking to on, this is the default but just make sure @@ -1952,7 +1955,7 @@ async def service_comms_stop(self): await asyncio.sleep(0.5) # not a mistake, wait a bit longer to make sure it's closed as we get no feedback (we only get the fact that the queue is empty) - async def service_panel_reconnect(self, call): + async def async_service_panel_reconnect(self, call): """Service call to re-connect the connection.""" if not self.isPanelConnected(): raise HomeAssistantError( @@ -1968,10 +1971,11 @@ async def service_panel_reconnect(self, call): # Check security permissions (that this user has access to the alarm panel entity) await self._checkUserPermission(call, POLICY_CONTROL, Platform.ALARM_CONTROL_PANEL + "." + slugify(self.getAlarmPanelUniqueIdent())) - self.logstate_debug("User has requested visonic panel reconnection") + self.logstate_debug(f"Stopping Client and Reconnecting to Visonic Panel {self.getPanelID()}") + #self.logstate_debug("User has requested visonic panel reconnection") await self.async_service_panel_stop() await asyncio.sleep(3.0) - await self.async_service_panel_start(False) + await self.async_service_panel_start(True) async def async_disconnect_callback(self): """Service call to disconnect.""" diff --git a/custom_components/visonic/examples/complete_example.py b/custom_components/visonic/examples/complete_example.py index 7372a19..84fef67 100644 --- a/custom_components/visonic/examples/complete_example.py +++ b/custom_components/visonic/examples/complete_example.py @@ -512,7 +512,7 @@ async def service_panel_start(self): else: print("Failure - not connected to panel") - async def service_panel_reconnect(self, call): + async def async_service_panel_reconnect(self, call): """ Service call to re-connect the connection """ print("User has requested visonic panel reconnection") await self.service_comms_stop() diff --git a/custom_components/visonic/pyconst.py b/custom_components/visonic/pyconst.py index 30c6c26..d0dfc3a 100644 --- a/custom_components/visonic/pyconst.py +++ b/custom_components/visonic/pyconst.py @@ -401,7 +401,7 @@ def getSensorType(self) -> AlSensorType: return AlSensorType.UNKNOWN @abstractmethod - def getZoneLocation(self) -> str: + def getZoneLocation(self) -> (str, str): return "" @abstractmethod diff --git a/custom_components/visonic/pyhelper.py b/custom_components/visonic/pyhelper.py index eeafb7e..51d5e35 100644 --- a/custom_components/visonic/pyhelper.py +++ b/custom_components/visonic/pyhelper.py @@ -225,6 +225,7 @@ def __init__(self, **kwargs): self.sid = kwargs.get("sid", 0) # int sensor id self.ztype = kwargs.get("ztype", 0) # int zone type self.zname = kwargs.get("zname", "Unknown") # str zone name + self.zpanelname = kwargs.get("zpanelname", "") # str zone name self.zchime = kwargs.get("zchime", "Unknown") # str zone chime self.zchimeref = kwargs.get("zchimeref", {}) # set partition set (could be in more than one partition) self.partition = kwargs.get("partition", {}) # set partition set (could be in more than one partition) @@ -298,6 +299,7 @@ def __eq__(self, other): and self.model == other.model and self.ztype == other.ztype and self.zname == other.zname + and self.zpanelname == other.zpanelname and self.zchime == other.zchime and self.partition == other.partition and self.bypass == other.bypass @@ -365,8 +367,8 @@ def getSensorType(self) -> AlSensorType: def getLastTriggerTime(self) -> datetime: return self.triggertime - def getZoneLocation(self) -> str: - return self.zname + def getZoneLocation(self) -> (str, str): + return (self.zname, self.zpanelname) def getZoneType(self) -> str: return self.ztypeName @@ -500,62 +502,6 @@ def do_tamper(self, val : bool) -> bool: return True # The value has changed return False # The value has not changed -""" - # JSON conversions - def fromJSON(self, decode): - #log.debug(f" In sensor fromJSON start {self}") - if "triggered" in decode: - self.triggered = toBool(decode["triggered"]) - if "open" in decode: - self.status = toBool(decode["open"]) - if "bypass" in decode: - self.bypass = toBool(decode["bypass"]) - if "low_battery" in decode: - self.lowbatt = toBool(decode["low_battery"]) - if "enrolled" in decode: - self.enrolled = toBool(decode["enrolled"]) - if "device_type" in decode: - st = decode["device_type"] - self.stype = AlSensorType.value_of(st.upper()) - if "trigger_time" in decode: - self.triggertime = datetime.fromisoformat(decode["trigger_time"]) if str(decode["trigger_time"]) != "" else None - if "location" in decode: - self.zname = titlecase(decode["location"]) - if "zone_type" in decode: - self.ztypeName = titlecase(decode["zone_type"]) - if "device_tamper" in decode: - self.tamper = toBool(decode["device_tamper"]) - if "zone_tamper" in decode: - self.ztamper = toBool(decode["zone_tamper"]) - if "chime" in decode: - self.zchime = titlecase(decode["chime"]) - if "sensor_model" in decode: - self.model = titlecase(decode["sensor_model"]) - if "motion_delay_time" in decode: - self.motiondelaytime = titlecase(decode["motion_delay_time"]) - #log.debug(f" In sensor fromJSON end {self}") - self.hasJPG = False - - def toJSON(self) -> dict: - dd=json.dumps({ - "zone": self.getDeviceID(), - "triggered": self.isTriggered(), - "open": self.isOpen(), - "bypass": self.isBypass(), - "low_battery": self.isLowBattery(), - "enrolled": self.isEnrolled(), - "device_type": str(self.getSensorType()), - "trigger_time": datetime.isoformat(self.getLastTriggerTime()) if self.getLastTriggerTime() is not None else "", - "location": str(self.getZoneLocation()), - "zone_type": str(self.getZoneType()), - "device_tamper": self.isTamper(), - "zone_tamper": self.isZoneTamper(), - "sensor_model": str(self.getSensorModel()), - "motion_delay_time": "" if self.getMotionDelayTime() is None else self.getMotionDelayTime(), - "chime": str(self.getChimeType()) }) # , ensure_ascii=True - return dd -""" - class AlSwitchDeviceHelper(AlSwitchDevice): def __init__(self, **kwargs): @@ -1347,9 +1293,9 @@ def fromJSON(self, decode) -> bool: oldPanelBypass = self.PanelBypass oldPanelAlarm = self.PanelAlarmStatus - if "mode" in decode: - d = decode["mode"].replace(" ","_").upper() - log.debug("Mode="+str(d)) + if "emulationmode" in decode: + d = decode["emulationmode"].replace(" ","_").upper() + log.debug("emulationmode="+str(d)) self.PanelMode = AlPanelMode.value_of(d) if "status" in decode: d = decode["status"].replace(" ","_").upper() diff --git a/custom_components/visonic/pyvisonic.py b/custom_components/visonic/pyvisonic.py index c5f66dd..be29180 100644 --- a/custom_components/visonic/pyvisonic.py +++ b/custom_components/visonic/pyvisonic.py @@ -40,7 +40,7 @@ # import requests # The defaults are set for use in Home Assistant. -# If using MocroPython / CircuitPython then set these values in the environment +# If using MicroPython / CircuitPython then set these values in the environment MicroPython = os.getenv("MICRO_PYTHON") if MicroPython is not None: @@ -96,7 +96,6 @@ def convertByteArray(s) -> bytearray: from string import punctuation from textwrap import wrap - try: from .pyconst import (AlTransport, AlPanelDataStream, NO_DELAY_SET, PanelConfig, AlConfiguration, AlPanelMode, AlPanelCommand, AlTroubleType, AlPanelEventData, AlAlarmType, AlPanelStatus, AlSensorCondition, AlCommandStatus, AlX10Command, AlCondition, AlLogPanelEvent, AlSensorType, AlTerminationType, PE_PARTITION) @@ -108,7 +107,7 @@ def convertByteArray(s) -> bytearray: from pyhelper import (toString, MyChecksumCalc, AlImageManager, ImageRecord, titlecase, AlPanelInterfaceHelper, AlSensorDeviceHelper, AlSwitchDeviceHelper) -PLUGIN_VERSION = "1.7.1.0" +PLUGIN_VERSION = "1.8.0.0" # Obfuscate sensitive data, regardless of the other Debug settings. # Setting this to True limits the logging of messages sent to the panel to CMD or NONE @@ -207,7 +206,7 @@ class DebugLevel(IntEnum): # Interval (in seconds) to get the time and for most panels to try and set it if it's out by more than TIME_INTERVAL_ERROR seconds # PowerMaster uses time interval for checking motion triggers so more critical to keep it updated POWERMASTER_CHECK_TIME_INTERVAL = 300 # 5 minutes (this uses B0 messages and not DOWNLOAD panel state) -POWERMAX_CHECK_TIME_INTERVAL = 14400 # 4 hours (this uses the DOWNLOAD panel state +POWERMAX_CHECK_TIME_INTERVAL = 14400 # 4 hours (this uses the DOWNLOAD panel state) TIME_INTERVAL_ERROR = 3 # Message/Packet Constants to make the code easier to read @@ -273,7 +272,7 @@ def peek_nowait(self): # Don't know what 9, 11, 12 or 14 are so just copy other settings. I know that there are Commercial/Industry Panel versions so it might be them # This data defines each panel type's maximum capability # I know that panel types 4 and 5 support 3 partitions but I can't figure out how they are represented in A5 and A7 messages, so partitions only supported for PowerMaster and B0 messages -pmPanelConfig = { # 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 See pmPanelType above +pmPanelConfig = { # 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 See pmPanelType above "CFG_SUPPORTED" : ( False, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True ), # Supported Panels i.e. not a PowerMax "CFG_KEEPALIVE" : ( 0, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 15, 25, 25, 15 ), # Keep Alive message interval if no other messages sent "CFG_DLCODE_1" : ( "", "5650", "5650", "5650", "5650", "5650", "5650", "AAAA", "AAAA", "AAAA", "AAAA", "AAAA", "AAAA", "AAAA", "AAAA", "AAAA", "AAAA" ), # Default download codes (for reset panels or panels that have not been changed) @@ -600,23 +599,6 @@ class IndexName(IntEnum): EVENT_TYPE_FORCE_ARM = 0x59 EVENT_TYPE_DISARM = 0x55 -# Zone names are taken from the panel, so no langauage support needed, these are updated when EEPROM is downloaded or the B0 message is received -# TODO : Ensure that the EPROM and downloaded strings match the lower case with underscores to match the language files where possible -pmZoneName = [ - "attic", "back_door", "basement", "bathroom", "bedroom", "child_room", "conservatory", "play_room", "dining_room", "downstairs", - "emergency", "fire", "front_door", "garage", "garage_door", "guest_room", "hall", "kitchen", "laundry_room", "living_room", - "master_bathroom", "master_bedroom", "office", "upstairs", "utility_room", "yard", "custom_1", "custom_2", "custom_3", - "custom_4", "custom_5", "not_installed" -] -# ['Loft', 'Back door', 'Cellar', 'Bathroom', 'Bedroom', 'Child room', 'Conservatory', 'Play room', 'Dining room', 'Downstairs'] -# ['Emergency', 'Fire', 'Front door', 'Garage', 'Garage door', 'Office', 'Hall', 'Kitchen', 'Laundry room', 'Living room'] -# ['Master bath', 'Master Bdrm', 'Study', 'Upstairs', 'Utility room', 'Garden'] - - - - - - ############################################################################################################################################################################################################################################## ########################## EEPROM Decode ################################################################################################################################################################################################### ############################################################################################################################################################################################################################################## @@ -755,7 +737,7 @@ class IndexName(IntEnum): "x10ZoneNames" : SettingsCommand( Dumpy, 16, "BYTE", 2864, 8, 1, -1, "X10 Location Name references", {} ), # #"MaybeScreenSaver":SettingsCommand( Dumpy, 75, "BYTE", 5888, 8, 1, -1, "Maybe the screen saver", {} ), # Structure not known -# "ZoneStringNames": SettingsCommand( Dumpy, 32,"STRING", 6400, 128, 16, -1, "Zone String Names", {} ), # Zone String Names e.g "Attic", "Back door", "Basement", "Bathroom" etc 32 strings of 16 characters each, replace pmZoneName + "ZoneStringNames": SettingsCommand( Dumpy, 32,"STRING", 6400, 128, 16, -1, "Zone String Names", {} ), # Zone String Names e.g "Attic", "Back door", "Basement", "Bathroom" etc 32 strings of 16 characters each #PowerMax Only @@ -824,47 +806,62 @@ class PanelSetting(IntEnum): # PanelModel = 19 # PanelDownload = 20 DeviceTypes = 21 -# ZoneNameString = 22 + ZoneNameString = 22 PartitionEnabled = 23 + ZoneCustNameStr = 24 # TestTest = 200 -B0All = True -PanelSettingsCollection = collections.namedtuple('PanelSettingsCollection', 'length display datatype datacount msg') # overall length in bytes, datatype in bits -pmPanelSettingsB0 = { - 0x0000 : PanelSettingsCollection( 6, B0All, 1, 6, "Central Station Account Number 1"), # size of each entry is 6 nibbles - 0x0001 : PanelSettingsCollection( 6, B0All, 1, 6, "Central Station Account Number 2"), # size of each entry is 6 nibbles - 0x0002 : PanelSettingsCollection( 7, B0All, 2, 0, "Panel Serial Number"), - 0x0003 : PanelSettingsCollection( 9, B0All, 1, 12, "Central Station IP 1"), # 12 nibbles e.g. 192.168.010.001 - 0x0004 : PanelSettingsCollection( 6, B0All, 1, 0, "Central Station Port 1"), # - 0x0005 : PanelSettingsCollection( 9, B0All, 1, 12, "Central Station IP 2"), # 12 nibbles - 0x0006 : PanelSettingsCollection( 6, B0All, 1, 0, "Central Station Port 2"), # - 0x0007 : PanelSettingsCollection( 39, B0All, 4, 0, "Capabilities unknown"), # - 0x0008 : PanelSettingsCollection( 99, False, 1, 4, "User Code"), # size of each entry is 4 nibbles - 0x000D : PanelSettingsCollection( 0, B0All, 10, 32, "Zone Names"), # 32 nibbles i.e. each string name is 16 bytes long - 0x000F : PanelSettingsCollection( 5, False, 1, 4, "Download Code"), # size of each entry is 4 nibbles - 0x0010 : PanelSettingsCollection( 4, B0All, 4, 0, "Panel EPROM Version 1"), - 0x0024 : PanelSettingsCollection( 5, B0All, 4, 0, "Panel EPROM Version 2"), - 0x0029 : PanelSettingsCollection( 4, B0All, 4, 0, "Unknown B"), - 0x0030 : PanelSettingsCollection( 4, B0All, 4, 0, "Unknown C"), - 0x002C : PanelSettingsCollection( 19, B0All, 6, 0, "Panel Default Version"), - 0x002D : PanelSettingsCollection( 19, B0All, 6, 0, "Panel Software Version"), - 0x0033 : PanelSettingsCollection( 67, B0All, 4, 64, "Zone Chime Data"), - 0x0036 : PanelSettingsCollection( 67, B0All, 4, 64, "Partition Data"), - 0x003C : PanelSettingsCollection( 18, B0All, 8, 0, "Panel Hardware Version"), - 0x003D : PanelSettingsCollection( 19, B0All, 6, 0, "Panel RSU Version"), - 0x003E : PanelSettingsCollection( 19, B0All, 6, 0, "Panel Boot Version"), - 0x0054 : PanelSettingsCollection( 5, False, 1, 4, "Installer Code"), # size of each entry is 4 nibbles - 0x0055 : PanelSettingsCollection( 5, False, 1, 4, "Master Code"), # size of each entry is 4 nibbles - 0x0058 : PanelSettingsCollection( 4, B0All, 4, 2, "Unknown D"), - 0x0106 : PanelSettingsCollection( 4, B0All, 4, 1, "Unknown A"), # size of each entry is 4 nibbles +B0All = not OBFUS # False +PanelSettingsCollection = collections.namedtuple('PanelSettingsCollection', 'sequence length display datatype datacount msg') # overall length in bytes, datatype in bits +pmPanelSettingsB0_35 = { + 0x0000 : PanelSettingsCollection( None, 6, B0All, 1, 6, "Central Station Account Number 1"), # size of each entry is 6 nibbles + 0x0001 : PanelSettingsCollection( None, 6, B0All, 1, 6, "Central Station Account Number 2"), # size of each entry is 6 nibbles + 0x0002 : PanelSettingsCollection( None, 7, B0All, 2, 0, "Panel Serial Number"), + 0x0003 : PanelSettingsCollection( None, 9, B0All, 1, 12, "Central Station IP 1"), # 12 nibbles e.g. 192.168.010.001 + 0x0004 : PanelSettingsCollection( None, 6, B0All, 1, 0, "Central Station Port 1"), # + 0x0005 : PanelSettingsCollection( None, 9, B0All, 1, 12, "Central Station IP 2"), # 12 nibbles + 0x0006 : PanelSettingsCollection( None, 6, B0All, 1, 0, "Central Station Port 2"), # + 0x0007 : PanelSettingsCollection( None, 39, B0All, 4, 0, "Capabilities unknown"), # + 0x0008 : PanelSettingsCollection( None, 99, not OBFUS, 1, 4, "User Code"), # size of each entry is 4 nibbles + 0x000D : PanelSettingsCollection( [1,2,255], 0, B0All, 10, 32, "Zone Names"), # 32 nibbles i.e. each string name is 16 bytes long. The 0x35 message has 3 sequenced messages. + 0x000F : PanelSettingsCollection( None, 5, not OBFUS, 1, 4, "Download Code"), # size of each entry is 4 nibbles + 0x0010 : PanelSettingsCollection( None, 4, B0All, 4, 0, "Panel EPROM Version 1"), + 0x0024 : PanelSettingsCollection( None, 5, B0All, 4, 0, "Panel EPROM Version 2"), + 0x0029 : PanelSettingsCollection( None, 4, B0All, 4, 0, "Unknown B"), + 0x0030 : PanelSettingsCollection( None, 4, B0All, 4, 0, "Partition Enabled"), + 0x002C : PanelSettingsCollection( None, 19, B0All, 6, 0, "Panel Default Version"), + 0x002D : PanelSettingsCollection( None, 19, B0All, 6, 0, "Panel Software Version"), + 0x0033 : PanelSettingsCollection( None, 67, B0All, 4, 64, "Zone Chime Data"), + 0x0036 : PanelSettingsCollection( None, 67, B0All, 4, 64, "Partition Data"), + 0x003C : PanelSettingsCollection( None, 18, B0All, 8, 0, "Panel Hardware Version"), + 0x003D : PanelSettingsCollection( None, 19, B0All, 6, 0, "Panel RSU Version"), + 0x003E : PanelSettingsCollection( None, 19, B0All, 6, 0, "Panel Boot Version"), + 0x0042 : PanelSettingsCollection( [1,2,255], 0, B0All, 10, 32, "Custom Zone Names"), + 0x0054 : PanelSettingsCollection( None, 5, not OBFUS, 1, 4, "Installer Code"), # size of each entry is 4 nibbles + 0x0055 : PanelSettingsCollection( None, 5, not OBFUS, 1, 4, "Master Code"), # size of each entry is 4 nibbles + 0x0058 : PanelSettingsCollection( None, 4, B0All, 4, 2, "Unknown D"), + 0x0106 : PanelSettingsCollection( None, 4, B0All, 4, 1, "Unknown A"), # size of each entry is 4 nibbles +} + +pmPanelSettingsB0_42 = { + 0x0000 : PanelSettingsCollection( None, 6, B0All, 1, 6, "Central Station Account Number 1"), # size of each entry is 6 nibbles + 0x0008 : PanelSettingsCollection( None, 110, not OBFUS, 1, 48, "User Code"), # size of each entry is 4 nibbles + 0x000D : PanelSettingsCollection( None, 0, B0All, 10, 21, "Zone Names"), # 32 nibbles i.e. each string name is 16 bytes long. The 0x35 message has 3 sequenced messages. + 0x0030 : PanelSettingsCollection( None, 15, B0All, 4, 1, "Partition Enabled"), + 0x0033 : PanelSettingsCollection( None, 78, B0All, 4, 64, "Zone Chime Data"), + 0x0036 : PanelSettingsCollection( None, 78, B0All, 4, 64, "Partition Data"), + 0x0042 : PanelSettingsCollection( None, 0, B0All, 10, 32, "Custom Zone Names"), # variable length with the sequence of messages +# 0x00a4 : PanelSettingsCollection( None, 0, B0All, 4, 2, "XXXXXXXXX"), + 0x0106 : PanelSettingsCollection( None, 4, B0All, 4, 1, "Unknown A"), # size of each entry is 4 nibbles } + # pmPanelSettingCodes represents the ways that we can get data to populate the PanelSettings # A PowerMax Panel only has 1 way and that is to download the EPROM = PMaxEPROM # A PowerMaster Panel has 3 ways: # 1. Download the EPROM = PMasterEPROM -# 2. Ask the panel for a B0 panel settings message 0x51 e.g. 0x0800 sends the user codes = PMasterB0Panel +# 2. Ask the panel for a B0 panel settings message 0x51 e.g. 0x0800 sends the user codes = PMasterB035Panel # 3. Ask the panel for a B0 data message = PMasterB0Mess PMasterB0Index # These are conversion to string functions @@ -878,37 +875,39 @@ def psc_lba(p): # p = a list of bytearrays def psc_dummy(p): return p -PanelSettingCodesType = collections.namedtuple('PanelSettingCodesType', 'item PMaxEPROM PMasterEPROM PMasterB0Panel PMasterB0Mess PMasterB0Index tostring default') -# For PMasterB0Mess there is an assumption that the message type is 0x03, and this is the subtype +PanelSettingCodesType = collections.namedtuple('PanelSettingCodesType', 'item PMaxEPROM PMasterEPROM PMasterB035Panel PMasterB042Panel PMasterB0Mess PMasterB0Index tostring default') +# For PMasterB0Mess there is an assumption that the message type is 0x03, and this is the subtype # PMasterB0Index index 3 is Sensor data, I should have an enum for this -pmPanelSettingCodes = { # These are used to create the self.PanelSettings dictionary to create a common set of settings across the different ways of obtaining them -# item PMaxEPROM PMasterEPROM PMasterB0Panel PMasterB0Mess PMasterB0Index tostring default - PanelSetting.UserCodes : PanelSettingCodesType( None, "userCodeMax", "userCodeMaster", 0x0008, None, None, toString , bytearray([0,0]) ), - PanelSetting.PartitionData : PanelSettingCodesType( None, "PartitionData", "PartitionData", 0x0036, None, None, toString , bytearray()), - PanelSetting.ZoneNames : PanelSettingCodesType( None, "ZoneNamePMax", "ZoneNamePMaster", None , "ZONE_NAMES", IndexName.ZONES, toString , bytearray()), -# PanelSetting.ZoneNameString : PanelSettingCodesType( None, None, None, 0x000D, None, None, toString , bytearray()), # The string names themselves - PanelSetting.ZoneTypes : PanelSettingCodesType( None, None, None, None , "ZONE_TYPES", IndexName.ZONES, toString , bytearray()), # - PanelSetting.ZoneExt : PanelSettingCodesType( None, None, "ZoneExtPMaster", None , None, None, toString , bytearray()), - PanelSetting.DeviceTypes : PanelSettingCodesType( None, None, None , None , "DEVICE_TYPES", IndexName.ZONES, toString , bytearray()), - PanelSetting.ZoneDelay : PanelSettingCodesType( None, None, "ZoneDelay", None , None, None, toString , bytearray(64)), # Initialise to 0s so it passes the I've got it from the panel test until I know how to get this using B0 data - PanelSetting.ZoneData : PanelSettingCodesType( None, "ZoneDataPMax", "ZoneDataPMaster", None , None, None, toString , bytearray()), - PanelSetting.ZoneEnrolled : PanelSettingCodesType( None, None, None, None , "SENSOR_ENROL", IndexName.ZONES, psc_dummy, [] ), - PanelSetting.PanelBypass : PanelSettingCodesType( 0, "panelbypass", "panelbypass", None , None, None, psc_dummy, [NOBYPASSSTR]), - PanelSetting.PartitionEnabled : PanelSettingCodesType( None, None, None, 0x0030, None, None, toString , bytearray() ), -# PanelSetting.TestTest : PanelSettingCodesType( None, None, None, 0x0031, None, None, toString , bytearray() ), - PanelSetting.ZoneChime : PanelSettingCodesType( None, None, None, 0x0033, None, None, toString , bytearray() ) -} - -# PanelSetting.PanelDownload : PanelSettingCodesType( None, "masterDlCode", "masterDlCode", 0x000f, None, None, psc_dummy , bytearray()), -# PanelSetting.PanelSerial : PanelSettingCodesType( 0, "panelSerial", "panelSerial", 0x0002, None, None, psc_dummy, ["Undefined"] ), -# PanelSetting.Keypad_1Way : PanelSettingCodesType( None, "Keypad1PMax", None, None , None, None, toString , bytearray()), # PowerMaster Panels do not have 1 way keypads -# PanelSetting.Keypad_2Way : PanelSettingCodesType( None, "Keypad2PMax", "KeypadPMaster", None , None, None, toString , bytearray()), -# PanelSetting.KeyFob : PanelSettingCodesType( None, "KeyFobsPMax", "", None , None, None, toString , bytearray()), -# PanelSetting.Sirens : PanelSettingCodesType( None, "SirensPMax", "SirensPMaster", None , None, None, toString , bytearray()), -# PanelSetting.AlarmLED : PanelSettingCodesType( None, None, "AlarmLED", None , None, None, toString , bytearray()), -# PanelSetting.ZoneSignal : PanelSettingCodesType( None, "ZoneSignalPMax", "", None , None, None, toString , bytearray()), -# PanelSetting.PanicAlarm : PanelSettingCodesType( 0, "panicAlarm", "panicAlarm", None , None, None, psc_dummy, [False]), -# PanelSetting.PanelModel : PanelSettingCodesType( 0, "panelModelCode", "panelModelCode", None , None, None, psc_lba , [bytearray([0,0,0,0])]), +# These are used to create the self.PanelSettings dictionary to create a common set of settings across the different ways of obtaining them +# item PMaxEPROM PMasterEPROM PMasterB035Panel PMasterB042Panel PMasterB0Mess PMasterB0Index tostring default +pmPanelSettingCodes = { + PanelSetting.UserCodes : PanelSettingCodesType( None, "userCodeMax", "userCodeMaster", None , 0x0008, None, None, toString , bytearray([0,0]) ), + PanelSetting.PartitionData : PanelSettingCodesType( None, "PartitionData", "PartitionData", None , 0x0036, None, None, toString , bytearray()), + PanelSetting.ZoneNames : PanelSettingCodesType( None, "ZoneNamePMax", "ZoneNamePMaster", None , None , "ZONE_NAMES", IndexName.ZONES, toString , bytearray()), + PanelSetting.ZoneNameString : PanelSettingCodesType( None, "ZoneStringNames","ZoneStringNames", None , 0x000D, None, None, psc_dummy, [] ), # The string names themselves + PanelSetting.ZoneCustNameStr : PanelSettingCodesType( None, None, None, None , 0x0042, None, None, psc_dummy, [] ), # The string names themselves force a 42 message to test + PanelSetting.ZoneTypes : PanelSettingCodesType( None, None, None, None , None , "ZONE_TYPES", IndexName.ZONES, toString , bytearray()), # + PanelSetting.ZoneExt : PanelSettingCodesType( None, None, "ZoneExtPMaster", None , None , None, None, toString , bytearray()), + PanelSetting.DeviceTypes : PanelSettingCodesType( None, None, None , None , None , "DEVICE_TYPES", IndexName.ZONES, toString , bytearray()), + PanelSetting.ZoneDelay : PanelSettingCodesType( None, None, "ZoneDelay", None , None , None, None, toString , bytearray(64)), # Initialise to 0s so it passes the I've got it from the panel test until I know how to get this using B0 data + PanelSetting.ZoneData : PanelSettingCodesType( None, "ZoneDataPMax", "ZoneDataPMaster", None , None , None, None, toString , bytearray()), + PanelSetting.ZoneEnrolled : PanelSettingCodesType( None, None, None, None , None , "SENSOR_ENROL", IndexName.ZONES, psc_dummy, [] ), + PanelSetting.PanelBypass : PanelSettingCodesType( 0, "panelbypass", "panelbypass", None , None , None, None, psc_dummy, [NOBYPASSSTR]), + PanelSetting.PartitionEnabled : PanelSettingCodesType( None, None, None, None , 0x0030, None, None, toString , bytearray() ), +# PanelSetting.TestTest : PanelSettingCodesType( None, None, None, None , 0x0031, None, None, toString , bytearray() ), + PanelSetting.ZoneChime : PanelSettingCodesType( None, None, None, None , 0x0033, None, None, toString , bytearray() ) +} + +# PanelSetting.PanelDownload : PanelSettingCodesType( None, "masterDlCode", "masterDlCode", 0x000f, 0x000f, None, None, psc_dummy , bytearray()), +# PanelSetting.PanelSerial : PanelSettingCodesType( 0, "panelSerial", "panelSerial", 0x0002, 0x0002, None, None, psc_dummy, ["Undefined"] ), +# PanelSetting.Keypad_1Way : PanelSettingCodesType( None, "Keypad1PMax", None, None , None , None, None, toString , bytearray()), # PowerMaster Panels do not have 1 way keypads +# PanelSetting.Keypad_2Way : PanelSettingCodesType( None, "Keypad2PMax", "KeypadPMaster", None , None , None, None, toString , bytearray()), +# PanelSetting.KeyFob : PanelSettingCodesType( None, "KeyFobsPMax", "", None , None , None, None, toString , bytearray()), +# PanelSetting.Sirens : PanelSettingCodesType( None, "SirensPMax", "SirensPMaster", None , None , None, None, toString , bytearray()), +# PanelSetting.AlarmLED : PanelSettingCodesType( None, None, "AlarmLED", None , None , None, None, toString , bytearray()), +# PanelSetting.ZoneSignal : PanelSettingCodesType( None, "ZoneSignalPMax", "", None , None , None, None, toString , bytearray()), +# PanelSetting.PanicAlarm : PanelSettingCodesType( 0, "panicAlarm", "panicAlarm", None , None , None, None, psc_dummy, [False]), +# PanelSetting.PanelModel : PanelSettingCodesType( 0, "panelModelCode", "panelModelCode", None , None , None, None, psc_lba , [bytearray([0,0,0,0])]), # These blocks are not value specific, they are used to download blocks of EEPROM data that we need without reference to what the data means # They are used when EEPROM_DOWNLOAD_ALL is False @@ -917,15 +916,15 @@ def psc_dummy(p): pmBlockDownload_Short = { "PowerMax" : ( ( 0x0100, 0x0500 ), - ( 0x0900, 0x0C00 ) -# ( 0x1900, 0x1B00 ), # ZoneStringNames 0x1900 = 6400 Decimal + ( 0x0900, 0x0C00 ), + ( 0x1900, 0x1B00 ), # ZoneStringNames 0x1900 = 6400 Decimal # ( 0x5800, 0x5A00 ), # pmZoneType_t starts at 0x5828 # ( 0x64D8, 0x6510 ) # pmZoneChime starts at 0x64D8 ), "PowerMaster" : ( ( 0x0100, 0x0500 ), ( 0x0900, 0x0C00 ), -# ( 0x1900, 0x1B00 ), # ZoneStringNames 0x1900 = 6400 Decimal + ( 0x1900, 0x1B00 ), # ZoneStringNames 0x1900 = 6400 Decimal # ( 0x8100, 0x8200 ), # ( 0x8EB0, 0x8EE8 ), # Chime ( 0xB600, 0xBB00 ), @@ -950,6 +949,13 @@ def psc_dummy(p): # for d in pmBlockDownload[t]: # print(toString(d)) +# Zone names are translated using the language translation file. These need to match the keys in the translations. +pmZoneName = [ + "attic", "back_door", "basement", "bathroom", "bedroom", "child_room", "conservatory", "play_room", "dining_room", "downstairs", + "emergency", "fire", "front_door", "garage", "garage_door", "guest_room", "hall", "kitchen", "laundry_room", "living_room", + "master_bathroom", "master_bedroom", "office", "upstairs", "utility_room", "yard", "custom_1", "custom_2", "custom_3", + "custom_4", "custom_5", "not_installed" +] pmZoneTypeKey = ( "non-alarm", "emergency", "flood", "gas", "delay_1", "delay_2", "interior_follow", "perimeter", "perimeter_follow", "24_hours_silent", "24_hours_audible", "fire", "interior", "home_delay", "temperature", "outdoor", "undefined" ) @@ -1291,6 +1297,9 @@ def resetGlobals(self): self.B0_Message_Wanted = set() self.B0_Message_Waiting = set() self.B0_LastPanelStateTime = self._getUTCTimeFunction() + + # Time difference between Panel and Integration + self.Panel_Integration_Time_Difference = None # determine when MSG_ENROLL is sent to the panel self.nextDownloadCode = None @@ -1380,7 +1389,6 @@ def updateSettings(self, newdata: PanelConfig): if len(tmpDLCode) == 4 and type(tmpDLCode) is str: self.DownloadCode = tmpDLCode[0:2] + " " + tmpDLCode[2:4] log.debug(f"[Settings] Download Code set to {self.DownloadCode}") - if self.DisableAllCommands: self.ForceStandardMode = True # By the time we get here there are 3 combinations of self.DisableAllCommands and self.ForceStandardMode @@ -1390,13 +1398,7 @@ def updateSettings(self, newdata: PanelConfig): # The if statement above ensure these are the only supported combinations. log.debug(f"[Settings] ForceStandard = {self.ForceStandardMode} DisableAllCommands = {self.DisableAllCommands}") - if self.ForceStandardMode: - log.error(f"[Settings] Force Standard is not supported in this development release. Please either change your configuration or go back to the main release from HACS.") - self.ForceStandardMode = False # INTERFACE : Get user variable from HA to force standard mode or try for PowerLink - self.DisableAllCommands = False # INTERFACE : Get user variable from HA to allow or disable all commands to the panel - - - def getPanelZoneSetting(self, p : PanelSetting, zone : int) -> int | bool | None: + def getPanelZoneSetting(self, p : PanelSetting, zone : int) -> str | int | bool | None: # Do not use for usercodes if p in self.PanelSettings: if zone < len(self.PanelSettings[p]): @@ -1625,6 +1627,44 @@ def _create_B0_35_Data_Request(self, taglist : list = None, strlist : str = None log.debug(f"[_create_B0_35_Data_request] Returning {toString(To_Send)}") return To_Send + def _create_B0_42_Data_Request(self, taglist : list = None, strlist : str = None) -> bytearray: + if taglist is None and strlist is None: + return bytearray() + elif taglist is not None: + PM_Request_Data = bytearray(taglist) + elif strlist is not None: + log.debug(f"[_create_B0_42_Data_request] {strlist}") + PM_Request_Data = convertByteArray(strlist) + else: + log.debug(f"[_create_B0_42_Data_request] Error not sending anything as both params set incorrectly") + return bytearray() + + if len(PM_Request_Data) % 2 == 1: # its an odd number of bytes + log.debug(f"[_create_B0_42_Data_request] Error not sending anything as its an odd number of bytes {toString(PM_Request_Data)}") + return bytearray() + elif len(PM_Request_Data) == 2: + PM_Request_Data.extend(convertByteArray("00 00 ff ff")) + special = 2 + elif len(PM_Request_Data) == 4: + PM_Request_Data.extend(convertByteArray("00 00")) + special = 6 + else: + special = 6 + + PM_Request_Start = convertByteArray(f'b0 01 42 99 0{special} ff 08 0c 99') + PM_Request_End = convertByteArray('43') + + PM_Data = PM_Request_Start + PM_Request_Data + PM_Request_End + + PM_Data[3] = len(PM_Request_Data) + 5 + PM_Data[8] = len(PM_Request_Data) + + CS = self._calculateCRC(PM_Data) # returns a bytearray with a single byte + To_Send = bytearray([0x0d]) + PM_Data + CS + bytearray([0x0a]) + + log.debug(f"[_create_B0_42_Data_request] Returning {toString(To_Send)}") + return To_Send + def _create_B0_Data_Request(self, taglist : list = None, strlist : str = None) -> bytearray: if taglist is None and strlist is None: @@ -1723,8 +1763,7 @@ def sleepytime(interval) -> float: post_delay = self._sendPdu(instruction) #log.debug(f"[_despatcher] Nothing to do queue size {self.SendQueue.qsize()}") else: #elif not self.pmDownloadMode: - # We're waiting for a message back from the panel before continuing (and we're not downloading EPROM) - # Do not do the timeouts when getting EPROM, the sequencer sorts it all out + # We're waiting for a message back from the panel before continuing # self.pmExpectedResponse will prevent us sending another message to the panel if interval > RESPONSE_TIMEOUT: # If the panel is lazy or we've got the timing wrong........ @@ -1738,9 +1777,8 @@ def sleepytime(interval) -> float: log.debug(f"[_despatcher] While Waiting for: {st}") # Reset Send state (clear queue and reset flags) self._clearReceiveResponseList() - #self._emptySendQueue(pri_level = 1) self._triggerRestoreStatus() # Clear message buffers and send a Restore (if in Powerlink) or Status (not in Powerlink) to the Panel - elif interval > RESEND_MESSAGE_TIMEOUT: + elif self.pmLastSentMessage is not None and interval > RESEND_MESSAGE_TIMEOUT: # If there's a timeout then resend the previous message. If that doesn't work then dump the message and continue, but log the error if not self.pmLastSentMessage.triedResendingMessage: # resend the last message @@ -1750,13 +1788,12 @@ def sleepytime(interval) -> float: post_delay = self._sendPdu(self.pmLastSentMessage) else: # tried resending once, no point in trying again so reset settings, start from scratch - log.debug("[_despatcher] ****************************** Resend Timer Expired ********************************") - log.debug("[_despatcher] Tried Re-Sending last message but didn't work. Message is dumped") + log.debug(f"[_despatcher] ****************************** Resend Timer Expired ********************************") + log.debug(f"[_despatcher] Tried Re-Sending last message but didn't work. Message is dumped") # Reset Send state (clear queue and reset flags) self._clearReceiveResponseList() self._emptySendQueue(pri_level = 1) - # restart the watchdog and keep-alive counters - #self._triggerRestoreStatus() + # restart the watchdog and keep-alive counters self._reset_watchdog_timeout() self._reset_keep_alive_messages() @@ -1859,6 +1896,43 @@ def _resetPanelInterface(): if not self.PowerLinkBridgeConnected and self.pmInitSupportedByPanel: self._addMessageToSendList("MSG_INIT") + def _requestMissingPanelConfig(missing): + m = [] + for a in missing: + if a in pmPanelSettingCodes and pmPanelSettingCodes[a].PMasterB035Panel is not None: + m.append(pmPanelSettingCodes[a].PMasterB035Panel) # 35 message types to ask for + if len(m) > 0: + log.debug(f"[_requestMissingPanelConfig] Type 35 Wanting {m}") + tmp = bytearray() + for a in m: + y1, y2 = (a & 0xFFFF).to_bytes(2, "little") + tmp = tmp + bytearray([y1, y2]) + s = self._create_B0_35_Data_Request(strlist = toString(tmp)) + self._addMessageToSendList(s, priority = MessagePriority.IMMEDIATE) + else: + m = [] + for a in missing: + if a in pmPanelSettingCodes and pmPanelSettingCodes[a].PMasterB042Panel is not None: + m.append(pmPanelSettingCodes[a].PMasterB042Panel) # 42 message types to ask for + if len(m) > 0: + log.debug(f"[_requestMissingPanelConfig] Type 42 Wanting {m}") + tmp = bytearray() + for a in m: + y1, y2 = (a & 0xFFFF).to_bytes(2, "little") + tmp = tmp + bytearray([y1, y2]) + s = self._create_B0_42_Data_Request(strlist = toString(tmp)) + self._addMessageToSendList(s, priority = MessagePriority.IMMEDIATE) + else: + m = [] + for a in missing: + if a in pmPanelSettingCodes and pmPanelSettingCodes[a].PMasterB0Mess is not None: + m.append(pmPanelSettingCodes[a].PMasterB0Mess) + if len(m) > 0: + log.debug(f"[_requestMissingPanelConfig] Wanting {m}") + tmp = [pmSendMsgB0[i].data if isinstance(i, str) and i in pmSendMsgB0 else i for i in m] + s = self._create_B0_Data_Request(taglist = tmp) + self._addMessageToSendList(s, priority = MessagePriority.IMMEDIATE) + def _gotoStandardModeStopDownload(): if not self.PowerLinkBridgeConnected: # Should not be in this function when this is True but use it anyway if self.DisableAllCommands: @@ -1952,6 +2026,9 @@ def startDespatcher(): log.debug("[_sequencer] Starting _despatcher") self.despatcherTask = self.loop.create_task(self._despatcher()) + def toStringList(ll) -> []: + return {pmSendMsgB0_reverseLookup[i].data if isinstance(i, int) and i in pmSendMsgB0_reverseLookup else i for i in ll} + def reset_vars(): self.resetGlobals() @@ -2055,7 +2132,8 @@ def reset_vars(): self._addMessageToSendList("MSG_PL_BRIDGE", priority = MessagePriority.IMMEDIATE, options=[ [1, command], [2, param] ]) # Tell the Bridge to send me the status # Make a 1 off request for the panel to send the Download Code and the panel name e.g. PowerMaster-10 self.EnableB0ReceiveProcessing = True - s = self._create_B0_35_Data_Request(strlist = "3c 00 0f 00") + #s = self._create_B0_35_Data_Request(strlist = "3c 00 0f 00") + s = self._create_B0_42_Data_Request(strlist = "3c 00 0f 00") self._addMessageToSendList(s, priority = MessagePriority.IMMEDIATE) await asyncio.sleep(1.0) @@ -2155,40 +2233,16 @@ def reset_vars(): log.debug(f"[Process Settings] checkPanelDataPresent missing items {missing}") zoneCnt = pmPanelConfig["CFG_WIRELESS"][self.PanelType] + pmPanelConfig["CFG_WIRED"][self.PanelType] - if len(missing) == 0 and len(self.PanelSettings[PanelSetting.ZoneEnrolled]) >= zoneCnt: + if len(missing) == 0 and len(self.PanelSettings[PanelSetting.ZoneEnrolled]) >= zoneCnt: # Check to make certain we at least have the sensor enrolled data + if len(self.PanelSettings[PanelSetting.ZoneNameString]) == 21 and len(self.PanelSettings[PanelSetting.ZoneCustNameStr]) == 10: + log.debug(f"[Process Settings] extending ZoneNameString by ZoneCustNameStr") + self.PanelSettings[PanelSetting.ZoneNameString].extend(self.PanelSettings[PanelSetting.ZoneCustNameStr]) + self.PanelSettings[PanelSetting.ZoneNameString].append('Custom 6') # Make length 32 to make sure, this should not be used though _sequencerState = SequencerType.CreateSensors - - elif counter >= 15: # timeout + elif counter >= 20: # timeout _sequencerState = SequencerType.Reset - - elif counter % 2 == 0: # every 2 seconds (or so) - - m = [] - for a in missing: - if a in pmPanelSettingCodes and pmPanelSettingCodes[a].PMasterB0Panel is not None: - m.append(pmPanelSettingCodes[a].PMasterB0Panel) # 35 message types to ask for - if len(m) > 0: - log.debug(f"[Process Settings] Type 35 Wanting {m}") - tmp = bytearray() - for a in m: - y1, y2 = (a & 0xFFFF).to_bytes(2, "little") - tmp = tmp + bytearray([y1, y2]) - s = self._create_B0_35_Data_Request(strlist = toString(tmp)) - self._addMessageToSendList(s, priority = MessagePriority.IMMEDIATE) - else: - - m = [] - for a in missing: - if a in pmPanelSettingCodes and pmPanelSettingCodes[a].PMasterB0Mess is not None: - m.append(pmPanelSettingCodes[a].PMasterB0Mess) - if len(m) > 0: - log.debug(f"[Process Settings] Wanting {m}") - tmp = [pmSendMsgB0[i].data if isinstance(i, str) and i in pmSendMsgB0 else i for i in m] - s = self._create_B0_Data_Request(taglist = tmp) - self._addMessageToSendList(s, priority = MessagePriority.IMMEDIATE) - - #_last_B0_wanted_request_time = tnow - + elif counter % 3 == 0: # every 3 seconds (or so). This is a compromise delay, not too often so the panel starts sending back "wait" messages. + _requestMissingPanelConfig(missing) continue # just do the while loop elif _sequencerState == SequencerType.InitialiseEPROMDownload: ################################################################ InitialiseEPROMDownload ############################################## @@ -2542,7 +2596,8 @@ def reset_vars(): (self.PartitionState[0].PanelState == AlPanelStatus.DOWNLOADING or self.PanelMode == AlPanelMode.DOWNLOAD): # We may still be in the downloading state or the panel is in the downloading state _my_panel_state_trigger_count = _my_panel_state_trigger_count - 1 - log.debug(f"[_sequencer] By here we should be in normal operation, we are in {self.PanelMode.name} panel mode and status is {self.PartitionState[0].PanelState} {_my_panel_state_trigger_count=}") + log.debug(f"[_sequencer] By here we should be in normal operation, we are in {self.PanelMode.name} panel mode" + f" and status is {self.PartitionState[0].PanelState} {_my_panel_state_trigger_count=}") if _my_panel_state_trigger_count < 0: if self.PanelMode in [AlPanelMode.POWERLINK_BRIDGED]: # Restart the sequence from the beginning @@ -2726,10 +2781,10 @@ def reset_vars(): diff = (tnow - _last_B0_wanted_request_time).total_seconds() if diff >= 5: # every 5 seconds (or more) do an update if len(self.B0_Message_Waiting) > 0: # have we received the data that we last asked for - log.debug(f"[_sequencer] ****************************** Waiting For B0_Message_Waiting **************************** {self.B0_Message_Waiting}") + log.debug(f"[_sequencer] ****************************** Waiting For B0_Message_Waiting **************************** {toStringList(self.B0_Message_Waiting)}") self.B0_Message_Wanted.update(self.B0_Message_Waiting) # ask again for them if len(self.B0_Message_Wanted) > 0: - log.debug(f"[_sequencer] ****************************** Asking For B0_Message_Wanted **************************** {self.B0_Message_Wanted} timediff={diff}") + log.debug(f"[_sequencer] ****************************** Asking For B0_Message_Wanted **************************** {toStringList(self.B0_Message_Wanted)} timediff={diff}") tmp = {pmSendMsgB0[i].data if isinstance(i, str) and i in pmSendMsgB0 else i for i in self.B0_Message_Wanted} self.B0_Message_Wanted = set() s = self._create_B0_Data_Request(taglist = list(tmp)) @@ -3180,11 +3235,11 @@ def __init__(self, *args, **kwargs) -> None: self.B0_logCounter = 0 self.B0_temp = {} - - # Time difference between Panel and Integration - self.Panel_Integration_Time_Difference = None self.beezero_024B_sensorcount = None + + self.builderMessage = {} # Temporary variable + self.builderData = {} # Temporary variable # _saveEPROMSettings: add a certain setting to the settings table # When we send a MSG_DL and insert the 4 bytes from pmDownloadItem_t, what we're doing is setting the page, index and len @@ -3405,12 +3460,12 @@ def _updateSensor(self, zone) -> bool: updated = False created_new_sensor = False - + if zone not in self.SensorList: self.SensorList[zone] = SensorDevice( id = zone + 1 ) created_new_sensor = True - zoneName = "unknown" + zoneName = "not_installed" if zone < len(self.PanelSettings[PanelSetting.ZoneNames]): # zoneName = pmZoneName[zoneNames & 0x1F] @@ -3418,6 +3473,11 @@ def _updateSensor(self, zone) -> bool: updated = True self.SensorList[zone].zname = zoneName + if zoneNames is not None and self.SensorList[zone].zpanelname != (zn := self.getPanelZoneSetting(PanelSetting.ZoneNameString, zoneNames & 0x1F)): + updated = True + self.SensorList[zone].zpanelname = zn + log.debug(f"[Process Settings] _updateSensor Zone Panel Name {zone} : {zn} list is {self.PanelSettings[PanelSetting.ZoneNameString]}") + if device_type is not None: sensorType = AlSensorType.UNKNOWN sensorModel = "Model Unknown" @@ -3650,6 +3710,8 @@ def checkPanelDataPresent(self) -> list: if self.isPowerMaster(): need_these = {PanelSetting.UserCodes : pmPanelConfig["CFG_USERCODES"][self.PanelType], PanelSetting.ZoneNames : zoneCnt, + PanelSetting.ZoneNameString : 21, # All panels have 31 zone names (21 fixed and 10 user defined) e.g. Living Roon, Kitchen etc + PanelSetting.ZoneCustNameStr : 10, # 10 user defined zones PanelSetting.ZoneTypes : zoneCnt, PanelSetting.DeviceTypes : zoneCnt, PanelSetting.ZoneEnrolled : zoneCnt, @@ -4011,7 +4073,7 @@ def statelist(): processNormalData = not self.pmDownloadMode and self.PanelMode in [AlPanelMode.STANDARD, AlPanelMode.MINIMAL_ONLY, AlPanelMode.STANDARD_PLUS, AlPanelMode.POWERLINK_BRIDGED, AlPanelMode.POWERLINK] processB0 = self.EnableB0ReceiveProcessing or processNormalData - #log.debug(f"[_processReceivedPacket] {processAB=} {processNormalData=} {self.pmDownloadMode=}") + log.debug(f"[_processReceivedPacket] {processAB=} {processB0=} {processNormalData=} {self.pmDownloadMode=}") # Leave this here as it needs to be created dynamically to create the condition and message columns DecodeMessage = collections.namedtuple('DecodeMessage', 'condition, func, pushchange, message') @@ -4365,9 +4427,7 @@ def handle_msgtypeA5(self, data): # Status Message send_zone_type_request = False self.enrolled_old = val - # Build a B0 chunk out of the data to process it in a common way - ch = chunky(datasize = BITS, length = 4, data = data[2:6]) - self.updatePanelSetting(key = PanelSetting.ZoneEnrolled, ch = ch, display = True, msg = f"A5 Zone Enrolled Data") + self.updatePanelSetting(key = PanelSetting.ZoneEnrolled, length = 4, datasize = BITS, data = data[2:6], display = True, msg = f"A5 Zone Enrolled Data") self._updateAllSensors() self.do_sensor_update(data[6:10], "do_bypass", "[handle_msgtypeA5] Bypassed Zones 32-01") @@ -4474,7 +4534,7 @@ def handle_msgtypeAB(self, data) -> bool: # PowerLink Message self._reset_watchdog_timeout() subType = data[0] - if self.PanelMode in [AlPanelMode.POWERLINK, AlPanelMode.STANDARD_PLUS] and subType == 1: + if subType == 1 and self.PanelMode in [AlPanelMode.POWERLINK, AlPanelMode.STANDARD_PLUS]: # Panel Time log.debug("[handle_msgtypeAB] ***************************** Got Panel Time ****************************") @@ -4505,7 +4565,7 @@ def handle_msgtypeAB(self, data) -> bool: # PowerLink Message log.debug("[handle_msgtypeAB] ********************* Panel Mode not Powerlink / Standard Plus **********************") self.PanelKeepAlive = True - elif self.PanelMode == AlPanelMode.POWERLINK and subType == 5: # -- phone message + elif subType == 5 and self.PanelMode == AlPanelMode.POWERLINK: # -- phone message action = data[2] if action == 1: log.debug("[handle_msgtypeAB] PowerLink Phone: Calling User") @@ -4520,8 +4580,8 @@ def handle_msgtypeAB(self, data) -> bool: # PowerLink Message else: log.debug(f"[handle_msgtypeAB] PowerLink Phone: Unknown Action {hex(data[1]).upper()}") - elif self.PanelMode == AlPanelMode.POWERLINK and subType == 10 and data[2] == 0: - log.debug(f"[handle_msgtypeAB] PowerLink telling us what the code {data[3]} {data[4]} is for downloads, currently commented out as I'm not certain of this") + elif subType == 10 and data[2] == 0 and self.PanelMode == AlPanelMode.POWERLINK: + log.debug(f"[handle_msgtypeAB] PowerLink telling us what the code {data[3]} {data[4]} is for downloads, currently not used as I'm not certain of this") elif subType == 10 and data[2] == 1: if self.PanelMode == AlPanelMode.POWERLINK: @@ -4713,6 +4773,42 @@ def settings_data_type_formatter( self, data_type: int, data: bytes, string_size ] return data.hex(" ") + + def updatePanelSetting(self, key, length, datasize, data, display : bool = False, msg : str = ""): + + s = pmPanelSettingCodes[key].tostring(self.PanelSettings[key]) # Save the data before the update + + if pmPanelSettingCodes[key].item is not None: + if len(data) > pmPanelSettingCodes[key].item: + self.PanelSettings[key] = data[pmPanelSettingCodes[key].item] + elif datasize == BITS: + if len(self.PanelSettings[key]) < length * 8 : + # replace as current length less than the new data + #log.debug(f"[updatePanelSetting] {key=} replace") + self.PanelSettings[key] = [] + for i in range(0, length): + for j in range(0,8): # 8 bits in a byte + self.PanelSettings[key].append((data[i] & (1 << j)) != 0) + else: + # overwrite as current length is same as or more than new data + #log.debug(f"[updatePanelSetting] {key=} overwrite") + for i in range(0, length): + for j in range(0,8): # 8 bits in a byte + self.PanelSettings[key][(i*8)+j] = (data[i] & (1 << j)) != 0 + else: + self.PanelSettings[key] = data + + if display: + v = pmPanelSettingCodes[key].tostring(self.PanelSettings[key]) + if len(s) > 100 or len(v) > 100: + log.debug(f"[updatePanelSetting] {key=} ({msg})") + log.debug(f"[updatePanelSetting] replacing {s}") + log.debug(f"[updatePanelSetting] with {v}") + else: + log.debug(f"[updatePanelSetting] {key=} ({msg}) replacing {s} with {v}") + else: + log.debug(f"[updatePanelSetting] {key=} ({msg})") + def _extract_35_data(self, ch): #03 35 0b ff 08 ff 06 00 00 01 00 00 00 02 43 @@ -4724,26 +4820,59 @@ def _extract_35_data(self, ch): data = ch.data[3:] log.debug("[_extract_35_data] ***************************** Panel Settings ********************************") data_setting = self.settings_data_type_formatter(datatype, data) + if not OBFUS: - log.debug(f"[_extract_35_data] data_setting = {data_setting} type is {type(data_setting)}") - log.debug(f"[_extract_35_data] dataContent={hex(dataContent)} panel setting {datatype=} {datalen=} data={toString(data)}") - if dataContent in pmPanelSettingsB0: - d = pmPanelSettingsB0[dataContent] + log.debug(f"[_extract_35_data] dataContent={hex(dataContent)} panel setting {datatype=} {datalen=} data={toString(data)}") + log.debug(f"[_extract_35_data] data_setting type = {type(data_setting)} data_setting = {data_setting}") + + building = False + if dataContent in pmPanelSettingsB0_35: + d = pmPanelSettingsB0_35[dataContent] if (d.length == 0 or ch.length == d.length) and datatype == d.datatype: # Check the PanelSettings to see if there's one that refers to this dataContent for key in PanelSetting: - if key in pmPanelSettingCodes and pmPanelSettingCodes[key].PMasterB0Panel == dataContent: - ch.data = data - self.updatePanelSetting(key, ch, d.display, d.msg) + if key in pmPanelSettingCodes and pmPanelSettingCodes[key].PMasterB035Panel == dataContent: + if d.sequence is not None and isinstance(d.sequence, list): # We have a list of sequence identifiers e.g. [1,2,255] + # We should really check to make sure that we get all messages in the list but not yet --> TODO + # I'm assuming that 0x35 messages (and 0x42 maybe) are the only messages with sequences + if ch.datasize == BYTES and datatype == DataType.SPACE_PADDED_STRING_LIST: + # It is currently only PanelSetting.ZoneNameString that needs this + if key not in self.builderData: + self.builderData[key] = [] # empty the data list to concatenate the sequenced message data + self.builderMessage[key] = d.sequence # get the list of sequences there needs to be to complete the data + if ch.sequence in self.builderMessage[key]: + self.builderData[key].extend(data_setting) # Add actual data to the end of the list, we assume that the panel sends the data in order and we don't need to check the order + self.builderMessage[key].remove(ch.sequence) # Got this sequence data so remove from the list + else: + log.debug(f"[_extract_35_data] building {key} Unexpected data sequence {ch.sequence} or sequence sent more than once") + if ch.sequence == 255: + if len(self.builderMessage[key]) > 0: + # Received the sequence end but we are missing some of the sequence + log.debug(f"[_extract_35_data] building {key} We have the sequence terminator message but we still have missing sequenced messages {self.builderMessage[key]}. Dumping all message data and not using it") + else: + # copy across to use it + self.PanelSettings[key] = self.builderData[key] + if d.display: + log.debug(f"[_extract_35_data] building {key} {self.builderData[key]}") + building = ch.sequence != 255 + else: + self.updatePanelSetting(key = key, length = ch.length, datasize = ch.datasize, data = data, display = d.display, msg = d.msg) break else: log.debug(f"[_extract_35_data] {d.msg} data lengths differ: {ch.length=} {d.length=} type: {datatype=} {d.datatype=}") else: log.debug(f"[_extract_35_data] dataContent={hex(dataContent)} panel setting unknown {datatype=} {datalen=} data={toString(ch.data[3:])}") - + + if not building: + # remove it from the dictionary + self.builderMessage = {} # + self.builderData = {} # + if dataContent == 0x000F and datalen == 2 and isinstance(data_setting,str): - if not OBFUS: - log.debug(f"[_extract_35_data] Download code : {data_setting}") + if OBFUS: + log.debug(f"[_extract_35_data] Got Download Code") + else: + log.debug(f"[_extract_35_data] Download Code : {data_setting}") self.DownloadCode = data_setting[:2] + " " + data_setting[2:] elif dataContent == 0x003C and datalen == 15 and isinstance(data_setting,str): # 8 is a string @@ -4758,7 +4887,7 @@ def _extract_35_data(self, ch): if not self._setDataFromPanelType(p): log.debug(f"[_extract_35_data] Panel Type {data[5]} Unknown") else: - log.debug(f"[_extract_35_data] PanelType={self.PanelType} : {self.PanelModel} , Model={self.ModelType} Powermaster {self.PowerMaster}") + log.debug(f"[_extract_35_data] PanelType={self.PanelType} : {self.PanelModel} , Model={hexify(self.ModelType)} Powermaster {self.PowerMaster}") self.pmGotPanelDetails = True break else: @@ -4773,54 +4902,21 @@ def _extract_35_data(self, ch): log.debug("[_extract_35_data] ***************************** Panel Settings Exit ***************************") - def updatePanelSetting(self, key, ch, display : bool = False, msg : str = ""): - - s = pmPanelSettingCodes[key].tostring(self.PanelSettings[key]) - - if pmPanelSettingCodes[key].item is not None: - if len(ch.data) > pmPanelSettingCodes[key].item: - self.PanelSettings[key] = ch.data[pmPanelSettingCodes[key].item] - elif ch.datasize == BITS: - if len(self.PanelSettings[key]) < ch.length * 8 : - # replace as current length less than the new data - #log.debug(f"[updatePanelSetting] {key=} replace") - self.PanelSettings[key] = [] - for i in range(0, ch.length): - for j in range(0,8): # 8 bits in a byte - self.PanelSettings[key].append((ch.data[i] & (1 << j)) != 0) - else: - # overwrite as current length is same as or more than new data - #log.debug(f"[updatePanelSetting] {key=} overwrite") - for i in range(0, ch.length): - for j in range(0,8): # 8 bits in a byte - self.PanelSettings[key][(i*8)+j] = (ch.data[i] & (1 << j)) != 0 - else: - self.PanelSettings[key] = ch.data - - if display: - v = pmPanelSettingCodes[key].tostring(self.PanelSettings[key]) - if len(s) > 100 or len(v) > 100: - log.debug(f"[updatePanelSetting] {key=} ({msg})") - log.debug(f"[updatePanelSetting] replacing {s}") - log.debug(f"[updatePanelSetting] with {v}") - else: - log.debug(f"[updatePanelSetting] {key=} ({msg}) replacing {s} with {v}") - else: - log.debug(f"[updatePanelSetting] {key=} ({msg})") - - def _extract_42_data(self, data: bytes) -> tuple[int, str | int | list[str | int]]: + def _extract_42_data(self, ch) -> tuple[int, str | int | list[str | int]]: """Format a command 42 message. This has many parameter options to retrieve EPROM settings. bytes 0 & 1 are the parameter bytes 2 & 3 is the max number of data items - bytes 4 & 5 is the size of each data item - bytes 6 & 7 - don't know - bytes 8 & 9 is the data type + bytes 4 & 5 is the size of each data item (in bits) + bytes 6 & 7 don't know + bytes 8 is the data type + bytes 9 byte_size bytes 10 & 11 is the start index of data item bytes 12 & 13 is the number of data items bytes 14 to end is data + """ def chunk_bytearray(data: bytearray, size: int) -> list[bytes]: @@ -4828,41 +4924,114 @@ def chunk_bytearray(data: bytearray, size: int) -> list[bytes]: if data: return [data[i : i + size] for i in range(0, len(data), size)] - def settings_42_data_type_formatter( - data_type: int, data: bytes, byte_size: int = 1 - ) -> int | str | bytearray: + def settings_42_data_type_formatter(datatype: int, data: bytes, byte_size: int = 1) -> int | str | bytearray: """Preformatter for settings 42.""" - if data_type == DataType.SPACE_PADDED_STRING_LIST: + if datatype == DataType.SPACE_PADDED_STRING_LIST: # Remove any \x00 also when decoding. # On 42 46 00 strings are \n terminated name = data.decode("ascii", errors="ignore") return name.replace("\x00", "").rstrip("\n").rstrip(" ") - return self.settings_data_type_formatter(data_type, data, byte_size=byte_size) + return self.settings_data_type_formatter(datatype, data, byte_size=byte_size) + + data = ch.data - setting = b2i(data[0:2], big_endian=False) - # max_data_items = b2i(data[2:4], big_endian=False) + dataContent = b2i(data[0:2], big_endian=False) + max_data_items = b2i(data[2:4], big_endian=False) data_item_size = max(1, int(b2i(data[4:6], big_endian=False) / 8)) - data_type = data[8] # This is actually 2 bytes, what is second byte?? + not_known = b2i(data[6:8], big_endian=False) + datatype = data[8] # This is actually 2 bytes, what is second byte?? byte_size = 2 if data[9] == 0 else 1 - # start_entry = b2i(data[10:12], big_endian=False) - # entries = b2i(data[12:14], big_endian=False) + start_entry = b2i(data[10:12], big_endian=False) + no_of_entries = b2i(data[12:14], big_endian=False) # Split into entries data_items = chunk_bytearray(data[14:], data_item_size) - decoded_format = [] - log.debug(f"[_extract_42_data] 42 {setting=} {data_item_size=} {data_type=} {byte_size=}") - if data_items: + + processed_data = False + + log.debug(f"[_extract_42_data] 42 {dataContent=} {data_item_size=} {datatype=} {byte_size=}") + + if data_items and dataContent in pmPanelSettingsB0_42: + d = pmPanelSettingsB0_42[dataContent] + if (d.length == 0 or ch.length == d.length) and datatype == d.datatype: + # Check the PanelSettings to see if there's one that refers to this dataContent + for key in PanelSetting: + if key in pmPanelSettingCodes and pmPanelSettingCodes[key].PMasterB042Panel == dataContent: + log.debug(f"[_extract_42_data] Matched it {key=} {start_entry=} {no_of_entries=} len data_items = {len(data_items)} {d=}") + if ch.datasize == BYTES and datatype == DataType.SPACE_PADDED_STRING_LIST: + processed_data = True + s = f"{self.PanelSettings[key]}" # Save the data before the update + for idx, data_item in enumerate(data_items): + # We don't need to use sequence as we get the start_entry and no_of_entries in the message itself + #log.debug(f"[_extract_42_data] 42 data_item = {toString(data_item)}") + # Convert to correct data type + data_setting = settings_42_data_type_formatter(datatype, data_item, byte_size=byte_size) + if len(self.PanelSettings[key]) > start_entry+idx: + #log.debug(f"[_extract_42_data] Replacing {self.PanelSettings[key][start_entry+idx]} With {data_setting}") + self.PanelSettings[key][start_entry+idx] = data_setting + elif len(self.PanelSettings[key]) == start_entry+idx: # is it just 1 less, then simply append to the end + self.PanelSettings[key].append(data_setting) + #log.debug(f"[_extract_42_data] Appending, it is now {self.PanelSettings[key]}") + else: + aa = [f"Undefined{i}" for i in range(len(self.PanelSettings[key]), start_entry+idx+1)] + self.PanelSettings[key].extend(aa) + self.PanelSettings[key][start_entry+idx] = data_setting + #log.debug(f"[_extract_42_data] Extended it to fit {self.PanelSettings[key][start_entry+idx]}") + #log.debug(f"[_extract_42_data] it is now {self.PanelSettings[key]}") + + log.debug(f"[_extract_42_data] before {s}") + log.debug(f"[_extract_42_data] after {self.PanelSettings[key]}") + + elif no_of_entries == d.datacount: + processed_data = True + self.updatePanelSetting(key = key, length = ch.length, datasize = ch.datasize, data = data[14:], display = d.display, msg = d.msg) + + if dataContent == 0x000F and data_item_size == 2 and len(data_items) == 1 and isinstance(data_items[0],bytearray) and len(data_items[0]) == 2: # + processed_data = True + self.DownloadCode = toString(data_items[0]) + if OBFUS: + log.debug(f"[_extract_42_data] Got Download Code") + else: + log.debug(f"[_extract_42_data] Download Code : {self.DownloadCode}") + + elif dataContent == 0x003C and data_item_size == 15 and len(data_items) == 1 and isinstance(data_items[0],bytearray) and len(data_items[0]) == 15: # + processed_data = True + if not self.pmGotPanelDetails: + data_setting = settings_42_data_type_formatter(datatype, data_items[0], byte_size=byte_size) + log.debug(f"[_extract_42_data] Panel data_items {data_setting}") + name = data_setting.replace("-"," ") + log.debug(f"[_extract_42_data] Panel Name {name}. Not got panel details so trying to reconcile:") + for p,v in pmPanelType.items(): + log.debug(f"[_extract_42_data] Checking: {p} {v}") + if name == v: + log.debug(f"[_extract_42_data] Fount it: {v}") + self.ModelType = 0xDA7E # No idea what model type it is so just set it to a valid number, DAVE + if not self._setDataFromPanelType(p): + log.debug(f"[_extract_42_data] Panel Type {data[5]} Unknown") + else: + log.debug(f"[_extract_42_data] PanelType={self.PanelType} : {self.PanelModel} , Model={hexify(self.ModelType)} Powermaster {self.PowerMaster}") + self.pmGotPanelDetails = True + break + else: + log.debug("[_extract_42_data] Not Processed as already got Panel Details") + + elif dataContent == 0x0030 and data_item_size == 1 and len(data_items) == 1 and isinstance(data_items[0],bytearray) and len(data_items[0]) == 1: # + processed_data = True + log.debug(f"[_extract_42_data] processing 0x0030 {type(data_items[0][0])} {data_items[0][0]=}") + if data_items[0][0] == 0: + log.debug(f"[_extract_42_data] Seems to indicate that partitions are disabled in the panel") + else: + log.debug(f"[_extract_42_data] Seems to indicate that partitions are enabled in the panel {data_items[0][0]}") + self.partitionsEnabled = data_items[0][0] != 0 + + if not processed_data and data_items: for data_item in data_items: - #log.debug(f"[_extract_42_data] 42 data_item = {toString(data_item)}") + log.debug(f"[_extract_42_data] 42 data_item = {toString(data_item)}") # Convert to correct data type - data_setting = settings_42_data_type_formatter(data_type, data_item, byte_size=byte_size) - decoded_format.append(data_setting) + data_setting = settings_42_data_type_formatter(datatype, data_item, byte_size=byte_size) # if not OBFUS: log.debug(f"[_extract_42_data] formatted = {data_setting}") - if len(decoded_format) == 1: - return setting, decoded_format[0] - return setting, decoded_format def processChunk(self, ch : chunky): @@ -4982,8 +5151,9 @@ def processChunk(self, ch : chunky): self._updateAllSensors() case ("PANEL_SETTINGS_42", _ , _ , _ ): + log.debug(f"[handle_msgtypeB0] Got PANEL_SETTINGS_42 {ch}") if experimental: - setting, decoded_format = self._extract_42_data(ch.data) + self._extract_42_data(ch) self._updateAllSensors() case ("ASK_ME_1", BYTES, IndexName.MIXED, _ ): @@ -5185,14 +5355,18 @@ def chunkme(t, s, data) -> list: k = pmPanelSettingCodes[key].PMasterB0Mess if k is not None and k in pmSendMsgB0 and pmSendMsgB0[k].data == subType and pmPanelSettingCodes[key].PMasterB0Index == chunk.index: #log.debug(f" {subType=} {key=} replacing {self.PanelSettings[key]} with {toString(chunk.data)}") - self.updatePanelSetting(key, chunk, True, f"{subType=}") + self.updatePanelSetting(key = key, length = chunk.length, datasize = chunk.datasize, data = chunk.data, display = True, msg = f"{subType=}") break self.processChunk(chunk) - elif msgInfo.data == "INVALID_COMMAND": # - log.debug(f"[handle_msgtypeB0] INVALID_COMMAND B0 sent to the panel:") + elif subType == 0x06: # msgInfo.data == "INVALID_COMMAND": # + log.debug(f"[handle_msgtypeB0] The Panel Indicates an INVALID_COMMAND B0 sent to the panel:") if msgLen % 2 == 0: # msgLen is an even number for i in range(0, msgLen, 2): - log.debug(f"[handle_msgtypeB0] The Invalid Command is {hexify(data[3+i]):0>2} {hexify(data[4+i]):0>2}") + log.debug(f"[handle_msgtypeB0] The Panel Indicates {hexify(data[3+i]):0>2} {hexify(data[4+i]):0>2}") + command = data[3+i] + message = data[4+i] + if command == 0x0d: # I think this is "retry later" instruction from the panel (and if it isn't then we can still ask for the message again) + self.B0_Message_Wanted.add(message) else: # Process the messages that we know about and are not chunked log.debug(f"[handle_msgtypeB0] Message {msgInfo.data} known about but not chunky and not currently processed") @@ -5439,7 +5613,7 @@ def getPanelStatusDict(self, partition : int | None = None, include_extended_sta if partition is None or partition == 1: b = { "Protocol Version" : PLUGIN_VERSION, - "mode" : self.PanelMode.name.lower() } + "emulationmode" : str(self.PanelMode.name.lower()) } self.merge(a,b) f = self.getPanelFixedDict() diff --git a/custom_components/visonic/sensor.py b/custom_components/visonic/sensor.py index c8f930b..2a79034 100644 --- a/custom_components/visonic/sensor.py +++ b/custom_components/visonic/sensor.py @@ -162,11 +162,8 @@ def update(self): elif data is not None: self._device_state_attributes = data - if "lastevent" in self._device_state_attributes and len(self._device_state_attributes["lastevent"]) > 2: - pos = self._device_state_attributes["lastevent"].find('/') - #_LOGGER.debug(f"[sensor] {pos=}") - if pos > 2: - self._last_triggered = self._device_state_attributes["lastevent"][0:pos] + if "lasteventname" in self._device_state_attributes and len(self._device_state_attributes["lasteventname"]) > 2: + self._last_triggered = self._device_state_attributes["lasteventname"] @property def state(self): diff --git a/custom_components/visonic/services.yaml b/custom_components/visonic/services.yaml index d2dc0c4..e0d4c9e 100644 --- a/custom_components/visonic/services.yaml +++ b/custom_components/visonic/services.yaml @@ -9,7 +9,7 @@ alarm_panel_eventlog: domain: alarm_control_panel code: example: "1234" - default: "0000" + default: "" selector: text: @@ -44,7 +44,7 @@ alarm_panel_command: code: required: false example: "1234" - default: "0000" + default: "" selector: text: @@ -100,7 +100,7 @@ alarm_sensor_bypass: code: required: false example: "1234" - default: "0000" + default: "" selector: text: diff --git a/custom_components/visonic/translations/en.json b/custom_components/visonic/translations/en.json index f64e2b5..448898e 100644 --- a/custom_components/visonic/translations/en.json +++ b/custom_components/visonic/translations/en.json @@ -379,6 +379,9 @@ "device_name": { "name": "Device Name" }, + "zone_name_panel": { + "name": "Zone Name (Panel)" + }, "zone_name": { "name": "Zone Name", "state": { @@ -580,7 +583,8 @@ "false": "Not required" } }, - "mode": { + "emulationmode": { + "name": "Emulation Mode", "state": { "unknown": "Unknown", "problem": "Problem", @@ -595,6 +599,7 @@ } }, "state": { + "name": "Panel State", "state": { "unknown": "Unknown", "disarmed": "Disarmed", diff --git a/custom_components/visonic/translations/fr.json b/custom_components/visonic/translations/fr.json index df9d7d2..bd2ac89 100644 --- a/custom_components/visonic/translations/fr.json +++ b/custom_components/visonic/translations/fr.json @@ -379,6 +379,9 @@ "device_name": { "name": "Nom Appareil" }, + "zone_name_panel": { + "name": "Nom Zone (Panel)" + }, "zone_name": { "name": "Nom Zone", "state": { @@ -580,7 +583,8 @@ "false": "Non requis" } }, - "mode": { + "emulationmode": { + "name": "Emulation Mode", "state": { "unknown": "Inconnu", "problem": "Probl\u00e8me", @@ -595,6 +599,7 @@ } }, "state": { + "name": "Panel State", "state": { "unknown": "Inconnu", "disarmed": "D\u00e9sarm\u00e9", diff --git a/custom_components/visonic/translations/it.json b/custom_components/visonic/translations/it.json index f4f2215..03e82ec 100644 --- a/custom_components/visonic/translations/it.json +++ b/custom_components/visonic/translations/it.json @@ -379,6 +379,9 @@ "device_name": { "name": "Device Name" }, + "zone_name_panel": { + "name": "Zone Name (Panel)" + }, "zone_name": { "name": "Zone Name", "state": { @@ -580,7 +583,8 @@ "false": "Not required" } }, - "mode": { + "emulationmode": { + "name": "Emulation Mode", "state": { "unknown": "Unknown", "problem": "Problem", @@ -595,6 +599,7 @@ } }, "state": { + "name": "Panel State", "state": { "unknown": "Unknown", "disarmed": "Disarmed", diff --git a/custom_components/visonic/translations/nl.json b/custom_components/visonic/translations/nl.json index cc214ff..c56d18b 100644 --- a/custom_components/visonic/translations/nl.json +++ b/custom_components/visonic/translations/nl.json @@ -379,6 +379,9 @@ "device_name": { "name": "Apparaat Naam" }, + "zone_name_panel": { + "name": "Zone Naam (Paneel)" + }, "zone_name": { "name": "Zone Naam", "state": { @@ -580,7 +583,8 @@ "false": "Niet verplicht" } }, - "mode": { + "emulationmode": { + "name": "Emulatiemodus", "state": { "unknown": "Onbekend", "problem": "Probleem", @@ -595,6 +599,7 @@ } }, "state": { + "name": "Panel Status", "state": { "unknown": "Onbekend", "disarmed": "Uitgeschakeld",