Skip to content

Commit

Permalink
Merge branch 'main' of github.com:koush/scrypted
Browse files Browse the repository at this point in the history
  • Loading branch information
koush committed May 21, 2023
2 parents 80e433f + 60786ab commit 302272e
Show file tree
Hide file tree
Showing 6 changed files with 112 additions and 22 deletions.
4 changes: 2 additions & 2 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.7.20",
"version": "0.7.21",
"description": "Arlo Plugin for Scrypted",
"keywords": [
"scrypted",
Expand Down
121 changes: 104 additions & 17 deletions plugins/arlo/src/arlo_plugin/camera.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from __future__ import annotations

import asyncio
import aiohttp
from async_timeout import timeout as async_timeout
from datetime import datetime, timedelta
import json
import threading
Expand All @@ -10,8 +12,9 @@
import scrypted_arlo_go

import scrypted_sdk
from scrypted_sdk.types import Setting, Settings, Device, Camera, VideoCamera, VideoClips, VideoClip, VideoClipOptions, MotionSensor, AudioSensor, Battery, Charger, ChargeState, DeviceProvider, MediaObject, ResponsePictureOptions, ResponseMediaStreamOptions, ScryptedMimeTypes, ScryptedInterface, ScryptedDeviceType
from scrypted_sdk.types import Setting, Settings, SettingValue, Device, Camera, VideoCamera, VideoClips, VideoClip, VideoClipOptions, MotionSensor, AudioSensor, Battery, Charger, ChargeState, DeviceProvider, MediaObject, ResponsePictureOptions, ResponseMediaStreamOptions, ScryptedMimeTypes, ScryptedInterface, ScryptedDeviceType

from .debug import EXPERIMENTAL
from .base import ArloDeviceBase
from .spotlight import ArloSpotlight, ArloFloodlight
from .vss import ArloSirenVirtualSecuritySystem
Expand Down Expand Up @@ -75,9 +78,16 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider,
intercom_session = None
light: ArloSpotlight = None
vss: ArloSirenVirtualSecuritySystem = None
picture_lock: asyncio.Lock = None

# eco mode bookkeeping
last_picture: bytes = None
last_picture_time: datetime = datetime(1970, 1, 1)

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.picture_lock = asyncio.Lock()

self.start_motion_subscription()
self.start_audio_subscription()
self.start_battery_subscription()
Expand Down Expand Up @@ -142,13 +152,14 @@ def get_applicable_interfaces(self) -> List[str]:
ScryptedInterface.Settings.value,
])

if self.two_way_audio:
results.discard(ScryptedInterface.RTCSignalingChannel.value)
results.add(ScryptedInterface.Intercom.value)
if EXPERIMENTAL:
if self.two_way_audio:
results.discard(ScryptedInterface.RTCSignalingChannel.value)
results.add(ScryptedInterface.Intercom.value)

if self.webrtc_emulation:
results.add(ScryptedInterface.RTCSignalingChannel.value)
results.discard(ScryptedInterface.Intercom.value)
if self.webrtc_emulation:
results.add(ScryptedInterface.RTCSignalingChannel.value)
results.discard(ScryptedInterface.Intercom.value)

if self.has_battery:
results.add(ScryptedInterface.Battery.value)
Expand All @@ -163,9 +174,10 @@ def get_applicable_interfaces(self) -> List[str]:
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)
if EXPERIMENTAL:
if not self._can_push_to_talk():
results.discard(ScryptedInterface.RTCSignalingChannel.value)
results.discard(ScryptedInterface.Intercom.value)

return list(results)

Expand Down Expand Up @@ -232,6 +244,21 @@ def wired_to_power(self) -> bool:
else:
return False

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

@property
def snapshot_throttle_interval(self) -> bool:
interval = self.storage.getItem("snapshot_throttle_interval")
if interval is None:
interval = 60
self.storage.setItem("snapshot_throttle_interval", interval)
return int(interval)

