Skip to content

Commit

Permalink
arlo: add basestation and basestation siren + other tweaks (#636)
Browse files Browse the repository at this point in the history
* configure stream refresh

* use modelId directly

* bump 0.6.8

* lower refresh rate to 20 min

* basestations as DeviceProviders + various type annotations

* reorder

* trickle discover basestations to avoid clobbering cameras

* generalize device creation + start of siren

* functional basestation siren

* bump 0.7.0 for beta
  • Loading branch information
bjia56 authored Mar 18, 2023
1 parent b7b6ac0 commit 8a56e78
Show file tree
Hide file tree
Showing 10 changed files with 306 additions and 125 deletions.
6 changes: 3 additions & 3 deletions plugins/arlo/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion plugins/arlo/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@scrypted/arlo",
"version": "0.6.7",
"version": "0.7.0",
"description": "Arlo Plugin for Scrypted",
"keywords": [
"scrypted",
Expand Down
26 changes: 26 additions & 0 deletions plugins/arlo/src/arlo_plugin/arlo/arlo_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -709,3 +709,29 @@ def callback(self, event):
trigger,
callback,
)

def SirenOn(self, basestation):
return self.Notify(basestation, {
"action": "set",
"resource": "siren",
"publishResponse": True,
"properties": {
"sirenState": "on",
"duration": 300,
"volume": 8,
"pattern": "alarm"
}
})

def SirenOff(self, basestation):
return self.Notify(basestation, {
"action": "set",
"resource": "siren",
"publishResponse": True,
"properties": {
"sirenState": "off",
"duration": 300,
"volume": 8,
"pattern": "alarm"
}
})
44 changes: 44 additions & 0 deletions plugins/arlo/src/arlo_plugin/basestation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from scrypted_sdk import ScryptedDeviceBase
from scrypted_sdk.types import DeviceProvider, ScryptedInterface, ScryptedDeviceType

from .device_base import ArloDeviceBase
from .siren import ArloSiren


class ArloBasestation(ArloDeviceBase, DeviceProvider):
siren: ArloSiren = None

def get_applicable_interfaces(self) -> list:
return [ScryptedInterface.DeviceProvider.value]

def get_device_type(self) -> str:
return ScryptedDeviceType.DeviceProvider.value

def get_builtin_child_device_manifests(self) -> list:
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": f'{self.arlo_device["deviceId"]}.siren',
"name": f'{self.arlo_device["deviceName"]} Siren',
"interfaces": [ScryptedInterface.OnOff.value],
"type": ScryptedDeviceType.Siren.value,
"providerNativeId": self.nativeId,
}
]

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)

if nativeId.endswith("siren"):
if not self.siren:
self.siren = ArloSiren(nativeId, self.arlo_device, self.arlo_basestation, self.provider)
return self.siren

return None
81 changes: 40 additions & 41 deletions plugins/arlo/src/arlo_plugin/camera.py
Original file line number Diff line number Diff line change
@@ -1,47 +1,30 @@
import asyncio
import json
import threading
import time

import scrypted_arlo_go

import scrypted_sdk
from scrypted_sdk import ScryptedDeviceBase
from scrypted_sdk.types import Settings, Camera, VideoCamera, MotionSensor, Battery, ScryptedMimeTypes, ScryptedInterface
from scrypted_sdk.types import Settings, Camera, VideoCamera, MotionSensor, Battery, MediaObject, ScryptedMimeTypes, ScryptedInterface, ScryptedDeviceType

from .device_base import ArloDeviceBase
from .provider import ArloProvider
from .child_process import HeartbeatChildProcess
from .logging import ScryptedDeviceLoggerMixin
from .util import BackgroundTaskMixin


class ArloCamera(ScryptedDeviceBase, Settings, Camera, VideoCamera, MotionSensor, Battery, ScryptedDeviceLoggerMixin, BackgroundTaskMixin):
timeout = 30
nativeId = None
arlo_device = None
arlo_basestation = None
provider = None
class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, MotionSensor, Battery):
timeout: int = 30
intercom_session = None

def __init__(self, nativeId, arlo_device, arlo_basestation, provider):
super().__init__(nativeId=nativeId)
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.logger_name = nativeId

