Compare commits

...

22 Commits

Author SHA1 Message Date
Koushik Dutta
badb1905ce prerelease 2023-03-25 21:54:40 -07:00
Koushik Dutta
735c2dce7b Merge branch 'main' of github.com:koush/scrypted 2023-03-25 21:52:56 -07:00
Koushik Dutta
ffae3f246f python-codecs: fix mac crash 2023-03-25 21:52:51 -07:00
Koushik Dutta
31b424f89f server: mac python fixes 2023-03-25 21:52:32 -07:00
Brett Jia
3b7acc3a90 homekit: merge child lights into cameras (#659) 2023-03-25 20:09:42 -07:00
Koushik Dutta
7e66d1ac7f prebeta 2023-03-25 19:45:11 -07:00
Koushik Dutta
a613da069e server: relax failure on python arch mismatch 2023-03-25 19:45:05 -07:00
Koushik Dutta
40b73c6589 prebeta 2023-03-25 18:42:52 -07:00
Koushik Dutta
ef16ca83a2 server: detect python architecture vs machine mismatch 2023-03-25 18:42:39 -07:00
Koushik Dutta
76bf1d0d3f docker: rollback linux changes 2023-03-25 18:35:40 -07:00
Koushik Dutta
3d5ccf25d1 server: log host os specs 2023-03-25 15:05:08 -07:00
Koushik Dutta
36fcb713d9 videoanalysis: ffmpeg frame generator fixes 2023-03-25 15:04:40 -07:00
Koushik Dutta
e306631850 docker: arm fixes 2023-03-25 14:40:37 -07:00
Koushik Dutta
17400fa886 docker: arm fixes 2023-03-25 14:37:17 -07:00
Koushik Dutta
c6dc628616 docker: arm fixes 2023-03-25 14:31:40 -07:00
Koushik Dutta
f974653e73 videoanalysis: make new pipeline the default 2023-03-25 12:05:35 -07:00
Koushik Dutta
b83880a8a3 Merge branch 'main' of github.com:koush/scrypted 2023-03-25 11:34:37 -07:00
Koushik Dutta
ee4d8f52df pam-diff: fixup score reporting 2023-03-25 11:34:33 -07:00
Brett Jia
3854b75c6e arlo: video clips + virtual security system for sirens (#656)
* 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
2023-03-25 11:13:28 -07:00
Koushik Dutta
07c3173506 docker: fix pip execution command 2023-03-25 10:43:12 -07:00
Koushik Dutta
2894ab1b96 prerelease 2023-03-25 09:28:26 -07:00
Koushik Dutta
99995ea882 server: start watchdog/stats after plugin dependency installation completes 2023-03-25 09:27:04 -07:00
35 changed files with 567 additions and 144 deletions

View File

@@ -59,6 +59,10 @@ RUN apt-get -y install \
# armv7l does not have wheels for any of these
# and compile times would forever, if it works at all.
# furthermore, it's possible to run 32bit docker on 64bit arm,
# which causes weird behavior in python which looks at the arch version
# which still reports 64bit, even if running in 32bit docker.
# this scenario is not supported and will be reported at runtime.
RUN if [ "$(uname -m)" = "armv7l" ]; \
then \
apt-get -y install \
@@ -73,7 +77,7 @@ RUN if [ "$(uname -m)" = "armv7l" ]; \
RUN python3 -m pip install --upgrade pip
# pyvips is broken on x86 due to mismatch ffi
# https://stackoverflow.com/questions/62658237/it-seems-that-the-version-of-the-libffi-library-seen-at-runtime-is-different-fro
RUN pip install --force-reinstall --no-binary :all: cffi
RUN python3 -m pip install --force-reinstall --no-binary :all: cffi
RUN python3 -m pip install aiofiles debugpy typing_extensions psutil
################################################################

View File

@@ -56,6 +56,10 @@ RUN apt-get -y install \
# armv7l does not have wheels for any of these
# and compile times would forever, if it works at all.
# furthermore, it's possible to run 32bit docker on 64bit arm,
# which causes weird behavior in python which looks at the arch version
# which still reports 64bit, even if running in 32bit docker.
# this scenario is not supported and will be reported at runtime.
RUN if [ "$(uname -m)" = "armv7l" ]; \
then \
apt-get -y install \
@@ -70,7 +74,7 @@ RUN if [ "$(uname -m)" = "armv7l" ]; \
RUN python3 -m pip install --upgrade pip
# pyvips is broken on x86 due to mismatch ffi
# https://stackoverflow.com/questions/62658237/it-seems-that-the-version-of-the-libffi-library-seen-at-runtime-is-different-fro
RUN pip install --force-reinstall --no-binary :all: cffi
RUN python3 -m pip install --force-reinstall --no-binary :all: cffi
RUN python3 -m pip install aiofiles debugpy typing_extensions psutil
################################################################

View File

@@ -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"
]
}

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/arlo",
"version": "0.7.0",
"version": "0.7.4",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/arlo",
"version": "0.7.0",
"version": "0.7.4",
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
}

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/arlo",
"version": "0.7.0",
"version": "0.7.4",
"description": "Arlo Plugin for Scrypted",
"keywords": [
"scrypted",

View File

@@ -29,7 +29,8 @@ from .sse_stream_async import EventStream
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
@@ -735,3 +736,52 @@ class Arlo(object):
"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
}
)

View File

