Compare commits

..

19 Commits

Author SHA1 Message Date
Koushik Dutta
2863756bd6 Revert "webrtc: startRtpForwarderProcess remove werift dependency"
This reverts commit 143a0b2c41.
2023-04-03 14:26:56 -07:00
Koushik Dutta
cc408850a0 videoanalysis: changing pipeline should restart video analysis 2023-04-03 12:26:02 -07:00
Koushik Dutta
ed1ceeda51 core: return correct acls for admins 2023-04-03 11:16:14 -07:00
Koushik Dutta
df09d8e92a Merge branch 'main' of github.com:koush/scrypted 2023-04-03 08:34:35 -07:00
Koushik Dutta
298ac960d1 core: fix checkbox ui 2023-04-03 08:34:30 -07:00
Nick Berardi
62d4d55aae unifi: added native zoom capability (#684) 2023-04-03 08:24:56 -07:00
Nick Berardi
a2121c0dc5 alexa: add setting to publish debug events to console (#685) 2023-04-03 08:24:31 -07:00
Koushik Dutta
9b5ea27c0b core: fix checkbox ui 2023-04-03 08:23:20 -07:00
Koushik Dutta
0b0e90fc04 server: fix version being off by 1 in release notes/tag 2023-04-02 10:13:52 -07:00
Koushik Dutta
d8aff609bf core: publish 2023-04-02 09:37:54 -07:00
Koushik Dutta
d8283c261a homekit: publish beta 2023-04-02 09:37:49 -07:00
Koushik Dutta
e3aca964be Merge branch 'main' of github.com:koush/scrypted 2023-04-02 09:34:07 -07:00
Koushik Dutta
a96025c45f prerelease 2023-04-02 09:33:56 -07:00
Koushik Dutta
6afd4b4579 server: aggressively kill python plugin processes and forks 2023-04-02 09:33:48 -07:00
Brett Jia
f97669949d sdk, core: add Charger interface (#680)
* add Charger interface

* add charger icon to web ui

* import correct path

* get charge state displayed correctly
2023-04-01 21:03:54 -07:00
Koushik Dutta
0a0a31574f Merge branch 'main' of github.com:koush/scrypted 2023-04-01 15:02:46 -07:00
Koushik Dutta
90fb751a22 reolink: stream spec hints 2023-04-01 15:02:39 -07:00
Brett Jia
b8d06fada5 arlo: toggle for wired power + audio sensors (#679)
* remove hints to force prebuffer snapshots to fetch stream

* cleanup exception guard + catch prebuffer snapshot errors

* settings to remove Battery interface

* delayed init to load battery percentage

* fix plugin crash due to missing smart features dict

* properly add toggle for wired power

* fix race condition when multiple settings are updated at once

* bump 0.7.10 for beta

* audio detection + clean up battery/no-battery settings

* bump 0.7.11 for beta

* remove basestation models from camera class

* bump 0.7.12 for release
2023-04-01 14:28:54 -07:00
Koushik Dutta
2cecb1686f core: fix ui hang, readd launcher 2023-03-31 23:21:10 -07:00
43 changed files with 447 additions and 146 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/alexa",
"version": "0.2.3",
"version": "0.2.4",
"scripts": {
"scrypted-setup-project": "scrypted-setup-project",
"prescrypted-setup-project": "scrypted-package-json",

View File

@@ -15,6 +15,11 @@ const includeToken = 4;
export let DEBUG = false;
function debug(...args: any[]) {
if (DEBUG)
console.debug(...args);
}
class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, MixinProvider, Settings {
storageSettings = new StorageSettings(this, {
tokenInfo: {
@@ -34,6 +39,14 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
description: 'This is the endpoint Alexa will use to send events to. This is set after you login.',
type: 'string',
readonly: true
},
debug: {
title: 'Debug Events',
description: 'Log all events to the console. This will be very noisy and should not be left enabled.',
type: 'boolean',
onPut(oldValue: boolean, newValue: boolean) {
DEBUG = newValue;
}
}
});
@@ -44,6 +57,8 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
constructor(nativeId?: string) {
super(nativeId);
DEBUG = this.storageSettings.values.debug ?? false;
alexaHandlers.set('Alexa.Authorization/AcceptGrant', this.onAlexaAuthorization);
alexaHandlers.set('Alexa.Discovery/Discover', this.onDiscoverEndpoints);
@@ -141,12 +156,23 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
if (!supportedType)
return;
const report = await supportedType.sendEvent(eventSource, eventDetails, eventData);
let report = await supportedType.sendEvent(eventSource, eventDetails, eventData);
if (!report && eventDetails.eventInterface === ScryptedInterface.Online) {
report = {};
}
if (!report && eventDetails.eventInterface === ScryptedInterface.Battery) {
report = {};
}
if (!report) {
this.console.warn(`${eventDetails.eventInterface}.${eventDetails.property} not supported for device ${eventSource.type}`);
return;
}
debug("event", eventDetails.eventInterface, eventDetails.property, eventSource.type);
let data = {
"event": {
"header": {
@@ -234,7 +260,7 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
const endpoint = await this.getAlexaEndpoint();
const self = this;
this.console.assert(!DEBUG, `event:`, data);
debug("send event to alexa", data);
return axios.post(`https://${endpoint}/v3/events`, data, {
headers: {
@@ -570,6 +596,8 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
const { authorization } = request.headers;
if (!this.validAuths.has(authorization)) {
try {
debug("making authorization request to Scrypted");
await axios.get('https://home.scrypted.app/_punch/getcookie', {
headers: {
'Authorization': authorization,
@@ -590,11 +618,11 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
const { directive } = body;
const { namespace, name } = directive.header;
this.console.assert(!DEBUG, `request: ${namespace}/${name}`);
const mapName = `${namespace}/${name}`;
const handler = alexaHandlers.get(mapName);
debug("received directive from alexa", mapName, body);
const handler = alexaHandlers.get(mapName);
if (handler)
return handler.apply(this, [request, response, directive]);
@@ -641,7 +669,7 @@ class HttpResponseLoggingImpl implements AlexaHttpResponse {
if (options.code !== 200)
this.console.error(`response error ${options.code}:`, body);
else
this.console.assert(!DEBUG, `response ${options.code}:`, body);
debug("response to alexa directive", options.code, body);
if (typeof body === 'object')
body = JSON.stringify(body);

View File

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

View File

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

View File

@@ -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')

View File

@@ -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
return []

View File

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

View File

@@ -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

View File

@@ -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)

View File

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

View File

@@ -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)

View File

@@ -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()
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

View File

@@ -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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/core",
"version": "0.1.103",
"version": "0.1.108",
"description": "Scrypted Core plugin. Provides the UI, websocket, and engine.io APIs.",
"author": "Scrypted",
"license": "Apache-2.0",

View File

@@ -92,6 +92,17 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Eng
this.automationCore = new AutomationCore();
})();
deviceManager.onDeviceDiscovered({
name: 'Add to Launcher',
nativeId: 'launcher',
interfaces: [
'@scrypted/launcher-ignore',
ScryptedInterface.MixinProvider,
ScryptedInterface.Readme,
],
type: ScryptedDeviceType.Builtin,
});
(async () => {
await deviceManager.onDeviceDiscovered(
{

View File

@@ -23,6 +23,15 @@ export class User extends ScryptedDeviceBase implements Settings, ScryptedUser {
})
async getScryptedUserAccessControl(): Promise<ScryptedUserAccessControl> {
const usersService = await sdk.systemManager.getComponent('users');
const users: DBUser[] = await usersService.getAllUsers();
const user = users.find(user => user.username === this.username);
if (!user)
throw new Error("user not found");
if (user.admin)
return;
const self = sdk.deviceManager.getDeviceState(this.nativeId);
const ret: ScryptedUserAccessControl = {

View File

@@ -1,3 +1,4 @@
import { timeoutPromise } from "@scrypted/common/src/promise-utils";
import { MixinProvider, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, SystemManager } from "@scrypted/types";
export async function setMixin(systemManager: SystemManager, device: ScryptedDevice, mixinId: string, enabled: boolean) {
@@ -14,19 +15,21 @@ export async function setMixin(systemManager: SystemManager, device: ScryptedDev
plugins.setMixins(device.id, mixins);
}
export function getAllDevices(systemManager: SystemManager) {
return Object.keys(systemManager.getSystemState()).map(id => systemManager.getDeviceById(id)).filter(device => !!device);
export function getAllDevices<T>(systemManager: SystemManager) {
return Object.keys(systemManager.getSystemState()).map(id => systemManager.getDeviceById(id) as T & ScryptedDevice).filter(device => !!device);
}
export async function getDeviceAvailableMixins(systemManager: SystemManager, device: ScryptedDevice): Promise<(ScryptedDevice & MixinProvider)[]> {
const results = await Promise.all(getAllDevices(systemManager).map(async (check) => {
const results = await Promise.all(getAllDevices<MixinProvider>(systemManager).map(async (check) => {
try {
if (check.interfaces.includes(ScryptedInterface.MixinProvider)) {
if (await (check as any as MixinProvider).canMixin(device.type, device.interfaces))
return check as MixinProvider & ScryptedDevice;
const canMixin = await timeoutPromise(5000, check.canMixin(device.type, device.interfaces));
if (canMixin)
return check;
}
}
catch (e) {
console.warn(check.name, 'canMixin error', e)
}
}));
@@ -47,7 +50,7 @@ export async function getMixinProviderAvailableDevices(systemManager: SystemMana
devices.map(async (device) => {
try {
if (device.mixins?.includes(mixinProvider.id) || (await mixinProvider.canMixin(device.type, device.interfaces)))
return device;
return device;
}
catch (e) {
}

View File

@@ -215,6 +215,7 @@ import Notifier from "../interfaces/Notifier.vue";
import OnOff from "../interfaces/OnOff.vue";
import Brightness from "../interfaces/Brightness.vue";
import Battery from "../interfaces/Battery.vue";
import Charger from "../interfaces/Charger.vue";
import Lock from "../interfaces/Lock.vue";
import ColorSettingHsv from "../interfaces/ColorSettingHsv.vue";
import ColorSettingRgb from "../interfaces/ColorSettingRgb.vue";
@@ -263,6 +264,7 @@ const cardHeaderInterfaces = [
ScryptedInterface.AudioSensor,
ScryptedInterface.HumiditySensor,
ScryptedInterface.Thermometer,
ScryptedInterface.Charger,
ScryptedInterface.Battery,
ScryptedInterface.Lock,
ScryptedInterface.OnOff,
@@ -362,6 +364,7 @@ export default {
Lock,
OnOff,
Charger,
Battery,
Thermometer,
HumiditySensor,

View File

@@ -0,0 +1,52 @@
<template>
<v-tooltip left>
<template v-slot:activator="{ on }">
<v-icon
v-on="on"
v-if="lazyValue.chargeState === Charging"
class="mr-1 mr-1"
small
>fa-plug</v-icon>
<v-icon
v-on="on"
v-else-if="lazyValue.chargeState == Trickle"
class="mr-1 mr-1"
small
>fa-plug-circle-minus</v-icon>
<v-icon
v-on="on"
v-else
class="mr-1 mr-1"
small
>fa-plug-circle-xmark</v-icon>
</template>
<span>{{ chargeText }}</span>
</v-tooltip>
</template>
<script>
import { ChargeState } from '@scrypted/types';
import RPCInterface from "./RPCInterface.vue";
export default {
mixins: [RPCInterface],
data() {
return {
Charging: ChargeState.Charging,
Trickle: ChargeState.Trickle,
NotCharging: ChargeState.NotCharging,
};
},
computed: {
chargeText() {
if (this.lazyValue.chargeState === "trickle") {
return "Trickle Charging";
}
if (this.lazyValue.chargeState === "charging") {
return "Charging";
}
return "Not Charging";
},
},
};
</script>

View File

@@ -81,6 +81,7 @@ export default {
const mediaManager = this.$scrypted.mediaManager;
const mo = await mediaManager.createMediaObject(buffer, 'image/*');
const detected = await this.rpc().detectObjects(mo);
console.log(detected);
this.lastDetection = detected;
},
allowDrop(ev) {

View File

@@ -41,8 +41,7 @@
</template>
</DevicePicker>
<DevicePicker v-else-if="lazyValue.type === 'interface'" v-model="lazyValue.value" :multiple="lazyValue.multiple"
:readonly="lazyValue.readonly" :devices="interfaces" :title="lazyValue.title"
:description="lazyValue.description">
:readonly="lazyValue.readonly" :devices="interfaces" :title="lazyValue.title" :description="lazyValue.description">
<template v-slot:append-outer>
<v-btn v-if="dirty && device" color="success" @click="save" class="shift-up">
<v-icon>send</v-icon>
@@ -52,7 +51,7 @@
<div v-else-if="lazyValue.type === 'clippath'" class="mb-2">
<v-btn small block @click="editZone">{{ lazyValue.title }} </v-btn>
<Camera :value="device" :device="device" :clipPathValue="sanitizedClipPathValue" :showDialog="editingZone"
:hidePreview="true" @dialog="editingZoneChanged" @clipPath="lazyValue.value = $event"></Camera>
:hidePreview="true" @dialog="editingZoneChanged" @clipPath="updateClipPath"></Camera>
</div>
<v-textarea v-else-if="lazyValue.type === 'textarea'" v-model="lazyValue.value" outlined persistent-hint
:hint="lazyValue.description" :label="lazyValue.title">
@@ -88,6 +87,7 @@ export default {
data() {
return {
editingZone: false,
clipPathThrottle: null,
};
},
watch: {
@@ -142,7 +142,7 @@ export default {
);
},
set(val) {
this.lazyValue.value = val.toString();
this.lazyValue.value = !!val;
},
},
dirty() {
@@ -228,6 +228,17 @@ export default {
},
methods: {
onChange() { },
updateClipPath(e) {
clearTimeout(this.clipPathThrottle);
this.clipPathThrottle = setTimeout(() => {
this.lazyValue.value = e;
this.rpc().putSetting(
this.lazyValue.key,
this.createInputValue().value
);
this.onInput();
}, 500)
},
editingZoneChanged(value) {
this.editingZone = value;
if (!value) {

View File

@@ -58,6 +58,8 @@ import {
faLightbulb,
faToggleOn,
faPlug,
faPlugCircleMinus,
faPlugCircleXmark,
faExclamationTriangle,
faSun,
faCode,
@@ -150,6 +152,8 @@ const icons: IconDefinition[] =[
faLightbulb,
faToggleOn,
faPlug,
faPlugCircleMinus,
faPlugCircleXmark,
faExclamationTriangle,
faSun,
faCode,

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/homekit",
"version": "1.2.22",
"version": "1.2.23",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/homekit",
"version": "1.2.22",
"version": "1.2.23",
"dependencies": {
"@koush/werift-src": "file:../../external/werift",
"check-disk-space": "^3.3.1",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/homekit",
"version": "1.2.22",
"version": "1.2.23",
"description": "HomeKit Plugin for Scrypted",
"scripts": {
"scrypted-setup-project": "scrypted-setup-project",

View File

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

View File

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

View File

@@ -61,6 +61,10 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
choices,
}
},
onPut: () => {
this.endObjectDetection();
this.maybeStartMotionDetection();
},
defaultValue: 'Default',
},
motionSensorSupplementation: {

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/reolink",
"version": "0.0.17",
"version": "0.0.19",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/reolink",
"version": "0.0.17",
"version": "0.0.19",
"license": "Apache",
"dependencies": {
"@koush/axios-digest-auth": "^0.8.5",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/reolink",
"version": "0.0.17",
"version": "0.0.19",
"description": "Reolink Plugin for Scrypted",
"author": "Scrypted",
"license": "Apache",

View File

@@ -115,6 +115,20 @@ class ReolinkCamera extends RtspSmartCamera implements Camera {
});
}
// rough guesses for rebroadcast stream selection.
ret[0].video = {
width: 2560,
height: 1920,
}
ret[1].video = {
width: 896,
height: 672,
}
ret[2].video = {
width: 640,
height: 480,
}
const channel = (this.getRtspChannel() + 1).toString().padStart(2, '0');
const rtspPreviews = [
`h264Preview_${channel}_main`,

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/unifi-protect",
"version": "0.0.131",
"version": "0.0.132",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/unifi-protect",
"version": "0.0.131",
"version": "0.0.132",
"license": "Apache",
"dependencies": {
"@koush/unifi-protect": "file:../../external/unifi-protect",
@@ -61,7 +61,7 @@
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.2.68",
"version": "0.2.86",
"license": "ISC",
"dependencies": {
"@babel/preset-typescript": "^7.18.6",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/unifi-protect",
"version": "0.0.131",
"version": "0.0.132",
"description": "Unifi Protect Plugin for Scrypted",
"author": "Scrypted",
"license": "Apache",

View File

@@ -1,6 +1,6 @@
import { ffmpegLogInitialOutput, safeKillFFmpeg } from '@scrypted/common/src/media-helpers';
import { fitHeightToWidth } from "@scrypted/common/src/resolution-utils";
import sdk, { Camera, DeviceProvider, FFmpegInput, Intercom, MediaObject, MediaStreamOptions, MediaStreamUrl, MotionSensor, Notifier, NotifierOptions, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, OnOff, PictureOptions, ResponseMediaStreamOptions, ResponsePictureOptions, ScryptedDeviceBase, ScryptedInterface, ScryptedMimeTypes, Setting, Settings, VideoCamera, VideoCameraConfiguration } from "@scrypted/sdk";
import sdk, { Camera, DeviceProvider, FFmpegInput, Intercom, MediaObject, MediaStreamOptions, MediaStreamUrl, MotionSensor, Notifier, NotifierOptions, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, OnOff, Online, PanTiltZoom, PanTiltZoomCommand, PictureOptions, ResponseMediaStreamOptions, ResponsePictureOptions, ScryptedDeviceBase, ScryptedInterface, ScryptedMimeTypes, Setting, Settings, VideoCamera, VideoCameraConfiguration } from "@scrypted/sdk";
import child_process, { ChildProcess } from 'child_process';
import { once } from "events";
import { Readable } from "stream";
@@ -38,7 +38,7 @@ export class UnifiPackageCamera extends ScryptedDeviceBase implements Camera, Vi
}
}
export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Intercom, Camera, VideoCamera, VideoCameraConfiguration, MotionSensor, Settings, ObjectDetector, DeviceProvider, OnOff {
export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Intercom, Camera, VideoCamera, VideoCameraConfiguration, MotionSensor, Settings, ObjectDetector, DeviceProvider, OnOff, PanTiltZoom, Online {
motionTimeout: NodeJS.Timeout;
detectionTimeout: NodeJS.Timeout;
ringTimeout: NodeJS.Timeout;
@@ -61,6 +61,15 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco
this.updateState(protectCamera);
}
async ptzCommand(command: PanTiltZoomCommand): Promise<void> {
const camera = this.findCamera() as any;
await this.protect.api.updateCamera(camera, {
ispSettings: {
zoomPosition: Math.abs(command.zoom * 100),
}
});
}
async setStatusLight(on: boolean) {
const camera = this.findCamera() as any;
await this.protect.api.updateCamera(camera, {
@@ -411,6 +420,11 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco
if (!camera)
return;
this.on = !!camera.ledSettings?.isEnabled;
this.online = !!camera.isConnected;
this.setMotionDetected(!!camera.isMotionDetected);
if (!!camera.featureFlags.canOpticalZoom) {
this.ptzCapabilities = { pan: false, tilt: false, zoom: true };
}
}
}

View File

@@ -376,6 +376,9 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
if (camera.featureFlags.hasLedStatus) {
d.interfaces.push(ScryptedInterface.OnOff);
}
if (camera.featureFlags.canOpticalZoom) {
d.interfaces.push(ScryptedInterface.PanTiltZoom);
}
d.interfaces.push(ScryptedInterface.ObjectDetector);
devices.push(d);
}

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/webrtc",
"version": "0.1.39",
"version": "0.1.40",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/webrtc",
"version": "0.1.39",
"version": "0.1.40",
"dependencies": {
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/webrtc",
"version": "0.1.39",
"version": "0.1.40",
"scripts": {
"scrypted-setup-project": "scrypted-setup-project",
"prescrypted-setup-project": "scrypted-package-json",

View File

@@ -1,3 +1,4 @@
import { RtpPacket } from "../../../external/werift/packages/rtp/src/rtp/rtp";
import { Deferred } from "@scrypted/common/src/deferred";
import { closeQuiet, createBindZero, listenZeroSingleClient, reserveUdpPort } from "@scrypted/common/src/listen-cluster";
import { ffmpegLogInitialOutput, safeKillFFmpeg, safePrintFFmpegArguments } from "@scrypted/common/src/media-helpers";
@@ -232,8 +233,6 @@ export async function startRtpForwarderProcess(console: Console, ffmpegInput: FF
// if the rtsp client is over tcp, then the restream server must also be tcp, as
// the rtp packets (which can be a max of 64k) may be too large for udp.
const clientIsTcp = await setupRtspClient(console, rtspClient, channel, audioSection, false, rtp => {
const payload = rtp.subarray(12);
// live555 sends rtp aac packets without AU header followed by ADTS packets (which contain codec info)
// which ffmpeg can not handle.
// the solution is to demux the adts and send that to ffmpeg raw.
@@ -241,8 +240,10 @@ export async function startRtpForwarderProcess(console: Console, ffmpegInput: FF
if (firstPacket) {
firstPacket = false;
if (audioSection.codec === 'aac') {
const packet = RtpPacket.deSerialize(rtp);
const buf = packet.payload;
// adts header is 12 bits of 1s
if (payload[0] == 0xff && (payload[1] & 0xf0) == 0xf0) {
if (buf[0] == 0xff && (buf[1] & 0xf0) == 0xf0) {
adts = true;
allowAudioTranscoderExit = true;
const ffmpegArgs = [
@@ -270,7 +271,8 @@ export async function startRtpForwarderProcess(console: Console, ffmpegInput: FF
rtspServer?.sendTrack(audioControl, rtp, false);
}
else {
audioPipe?.write(payload);
const packet = RtpPacket.deSerialize(rtp);
audioPipe?.write(packet.payload);
}
});

View File

@@ -15,6 +15,11 @@ class AirQuality(Enum):
Poor = "Poor"
Unknown = "Unknown"
class ChargeState(Enum):
Charging = "charging"
NotCharging = "not-charging"
Trickle = "trickle"
class FanMode(Enum):
Auto = "Auto"
Manual = "Manual"
@@ -85,6 +90,7 @@ class ScryptedInterface(Enum):
BufferConverter = "BufferConverter"
CO2Sensor = "CO2Sensor"
Camera = "Camera"
Charger = "Charger"
ColorSettingHsv = "ColorSettingHsv"
ColorSettingRgb = "ColorSettingRgb"
ColorSettingTemperature = "ColorSettingTemperature"
@@ -750,6 +756,10 @@ class Camera:
pass
pass
class Charger:
chargeState: ChargeState
pass
class ColorSettingHsv:
hsv: ColorHsv
async def setHsv(self, hue: float, saturation: float, value: float) -> None:
@@ -1342,6 +1352,7 @@ class ScryptedInterfaceProperty(Enum):
lockState = "lockState"
entryOpen = "entryOpen"
batteryLevel = "batteryLevel"
chargeState = "chargeState"
online = "online"
fromMimeType = "fromMimeType"
toMimeType = "toMimeType"
@@ -1618,6 +1629,13 @@ class DeviceState:
def batteryLevel(self, value: float):
self.setScryptedProperty("batteryLevel", value)
@property
def chargeState(self) -> ChargeState:
return self.getScryptedProperty("chargeState")
@chargeState.setter
def chargeState(self, value: ChargeState):
self.setScryptedProperty("chargeState", value)
@property
def online(self) -> bool:
return self.getScryptedProperty("online")
@@ -2097,6 +2115,13 @@ ScryptedInterfaceDescriptors = {
"batteryLevel"
]
},
"Charger": {
"name": "Charger",
"methods": [],
"properties": [
"chargeState"
]
},
"Refresh": {
"name": "Refresh",
"methods": [

View File

@@ -5,7 +5,7 @@ export type ScryptedNativeId = string | undefined;
/**
* All devices in Scrypted implement ScryptedDevice, which contains the id, name, and type. Add listeners to subscribe to events from that device.
*
*
* @category Core Reference
*/
export interface ScryptedDevice {
@@ -78,7 +78,7 @@ export interface EventDetails {
}
/**
* Returned when an event listener is attached to an EventEmitter. Call removeListener to unregister from events.
*
*
* @category Core Reference
*/
export interface EventListenerRegister {
@@ -230,7 +230,7 @@ export interface Notifier {
}
/**
* MediaObject is an intermediate object within Scrypted to represent all media objects. Plugins should use the MediaConverter to convert the Scrypted MediaObject into a desired type, whether it is a externally accessible URL, a Buffer, etc.
*
*
* @category Media Reference
*/
export interface MediaObject {
@@ -560,7 +560,7 @@ export interface ResponseMediaStreamOptions extends MediaStreamOptions {
/**
* Set this to true to allow for prebuffering even if the device implements the Battery interface.
* Handy if you have a device that can continuously prebuffer when on mains power, but you still
* Handy if you have a device that can continuously prebuffer when on mains power, but you still
* want battery status reported.
*/
allowBatteryPrebuffer?: boolean;
@@ -752,7 +752,7 @@ export interface VideoClips {
}
/**
* Intercom devices can playback audio.
* Intercom devices can playback audio.
*/
export interface Intercom {
startIntercom(media: MediaObject): Promise<void>;
@@ -874,7 +874,7 @@ export interface EntrySensor {
}
/**
* DeviceManager is the interface used by DeviceProvider to report new devices, device states, and device events to Scrypted.
*
*
* @category Device Provider Reference
*/
export interface DeviceManager {
@@ -952,7 +952,7 @@ export interface DeviceManager {
}
/**
* DeviceProvider acts as a controller/hub and exposes multiple devices to Scrypted Device Manager.
*
*
* @category Device Provider Reference
*/
export interface DeviceProvider {
@@ -970,7 +970,7 @@ export interface DeviceProvider {
}
/**
* DeviceManifest is passed to DeviceManager.onDevicesChanged to sync a full list of devices from the controller/hub (Hue, SmartThings, etc)
*
*
* @category Device Provider Reference
*/
export interface DeviceManifest {
@@ -985,7 +985,7 @@ export interface DeviceCreatorSettings {
}
/**
* A DeviceProvider that allows the user to create a device.
*
*
* @category Device Provider Reference
*/
export interface DeviceCreator {
@@ -1013,7 +1013,7 @@ export interface AdoptDevice {
}
/**
* A DeviceProvider that has a device discovery mechanism.
*
*
* @category Device Provider Reference
*/
export interface DeviceDiscovery {
@@ -1035,9 +1035,21 @@ export interface DeviceDiscovery {
export interface Battery {
batteryLevel?: number;
}
export enum ChargeState {
Trickle = 'trickle',
Charging = 'charging',
NotCharging = 'not-charging',
}
/**
* Charger reports whether or not a device is being charged from an external power source.
* Usually used for battery powered devices.
*/
export interface Charger {
chargeState?: ChargeState;
}
/**
* Refresh indicates that this device has properties that are not automatically updated, and must be periodically refreshed via polling. Device implementations should never implement their own underlying polling algorithm, and instead implement Refresh to allow Scrypted to manage polling intelligently.
*
*
* @category Device Provider Reference
*/
export interface Refresh {
@@ -1537,7 +1549,7 @@ export interface DeviceInformation {
}
/**
* Device objects are created by DeviceProviders when new devices are discover and synced to Scrypted via the DeviceManager.
*
*
* @category Device Provider Reference
*/
export interface Device {
@@ -1563,7 +1575,7 @@ export interface EndpointAccessControlAllowOrigin {
/**
* EndpointManager provides publicly accessible URLs that can be used to contact your Scrypted Plugin.
*
*
* @category Webhook and Push Reference
*/
export interface EndpointManager {
@@ -1656,7 +1668,7 @@ export interface EndpointManager {
}
/**
* SystemManager is used by scripts to query device state and access devices.
*
*
* @category Core Reference
*/
export interface SystemManager {
@@ -1714,7 +1726,7 @@ export interface SystemManager {
}
/**
* MixinProviders can add and intercept interfaces to other devices to add or augment their behavior.
*
*
* @category Mixin Reference
*/
export interface MixinProvider {
@@ -1735,7 +1747,7 @@ export interface MixinProvider {
}
/**
* The HttpRequestHandler allows handling of web requests under the endpoint path: /endpoint/npm-package-name/*.
*
*
* @category Webhook and Push Reference
*/
export interface HttpRequestHandler {
@@ -1760,7 +1772,7 @@ export interface HttpRequest {
}
/**
* Response object provided by the HttpRequestHandler.
*
*
* @category Webhook and Push Reference
*/
export interface HttpResponse {
@@ -1791,7 +1803,7 @@ export interface EngineIOHandler {
}
/**
* @category Webhook and Push Reference
*
*
*/
export interface PushHandler {
/**
@@ -1885,6 +1897,7 @@ export enum ScryptedInterface {
DeviceDiscovery = "DeviceDiscovery",
DeviceCreator = "DeviceCreator",
Battery = "Battery",
Charger = "Charger",
Refresh = "Refresh",
MediaPlayer = "MediaPlayer",
Online = "Online",
@@ -1935,7 +1948,7 @@ export type RTCSignalingSendIceCandidate = (candidate: RTCIceCandidateInit) => P
/**
* Implemented by WebRTC cameras to negotiate a peer connection session with Scrypted.
*
*
* @category WebRTC Reference
*/
export interface RTCSignalingSession {
@@ -1979,7 +1992,7 @@ export interface RTCSignalingOptions {
}
/**
* A flexible RTC signaling endpoint, typically a browser, that can handle offer and answer.
* A flexible RTC signaling endpoint, typically a browser, that can handle offer and answer.
* Like Chromecast, etc, which has a Chromecast AppId that can connect to Scrypted.
*/
export interface RTCSignalingClient {
@@ -2029,7 +2042,7 @@ export interface RTCConnectionManagement {
* An inflexible RTC Signaling channel, typically a vendor, like Nest or Ring.
* They generally can only handle either offer or answer, but not both. Usually has
* strict requirements and expectations on client setup.
*
*
* @category WebRTC Reference
*/
export interface RTCSignalingChannel {

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/server",
"version": "0.7.42",
"version": "0.7.44",
"description": "",
"dependencies": {
"@mapbox/node-pre-gyp": "^1.0.10",
@@ -72,8 +72,7 @@
"prebeta": "npm version patch && git add package.json && npm run build && git commit -m prebeta",
"beta": "npm publish --tag beta",
"release": "npm publish",
"prerelease": "npm version patch && git add package.json && npm run build && git commit -m prerelease",
"postrelease": "git tag v$npm_package_version && git push origin v$npm_package_version",
"postrelease": "git tag v$npm_package_version && git push origin v$npm_package_version && npm version patch && git add package.json && npm run build && git commit -m postrelease",
"docker": "scripts/github-workflow-publish-docker.sh"
},
"author": "",

View File

@@ -651,9 +651,7 @@ async def plugin_async_main(loop: AbstractEventLoop, rpcTransport: rpc_reader.Rp
try:
await readLoop()
finally:
if type(rpcTransport) == rpc_reader.RpcConnectionTransport:
r: rpc_reader.RpcConnectionTransport = rpcTransport
r.executor.shutdown()
os._exit(0)
def main(rpcTransport: rpc_reader.RpcTransport):
loop = asyncio.new_event_loop()
@@ -675,6 +673,9 @@ def plugin_main(rpcTransport: rpc_reader.RpcTransport):
from gi.repository import GLib, Gst
Gst.init(None)
# can't remember why starting the glib main loop is necessary.
# maybe gstreamer on linux and other things needed it?
# seems optional on other platforms.
loop = GLib.MainLoop()
worker = threading.Thread(target=main, args=(rpcTransport,), name="asyncio-main")