self.nativeId = nativeId
self.arlo_device = arlo_device
self.arlo_basestation = arlo_basestation
self.provider = provider
self.logger.setLevel(self.provider.get_current_log_level())

self.intercom_session = None

self.stop_subscriptions = False
self.start_motion_subscription()
self.start_battery_subscription()

def __del__(self):
self.stop_subscriptions = True
self.cancel_pending_tasks()

def start_motion_subscription(self):
def start_motion_subscription(self) -> None:
def callback(motionDetected):
self.motionDetected = motionDetected
return self.stop_subscriptions
Expand All @@ -50,7 +33,7 @@ def callback(motionDetected):
self.provider.arlo.SubscribeToMotionEvents(self.arlo_basestation, self.arlo_device, callback)
)

def start_battery_subscription(self):
def start_battery_subscription(self) -> None:
def callback(batteryLevel):
self.batteryLevel = batteryLevel
return self.stop_subscriptions
Expand Down Expand Up @@ -82,15 +65,18 @@ def get_applicable_interfaces(self) -> list:

return list(results)

def get_device_type(self) -> str:
return ScryptedDeviceType.Camera.value

@property
def webrtc_emulation(self):
def webrtc_emulation(self) -> bool:
if self.storage:
return self.storage.getItem("webrtc_emulation")
return True if self.storage.getItem("webrtc_emulation") else False
else:
return False

@property
def two_way_audio(self):
def two_way_audio(self) -> bool:
if self.storage:
val = self.storage.getItem("two_way_audio")
if val is None:
Expand All @@ -99,7 +85,7 @@ def two_way_audio(self):
else:
return True