@property
def has_cloud_recording(self) -> bool:
return self.provider.arlo.GetSmartFeatures(self.arlo_device).get("planFeatures", {}).get("eventRecording", False)
Expand Down Expand Up @@ -261,6 +288,7 @@ async def getSettings(self) -> List[Setting]:
if self.has_battery:
result.append(
{
"group": "General",
"key": "wired_to_power",
"title": "Plugged In to External Power",
"value": self.wired_to_power,
Expand All @@ -270,16 +298,43 @@ async def getSettings(self) -> List[Setting]:
"type": "boolean",
},
)
if self._can_push_to_talk():
result.append(
{
"group": "General",
"key": "eco_mode",
"title": "Eco Mode",
"value": self.eco_mode,
"description": "Configures Scrypted to limit the number of requests made to this camera. " + \
"Additional eco mode settings will appear when this is turned on.",
"type": "boolean",
}
)
if self.eco_mode:
result.append(
{
"group": "Eco Mode",
"key": "snapshot_throttle_interval",
"title": "Snapshot Throttle Interval",
"value": self.snapshot_throttle_interval,
"description": "Time, in minutes, to throttle snapshot requests. " + \
"When eco mode is on, snapshot requests to the camera will be throttled for the given duration. " + \
"Cached snapshots may be returned if the time since the last snapshot has not exceeded the interval. " + \
"A value of 0 will disable throttling even when eco mode is on.",
"type": "number",
}
)
if self._can_push_to_talk() and EXPERIMENTAL:
result.extend([
{
"group": "General",
"key": "two_way_audio",
"title": "(Experimental) Enable native two-way audio",
"value": self.two_way_audio,
"description": "Enables two-way audio for this device. Not yet completely functional on all audio senders.",
"type": "boolean",
},
{
"group": "General",
"key": "webrtc_emulation",
"title": "(Highly Experimental) Emulate WebRTC Camera",
"value": self.webrtc_emulation,
Expand All @@ -291,10 +346,28 @@ async def getSettings(self) -> List[Setting]:
return result

@async_print_exception_guard
async def putSetting(self, key, value) -> None:
async def putSetting(self, key: str, value: SettingValue) -> None:
if not self.validate_setting(key, value):
await self.onDeviceEvent(ScryptedInterface.Settings.value, None)
return

if key in ["webrtc_emulation", "two_way_audio", "wired_to_power"]:
self.storage.setItem(key, value == "true" or value == True)
await self.provider.discover_devices()
elif key in ["eco_mode"]:
self.storage.setItem(key, value == "true" or value == True)
else:
self.storage.setItem(key, value)
await self.onDeviceEvent(ScryptedInterface.Settings.value, None)

def validate_setting(self, key: str, val: SettingValue) -> bool:
if key == "snapshot_throttle_interval":
try:
val = int(val)
except ValueError:
self.logger.error(f"Invalid snapshot throttle interval '{val}' - must be an integer")
return False
return True

async def getPictureOptions(self) -> List[ResponsePictureOptions]:
return []
Expand All @@ -313,13 +386,27 @@ async def takePicture(self, options: dict = None) -> MediaObject:
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}")
async with self.picture_lock:
if self.eco_mode and self.snapshot_throttle_interval > 0:
if datetime.now() - self.last_picture_time <= timedelta(minutes=self.snapshot_throttle_interval):
self.logger.info("Using cached image")
return await scrypted_sdk.mediaManager.createMediaObject(self.last_picture, "image/jpeg")

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}")

if pic_url is None:
raise Exception("Error taking snapshot")

if pic_url is None:
raise Exception("Error taking snapshot")
async with async_timeout(self.timeout):
async with aiohttp.ClientSession() as session:
async with session.get(pic_url) as resp:
if resp.status != 200:
raise Exception(f"Unexpected status downloading snapshot image: {resp.status}")
self.last_picture = await resp.read()
self.last_picture_time = datetime.now()

return await scrypted_sdk.mediaManager.createMediaObject(str.encode(pic_url), ScryptedMimeTypes.Url.value)
return await scrypted_sdk.mediaManager.createMediaObject(self.last_picture, "image/jpeg")

async def getVideoStreamOptions(self) -> List[ResponseMediaStreamOptions]:
return [
Expand Down
1 change: 1 addition & 0 deletions plugins/arlo/src/arlo_plugin/debug.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
EXPERIMENTAL = False
2 changes: 2 additions & 0 deletions plugins/arlo/src/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
paho-mqtt==1.6.1
sseclient==0.0.22
aiohttp==3.8.4
requests==2.28.2
cachetools==5.3.0
scrypted-arlo-go==0.0.2
cloudscraper==1.2.71
cryptography==38.0.4
async-timeout==4.0.2
--extra-index-url=https://www.piwheels.org/simple/
--extra-index-url=https://bjia56.github.io/scrypted-arlo-go/
--prefer-binary
4 changes: 2 additions & 2 deletions sdk/types/scrypted_python/scrypted_sdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,15 @@ def fork() -> PluginFork:
class ScryptedStatic:
def __init__(self) -> None:
self.systemManager: SystemManager = None
self.deviceManager: SystemManager = None
self.deviceManager: DeviceManager = None
self.mediaManager: MediaManager = None
self.zip: ZipFile = None
self.remote: Any = None
self.api: Any = None
self.fork: Callable[[], PluginFork]
self.connectRPCObject: Callable[[Any], asyncio.Task[Any]]

def sdk_init(z: ZipFile, r, sm: DeviceManager, dm: SystemManager, mm: MediaManager):
def sdk_init(z: ZipFile, r, sm: SystemManager, dm: DeviceManager, mm: MediaManager):
global zip
global remote
global systemManager
Expand Down

0 comments on commit 302272e

Please sign in to comment.