Skip to content

Commit

Permalink
arlo: video clips + virtual security system for sirens (#656)
Browse files Browse the repository at this point in the history
* fix doorbell device type

* bump 0.7.1 for beta

* standalone camera fixes

* bump 0.7.2 for beta

* more type annotations + trickle discover all devices

* fetch arlo library clips

* log options

* cache library at lower level and fetch clips on demand

* move library timedelta range lower in stack

* wip siren as security system

* virtual security system and tweaks

* vss documentation and settings

* expand vss usage docs

* more docs changes

* force homekit and scrypted to update given vss and siren state

* RE-ENABLING SIREN!!!

* bump 0.7.3 for beta

* bump 0.7.3 for release
  • Loading branch information
bjia56 authored Mar 25, 2023
1 parent 07c3173 commit 3854b75
Show file tree
Hide file tree
Showing 13 changed files with 456 additions and 79 deletions.
2 changes: 1 addition & 1 deletion plugins/arlo/.vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@
//"scrypted.volumeRoot": "${config:scrypted.serverRoot}/volume",

"python.analysis.extraPaths": [
"./node_modules/@scrypted/sdk/scrypted_python"
"./node_modules/@scrypted/sdk/types/scrypted_python"
]
}
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.0",
"version": "0.7.4",
"description": "Arlo Plugin for Scrypted",
"keywords": [
"scrypted",
Expand Down
52 changes: 51 additions & 1 deletion plugins/arlo/src/arlo_plugin/arlo/arlo_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
from .logging import logger

# Import all of the other stuff.
from datetime import datetime
from datetime import datetime, timedelta
from cachetools import cached, TTLCache

import asyncio
import sys
Expand Down Expand Up @@ -735,3 +736,52 @@ def SirenOff(self, basestation):
"pattern": "alarm"
}
})

def GetLibrary(self, device, from_date: datetime, to_date: datetime):
"""
This call returns the following:
presignedContentUrl is a link to the actual video in Amazon AWS.
presignedThumbnailUrl is a link to the thumbnail .jpg of the actual video in Amazon AWS.
[
{
"mediaDurationSecond": 30,
"contentType": "video/mp4",
"name": "XXXXXXXXXXXXX",
"presignedContentUrl": "https://arlos3-prod-z2.s3.amazonaws.com/XXXXXXX_XXXX_XXXX_XXXX_XXXXXXXXXXXXX/XXX-XXXXXXX/XXXXXXXXXXXXX/recordings/XXXXXXXXXXXXX.mp4?AWSAccessKeyId=XXXXXXXXXXXXXXXXXXXX&Expires=1472968703&Signature=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"lastModified": 1472881430181,
"localCreatedDate": XXXXXXXXXXXXX,
"presignedThumbnailUrl": "https://arlos3-prod-z2.s3.amazonaws.com/XXXXXXX_XXXX_XXXX_XXXX_XXXXXXXXXXXXX/XXX-XXXXXXX/XXXXXXXXXXXXX/recordings/XXXXXXXXXXXXX_thumb.jpg?AWSAccessKeyId=XXXXXXXXXXXXXXXXXXXX&Expires=1472968703&Signature=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"reason": "motionRecord",
"deviceId": "XXXXXXXXXXXXX",
"createdBy": "XXXXXXXXXXXXX",
"createdDate": "20160903",
"timeZone": "America/Chicago",
"ownerId": "XXX-XXXXXXX",
"utcCreatedDate": XXXXXXXXXXXXX,
"currentState": "new",
"mediaDuration": "00:00:30"
}
]
"""
# give the query range a bit of buffer
from_date_internal = from_date - timedelta(days=1)
to_date_internal = to_date + timedelta(days=1)

return [
result for result in
self._getLibraryCached(from_date_internal.strftime("%Y%m%d"), to_date_internal.strftime("%Y%m%d"))
if result["deviceId"] == device["deviceId"]
and datetime.fromtimestamp(int(result["name"]) / 1000.0) <= to_date
and datetime.fromtimestamp(int(result["name"]) / 1000.0) >= from_date
]

@cached(cache=TTLCache(maxsize=512, ttl=60))
def _getLibraryCached(self, from_date: str, to_date: str):
logger.debug(f"Library cache miss for {from_date}, {to_date}")
return self.request.post(
f'https://{self.BASE_URL}/hmsweb/users/library',
{
'dateFrom': from_date,
'dateTo': to_date
}
)
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
from __future__ import annotations

import traceback
from typing import List, TYPE_CHECKING

from scrypted_sdk import ScryptedDeviceBase
from scrypted_sdk.types import Device

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

if TYPE_CHECKING:
# https://adamj.eu/tech/2021/05/13/python-type-hints-how-to-fix-circular-imports/
from .provider import ArloProvider


class ArloDeviceBase(ScryptedDeviceBase, ScryptedDeviceLoggerMixin, BackgroundTaskMixin):
nativeId: str = None
Expand All @@ -22,19 +32,19 @@ def __init__(self, nativeId: str, arlo_device: dict, arlo_basestation: dict, pro
self.provider = provider
self.logger.setLevel(self.provider.get_current_log_level())

def __del__(self):
def __del__(self) -> None:
self.stop_subscriptions = True
self.cancel_pending_tasks()

def get_applicable_interfaces(self) -> list:
def get_applicable_interfaces(self) -> List[str]:
"""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:
def get_device_manifest(self) -> Device:
"""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"]:
Expand All @@ -54,6 +64,17 @@ def get_device_manifest(self) -> dict:
"providerNativeId": parent,
}

def get_builtin_child_device_manifests(self) -> list:
def get_builtin_child_device_manifests(self) -> List[Device]:
"""Returns the list of child device manifests representing hardware features built into this device."""
return []
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
52 changes: 33 additions & 19 deletions plugins/arlo/src/arlo_plugin/basestation.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,33 @@
from __future__ import annotations

from typing import List, TYPE_CHECKING

from scrypted_sdk import ScryptedDeviceBase
from scrypted_sdk.types import DeviceProvider, ScryptedInterface, ScryptedDeviceType
from scrypted_sdk.types import Device, DeviceProvider, ScryptedInterface, ScryptedDeviceType

from .base import ArloDeviceBase
from .vss import ArloSirenVirtualSecuritySystem

from .device_base import ArloDeviceBase
from .siren import ArloSiren
if TYPE_CHECKING:
# https://adamj.eu/tech/2021/05/13/python-type-hints-how-to-fix-circular-imports/
from .provider import ArloProvider


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

def get_applicable_interfaces(self) -> list:
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)

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

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

def get_builtin_child_device_manifests(self) -> list:
def get_builtin_child_device_manifests(self) -> List[Device]:
vss_id = f'{self.arlo_device["deviceId"]}.vss'
vss = self.get_or_create_vss(vss_id)
return [
{
"info": {
Expand All @@ -23,22 +36,23 @@ def get_builtin_child_device_manifests(self) -> list:
"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,
"nativeId": vss_id,
"name": f'{self.arlo_device["deviceName"]} Siren Virtual Security System',
"interfaces": vss.get_applicable_interfaces(),
"type": vss.get_device_type(),
"providerNativeId": self.nativeId,
}
]
},
] + vss.get_builtin_child_device_manifests()

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
return self.get_or_create_vss(nativeId)

def get_or_create_vss(self, nativeId: str) -> ArloSirenVirtualSecuritySystem:
if not nativeId.endswith("vss"):
return None
if not self.vss:
self.vss = ArloSirenVirtualSecuritySystem(nativeId, self.arlo_device, self.arlo_basestation, self.provider)
return self.vss
Loading

0 comments on commit 3854b75

Please sign in to comment.