@@ -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
@@ -22,11 +32,11 @@ class ArloDeviceBase(ScryptedDeviceBase, ScryptedDeviceLoggerMixin, BackgroundTa
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 []
@@ -34,7 +44,7 @@ class ArloDeviceBase(ScryptedDeviceBase, ScryptedDeviceLoggerMixin, BackgroundTa
"""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"]:
@@ -54,6 +64,17 @@ class ArloDeviceBase(ScryptedDeviceBase, ScryptedDeviceLoggerMixin, BackgroundTa
"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

View File

@@ -1,20 +1,33 @@
from scrypted_sdk import ScryptedDeviceBase
from scrypted_sdk.types import DeviceProvider, ScryptedInterface, ScryptedDeviceType
from __future__ import annotations
from .device_base import ArloDeviceBase
from .siren import ArloSiren
from typing import List, TYPE_CHECKING
from scrypted_sdk import ScryptedDeviceBase
from scrypted_sdk.types import Device, DeviceProvider, ScryptedInterface, ScryptedDeviceType
from .base import ArloDeviceBase
from .vss import ArloSirenVirtualSecuritySystem
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": {
@@ -23,22 +36,23 @@ class ArloBasestation(ArloDeviceBase, DeviceProvider):
"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)
return self.get_or_create_vss(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
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

View File

@@ -1,26 +1,32 @@
from __future__ import annotations
import asyncio
from datetime import datetime, timedelta
import json
import threading
import time
from typing import List, TYPE_CHECKING
import scrypted_arlo_go
import scrypted_sdk
from scrypted_sdk.types import Settings, Camera, VideoCamera, MotionSensor, Battery, MediaObject, ScryptedMimeTypes, ScryptedInterface, ScryptedDeviceType
from scrypted_sdk.types import Setting, Settings, Camera, VideoCamera, VideoClips, VideoClip, VideoClipOptions, MotionSensor, Battery, MediaObject, ResponsePictureOptions, ResponseMediaStreamOptions, ScryptedMimeTypes, ScryptedInterface, ScryptedDeviceType
from .device_base import ArloDeviceBase
from .provider import ArloProvider
from .base import ArloDeviceBase
from .child_process import HeartbeatChildProcess
from .util import BackgroundTaskMixin
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, MotionSensor, Battery):
class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, VideoClips, MotionSensor, Battery):
timeout: int = 30
intercom_session = 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)
self.start_motion_subscription()
self.start_battery_subscription()
@@ -42,13 +48,14 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, MotionSensor, Ba
self.provider.arlo.SubscribeToBatteryEvents(self.arlo_basestation, self.arlo_device, callback)
)
def get_applicable_interfaces(self) -> list:
def get_applicable_interfaces(self) -> List[str]:
results = set([
ScryptedInterface.VideoCamera.value,
ScryptedInterface.Camera.value,
ScryptedInterface.MotionSensor.value,
ScryptedInterface.Battery.value,
ScryptedInterface.Settings.value,
ScryptedInterface.VideoClips.value,
])
if self.two_way_audio:
@@ -85,7 +92,7 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, MotionSensor, Ba
else:
return True
async def getSettings(self) -> list:
async def getSettings(self) -> List[Setting]:
if self._can_push_to_talk():
return [
{
@@ -111,7 +118,7 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, MotionSensor, Ba
self.storage.setItem(key, value == "true")
await self.provider.discoverDevices()
async def getPictureOptions(self) -> list:
async def getPictureOptions(self) -> List[ResponsePictureOptions]:
return []
async def takePicture(self, options: dict = None) -> MediaObject:
@@ -131,7 +138,7 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, MotionSensor, Ba
return await scrypted_sdk.mediaManager.createMediaObject(str.encode(pic_url), ScryptedMimeTypes.Url.value)
async def getVideoStreamOptions(self) -> list:
async def getVideoStreamOptions(self) -> List[ResponseMediaStreamOptions]:
return [
{
"id": 'default',
@@ -200,21 +207,85 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, MotionSensor, Ba
except Exception as e:
self.logger.error(e)
async def startIntercom(self, media):
async def startIntercom(self, media) -> None:
self.logger.info("Starting intercom")
self.intercom_session = ArloCameraRTCSignalingSession(self)
await self.intercom_session.initialize_push_to_talk(media)
async def stopIntercom(self):
async def stopIntercom(self) -> None:
self.logger.info("Stopping intercom")
if self.intercom_session is not None:
await self.intercom_session.shutdown()
self.intercom_session = None
def _can_push_to_talk(self):
def _can_push_to_talk(self) -> bool:
# Right now, only implement push to talk for basestation cameras
return self.arlo_device["deviceId"] != self.arlo_device["parentId"]
async def getVideoClip(self, videoId: str) -> MediaObject:
self.logger.info(f"Getting video clip {videoId}")
id_as_time = int(videoId) / 1000.0
start = datetime.fromtimestamp(id_as_time) - timedelta(seconds=10)
end = datetime.fromtimestamp(id_as_time) + timedelta(seconds=10)
library = self.provider.arlo.GetLibrary(self.arlo_device, start, end)
for recording in library:
if videoId == recording["name"]:
return await scrypted_sdk.mediaManager.createMediaObjectFromUrl(recording["presignedContentUrl"])
self.logger.warn(f"Clip {videoId} not found")
return None
async def getVideoClipThumbnail(self, thumbnailId: str) -> MediaObject:
self.logger.info(f"Getting video clip thumbnail {thumbnailId}")
id_as_time = int(thumbnailId) / 1000.0
start = datetime.fromtimestamp(id_as_time) - timedelta(seconds=10)
end = datetime.fromtimestamp(id_as_time) + timedelta(seconds=10)
library = self.provider.arlo.GetLibrary(self.arlo_device, start, end)
for recording in library:
if thumbnailId == recording["name"]:
return await scrypted_sdk.mediaManager.createMediaObjectFromUrl(recording["presignedThumbnailUrl"])
self.logger.warn(f"Clip thumbnail {thumbnailId} not found")
return None
async def getVideoClips(self, options: VideoClipOptions = None) -> List[VideoClip]:
self.logger.info(f"Fetching remote video clips {options}")
start = datetime.fromtimestamp(options["startTime"] / 1000.0)
end = datetime.fromtimestamp(options["endTime"] / 1000.0)
library = self.provider.arlo.GetLibrary(self.arlo_device, start, end)
clips = []
for recording in library:
clip = {
"duration": recording["mediaDurationSecond"] * 1000.0,
"id": recording["name"],
"thumbnailId": recording["name"],
"videoId": recording["name"],
"startTime": recording["utcCreatedDate"],
"description": recording["reason"],
"resources": {
"thumbnail": {
"href": recording["presignedThumbnailUrl"],
},
"video": {
"href": recording["presignedContentUrl"],
},
},
}
clips.append(clip)
if options.get("reverseOrder"):
clips.reverse()
return clips
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")
class ArloCameraRTCSignalingSession(BackgroundTaskMixin):
def __init__(self, camera):

View File

@@ -1,13 +1,19 @@
from scrypted_sdk.types import BinarySensor, ScryptedInterface
from __future__ import annotations
from typing import List, TYPE_CHECKING
from scrypted_sdk.types import BinarySensor, ScryptedInterface, ScryptedDeviceType
from .camera import ArloCamera
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 ArloDoorbell(ArloCamera, BinarySensor):
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) -> None:
@@ -19,7 +25,10 @@ class ArloDoorbell(ArloCamera, BinarySensor):
self.provider.arlo.SubscribeToDoorbellEvents(self.arlo_basestation, self.arlo_device, callback)
)
def get_applicable_interfaces(self) -> list:
def get_device_type(self) -> str:
return ScryptedDeviceType.Doorbell.value
def get_applicable_interfaces(self) -> List[str]:
camera_interfaces = super().get_applicable_interfaces()
camera_interfaces.append(ScryptedInterface.BinarySensor.value)

View File

@@ -6,16 +6,21 @@ import logging
import re
import requests
import traceback
from typing import List
import scrypted_sdk
from scrypted_sdk import ScryptedDeviceBase
from scrypted_sdk.types import Settings, DeviceProvider, DeviceDiscovery, ScryptedInterface, ScryptedDeviceType
from scrypted_sdk.types import Setting, SettingValue, Settings, DeviceProvider, DeviceDiscovery, ScryptedInterface
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 .camera import ArloCamera
from .doorbell import ArloDoorbell
from .basestation import ArloBasestation
from .base import ArloDeviceBase
class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, DeviceDiscovery, ScryptedDeviceLoggerMixin, BackgroundTaskMixin):
@@ -366,7 +371,7 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, DeviceDiscovery
self.logger.info(f"Exiting IMAP refresh loop {id(imap_signal)}")
return
async def getSettings(self) -> list:
async def getSettings(self) -> List[Setting]:
results = [
{
"group": "General",
@@ -477,7 +482,7 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, DeviceDiscovery
return results
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
@@ -523,7 +528,7 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, DeviceDiscovery
_ = self.arlo
await self.onDeviceEvent(ScryptedInterface.Settings.value, None)
def validate_setting(self, key: str, val: str) -> bool:
def validate_setting(self, key: str, val: SettingValue) -> bool:
if key == "refresh_interval":
try:
val = int(val)
@@ -570,53 +575,65 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, DeviceDiscovery
nativeId = basestation["deviceId"]
if nativeId in self.arlo_basestations:
self.logger.info(f"Skipping basestation {nativeId} as it already exists")
self.logger.info(f"Skipping basestation {nativeId} ({basestation['modelId']}) as it has already been added")
continue
self.arlo_basestations[nativeId] = basestation
device = await self.getDevice(nativeId)
scrypted_interfaces = device.get_applicable_interfaces()
manifest = device.get_device_manifest()
self.logger.debug(f"Interfaces for {nativeId} ({basestation['modelId']}): {scrypted_interfaces}")
self.logger.info(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)
# add any builtin child devices
provider_to_device_map.setdefault(nativeId, []).extend(device.get_builtin_child_device_manifests())
# we also want to trickle discover them so they are added without deleting all existing
# we want to trickle discover them so they are added without deleting all existing
# root level devices - this is for backward compatibility
await scrypted_sdk.deviceManager.onDeviceDiscovered(manifest)
# add any builtin child devices and trickle discover them
child_manifests = device.get_builtin_child_device_manifests()
for child_manifest in child_manifests:
await scrypted_sdk.deviceManager.onDeviceDiscovered(child_manifest)
provider_to_device_map.setdefault(child_manifest["providerNativeId"], []).append(child_manifest)
self.logger.info(f"Discovered {len(basestations)} basestations")
cameras = self.arlo.GetDevices(['camera', "arloq", "arloqs", "doorbell"])
for camera in cameras:
if camera["deviceId"] != camera["parentId"] and camera["parentId"] not in self.arlo_basestations:
self.logger.info(f"Skipping camera {camera['deviceId']} because its basestation was not found")
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} as it already exists")
self.logger.info(f"Skipping camera {nativeId} ({camera['modelId']}) as it has already been added")
continue
self.arlo_cameras[nativeId] = camera
device = await self.getDevice(nativeId)
scrypted_interfaces = device.get_applicable_interfaces()
manifest = device.get_device_manifest()
self.logger.debug(f"Interfaces for {nativeId} ({camera['modelId']}): {scrypted_interfaces}")
if camera["deviceId"] == camera["parentId"]:
# these are standalone cameras with no basestation, so they act as their
# own basestation
self.arlo_basestations[camera["deviceId"]] = camera
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}")
if camera["deviceId"] == camera["parentId"]:
provider_to_device_map.setdefault(None, []).append(manifest)
else:
provider_to_device_map.setdefault(camera["parentId"], []).append(manifest)
# add any builtin child devices
provider_to_device_map.setdefault(nativeId, []).extend(device.get_builtin_child_device_manifests())
# trickle discover this camera so it exists for later steps
await scrypted_sdk.deviceManager.onDeviceDiscovered(manifest)
# add any builtin child devices and trickle discover them
child_manifests = device.get_builtin_child_device_manifests()
for child_manifest in child_manifests:
await scrypted_sdk.deviceManager.onDeviceDiscovered(child_manifest)
provider_to_device_map.setdefault(child_manifest["providerNativeId"], []).append(child_manifest)
camera_devices.append(manifest)
@@ -638,7 +655,7 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, DeviceDiscovery
"devices": provider_to_device_map[None]
})
async def getDevice(self, nativeId: str) -> ScryptedDeviceBase:
async def getDevice(self, nativeId: str) -> ArloDeviceBase:
ret = self.scrypted_devices.get(nativeId, None)
if ret is None:
ret = self.create_device(nativeId)
@@ -646,21 +663,19 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, DeviceDiscovery
self.scrypted_devices[nativeId] = ret
return ret
def create_device(self, nativeId: str) -> ScryptedDeviceBase:
from .camera import ArloCamera
from .doorbell import ArloDoorbell
from .basestation import ArloBasestation
def create_device(self, nativeId: str) -> ArloDeviceBase:
if nativeId not in self.arlo_cameras and nativeId not in self.arlo_basestations:
self.logger.warning(f"Cannot create device for nativeId {nativeId}, maybe it hasn't been loaded yet?")
return None
arlo_device = self.arlo_cameras.get(nativeId)
if not arlo_device:
# this is a basestation, so build the basestation object
arlo_device = self.arlo_basestations[nativeId]
return ArloBasestation(nativeId, arlo_device, arlo_device, self)
return ArloBasestation(nativeId, arlo_device, self)
if arlo_device["parentId"] not in self.arlo_basestations:
self.logger.warning(f"Cannot create camera with nativeId {nativeId} when {arlo_device['parentId']} is not a valid basestation")
return None
arlo_basestation = self.arlo_basestations[arlo_device["parentId"]]

View File

@@ -1,17 +1,61 @@
from scrypted_sdk.types import OnOff, ScryptedInterface
from __future__ import annotations
from .device_base import ArloDeviceBase
from typing import List, TYPE_CHECKING
from scrypted_sdk.types import OnOff, SecuritySystemMode, 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 .vss import ArloSirenVirtualSecuritySystem
class ArloSiren(ArloDeviceBase, OnOff):
vss: ArloSirenVirtualSecuritySystem = None
def get_applicable_interfaces(self) -> list:
def __init__(self, nativeId: str, arlo_device: dict, arlo_basestation: dict, provider: ArloProvider, vss: ArloSirenVirtualSecuritySystem) -> None:
super().__init__(nativeId=nativeId, arlo_device=arlo_device, arlo_basestation=arlo_basestation, provider=provider)
self.vss = vss
def get_applicable_interfaces(self) -> List[str]:
return [ScryptedInterface.OnOff.value]
def get_device_type(self) -> str:
return ScryptedDeviceType.Siren.value
@ArloDeviceBase.async_print_exception_guard
async def turnOn(self) -> None:
self.logger.info("Turning on")
if self.vss.securitySystemState["mode"] == SecuritySystemMode.Disarmed.value:
self.logger.info("Virtual security system is disarmed, ignoring trigger")
# set and unset this property to force homekit to display the
# switch as off
self.on = True
self.on = False
self.vss.securitySystemState = {
**self.vss.securitySystemState,
"triggered": False,
}
return
self.provider.arlo.SirenOn(self.arlo_device)
self.on = True
self.vss.securitySystemState = {
**self.vss.securitySystemState,
"triggered": True,
}
@ArloDeviceBase.async_print_exception_guard
async def turnOff(self) -> None:
self.logger.info("Turning off")
self.provider.arlo.SirenOff(self.arlo_device)
self.provider.arlo.SirenOff(self.arlo_device)
self.on = False
self.vss.securitySystemState = {
**self.vss.securitySystemState,
"triggered": False,
}

View File

@@ -2,12 +2,12 @@ import asyncio
class BackgroundTaskMixin:
def create_task(self, coroutine):
def create_task(self, coroutine) -> asyncio.Task:
task = asyncio.get_event_loop().create_task(coroutine)
self.register_task(task)
return task
def register_task(self, task):
def register_task(self, task) -> None:
if not hasattr(self, "background_tasks"):
self.background_tasks = set()
@@ -21,6 +21,8 @@ class BackgroundTaskMixin:
task.add_done_callback(print_exception)
task.add_done_callback(self.background_tasks.discard)
def cancel_pending_tasks(self):
def cancel_pending_tasks(self) -> None:
if not hasattr(self, "background_tasks"):
return
for task in self.background_tasks:
task.cancel()

View File

@@ -0,0 +1,150 @@
from __future__ import annotations
import asyncio
from typing import List, TYPE_CHECKING
from scrypted_sdk.types import Device, DeviceProvider, Setting, Settings, SettingValue, SecuritySystem, SecuritySystemMode, Readme, ScryptedInterface, ScryptedDeviceType
from .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 ArloSirenVirtualSecuritySystem(ArloDeviceBase, SecuritySystem, 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]
siren: ArloSiren = 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)
self.create_task(self.delayed_init())
@property
def mode(self) -> str:
mode = self.storage.getItem("mode")
if mode is None or mode not in ArloSirenVirtualSecuritySystem.SUPPORTED_MODES:
mode = SecuritySystemMode.Disarmed.value
return mode
@mode.setter
def mode(self, mode: str) -> None:
if mode not in ArloSirenVirtualSecuritySystem.SUPPORTED_MODES:
raise ValueError(f"invalid mode {mode}")
self.storage.setItem("mode", mode)
self.securitySystemState = {
**self.securitySystemState,
"mode": mode,
}
self.create_task(self.onDeviceEvent(ScryptedInterface.Settings.value, None))
async def delayed_init(self) -> None:
iterations = 1
while not self.stop_subscriptions:
if iterations > 100:
self.logger.error("Delayed init exceeded iteration limit, giving up")
return
try:
self.securitySystemState = {
"supportedModes": ArloSirenVirtualSecuritySystem.SUPPORTED_MODES,
"mode": self.mode,
}
return
except Exception as e:
self.logger.info(f"Delayed init failed, will try again: {e}")
await asyncio.sleep(0.1)
iterations += 1
def get_applicable_interfaces(self) -> List[str]:
return [
ScryptedInterface.SecuritySystem.value,
ScryptedInterface.DeviceProvider.value,
ScryptedInterface.Settings.value,
ScryptedInterface.Readme.value,
]
def get_device_type(self) -> str:
return ScryptedDeviceType.SecuritySystem.value
def get_builtin_child_device_manifests(self) -> List[Device]:
siren = self.get_or_create_siren()
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": siren.nativeId,
"name": f'{self.arlo_device["deviceName"]} Siren',
"interfaces": siren.get_applicable_interfaces(),
"type": siren.get_device_type(),
"providerNativeId": self.nativeId,
}
]
async def getSettings(self) -> List[Setting]:
return [
{
"key": "mode",
"title": "Arm Mode",
"description": "If disarmed, the associated siren will not be physically triggered even if toggled.",
"value": self.mode,
"choices": ArloSirenVirtualSecuritySystem.SUPPORTED_MODES,
},
]
async def putSetting(self, key: str, value: SettingValue) -> None:
if key != "mode":
raise ValueError(f"invalid setting {key}")
self.mode = value
if self.mode == SecuritySystemMode.Disarmed.value:
await self.get_or_create_siren().turnOff()
async def getReadmeMarkdown(self) -> str:
return """
# Virtual Security System for Arlo Sirens
This security system device is not a real physical device, but a virtual, emulated device provided by the Arlo Scrypted plugin. Its purpose is to grant security system semantics of Arm/Disarm to avoid the accidental, unwanted triggering of the real physical siren through integrations such as Homekit.
To allow the siren to trigger, set the Arm Mode to any of the Armed options. When Disarmed, any triggers of the siren will be ignored. Switching modes will not perform any changes to Arlo cloud or your Arlo account, but rather only to this Scrypted device.
If this virtual security system is synced to Homekit, the siren device will be merged into the same security system accessory as a switch. The siren device will not be added as a separate accessory. To access the siren as a switch without the security system, disable syncing of the virtual security system and enable syncing of the siren, then ensure that the virtual security system is armed manually in its settings in Scrypted.
""".strip()
async def getDevice(self, nativeId: str) -> ArloDeviceBase:
if not nativeId.endswith("siren"):
return None
return self.get_or_create_siren()
def get_or_create_siren(self) -> ArloSiren:
siren_id = f'{self.arlo_device["deviceId"]}.siren'
if not self.siren:
self.siren = ArloSiren(siren_id, self.arlo_device, self.arlo_basestation, self.provider, self)
return self.siren
async def armSecuritySystem(self, mode: SecuritySystemMode) -> None:
self.logger.info(f"Arming {mode}")
self.mode = mode
self.securitySystemState = {
**self.securitySystemState,
"mode": mode,
}
if mode == SecuritySystemMode.Disarmed.value:
await self.get_or_create_siren().turnOff()
@ArloDeviceBase.async_print_exception_guard
async def disarmSecuritySystem(self) -> None:
self.logger.info(f"Disarming")
self.mode = SecuritySystemMode.Disarmed.value
self.securitySystemState = {
**self.securitySystemState,
"mode": SecuritySystemMode.Disarmed.value,
}
await self.get_or_create_siren().turnOff()

View File

@@ -1,6 +1,7 @@
paho-mqtt==1.6.1
sseclient==0.0.22
requests
cachetools
scrypted-arlo-go==0.0.1
--extra-index-url=https://www.piwheels.org/simple/
--extra-index-url=https://bjia56.github.io/scrypted-arlo-go/

View File

@@ -1,5 +1,5 @@
import { Deferred } from '@scrypted/common/src/deferred';
import sdk, { AudioSensor, Camera, Intercom, MotionSensor, ObjectsDetected, OnOff, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, VideoCamera, VideoCameraConfiguration } from '@scrypted/sdk';
import sdk, { AudioSensor, Camera, Intercom, MotionSensor, ObjectsDetected, OnOff, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, DeviceProvider, VideoCamera, VideoCameraConfiguration } from '@scrypted/sdk';
import { defaultObjectDetectionContactSensorTimeout } from '../camera-mixin';
import { addSupportedType, bindCharacteristic, DummyDevice } from '../common';
import { AudioRecordingCodec, AudioRecordingCodecType, AudioRecordingSamplerate, AudioStreamingCodec, AudioStreamingCodecType, AudioStreamingSamplerate, CameraController, CameraRecordingConfiguration, CameraRecordingDelegate, CameraRecordingOptions, CameraStreamingOptions, Characteristic, CharacteristicEventTypes, H264Level, H264Profile, MediaContainerType, OccupancySensor, RecordingPacket, Service, SRTPCryptoSuites, VideoCodecType, WithUUID } from '../hap';
@@ -7,7 +7,7 @@ import type { HomeKitPlugin } from '../main';
import { handleFragmentsRequests, iframeIntervalSeconds } from './camera/camera-recording';
import { createCameraStreamingDelegate } from './camera/camera-streaming';
import { FORCE_OPUS } from './camera/camera-utils';
import { makeAccessory } from './common';
import { makeAccessory, mergeOnOffDevicesByType } from './common';
const { deviceManager, systemManager } = sdk;
@@ -303,6 +303,15 @@ addSupportedType({
}
}
if (device.interfaces.includes(ScryptedInterface.DeviceProvider)) {
// merge in lights
const { devices } = mergeOnOffDevicesByType(device as ScryptedDevice as ScryptedDevice & DeviceProvider, accessory, ScryptedDeviceType.Light);
// ensure child devices are skipped by the rest of homekit by
// reporting that they've been merged
devices.map(device => homekitPlugin.mergedDevices.add(device.id));
}
return accessory;
}
});

View File

@@ -167,29 +167,30 @@ export function addFan(device: ScryptedDevice & Fan & OnOff, accessory: Accessor
}
/*
* addChildSirens looks for siren-type child devices of the given device provider
* and merges them as switches to the accessory represented by the device provider.
* mergeOnOffDevicesByType looks for the specified type of child devices under the
* given device provider and merges them as switches to the accessory represented
* by the device provider.
*
* Returns the services created as well as all of the child siren devices which have
* Returns the services created as well as all of the child OnOff devices which have
* been merged.
*/
export function addChildSirens(device: ScryptedDevice & DeviceProvider, accessory: Accessory): { services: Service[], devices: (ScryptedDevice & OnOff)[] } {
export function mergeOnOffDevicesByType(device: ScryptedDevice & DeviceProvider, accessory: Accessory, type: ScryptedDeviceType): { services: Service[], devices: (ScryptedDevice & OnOff)[] } {
if (!device.interfaces.includes(ScryptedInterface.DeviceProvider))
return undefined;
const children = getChildDevices(device);
const sirenDevices = [];
const mergedDevices = [];
const services = children.map((child: ScryptedDevice & OnOff) => {
if (child.type !== ScryptedDeviceType.Siren || !child.interfaces.includes(ScryptedInterface.OnOff))
if (child.type !== type || !child.interfaces.includes(ScryptedInterface.OnOff))
return undefined;
const onOffService = getOnOffService(child, accessory, Service.Switch)
sirenDevices.push(child);
mergedDevices.push(child);
return onOffService;
});
return {
services: services.filter(service => !!service),
devices: sirenDevices,
devices: mergedDevices,
};
}

View File

@@ -1,7 +1,7 @@
import { SecuritySystem, SecuritySystemMode, SecuritySystemObstruction, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, DeviceProvider } from '@scrypted/sdk';
import { addSupportedType, bindCharacteristic, DummyDevice } from '../common';
import { Characteristic, CharacteristicEventTypes, CharacteristicSetCallback, CharacteristicValue, Service } from '../hap';
import { makeAccessory, addChildSirens } from './common';
import { makeAccessory, mergeOnOffDevicesByType } from './common';
import type { HomeKitPlugin } from "../main";
addSupportedType({
@@ -90,7 +90,8 @@ addSupportedType({
() => !!device.securitySystemState?.triggered);
if (device.interfaces.includes(ScryptedInterface.DeviceProvider)) {
const { devices } = addChildSirens(device as ScryptedDevice as ScryptedDevice & DeviceProvider, accessory);
// merge in sirens
const { devices } = mergeOnOffDevicesByType(device as ScryptedDevice as ScryptedDevice & DeviceProvider, accessory, ScryptedDeviceType.Siren);
// ensure child devices are skipped by the rest of homekit by
// reporting that they've been merged

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/objectdetector",
"version": "0.0.113",
"version": "0.0.116",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/objectdetector",
"version": "0.0.113",
"version": "0.0.116",
"license": "Apache-2.0",
"dependencies": {
"@scrypted/common": "file:../../common",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/objectdetector",
"version": "0.0.113",
"version": "0.0.116",
"description": "Scrypted Video Analysis Plugin. Installed alongside a detection service like OpenCV or TensorFlow.",
"author": "Scrypted",
"license": "Apache-2.0",

View File

@@ -1,6 +1,7 @@
import { Deferred } from "@scrypted/common/src/deferred";
import { ffmpegLogInitialOutput, safeKillFFmpeg } from "@scrypted/common/src/media-helpers";
import { ffmpegLogInitialOutput, safeKillFFmpeg, safePrintFFmpegArguments } from "@scrypted/common/src/media-helpers";
import { readLength, readLine } from "@scrypted/common/src/read-stream";
import { addVideoFilterArguments } from "@scrypted/common/src/ffmpeg-helpers";
import sdk, { FFmpegInput, Image, ImageOptions, MediaObject, ScryptedDeviceBase, ScryptedMimeTypes, VideoFrame, VideoFrameGenerator, VideoFrameGeneratorOptions } from "@scrypted/sdk";
import child_process from 'child_process';
import sharp from 'sharp';
@@ -99,10 +100,15 @@ export class FFmpegVideoFrameGenerator extends ScryptedDeviceBase implements Vid
'pipe:3',
];
// this seems to reduce latency.
addVideoFilterArguments(args, 'fps=10', 'fps');
const cp = child_process.spawn(await sdk.mediaManager.getFFmpegPath(), args, {
stdio: ['pipe', 'pipe', 'pipe', 'pipe'],
});
ffmpegLogInitialOutput(this.console, cp);
const console = mediaObject?.sourceId ? sdk.deviceManager.getMixinConsole(mediaObject.sourceId) : this.console;
safePrintFFmpegArguments(console, args);
ffmpegLogInitialOutput(console, cp);
let finished = false;
let frameDeferred: Deferred<RawFrame>;
@@ -143,14 +149,14 @@ export class FFmpegVideoFrameGenerator extends ScryptedDeviceBase implements Vid
});
}
else {
// this.console.warn('skipped frame');
this.console.warn('skipped frame');
}
}
}
catch (e) {
}
finally {
this.console.log('finished reader');
console.log('finished reader');
finished = true;
frameDeferred?.reject(new Error('frame generator finished'));
}
@@ -171,16 +177,20 @@ export class FFmpegVideoFrameGenerator extends ScryptedDeviceBase implements Vid
}
});
const vipsImage = new VipsImage(image, width, height);
const mo = await createVipsMediaObject(vipsImage);
yield mo;
vipsImage.image.destroy();
vipsImage.image = undefined;
try {
const mo = await createVipsMediaObject(vipsImage);
yield mo;
}
finally {
vipsImage.image = undefined;
image.destroy();
}
}
}
catch (e) {
}
finally {
this.console.log('finished generator');
console.log('finished generator');
finished = true;
safeKillFFmpeg(cp);
}

View File

@@ -980,7 +980,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
this.storageSettings.settings.motionSensorSupplementation.hide = !this.hasMotionType || !this.mixinDeviceInterfaces.includes(ScryptedInterface.MotionSensor);
this.storageSettings.settings.captureMode.hide = this.hasMotionType || !!this.plugin.storageSettings.values.newPipeline;
this.storageSettings.settings.newPipeline.hide = this.hasMotionType || !this.plugin.storageSettings.values.newPipeline;
this.storageSettings.settings.newPipeline.hide = !this.plugin.storageSettings.values.newPipeline;
this.storageSettings.settings.detectionDuration.hide = this.hasMotionType;
this.storageSettings.settings.detectionTimeout.hide = this.hasMotionType;
this.storageSettings.settings.motionDuration.hide = !this.hasMotionType;
@@ -1248,8 +1248,9 @@ class ObjectDetectionPlugin extends AutoenableMixinProvider implements Settings,
storageSettings = new StorageSettings(this, {
newPipeline: {
title: 'New Video Pipeline',
description: 'WARNING! DO NOT ENABLE: Use the new video pipeline. Leave blank to use the legacy pipeline.',
description: 'Enables the new video pipeline addded on 2023/03/25. If there are issues with motion or object detection, disable this to switch back to the old pipeline. Then reload the plugin.',
type: 'boolean',
defaultValue: true,
},
activeMotionDetections: {
title: 'Active Motion Detection Sessions',

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/pam-diff",
"version": "0.0.17",
"version": "0.0.18",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/pam-diff",
"version": "0.0.17",
"version": "0.0.18",
"hasInstallScript": true,
"dependencies": {
"@types/node": "^16.6.1",

View File

@@ -43,5 +43,5 @@
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
},
"version": "0.0.17"
"version": "0.0.18"
}

View File

@@ -231,7 +231,7 @@ ENDHDR
detections.push(
{
className: 'motion',
score: trigger.percent / 100,
score: 1,
boundingBox: [blob.minX, blob.minY, blob.maxX - blob.minX, blob.maxY - blob.minY],
}
)
@@ -241,7 +241,7 @@ ENDHDR
detections.push(
{
className: 'motion',
score: trigger.percent / 100,
score: 1,
}
)
}

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/python-codecs",
"version": "0.1.18",
"version": "0.1.19",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/python-codecs",
"version": "0.1.18",
"version": "0.1.19",
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
}

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/python-codecs",
"version": "0.1.18",
"version": "0.1.19",
"description": "Python Codecs for Scrypted",
"keywords": [
"scrypted",

View File

@@ -34,7 +34,7 @@ async def generateVideoFramesGstreamer(mediaObject: scrypted_sdk.MediaObject, op
else:
raise Exception('unknown container %s' % container)
elif videosrc.startswith('rtsp'):
videosrc = 'rtspsrc buffer-mode=0 location=%s protocols=tcp latency=0 is-live=false' % videosrc
videosrc = 'rtspsrc buffer-mode=0 location=%s protocols=tcp latency=0' % videosrc
if videoCodec == 'h264':
videosrc += ' ! rtph264depay ! h264parse'

View File

@@ -27,7 +27,9 @@
"${workspaceFolder}/**/*.js"
],
"env": {
"SCRYPTED_PYTHON_PATH": "python3.10",
// force usage of system python because brew python is 3.11
// which has no wheels for coreml tools or tflite-runtime
"SCRYPTED_PYTHON_PATH": "/usr/bin/python3",
// "SCRYPTED_SHARED_WORKER": "true",
// "SCRYPTED_DISABLE_AUTHENTICATION": "true",
// "DEBUG": "*",

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/server",
"version": "0.7.26",
"version": "0.7.32",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/server",
"version": "0.7.26",
"version": "0.7.32",
"license": "ISC",
"dependencies": {
"@mapbox/node-pre-gyp": "^1.0.10",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/server",
"version": "0.7.28",
"version": "0.7.33",
"description": "",
"dependencies": {
"@mapbox/node-pre-gyp": "^1.0.10",

View File

@@ -218,6 +218,7 @@ class PluginRemote:
consoles: Mapping[str, Future[Tuple[StreamReader, StreamWriter]]] = {}
def __init__(self, peer: rpc.RpcPeer, api, pluginId, hostInfo, loop: AbstractEventLoop):
self.allMemoryStats = {}
self.peer = peer
self.api = api
self.pluginId = pluginId
@@ -369,6 +370,18 @@ class PluginRemote:
plugin_volume = os.environ.get('SCRYPTED_PLUGIN_VOLUME')
# it's possible to run 32bit docker on aarch64, which cause pip requirements
# to fail because pip only allows filtering on machine, even if running a different architeture.
# this will cause prebuilt wheel installation to fail.
if platform.machine() == 'aarch64' and platform.architecture()[0] == '32bit':
print('=============================================')
print('Python machine vs architecture mismatch detected. Plugin installation may fail.')
print('If Scrypted is running in docker, the docker version may be 32bit while the host kernel is 64bit.')
print('This may be resolved by reinstalling a 64bit docker.')
print('The docker architecture can be checked with the command: "file $(which docker)"')
print('The host architecture can be checked with: "uname -m"')
print('=============================================')
python_version = 'python%s' % str(
sys.version_info[0])+"."+str(sys.version_info[1])
print('python version:', python_version)
@@ -446,6 +459,8 @@ class PluginRemote:
self.deviceManager = DeviceManager(self.nativeIds, self.systemManager)
self.mediaManager = MediaManager(await self.api.getMediaManager())
await self.start_stats_runner()
try:
from scrypted_sdk import sdk_init2 # type: ignore
@@ -479,7 +494,7 @@ class PluginRemote:
forkPeer.peerName = 'thread'
async def updateStats(stats):
allMemoryStats[forkPeer] = stats
self.allMemoryStats[forkPeer] = stats
forkPeer.params['updateStats'] = updateStats
async def forkReadLoop():
@@ -489,7 +504,7 @@ class PluginRemote:
# traceback.print_exc()
print('fork read loop exited')
finally:
allMemoryStats.pop(forkPeer)
self.allMemoryStats.pop(forkPeer)
parent_conn.close()
rpcTransport.executor.shutdown()
asyncio.run_coroutine_threadsafe(forkReadLoop(), loop=self.loop)
@@ -580,16 +595,8 @@ class PluginRemote:
async def getServicePort(self, name):
pass
allMemoryStats = {}
async def plugin_async_main(loop: AbstractEventLoop, rpcTransport: rpc_reader.RpcTransport):
peer, readLoop = await rpc_reader.prepare_peer_readloop(loop, rpcTransport)
peer.params['print'] = print
peer.params['getRemote'] = lambda api, pluginId, hostInfo: PluginRemote(peer, api, pluginId, hostInfo, loop)
async def get_update_stats():
update_stats = await peer.getParam('updateStats')
async def start_stats_runner(self):
update_stats = await self.peer.getParam('updateStats')
if not update_stats:
print('host did not provide update_stats')
return
@@ -608,7 +615,7 @@ async def plugin_async_main(loop: AbstractEventLoop, rpcTransport: rpc_reader.Rp
except:
heapTotal = 0
for _, stats in allMemoryStats.items():
for _, stats in self.allMemoryStats.items():
ptime += stats['cpu']['user']
heapTotal += stats['memoryUsage']['heapTotal']
@@ -621,12 +628,15 @@ async def plugin_async_main(loop: AbstractEventLoop, rpcTransport: rpc_reader.Rp
'heapTotal': heapTotal,
},
}
asyncio.run_coroutine_threadsafe(update_stats(stats), loop)
loop.call_later(10, stats_runner)
asyncio.run_coroutine_threadsafe(update_stats(stats), self.loop)
self.loop.call_later(10, stats_runner)
stats_runner()
asyncio.run_coroutine_threadsafe(get_update_stats(), loop)
async def plugin_async_main(loop: AbstractEventLoop, rpcTransport: rpc_reader.RpcTransport):
peer, readLoop = await rpc_reader.prepare_peer_readloop(loop, rpcTransport)
peer.params['print'] = print
peer.params['getRemote'] = lambda api, pluginId, hostInfo: PluginRemote(peer, api, pluginId, hostInfo, loop)
try:
await readLoop()

View File

@@ -1,3 +1,4 @@
import os from 'os';
import { Device, EngineIOHandler } from '@scrypted/types';
import AdmZip from 'adm-zip';
import crypto from 'crypto';
@@ -310,7 +311,7 @@ export class PluginHost {
this.worker.stdout.on('data', data => console.log(data.toString()));
this.worker.stderr.on('data', data => console.error(data.toString()));
const consoleHeader = `server version: ${serverVersion}\nplugin version: ${this.pluginId} ${this.packageJson.version}\n`;
const consoleHeader = `${os.platform()} ${os.arch()} ${os.machine()} ${os.version()}\nserver version: ${serverVersion}\nplugin version: ${this.pluginId} ${this.packageJson.version}\n`;
this.consoleServer = createConsoleServer(this.worker.stdout, this.worker.stderr, consoleHeader);
const disconnect = () => {

View File

@@ -39,12 +39,6 @@ export function startPluginRemote(mainFilename: string, pluginId: string, peerSe
const { getDeviceConsole, getMixinConsole } = prepareConsoles(() => peer.selfName, () => systemManager, () => deviceManager, getPlugins);
// process.cpuUsage is for the entire process.
// process.memoryUsage is per thread.
const allMemoryStats = new Map<NodeThreadWorker, NodeJS.MemoryUsage>();
peer.getParam('updateStats').then(updateStats => startStatsUpdater(allMemoryStats, updateStats));
let replPort: Promise<number>;
let _pluginConsole: Console;
@@ -240,6 +234,12 @@ export function startPluginRemote(mainFilename: string, pluginId: string, peerSe
await installOptionalDependencies(getPluginConsole(), packageJson);
// process.cpuUsage is for the entire process.
// process.memoryUsage is per thread.
const allMemoryStats = new Map<NodeThreadWorker, NodeJS.MemoryUsage>();
// start the stats updater/watchdog after installation has finished, as that may take some time.
peer.getParam('updateStats').then(updateStats => startStatsUpdater(allMemoryStats, updateStats));
const main = pluginReader('main.nodejs.js');
pluginReader = undefined;
const script = main.toString();

View File

@@ -39,7 +39,10 @@ export class PythonRuntimeWorker extends ChildProcessWorker {
'/usr/local/lib/gstreamer-1.0',
];
for (const gstPath of gstPaths) {
if (fs.existsSync(path.join(gstPath, 'libgstx264.dylib'))) {
// search for common plugins.
if (fs.existsSync(path.join(gstPath, 'libgstx264.dylib'))
|| fs.existsSync(path.join(gstPath, 'libgstlibav.dylib'))
|| fs.existsSync(path.join(gstPath, 'libgstvideotestsrc.dylib'))) {
gstEnv['GST_PLUGIN_PATH'] = gstPath;
break;
}