diff --git a/plugins/arlo/package-lock.json b/plugins/arlo/package-lock.json index cb3c291c1..d3df95d16 100644 --- a/plugins/arlo/package-lock.json +++ b/plugins/arlo/package-lock.json @@ -1,12 +1,12 @@ { "name": "@scrypted/arlo", - "version": "0.7.7", + "version": "0.7.12", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@scrypted/arlo", - "version": "0.7.7", + "version": "0.7.12", "devDependencies": { "@scrypted/sdk": "file:../../sdk" } diff --git a/plugins/arlo/package.json b/plugins/arlo/package.json index 2f408198f..55b3fd96f 100644 --- a/plugins/arlo/package.json +++ b/plugins/arlo/package.json @@ -1,6 +1,6 @@ { "name": "@scrypted/arlo", - "version": "0.7.7", + "version": "0.7.12", "description": "Arlo Plugin for Scrypted", "keywords": [ "scrypted", diff --git a/plugins/arlo/src/arlo_plugin/arlo/arlo_async.py b/plugins/arlo/src/arlo_plugin/arlo/arlo_async.py index 1a7fc41b6..1ce7c9921 100644 --- a/plugins/arlo/src/arlo_plugin/arlo/arlo_async.py +++ b/plugins/arlo/src/arlo_plugin/arlo/arlo_async.py @@ -383,6 +383,33 @@ class Arlo(object): self.HandleEvents(basestation, resource, [('is', 'motionDetected')], callbackwrapper) ) + def SubscribeToAudioEvents(self, basestation, camera, callback): + """ + Use this method to subscribe to audio events. You must provide a callback function which will get called once per audio event. + + The callback function should have the following signature: + def callback(self, event) + + This is an example of handling a specific event, in reality, you'd probably want to write a callback for HandleEvents() + that has a big switch statement in it to handle all the various events Arlo produces. + + Returns the Task object that contains the subscription loop. + """ + resource = f"cameras/{camera.get('deviceId')}" + + def callbackwrapper(self, event): + properties = event.get('properties', {}) + stop = None + if 'audioDetected' in properties: + stop = callback(properties['audioDetected']) + if not stop: + return None + return stop + + return asyncio.get_event_loop().create_task( + self.HandleEvents(basestation, resource, [('is', 'audioDetected')], callbackwrapper) + ) + def SubscribeToBatteryEvents(self, basestation, camera, callback): """ Use this method to subscribe to battery events. You must provide a callback function which will get called once per battery event. @@ -864,11 +891,11 @@ class Arlo(object): } ) - def GetSmartFeatures(self, device): + def GetSmartFeatures(self, device) -> dict: smart_features = self._getSmartFeaturesCached() key = f"{device['owner']['ownerId']}_{device['deviceId']}" - return smart_features["features"].get(key) + return smart_features["features"].get(key, {}) @cached(cache=TTLCache(maxsize=1, ttl=60)) - def _getSmartFeaturesCached(self): + def _getSmartFeaturesCached(self) -> dict: 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/base.py b/plugins/arlo/src/arlo_plugin/base.py index cb2032418..abfe1008a 100644 --- a/plugins/arlo/src/arlo_plugin/base.py +++ b/plugins/arlo/src/arlo_plugin/base.py @@ -66,15 +66,4 @@ class ArloDeviceBase(ScryptedDeviceBase, ScryptedDeviceLoggerMixin, BackgroundTa def get_builtin_child_device_manifests(self) -> List[Device]: """Returns the list of child device manifests representing hardware features built into this device.""" - return [] - - @classmethod - def async_print_exception_guard(self, fn): - """Decorator to print an exception's stack trace before re-raising the exception.""" - async def wrapped(*args, **kwargs): - try: - return await fn(*args, **kwargs) - except Exception: - traceback.print_exc() - raise - return wrapped \ No newline at end of file + return [] \ 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 4ddb015a4..9221c6ad2 100644 --- a/plugins/arlo/src/arlo_plugin/camera.py +++ b/plugins/arlo/src/arlo_plugin/camera.py @@ -10,20 +10,20 @@ from typing import List, TYPE_CHECKING import scrypted_arlo_go import scrypted_sdk -from scrypted_sdk.types import Setting, Settings, Device, 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, AudioSensor, Battery, DeviceProvider, MediaObject, ResponsePictureOptions, ResponseMediaStreamOptions, ScryptedMimeTypes, ScryptedInterface, ScryptedDeviceType from .base import ArloDeviceBase from .spotlight import ArloSpotlight, ArloFloodlight from .vss import ArloSirenVirtualSecuritySystem from .child_process import HeartbeatChildProcess -from .util import BackgroundTaskMixin +from .util import BackgroundTaskMixin, async_print_exception_guard if TYPE_CHECKING: # https://adamj.eu/tech/2021/05/13/python-type-hints-how-to-fix-circular-imports/ from .provider import ArloProvider -class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider, VideoClips, MotionSensor, Battery): +class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider, VideoClips, MotionSensor, AudioSensor, Battery): MODELS_WITH_SPOTLIGHTS = [ "vmc4040p", "vmc2030", @@ -38,10 +38,6 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider, MODELS_WITH_FLOODLIGHTS = ["fb1001"] MODELS_WITH_SIRENS = [ - "vmb4000", - "vmb4500", - "vmb4540", - "vmb5000", "vmc4040p", "fb1001", "vmc2030", @@ -56,6 +52,25 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider, "vmc4030p", ] + MODELS_WITH_AUDIO_SENSORS = [ + "vmc4040p", + "fb1001", + "vmc4041p", + "vmc4050p", + "vmc5040", + "vmc3040", + "vmc3040s", + "vmc4030", + "vml4030", + "vmc4030p", + ] + + MODELS_WITHOUT_BATTERY = [ + "avd1001", + "vmc3040", + "vmc3040s", + ] + timeout: int = 30 intercom_session = None light: ArloSpotlight = None @@ -64,6 +79,7 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider, 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) self.start_motion_subscription() + self.start_audio_subscription() self.start_battery_subscription() def start_motion_subscription(self) -> None: @@ -75,7 +91,22 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider, self.provider.arlo.SubscribeToMotionEvents(self.arlo_basestation, self.arlo_device, callback) ) + def start_audio_subscription(self) -> None: + if not self.has_audio_sensor: + return + + def callback(audioDetected): + self.audioDetected = audioDetected + return self.stop_subscriptions + + self.register_task( + self.provider.arlo.SubscribeToAudioEvents(self.arlo_basestation, self.arlo_device, callback) + ) + def start_battery_subscription(self) -> None: + if self.wired_to_power: + return + def callback(batteryLevel): self.batteryLevel = batteryLevel return self.stop_subscriptions @@ -89,7 +120,6 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider, ScryptedInterface.VideoCamera.value, ScryptedInterface.Camera.value, ScryptedInterface.MotionSensor.value, - ScryptedInterface.Battery.value, ScryptedInterface.Settings.value, ]) @@ -101,9 +131,18 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider, results.add(ScryptedInterface.RTCSignalingChannel.value) results.discard(ScryptedInterface.Intercom.value) + if self.has_battery: + results.add(ScryptedInterface.Battery.value) + + if self.wired_to_power: + results.discard(ScryptedInterface.Battery.value) + if self.has_siren or self.has_spotlight or self.has_floodlight: results.add(ScryptedInterface.DeviceProvider.value) + if self.has_audio_sensor: + results.add(ScryptedInterface.AudioSensor.value) + if self.has_cloud_recording: results.add(ScryptedInterface.VideoClips.value) @@ -169,9 +208,16 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider, else: return True + @property + def wired_to_power(self) -> bool: + if self.storage: + return True if self.storage.getItem("wired_to_power") else False + else: + return False + @property def has_cloud_recording(self) -> bool: - return self.provider.arlo.GetSmartFeatures(self.arlo_device)["planFeatures"]["eventRecording"] + return self.provider.arlo.GetSmartFeatures(self.arlo_device).get("planFeatures", {}).get("eventRecording", False) @property def has_spotlight(self) -> bool: @@ -185,9 +231,30 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider, def has_siren(self) -> bool: return any([self.arlo_device["modelId"].lower().startswith(model) for model in ArloCamera.MODELS_WITH_SIRENS]) + @property + def has_audio_sensor(self) -> bool: + return any([self.arlo_device["modelId"].lower().startswith(model) for model in ArloCamera.MODELS_WITH_AUDIO_SENSORS]) + + @property + def has_battery(self) -> bool: + return not any([self.arlo_device["modelId"].lower().startswith(model) for model in ArloCamera.MODELS_WITHOUT_BATTERY]) + async def getSettings(self) -> List[Setting]: + result = [] + if self.has_battery: + result.append( + { + "key": "wired_to_power", + "title": "Plugged In to External Power", + "value": self.wired_to_power, + "description": "Informs Scrypted that this device is plugged in to an external power source. " + \ + "Will allow features like persistent prebuffer to work, however will no longer report this device's battery percentage. " + \ + "Note that a persistent prebuffer may cause excess battery drain if the external power is not able to charge faster than the battery consumption rate.", + "type": "boolean", + }, + ) if self._can_push_to_talk(): - return [ + result.extend([ { "key": "two_way_audio", "title": "(Experimental) Enable native two-way audio", @@ -203,17 +270,18 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider, "If enabled, takes precedence over native two-way audio. May use increased system resources.", "type": "boolean", }, - ] - return [] + ]) + return result async def putSetting(self, key, value) -> None: - if key in ["webrtc_emulation", "two_way_audio"]: + if key in ["webrtc_emulation", "two_way_audio", "wired_to_power"]: self.storage.setItem(key, value == "true") await self.provider.discover_devices() async def getPictureOptions(self) -> List[ResponsePictureOptions]: return [] + @async_print_exception_guard async def takePicture(self, options: dict = None) -> MediaObject: self.logger.info("Taking picture") @@ -221,7 +289,11 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider, msos = await real_device.getVideoStreamOptions() if any(["prebuffer" in m for m in msos]): self.logger.info("Getting snapshot from prebuffer") - return await real_device.getVideoStream({"refresh": False}) + try: + return await real_device.getVideoStream({"refresh": False}) + except Exception as e: + self.logger.warning(f"Could not fetch from prebuffer due to: {e}") + self.logger.warning("Will try to fetch snapshot from Arlo cloud") pic_url = await asyncio.wait_for(self.provider.arlo.TriggerFullFrameSnapshot(self.arlo_basestation, self.arlo_device), timeout=self.timeout) self.logger.debug(f"Got snapshot URL for at {pic_url}") @@ -273,32 +345,30 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider, } return await scrypted_sdk.mediaManager.createFFmpegMediaObject(ffmpeg_input) + @async_print_exception_guard async def startRTCSignalingSession(self, scrypted_session): - try: - plugin_session = ArloCameraRTCSignalingSession(self) - await plugin_session.initialize() + plugin_session = ArloCameraRTCSignalingSession(self) + await plugin_session.initialize() - scrypted_setup = { - "type": "offer", - "audio": { - "direction": "sendrecv" if self._can_push_to_talk() else "recvonly", - }, - "video": { - "direction": "recvonly", - } + scrypted_setup = { + "type": "offer", + "audio": { + "direction": "sendrecv" if self._can_push_to_talk() else "recvonly", + }, + "video": { + "direction": "recvonly", } - plugin_setup = {} + } + plugin_setup = {} - scrypted_offer = await scrypted_session.createLocalDescription("offer", scrypted_setup, sendIceCandidate=plugin_session.addIceCandidate) - self.logger.info(f"Scrypted offer sdp:\n{scrypted_offer['sdp']}") - await plugin_session.setRemoteDescription(scrypted_offer, plugin_setup) - plugin_answer = await plugin_session.createLocalDescription("answer", plugin_setup, scrypted_session.sendIceCandidate) - self.logger.info(f"Scrypted answer sdp:\n{plugin_answer['sdp']}") - await scrypted_session.setRemoteDescription(plugin_answer, scrypted_setup) + scrypted_offer = await scrypted_session.createLocalDescription("offer", scrypted_setup, sendIceCandidate=plugin_session.addIceCandidate) + self.logger.info(f"Scrypted offer sdp:\n{scrypted_offer['sdp']}") + await plugin_session.setRemoteDescription(scrypted_offer, plugin_setup) + plugin_answer = await plugin_session.createLocalDescription("answer", plugin_setup, scrypted_session.sendIceCandidate) + self.logger.info(f"Scrypted answer sdp:\n{plugin_answer['sdp']}") + await scrypted_session.setRemoteDescription(plugin_answer, scrypted_setup) - return ArloCameraRTCSessionControl(plugin_session) - except Exception as e: - self.logger.error(e) + return ArloCameraRTCSessionControl(plugin_session) async def startIntercom(self, media) -> None: self.logger.info("Starting intercom") @@ -374,7 +444,7 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider, clips.reverse() return clips - @ArloDeviceBase.async_print_exception_guard + @async_print_exception_guard async def removeVideoClips(self, videoClipIds: List[str]) -> None: # Arlo does support deleting, but let's be safe and disable that raise Exception("deleting Arlo video clips is not implemented by this plugin") diff --git a/plugins/arlo/src/arlo_plugin/doorbell.py b/plugins/arlo/src/arlo_plugin/doorbell.py index db2a0f1ac..d4f812f93 100644 --- a/plugins/arlo/src/arlo_plugin/doorbell.py +++ b/plugins/arlo/src/arlo_plugin/doorbell.py @@ -31,8 +31,4 @@ class ArloDoorbell(ArloCamera, BinarySensor): def get_applicable_interfaces(self) -> List[str]: camera_interfaces = super().get_applicable_interfaces() camera_interfaces.append(ScryptedInterface.BinarySensor.value) - - model_id = self.arlo_device['modelId'].lower() - if model_id.startswith("avd1001"): - camera_interfaces.remove(ScryptedInterface.Battery.value) return camera_interfaces diff --git a/plugins/arlo/src/arlo_plugin/provider.py b/plugins/arlo/src/arlo_plugin/provider.py index 535ba2187..74f1cab57 100644 --- a/plugins/arlo/src/arlo_plugin/provider.py +++ b/plugins/arlo/src/arlo_plugin/provider.py @@ -16,7 +16,7 @@ from .arlo import Arlo from .arlo.arlo_async import change_stream_class from .arlo.logging import logger as arlo_lib_logger from .logging import ScryptedDeviceLoggerMixin -from .util import BackgroundTaskMixin +from .util import BackgroundTaskMixin, async_print_exception_guard from .camera import ArloCamera from .doorbell import ArloDoorbell from .basestation import ArloBasestation @@ -30,6 +30,7 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, ScryptedDeviceL scrypted_devices = None _arlo = None _arlo_mfa_complete_auth = None + device_discovery_lock: asyncio.Lock = None plugin_verbosity_choices = { "Normal": logging.INFO, @@ -50,6 +51,7 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, ScryptedDeviceL self.imap = None self.imap_signal = None self.imap_skip_emails = None + self.device_discovery_lock = asyncio.Lock() self.propagate_verbosity() self.propagate_transport() @@ -193,9 +195,6 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, ScryptedDeviceL (self.arlo_basestations[camera["parentId"]], camera) for camera in self.arlo_cameras.values() ]) - for nativeId in self.arlo_cameras.keys(): - await self.getDevice(nativeId) - self.arlo.event_stream.set_refresh_interval(self.refresh_interval) except requests.exceptions.HTTPError as e: traceback.print_exc() @@ -558,8 +557,12 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, ScryptedDeviceL return False return True - @ArloDeviceBase.async_print_exception_guard - async def discover_devices(self, duration: int = 0) -> None: + @async_print_exception_guard + async def discover_devices(self) -> None: + async with self.device_discovery_lock: + return await self.discover_devices_impl() + + async def discover_devices_impl(self) -> None: if not self.arlo: raise Exception("Arlo client not connected, cannot discover devices") @@ -581,7 +584,7 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, ScryptedDeviceL continue self.arlo_basestations[nativeId] = basestation - device = await self.getDevice(nativeId) + device = await self.getDevice_impl(nativeId) scrypted_interfaces = device.get_applicable_interfaces() manifest = device.get_device_manifest() self.logger.debug(f"Interfaces for {nativeId} ({basestation['modelId']}): {scrypted_interfaces}") @@ -620,7 +623,7 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, ScryptedDeviceL # own basestation self.arlo_basestations[camera["deviceId"]] = camera - device: ArloDeviceBase = await self.getDevice(nativeId) + device = await self.getDevice_impl(nativeId) scrypted_interfaces = device.get_applicable_interfaces() manifest = device.get_device_manifest() self.logger.debug(f"Interfaces for {nativeId} ({camera['modelId']}): {scrypted_interfaces}") @@ -660,6 +663,10 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, ScryptedDeviceL }) async def getDevice(self, nativeId: str) -> ArloDeviceBase: + async with self.device_discovery_lock: + return await self.getDevice_impl(nativeId) + + async def getDevice_impl(self, nativeId: str) -> ArloDeviceBase: ret = self.scrypted_devices.get(nativeId, None) if ret is None: ret = self.create_device(nativeId) diff --git a/plugins/arlo/src/arlo_plugin/siren.py b/plugins/arlo/src/arlo_plugin/siren.py index 8534c2b3c..a1c0e9e97 100644 --- a/plugins/arlo/src/arlo_plugin/siren.py +++ b/plugins/arlo/src/arlo_plugin/siren.py @@ -5,6 +5,7 @@ from typing import List, TYPE_CHECKING from scrypted_sdk.types import OnOff, SecuritySystemMode, ScryptedInterface, ScryptedDeviceType from .base import ArloDeviceBase +from .util import async_print_exception_guard if TYPE_CHECKING: # https://adamj.eu/tech/2021/05/13/python-type-hints-how-to-fix-circular-imports/ @@ -25,7 +26,7 @@ class ArloSiren(ArloDeviceBase, OnOff): def get_device_type(self) -> str: return ScryptedDeviceType.Siren.value - @ArloDeviceBase.async_print_exception_guard + @async_print_exception_guard async def turnOn(self) -> None: from .basestation import ArloBasestation self.logger.info("Turning on") @@ -56,7 +57,7 @@ class ArloSiren(ArloDeviceBase, OnOff): "triggered": True, } - @ArloDeviceBase.async_print_exception_guard + @async_print_exception_guard async def turnOff(self) -> None: from .basestation import ArloBasestation self.logger.info("Turning off") diff --git a/plugins/arlo/src/arlo_plugin/spotlight.py b/plugins/arlo/src/arlo_plugin/spotlight.py index 47604d1a6..b9511de9f 100644 --- a/plugins/arlo/src/arlo_plugin/spotlight.py +++ b/plugins/arlo/src/arlo_plugin/spotlight.py @@ -5,6 +5,7 @@ from typing import List, TYPE_CHECKING from scrypted_sdk.types import OnOff, ScryptedInterface, ScryptedDeviceType from .base import ArloDeviceBase +from .util import async_print_exception_guard if TYPE_CHECKING: # https://adamj.eu/tech/2021/05/13/python-type-hints-how-to-fix-circular-imports/ @@ -25,13 +26,13 @@ class ArloSpotlight(ArloDeviceBase, OnOff): def get_device_type(self) -> str: return ScryptedDeviceType.Light.value - @ArloDeviceBase.async_print_exception_guard + @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_print_exception_guard async def turnOff(self) -> None: self.logger.info("Turning off") self.provider.arlo.SpotlightOff(self.arlo_basestation, self.arlo_device) @@ -40,13 +41,13 @@ class ArloSpotlight(ArloDeviceBase, OnOff): class ArloFloodlight(ArloSpotlight): - @ArloDeviceBase.async_print_exception_guard + @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_print_exception_guard async def turnOff(self) -> None: self.logger.info("Turning off") self.provider.arlo.FloodlightOff(self.arlo_basestation, self.arlo_device) diff --git a/plugins/arlo/src/arlo_plugin/util.py b/plugins/arlo/src/arlo_plugin/util.py index 1ac329e10..d33ab9e1a 100644 --- a/plugins/arlo/src/arlo_plugin/util.py +++ b/plugins/arlo/src/arlo_plugin/util.py @@ -1,4 +1,5 @@ import asyncio +import traceback class BackgroundTaskMixin: @@ -25,4 +26,14 @@ class BackgroundTaskMixin: if not hasattr(self, "background_tasks"): return for task in self.background_tasks: - task.cancel() \ No newline at end of file + task.cancel() + +def async_print_exception_guard(fn): + """Decorator to print an exception's stack trace before re-raising the exception.""" + async def wrapped(*args, **kwargs): + try: + return await fn(*args, **kwargs) + except Exception: + traceback.print_exc() + raise + return wrapped \ No newline at end of file diff --git a/plugins/arlo/src/arlo_plugin/vss.py b/plugins/arlo/src/arlo_plugin/vss.py index 37e60bcce..a6507fc7f 100644 --- a/plugins/arlo/src/arlo_plugin/vss.py +++ b/plugins/arlo/src/arlo_plugin/vss.py @@ -7,6 +7,7 @@ from scrypted_sdk.types import Device, DeviceProvider, Setting, Settings, Settin from .base import ArloDeviceBase from .siren import ArloSiren +from .util import async_print_exception_guard if TYPE_CHECKING: # https://adamj.eu/tech/2021/05/13/python-type-hints-how-to-fix-circular-imports/ @@ -15,7 +16,7 @@ if TYPE_CHECKING: from .camera import ArloCamera -class ArloSirenVirtualSecuritySystem(ArloDeviceBase, SecuritySystem, DeviceProvider): +class ArloSirenVirtualSecuritySystem(ArloDeviceBase, SecuritySystem, Settings, Readme, DeviceProvider): """A virtual, emulated security system that controls when scrypted events can trip the real physical siren.""" SUPPORTED_MODES = [SecuritySystemMode.AwayArmed.value, SecuritySystemMode.HomeArmed.value, SecuritySystemMode.Disarmed.value] @@ -133,6 +134,7 @@ If this virtual security system is synced to Homekit, the siren device will be m self.siren = ArloSiren(siren_id, self.arlo_device, self.arlo_basestation, self.provider, self) return self.siren + @async_print_exception_guard async def armSecuritySystem(self, mode: SecuritySystemMode) -> None: self.logger.info(f"Arming {mode}") self.mode = mode @@ -143,7 +145,7 @@ If this virtual security system is synced to Homekit, the siren device will be m if mode == SecuritySystemMode.Disarmed.value: await self.get_or_create_siren().turnOff() - @ArloDeviceBase.async_print_exception_guard + @async_print_exception_guard async def disarmSecuritySystem(self) -> None: self.logger.info(f"Disarming") self.mode = SecuritySystemMode.Disarmed.value