From bc491ebd7a36ad76edd193a9a04c2b5c6707340f Mon Sep 17 00:00:00 2001 From: Brett Jia Date: Sat, 25 Mar 2023 19:12:15 -0400 Subject: [PATCH 1/9] only create vss and siren for supported basestation models --- plugins/arlo/src/arlo_plugin/basestation.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/plugins/arlo/src/arlo_plugin/basestation.py b/plugins/arlo/src/arlo_plugin/basestation.py index c0df011c56..f6893ce201 100644 --- a/plugins/arlo/src/arlo_plugin/basestation.py +++ b/plugins/arlo/src/arlo_plugin/basestation.py @@ -14,6 +14,11 @@ class ArloBasestation(ArloDeviceBase, DeviceProvider): + MODELS_WITH_SIRENS = [ + "vmb4000", + "vmb4500" + ] + vss: ArloSirenVirtualSecuritySystem = None def __init__(self, nativeId: str, arlo_basestation: dict, provider: ArloProvider) -> None: @@ -26,6 +31,10 @@ def get_device_type(self) -> str: return ScryptedDeviceType.DeviceProvider.value def get_builtin_child_device_manifests(self) -> List[Device]: + if not any([self.arlo_device['modelId'].lower().startswith(model) for model in ArloBasestation.MODELS_WITH_SIRENS]): + # this basestation has no builtin siren, so no manifests to return + return [] + vss_id = f'{self.arlo_device["deviceId"]}.vss' vss = self.get_or_create_vss(vss_id) return [ From bb9d9d96cbc3ff0450bf7b1d6849a835bb50f684 Mon Sep 17 00:00:00 2001 From: Brett Jia Date: Sat, 25 Mar 2023 20:59:22 -0400 Subject: [PATCH 2/9] VideoClips only if camera has cloud recording + start of Cameras as DeviceProviders --- .../arlo/src/arlo_plugin/arlo/arlo_async.py | 11 +++++- plugins/arlo/src/arlo_plugin/camera.py | 36 +++++++++++++++++-- plugins/arlo/src/arlo_plugin/provider.py | 1 + 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/plugins/arlo/src/arlo_plugin/arlo/arlo_async.py b/plugins/arlo/src/arlo_plugin/arlo/arlo_async.py index e652bd1f6d..d0f5547d66 100644 --- a/plugins/arlo/src/arlo_plugin/arlo/arlo_async.py +++ b/plugins/arlo/src/arlo_plugin/arlo/arlo_async.py @@ -784,4 +784,13 @@ def _getLibraryCached(self, from_date: str, to_date: str): 'dateFrom': from_date, 'dateTo': to_date } - ) \ No newline at end of file + ) + + def GetSmartFeatures(self, device): + smart_features = self._getSmartFeaturesCached() + key = f"{device['owner']['ownerId']}_{device['deviceId']}" + return smart_features["features"].get(key) + + @cached(cache=TTLCache(maxsize=1, ttl=60)) + def _getSmartFeaturesCached(self): + return self.request.get(f'https://{self.BASE_URL}/hmsweb/users/subscription/smart/features') \ No newline at end of file diff --git a/plugins/arlo/src/arlo_plugin/camera.py b/plugins/arlo/src/arlo_plugin/camera.py index a213830be9..e5d27ba8d4 100644 --- a/plugins/arlo/src/arlo_plugin/camera.py +++ b/plugins/arlo/src/arlo_plugin/camera.py @@ -10,7 +10,7 @@ import scrypted_arlo_go import scrypted_sdk -from scrypted_sdk.types import Setting, Settings, Camera, VideoCamera, VideoClips, VideoClip, VideoClipOptions, MotionSensor, Battery, MediaObject, ResponsePictureOptions, ResponseMediaStreamOptions, ScryptedMimeTypes, ScryptedInterface, ScryptedDeviceType +from scrypted_sdk.types import Setting, Settings, Camera, VideoCamera, VideoClips, VideoClip, VideoClipOptions, MotionSensor, Battery, DeviceProvider, MediaObject, ResponsePictureOptions, ResponseMediaStreamOptions, ScryptedMimeTypes, ScryptedInterface, ScryptedDeviceType from .base import ArloDeviceBase from .child_process import HeartbeatChildProcess @@ -21,7 +21,20 @@ from .provider import ArloProvider -class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, VideoClips, MotionSensor, Battery): +class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider, VideoClips, MotionSensor, Battery): + MODELS_WITH_SPOTLIGHTS = [ + "vmc4040p", + "vmc2030", + "vmc2032", + "vmc4041p", + "vmc4050p", + "vmc5040", + "vml2030", + "vml4030", + ] + + MODELS_WITH_SIRENS = [] + timeout: int = 30 intercom_session = None @@ -55,7 +68,6 @@ def get_applicable_interfaces(self) -> List[str]: ScryptedInterface.MotionSensor.value, ScryptedInterface.Battery.value, ScryptedInterface.Settings.value, - ScryptedInterface.VideoClips.value, ]) if self.two_way_audio: @@ -66,6 +78,12 @@ def get_applicable_interfaces(self) -> List[str]: results.add(ScryptedInterface.RTCSignalingChannel.value) results.discard(ScryptedInterface.Intercom.value) + if self.has_siren or self.has_spotlight: + results.add(ScryptedInterface.DeviceProvider.value) + + if self.has_cloud_recording: + results.add(ScryptedInterface.VideoClips.value) + if not self._can_push_to_talk(): results.discard(ScryptedInterface.RTCSignalingChannel.value) results.discard(ScryptedInterface.Intercom.value) @@ -92,6 +110,18 @@ def two_way_audio(self) -> bool: else: return True + @property + def has_cloud_recording(self) -> bool: + return self.provider.arlo.GetSmartFeatures(self.arlo_device)["planFeatures"]["eventRecording"] + + @property + def has_spotlight(self) -> bool: + return any([self.arlo_device["modelId"].lower().startswith(model) for model in ArloCamera.MODELS_WITH_SPOTLIGHTS]) + + @property + def has_siren(self) -> bool: + return any([self.arlo_device["modelId"].lower().startswith(model) for model in ArloCamera.MODELS_WITH_SIRENS]) + async def getSettings(self) -> List[Setting]: if self._can_push_to_talk(): return [ diff --git a/plugins/arlo/src/arlo_plugin/provider.py b/plugins/arlo/src/arlo_plugin/provider.py index 9e50de08cf..dd6ef63a08 100644 --- a/plugins/arlo/src/arlo_plugin/provider.py +++ b/plugins/arlo/src/arlo_plugin/provider.py @@ -558,6 +558,7 @@ def validate_setting(self, key: str, val: SettingValue) -> bool: return False return True + @ArloDeviceBase.async_print_exception_guard async def discoverDevices(self, duration: int = 0) -> None: if not self.arlo: raise Exception("Arlo client not connected, cannot discover devices") From 157f5d920fe4407e472d5eaa66a4cfb67b76b695 Mon Sep 17 00:00:00 2001 From: Brett Jia Date: Sat, 25 Mar 2023 21:19:54 -0400 Subject: [PATCH 3/9] make verbose logging a boolean toggle --- plugins/arlo/src/arlo_plugin/provider.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/plugins/arlo/src/arlo_plugin/provider.py b/plugins/arlo/src/arlo_plugin/provider.py index dd6ef63a08..a56fcfd023 100644 --- a/plugins/arlo/src/arlo_plugin/provider.py +++ b/plugins/arlo/src/arlo_plugin/provider.py @@ -472,11 +472,10 @@ async def getSettings(self) -> List[Setting]: { "group": "General", "key": "plugin_verbosity", - "title": "Plugin Verbosity", - "description": "Select the verbosity of this plugin. 'Verbose' will show debugging messages, " - "including events received from connected Arlo cameras.", - "value": self.plugin_verbosity, - "choices": sorted(self.plugin_verbosity_choices.keys()), + "title": "Verbose Logging", + "description": "Enable this option to show debug messages, including events received from connected Arlo cameras.", + "value": self.plugin_verbosity == "Verbose", + "type": "boolean", }, ]) @@ -493,13 +492,14 @@ async def putSetting(self, key: str, value: SettingValue) -> None: elif key == "force_reauth": # force arlo client to be invalidated and reloaded self.invalidate_arlo_client() + elif key == "plugin_verbosity": + self.storage.setItem(key, "Verbose" if value == "true" else "Normal") + self.propagate_verbosity() + skip_arlo_client = True else: self.storage.setItem(key, value) - if key == "plugin_verbosity": - self.propagate_verbosity() - skip_arlo_client = True - elif key == "arlo_transport": + if key == "arlo_transport": self.propagate_transport() # force arlo client to be invalidated and reloaded, but # keep any mfa codes From 3cbcf55ce9f86ed5ab9eb1454886ada8d87f7124 Mon Sep 17 00:00:00 2001 From: Brett Jia Date: Sat, 25 Mar 2023 21:41:05 -0400 Subject: [PATCH 4/9] camera spotlights and floodlights --- .../arlo/src/arlo_plugin/arlo/arlo_async.py | 52 ++++++++++++++++++ plugins/arlo/src/arlo_plugin/camera.py | 48 ++++++++++++++++- plugins/arlo/src/arlo_plugin/spotlight.py | 53 +++++++++++++++++++ 3 files changed, 151 insertions(+), 2 deletions(-) create mode 100644 plugins/arlo/src/arlo_plugin/spotlight.py diff --git a/plugins/arlo/src/arlo_plugin/arlo/arlo_async.py b/plugins/arlo/src/arlo_plugin/arlo/arlo_async.py index d0f5547d66..b7309267cb 100644 --- a/plugins/arlo/src/arlo_plugin/arlo/arlo_async.py +++ b/plugins/arlo/src/arlo_plugin/arlo/arlo_async.py @@ -737,6 +737,58 @@ def SirenOff(self, basestation): } }) + def SpotlightOn(self, basestation, camera): + resource = f"cameras/{camera.get('deviceId')}" + return self.Notify(basestation, { + "action": "set", + "resource": resource, + "publishResponse": True, + "properties": { + "spotlight": { + "enabled": True, + }, + }, + }) + + def SpotlightOff(self, basestation, camera): + resource = f"cameras/{camera.get('deviceId')}" + return self.Notify(basestation, { + "action": "set", + "resource": resource, + "publishResponse": True, + "properties": { + "spotlight": { + "enabled": False, + }, + }, + }) + + def FloodlightOn(self, basestation, camera): + resource = f"cameras/{camera.get('deviceId')}" + return self.Notify(basestation, { + "action": "set", + "resource": resource, + "publishResponse": True, + "properties": { + "floodlight": { + "on": True, + }, + }, + }) + + def FloodlightOff(self, basestation, camera): + resource = f"cameras/{camera.get('deviceId')}" + return self.Notify(basestation, { + "action": "set", + "resource": resource, + "publishResponse": True, + "properties": { + "floodlight": { + "on": False, + }, + }, + }) + def GetLibrary(self, device, from_date: datetime, to_date: datetime): """ This call returns the following: diff --git a/plugins/arlo/src/arlo_plugin/camera.py b/plugins/arlo/src/arlo_plugin/camera.py index e5d27ba8d4..cfee5d97be 100644 --- a/plugins/arlo/src/arlo_plugin/camera.py +++ b/plugins/arlo/src/arlo_plugin/camera.py @@ -10,9 +10,10 @@ import scrypted_arlo_go import scrypted_sdk -from scrypted_sdk.types import Setting, Settings, Camera, VideoCamera, VideoClips, VideoClip, VideoClipOptions, MotionSensor, Battery, DeviceProvider, MediaObject, ResponsePictureOptions, ResponseMediaStreamOptions, ScryptedMimeTypes, ScryptedInterface, ScryptedDeviceType +from scrypted_sdk.types import Setting, Settings, Device, Camera, VideoCamera, VideoClips, VideoClip, VideoClipOptions, MotionSensor, Battery, DeviceProvider, MediaObject, ResponsePictureOptions, ResponseMediaStreamOptions, ScryptedMimeTypes, ScryptedInterface, ScryptedDeviceType from .base import ArloDeviceBase +from .spotlight import ArloSpotlight, ArloFloodlight from .child_process import HeartbeatChildProcess from .util import BackgroundTaskMixin @@ -33,10 +34,13 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider, "vml4030", ] + MODELS_WITH_FLOODLIGHTS = ["fb1001"] + MODELS_WITH_SIRENS = [] timeout: int = 30 intercom_session = None + light: ArloSpotlight = None def __init__(self, nativeId: str, arlo_device: dict, arlo_basestation: dict, provider: ArloProvider) -> None: super().__init__(nativeId=nativeId, arlo_device=arlo_device, arlo_basestation=arlo_basestation, provider=provider) @@ -78,7 +82,7 @@ def get_applicable_interfaces(self) -> List[str]: results.add(ScryptedInterface.RTCSignalingChannel.value) results.discard(ScryptedInterface.Intercom.value) - if self.has_siren or self.has_spotlight: + if self.has_siren or self.has_spotlight or self.has_floodlight: results.add(ScryptedInterface.DeviceProvider.value) if self.has_cloud_recording: @@ -93,6 +97,26 @@ def get_applicable_interfaces(self) -> List[str]: def get_device_type(self) -> str: return ScryptedDeviceType.Camera.value + def get_builtin_child_device_manifests(self) -> List[Device]: + if self.has_spotlight or self.has_floodlight: + light = self.get_or_create_spotlight_or_floodlight() + return [ + { + "info": { + "model": f"{self.arlo_device['modelId']} {self.arlo_device['properties'].get('hwVersion', '')}".strip(), + "manufacturer": "Arlo", + "firmware": self.arlo_device.get("firmwareVersion"), + "serialNumber": self.arlo_device["deviceId"], + }, + "nativeId": light.nativeId, + "name": f'{self.arlo_device["deviceName"]} {"Spotlight" if self.has_spotlight else "Floodlight"}', + "interfaces": light.get_applicable_interfaces(), + "type": light.get_device_type(), + "providerNativeId": self.nativeId, + } + ] + return [] + @property def webrtc_emulation(self) -> bool: if self.storage: @@ -118,6 +142,10 @@ def has_cloud_recording(self) -> bool: def has_spotlight(self) -> bool: return any([self.arlo_device["modelId"].lower().startswith(model) for model in ArloCamera.MODELS_WITH_SPOTLIGHTS]) + @property + def has_floodlight(self) -> bool: + return any([self.arlo_device["modelId"].lower().startswith(model) for model in ArloCamera.MODELS_WITH_FLOODLIGHTS]) + @property def has_siren(self) -> bool: return any([self.arlo_device["modelId"].lower().startswith(model) for model in ArloCamera.MODELS_WITH_SIRENS]) @@ -316,6 +344,22 @@ async def removeVideoClips(self, videoClipIds: List[str]) -> None: self.logger.error("deleting Arlo video clips is not implemented by this plugin") raise Exception("deleting Arlo video clips is not implemented by this plugin") + async def getDevice(self, nativeId: str) -> ArloDeviceBase: + if (nativeId.endswith("spotlight") and self.has_spotlight) or (nativeId.endswith("floodlight") and self.has_floodlight): + return self.get_or_create_spotlight_or_floodlight() + return None + + def get_or_create_spotlight_or_floodlight(self) -> ArloSpotlight: + if self.has_spotlight: + light_id = f'{self.arlo_device["deviceId"]}.spotlight' + if not self.light: + self.light = ArloSpotlight(light_id, self.arlo_device, self.arlo_basestation, self.provider, self) + elif self.has_floodlight: + light_id = f'{self.arlo_device["deviceId"]}.floodlight' + if not self.light: + self.light = ArloFloodlight(light_id, self.arlo_device, self.arlo_basestation, self.provider, self) + return self.light + class ArloCameraRTCSignalingSession(BackgroundTaskMixin): def __init__(self, camera): diff --git a/plugins/arlo/src/arlo_plugin/spotlight.py b/plugins/arlo/src/arlo_plugin/spotlight.py new file mode 100644 index 0000000000..47604d1a6e --- /dev/null +++ b/plugins/arlo/src/arlo_plugin/spotlight.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +from typing import List, TYPE_CHECKING + +from scrypted_sdk.types import OnOff, ScryptedInterface, ScryptedDeviceType + +from .base import ArloDeviceBase + +if TYPE_CHECKING: + # https://adamj.eu/tech/2021/05/13/python-type-hints-how-to-fix-circular-imports/ + from .provider import ArloProvider + from .camera import ArloCamera + + +class ArloSpotlight(ArloDeviceBase, OnOff): + camera: ArloCamera = None + + def __init__(self, nativeId: str, arlo_device: dict, arlo_basestation: dict, provider: ArloProvider, camera: ArloCamera) -> None: + super().__init__(nativeId=nativeId, arlo_device=arlo_device, arlo_basestation=arlo_basestation, provider=provider) + self.camera = camera + + def get_applicable_interfaces(self) -> List[str]: + return [ScryptedInterface.OnOff.value] + + def get_device_type(self) -> str: + return ScryptedDeviceType.Light.value + + @ArloDeviceBase.async_print_exception_guard + async def turnOn(self) -> None: + self.logger.info("Turning on") + self.provider.arlo.SpotlightOn(self.arlo_basestation, self.arlo_device) + self.on = True + + @ArloDeviceBase.async_print_exception_guard + async def turnOff(self) -> None: + self.logger.info("Turning off") + self.provider.arlo.SpotlightOff(self.arlo_basestation, self.arlo_device) + self.on = False + + +class ArloFloodlight(ArloSpotlight): + + @ArloDeviceBase.async_print_exception_guard + async def turnOn(self) -> None: + self.logger.info("Turning on") + self.provider.arlo.FloodlightOn(self.arlo_basestation, self.arlo_device) + self.on = True + + @ArloDeviceBase.async_print_exception_guard + async def turnOff(self) -> None: + self.logger.info("Turning off") + self.provider.arlo.FloodlightOff(self.arlo_basestation, self.arlo_device) + self.on = False \ No newline at end of file From ca00387a021c366560add31523046d4025b825c0 Mon Sep 17 00:00:00 2001 From: Brett Jia Date: Sat, 25 Mar 2023 21:41:37 -0400 Subject: [PATCH 5/9] tweak video clip delete warning --- plugins/arlo/src/arlo_plugin/camera.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/arlo/src/arlo_plugin/camera.py b/plugins/arlo/src/arlo_plugin/camera.py index cfee5d97be..a7e3690e63 100644 --- a/plugins/arlo/src/arlo_plugin/camera.py +++ b/plugins/arlo/src/arlo_plugin/camera.py @@ -339,9 +339,9 @@ async def getVideoClips(self, options: VideoClipOptions = None) -> List[VideoCli clips.reverse() return clips + @ArloDeviceBase.async_print_exception_guard async def removeVideoClips(self, videoClipIds: List[str]) -> None: # Arlo does support deleting, but let's be safe and disable that - self.logger.error("deleting Arlo video clips is not implemented by this plugin") raise Exception("deleting Arlo video clips is not implemented by this plugin") async def getDevice(self, nativeId: str) -> ArloDeviceBase: From c333ee7928941c88e8f3ff63e0d7bcf297b80d6e Mon Sep 17 00:00:00 2001 From: Brett Jia Date: Sat, 25 Mar 2023 23:21:15 -0400 Subject: [PATCH 6/9] bump 0.7.5 for beta --- plugins/arlo/package-lock.json | 4 ++-- plugins/arlo/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/arlo/package-lock.json b/plugins/arlo/package-lock.json index 0dbb873e5d..ff7e40f1e1 100644 --- a/plugins/arlo/package-lock.json +++ b/plugins/arlo/package-lock.json @@ -1,12 +1,12 @@ { "name": "@scrypted/arlo", - "version": "0.7.4", + "version": "0.7.5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@scrypted/arlo", - "version": "0.7.4", + "version": "0.7.5", "devDependencies": { "@scrypted/sdk": "file:../../sdk" } diff --git a/plugins/arlo/package.json b/plugins/arlo/package.json index 7b8c4abf64..6cbe002366 100644 --- a/plugins/arlo/package.json +++ b/plugins/arlo/package.json @@ -1,6 +1,6 @@ { "name": "@scrypted/arlo", - "version": "0.7.4", + "version": "0.7.5", "description": "Arlo Plugin for Scrypted", "keywords": [ "scrypted", From 834eb7902c76b9d15426dace4cf4231edaee2146 Mon Sep 17 00:00:00 2001 From: Brett Jia Date: Mon, 27 Mar 2023 08:46:30 -0400 Subject: [PATCH 7/9] bump 0.7.6 for release + pin deps --- plugins/arlo/package-lock.json | 4 ++-- plugins/arlo/package.json | 2 +- plugins/arlo/src/requirements.txt | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/arlo/package-lock.json b/plugins/arlo/package-lock.json index ff7e40f1e1..e8d1141fb1 100644 --- a/plugins/arlo/package-lock.json +++ b/plugins/arlo/package-lock.json @@ -1,12 +1,12 @@ { "name": "@scrypted/arlo", - "version": "0.7.5", + "version": "0.7.6", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@scrypted/arlo", - "version": "0.7.5", + "version": "0.7.6", "devDependencies": { "@scrypted/sdk": "file:../../sdk" } diff --git a/plugins/arlo/package.json b/plugins/arlo/package.json index 6cbe002366..1854888ee6 100644 --- a/plugins/arlo/package.json +++ b/plugins/arlo/package.json @@ -1,6 +1,6 @@ { "name": "@scrypted/arlo", - "version": "0.7.5", + "version": "0.7.6", "description": "Arlo Plugin for Scrypted", "keywords": [ "scrypted", diff --git a/plugins/arlo/src/requirements.txt b/plugins/arlo/src/requirements.txt index d9d7138a7c..b2d360f899 100644 --- a/plugins/arlo/src/requirements.txt +++ b/plugins/arlo/src/requirements.txt @@ -1,7 +1,7 @@ paho-mqtt==1.6.1 sseclient==0.0.22 -requests -cachetools +requests==2.28.2 +cachetools==5.3.0 scrypted-arlo-go==0.0.1 --extra-index-url=https://www.piwheels.org/simple/ --extra-index-url=https://bjia56.github.io/scrypted-arlo-go/ From b39fcfa7c2af6349c778874b35d1b3b22ece5f10 Mon Sep 17 00:00:00 2001 From: Brett Jia Date: Mon, 27 Mar 2023 17:18:39 -0400 Subject: [PATCH 8/9] expose sirens on supported cameras --- .../arlo/src/arlo_plugin/arlo/arlo_async.py | 30 ++++++++- plugins/arlo/src/arlo_plugin/basestation.py | 20 +++--- plugins/arlo/src/arlo_plugin/camera.py | 64 ++++++++++++++++--- plugins/arlo/src/arlo_plugin/provider.py | 17 +++-- plugins/arlo/src/arlo_plugin/siren.py | 14 +++- plugins/arlo/src/arlo_plugin/vss.py | 8 ++- 6 files changed, 122 insertions(+), 31 deletions(-) diff --git a/plugins/arlo/src/arlo_plugin/arlo/arlo_async.py b/plugins/arlo/src/arlo_plugin/arlo/arlo_async.py index b7309267cb..1a7fc41b6f 100644 --- a/plugins/arlo/src/arlo_plugin/arlo/arlo_async.py +++ b/plugins/arlo/src/arlo_plugin/arlo/arlo_async.py @@ -711,7 +711,20 @@ def callback(self, event): callback, ) - def SirenOn(self, basestation): + def SirenOn(self, basestation, camera=None): + if camera is not None: + resource = f"siren/{camera.get('deviceId')}" + return self.Notify(basestation, { + "action": "set", + "resource": resource, + "publishResponse": True, + "properties": { + "sirenState": "on", + "duration": 300, + "volume": 8, + "pattern": "alarm" + } + }) return self.Notify(basestation, { "action": "set", "resource": "siren", @@ -724,7 +737,20 @@ def SirenOn(self, basestation): } }) - def SirenOff(self, basestation): + def SirenOff(self, basestation, camera=None): + if camera is not None: + resource = f"siren/{camera.get('deviceId')}" + return self.Notify(basestation, { + "action": "set", + "resource": resource, + "publishResponse": True, + "properties": { + "sirenState": "off", + "duration": 300, + "volume": 8, + "pattern": "alarm" + } + }) return self.Notify(basestation, { "action": "set", "resource": "siren", diff --git a/plugins/arlo/src/arlo_plugin/basestation.py b/plugins/arlo/src/arlo_plugin/basestation.py index f6893ce201..dd12b4c199 100644 --- a/plugins/arlo/src/arlo_plugin/basestation.py +++ b/plugins/arlo/src/arlo_plugin/basestation.py @@ -24,6 +24,10 @@ class ArloBasestation(ArloDeviceBase, DeviceProvider): def __init__(self, nativeId: str, arlo_basestation: dict, provider: ArloProvider) -> None: super().__init__(nativeId=nativeId, arlo_device=arlo_basestation, arlo_basestation=arlo_basestation, provider=provider) + @property + def has_siren(self) -> bool: + return any([self.arlo_device["modelId"].lower().startswith(model) for model in ArloBasestation.MODELS_WITH_SIRENS]) + def get_applicable_interfaces(self) -> List[str]: return [ScryptedInterface.DeviceProvider.value] @@ -31,12 +35,11 @@ def get_device_type(self) -> str: return ScryptedDeviceType.DeviceProvider.value def get_builtin_child_device_manifests(self) -> List[Device]: - if not any([self.arlo_device['modelId'].lower().startswith(model) for model in ArloBasestation.MODELS_WITH_SIRENS]): + if not self.has_siren: # this basestation has no builtin siren, so no manifests to return return [] - vss_id = f'{self.arlo_device["deviceId"]}.vss' - vss = self.get_or_create_vss(vss_id) + vss = self.get_or_create_vss() return [ { "info": { @@ -45,7 +48,7 @@ def get_builtin_child_device_manifests(self) -> List[Device]: "firmware": self.arlo_device.get("firmwareVersion"), "serialNumber": self.arlo_device["deviceId"], }, - "nativeId": vss_id, + "nativeId": vss.nativeId, "name": f'{self.arlo_device["deviceName"]} Siren Virtual Security System', "interfaces": vss.get_applicable_interfaces(), "type": vss.get_device_type(), @@ -57,11 +60,12 @@ async def getDevice(self, nativeId: str) -> ScryptedDeviceBase: if not nativeId.startswith(self.nativeId): # must be a camera, so get it from the provider return await self.provider.getDevice(nativeId) - return self.get_or_create_vss(nativeId) - - def get_or_create_vss(self, nativeId: str) -> ArloSirenVirtualSecuritySystem: if not nativeId.endswith("vss"): return None + return self.get_or_create_vss() + + def get_or_create_vss(self) -> ArloSirenVirtualSecuritySystem: + vss_id = f'{self.arlo_device["deviceId"]}.vss' if not self.vss: - self.vss = ArloSirenVirtualSecuritySystem(nativeId, self.arlo_device, self.arlo_basestation, self.provider) + self.vss = ArloSirenVirtualSecuritySystem(vss_id, self.arlo_device, self.arlo_basestation, self.provider, self) return self.vss \ No newline at end of file diff --git a/plugins/arlo/src/arlo_plugin/camera.py b/plugins/arlo/src/arlo_plugin/camera.py index a7e3690e63..4ddb015a4e 100644 --- a/plugins/arlo/src/arlo_plugin/camera.py +++ b/plugins/arlo/src/arlo_plugin/camera.py @@ -14,6 +14,7 @@ from .base import ArloDeviceBase from .spotlight import ArloSpotlight, ArloFloodlight +from .vss import ArloSirenVirtualSecuritySystem from .child_process import HeartbeatChildProcess from .util import BackgroundTaskMixin @@ -36,11 +37,29 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider, MODELS_WITH_FLOODLIGHTS = ["fb1001"] - MODELS_WITH_SIRENS = [] + MODELS_WITH_SIRENS = [ + "vmb4000", + "vmb4500", + "vmb4540", + "vmb5000", + "vmc4040p", + "fb1001", + "vmc2030", + "vmc2020", + "vmc2032", + "vmc4041p", + "vmc4050p", + "vmc5040", + "vml2030", + "vmc4030", + "vml4030", + "vmc4030p", + ] timeout: int = 30 intercom_session = None light: ArloSpotlight = None + vss: ArloSirenVirtualSecuritySystem = None def __init__(self, nativeId: str, arlo_device: dict, arlo_basestation: dict, provider: ArloProvider) -> None: super().__init__(nativeId=nativeId, arlo_device=arlo_device, arlo_basestation=arlo_basestation, provider=provider) @@ -98,9 +117,25 @@ def get_device_type(self) -> str: return ScryptedDeviceType.Camera.value def get_builtin_child_device_manifests(self) -> List[Device]: + results = [] if self.has_spotlight or self.has_floodlight: light = self.get_or_create_spotlight_or_floodlight() - return [ + results.append({ + "info": { + "model": f"{self.arlo_device['modelId']} {self.arlo_device['properties'].get('hwVersion', '')}".strip(), + "manufacturer": "Arlo", + "firmware": self.arlo_device.get("firmwareVersion"), + "serialNumber": self.arlo_device["deviceId"], + }, + "nativeId": light.nativeId, + "name": f'{self.arlo_device["deviceName"]} {"Spotlight" if self.has_spotlight else "Floodlight"}', + "interfaces": light.get_applicable_interfaces(), + "type": light.get_device_type(), + "providerNativeId": self.nativeId, + }) + if self.has_siren: + vss = self.get_or_create_vss() + results.extend([ { "info": { "model": f"{self.arlo_device['modelId']} {self.arlo_device['properties'].get('hwVersion', '')}".strip(), @@ -108,14 +143,14 @@ def get_builtin_child_device_manifests(self) -> List[Device]: "firmware": self.arlo_device.get("firmwareVersion"), "serialNumber": self.arlo_device["deviceId"], }, - "nativeId": light.nativeId, - "name": f'{self.arlo_device["deviceName"]} {"Spotlight" if self.has_spotlight else "Floodlight"}', - "interfaces": light.get_applicable_interfaces(), - "type": light.get_device_type(), + "nativeId": vss.nativeId, + "name": f'{self.arlo_device["deviceName"]} Siren Virtual Security System', + "interfaces": vss.get_applicable_interfaces(), + "type": vss.get_device_type(), "providerNativeId": self.nativeId, - } - ] - return [] + }, + ] + vss.get_builtin_child_device_manifests()) + return results @property def webrtc_emulation(self) -> bool: @@ -174,7 +209,7 @@ async def getSettings(self) -> List[Setting]: async def putSetting(self, key, value) -> None: if key in ["webrtc_emulation", "two_way_audio"]: self.storage.setItem(key, value == "true") - await self.provider.discoverDevices() + await self.provider.discover_devices() async def getPictureOptions(self) -> List[ResponsePictureOptions]: return [] @@ -347,6 +382,8 @@ async def removeVideoClips(self, videoClipIds: List[str]) -> None: async def getDevice(self, nativeId: str) -> ArloDeviceBase: if (nativeId.endswith("spotlight") and self.has_spotlight) or (nativeId.endswith("floodlight") and self.has_floodlight): return self.get_or_create_spotlight_or_floodlight() + if nativeId.endswith("vss") and self.has_siren: + return self.get_or_create_vss() return None def get_or_create_spotlight_or_floodlight(self) -> ArloSpotlight: @@ -360,6 +397,13 @@ def get_or_create_spotlight_or_floodlight(self) -> ArloSpotlight: self.light = ArloFloodlight(light_id, self.arlo_device, self.arlo_basestation, self.provider, self) return self.light + def get_or_create_vss(self) -> ArloSirenVirtualSecuritySystem: + if self.has_siren: + vss_id = f'{self.arlo_device["deviceId"]}.vss' + if not self.vss: + self.vss = ArloSirenVirtualSecuritySystem(vss_id, self.arlo_device, self.arlo_basestation, self.provider, self) + return self.vss + class ArloCameraRTCSignalingSession(BackgroundTaskMixin): def __init__(self, camera): diff --git a/plugins/arlo/src/arlo_plugin/provider.py b/plugins/arlo/src/arlo_plugin/provider.py index a56fcfd023..535ba21876 100644 --- a/plugins/arlo/src/arlo_plugin/provider.py +++ b/plugins/arlo/src/arlo_plugin/provider.py @@ -10,7 +10,7 @@ import scrypted_sdk from scrypted_sdk import ScryptedDeviceBase -from scrypted_sdk.types import Setting, SettingValue, Settings, DeviceProvider, DeviceDiscovery, ScryptedInterface +from scrypted_sdk.types import Setting, SettingValue, Settings, DeviceProvider, ScryptedInterface from .arlo import Arlo from .arlo.arlo_async import change_stream_class @@ -23,7 +23,7 @@ from .base import ArloDeviceBase -class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, DeviceDiscovery, ScryptedDeviceLoggerMixin, BackgroundTaskMixin): +class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, ScryptedDeviceLoggerMixin, BackgroundTaskMixin): arlo_cameras = None arlo_basestations = None _arlo_mfa_code = None @@ -188,7 +188,7 @@ def arlo(self) -> Arlo: async def do_arlo_setup(self) -> None: try: - await self.discoverDevices() + await self.discover_devices() await self.arlo.Subscribe([ (self.arlo_basestations[camera["parentId"]], camera) for camera in self.arlo_cameras.values() ]) @@ -559,7 +559,7 @@ def validate_setting(self, key: str, val: SettingValue) -> bool: return True @ArloDeviceBase.async_print_exception_guard - async def discoverDevices(self, duration: int = 0) -> None: + async def discover_devices(self, duration: int = 0) -> None: if not self.arlo: raise Exception("Arlo client not connected, cannot discover devices") @@ -574,6 +574,7 @@ async def discoverDevices(self, duration: int = 0) -> None: basestations = self.arlo.GetDevices(['basestation', 'siren']) for basestation in basestations: nativeId = basestation["deviceId"] + self.logger.debug(f"Adding {nativeId}") if nativeId in self.arlo_basestations: self.logger.info(f"Skipping basestation {nativeId} ({basestation['modelId']}) as it has already been added") @@ -583,7 +584,7 @@ async def discoverDevices(self, duration: int = 0) -> None: device = await self.getDevice(nativeId) scrypted_interfaces = device.get_applicable_interfaces() manifest = device.get_device_manifest() - self.logger.info(f"Interfaces for {nativeId} ({basestation['modelId']}): {scrypted_interfaces}") + self.logger.debug(f"Interfaces for {nativeId} ({basestation['modelId']}): {scrypted_interfaces}") # for basestations, we want to add them to the top level DeviceProvider provider_to_device_map.setdefault(None, []).append(manifest) @@ -602,11 +603,13 @@ async def discoverDevices(self, duration: int = 0) -> None: cameras = self.arlo.GetDevices(['camera', "arloq", "arloqs", "doorbell"]) for camera in cameras: + nativeId = camera["deviceId"] + self.logger.debug(f"Adding {nativeId}") + if camera["deviceId"] != camera["parentId"] and camera["parentId"] not in self.arlo_basestations: self.logger.info(f"Skipping camera {camera['deviceId']} ({camera['modelId']}) because its basestation was not found") continue - nativeId = camera["deviceId"] if nativeId in self.arlo_cameras: self.logger.info(f"Skipping camera {nativeId} ({camera['modelId']}) as it has already been added") continue @@ -620,7 +623,7 @@ async def discoverDevices(self, duration: int = 0) -> None: device: ArloDeviceBase = await self.getDevice(nativeId) scrypted_interfaces = device.get_applicable_interfaces() manifest = device.get_device_manifest() - self.logger.info(f"Interfaces for {nativeId} ({camera['modelId']}): {scrypted_interfaces}") + self.logger.debug(f"Interfaces for {nativeId} ({camera['modelId']}): {scrypted_interfaces}") if camera["deviceId"] == camera["parentId"]: provider_to_device_map.setdefault(None, []).append(manifest) diff --git a/plugins/arlo/src/arlo_plugin/siren.py b/plugins/arlo/src/arlo_plugin/siren.py index fabe411eb4..8534c2b3cb 100644 --- a/plugins/arlo/src/arlo_plugin/siren.py +++ b/plugins/arlo/src/arlo_plugin/siren.py @@ -27,6 +27,7 @@ def get_device_type(self) -> str: @ArloDeviceBase.async_print_exception_guard async def turnOn(self) -> None: + from .basestation import ArloBasestation self.logger.info("Turning on") if self.vss.securitySystemState["mode"] == SecuritySystemMode.Disarmed.value: @@ -42,7 +43,12 @@ async def turnOn(self) -> None: } return - self.provider.arlo.SirenOn(self.arlo_device) + if isinstance(self.vss.parent, ArloBasestation): + self.logger.debug("Parent device is a basestation") + self.provider.arlo.SirenOn(self.arlo_basestation) + else: + self.logger.debug("Parent device is a camera") + self.provider.arlo.SirenOn(self.arlo_basestation, self.arlo_device) self.on = True self.vss.securitySystemState = { @@ -52,8 +58,12 @@ async def turnOn(self) -> None: @ArloDeviceBase.async_print_exception_guard async def turnOff(self) -> None: + from .basestation import ArloBasestation self.logger.info("Turning off") - self.provider.arlo.SirenOff(self.arlo_device) + if isinstance(self.vss.parent, ArloBasestation): + self.provider.arlo.SirenOff(self.arlo_basestation) + else: + self.provider.arlo.SirenOff(self.arlo_basestation, self.arlo_device) self.on = False self.vss.securitySystemState = { **self.vss.securitySystemState, diff --git a/plugins/arlo/src/arlo_plugin/vss.py b/plugins/arlo/src/arlo_plugin/vss.py index f173ea18a7..37e60bcce0 100644 --- a/plugins/arlo/src/arlo_plugin/vss.py +++ b/plugins/arlo/src/arlo_plugin/vss.py @@ -11,6 +11,8 @@ if TYPE_CHECKING: # https://adamj.eu/tech/2021/05/13/python-type-hints-how-to-fix-circular-imports/ from .provider import ArloProvider + from .basestation import ArloBasestation + from .camera import ArloCamera class ArloSirenVirtualSecuritySystem(ArloDeviceBase, SecuritySystem, DeviceProvider): @@ -19,9 +21,11 @@ class ArloSirenVirtualSecuritySystem(ArloDeviceBase, SecuritySystem, DeviceProvi SUPPORTED_MODES = [SecuritySystemMode.AwayArmed.value, SecuritySystemMode.HomeArmed.value, SecuritySystemMode.Disarmed.value] siren: ArloSiren = None + parent: ArloBasestation | ArloCamera = None - def __init__(self, nativeId: str, arlo_device: dict, arlo_basestation: dict, provider: ArloProvider) -> None: + def __init__(self, nativeId: str, arlo_device: dict, arlo_basestation: dict, provider: ArloProvider, parent: ArloBasestation | ArloCamera) -> None: super().__init__(nativeId=nativeId, arlo_device=arlo_device, arlo_basestation=arlo_basestation, provider=provider) + self.parent = parent self.create_task(self.delayed_init()) @property @@ -56,7 +60,7 @@ async def delayed_init(self) -> None: } return except Exception as e: - self.logger.info(f"Delayed init failed, will try again: {e}") + self.logger.debug(f"Delayed init failed, will try again: {e}") await asyncio.sleep(0.1) iterations += 1 From dc3d0544e928312c352a9184933bf7c310d91a6f Mon Sep 17 00:00:00 2001 From: Brett Jia Date: Mon, 27 Mar 2023 17:35:26 -0400 Subject: [PATCH 9/9] bump 0.7.7 for release --- plugins/arlo/package-lock.json | 4 ++-- plugins/arlo/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/arlo/package-lock.json b/plugins/arlo/package-lock.json index e8d1141fb1..cb3c291c17 100644 --- a/plugins/arlo/package-lock.json +++ b/plugins/arlo/package-lock.json @@ -1,12 +1,12 @@ { "name": "@scrypted/arlo", - "version": "0.7.6", + "version": "0.7.7", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@scrypted/arlo", - "version": "0.7.6", + "version": "0.7.7", "devDependencies": { "@scrypted/sdk": "file:../../sdk" } diff --git a/plugins/arlo/package.json b/plugins/arlo/package.json index 1854888ee6..2f408198f6 100644 --- a/plugins/arlo/package.json +++ b/plugins/arlo/package.json @@ -1,6 +1,6 @@ { "name": "@scrypted/arlo", - "version": "0.7.6", + "version": "0.7.7", "description": "Arlo Plugin for Scrypted", "keywords": [ "scrypted",