From cc65718ca948ad268a3927a46e9ca27c2bfc3b20 Mon Sep 17 00:00:00 2001 From: Wheemer Date: Fri, 29 May 2026 22:13:31 -0230 Subject: [PATCH 1/4] Expand device map and boolean states --- xsense/entity_map.py | 22 +++++++++++++++++++++- xsense/mapping.py | 19 ++++++++++++++----- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/xsense/entity_map.py b/xsense/entity_map.py index 089fdec..f99213c 100644 --- a/xsense/entity_map.py +++ b/xsense/entity_map.py @@ -203,6 +203,26 @@ def SATestAction(shadow = 'appSelfTest'): 'XS03-iWX': { # Smoke RF 'type': EntityType.SMOKE, + 'actions': [ + TestAction(), + ], + }, + 'XS03-WX': { + 'type': EntityType.SMOKE, + }, + 'XS0D-MR': { + 'type': EntityType.SMOKE, + }, + 'XS0D-MR61': { + 'type': EntityType.SMOKE, + }, + 'XC0C-MR': { + 'type': EntityType.CO, + }, + 'XP0A-iR': { + 'type': EntityType.COMBI, + }, + 'XPOA-IR': { + 'type': EntityType.COMBI, }, - 'XS03-WX': {} } diff --git a/xsense/mapping.py b/xsense/mapping.py index 13fada3..c80074a 100644 --- a/xsense/mapping.py +++ b/xsense/mapping.py @@ -1,5 +1,12 @@ import typing + +def map_bool(value): + if isinstance(value, bool): + return value + return value in (1, '1') + + property_mapper = { '*': { 'wifiRssi': 'wifiRSSI' @@ -42,13 +49,15 @@ type_mapping = { 'batInfo': int, 'rfLevel': int, - 'alarmStatus': lambda x: x == '1', - 'alarmEnabled': lambda x: x == '1', - 'muteStatus': lambda x: x == '1', - 'continuedAlarm': lambda x: x == '1', + 'alarmStatus': map_bool, + 'alarmEnabled': map_bool, + 'muteStatus': map_bool, + 'continuedAlarm': map_bool, 'coPpm': int, 'coLevel': int, - 'isLifeEnd': lambda x: x == '1', + 'isLifeEnd': map_bool, + 'isOpen': map_bool, + 'activate': map_bool, 'temperature': float, 'humidity': float } From e278fd9e04842495e3cb4f99ee23cd0d8a4c9267 Mon Sep 17 00:00:00 2001 From: Wheemer Date: Fri, 29 May 2026 22:14:17 -0230 Subject: [PATCH 2/4] Normalize entity state parsing --- xsense/entity.py | 4 ++-- xsense/mapping.py | 6 ++++-- xsense/mqtt_helper.py | 2 +- xsense/station.py | 5 +++-- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/xsense/entity.py b/xsense/entity.py index c8abf18..be4f84e 100644 --- a/xsense/entity.py +++ b/xsense/entity.py @@ -1,5 +1,5 @@ from xsense.entity_map import entities -from xsense.mapping import map_values +from xsense.mapping import map_bool, map_values class Entity: @@ -21,7 +21,7 @@ def __init__( def set_data(self, values: dict): data = values.copy() if 'online' in values: - self.online = values.pop('online') != '0' + self.online = map_bool(values.pop('online')) if values.get('onlineTime'): self.online = True data |= data.pop('status', {}) diff --git a/xsense/mapping.py b/xsense/mapping.py index c80074a..fb9c4a6 100644 --- a/xsense/mapping.py +++ b/xsense/mapping.py @@ -68,8 +68,10 @@ def map_type(k: str, value: typing.Any): def map_values(device_type: str, data: typing.Dict): - mapping = property_mapper[device_type] if device_type in property_mapper else {} - mapping.update(property_mapper.get('*', {})) + mapping = { + **property_mapper.get('*', {}), + **property_mapper.get(device_type, {}), + } return { mapping.get(k, k): map_type(mapping.get(k, k), v) diff --git a/xsense/mqtt_helper.py b/xsense/mqtt_helper.py index 514ac19..81059b9 100644 --- a/xsense/mqtt_helper.py +++ b/xsense/mqtt_helper.py @@ -33,7 +33,7 @@ def _get_path(self): signed = self.signer.presign_url(f'wss://{self.house.mqtt_server}/mqtt', self.house.mqtt_region) url_parts = signed.split("/") self._mqtt_path = "/" + "/".join(url_parts[3:]) - _sig_age = datetime.now() + self._sig_age = datetime.now() return self._mqtt_path diff --git a/xsense/station.py b/xsense/station.py index 8de3842..0f324f7 100644 --- a/xsense/station.py +++ b/xsense/station.py @@ -2,6 +2,7 @@ from xsense.device import Device from xsense.entity import Entity +from xsense.mapping import map_bool class Station(Entity): @@ -19,7 +20,7 @@ def __init__( self.entity_id = kwargs.get('stationId') self.name = kwargs.get('stationName') self.sn = kwargs.get('stationSn') - self.online = kwargs.get('onLine', True) + self.online = map_bool(kwargs.get('onLine', True)) self.type = kwargs.get('category') self.has_alarm = False @@ -30,7 +31,7 @@ def set_devices(self, data): self.device_order = data.get('deviceSort') result = {} result_sn = {} - for i in data.get('devices'): + for i in data.get('devices', []): d = Device( self, **i From 396a9bcbb5c7b01559cce1d964c544dece49cf2a Mon Sep 17 00:00:00 2001 From: Wheemer Date: Fri, 29 May 2026 23:03:20 -0230 Subject: [PATCH 3/4] Add APK device coverage --- xsense/entity_map.py | 125 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 114 insertions(+), 11 deletions(-) diff --git a/xsense/entity_map.py b/xsense/entity_map.py index f99213c..b0ba655 100644 --- a/xsense/entity_map.py +++ b/xsense/entity_map.py @@ -6,13 +6,19 @@ class EntityType(Enum): ALARM = 'alarm' BASE = "base" BASESTATION = 'station' + CAMERA = 'camera' CO = "co" COMBI = 'combi' DOOR = 'door' HEAT = 'heat' KEYPAD = 'keypad' + LIGHT = 'light' + LISTENER = 'listener' MAILBOX = 'mailbox' MOTION = 'motion' + RADON = 'radon' + REMOTE = 'remote' + SMARTDROP = 'smartdrop' SMOKE = "smoke" TEMPERATURE = "temperature" WATER = "water" @@ -56,8 +62,12 @@ def SATestAction(shadow = 'appSelfTest'): entities = { - 'SAL51': {}, # listener - 'SAL100': {}, # listener + 'SAL51': { + 'type': EntityType.LISTENER, + }, + 'SAL100': { + 'type': EntityType.LISTENER, + }, 'SBS10': { 'type': EntityType.BASESTATION, }, @@ -65,8 +75,18 @@ def SATestAction(shadow = 'appSelfTest'): 'type': EntityType.BASESTATION, 'identifier': lambda entity: f'SBS50{entity.sn}', }, - # SSC0A - Camera - # SSC0B + 'SSC0A': { + 'type': EntityType.CAMERA, + }, + 'SSC0B': { + 'type': EntityType.CAMERA, + }, + 'SC01-MN': { + 'type': EntityType.COMBI, + }, + 'SC01-MR': { + 'type': EntityType.COMBI, + }, 'SC06-WX': { 'identifier': lambda entity: f'SC06-WX-{entity.sn}', 'type': EntityType.COMBI, @@ -87,12 +107,33 @@ def SATestAction(shadow = 'appSelfTest'): MuteAction('1') ] }, - # 'SDA51': {}, - Driveway alarm + 'SD11-MR': { + 'type': EntityType.SMOKE, + }, + 'SD19-MN': { + 'type': EntityType.SMOKE, + }, + 'SD19-MR': { + 'type': EntityType.SMOKE, + }, + 'SDA51': { + 'type': EntityType.ALARM, + }, 'SDS0A': { 'type': EntityType.DOOR, }, - # 'SES01': {}, - Door sensor - # 'SKF01': {}, - Remote Control + 'SES01': { + 'type': EntityType.DOOR, + }, + 'SKF01': { + 'type': EntityType.REMOTE, + }, + 'SK0Z-3S': { + 'type': EntityType.SMOKE, + }, + 'SKP01': { + 'type': EntityType.KEYPAD, + }, 'SKP0A': { 'type': EntityType.KEYPAD, }, @@ -110,9 +151,18 @@ def SATestAction(shadow = 'appSelfTest'): 'SMS0A': { 'type': EntityType.MOTION, }, - # 'SSD01': {}, - # 'SPL51': {}, - # 'SSL51': {}, + 'SMS01': { + 'type': EntityType.MOTION, + }, + 'SPL51': { + 'type': EntityType.LIGHT, + }, + 'SSD01': { + 'type': EntityType.SMARTDROP, + }, + 'SSL51': { + 'type': EntityType.LIGHT, + }, 'STH0A': { 'type': EntityType.TEMPERATURE, 'actions': [ @@ -127,6 +177,9 @@ def SATestAction(shadow = 'appSelfTest'): MuteAction('1', 'extendMute') ], }, + 'STH0C': { + 'type': EntityType.TEMPERATURE, + }, 'STH51': { 'type': EntityType.TEMPERATURE, 'actions': [ @@ -134,7 +187,15 @@ def SATestAction(shadow = 'appSelfTest'): MuteAction('1', 'extendMute') ], }, - # 'SWL51': {}, + 'SWL51': { + 'type': EntityType.LIGHT, + }, + 'SWS0A': { + 'type': EntityType.WATER, + }, + 'SWS0B': { + 'type': EntityType.WATER, + }, 'SWS51': { 'type': EntityType.WATER, 'actions': [ @@ -142,9 +203,24 @@ def SATestAction(shadow = 'appSelfTest'): MuteAction(shadow='appWater', topic='2nd_appwater', extra={'silencetime': '', 'setType': '0'}) ], }, + 'CB0Z-3S': { + 'type': EntityType.COMBI, + }, + 'LP/N-SA-0B': { + 'type': EntityType.SMOKE, + }, + 'LP/N-SCA-0A': { + 'type': EntityType.COMBI, + }, + 'XC0C-iA': { + 'type': EntityType.CO, + }, 'XC0C-iR': { 'type': EntityType.CO, }, + 'XC0M-iR': { + 'type': EntityType.CO, + }, 'XC01-M': { # CO RF 'type': EntityType.CO, @@ -173,6 +249,9 @@ def SATestAction(shadow = 'appSelfTest'): FireDrillAction() ] }, + 'XR0A-iR': { + 'type': EntityType.RADON, + }, 'XP02S-MR': { 'type': EntityType.SMOKE, 'actions': [ @@ -225,4 +304,28 @@ def SATestAction(shadow = 'appSelfTest'): 'XPOA-IR': { 'type': EntityType.COMBI, }, + 'XP0H-MR': { + 'type': EntityType.COMBI, + }, + 'XP0H-iR': { + 'type': EntityType.COMBI, + }, + 'XP0J-iA': { + 'type': EntityType.COMBI, + }, + 'XP0P-MR': { + 'type': EntityType.COMBI, + }, + 'XS0B-iR': { + 'type': EntityType.SMOKE, + }, + 'XS0E-iR': { + 'type': EntityType.SMOKE, + }, + 'XS0F-PMA': { + 'type': EntityType.SMOKE, + }, + 'XS0R-iA': { + 'type': EntityType.SMOKE, + }, } From 484f823bdcc0bdb9aeb0b33af7ee2aa1ba7fb7fb Mon Sep 17 00:00:00 2001 From: Wheemer Date: Fri, 29 May 2026 22:36:47 -0230 Subject: [PATCH 4/4] Add device-specific mute actions --- xsense/entity_map.py | 75 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 64 insertions(+), 11 deletions(-) diff --git a/xsense/entity_map.py b/xsense/entity_map.py index b0ba655..d005984 100644 --- a/xsense/entity_map.py +++ b/xsense/entity_map.py @@ -1,5 +1,5 @@ from enum import Enum -from typing import Callable, Dict +from typing import Callable, Dict, Optional, Union class EntityType(Enum): @@ -24,7 +24,12 @@ class EntityType(Enum): WATER = "water" -def MuteAction(shadow: str = 'appMute', topic: str|None|Callable = '2nd_appmute', extra: Dict|None=None): +def MuteAction( + shadow: str = 'appMute', + topic: Union[str, Callable, None] = '2nd_appmute', + extra: Optional[Dict]=None, + mute_type: Optional[str]=None +): data = { 'action': 'mute', 'topic': topic, @@ -32,6 +37,8 @@ def MuteAction(shadow: str = 'appMute', topic: str|None|Callable = '2nd_appmute' } if extra: data['extra'] = extra + if mute_type is not None: + data.setdefault('extra', {})['muteType'] = mute_type return data @@ -48,7 +55,8 @@ def FireDrillAction(): return { 'action': 'firedrill', 'topic': '2nd_firedrill', - 'shadow': 'appFireDrill' + 'shadow': 'appFireDrill', + 'data': {'drill': '1'} } @@ -64,9 +72,15 @@ def SATestAction(shadow = 'appSelfTest'): entities = { 'SAL51': { 'type': EntityType.LISTENER, + 'actions': [ + MuteAction('appListener', mute_type='1'), + ], }, 'SAL100': { 'type': EntityType.LISTENER, + 'actions': [ + MuteAction('appListener', mute_type='1'), + ], }, 'SBS10': { 'type': EntityType.BASESTATION, @@ -86,6 +100,9 @@ def SATestAction(shadow = 'appSelfTest'): }, 'SC01-MR': { 'type': EntityType.COMBI, + 'actions': [ + MuteAction('appSc07mrMute', mute_type='1'), + ], }, 'SC06-WX': { 'identifier': lambda entity: f'SC06-WX-{entity.sn}', @@ -98,26 +115,41 @@ def SATestAction(shadow = 'appSelfTest'): 'identifier': lambda entity: f'SC07-MR-{entity.sn}', 'type': EntityType.COMBI, 'actions': [ + MuteAction('appSc07mrMute', mute_type='1'), ] }, 'SC07-WX': { 'identifier': lambda entity: f'SC07-WX-{entity.sn}', 'type': EntityType.COMBI, 'actions': [ - MuteAction('1') + MuteAction(mute_type='1') ] }, 'SD11-MR': { 'type': EntityType.SMOKE, + 'actions': [ + TestAction(), + ], }, 'SD19-MN': { 'type': EntityType.SMOKE, + 'actions': [ + TestAction(), + ], }, 'SD19-MR': { 'type': EntityType.SMOKE, }, 'SDA51': { 'type': EntityType.ALARM, + 'actions': [ + { + 'action': 'mute', + 'topic': '2nd_appdriveway', + 'shadow': 'appDriveway', + 'data': {'mute': '1'} + }, + ], }, 'SDS0A': { 'type': EntityType.DOOR, @@ -130,6 +162,9 @@ def SATestAction(shadow = 'appSelfTest'): }, 'SK0Z-3S': { 'type': EntityType.SMOKE, + 'actions': [ + TestAction(), + ], }, 'SKP01': { 'type': EntityType.KEYPAD, @@ -167,14 +202,14 @@ def SATestAction(shadow = 'appSelfTest'): 'type': EntityType.TEMPERATURE, 'actions': [ TestAction('thSelfTest'), - MuteAction('1', 'extendMute') + MuteAction('extendMute', '2nd_appmute', extra={'type': 'STH0A'}, mute_type='1') ], }, 'STH0B': { 'type': EntityType.TEMPERATURE, 'actions': [ TestAction('thSelfTest'), - MuteAction('1', 'extendMute') + MuteAction('extendMute', '2nd_appmute', extra={'type': 'STH0B'}, mute_type='1') ], }, 'STH0C': { @@ -184,7 +219,7 @@ def SATestAction(shadow = 'appSelfTest'): 'type': EntityType.TEMPERATURE, 'actions': [ TestAction('thSelfTest'), - MuteAction('1', 'extendMute') + MuteAction('extendMute', '2nd_appmute', extra={'type': 'STH51'}, mute_type='1') ], }, 'SWL51': { @@ -200,7 +235,7 @@ def SATestAction(shadow = 'appSelfTest'): 'type': EntityType.WATER, 'actions': [ TestAction('waterSelfTest'), - MuteAction(shadow='appWater', topic='2nd_appwater', extra={'silencetime': '', 'setType': '0'}) + MuteAction(shadow='appWater', topic='2nd_appwater', extra={'silenceTime': '', 'setType': '0'}) ], }, 'CB0Z-3S': { @@ -208,6 +243,9 @@ def SATestAction(shadow = 'appSelfTest'): }, 'LP/N-SA-0B': { 'type': EntityType.SMOKE, + 'actions': [ + TestAction(), + ], }, 'LP/N-SCA-0A': { 'type': EntityType.COMBI, @@ -226,26 +264,28 @@ def SATestAction(shadow = 'appSelfTest'): 'type': EntityType.CO, 'actions': [ TestAction(shadow='appCoSelfTest'), - MuteAction('1', '"appCoMute') + MuteAction('appCoMute', mute_type='1') ] }, 'XC04-WX': { 'identifier': lambda entity: f'XC04-WX-{entity.sn}', 'type': EntityType.CO, 'actions': [ - MuteAction('1') + MuteAction(mute_type='1') ] }, 'XH02-M': { 'type': EntityType.HEAT, 'actions': [ TestAction(shadow='appXh02mSelfTest'), + MuteAction('appXh02mMute', mute_type='1', extra={'userParam': 'source=1'}), ] }, 'XP0A-MR': { 'type': EntityType.COMBI, 'actions': [ TestAction(shadow='app2ndSelfTest'), + MuteAction('appXp0amrMute', mute_type='1', extra={'userParam': 'source=1'}), FireDrillAction() ] }, @@ -275,7 +315,7 @@ def SATestAction(shadow = 'appSelfTest'): 'type': EntityType.SMOKE, 'actions': [ TestAction(), - MuteAction(), + MuteAction('app2ndMute', mute_type='1'), FireDrillAction(), ], }, @@ -291,6 +331,9 @@ def SATestAction(shadow = 'appSelfTest'): }, 'XS0D-MR': { 'type': EntityType.SMOKE, + 'actions': [ + TestAction(), + ], }, 'XS0D-MR61': { 'type': EntityType.SMOKE, @@ -306,6 +349,9 @@ def SATestAction(shadow = 'appSelfTest'): }, 'XP0H-MR': { 'type': EntityType.COMBI, + 'actions': [ + MuteAction('appSc07mrMute', mute_type='1'), + ], }, 'XP0H-iR': { 'type': EntityType.COMBI, @@ -315,6 +361,9 @@ def SATestAction(shadow = 'appSelfTest'): }, 'XP0P-MR': { 'type': EntityType.COMBI, + 'actions': [ + MuteAction('appSc07mrMute', mute_type='1'), + ], }, 'XS0B-iR': { 'type': EntityType.SMOKE, @@ -324,6 +373,10 @@ def SATestAction(shadow = 'appSelfTest'): }, 'XS0F-PMA': { 'type': EntityType.SMOKE, + 'actions': [ + TestAction(), + MuteAction('app2ndMute', mute_type='1'), + ], }, 'XS0R-iA': { 'type': EntityType.SMOKE,