From 20ef83ff0cbe66cf34ac88fe563c3dd2f6a31097 Mon Sep 17 00:00:00 2001 From: Alex Brasetvik Date: Sun, 11 Jun 2023 15:16:10 +0200 Subject: [PATCH 1/6] Rename messages to constants --- tests/test_light.py | 2 +- xcomfort/bridge.py | 2 +- xcomfort/connection.py | 2 +- xcomfort/{messages.py => constants.py} | 0 xcomfort/devices.py | 2 +- xcomfort/room.py | 2 +- 6 files changed, 5 insertions(+), 5 deletions(-) rename xcomfort/{messages.py => constants.py} (100%) diff --git a/tests/test_light.py b/tests/test_light.py index 123308d..9a1d53f 100644 --- a/tests/test_light.py +++ b/tests/test_light.py @@ -3,7 +3,7 @@ import json from xcomfort.bridge import Bridge from xcomfort.devices import (Light, LightState) -from xcomfort.messages import Messages +from xcomfort.constants import Messages class MockBridge(Bridge): diff --git a/xcomfort/bridge.py b/xcomfort/bridge.py index bd6875e..1f3a440 100644 --- a/xcomfort/bridge.py +++ b/xcomfort/bridge.py @@ -7,7 +7,7 @@ import rx.operators as ops from enum import Enum from .connection import SecureBridgeConnection, setup_secure_connection -from .messages import Messages +from .constants import Messages from .devices import (BridgeDevice, Light, RcTouch, Heater, Shade) from .room import Room, RoomState, RctMode, RctState, RctModeRange from .comp import Comp, CompState diff --git a/xcomfort/connection.py b/xcomfort/connection.py index 5d87c5c..4e4fbd1 100644 --- a/xcomfort/connection.py +++ b/xcomfort/connection.py @@ -6,7 +6,7 @@ import time import rx from enum import IntEnum -from .messages import Messages +from .constants import Messages from Crypto.Hash import SHA256 from Crypto.PublicKey import RSA from Crypto.Cipher import PKCS1_OAEP, PKCS1_v1_5, AES diff --git a/xcomfort/messages.py b/xcomfort/constants.py similarity index 100% rename from xcomfort/messages.py rename to xcomfort/constants.py diff --git a/xcomfort/devices.py b/xcomfort/devices.py index fdbc7d2..5e55be5 100644 --- a/xcomfort/devices.py +++ b/xcomfort/devices.py @@ -1,6 +1,6 @@ from contextlib import nullcontext import rx -from .messages import Messages, ShadeOperationState +from .constants import Messages, ShadeOperationState class DeviceState: def __init__(self, payload): diff --git a/xcomfort/room.py b/xcomfort/room.py index fb78076..094e16b 100644 --- a/xcomfort/room.py +++ b/xcomfort/room.py @@ -7,7 +7,7 @@ import rx.operators as ops from enum import Enum from .connection import SecureBridgeConnection, setup_secure_connection -from .messages import Messages +from .constants import Messages from .devices import (BridgeDevice, Light, RcTouch, Heater, Shade) class RctMode(Enum): From b9f11f33e8ac795a69bb954dda56df39d8e1edbc Mon Sep 17 00:00:00 2001 From: Alex Brasetvik Date: Thu, 2 May 2024 23:54:36 +0200 Subject: [PATCH 2/6] Add and use some new constants --- xcomfort/bridge.py | 11 +++++------ xcomfort/constants.py | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/xcomfort/bridge.py b/xcomfort/bridge.py index 1f3a440..704d3b9 100644 --- a/xcomfort/bridge.py +++ b/xcomfort/bridge.py @@ -7,7 +7,7 @@ import rx.operators as ops from enum import Enum from .connection import SecureBridgeConnection, setup_secure_connection -from .constants import Messages +from .constants import DeviceTypes, Messages from .devices import (BridgeDevice, Light, RcTouch, Heater, Shade) from .room import Room, RoomState, RctMode, RctState, RctModeRange from .comp import Comp, CompState @@ -130,18 +130,17 @@ def _create_device_from_payload(self, payload): name = payload['name'] dev_type = payload["devType"] comp_id = payload["compId"] - - if dev_type == 100 or dev_type == 101: + if dev_type in (DeviceTypes.ACTUATOR_SWITCH, DeviceTypes.ACTUATOR_DIMM): dimmable = payload['dimmable'] return Light(self, device_id, name, dimmable) - if dev_type == 102: + elif dev_type == DeviceTypes.SHADING_ACTUATOR: return Shade(self, device_id, name, comp_id, payload) - if dev_type == 440: + elif dev_type == DeviceTypes.HEATING_ACTUATOR: return Heater(self, device_id, name, comp_id) - if dev_type == 450: + elif dev_type == DeviceTypes.RC_TOUCH: return RcTouch(self, device_id, name, comp_id) return BridgeDevice(self, device_id, name) diff --git a/xcomfort/constants.py b/xcomfort/constants.py index b03c38b..64d366a 100644 --- a/xcomfort/constants.py +++ b/xcomfort/constants.py @@ -110,3 +110,42 @@ class ShadeOperationState(IntEnum): LOCK = 11 UNLOCK = 12 QUIT = 13 + + +class DeviceTypes(IntEnum): + ACTUATOR_SWITCH = 100 + ACTUATOR_DIMM = 101 + SHADING_ACTUATOR = 102 + SWITCH = 202 + ROCKER = 220 + TEMP_SENSOR = 410 + HEATING_ACTUATOR = 440 + HEATING_VALVE = 441 + HEATING_WATER_VALVE = 442 + RC_TOUCH = 450 + TEMP_HUMIDITY_SENSOR = 451 + WATER_GUARD = 497 + WATER_SENSOR = 499 + + +class HeatingTypes(IntEnum): + ELECTRIC_FLOOR_FOIL = 1 + ELECTRIC_FLOOR_CABLE = 2 + WATER_FLOOR = 3 + ELECTRIC_RADIATOR = 4 + ELECTRIC_INFRARED = 5 + WATER_RADIATOR = 6 + + +class ComponentTypes(IntEnum): + PUSH_BUTTON_1 = 1 + PUSH_BUTTON_2 = 2 + MULTI_HEATING_ACTUATOR = 71 + LIGHT_SWITCH_ACTUATOR = 74 + DOOR_WINDOW_SENSOR = 76 + DIMMING_ACTUATOR = 77 + RC_TOUCH = 78 + BRIDGE = 83 + WATER_GUARD = 84 + WATER_SENSOR = 85 + SHADING_ACTUATOR = 86 From 2a1c51c10fc7e6b403b16e832287103b78160d5e Mon Sep 17 00:00:00 2001 From: Alex Brasetvik Date: Thu, 2 May 2024 23:57:00 +0200 Subject: [PATCH 3/6] Do not load loads as lights --- xcomfort/bridge.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/xcomfort/bridge.py b/xcomfort/bridge.py index 704d3b9..6223ef8 100644 --- a/xcomfort/bridge.py +++ b/xcomfort/bridge.py @@ -131,8 +131,11 @@ def _create_device_from_payload(self, payload): dev_type = payload["devType"] comp_id = payload["compId"] if dev_type in (DeviceTypes.ACTUATOR_SWITCH, DeviceTypes.ACTUATOR_DIMM): - dimmable = payload['dimmable'] - return Light(self, device_id, name, dimmable) + if payload.get("usage") == 0: + # If usage = 1 then it's configured as a "load", + # and not as a light. + dimmable = payload["dimmable"] + return Light(self, device_id, name, dimmable) elif dev_type == DeviceTypes.SHADING_ACTUATOR: return Shade(self, device_id, name, comp_id, payload) From 3aec68151b49b355267b41a49a9e757c7c6c0d7b Mon Sep 17 00:00:00 2001 From: Alex Brasetvik Date: Fri, 3 May 2024 00:13:30 +0200 Subject: [PATCH 4/6] Remove unused imports --- xcomfort/bridge.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/xcomfort/bridge.py b/xcomfort/bridge.py index 6223ef8..6aca77c 100644 --- a/xcomfort/bridge.py +++ b/xcomfort/bridge.py @@ -1,17 +1,12 @@ -from unicodedata import numeric import aiohttp import asyncio -import string -import time -import rx -import rx.operators as ops from enum import Enum from .connection import SecureBridgeConnection, setup_secure_connection from .constants import DeviceTypes, Messages from .devices import (BridgeDevice, Light, RcTouch, Heater, Shade) -from .room import Room, RoomState, RctMode, RctState, RctModeRange -from .comp import Comp, CompState - +# Some HA code relies on bridge having imported these: +from .room import Room, RoomState, RctMode, RctState, RctModeRange # noqa +from .comp import Comp, CompState # noqa class State(Enum): Uninitialized = 0 From cd25a2e9497f97b2957a7a9c9e21623844475be8 Mon Sep 17 00:00:00 2001 From: Alex Brasetvik Date: Fri, 3 May 2024 00:04:04 +0200 Subject: [PATCH 5/6] Add Door- and WindowSensor --- xcomfort/bridge.py | 12 ++++++++++-- xcomfort/devices.py | 28 +++++++++++++++++++++++++++- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/xcomfort/bridge.py b/xcomfort/bridge.py index 6aca77c..811761f 100644 --- a/xcomfort/bridge.py +++ b/xcomfort/bridge.py @@ -1,9 +1,10 @@ +from typing import Optional import aiohttp import asyncio from enum import Enum from .connection import SecureBridgeConnection, setup_secure_connection -from .constants import DeviceTypes, Messages -from .devices import (BridgeDevice, Light, RcTouch, Heater, Shade) +from .constants import ComponentTypes, DeviceTypes, Messages +from .devices import (BridgeDevice, DoorSensor, Light, RcTouch, Heater, Shade, WindowSensor) # Some HA code relies on bridge having imported these: from .room import Room, RoomState, RctMode, RctState, RctModeRange # noqa from .comp import Comp, CompState # noqa @@ -141,6 +142,13 @@ def _create_device_from_payload(self, payload): elif dev_type == DeviceTypes.RC_TOUCH: return RcTouch(self, device_id, name, comp_id) + elif dev_type == DeviceTypes.SWITCH: + component: Optional[Comp] = self._comps.get(comp_id) + if component and component.comp_type == ComponentTypes.DOOR_WINDOW_SENSOR: + if component.payload.get("mode") == "1310": + return DoorSensor(self, device_id, name, comp_id, payload) + return WindowSensor(self, device_id, name, comp_id, payload) + return BridgeDevice(self, device_id, name) def _create_room_from_payload(self, payload): diff --git a/xcomfort/devices.py b/xcomfort/devices.py index 5e55be5..5311a6e 100644 --- a/xcomfort/devices.py +++ b/xcomfort/devices.py @@ -1,4 +1,5 @@ -from contextlib import nullcontext +from typing import Optional + import rx from .constants import Messages, ShadeOperationState @@ -200,3 +201,28 @@ async def move_to_position(self, position: int): def __str__(self) -> str: return f"" + + +class DoorWindowSensor(BridgeDevice): + def __init__(self, bridge, device_id, name, comp_id, payload): + BridgeDevice.__init__(self, bridge, device_id, name) + + self.comp_id = comp_id + self.payload = payload + self.is_open: Optional[bool] = None + self.is_closed: Optional[bool] = None + + def handle_state(self, payload): + if (state := payload.get("curstate")) is not None: + self.is_closed = state == 1 + self.is_open = not self.is_closed + + self.state.on_next(self.is_closed) + + +class WindowSensor(DoorWindowSensor): + pass + + +class DoorSensor(DoorWindowSensor): + pass From b53b65667df4916c374dc40af9a727cf30551f9b Mon Sep 17 00:00:00 2001 From: Alex Brasetvik Date: Fri, 3 May 2024 00:18:40 +0200 Subject: [PATCH 6/6] Add Rocker devices (switches) These only seem able to report their state, not to be triggerable via the API --- xcomfort/bridge.py | 7 ++++++- xcomfort/devices.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/xcomfort/bridge.py b/xcomfort/bridge.py index 811761f..c883ebd 100644 --- a/xcomfort/bridge.py +++ b/xcomfort/bridge.py @@ -4,7 +4,7 @@ from enum import Enum from .connection import SecureBridgeConnection, setup_secure_connection from .constants import ComponentTypes, DeviceTypes, Messages -from .devices import (BridgeDevice, DoorSensor, Light, RcTouch, Heater, Shade, WindowSensor) +from .devices import (BridgeDevice, DoorSensor, Light, RcTouch, Heater, Rocker, Shade, WindowSensor) # Some HA code relies on bridge having imported these: from .room import Room, RoomState, RctMode, RctState, RctModeRange # noqa from .comp import Comp, CompState # noqa @@ -149,6 +149,11 @@ def _create_device_from_payload(self, payload): return DoorSensor(self, device_id, name, comp_id, payload) return WindowSensor(self, device_id, name, comp_id, payload) + elif dev_type == DeviceTypes.ROCKER: + # What Xcomfort calls a rocker HomeAssistant (and most humans) call a + # switch + return Rocker(self, device_id, name, comp_id, payload) + return BridgeDevice(self, device_id, name) def _create_room_from_payload(self, payload): diff --git a/xcomfort/devices.py b/xcomfort/devices.py index 5311a6e..fed3212 100644 --- a/xcomfort/devices.py +++ b/xcomfort/devices.py @@ -226,3 +226,33 @@ class WindowSensor(DoorWindowSensor): class DoorSensor(DoorWindowSensor): pass + + +class Rocker(BridgeDevice): + def __init__(self, bridge, device_id, name, comp_id, payload): + BridgeDevice.__init__(self, bridge, device_id, name) + self.comp_id = comp_id + self.payload = payload + self.is_on: bool | None = None + if "curstate" in payload: + self.is_on = bool(payload["curstate"]) + + @property + def name_with_controlled(self) -> str: + """Name of Rocker, with the names of controlled devices in parens.""" + names_of_controlled: set[str] = set() + for device_id in self.payload.get("controlId", []): + device = self.bridge._devices.get(device_id) + if device: + names_of_controlled.add(device.name) + + return f"{self.name} ({', '.join(sorted(names_of_controlled))})" + + def handle_state(self, payload, broadcast: bool = True) -> None: + self.payload.update(payload) + self.is_on = bool(payload["curstate"]) + if broadcast: + self.state.on_next(self.is_on) + + def __str__(self): + return f'Rocker({self.device_id}, "{self.name}", is_on: {self.is_on} payload: {self.payload})'