wyzeapy.services.camera_service
1# Copyright (c) 2021. Mulliken, LLC - All Rights Reserved 2# You may use, distribute and modify this code under the terms 3# of the attached license. You should have received a copy of 4# the license with this file. If not, please write to: 5# katie@mulliken.net to receive a copy 6import asyncio 7import logging 8import time 9from threading import Thread 10from typing import Any, List, Optional, Dict, Callable, Tuple 11 12from aiohttp import ClientOSError, ContentTypeError 13 14from ..exceptions import UnknownApiError 15from .base_service import BaseService 16from .update_manager import DeviceUpdater 17from ..types import Device, DeviceTypes, Event, PropertyIDs, DeviceMgmtToggleProps 18from ..utils import return_event_for_device, create_pid_pair 19 20_LOGGER = logging.getLogger(__name__) 21 22# NOTE: Make sure to also define props in devicemgmt_create_capabilities_payload() 23DEVICEMGMT_API_MODELS = ["LD_CFP", "AN_RSCW", "GW_GC1"] # Floodlight pro, battery cam pro, and OG use a diffrent api (devicemgmt) 24 25 26class Camera(Device): 27 def __init__(self, dictionary: Dict[Any, Any]): 28 super().__init__(dictionary) 29 30 self.last_event: Optional[Event] = None 31 self.last_event_ts: int = int(time.time() * 1000) 32 self.on: bool = True 33 self.siren: bool = False 34 self.floodlight: bool = False 35 self.garage: bool = False 36 37 38class CameraService(BaseService): 39 _updater_thread: Optional[Thread] = None 40 _subscribers: List[Tuple[Camera, Callable[[Camera], None]]] = [] 41 42 async def update(self, camera: Camera): 43 # Get updated device_params 44 async with BaseService._update_lock: 45 camera.device_params = await self.get_updated_params(camera.mac) 46 47 # Get camera events 48 response = await self._get_event_list(10) 49 raw_events = response['data']['event_list'] 50 latest_events = [Event(raw_event) for raw_event in raw_events] 51 52 if (event := return_event_for_device(camera, latest_events)) is not None: 53 camera.last_event = event 54 camera.last_event_ts = event.event_ts 55 56 # Update camera state 57 if (camera.product_model in DEVICEMGMT_API_MODELS): # New api 58 state_response: Dict[str, Any] = await self._get_iot_prop_devicemgmt(camera) 59 for propCategory in state_response['data']['capabilities']: 60 if propCategory['name'] == "camera": 61 camera.motion = propCategory['properties']['motion-detect-recording'] 62 if propCategory['name'] == "floodlight" or propCategory['name'] == "spotlight": 63 camera.floodlight = propCategory['properties']['on'] 64 if propCategory['name'] == "siren": 65 camera.siren = propCategory['properties']['state'] 66 if propCategory['name'] == "iot-device": 67 camera.notify = propCategory['properties']['push-switch'] 68 camera.on = propCategory['properties']['iot-power'] 69 camera.available = propCategory['properties']['iot-state'] 70 71 else: # All other cam types (old api?) 72 state_response: List[Tuple[PropertyIDs, Any]] = await self._get_property_list(camera) 73 for property, value in state_response: 74 if property is PropertyIDs.AVAILABLE: 75 camera.available = value == "1" 76 if property is PropertyIDs.ON: 77 camera.on = value == "1" 78 if property is PropertyIDs.CAMERA_SIREN: 79 camera.siren = value == "1" 80 if property is PropertyIDs.ACCESSORY: 81 camera.floodlight = value == "1" 82 if camera.device_params["dongle_product_model"] == "HL_CGDC": 83 camera.garage = value == "1" # 1 = open, 2 = closed by automation or smart platform (Alexa, Google Home, Rules), 0 = closed by app 84 if property is PropertyIDs.NOTIFICATION: 85 camera.notify = value == "1" 86 if property is PropertyIDs.MOTION_DETECTION: 87 camera.motion = value == "1" 88 89 return camera 90 91 async def register_for_updates(self, camera: Camera, callback: Callable[[Camera], None]): 92 loop = asyncio.get_event_loop() 93 if not self._updater_thread: 94 self._updater_thread = Thread(target=self.update_worker, args=[loop, ], daemon=True) 95 self._updater_thread.start() 96 97 self._subscribers.append((camera, callback)) 98 99 async def deregister_for_updates(self, camera: Camera): 100 self._subscribers = [(cam, callback) for cam, callback in self._subscribers if cam.mac != camera.mac] 101 102 def update_worker(self, loop): 103 while True: 104 if len(self._subscribers) < 1: 105 time.sleep(0.1) 106 else: 107 for camera, callback in self._subscribers: 108 try: 109 callback(asyncio.run_coroutine_threadsafe(self.update(camera), loop).result()) 110 except UnknownApiError as e: 111 _LOGGER.warning(f"The update method detected an UnknownApiError: {e}") 112 except ClientOSError as e: 113 _LOGGER.error(f"A network error was detected: {e}") 114 except ContentTypeError as e: 115 _LOGGER.error(f"Server returned unexpected ContentType: {e}") 116 117 async def get_cameras(self) -> List[Camera]: 118 if self._devices is None: 119 self._devices = await self.get_object_list() 120 121 cameras = [device for device in self._devices if device.type is DeviceTypes.CAMERA] 122 123 return [Camera(camera.raw_dict) for camera in cameras] 124 125 async def turn_on(self, camera: Camera): 126 if (camera.product_model in DEVICEMGMT_API_MODELS): await self._run_action_devicemgmt(camera, "power", "wakeup") # Some camera models use a diffrent api 127 else: await self._run_action(camera, "power_on") 128 129 async def turn_off(self, camera: Camera): 130 if (camera.product_model in DEVICEMGMT_API_MODELS): await self._run_action_devicemgmt(camera, "power", "sleep") # Some camera models use a diffrent api 131 else: await self._run_action(camera, "power_off") 132 133 async def siren_on(self, camera: Camera): 134 if (camera.product_model in DEVICEMGMT_API_MODELS): await self._run_action_devicemgmt(camera, "siren", "siren-on") # Some camera models use a diffrent api 135 else: await self._run_action(camera, "siren_on") 136 137 async def siren_off(self, camera: Camera): 138 if (camera.product_model in DEVICEMGMT_API_MODELS): await self._run_action_devicemgmt(camera, "siren", "siren-off") # Some camera models use a diffrent api 139 else: await self._run_action(camera, "siren_off") 140 141 # Also controls lamp socket and BCP spotlight 142 async def floodlight_on(self, camera: Camera): 143 if (camera.product_model == "AN_RSCW"): await self._run_action_devicemgmt(camera, "spotlight", "1") # Battery cam pro integrated spotlight is controllable 144 elif (camera.product_model in DEVICEMGMT_API_MODELS): await self._run_action_devicemgmt(camera, "floodlight", "1") # Some camera models use a diffrent api 145 else: await self._set_property(camera, PropertyIDs.ACCESSORY.value, "1") 146 147 # Also controls lamp socket and BCP spotlight 148 async def floodlight_off(self, camera: Camera): 149 if (camera.product_model == "AN_RSCW"): await self._run_action_devicemgmt(camera, "spotlight", "0") # Battery cam pro integrated spotlight is controllable 150 elif (camera.product_model in DEVICEMGMT_API_MODELS): await self._run_action_devicemgmt(camera, "floodlight", "0") # Some camera models use a diffrent api 151 else: await self._set_property(camera, PropertyIDs.ACCESSORY.value, "2") 152 153 # Garage door trigger uses run action on all models 154 async def garage_door_open(self, camera: Camera): 155 await self._run_action(camera, "garage_door_trigger") 156 157 async def garage_door_close(self, camera: Camera): 158 await self._run_action(camera, "garage_door_trigger") 159 160 async def turn_on_notifications(self, camera: Camera): 161 if (camera.product_model in DEVICEMGMT_API_MODELS): await self._set_toggle(camera, DeviceMgmtToggleProps.NOTIFICATION_TOGGLE.value, "1") 162 else: await self._set_property(camera, PropertyIDs.NOTIFICATION.value, "1") 163 164 async def turn_off_notifications(self, camera: Camera): 165 if (camera.product_model in DEVICEMGMT_API_MODELS): await self._set_toggle(camera, DeviceMgmtToggleProps.NOTIFICATION_TOGGLE.value, "0") 166 else: await self._set_property(camera, PropertyIDs.NOTIFICATION.value, "0") 167 168 # Both properties need to be set on newer cams, older cameras seem to only react 169 # to the first property but it doesnt hurt to set both 170 async def turn_on_motion_detection(self, camera: Camera): 171 if (camera.product_model in DEVICEMGMT_API_MODELS): await self._set_toggle(camera, DeviceMgmtToggleProps.EVENT_RECORDING_TOGGLE.value, "1") 172 elif (camera.product_model in ["WVOD1", "HL_WCO2"]): await self._set_property_list(camera, [create_pid_pair(PropertyIDs.WCO_MOTION_DETECTION, "1")]) 173 else: 174 await self._set_property(camera, PropertyIDs.MOTION_DETECTION.value, "1") 175 await self._set_property(camera, PropertyIDs.MOTION_DETECTION_TOGGLE.value, "1") 176 177 async def turn_off_motion_detection(self, camera: Camera): 178 if (camera.product_model in DEVICEMGMT_API_MODELS): await self._set_toggle(camera, DeviceMgmtToggleProps.EVENT_RECORDING_TOGGLE.value, "0") 179 elif (camera.product_model in ["WVOD1", "HL_WCO2"]): await self._set_property_list(camera, [create_pid_pair(PropertyIDs.WCO_MOTION_DETECTION, "0")]) 180 else: 181 await self._set_property(camera, PropertyIDs.MOTION_DETECTION.value, "0") 182 await self._set_property(camera, PropertyIDs.MOTION_DETECTION_TOGGLE.value, "0")
DEVICEMGMT_API_MODELS =
['LD_CFP', 'AN_RSCW', 'GW_GC1']
27class Camera(Device): 28 def __init__(self, dictionary: Dict[Any, Any]): 29 super().__init__(dictionary) 30 31 self.last_event: Optional[Event] = None 32 self.last_event_ts: int = int(time.time() * 1000) 33 self.on: bool = True 34 self.siren: bool = False 35 self.floodlight: bool = False 36 self.garage: bool = False
last_event: Optional[wyzeapy.types.Event]
Inherited Members
39class CameraService(BaseService): 40 _updater_thread: Optional[Thread] = None 41 _subscribers: List[Tuple[Camera, Callable[[Camera], None]]] = [] 42 43 async def update(self, camera: Camera): 44 # Get updated device_params 45 async with BaseService._update_lock: 46 camera.device_params = await self.get_updated_params(camera.mac) 47 48 # Get camera events 49 response = await self._get_event_list(10) 50 raw_events = response['data']['event_list'] 51 latest_events = [Event(raw_event) for raw_event in raw_events] 52 53 if (event := return_event_for_device(camera, latest_events)) is not None: 54 camera.last_event = event 55 camera.last_event_ts = event.event_ts 56 57 # Update camera state 58 if (camera.product_model in DEVICEMGMT_API_MODELS): # New api 59 state_response: Dict[str, Any] = await self._get_iot_prop_devicemgmt(camera) 60 for propCategory in state_response['data']['capabilities']: 61 if propCategory['name'] == "camera": 62 camera.motion = propCategory['properties']['motion-detect-recording'] 63 if propCategory['name'] == "floodlight" or propCategory['name'] == "spotlight": 64 camera.floodlight = propCategory['properties']['on'] 65 if propCategory['name'] == "siren": 66 camera.siren = propCategory['properties']['state'] 67 if propCategory['name'] == "iot-device": 68 camera.notify = propCategory['properties']['push-switch'] 69 camera.on = propCategory['properties']['iot-power'] 70 camera.available = propCategory['properties']['iot-state'] 71 72 else: # All other cam types (old api?) 73 state_response: List[Tuple[PropertyIDs, Any]] = await self._get_property_list(camera) 74 for property, value in state_response: 75 if property is PropertyIDs.AVAILABLE: 76 camera.available = value == "1" 77 if property is PropertyIDs.ON: 78 camera.on = value == "1" 79 if property is PropertyIDs.CAMERA_SIREN: 80 camera.siren = value == "1" 81 if property is PropertyIDs.ACCESSORY: 82 camera.floodlight = value == "1" 83 if camera.device_params["dongle_product_model"] == "HL_CGDC": 84 camera.garage = value == "1" # 1 = open, 2 = closed by automation or smart platform (Alexa, Google Home, Rules), 0 = closed by app 85 if property is PropertyIDs.NOTIFICATION: 86 camera.notify = value == "1" 87 if property is PropertyIDs.MOTION_DETECTION: 88 camera.motion = value == "1" 89 90 return camera 91 92 async def register_for_updates(self, camera: Camera, callback: Callable[[Camera], None]): 93 loop = asyncio.get_event_loop() 94 if not self._updater_thread: 95 self._updater_thread = Thread(target=self.update_worker, args=[loop, ], daemon=True) 96 self._updater_thread.start() 97 98 self._subscribers.append((camera, callback)) 99 100 async def deregister_for_updates(self, camera: Camera): 101 self._subscribers = [(cam, callback) for cam, callback in self._subscribers if cam.mac != camera.mac] 102 103 def update_worker(self, loop): 104 while True: 105 if len(self._subscribers) < 1: 106 time.sleep(0.1) 107 else: 108 for camera, callback in self._subscribers: 109 try: 110 callback(asyncio.run_coroutine_threadsafe(self.update(camera), loop).result()) 111 except UnknownApiError as e: 112 _LOGGER.warning(f"The update method detected an UnknownApiError: {e}") 113 except ClientOSError as e: 114 _LOGGER.error(f"A network error was detected: {e}") 115 except ContentTypeError as e: 116 _LOGGER.error(f"Server returned unexpected ContentType: {e}") 117 118 async def get_cameras(self) -> List[Camera]: 119 if self._devices is None: 120 self._devices = await self.get_object_list() 121 122 cameras = [device for device in self._devices if device.type is DeviceTypes.CAMERA] 123 124 return [Camera(camera.raw_dict) for camera in cameras] 125 126 async def turn_on(self, camera: Camera): 127 if (camera.product_model in DEVICEMGMT_API_MODELS): await self._run_action_devicemgmt(camera, "power", "wakeup") # Some camera models use a diffrent api 128 else: await self._run_action(camera, "power_on") 129 130 async def turn_off(self, camera: Camera): 131 if (camera.product_model in DEVICEMGMT_API_MODELS): await self._run_action_devicemgmt(camera, "power", "sleep") # Some camera models use a diffrent api 132 else: await self._run_action(camera, "power_off") 133 134 async def siren_on(self, camera: Camera): 135 if (camera.product_model in DEVICEMGMT_API_MODELS): await self._run_action_devicemgmt(camera, "siren", "siren-on") # Some camera models use a diffrent api 136 else: await self._run_action(camera, "siren_on") 137 138 async def siren_off(self, camera: Camera): 139 if (camera.product_model in DEVICEMGMT_API_MODELS): await self._run_action_devicemgmt(camera, "siren", "siren-off") # Some camera models use a diffrent api 140 else: await self._run_action(camera, "siren_off") 141 142 # Also controls lamp socket and BCP spotlight 143 async def floodlight_on(self, camera: Camera): 144 if (camera.product_model == "AN_RSCW"): await self._run_action_devicemgmt(camera, "spotlight", "1") # Battery cam pro integrated spotlight is controllable 145 elif (camera.product_model in DEVICEMGMT_API_MODELS): await self._run_action_devicemgmt(camera, "floodlight", "1") # Some camera models use a diffrent api 146 else: await self._set_property(camera, PropertyIDs.ACCESSORY.value, "1") 147 148 # Also controls lamp socket and BCP spotlight 149 async def floodlight_off(self, camera: Camera): 150 if (camera.product_model == "AN_RSCW"): await self._run_action_devicemgmt(camera, "spotlight", "0") # Battery cam pro integrated spotlight is controllable 151 elif (camera.product_model in DEVICEMGMT_API_MODELS): await self._run_action_devicemgmt(camera, "floodlight", "0") # Some camera models use a diffrent api 152 else: await self._set_property(camera, PropertyIDs.ACCESSORY.value, "2") 153 154 # Garage door trigger uses run action on all models 155 async def garage_door_open(self, camera: Camera): 156 await self._run_action(camera, "garage_door_trigger") 157 158 async def garage_door_close(self, camera: Camera): 159 await self._run_action(camera, "garage_door_trigger") 160 161 async def turn_on_notifications(self, camera: Camera): 162 if (camera.product_model in DEVICEMGMT_API_MODELS): await self._set_toggle(camera, DeviceMgmtToggleProps.NOTIFICATION_TOGGLE.value, "1") 163 else: await self._set_property(camera, PropertyIDs.NOTIFICATION.value, "1") 164 165 async def turn_off_notifications(self, camera: Camera): 166 if (camera.product_model in DEVICEMGMT_API_MODELS): await self._set_toggle(camera, DeviceMgmtToggleProps.NOTIFICATION_TOGGLE.value, "0") 167 else: await self._set_property(camera, PropertyIDs.NOTIFICATION.value, "0") 168 169 # Both properties need to be set on newer cams, older cameras seem to only react 170 # to the first property but it doesnt hurt to set both 171 async def turn_on_motion_detection(self, camera: Camera): 172 if (camera.product_model in DEVICEMGMT_API_MODELS): await self._set_toggle(camera, DeviceMgmtToggleProps.EVENT_RECORDING_TOGGLE.value, "1") 173 elif (camera.product_model in ["WVOD1", "HL_WCO2"]): await self._set_property_list(camera, [create_pid_pair(PropertyIDs.WCO_MOTION_DETECTION, "1")]) 174 else: 175 await self._set_property(camera, PropertyIDs.MOTION_DETECTION.value, "1") 176 await self._set_property(camera, PropertyIDs.MOTION_DETECTION_TOGGLE.value, "1") 177 178 async def turn_off_motion_detection(self, camera: Camera): 179 if (camera.product_model in DEVICEMGMT_API_MODELS): await self._set_toggle(camera, DeviceMgmtToggleProps.EVENT_RECORDING_TOGGLE.value, "0") 180 elif (camera.product_model in ["WVOD1", "HL_WCO2"]): await self._set_property_list(camera, [create_pid_pair(PropertyIDs.WCO_MOTION_DETECTION, "0")]) 181 else: 182 await self._set_property(camera, PropertyIDs.MOTION_DETECTION.value, "0") 183 await self._set_property(camera, PropertyIDs.MOTION_DETECTION_TOGGLE.value, "0")
Base service class for interacting with Wyze devices.
43 async def update(self, camera: Camera): 44 # Get updated device_params 45 async with BaseService._update_lock: 46 camera.device_params = await self.get_updated_params(camera.mac) 47 48 # Get camera events 49 response = await self._get_event_list(10) 50 raw_events = response['data']['event_list'] 51 latest_events = [Event(raw_event) for raw_event in raw_events] 52 53 if (event := return_event_for_device(camera, latest_events)) is not None: 54 camera.last_event = event 55 camera.last_event_ts = event.event_ts 56 57 # Update camera state 58 if (camera.product_model in DEVICEMGMT_API_MODELS): # New api 59 state_response: Dict[str, Any] = await self._get_iot_prop_devicemgmt(camera) 60 for propCategory in state_response['data']['capabilities']: 61 if propCategory['name'] == "camera": 62 camera.motion = propCategory['properties']['motion-detect-recording'] 63 if propCategory['name'] == "floodlight" or propCategory['name'] == "spotlight": 64 camera.floodlight = propCategory['properties']['on'] 65 if propCategory['name'] == "siren": 66 camera.siren = propCategory['properties']['state'] 67 if propCategory['name'] == "iot-device": 68 camera.notify = propCategory['properties']['push-switch'] 69 camera.on = propCategory['properties']['iot-power'] 70 camera.available = propCategory['properties']['iot-state'] 71 72 else: # All other cam types (old api?) 73 state_response: List[Tuple[PropertyIDs, Any]] = await self._get_property_list(camera) 74 for property, value in state_response: 75 if property is PropertyIDs.AVAILABLE: 76 camera.available = value == "1" 77 if property is PropertyIDs.ON: 78 camera.on = value == "1" 79 if property is PropertyIDs.CAMERA_SIREN: 80 camera.siren = value == "1" 81 if property is PropertyIDs.ACCESSORY: 82 camera.floodlight = value == "1" 83 if camera.device_params["dongle_product_model"] == "HL_CGDC": 84 camera.garage = value == "1" # 1 = open, 2 = closed by automation or smart platform (Alexa, Google Home, Rules), 0 = closed by app 85 if property is PropertyIDs.NOTIFICATION: 86 camera.notify = value == "1" 87 if property is PropertyIDs.MOTION_DETECTION: 88 camera.motion = value == "1" 89 90 return camera
92 async def register_for_updates(self, camera: Camera, callback: Callable[[Camera], None]): 93 loop = asyncio.get_event_loop() 94 if not self._updater_thread: 95 self._updater_thread = Thread(target=self.update_worker, args=[loop, ], daemon=True) 96 self._updater_thread.start() 97 98 self._subscribers.append((camera, callback))
def
update_worker(self, loop):
103 def update_worker(self, loop): 104 while True: 105 if len(self._subscribers) < 1: 106 time.sleep(0.1) 107 else: 108 for camera, callback in self._subscribers: 109 try: 110 callback(asyncio.run_coroutine_threadsafe(self.update(camera), loop).result()) 111 except UnknownApiError as e: 112 _LOGGER.warning(f"The update method detected an UnknownApiError: {e}") 113 except ClientOSError as e: 114 _LOGGER.error(f"A network error was detected: {e}") 115 except ContentTypeError as e: 116 _LOGGER.error(f"Server returned unexpected ContentType: {e}")
143 async def floodlight_on(self, camera: Camera): 144 if (camera.product_model == "AN_RSCW"): await self._run_action_devicemgmt(camera, "spotlight", "1") # Battery cam pro integrated spotlight is controllable 145 elif (camera.product_model in DEVICEMGMT_API_MODELS): await self._run_action_devicemgmt(camera, "floodlight", "1") # Some camera models use a diffrent api 146 else: await self._set_property(camera, PropertyIDs.ACCESSORY.value, "1")
149 async def floodlight_off(self, camera: Camera): 150 if (camera.product_model == "AN_RSCW"): await self._run_action_devicemgmt(camera, "spotlight", "0") # Battery cam pro integrated spotlight is controllable 151 elif (camera.product_model in DEVICEMGMT_API_MODELS): await self._run_action_devicemgmt(camera, "floodlight", "0") # Some camera models use a diffrent api 152 else: await self._set_property(camera, PropertyIDs.ACCESSORY.value, "2")
171 async def turn_on_motion_detection(self, camera: Camera): 172 if (camera.product_model in DEVICEMGMT_API_MODELS): await self._set_toggle(camera, DeviceMgmtToggleProps.EVENT_RECORDING_TOGGLE.value, "1") 173 elif (camera.product_model in ["WVOD1", "HL_WCO2"]): await self._set_property_list(camera, [create_pid_pair(PropertyIDs.WCO_MOTION_DETECTION, "1")]) 174 else: 175 await self._set_property(camera, PropertyIDs.MOTION_DETECTION.value, "1") 176 await self._set_property(camera, PropertyIDs.MOTION_DETECTION_TOGGLE.value, "1")
178 async def turn_off_motion_detection(self, camera: Camera): 179 if (camera.product_model in DEVICEMGMT_API_MODELS): await self._set_toggle(camera, DeviceMgmtToggleProps.EVENT_RECORDING_TOGGLE.value, "0") 180 elif (camera.product_model in ["WVOD1", "HL_WCO2"]): await self._set_property_list(camera, [create_pid_pair(PropertyIDs.WCO_MOTION_DETECTION, "0")]) 181 else: 182 await self._set_property(camera, PropertyIDs.MOTION_DETECTION.value, "0") 183 await self._set_property(camera, PropertyIDs.MOTION_DETECTION_TOGGLE.value, "0")