async def getSettings(self):
async def getSettings(self) -> list:
if self._can_push_to_talk():
return [
{
Expand All @@ -120,15 +106,15 @@ async def getSettings(self):
]
return []

async def putSetting(self, key, value):
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()

async def getPictureOptions(self):
async def getPictureOptions(self) -> list:
return []

async def takePicture(self, options=None):
async def takePicture(self, options: dict = None) -> MediaObject:
self.logger.info("Taking picture")

real_device = await scrypted_sdk.systemManager.api.getDeviceById(self.getScryptedProperty("id"))
Expand All @@ -145,7 +131,7 @@ async def takePicture(self, options=None):

return await scrypted_sdk.mediaManager.createMediaObject(str.encode(pic_url), ScryptedMimeTypes.Url.value)

async def getVideoStreamOptions(self):
async def getVideoStreamOptions(self) -> list:
return [
{
"id": 'default',
Expand All @@ -163,16 +149,29 @@ async def getVideoStreamOptions(self):
}
]

async def _getVideoStreamURL(self):
async def _getVideoStreamURL(self) -> str:
self.logger.info("Requesting stream")
rtsp_url = await asyncio.wait_for(self.provider.arlo.StartStream(self.arlo_basestation, self.arlo_device), timeout=self.timeout)
self.logger.debug(f"Got stream URL at {rtsp_url}")
return rtsp_url

async def getVideoStream(self, options=None):
async def getVideoStream(self, options: dict = None) -> MediaObject:
self.logger.debug("Entered getVideoStream")
rtsp_url = await self._getVideoStreamURL()
return await scrypted_sdk.mediaManager.createMediaObject(str.encode(rtsp_url), ScryptedMimeTypes.Url.value)

mso = (await self.getVideoStreamOptions())[0]
mso['refreshAt'] = round(time.time() * 1000) + 30 * 60 * 1000

ffmpeg_input = {
'url': rtsp_url,
'container': 'rtsp',
'mediaStreamOptions': mso,
'inputArguments': [
'-f', 'rtsp',
'-i', rtsp_url,
]
}
return await scrypted_sdk.mediaManager.createFFmpegMediaObject(ffmpeg_input)

async def startRTCSignalingSession(self, scrypted_session):
try:
Expand Down Expand Up @@ -330,7 +329,7 @@ async def initialize_push_to_talk(self, media=None):
self.logger.info("Initializing push to talk")

session_id, ice_servers = self.provider.arlo.StartPushToTalk(self.arlo_basestation, self.arlo_device)
self.logger.debug(f"Received ice servers: {[ice['url'] for ice in ice_servers]}")
self.logger.debug(f"Received ice servers: {[ice['url'] for ice in ice_servers]}")

cfg = scrypted_arlo_go.WebRTCConfiguration(
ICEServers=scrypted_arlo_go.Slice_webrtc_ICEServer([
Expand Down Expand Up @@ -372,7 +371,7 @@ async def initialize_push_to_talk(self, media=None):
self.logger.debug("Starting audio track forwarder")
self.scrypted_pc.ForwardAudioTo(self.arlo_pc)
self.logger.debug("Started audio track forwarder")

self.sdp_answered = False

offer = self.arlo_pc.CreateOffer()
Expand Down
59 changes: 59 additions & 0 deletions plugins/arlo/src/arlo_plugin/device_base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from scrypted_sdk import ScryptedDeviceBase

from .logging import ScryptedDeviceLoggerMixin
from .util import BackgroundTaskMixin
from .provider import ArloProvider

class ArloDeviceBase(ScryptedDeviceBase, ScryptedDeviceLoggerMixin, BackgroundTaskMixin):
nativeId: str = None
arlo_device: dict = None
arlo_basestation: dict = None
provider: ArloProvider = None
stop_subscriptions: bool = False

def __init__(self, nativeId: str, arlo_device: dict, arlo_basestation: dict, provider: ArloProvider) -> None:
super().__init__(nativeId=nativeId)

self.logger_name = nativeId

self.nativeId = nativeId
self.arlo_device = arlo_device
self.arlo_basestation = arlo_basestation
self.provider = provider
self.logger.setLevel(self.provider.get_current_log_level())

def __del__(self):
self.stop_subscriptions = True
self.cancel_pending_tasks()

def get_applicable_interfaces(self) -> list:
"""Returns the list of Scrypted interfaces that applies to this device."""
return []

def get_device_type(self) -> str:
"""Returns the Scrypted device type that applies to this device."""
return ""

def get_device_manifest(self) -> dict:
"""Returns the Scrypted device manifest representing this device."""
parent = None
if self.arlo_device.get("parentId") and self.arlo_device["parentId"] != self.arlo_device["deviceId"]:
parent = self.arlo_device["parentId"]

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": self.arlo_device["deviceId"],
"name": self.arlo_device["deviceName"],
"interfaces": self.get_applicable_interfaces(),
"type": self.get_device_type(),
"providerNativeId": parent,
}

def get_builtin_child_device_manifests(self) -> list:
"""Returns the list of child device manifests representing hardware features built into this device."""
return []
14 changes: 7 additions & 7 deletions plugins/arlo/src/arlo_plugin/doorbell.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,29 @@
from scrypted_sdk.types import BinarySensor, ScryptedInterface

from .camera import ArloCamera
from .provider import ArloProvider


class ArloDoorbell(ArloCamera, BinarySensor):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
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_doorbell_subscription()

def start_doorbell_subscription(self):
def start_doorbell_subscription(self) -> None:
def callback(doorbellPressed):
self.binaryState = doorbellPressed
return self.stop_subscriptions

self.register_task(
self.provider.arlo.SubscribeToDoorbellEvents(self.arlo_basestation, self.arlo_device, callback)
)

def get_applicable_interfaces(self):
def get_applicable_interfaces(self) -> list:
camera_interfaces = super().get_applicable_interfaces()
camera_interfaces.append(ScryptedInterface.BinarySensor.value)

model_id = self.arlo_device['properties']['modelId'].lower()
model_id = self.arlo_device['modelId'].lower()
if model_id.startswith("avd1001"):
camera_interfaces.remove(ScryptedInterface.Battery.value)
return camera_interfaces

3 changes: 1 addition & 2 deletions plugins/arlo/src/arlo_plugin/logging.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import logging
import sys


class ScryptedDeviceLoggingWrapper(logging.Handler):
Expand All @@ -20,7 +19,7 @@ def createScryptedLogger(scrypted_device, name):

logger.setLevel(logging.INFO)

# configure logger to output to scrypted's log stream
# configure logger to output to scrypted's log stream
sh = ScryptedDeviceLoggingWrapper(scrypted_device)

# log formatting
Expand Down
Loading

0 comments on commit 8a56e78

Please sign in to comment.