From c7f6149ff42640e171066a0bdee73c8c2548758c Mon Sep 17 00:00:00 2001 From: blockarchitech Date: Sat, 15 Feb 2025 09:38:18 -0700 Subject: [PATCH 1/8] feat: garage door controller. --- src/wyzeapy/services/base_service.py | 17 +++++++-- src/wyzeapy/services/garage_service.py | 52 ++++++++++++++++++++++++++ src/wyzeapy/types.py | 5 ++- 3 files changed, 70 insertions(+), 4 deletions(-) create mode 100644 src/wyzeapy/services/garage_service.py diff --git a/src/wyzeapy/services/base_service.py b/src/wyzeapy/services/base_service.py index d85a761..0c36d87 100644 --- a/src/wyzeapy/services/base_service.py +++ b/src/wyzeapy/services/base_service.py @@ -29,7 +29,7 @@ class BaseService: _devices: Optional[List[Device]] = None _last_updated_time: time = 0 # preload a value of 0 so that comparison will succeed on the first run _min_update_time = 1200 # lets let the device_params update every 20 minutes for now. This could probably reduced signicficantly. - _update_lock: asyncio.Lock = asyncio.Lock() + _update_lock: asyncio.Lock() = asyncio.Lock() _update_manager: UpdateManager = UpdateManager() _update_loop = None _updater: DeviceUpdater = None @@ -120,10 +120,22 @@ async def get_object_list(self) -> List[Device]: json=payload) check_for_errors_standard(self, response_json) - # Cache the devices so that update calls can pull more recent device_params BaseService._devices = [Device(device) for device in response_json['data']['device_list']] + garage_doors = [] + for device in self._devices: + if 'dongle_product_model' not in device.device_params: + continue + if device.device_params['dongle_product_model'] == "HL_CGDC": + garage_doors.append(Device({ + "product_type": "GarageDoor", + "product_model": "HL_CGDC", + "mac": device.mac, + "device_params": device.device_params + })) + BaseService._devices.extend(garage_doors) + return BaseService._devices async def get_updated_params(self, device_mac: str = None) -> Dict[str, Optional[Any]]: @@ -166,7 +178,6 @@ async def _get_property_list(self, device: Device) -> List[Tuple[PropertyIDs, An check_for_errors_standard(self, response_json) properties = response_json['data']['property_list'] - property_list = [] for prop in properties: try: diff --git a/src/wyzeapy/services/garage_service.py b/src/wyzeapy/services/garage_service.py new file mode 100644 index 0000000..0422b08 --- /dev/null +++ b/src/wyzeapy/services/garage_service.py @@ -0,0 +1,52 @@ +from .base_service import BaseService +from ..types import Device, DeviceTypes + +class GarageDoor(Device): + open = False + closing = False + opening = False + +class GarageService(BaseService): + async def update(self, garage: GarageDoor): + async with BaseService._update_lock: + garage.device_params = await self.get_updated_params(garage.mac) + + device_info = await self._get_garage_info(garage) + garage.raw_dict = device_info["device"] + + garage.available = garage.raw_dict.get("power_switch") == 1 + + # store the nested dict for easier reference below + property_list = await self._get_property_list(garage) + for property in property_list: + if property[0] == "P1056": + garage.open = property[1] != "0" + if garage.open and garage.opening: + garage.opening = False + if not garage.open and garage.closing: + garage.closing = False + break + + return garage + + async def get_garages(self): + if self._devices is None: + self._devices = await self.get_object_list() + + garages = [device for device in self._devices if device.type is DeviceTypes.GARAGE_DOOR] + + return [GarageDoor(device.raw_dict) for device in garages] + + async def open(self, garage: GarageDoor): + if garage.open: + return + await self._run_action(garage, "garage_door_trigger") + self.opening = True + + async def close(self, garage: GarageDoor): + if not garage.open: + return + await self._run_action(garage, "garage_door_trigger") + self.closing = True + + diff --git a/src/wyzeapy/types.py b/src/wyzeapy/types.py index a4b0198..251447d 100644 --- a/src/wyzeapy/types.py +++ b/src/wyzeapy/types.py @@ -28,6 +28,7 @@ class DeviceTypes(Enum): CHIME_SENSOR = "ChimeSensor" CONTACT_SENSOR = "ContactSensor" MOTION_SENSOR = "MotionSensor" + LEAK_SENSOR = "LeakSensor" WRIST = "Wrist" BASE_STATION = "BaseStation" SCALE = "WyzeScale" @@ -42,6 +43,8 @@ class DeviceTypes(Enum): SENSE_V2_GATEWAY = "S1Gateway" KEYPAD = "Keypad" LIGHTSTRIP = "LightStrip" + GARAGE_DOOR = "GarageDoor" + class Device: product_type: str @@ -102,7 +105,7 @@ class PropertyIDs(Enum): CONTACT_STATE = "P1301" MOTION_STATE = "P1302" CAMERA_SIREN = "P1049" - FLOOD_LIGHT = "P1056" # Also lamp socket on v3/v4 with lamp socket accessory + FLOOD_LIGHT = "P1056" # Also lamp socket on v3/v4 with lamp socket accessory. And also garage door state on v3 with garage door accessory. SUN_MATCH = "P1528" MOTION_DETECTION = "P1047" # Current Motion Detection State of the Camera MOTION_DETECTION_TOGGLE = "P1001" # This toggles Camera Motion Detection On/Off From 4b38833be059ca8f57d13ecdd88103ddeeb724bd Mon Sep 17 00:00:00 2001 From: blockarchitech Date: Sat, 15 Feb 2025 09:40:15 -0700 Subject: [PATCH 2/8] chore: fix goof --- src/wyzeapy/services/base_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wyzeapy/services/base_service.py b/src/wyzeapy/services/base_service.py index 0c36d87..6076382 100644 --- a/src/wyzeapy/services/base_service.py +++ b/src/wyzeapy/services/base_service.py @@ -29,7 +29,7 @@ class BaseService: _devices: Optional[List[Device]] = None _last_updated_time: time = 0 # preload a value of 0 so that comparison will succeed on the first run _min_update_time = 1200 # lets let the device_params update every 20 minutes for now. This could probably reduced signicficantly. - _update_lock: asyncio.Lock() = asyncio.Lock() + _update_lock: asyncio.Lock = asyncio.Lock() _update_manager: UpdateManager = UpdateManager() _update_loop = None _updater: DeviceUpdater = None From 343e7f6d8709015f10e17fda96d02d352eed4d36 Mon Sep 17 00:00:00 2001 From: blockarchitech Date: Sat, 15 Feb 2025 17:04:19 -0700 Subject: [PATCH 3/8] chore: really got to fix my signing key. --- src/wyzeapy/services/garage_service.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/wyzeapy/services/garage_service.py b/src/wyzeapy/services/garage_service.py index 0422b08..5c01448 100644 --- a/src/wyzeapy/services/garage_service.py +++ b/src/wyzeapy/services/garage_service.py @@ -1,3 +1,4 @@ +from typing import Any, Dict from .base_service import BaseService from ..types import Device, DeviceTypes @@ -11,10 +12,10 @@ async def update(self, garage: GarageDoor): async with BaseService._update_lock: garage.device_params = await self.get_updated_params(garage.mac) - device_info = await self._get_garage_info(garage) - garage.raw_dict = device_info["device"] + device_info = await self._get_device_info(garage) + garage.raw_dict = device_info - garage.available = garage.raw_dict.get("power_switch") == 1 + garage.available = True # store the nested dict for easier reference below property_list = await self._get_property_list(garage) From e1bb7049956a396e3b6bb9a88d937cee6dee8d8e Mon Sep 17 00:00:00 2001 From: blockarchitech Date: Sun, 16 Feb 2025 14:40:48 -0700 Subject: [PATCH 4/8] chore: make garage fall under the camera, like a floodlight --- src/wyzeapy/__init__.py | 2 +- src/wyzeapy/services/base_service.py | 15 +------- src/wyzeapy/services/camera_service.py | 12 +++++- src/wyzeapy/services/garage_service.py | 53 -------------------------- src/wyzeapy/types.py | 1 - 5 files changed, 13 insertions(+), 70 deletions(-) delete mode 100644 src/wyzeapy/services/garage_service.py diff --git a/src/wyzeapy/__init__.py b/src/wyzeapy/__init__.py index 2c31f16..1fd4c0d 100644 --- a/src/wyzeapy/__init__.py +++ b/src/wyzeapy/__init__.py @@ -239,7 +239,7 @@ async def lock_service(self) -> LockService: if self._lock_service is None: self._lock_service = LockService(self._auth_lib) return self._lock_service - + @property async def sensor_service(self) -> SensorService: """Returns an instance of the sensor service""" diff --git a/src/wyzeapy/services/base_service.py b/src/wyzeapy/services/base_service.py index 6076382..5b8b87b 100644 --- a/src/wyzeapy/services/base_service.py +++ b/src/wyzeapy/services/base_service.py @@ -29,7 +29,7 @@ class BaseService: _devices: Optional[List[Device]] = None _last_updated_time: time = 0 # preload a value of 0 so that comparison will succeed on the first run _min_update_time = 1200 # lets let the device_params update every 20 minutes for now. This could probably reduced signicficantly. - _update_lock: asyncio.Lock = asyncio.Lock() + _update_lock: asyncio.Lock() = asyncio.Lock() _update_manager: UpdateManager = UpdateManager() _update_loop = None _updater: DeviceUpdater = None @@ -123,19 +123,6 @@ async def get_object_list(self) -> List[Device]: # Cache the devices so that update calls can pull more recent device_params BaseService._devices = [Device(device) for device in response_json['data']['device_list']] - garage_doors = [] - for device in self._devices: - if 'dongle_product_model' not in device.device_params: - continue - if device.device_params['dongle_product_model'] == "HL_CGDC": - garage_doors.append(Device({ - "product_type": "GarageDoor", - "product_model": "HL_CGDC", - "mac": device.mac, - "device_params": device.device_params - })) - BaseService._devices.extend(garage_doors) - return BaseService._devices async def get_updated_params(self, device_mac: str = None) -> Dict[str, Optional[Any]]: diff --git a/src/wyzeapy/services/camera_service.py b/src/wyzeapy/services/camera_service.py index 2eef0cc..dc76725 100644 --- a/src/wyzeapy/services/camera_service.py +++ b/src/wyzeapy/services/camera_service.py @@ -32,6 +32,7 @@ def __init__(self, dictionary: Dict[Any, Any]): self.on: bool = True self.siren: bool = False self.floodlight: bool = False + self.garage: bool = False class CameraService(BaseService): @@ -78,6 +79,8 @@ async def update(self, camera: Camera): camera.siren = value == "1" if property is PropertyIDs.FLOOD_LIGHT: camera.floodlight = value == "1" + if property is PropertyIDs.FLOOD_LIGHT and camera.device_params["dongle_product_model"] == "HL_CGDC": + camera.garage = value == "1" # 1 = open, 2 = closed by automation or smart platform (Alexa, Google Home, Rules), 0 = closed by app if property is PropertyIDs.NOTIFICATION: camera.notify = value == "1" if property is PropertyIDs.MOTION_DETECTION: @@ -146,7 +149,14 @@ async def floodlight_off(self, camera: Camera): if (camera.product_model == "AN_RSCW"): await self._run_action_devicemgmt(camera, "spotlight", "0") # Battery cam pro integrated spotlight is controllable elif (camera.product_model in DEVICEMGMT_API_MODELS): await self._run_action_devicemgmt(camera, "floodlight", "0") # Some camera models use a diffrent api else: await self._set_property(camera, PropertyIDs.FLOOD_LIGHT.value, "2") - + + # Garage door trigger uses run action on all models + async def garage_door_open(self, camera: Camera): + await self._run_action(camera, "garage_door_trigger") + + async def garage_door_close(self, camera: Camera): + await self._run_action(camera, "garage_door_trigger") + async def turn_on_notifications(self, camera: Camera): if (camera.product_model in DEVICEMGMT_API_MODELS): await self._set_toggle(camera, DeviceMgmtToggleProps.NOTIFICATION_TOGGLE.value, "1") else: await self._set_property(camera, PropertyIDs.NOTIFICATION.value, "1") diff --git a/src/wyzeapy/services/garage_service.py b/src/wyzeapy/services/garage_service.py deleted file mode 100644 index 5c01448..0000000 --- a/src/wyzeapy/services/garage_service.py +++ /dev/null @@ -1,53 +0,0 @@ -from typing import Any, Dict -from .base_service import BaseService -from ..types import Device, DeviceTypes - -class GarageDoor(Device): - open = False - closing = False - opening = False - -class GarageService(BaseService): - async def update(self, garage: GarageDoor): - async with BaseService._update_lock: - garage.device_params = await self.get_updated_params(garage.mac) - - device_info = await self._get_device_info(garage) - garage.raw_dict = device_info - - garage.available = True - - # store the nested dict for easier reference below - property_list = await self._get_property_list(garage) - for property in property_list: - if property[0] == "P1056": - garage.open = property[1] != "0" - if garage.open and garage.opening: - garage.opening = False - if not garage.open and garage.closing: - garage.closing = False - break - - return garage - - async def get_garages(self): - if self._devices is None: - self._devices = await self.get_object_list() - - garages = [device for device in self._devices if device.type is DeviceTypes.GARAGE_DOOR] - - return [GarageDoor(device.raw_dict) for device in garages] - - async def open(self, garage: GarageDoor): - if garage.open: - return - await self._run_action(garage, "garage_door_trigger") - self.opening = True - - async def close(self, garage: GarageDoor): - if not garage.open: - return - await self._run_action(garage, "garage_door_trigger") - self.closing = True - - diff --git a/src/wyzeapy/types.py b/src/wyzeapy/types.py index 251447d..9c2a3af 100644 --- a/src/wyzeapy/types.py +++ b/src/wyzeapy/types.py @@ -43,7 +43,6 @@ class DeviceTypes(Enum): SENSE_V2_GATEWAY = "S1Gateway" KEYPAD = "Keypad" LIGHTSTRIP = "LightStrip" - GARAGE_DOOR = "GarageDoor" class Device: From b72d62c43aeec0dddc3b3ebcb6546a9d52c5be90 Mon Sep 17 00:00:00 2001 From: Brian Rogers Date: Sun, 16 Feb 2025 14:43:02 -1000 Subject: [PATCH 5/8] Update __init__.py --- src/wyzeapy/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wyzeapy/__init__.py b/src/wyzeapy/__init__.py index 1fd4c0d..2c31f16 100644 --- a/src/wyzeapy/__init__.py +++ b/src/wyzeapy/__init__.py @@ -239,7 +239,7 @@ async def lock_service(self) -> LockService: if self._lock_service is None: self._lock_service = LockService(self._auth_lib) return self._lock_service - + @property async def sensor_service(self) -> SensorService: """Returns an instance of the sensor service""" From f07abe2752af70e285b86f3aef5ab878cac1cfa7 Mon Sep 17 00:00:00 2001 From: blockarchitech Date: Wed, 19 Feb 2025 13:38:21 -0700 Subject: [PATCH 6/8] chore: rename property ID for floodlight to be ACCESSORY --- src/wyzeapy/services/camera_service.py | 8 ++++---- src/wyzeapy/tests/test_camera_service.py | 6 +++--- src/wyzeapy/types.py | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/wyzeapy/services/camera_service.py b/src/wyzeapy/services/camera_service.py index dc76725..4c93422 100644 --- a/src/wyzeapy/services/camera_service.py +++ b/src/wyzeapy/services/camera_service.py @@ -77,9 +77,9 @@ async def update(self, camera: Camera): camera.on = value == "1" if property is PropertyIDs.CAMERA_SIREN: camera.siren = value == "1" - if property is PropertyIDs.FLOOD_LIGHT: + if property is PropertyIDs.ACCESSORY: camera.floodlight = value == "1" - if property is PropertyIDs.FLOOD_LIGHT and camera.device_params["dongle_product_model"] == "HL_CGDC": + if property is PropertyIDs.ACCESSORY and camera.device_params["dongle_product_model"] == "HL_CGDC": camera.garage = value == "1" # 1 = open, 2 = closed by automation or smart platform (Alexa, Google Home, Rules), 0 = closed by app if property is PropertyIDs.NOTIFICATION: camera.notify = value == "1" @@ -142,13 +142,13 @@ async def siren_off(self, camera: Camera): async def floodlight_on(self, camera: Camera): if (camera.product_model == "AN_RSCW"): await self._run_action_devicemgmt(camera, "spotlight", "1") # Battery cam pro integrated spotlight is controllable elif (camera.product_model in DEVICEMGMT_API_MODELS): await self._run_action_devicemgmt(camera, "floodlight", "1") # Some camera models use a diffrent api - else: await self._set_property(camera, PropertyIDs.FLOOD_LIGHT.value, "1") + else: await self._set_property(camera, PropertyIDs.ACCESSORY.value, "1") # Also controls lamp socket and BCP spotlight async def floodlight_off(self, camera: Camera): if (camera.product_model == "AN_RSCW"): await self._run_action_devicemgmt(camera, "spotlight", "0") # Battery cam pro integrated spotlight is controllable elif (camera.product_model in DEVICEMGMT_API_MODELS): await self._run_action_devicemgmt(camera, "floodlight", "0") # Some camera models use a diffrent api - else: await self._set_property(camera, PropertyIDs.FLOOD_LIGHT.value, "2") + else: await self._set_property(camera, PropertyIDs.ACCESSORY.value, "2") # Garage door trigger uses run action on all models async def garage_door_open(self, camera: Camera): diff --git a/src/wyzeapy/tests/test_camera_service.py b/src/wyzeapy/tests/test_camera_service.py index b3953c9..cc7c251 100644 --- a/src/wyzeapy/tests/test_camera_service.py +++ b/src/wyzeapy/tests/test_camera_service.py @@ -44,7 +44,7 @@ async def test_update_legacy_camera(self): (PropertyIDs.AVAILABLE, "1"), (PropertyIDs.ON, "1"), (PropertyIDs.CAMERA_SIREN, "0"), - (PropertyIDs.FLOOD_LIGHT, "0"), + (PropertyIDs.ACCESSORY, "0"), (PropertyIDs.NOTIFICATION, "1"), (PropertyIDs.MOTION_DETECTION, "1") ] @@ -125,14 +125,14 @@ async def test_floodlight_control_legacy_camera(self): await self.camera_service.floodlight_on(self.test_camera) self.camera_service._set_property.assert_awaited_with( self.test_camera, - PropertyIDs.FLOOD_LIGHT.value, + PropertyIDs.ACCESSORY.value, "1" ) await self.camera_service.floodlight_off(self.test_camera) self.camera_service._set_property.assert_awaited_with( self.test_camera, - PropertyIDs.FLOOD_LIGHT.value, + PropertyIDs.ACCESSORY.value, "2" ) diff --git a/src/wyzeapy/types.py b/src/wyzeapy/types.py index 9c2a3af..b953a3d 100644 --- a/src/wyzeapy/types.py +++ b/src/wyzeapy/types.py @@ -104,7 +104,7 @@ class PropertyIDs(Enum): CONTACT_STATE = "P1301" MOTION_STATE = "P1302" CAMERA_SIREN = "P1049" - FLOOD_LIGHT = "P1056" # Also lamp socket on v3/v4 with lamp socket accessory. And also garage door state on v3 with garage door accessory. + ACCESSORY = "P1056" # Is state for camera accessories, like garage doors, light sockets, and floodlights. SUN_MATCH = "P1528" MOTION_DETECTION = "P1047" # Current Motion Detection State of the Camera MOTION_DETECTION_TOGGLE = "P1001" # This toggles Camera Motion Detection On/Off From 98794d7b2464a50af37abb835db00880e1f7c733 Mon Sep 17 00:00:00 2001 From: blockarchitech Date: Wed, 19 Feb 2025 13:40:24 -0700 Subject: [PATCH 7/8] chore: fix python-black goof --- src/wyzeapy/services/base_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wyzeapy/services/base_service.py b/src/wyzeapy/services/base_service.py index 5b8b87b..edff2d0 100644 --- a/src/wyzeapy/services/base_service.py +++ b/src/wyzeapy/services/base_service.py @@ -29,7 +29,7 @@ class BaseService: _devices: Optional[List[Device]] = None _last_updated_time: time = 0 # preload a value of 0 so that comparison will succeed on the first run _min_update_time = 1200 # lets let the device_params update every 20 minutes for now. This could probably reduced signicficantly. - _update_lock: asyncio.Lock() = asyncio.Lock() + _update_lock: asyncio.Lock = asyncio.Lock() # fmt: skip _update_manager: UpdateManager = UpdateManager() _update_loop = None _updater: DeviceUpdater = None From 9b886203cae5d8e20e36ae51f8b220e79d80764f Mon Sep 17 00:00:00 2001 From: Katie Mulliken Date: Wed, 19 Feb 2025 17:02:57 -0500 Subject: [PATCH 8/8] fix: small format change for checking for garage --- src/wyzeapy/services/camera_service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wyzeapy/services/camera_service.py b/src/wyzeapy/services/camera_service.py index 4c93422..386c75e 100644 --- a/src/wyzeapy/services/camera_service.py +++ b/src/wyzeapy/services/camera_service.py @@ -79,8 +79,8 @@ async def update(self, camera: Camera): camera.siren = value == "1" if property is PropertyIDs.ACCESSORY: camera.floodlight = value == "1" - if property is PropertyIDs.ACCESSORY and camera.device_params["dongle_product_model"] == "HL_CGDC": - camera.garage = value == "1" # 1 = open, 2 = closed by automation or smart platform (Alexa, Google Home, Rules), 0 = closed by app + if camera.device_params["dongle_product_model"] == "HL_CGDC": + camera.garage = value == "1" # 1 = open, 2 = closed by automation or smart platform (Alexa, Google Home, Rules), 0 = closed by app if property is PropertyIDs.NOTIFICATION: camera.notify = value == "1" if property is PropertyIDs.MOTION_DETECTION: