mirror of
https://github.com/koush/scrypted.git
synced 2026-02-06 23:42:19 +00:00
Compare commits
56 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fec59af263 | ||
|
|
5d213a4c51 | ||
|
|
d444c4ab7c | ||
|
|
590f955ca9 | ||
|
|
7df4bf2723 | ||
|
|
3416347a1f | ||
|
|
c669bb8902 | ||
|
|
ce5fd2d4fd | ||
|
|
fa8a756059 | ||
|
|
73b85e1cd0 | ||
|
|
1300073712 | ||
|
|
3e296e12a5 | ||
|
|
bf98060a08 | ||
|
|
d1cd380123 | ||
|
|
1a2aadfb52 | ||
|
|
60c854a477 | ||
|
|
0790b60122 | ||
|
|
a3caa09df4 | ||
|
|
02ca8bd765 | ||
|
|
f9e1a94ab3 | ||
|
|
dd0da26df3 | ||
|
|
890f2e8daf | ||
|
|
2c8babe3ce | ||
|
|
8e31b5f970 | ||
|
|
0873a72848 | ||
|
|
145c66e1c8 | ||
|
|
2b60b45113 | ||
|
|
6f63927e2f | ||
|
|
528eabdfc0 | ||
|
|
e201ea1fc1 | ||
|
|
7790810b86 | ||
|
|
e9ec78909b | ||
|
|
26245e17ca | ||
|
|
5d87a1b2dd | ||
|
|
e1efde3868 | ||
|
|
525eb028c6 | ||
|
|
520c6a62a1 | ||
|
|
6e6898ce33 | ||
|
|
1344c9112c | ||
|
|
f2148ce26a | ||
|
|
81b00195d6 | ||
|
|
8f71778f05 | ||
|
|
2e5b8d90aa | ||
|
|
780182b94a | ||
|
|
57480f7606 | ||
|
|
1478684120 | ||
|
|
223b302bed | ||
|
|
f56cef1b50 | ||
|
|
83bfa30d4b | ||
|
|
611674af46 | ||
|
|
941ea7f346 | ||
|
|
2b9c2956d6 | ||
|
|
266d5bf8a3 | ||
|
|
d0007fc7bb | ||
|
|
75f90b78eb | ||
|
|
1e8959413e |
8
.github/ISSUE_TEMPLATE/bug_report.md
vendored
8
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -27,6 +27,11 @@ Created issues that do not meet these requirements or are improperly filled out
|
||||
1. Delete this section and everything above it.
|
||||
2. Fill out the sections below.
|
||||
|
||||
** Before You Submit**
|
||||
|
||||
- [ ] I checked that my issue isn't already filed: [Search open issues](https://github.com/koush/scrypted/issues).
|
||||
- [ ] I checked the relevant camera/device and/or plugin `Log` in the `Management Console` for errors or warnings that may help identify and resolve the issue myself.
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is. The issue tracker is only for reporting bugs in Scrypted, for general support check Discord. Hardrware support requests or assistance requests will be immediately closed.
|
||||
|
||||
@@ -43,6 +48,9 @@ A clear and concise description of what you expected to happen.
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Logs**
|
||||
Include a `Log` from the device/camera in the management console (and if applicable, the affacted plugin, like HomeKit).
|
||||
|
||||
**Server (please complete the following information):**
|
||||
- OS: [e.g. Ubuntu]
|
||||
- Installation Method: [e.g. Desktop App, Docker, Local]
|
||||
|
||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
runner: [ubuntu-latest, macos-14, macos-13, windows-latest]
|
||||
runner: [ubuntu-latest, ubuntu-24.04-arm, macos-14, macos-13, windows-latest]
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
|
||||
@@ -247,7 +247,8 @@ export function createRtspParser(options?: StreamParserOptions): RtspStreamParse
|
||||
'tcp',
|
||||
...(options?.vcodec || []),
|
||||
...(options?.acodec || []),
|
||||
'-pkt_size', '64000',
|
||||
// linux and windows seem to support 64000 but darwin is 32000?
|
||||
'-pkt_size', '32000',
|
||||
'-f', 'rtsp',
|
||||
],
|
||||
findSyncFrame(streamChunks: StreamChunk[]) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Home Assistant Addon Configuration
|
||||
name: Scrypted
|
||||
version: "v0.120.0-jammy-full"
|
||||
version: "v0.130.1-noble-full"
|
||||
slug: scrypted
|
||||
description: Scrypted is a high performance home video integration and automation platform
|
||||
url: "https://github.com/koush/scrypted"
|
||||
|
||||
@@ -55,7 +55,7 @@ services:
|
||||
# Scrypted NVR Storage (Part 3 of 3)
|
||||
|
||||
# Modify to add the additional volume for Scrypted NVR.
|
||||
# The following example would mount the /mnt/sda/video path on the host
|
||||
# The following example would mount the /mnt/media/video path on the host
|
||||
# to the /nvr path inside the docker container.
|
||||
# - /mnt/media/video:/nvr
|
||||
|
||||
|
||||
@@ -36,9 +36,8 @@ curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | gpg --yes --dea
|
||||
tee /etc/apt/sources.list.d/nvidia-container-toolkit.list
|
||||
apt -y update
|
||||
# is there a way to get a versioned package automatically?
|
||||
apt -y install nvidia-utils-560
|
||||
apt -y install cuda-drivers
|
||||
apt -y install nvidia-container-toolkit
|
||||
|
||||
nvidia-ctk runtime configure --runtime=docker
|
||||
nvidia-ctk config --set nvidia-container-cli.no-cgroups --in-place
|
||||
systemctl restart docker
|
||||
|
||||
@@ -27,7 +27,7 @@ echo "external/werift > npm install"
|
||||
npm install
|
||||
popd
|
||||
|
||||
for directory in rtsp amcrest onvif hikvision reolink unifi-protect webrtc homekit
|
||||
for directory in rtsp ffmpeg-camera amcrest onvif hikvision reolink unifi-protect webrtc homekit
|
||||
do
|
||||
echo "$directory > npm install"
|
||||
pushd plugins/$directory
|
||||
|
||||
50
packages/client/package-lock.json
generated
50
packages/client/package-lock.json
generated
@@ -1,25 +1,25 @@
|
||||
{
|
||||
"name": "@scrypted/client",
|
||||
"version": "1.3.9",
|
||||
"version": "1.3.10",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/client",
|
||||
"version": "1.3.9",
|
||||
"version": "1.3.10",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/types": "^0.3.92",
|
||||
"engine.io-client": "^6.6.1",
|
||||
"@scrypted/types": "^0.3.100",
|
||||
"engine.io-client": "^6.6.2",
|
||||
"follow-redirects": "^1.15.9",
|
||||
"rimraf": "^6.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/ip": "^1.1.3",
|
||||
"@types/node": "^22.7.4",
|
||||
"@types/node": "^22.10.7",
|
||||
"@types/ws": "^8.5.13",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.6.2"
|
||||
"typescript": "^5.7.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
@@ -76,9 +76,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@scrypted/types": {
|
||||
"version": "0.3.92",
|
||||
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.3.92.tgz",
|
||||
"integrity": "sha512-/M1Lg42/yoFWusj5+Lyp2S0JCiWDDWcmsjiUnTf1DahZ6/M2oZ3bwR/0KX3D9vJE79owWST1Gm0+Rdvpxuil9A==",
|
||||
"version": "0.3.100",
|
||||
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.3.100.tgz",
|
||||
"integrity": "sha512-s/07QCxjMWqODgWj2UpLehzeo2cGFrCA9X8mvpG3owT/+q+sb8v/UUcw9TLHGSN6yIriNhceg3i9WO07kEIT6A==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@socket.io/component-emitter": {
|
||||
@@ -120,12 +120,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.4.tgz",
|
||||
"integrity": "sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg==",
|
||||
"version": "22.10.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.7.tgz",
|
||||
"integrity": "sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.19.2"
|
||||
"undici-types": "~6.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/ws": {
|
||||
@@ -272,9 +273,10 @@
|
||||
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
|
||||
},
|
||||
"node_modules/engine.io-client": {
|
||||
"version": "6.6.1",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.1.tgz",
|
||||
"integrity": "sha512-aYuoak7I+R83M/BBPIOs2to51BmFIpC1wZe6zZzMrT2llVsHy5cvcmdsJgP2Qz6smHu+sD9oexiSUAVd8OfBPw==",
|
||||
"version": "6.6.2",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.2.tgz",
|
||||
"integrity": "sha512-TAr+NKeoVTjEVW8P3iHguO1LO6RlUz9O5Y8o7EY0fU+gY1NYqas7NN3slpFtbXEsLMHk0h90fJMfKjRkQ0qUIw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.1",
|
||||
@@ -623,10 +625,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.6.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz",
|
||||
"integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==",
|
||||
"version": "5.7.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
|
||||
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -636,10 +639,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.19.8",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
|
||||
"dev": true
|
||||
"version": "6.20.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
|
||||
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/v8-compile-cache-lib": {
|
||||
"version": "3.0.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/client",
|
||||
"version": "1.3.9",
|
||||
"version": "1.3.10",
|
||||
"description": "",
|
||||
"main": "dist/packages/client/src/index.js",
|
||||
"scripts": {
|
||||
@@ -13,14 +13,14 @@
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/ip": "^1.1.3",
|
||||
"@types/node": "^22.7.4",
|
||||
"@types/node": "^22.10.7",
|
||||
"@types/ws": "^8.5.13",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.6.2"
|
||||
"typescript": "^5.7.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@scrypted/types": "^0.3.92",
|
||||
"engine.io-client": "^6.6.1",
|
||||
"@scrypted/types": "^0.3.100",
|
||||
"engine.io-client": "^6.6.2",
|
||||
"follow-redirects": "^1.15.9",
|
||||
"rimraf": "^6.0.1"
|
||||
}
|
||||
|
||||
4
plugins/core/package-lock.json
generated
4
plugins/core/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.3.103",
|
||||
"version": "0.3.111",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.3.103",
|
||||
"version": "0.3.111",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.3.103",
|
||||
"version": "0.3.111",
|
||||
"description": "Scrypted Core plugin. Provides the UI, websocket, and engine.io APIs.",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -13,7 +13,7 @@ import { MediaCore } from './media-core';
|
||||
import { checkLegacyLxc, checkLxc } from './platform/lxc';
|
||||
import { ConsoleServiceNativeId, PluginSocketService, ReplServiceNativeId } from './plugin-socket-service';
|
||||
import { ScriptCore, ScriptCoreNativeId, newScript } from './script-core';
|
||||
import { TerminalService, TerminalServiceNativeId } from './terminal-service';
|
||||
import { TerminalService, TerminalServiceNativeId, newTerminalService } from './terminal-service';
|
||||
import { UsersCore, UsersNativeId } from './user';
|
||||
import { ClusterCore, ClusterCoreNativeId } from './cluster';
|
||||
|
||||
@@ -140,7 +140,7 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Dev
|
||||
{
|
||||
name: 'Terminal Service',
|
||||
nativeId: TerminalServiceNativeId,
|
||||
interfaces: [ScryptedInterface.StreamService, ScryptedInterface.TTY],
|
||||
interfaces: [ScryptedInterface.StreamService, ScryptedInterface.TTY, ScryptedInterface.ClusterForkInterface],
|
||||
type: ScryptedDeviceType.Builtin,
|
||||
},
|
||||
);
|
||||
@@ -242,7 +242,7 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Dev
|
||||
if (nativeId === UsersNativeId)
|
||||
return this.users ||= new UsersCore();
|
||||
if (nativeId === TerminalServiceNativeId)
|
||||
return this.terminalService ||= new TerminalService();
|
||||
return this.terminalService ||= new TerminalService(TerminalServiceNativeId, false);
|
||||
if (nativeId === ReplServiceNativeId)
|
||||
return this.replService ||= new PluginSocketService(ReplServiceNativeId, 'repl');
|
||||
if (nativeId === ConsoleServiceNativeId)
|
||||
@@ -331,5 +331,6 @@ export async function fork() {
|
||||
return {
|
||||
tsCompile,
|
||||
newScript,
|
||||
newTerminalService,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import sdk, { ScryptedDeviceBase, ScryptedInterface, ScryptedNativeId, StreamService, TTYSettings } from "@scrypted/sdk";
|
||||
import sdk, { ClusterForkInterface, ClusterForkInterfaceOptions, ScryptedDeviceBase, ScryptedInterface, ScryptedNativeId, StreamService, TTYSettings } from "@scrypted/sdk";
|
||||
import type { IPty, spawn as ptySpawn } from 'node-pty';
|
||||
import { createAsyncQueue } from '@scrypted/common/src/async-queue'
|
||||
import { ChildProcess, spawn as childSpawn } from "child_process";
|
||||
@@ -111,8 +111,11 @@ class NoninteractiveTerminal {
|
||||
}
|
||||
|
||||
|
||||
export class TerminalService extends ScryptedDeviceBase implements StreamService<Buffer | string, Buffer> {
|
||||
constructor(nativeId?: ScryptedNativeId) {
|
||||
export class TerminalService extends ScryptedDeviceBase implements StreamService<Buffer | string, Buffer>, ClusterForkInterface {
|
||||
private forks: { [clusterWorkerId: string]: TerminalService } = {};
|
||||
private forkClients: 0;
|
||||
|
||||
constructor(nativeId?: ScryptedNativeId, private isFork: boolean = false) {
|
||||
super(nativeId);
|
||||
}
|
||||
|
||||
@@ -134,6 +137,42 @@ export class TerminalService extends ScryptedDeviceBase implements StreamService
|
||||
return extraPaths;
|
||||
}
|
||||
|
||||
async forkInterface<StreamService>(forkInterface: ScryptedInterface, options?: ClusterForkInterfaceOptions): Promise<StreamService> {
|
||||
if (forkInterface !== ScryptedInterface.StreamService) {
|
||||
throw new Error('can only fork StreamService');
|
||||
}
|
||||
|
||||
if (!options?.clusterWorkerId) {
|
||||
throw new Error('clusterWorkerId required');
|
||||
}
|
||||
|
||||
if (this.isFork) {
|
||||
throw new Error('cannot fork a fork');
|
||||
}
|
||||
|
||||
const clusterWorkerId = options.clusterWorkerId;
|
||||
if (this.forks[clusterWorkerId]) {
|
||||
return this.forks[clusterWorkerId] as StreamService;
|
||||
}
|
||||
|
||||
const fork = sdk.fork<{
|
||||
newTerminalService: typeof newTerminalService,
|
||||
}>({ clusterWorkerId });
|
||||
try {
|
||||
const result = await fork.result;
|
||||
const terminalService = await result.newTerminalService();
|
||||
this.forks[clusterWorkerId] = terminalService;
|
||||
fork.worker.on('exit', () => {
|
||||
delete this.forks[clusterWorkerId];
|
||||
});
|
||||
return terminalService as StreamService;
|
||||
}
|
||||
catch (e) {
|
||||
fork.worker.terminate();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* The input to this stream can send buffers for normal terminal data and strings
|
||||
* for control messages. Control messages are JSON-formatted.
|
||||
@@ -149,6 +188,19 @@ export class TerminalService extends ScryptedDeviceBase implements StreamService
|
||||
const queue = createAsyncQueue<Buffer>();
|
||||
const extraPaths = await this.getExtraPaths();
|
||||
|
||||
if (this.isFork) {
|
||||
this.forkClients++;
|
||||
}
|
||||
|
||||
queue.endPromise.then(() => {
|
||||
if (this.isFork) {
|
||||
this.forkClients--;
|
||||
if (this.forkClients === 0) {
|
||||
process.exit();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function registerChildListeners() {
|
||||
cp.onExit(() => queue.end());
|
||||
|
||||
@@ -232,4 +284,8 @@ export class TerminalService extends ScryptedDeviceBase implements StreamService
|
||||
|
||||
return generator();
|
||||
}
|
||||
}
|
||||
|
||||
export async function newTerminalService(): Promise<TerminalService> {
|
||||
return new TerminalService(TerminalServiceNativeId, true);
|
||||
}
|
||||
124
plugins/doorbird/package-lock.json
generated
124
plugins/doorbird/package-lock.json
generated
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"name": "@scrypted/doorbird",
|
||||
"version": "0.0.2",
|
||||
"version": "0.0.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/doorbird",
|
||||
"version": "0.0.2",
|
||||
"version": "0.0.4",
|
||||
"dependencies": {
|
||||
"doorbird": "^2.1.2"
|
||||
"doorbird": "2.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"@types/node": "^18.15.11",
|
||||
"@types/node": "^22.10.10",
|
||||
"cross-env": "^7.0.3"
|
||||
}
|
||||
},
|
||||
@@ -24,36 +24,41 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/sdk": "file:../sdk",
|
||||
"@scrypted/server": "file:../server",
|
||||
"http-auth-utils": "^5.0.1",
|
||||
"node-fetch-commonjs": "^3.1.1",
|
||||
"typescript": "^5.3.3"
|
||||
"typescript": "^5.5.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.8",
|
||||
"@types/node": "^20.11.0",
|
||||
"monaco-editor": "^0.50.0",
|
||||
"ts-node": "^10.9.2"
|
||||
}
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.3.4",
|
||||
"version": "0.3.108",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
"adm-zip": "^0.4.13",
|
||||
"axios": "^1.6.5",
|
||||
"babel-loader": "^9.1.0",
|
||||
"babel-plugin-const-enum": "^1.1.0",
|
||||
"esbuild": "^0.15.9",
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"@rollup/plugin-commonjs": "^28.0.1",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^15.3.0",
|
||||
"@rollup/plugin-typescript": "^12.1.1",
|
||||
"@rollup/plugin-virtual": "^3.0.2",
|
||||
"adm-zip": "^0.5.16",
|
||||
"axios": "^1.7.8",
|
||||
"babel-loader": "^9.2.1",
|
||||
"babel-plugin-const-enum": "^1.2.0",
|
||||
"ncp": "^2.0.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"tmp": "^0.2.1",
|
||||
"ts-loader": "^9.4.2",
|
||||
"typescript": "^4.9.4",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-bundle-analyzer": "^4.5.0"
|
||||
"rimraf": "^6.0.1",
|
||||
"rollup": "^4.27.4",
|
||||
"tmp": "^0.2.3",
|
||||
"ts-loader": "^9.5.1",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.6.3",
|
||||
"webpack": "^5.96.1",
|
||||
"webpack-bundle-analyzer": "^4.10.2"
|
||||
},
|
||||
"bin": {
|
||||
"scrypted-changelog": "bin/scrypted-changelog.js",
|
||||
@@ -65,11 +70,9 @@
|
||||
"scrypted-webpack": "bin/scrypted-webpack.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/stringify-object": "^4.0.0",
|
||||
"stringify-object": "^3.3.0",
|
||||
"ts-node": "^10.4.0",
|
||||
"typedoc": "^0.23.21"
|
||||
"@types/node": "^22.10.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typedoc": "^0.26.11"
|
||||
}
|
||||
},
|
||||
"node_modules/@scrypted/common": {
|
||||
@@ -81,10 +84,14 @@
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "18.15.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz",
|
||||
"integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==",
|
||||
"dev": true
|
||||
"version": "22.10.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.10.tgz",
|
||||
"integrity": "sha512-X47y/mPNzxviAGY5TcYPtYL8JsY3kAq2n8fMmKoRCxq/c4v4pyGNCzM2R6+M5/umG4ZfHuT+sgqDYqWc9rJ6ww==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
@@ -92,11 +99,12 @@
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.6.5",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.5.tgz",
|
||||
"integrity": "sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==",
|
||||
"version": "1.7.9",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz",
|
||||
"integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.4",
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
@@ -145,10 +153,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
"shebang-command": "^2.0.0",
|
||||
@@ -167,25 +176,29 @@
|
||||
}
|
||||
},
|
||||
"node_modules/doorbird": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/doorbird/-/doorbird-2.1.2.tgz",
|
||||
"integrity": "sha512-ivwwsS/nOslDnuLg3UB60Axo76w5LQuZ67mCPEeWFr5+HbGYRL7PCY3iLjWYaIakh5+IvZyFPHKR4yHAvAc1WQ==",
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/doorbird/-/doorbird-2.6.0.tgz",
|
||||
"integrity": "sha512-HZBI5uFhwEVF8JFULQlpzXXvjSHmtQMJUNWfogq6vHe3kv7mCSmg0g/TDbeV5fVvisi8w7GxKD0/PpZCrtcGOg==",
|
||||
"dependencies": {
|
||||
"axios": "^1.2.1",
|
||||
"axios": "^1.6.2",
|
||||
"chacha-js": "^2.1.1",
|
||||
"libsodium-wrappers-sumo": "^0.7.11"
|
||||
"libsodium-wrappers-sumo": "^0.7.13"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.4",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz",
|
||||
"integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==",
|
||||
"version": "1.15.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
@@ -225,16 +238,16 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/libsodium-sumo": {
|
||||
"version": "0.7.11",
|
||||
"resolved": "https://registry.npmjs.org/libsodium-sumo/-/libsodium-sumo-0.7.11.tgz",
|
||||
"integrity": "sha512-bY+7ph7xpk51Ez2GbE10lXAQ5sJma6NghcIDaSPbM/G9elfrjLa0COHl/7P6Wb/JizQzl5UQontOOP1z0VwbLA=="
|
||||
"version": "0.7.15",
|
||||
"resolved": "https://registry.npmjs.org/libsodium-sumo/-/libsodium-sumo-0.7.15.tgz",
|
||||
"integrity": "sha512-5tPmqPmq8T8Nikpm1Nqj0hBHvsLFCXvdhBFV7SGOitQPZAA6jso8XoL0r4L7vmfKXr486fiQInvErHtEvizFMw=="
|
||||
},
|
||||
"node_modules/libsodium-wrappers-sumo": {
|
||||
"version": "0.7.11",
|
||||
"resolved": "https://registry.npmjs.org/libsodium-wrappers-sumo/-/libsodium-wrappers-sumo-0.7.11.tgz",
|
||||
"integrity": "sha512-DGypHOmJbB1nZn89KIfGOAkDgfv5N6SBGC3Qvmy/On0P0WD1JQvNRS/e3UL3aFF+xC0m+MYz5M+MnRnK2HMrKQ==",
|
||||
"version": "0.7.15",
|
||||
"resolved": "https://registry.npmjs.org/libsodium-wrappers-sumo/-/libsodium-wrappers-sumo-0.7.15.tgz",
|
||||
"integrity": "sha512-aSWY8wKDZh5TC7rMvEdTHoyppVq/1dTSAeAR7H6pzd6QRT3vQWcT5pGwCotLcpPEOLXX6VvqihSPkpEhYAjANA==",
|
||||
"dependencies": {
|
||||
"libsodium-sumo": "^0.7.11"
|
||||
"libsodium-sumo": "^0.7.15"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
@@ -307,6 +320,13 @@
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
|
||||
"integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ=="
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.20.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
|
||||
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/doorbird",
|
||||
"version": "0.0.2",
|
||||
"version": "0.0.4",
|
||||
"scripts": {
|
||||
"scrypted-setup-project": "scrypted-setup-project",
|
||||
"prescrypted-setup-project": "scrypted-package-json",
|
||||
@@ -33,12 +33,12 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"doorbird": "^2.1.2"
|
||||
"doorbird": "2.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"@types/node": "^18.15.11",
|
||||
"@types/node": "^22.10.10",
|
||||
"cross-env": "^7.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { listenZero } from '@scrypted/common/src/listen-cluster';
|
||||
import sdk, { BinarySensor, Camera, DeviceProvider, DeviceCreator, DeviceCreatorSettings, DeviceInformation, FFmpegInput, Intercom, MediaObject, PictureOptions, ResponseMediaStreamOptions, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, Settings, VideoCamera, MotionSensor } from '@scrypted/sdk';
|
||||
import child_process, { ChildProcess } from 'child_process';
|
||||
import { ffmpegLogInitialOutput, safePrintFFmpegArguments } from "@scrypted/common/src/media-helpers";
|
||||
import net from 'net';
|
||||
import { randomBytes } from 'crypto';
|
||||
import { PassThrough, Readable } from "stream";
|
||||
import { readLength } from "@scrypted/common/src/read-stream";
|
||||
import { authHttpFetch } from "@scrypted/common/src/http-auth-fetch";
|
||||
import { ApiRingEvent, ApiMotionEvent, DoorbirdAPI } from "./doorbird-api";
|
||||
import { listenZero } from '@scrypted/common/src/listen-cluster';
|
||||
import { ffmpegLogInitialOutput, safePrintFFmpegArguments } from "@scrypted/common/src/media-helpers";
|
||||
import { readLength } from "@scrypted/common/src/read-stream";
|
||||
import sdk, { BinarySensor, Camera, DeviceCreator, DeviceCreatorSettings, DeviceInformation, DeviceProvider, FFmpegInput, Intercom, MediaObject, MotionSensor, PictureOptions, ResponseMediaStreamOptions, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, Settings, VideoCamera } from '@scrypted/sdk';
|
||||
import child_process, { ChildProcess } from 'child_process';
|
||||
import { randomBytes } from 'crypto';
|
||||
import net from 'net';
|
||||
import { PassThrough, Readable } from "stream";
|
||||
import { ApiMotionEvent, ApiRingEvent, DoorbirdAPI } from "./doorbird-api";
|
||||
|
||||
const { deviceManager, mediaManager } = sdk;
|
||||
|
||||
@@ -384,7 +384,7 @@ class DoorbirdCamera extends ScryptedDeviceBase implements Intercom, Camera, Vid
|
||||
this.console.log('Doorbird: timed out waiting for tcp client from ffmpeg');
|
||||
server.close();
|
||||
}, 30000);
|
||||
const port = await listenZero(server);
|
||||
const port = await listenZero(server, '127.0.0.1');
|
||||
|
||||
return port;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "Node16",
|
||||
"target": "esnext",
|
||||
"moduleResolution": "Node16",
|
||||
"esModuleInterop": true,
|
||||
|
||||
4
plugins/hikvision/package-lock.json
generated
4
plugins/hikvision/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/hikvision",
|
||||
"version": "0.0.160",
|
||||
"version": "0.0.161",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/hikvision",
|
||||
"version": "0.0.160",
|
||||
"version": "0.0.161",
|
||||
"license": "Apache",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/hikvision",
|
||||
"version": "0.0.160",
|
||||
"version": "0.0.161",
|
||||
"description": "Hikvision Plugin for Scrypted",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache",
|
||||
|
||||
@@ -116,11 +116,15 @@ export class HikvisionCameraAPI implements HikvisionAPI {
|
||||
}
|
||||
|
||||
async checkIsOldModel() {
|
||||
// The old Hikvision DS-7608NI-E2 doesn't support channel capability checks, and the requests cause errors
|
||||
// The old Hikvision NVRs don't support channel capability checks, and the requests cause errors
|
||||
const oldModels = [
|
||||
/DS-76098NI-E2/,
|
||||
/ERI-K104-P4/
|
||||
];
|
||||
const model = await this.checkDeviceModel();
|
||||
if (!model)
|
||||
return;
|
||||
return !!model?.match(/DS-7608NI-E2/);
|
||||
return !!oldModels.find(oldModel => model?.match(oldModel));
|
||||
}
|
||||
|
||||
async checkStreamSetup(channel: string, isOld: boolean): Promise<HikvisionCameraStreamSetup> {
|
||||
|
||||
4
plugins/homekit/package-lock.json
generated
4
plugins/homekit/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/homekit",
|
||||
"version": "1.2.62",
|
||||
"version": "1.2.63",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/homekit",
|
||||
"version": "1.2.62",
|
||||
"version": "1.2.63",
|
||||
"dependencies": {
|
||||
"@koush/werift-src": "file:../../external/werift",
|
||||
"check-disk-space": "^3.4.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/homekit",
|
||||
"version": "1.2.62",
|
||||
"version": "1.2.63",
|
||||
"description": "HomeKit Plugin for Scrypted",
|
||||
"scripts": {
|
||||
"scrypted-setup-project": "scrypted-setup-project",
|
||||
|
||||
@@ -216,6 +216,7 @@ export class HomeKitPlugin extends ScryptedDeviceBase implements MixinProvider,
|
||||
throw Error(`error in device reordering, expected ${uniqueDeviceIds.size} unique devices but only got ${uniqueReorderedIds.size} entries!`);
|
||||
}
|
||||
|
||||
const autoAdd = this.storageSettings.values.autoAdd ?? true;
|
||||
for (const id of reorderedDeviceIds) {
|
||||
const device = systemManager.getDeviceById<Online>(id);
|
||||
const supportedType = supportedTypes[device.type];
|
||||
@@ -224,8 +225,7 @@ export class HomeKitPlugin extends ScryptedDeviceBase implements MixinProvider,
|
||||
|
||||
try {
|
||||
const mixins = (device.mixins || []).slice();
|
||||
const autoAdd = this.storageSettings.values.autoAdd ?? true;
|
||||
if (!mixins.includes(this.id) && autoAdd) {
|
||||
if (!mixins.includes(this.id)) {
|
||||
// don't sync this by default, as it's solely for automations
|
||||
if (device.type === ScryptedDeviceType.Notifier)
|
||||
continue;
|
||||
@@ -235,6 +235,8 @@ export class HomeKitPlugin extends ScryptedDeviceBase implements MixinProvider,
|
||||
continue;
|
||||
if (defaultIncluded[device.id] === includeToken)
|
||||
continue;
|
||||
if (!autoAdd)
|
||||
continue;
|
||||
mixins.push(this.id);
|
||||
await device.setMixins(mixins);
|
||||
defaultIncluded[device.id] = includeToken;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import sdk, { AirQuality, AirQualitySensor, CO2Sensor, DeviceProvider, Fan, FanMode, NOXSensor, OnOff, PM10Sensor, PM25Sensor, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, VOCSensor } from "@scrypted/sdk";
|
||||
import sdk, { AirQuality, AirQualitySensor, CO2Sensor, DeviceProvider, Fan, FanMode, HumidityMode, HumiditySensor, HumiditySetting, HumiditySettingStatus, NOXSensor, OnOff, PM10Sensor, PM25Sensor, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, VOCSensor } from "@scrypted/sdk";
|
||||
import { bindCharacteristic } from "../common";
|
||||
import { Accessory, Characteristic, CharacteristicEventTypes, Service, uuid } from '../hap';
|
||||
import type { HomeKitPlugin } from "../main";
|
||||
@@ -96,24 +96,161 @@ export function addCarbonDioxideSensor(device: ScryptedDevice & CO2Sensor, acces
|
||||
return co2Service;
|
||||
}
|
||||
|
||||
export function addFan(device: ScryptedDevice & Fan & OnOff, accessory: Accessory): Service {
|
||||
if (!device.interfaces.includes(ScryptedInterface.OnOff) && !device.interfaces.includes(ScryptedInterface.Fan))
|
||||
function commonHumidifierDehumidifier(mode: HumidityMode, subtype: string, name: string, device: ScryptedDevice & HumiditySetting & HumiditySensor, accessory: Accessory): Service {
|
||||
function currentState(mode: HumidityMode) {
|
||||
switch(mode) {
|
||||
case HumidityMode.Humidify:
|
||||
return Characteristic.CurrentHumidifierDehumidifierState.HUMIDIFYING;
|
||||
case HumidityMode.Dehumidify:
|
||||
return Characteristic.CurrentHumidifierDehumidifierState.DEHUMIDIFYING;
|
||||
case HumidityMode.Off:
|
||||
return Characteristic.CurrentHumidifierDehumidifierState.INACTIVE;
|
||||
default:
|
||||
return Characteristic.CurrentHumidifierDehumidifierState.IDLE;
|
||||
}
|
||||
}
|
||||
|
||||
function targetState(mode: HumidityMode) {
|
||||
switch(mode) {
|
||||
case HumidityMode.Humidify:
|
||||
return Characteristic.TargetHumidifierDehumidifierState.HUMIDIFIER;
|
||||
case HumidityMode.Dehumidify:
|
||||
return Characteristic.TargetHumidifierDehumidifierState.DEHUMIDIFIER;
|
||||
default:
|
||||
return Characteristic.TargetHumidifierDehumidifierState.HUMIDIFIER_OR_DEHUMIDIFIER;
|
||||
}
|
||||
}
|
||||
|
||||
const service = accessory.addService(Service.HumidifierDehumidifier, name, subtype);
|
||||
|
||||
bindCharacteristic(device, ScryptedInterface.HumiditySetting, service, Characteristic.Active,
|
||||
() => {
|
||||
if (!device.humiditySetting?.mode)
|
||||
return false;
|
||||
if (device.humiditySetting.mode === mode)
|
||||
return true;
|
||||
if (device.humiditySetting.mode === HumidityMode.Auto)
|
||||
return true;
|
||||
return false;
|
||||
});
|
||||
service.getCharacteristic(Characteristic.Active).on(CharacteristicEventTypes.SET, (value, callback) => {
|
||||
callback();
|
||||
device.setHumidity({
|
||||
mode: value ? mode : HumidityMode.Off
|
||||
});
|
||||
});
|
||||
|
||||
bindCharacteristic(device, ScryptedInterface.HumiditySensor, service, Characteristic.CurrentRelativeHumidity,
|
||||
() => device.humidity);
|
||||
|
||||
bindCharacteristic(device, ScryptedInterface.HumiditySetting, service, Characteristic.CurrentHumidifierDehumidifierState,
|
||||
() => currentState(device.humiditySetting?.activeMode));
|
||||
|
||||
bindCharacteristic(device, ScryptedInterface.HumiditySetting, service, Characteristic.TargetHumidifierDehumidifierState,
|
||||
() => targetState(device.humiditySetting?.mode));
|
||||
|
||||
service.getCharacteristic(Characteristic.TargetHumidifierDehumidifierState).on(CharacteristicEventTypes.SET, (value, callback) => {
|
||||
callback();
|
||||
device.setHumidity({
|
||||
mode: value === Characteristic.TargetHumidifierDehumidifierState.HUMIDIFIER
|
||||
? HumidityMode.Humidify
|
||||
: value === Characteristic.TargetHumidifierDehumidifierState.DEHUMIDIFIER
|
||||
? HumidityMode.Dehumidify
|
||||
: HumidityMode.Auto
|
||||
});
|
||||
});
|
||||
|
||||
function targetHumidity(setting: HumiditySettingStatus) {
|
||||
if (!setting)
|
||||
return 0;
|
||||
|
||||
if (setting?.availableModes.includes(HumidityMode.Humidify)
|
||||
&& setting?.availableModes.includes(HumidityMode.Dehumidify)) {
|
||||
if (setting?.activeMode === HumidityMode.Humidify)
|
||||
return setting?.humidifierSetpoint;
|
||||
if (setting?.activeMode === HumidityMode.Dehumidify)
|
||||
return setting?.dehumidifierSetpoint;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (setting?.availableModes.includes(HumidityMode.Humidify))
|
||||
return setting?.humidifierSetpoint;
|
||||
|
||||
if (setting?.availableModes.includes(HumidityMode.Dehumidify))
|
||||
return setting?.dehumidifierSetpoint;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
bindCharacteristic(device, ScryptedInterface.HumiditySetting, service, Characteristic.TargetRelativeHumidity,
|
||||
() => targetHumidity(device.humiditySetting));
|
||||
|
||||
return service;
|
||||
}
|
||||
|
||||
function addHumidifier(device: ScryptedDevice & HumiditySetting & HumiditySensor, accessory: Accessory): Service {
|
||||
var service = commonHumidifierDehumidifier(HumidityMode.Humidify, "humidifier", device.name + " Humidifier", device, accessory);
|
||||
|
||||
bindCharacteristic(device, ScryptedInterface.HumiditySetting, service, Characteristic.RelativeHumidityHumidifierThreshold,
|
||||
() => device.humiditySetting?.humidifierSetpoint);
|
||||
service.getCharacteristic(Characteristic.RelativeHumidityHumidifierThreshold).on(CharacteristicEventTypes.SET, (value, callback) => {
|
||||
callback();
|
||||
device.setHumidity({
|
||||
humidifierSetpoint: value as number,
|
||||
});
|
||||
});
|
||||
|
||||
return service;
|
||||
}
|
||||
|
||||
function addDehumidifer(device: ScryptedDevice & HumiditySetting & HumiditySensor, accessory: Accessory): Service {
|
||||
var service = commonHumidifierDehumidifier(HumidityMode.Dehumidify, "dehumidifier", device.name + " Dehumidifier", device, accessory);
|
||||
|
||||
bindCharacteristic(device, ScryptedInterface.HumiditySetting, service, Characteristic.RelativeHumidityDehumidifierThreshold,
|
||||
() => device.humiditySetting?.dehumidifierSetpoint);
|
||||
service.getCharacteristic(Characteristic.RelativeHumidityDehumidifierThreshold).on(CharacteristicEventTypes.SET, (value, callback) => {
|
||||
callback();
|
||||
device.setHumidity({
|
||||
dehumidifierSetpoint: value as number,
|
||||
});
|
||||
});
|
||||
|
||||
return service;
|
||||
}
|
||||
|
||||
export function addHumiditySetting(device: ScryptedDevice & HumiditySetting & HumiditySensor, accessory: Accessory): Service {
|
||||
if (!device.interfaces.includes(ScryptedInterface.HumiditySetting) && !device.interfaces.includes(ScryptedInterface.HumiditySensor))
|
||||
return undefined;
|
||||
|
||||
var service;
|
||||
|
||||
if (device.humiditySetting?.availableModes.includes(HumidityMode.Humidify)) {
|
||||
service = addHumidifier(device, accessory);
|
||||
}
|
||||
|
||||
if (device.humiditySetting?.availableModes.includes(HumidityMode.Dehumidify)) {
|
||||
service = addDehumidifer(device, accessory);
|
||||
}
|
||||
|
||||
return service;
|
||||
}
|
||||
|
||||
export function addFan(device: ScryptedDevice & Fan, accessory: Accessory): Service {
|
||||
if (!device.interfaces.includes(ScryptedInterface.Fan))
|
||||
return undefined;
|
||||
|
||||
const service = accessory.addService(Service.Fanv2, device.name);
|
||||
|
||||
if (device.interfaces.includes(ScryptedInterface.OnOff)) {
|
||||
bindCharacteristic(device, ScryptedInterface.OnOff, service, Characteristic.Active,
|
||||
() => !!device.on);
|
||||
bindCharacteristic(device, ScryptedInterface.OnOff, service, Characteristic.Active,
|
||||
() => device.fan?.active);
|
||||
|
||||
service.getCharacteristic(Characteristic.Active).on(CharacteristicEventTypes.SET, (value, callback) => {
|
||||
callback();
|
||||
if (value)
|
||||
device.turnOn();
|
||||
else
|
||||
device.turnOff();
|
||||
service.getCharacteristic(Characteristic.Active).on(CharacteristicEventTypes.SET, (value, callback) => {
|
||||
callback();
|
||||
device.setFan({
|
||||
mode: value ? FanMode.Auto : FanMode.Manual,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (device.fan?.counterClockwise !== undefined) {
|
||||
bindCharacteristic(device, ScryptedInterface.Fan, service, Characteristic.RotationDirection,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Fan, FanMode, HumidityMode, HumiditySensor, HumiditySetting, OnOff, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, TemperatureSetting, TemperatureUnit, Thermometer, ThermostatMode, AirQualitySensor, AirQuality, PM10Sensor, PM25Sensor, VOCSensor, NOXSensor, CO2Sensor } from '@scrypted/sdk';
|
||||
import { Fan, FanMode, HumidityMode, HumiditySensor, HumiditySetting, OnOff, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, TemperatureSetting, TemperatureUnit, Thermometer, ThermostatMode, AirQualitySensor, AirQuality, PM10Sensor, PM25Sensor, VOCSensor, NOXSensor, CO2Sensor, HumiditySettingStatus } from '@scrypted/sdk';
|
||||
import { addSupportedType, bindCharacteristic, DummyDevice, } from '../common';
|
||||
import { Characteristic, CharacteristicEventTypes, CharacteristicSetCallback, CharacteristicValue, Service } from '../hap';
|
||||
import { addAirQualitySensor, addCarbonDioxideSensor, addFan, makeAccessory } from './common';
|
||||
import { addAirQualitySensor, addCarbonDioxideSensor, addFan, addHumiditySetting, makeAccessory } from './common';
|
||||
import type { HomeKitPlugin } from "../main";
|
||||
|
||||
addSupportedType({
|
||||
@@ -178,72 +178,60 @@ addSupportedType({
|
||||
() => device.humidity || 0);
|
||||
}
|
||||
|
||||
if (device.interfaces.includes(ScryptedInterface.HumiditySetting) && device.interfaces.includes(ScryptedInterface.HumiditySensor)) {
|
||||
const humidityService = accessory.addService(Service.HumidifierDehumidifier);
|
||||
// add fan state to thermostat service even though it is not required or optional,
|
||||
// in order to expose to Home Assistant HomeKit Controller under their climate entity
|
||||
if (device.interfaces.includes(ScryptedInterface.Fan)) {
|
||||
bindCharacteristic(device, ScryptedInterface.Fan, service, Characteristic.TargetFanState,
|
||||
() => device.fan?.mode === FanMode.Manual
|
||||
? Characteristic.TargetFanState.MANUAL
|
||||
: Characteristic.TargetFanState.AUTO);
|
||||
|
||||
bindCharacteristic(device, ScryptedInterface.HumiditySetting, humidityService, Characteristic.Active,
|
||||
() => {
|
||||
if (!device.humiditySetting?.mode)
|
||||
return false;
|
||||
if (device.humiditySetting.mode === HumidityMode.Off)
|
||||
return false;
|
||||
return true;
|
||||
});
|
||||
humidityService.getCharacteristic(Characteristic.Active).on(CharacteristicEventTypes.SET, (value, callback) => {
|
||||
service.getCharacteristic(Characteristic.TargetFanState).on(CharacteristicEventTypes.SET, (value, callback) => {
|
||||
callback();
|
||||
device.setHumidity({
|
||||
mode: value ? HumidityMode.Auto : HumidityMode.Off
|
||||
device.setFan({
|
||||
mode: value === Characteristic.TargetFanState.MANUAL ? FanMode.Manual : FanMode.Auto,
|
||||
});
|
||||
});
|
||||
|
||||
bindCharacteristic(device, ScryptedInterface.HumiditySensor, humidityService, Characteristic.CurrentRelativeHumidity,
|
||||
() => device.humidity || 0);
|
||||
|
||||
bindCharacteristic(device, ScryptedInterface.HumiditySetting, humidityService, Characteristic.CurrentHumidifierDehumidifierState,
|
||||
() => !device.humiditySetting?.activeMode
|
||||
? Characteristic.CurrentHumidifierDehumidifierState.INACTIVE
|
||||
: device.humiditySetting.activeMode === HumidityMode.Dehumidify
|
||||
? Characteristic.CurrentHumidifierDehumidifierState.DEHUMIDIFYING
|
||||
: device.humiditySetting.activeMode === HumidityMode.Humidify
|
||||
? Characteristic.CurrentHumidifierDehumidifierState.HUMIDIFYING
|
||||
: Characteristic.CurrentHumidifierDehumidifierState.IDLE);
|
||||
|
||||
bindCharacteristic(device, ScryptedInterface.HumiditySetting, humidityService, Characteristic.TargetHumidifierDehumidifierState,
|
||||
() => !device.humiditySetting?.mode || device.humiditySetting?.mode === HumidityMode.Auto
|
||||
? Characteristic.TargetHumidifierDehumidifierState.HUMIDIFIER_OR_DEHUMIDIFIER
|
||||
: device.humiditySetting?.mode === HumidityMode.Dehumidify
|
||||
? Characteristic.TargetHumidifierDehumidifierState.DEHUMIDIFIER
|
||||
: Characteristic.TargetHumidifierDehumidifierState.HUMIDIFIER);
|
||||
humidityService.getCharacteristic(Characteristic.TargetHumidifierDehumidifierState).on(CharacteristicEventTypes.SET, (value, callback) => {
|
||||
callback();
|
||||
device.setHumidity({
|
||||
mode: value === Characteristic.TargetHumidifierDehumidifierState.HUMIDIFIER
|
||||
? HumidityMode.Humidify
|
||||
: value === Characteristic.TargetHumidifierDehumidifierState.DEHUMIDIFIER
|
||||
? HumidityMode.Dehumidify
|
||||
: HumidityMode.Auto
|
||||
});
|
||||
});
|
||||
|
||||
bindCharacteristic(device, ScryptedInterface.HumiditySetting, humidityService, Characteristic.RelativeHumidityHumidifierThreshold,
|
||||
() => device.humiditySetting?.humidifierSetpoint || 0);
|
||||
humidityService.getCharacteristic(Characteristic.RelativeHumidityHumidifierThreshold).on(CharacteristicEventTypes.SET, (value, callback) => {
|
||||
callback();
|
||||
device.setHumidity({
|
||||
humidifierSetpoint: value as number,
|
||||
});
|
||||
});
|
||||
|
||||
bindCharacteristic(device, ScryptedInterface.HumiditySetting, humidityService, Characteristic.RelativeHumidityDehumidifierThreshold,
|
||||
() => device.humiditySetting?.dehumidifierSetpoint || 0);
|
||||
humidityService.getCharacteristic(Characteristic.RelativeHumidityDehumidifierThreshold).on(CharacteristicEventTypes.SET, (value, callback) => {
|
||||
callback();
|
||||
device.setHumidity({
|
||||
dehumidifierSetpoint: value as number,
|
||||
});
|
||||
});
|
||||
bindCharacteristic(device, ScryptedInterface.Fan, service, Characteristic.CurrentFanState,
|
||||
() => !device.fan?.active
|
||||
? Characteristic.CurrentFanState.INACTIVE
|
||||
: !device.fan.speed
|
||||
? Characteristic.CurrentFanState.IDLE
|
||||
: Characteristic.CurrentFanState.BLOWING_AIR);
|
||||
}
|
||||
|
||||
// add relataive target humidity to thermostat service even though it is not required or optional,
|
||||
// in order to expose to Home Assistant HomeKit Controller under their climate entity
|
||||
if (device.interfaces.includes(ScryptedInterface.HumiditySetting)) {
|
||||
function targetHumidity(setting: HumiditySettingStatus) {
|
||||
if (!setting)
|
||||
return 0;
|
||||
|
||||
if (setting?.availableModes.includes(HumidityMode.Humidify)
|
||||
&& setting?.availableModes.includes(HumidityMode.Dehumidify)) {
|
||||
if (setting?.activeMode === HumidityMode.Humidify)
|
||||
return setting?.humidifierSetpoint;
|
||||
if (setting?.activeMode === HumidityMode.Dehumidify)
|
||||
return setting?.dehumidifierSetpoint;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (setting?.availableModes.includes(HumidityMode.Humidify))
|
||||
return setting?.humidifierSetpoint;
|
||||
|
||||
if (setting?.availableModes.includes(HumidityMode.Dehumidify))
|
||||
return setting?.dehumidifierSetpoint;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
bindCharacteristic(device, ScryptedInterface.HumiditySetting, service, Characteristic.TargetRelativeHumidity,
|
||||
() => targetHumidity(device.humiditySetting));
|
||||
}
|
||||
|
||||
addHumiditySetting(device, accessory);
|
||||
addFan(device, accessory);
|
||||
addAirQualitySensor(device, accessory);
|
||||
addCarbonDioxideSensor(device, accessory);
|
||||
|
||||
4
plugins/objectdetector/package-lock.json
generated
4
plugins/objectdetector/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/objectdetector",
|
||||
"version": "0.1.65",
|
||||
"version": "0.1.66",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/objectdetector",
|
||||
"version": "0.1.65",
|
||||
"version": "0.1.66",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/objectdetector",
|
||||
"version": "0.1.65",
|
||||
"version": "0.1.66",
|
||||
"description": "Scrypted Video Analysis Plugin. Installed alongside a detection service like OpenCV or TensorFlow.",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
173
plugins/objectdetector/src/ffmpeg-audiosensor.ts
Normal file
173
plugins/objectdetector/src/ffmpeg-audiosensor.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
import sdk, { AudioSensor, FFmpegInput, MixinProvider, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, SettingValue, VideoCamera, WritableDeviceState } from "@scrypted/sdk";
|
||||
import { SettingsMixinDeviceBase, SettingsMixinDeviceOptions } from "@scrypted/sdk/settings-mixin";
|
||||
import { StorageSettings } from "@scrypted/sdk/storage-settings";
|
||||
import { startRtpForwarderProcess } from '../../webrtc/src/rtp-forwarders';
|
||||
import { RtpPacket } from "../../../external/werift/packages/rtp/src/rtp/rtp";
|
||||
import { sleep } from "@scrypted/common/src/sleep";
|
||||
|
||||
function pcmU8ToDb(payload: Uint8Array): number {
|
||||
let sum = 0;
|
||||
const count = payload.length;
|
||||
|
||||
if (count === 0) return 0; // Treat empty input as silence (0 dB)
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const sample = payload[i] - 128; // Convert to signed range (-128 to 127)
|
||||
sum += sample * sample;
|
||||
}
|
||||
|
||||
const rms = Math.sqrt(sum / count);
|
||||
const minRMS = 1.0; // Define a minimum reference level to avoid log(0)
|
||||
|
||||
if (rms < minRMS) return 0; // Silence is 0 dB
|
||||
|
||||
const db = 20 * Math.log10(rms / minRMS); // Scale against the minimum audible level
|
||||
return db;
|
||||
}
|
||||
|
||||
class FFmpegAudioDetectionMixin extends SettingsMixinDeviceBase<AudioSensor> implements AudioSensor {
|
||||
storageSettings = new StorageSettings(this, {
|
||||
decibelThreshold: {
|
||||
title: 'Decibel Threshold',
|
||||
type: 'number',
|
||||
description: 'The decibel level at which to trigger an event.',
|
||||
defaultValue: 20,
|
||||
},
|
||||
audioTimeout: {
|
||||
title: 'Audio Timeout',
|
||||
type: 'number',
|
||||
description: 'The number of seconds to wait after the last audio event before resetting the audio sensor.',
|
||||
defaultValue: 10,
|
||||
},
|
||||
});
|
||||
ensureInterval: NodeJS.Timeout;
|
||||
forwarder: ReturnType<typeof startRtpForwarderProcess>;
|
||||
audioResetInterval: NodeJS.Timeout;
|
||||
|
||||
constructor(options: SettingsMixinDeviceOptions<AudioSensor>) {
|
||||
super(options);
|
||||
this.ensureInterval = setInterval(() => this.ensureAudioSensor(), 60000);
|
||||
this.ensureAudioSensor();
|
||||
};
|
||||
|
||||
ensureAudioSensor() {
|
||||
if (!this.ensureInterval)
|
||||
return;
|
||||
|
||||
if (this.forwarder)
|
||||
return;
|
||||
|
||||
this.audioDetected = false;
|
||||
clearInterval(this.audioResetInterval);
|
||||
this.audioResetInterval = undefined;
|
||||
|
||||
const fp = this.ensureAudioSensorInternal();
|
||||
this.forwarder = fp;
|
||||
|
||||
fp.catch(() => {
|
||||
if (this.forwarder === fp)
|
||||
this.forwarder = undefined;
|
||||
});
|
||||
|
||||
this.forwarder.then(f => {
|
||||
f.killPromise.then(() => {
|
||||
if (this.forwarder === fp)
|
||||
this.forwarder = undefined;
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
async ensureAudioSensorInternal() {
|
||||
await sleep(5000);
|
||||
if (!this.forwarder)
|
||||
throw new Error('released/killed');
|
||||
const realDevice = sdk.systemManager.getDeviceById<VideoCamera>(this.id);
|
||||
const mo = await realDevice.getVideoStream({
|
||||
video: null,
|
||||
audio: {},
|
||||
});
|
||||
const ffmpegInput = await sdk.mediaManager.convertMediaObjectToJSON<FFmpegInput>(mo, ScryptedMimeTypes.FFmpegInput);
|
||||
|
||||
let lastAudio = 0;
|
||||
|
||||
const forwarder = await startRtpForwarderProcess(this.console, ffmpegInput, {
|
||||
video: null,
|
||||
audio: {
|
||||
codecCopy: 'pcm_u8',
|
||||
encoderArguments: [
|
||||
'-acodec', 'pcm_u8',
|
||||
'-ac', '1',
|
||||
'-ar', '8000',
|
||||
],
|
||||
onRtp: rtp => {
|
||||
const now = Date.now();
|
||||
// if this.audioDetected is true skip the processing unless the lastAudio time is halfway through the interval
|
||||
if (this.audioDetected && now - lastAudio < this.storageSettings.values.audioTimeout * 500)
|
||||
return;
|
||||
|
||||
const packet = RtpPacket.deSerialize(rtp);
|
||||
const decibels = pcmU8ToDb(packet.payload);
|
||||
if (decibels < this.storageSettings.values.decibelThreshold)
|
||||
return;
|
||||
|
||||
this.audioDetected = true;
|
||||
lastAudio = now;
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
this.audioResetInterval = setInterval(() => {
|
||||
if (!this.audioDetected)
|
||||
return;
|
||||
if (Date.now() - lastAudio < this.storageSettings.values.audioTimeout * 1000)
|
||||
return;
|
||||
this.audioDetected = false;
|
||||
}, this.storageSettings.values.audioTimeout * 1000);
|
||||
|
||||
return forwarder;
|
||||
}
|
||||
|
||||
async getMixinSettings() {
|
||||
return this.storageSettings.getSettings();
|
||||
}
|
||||
|
||||
putMixinSetting(key: string, value: SettingValue) {
|
||||
return this.storageSettings.putSetting(key, value);
|
||||
}
|
||||
|
||||
async release() {
|
||||
this.forwarder?.then(f => f.kill());
|
||||
this.forwarder = undefined;
|
||||
|
||||
clearInterval(this.ensureInterval);
|
||||
this.ensureInterval = undefined;
|
||||
|
||||
clearTimeout(this.audioResetInterval);
|
||||
this.audioResetInterval = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export class FFmpegAudioDetectionMixinProvider extends ScryptedDeviceBase implements MixinProvider {
|
||||
async canMixin(type: ScryptedDeviceType, interfaces: string[]) {
|
||||
if (type !== ScryptedDeviceType.Camera && type !== ScryptedDeviceType.Doorbell)
|
||||
return;
|
||||
if (!interfaces.includes(ScryptedInterface.VideoCamera))
|
||||
return;
|
||||
return [ScryptedInterface.AudioSensor, ScryptedInterface.Settings];
|
||||
}
|
||||
|
||||
async getMixin(mixinDevice: any, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: WritableDeviceState): Promise<any> {
|
||||
return new FFmpegAudioDetectionMixin({
|
||||
group: 'Audio Detection',
|
||||
groupKey: 'audio-detection',
|
||||
mixinDevice,
|
||||
mixinDeviceInterfaces,
|
||||
mixinDeviceState,
|
||||
mixinProviderNativeId: this.nativeId,
|
||||
});
|
||||
}
|
||||
|
||||
async releaseMixin(id: string, mixinDevice: any) {
|
||||
await (mixinDevice as FFmpegAudioDetectionMixin)?.release();
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import { fixLegacyClipPath, normalizeBox, polygonContainsBoundingBox, polygonInt
|
||||
import { SMART_MOTIONSENSOR_PREFIX, SmartMotionSensor } from './smart-motionsensor';
|
||||
import { SMART_OCCUPANCYSENSOR_PREFIX, SmartOccupancySensor } from './smart-occupancy-sensor';
|
||||
import { getAllDevices, safeParseJson } from './util';
|
||||
import { FFmpegAudioDetectionMixinProvider } from './ffmpeg-audiosensor';
|
||||
|
||||
|
||||
const { systemManager } = sdk;
|
||||
@@ -1056,7 +1057,16 @@ export class ObjectDetectionPlugin extends AutoenableMixinProvider implements Se
|
||||
ScryptedInterface.VideoFrameGenerator,
|
||||
],
|
||||
nativeId: 'ffmpeg',
|
||||
})
|
||||
});
|
||||
|
||||
sdk.deviceManager.onDeviceDiscovered({
|
||||
name: 'FFmpeg Audio Detection',
|
||||
type: ScryptedDeviceType.Builtin,
|
||||
interfaces: [
|
||||
ScryptedInterface.MixinProvider,
|
||||
],
|
||||
nativeId: 'ffmpeg-audio-detection',
|
||||
});
|
||||
});
|
||||
|
||||
// on an interval check to see if system load allows squelched detectors to start up.
|
||||
@@ -1195,6 +1205,8 @@ export class ObjectDetectionPlugin extends AutoenableMixinProvider implements Se
|
||||
let ret: any;
|
||||
if (nativeId === 'ffmpeg')
|
||||
ret = this.devices.get(nativeId) || new FFmpegVideoFrameGenerator('ffmpeg');
|
||||
if (nativeId === 'ffmpeg-audio-detection')
|
||||
ret = this.devices.get(nativeId) || new FFmpegAudioDetectionMixinProvider('ffmpeg-audio-detection');
|
||||
if (nativeId?.startsWith(SMART_MOTIONSENSOR_PREFIX))
|
||||
ret = this.devices.get(nativeId) || new SmartMotionSensor(this, nativeId);
|
||||
if (nativeId?.startsWith(SMART_OCCUPANCYSENSOR_PREFIX))
|
||||
|
||||
4
plugins/prebuffer-mixin/package-lock.json
generated
4
plugins/prebuffer-mixin/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/prebuffer-mixin",
|
||||
"version": "0.10.43",
|
||||
"version": "0.10.46",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/prebuffer-mixin",
|
||||
"version": "0.10.43",
|
||||
"version": "0.10.46",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/prebuffer-mixin",
|
||||
"version": "0.10.43",
|
||||
"version": "0.10.46",
|
||||
"description": "Video Stream Rebroadcast, Prebuffer, and Management Plugin for Scrypted.",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -63,8 +63,6 @@ class PrebufferSession {
|
||||
usingScryptedParser = false;
|
||||
usingScryptedUdpParser = false;
|
||||
|
||||
audioDisabled = false;
|
||||
|
||||
mixinDevice: VideoCamera;
|
||||
console: Console;
|
||||
storage: Storage;
|
||||
@@ -507,20 +505,15 @@ class PrebufferSession {
|
||||
catch (e) {
|
||||
}
|
||||
|
||||
// audio codecs are determined by probing the camera to see what it reports.
|
||||
// if the camera does not specify a codec, rebroadcast will force audio off
|
||||
// to determine the codec without causing a parse failure.
|
||||
// camera may explicity request that its audio stream be muted via a null.
|
||||
// respect that setting.
|
||||
const audioSoftMuted = mso?.audio === null;
|
||||
const advertisedAudioCodec = mso?.audio?.codec;
|
||||
const advertisedAudioCodec = !audioSoftMuted && mso?.audio?.codec;
|
||||
|
||||
let detectedAudioCodec = this.storage.getItem(this.lastDetectedAudioCodecKey) || undefined;
|
||||
if (detectedAudioCodec === 'null')
|
||||
detectedAudioCodec = null;
|
||||
|
||||
this.audioDisabled = false;
|
||||
|
||||
const rbo: ParserOptions<PrebufferParsers> = {
|
||||
console: this.console,
|
||||
timeout: 60000,
|
||||
@@ -604,7 +597,6 @@ class PrebufferSession {
|
||||
if (audioSoftMuted) {
|
||||
// no audio? explicitly disable it.
|
||||
acodec = ['-an'];
|
||||
this.audioDisabled = true;
|
||||
}
|
||||
else {
|
||||
acodec = [
|
||||
@@ -628,9 +620,6 @@ class PrebufferSession {
|
||||
const extraInputArguments = userInputArguments || DEFAULT_FFMPEG_INPUT_ARGUMENTS;
|
||||
const extraOutputArguments = this.storage.getItem(this.ffmpegOutputArgumentsKey) || '';
|
||||
ffmpegInput.inputArguments.unshift(...extraInputArguments.split(' '));
|
||||
// ehh this seems to cause issues with frames being updated in the webassembly decoder..?
|
||||
// if (!userInputArguments && (ffmpegInput.container === 'rtmp' || ffmpegInput.url?.startsWith('rtmp:')))
|
||||
// ffmpegInput.inputArguments.unshift('-use_wallclock_as_timestamps', '1');
|
||||
|
||||
if (ffmpegInput.h264EncoderArguments?.length) {
|
||||
vcodec = [...ffmpegInput.h264EncoderArguments];
|
||||
@@ -1024,6 +1013,9 @@ class PrebufferSession {
|
||||
mediaStreamOptions.video.h264Info = this.getLastH264Probe();
|
||||
}
|
||||
|
||||
if (this.mixin.streamSettings.storageSettings.values.noAudio)
|
||||
mediaStreamOptions.audio = null;
|
||||
|
||||
let socketPromise: Promise<Duplex>;
|
||||
let url: string;
|
||||
let urls: string[];
|
||||
@@ -1134,10 +1126,7 @@ class PrebufferSession {
|
||||
|
||||
mediaStreamOptions.prebuffer = requestedPrebuffer;
|
||||
|
||||
if (this.audioDisabled) {
|
||||
mediaStreamOptions.audio = null;
|
||||
}
|
||||
else if (audioSection) {
|
||||
if (audioSection) {
|
||||
mediaStreamOptions.audio ||= {};
|
||||
mediaStreamOptions.audio.codec ||= audioSection.rtpmap.codec;
|
||||
mediaStreamOptions.audio.sampleRate ||= audioSection.rtpmap.clock;
|
||||
|
||||
31
plugins/reolink/package-lock.json
generated
31
plugins/reolink/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/reolink",
|
||||
"version": "0.0.100",
|
||||
"version": "0.0.104",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/reolink",
|
||||
"version": "0.0.100",
|
||||
"version": "0.0.104",
|
||||
"license": "Apache",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
@@ -35,22 +35,29 @@
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.3.67",
|
||||
"version": "0.3.108",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.24.7",
|
||||
"adm-zip": "^0.5.14",
|
||||
"axios": "^1.7.3",
|
||||
"babel-loader": "^9.1.3",
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"@rollup/plugin-commonjs": "^28.0.1",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^15.3.0",
|
||||
"@rollup/plugin-typescript": "^12.1.1",
|
||||
"@rollup/plugin-virtual": "^3.0.2",
|
||||
"adm-zip": "^0.5.16",
|
||||
"axios": "^1.7.8",
|
||||
"babel-loader": "^9.2.1",
|
||||
"babel-plugin-const-enum": "^1.2.0",
|
||||
"ncp": "^2.0.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^6.0.1",
|
||||
"rollup": "^4.27.4",
|
||||
"tmp": "^0.2.3",
|
||||
"ts-loader": "^9.5.1",
|
||||
"typescript": "^5.5.4",
|
||||
"webpack": "^5.93.0",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.6.3",
|
||||
"webpack": "^5.96.1",
|
||||
"webpack-bundle-analyzer": "^4.10.2"
|
||||
},
|
||||
"bin": {
|
||||
@@ -63,11 +70,9 @@
|
||||
"scrypted-webpack": "bin/scrypted-webpack.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.1.0",
|
||||
"@types/stringify-object": "^4.0.5",
|
||||
"stringify-object": "^3.3.0",
|
||||
"@types/node": "^22.10.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typedoc": "^0.26.5"
|
||||
"typedoc": "^0.26.11"
|
||||
}
|
||||
},
|
||||
"../onvif/onvif": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/reolink",
|
||||
"version": "0.0.100",
|
||||
"version": "0.0.104",
|
||||
"description": "Reolink Plugin for Scrypted",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache",
|
||||
|
||||
@@ -725,7 +725,8 @@ class ReolinkCamera extends RtspSmartCamera implements Camera, DeviceProvider, R
|
||||
[
|
||||
"Reolink TrackMix PoE",
|
||||
"Reolink TrackMix WiFi",
|
||||
"RLC-81MA"
|
||||
"RLC-81MA",
|
||||
"Trackmix Series W760"
|
||||
].includes(deviceInfo?.model)) {
|
||||
streams.push({
|
||||
name: '',
|
||||
|
||||
@@ -2,7 +2,7 @@ import { AutoenableMixinProvider } from "@scrypted/common/src/autoenable-mixin-p
|
||||
import { AuthFetchCredentialState, authHttpFetch } from '@scrypted/common/src/http-auth-fetch';
|
||||
import { RefreshPromise, TimeoutError, createMapPromiseDebouncer, singletonPromise, timeoutPromise } from "@scrypted/common/src/promise-utils";
|
||||
import { SettingsMixinDeviceBase, SettingsMixinDeviceOptions } from "@scrypted/common/src/settings-mixin";
|
||||
import sdk, { BufferConverter, Camera, DeviceManifest, DeviceProvider, FFmpegInput, HttpRequest, HttpRequestHandler, HttpResponse, MediaObject, MediaObjectOptions, MixinProvider, RequestMediaStreamOptions, RequestPictureOptions, ResponsePictureOptions, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, SettingValue, Settings, VideoCamera, WritableDeviceState } from "@scrypted/sdk";
|
||||
import sdk, { BufferConverter, Camera, DeviceManifest, DeviceProvider, FFmpegInput, HttpRequest, HttpRequestHandler, HttpResponse, MediaObject, MediaObjectOptions, MixinProvider, Online, RequestMediaStreamOptions, RequestPictureOptions, ResponsePictureOptions, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, SettingValue, Settings, VideoCamera, WritableDeviceState } from "@scrypted/sdk";
|
||||
import { StorageSettings } from "@scrypted/sdk/storage-settings";
|
||||
import https from 'https';
|
||||
import os from 'os';
|
||||
@@ -127,9 +127,8 @@ class SnapshotMixin extends SettingsMixinDeviceBase<Camera> implements Camera {
|
||||
return this.console;
|
||||
}
|
||||
|
||||
async takePictureInternal(options?: RequestPictureOptions): Promise<Buffer> {
|
||||
this.debugConsole?.log("Picture requested from camera", options);
|
||||
const eventSnapshot = options?.reason === 'event';
|
||||
async takePictureInternal(id: string, eventSnapshot: boolean): Promise<Buffer> {
|
||||
this.debugConsole?.log("Picture requested from camera", { id, eventSnapshot });
|
||||
const { snapshotsFromPrebuffer } = this.storageSettings.values;
|
||||
let usePrebufferSnapshots: boolean;
|
||||
switch (snapshotsFromPrebuffer) {
|
||||
@@ -162,11 +161,12 @@ class SnapshotMixin extends SettingsMixinDeviceBase<Camera> implements Camera {
|
||||
}
|
||||
}
|
||||
|
||||
const realDevice = systemManager.getDeviceById<VideoCamera & Online>(this.id);
|
||||
|
||||
let takePrebufferPicture: () => Promise<Buffer>;
|
||||
const preparePrebufferSnapshot = async () => {
|
||||
if (takePrebufferPicture)
|
||||
return takePrebufferPicture;
|
||||
const realDevice = systemManager.getDeviceById<VideoCamera>(this.id);
|
||||
const msos = await realDevice.getVideoStreamOptions();
|
||||
let prebufferChannel = msos?.find(mso => mso.prebuffer);
|
||||
if (prebufferChannel || !this.lastAvailablePicture) {
|
||||
@@ -250,7 +250,7 @@ class SnapshotMixin extends SettingsMixinDeviceBase<Camera> implements Camera {
|
||||
|
||||
if (this.mixinDeviceInterfaces.includes(ScryptedInterface.Camera)) {
|
||||
let takePictureOptions: RequestPictureOptions;
|
||||
if (!options?.id && this.storageSettings.values.defaultSnapshotChannel !== 'Camera Default') {
|
||||
if (!id && this.storageSettings.values.defaultSnapshotChannel !== 'Camera Default') {
|
||||
try {
|
||||
const psos = await this.getPictureOptions();
|
||||
const pso = psos.find(pso => pso.name === this.storageSettings.values.defaultSnapshotChannel);
|
||||
@@ -262,6 +262,9 @@ class SnapshotMixin extends SettingsMixinDeviceBase<Camera> implements Camera {
|
||||
}
|
||||
}
|
||||
try {
|
||||
// consider waking the camera if
|
||||
if (!eventSnapshot && this.mixinDeviceInterfaces.includes(ScryptedInterface.Battery) && !realDevice.online)
|
||||
throw new Error('Not waking sleeping camera for periodic snapshot.');
|
||||
return await this.mixinDevice.takePicture(takePictureOptions).then(mo => mediaManager.convertMediaObjectToBuffer(mo, 'image/jpeg'))
|
||||
}
|
||||
catch (e) {
|
||||
@@ -289,7 +292,7 @@ class SnapshotMixin extends SettingsMixinDeviceBase<Camera> implements Camera {
|
||||
event: options?.reason === 'event',
|
||||
}, eventSnapshot ? 0 : 4000, async () => {
|
||||
const snapshotTimer = Date.now();
|
||||
let picture = await this.takePictureInternal();
|
||||
let picture = await this.takePictureInternal(undefined, eventSnapshot);
|
||||
picture = await this.cropAndScale(picture);
|
||||
this.clearCachedPictures();
|
||||
const pictureTime = Date.now();
|
||||
|
||||
4
sdk/package-lock.json
generated
4
sdk/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.3.106",
|
||||
"version": "0.3.110",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.3.106",
|
||||
"version": "0.3.110",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.3.106",
|
||||
"version": "0.3.110",
|
||||
"description": "",
|
||||
"main": "dist/src/index.js",
|
||||
"exports": {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import sdk, { ScryptedInterface, Setting, Settings, SettingValue } from ".";
|
||||
import sdk, { ScryptedDeviceType, ScryptedInterface, Setting, Settings, SettingValue } from ".";
|
||||
|
||||
const { systemManager } = sdk;
|
||||
|
||||
@@ -57,9 +57,9 @@ function parseValue(value: string | null | undefined, setting: StorageSetting, r
|
||||
return value || readDefaultValue();
|
||||
}
|
||||
|
||||
export type HideFunction = (device: any) => boolean;
|
||||
export interface StorageSetting extends Omit<Setting, 'deviceFilter'> {
|
||||
deviceFilter?: string | ((test: { id: string, deviceInterface: string, interfaces: string[], type: ScryptedDeviceType, ScryptedDeviceType: typeof ScryptedDeviceType, ScryptedInterface: typeof ScryptedInterface }) => boolean);
|
||||
|
||||
export interface StorageSetting extends Setting {
|
||||
defaultValue?: any;
|
||||
persistedDefaultValue?: any;
|
||||
onPut?: (oldValue: any, newValue: any) => void;
|
||||
@@ -140,7 +140,9 @@ export class StorageSettings<T extends string> implements Settings {
|
||||
continue;
|
||||
s.key = key;
|
||||
s.value = this.getItemInternal(key as T, s, true);
|
||||
ret.push(s);
|
||||
if (typeof s.deviceFilter === 'function')
|
||||
s.deviceFilter = s.deviceFilter.toString();
|
||||
ret.push(s as Setting);
|
||||
delete s.onPut;
|
||||
delete s.onGet;
|
||||
delete s.mapPut;
|
||||
|
||||
4
sdk/types/package-lock.json
generated
4
sdk/types/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/types",
|
||||
"version": "0.3.98",
|
||||
"version": "0.3.102",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/types",
|
||||
"version": "0.3.98",
|
||||
"version": "0.3.102",
|
||||
"license": "ISC"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/types",
|
||||
"version": "0.3.98",
|
||||
"version": "0.3.102",
|
||||
"description": "",
|
||||
"main": "dist/index.js",
|
||||
"author": "",
|
||||
|
||||
@@ -117,6 +117,7 @@ class ScryptedInterface(str, Enum):
|
||||
BinarySensor = "BinarySensor"
|
||||
Brightness = "Brightness"
|
||||
BufferConverter = "BufferConverter"
|
||||
Buttons = "Buttons"
|
||||
Camera = "Camera"
|
||||
Charger = "Charger"
|
||||
ClusterForkInterface = "ClusterForkInterface"
|
||||
@@ -166,6 +167,7 @@ class ScryptedInterface(str, Enum):
|
||||
PM25Sensor = "PM25Sensor"
|
||||
PositionSensor = "PositionSensor"
|
||||
PowerSensor = "PowerSensor"
|
||||
PressButtons = "PressButtons"
|
||||
Program = "Program"
|
||||
PushHandler = "PushHandler"
|
||||
Readme = "Readme"
|
||||
@@ -184,6 +186,7 @@ class ScryptedInterface(str, Enum):
|
||||
ScryptedUser = "ScryptedUser"
|
||||
SecuritySystem = "SecuritySystem"
|
||||
Settings = "Settings"
|
||||
Sleep = "Sleep"
|
||||
StartStop = "StartStop"
|
||||
StreamService = "StreamService"
|
||||
TamperSensor = "TamperSensor"
|
||||
@@ -199,6 +202,7 @@ class ScryptedInterface(str, Enum):
|
||||
VideoFrameGenerator = "VideoFrameGenerator"
|
||||
VideoRecorder = "VideoRecorder"
|
||||
VideoRecorderManagement = "VideoRecorderManagement"
|
||||
VideoTextOverlay = "VideoTextOverlay"
|
||||
VOCSensor = "VOCSensor"
|
||||
|
||||
class ScryptedMimeTypes(str, Enum):
|
||||
@@ -286,11 +290,6 @@ class ClipPath(TypedDict):
|
||||
pass
|
||||
|
||||
|
||||
class Point(TypedDict):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class AudioStreamOptions(TypedDict):
|
||||
|
||||
bitrate: float
|
||||
@@ -667,6 +666,7 @@ class NotifierOptions(TypedDict):
|
||||
badge: str
|
||||
body: str
|
||||
bodyWithSubtitle: str
|
||||
critical: bool
|
||||
data: Any
|
||||
dir: NotificationDirection
|
||||
image: str
|
||||
@@ -946,12 +946,17 @@ class MediaConverterTypes(TypedDict):
|
||||
pass
|
||||
|
||||
|
||||
class Point(TypedDict):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class TamperState(TypedDict):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
TYPES_VERSION = "0.3.98"
|
||||
TYPES_VERSION = "0.3.102"
|
||||
|
||||
|
||||
class AirPurifier:
|
||||
@@ -1006,6 +1011,10 @@ class BufferConverter:
|
||||
pass
|
||||
|
||||
|
||||
class Buttons:
|
||||
|
||||
buttons: list[str]
|
||||
|
||||
class Camera:
|
||||
"""Camera devices can take still photos."""
|
||||
|
||||
@@ -1376,6 +1385,12 @@ class PowerSensor:
|
||||
|
||||
powerDetected: bool
|
||||
|
||||
class PressButtons:
|
||||
|
||||
async def pressButton(self, button: str) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class Program:
|
||||
|
||||
async def run(self, variables: Any = None) -> Any:
|
||||
@@ -1523,6 +1538,10 @@ class Settings:
|
||||
pass
|
||||
|
||||
|
||||
class Sleep:
|
||||
|
||||
sleeping: bool
|
||||
|
||||
class StartStop:
|
||||
"""StartStop represents a device that can be started, stopped, and possibly paused and resumed. Typically vacuum cleaners or washers."""
|
||||
|
||||
@@ -1649,6 +1668,12 @@ class VideoRecorderManagement:
|
||||
pass
|
||||
|
||||
|
||||
class VideoTextOverlay:
|
||||
|
||||
fontSize: float
|
||||
origin: Point # The top left position of the overlay in the image, normalized to 0-1.
|
||||
text: str
|
||||
|
||||
class VOCSensor:
|
||||
|
||||
vocDensity: float
|
||||
@@ -1864,6 +1889,7 @@ class ScryptedInterfaceProperty(str, Enum):
|
||||
colorTemperature = "colorTemperature"
|
||||
rgb = "rgb"
|
||||
hsv = "hsv"
|
||||
buttons = "buttons"
|
||||
running = "running"
|
||||
paused = "paused"
|
||||
docked = "docked"
|
||||
@@ -1872,6 +1898,9 @@ class ScryptedInterfaceProperty(str, Enum):
|
||||
temperatureUnit = "temperatureUnit"
|
||||
humidity = "humidity"
|
||||
audioVolumes = "audioVolumes"
|
||||
fontSize = "fontSize"
|
||||
origin = "origin"
|
||||
text = "text"
|
||||
recordingActive = "recordingActive"
|
||||
ptzCapabilities = "ptzCapabilities"
|
||||
lockState = "lockState"
|
||||
@@ -1884,6 +1913,7 @@ class ScryptedInterfaceProperty(str, Enum):
|
||||
converters = "converters"
|
||||
binaryState = "binaryState"
|
||||
tampered = "tampered"
|
||||
sleeping = "sleeping"
|
||||
powerDetected = "powerDetected"
|
||||
audioDetected = "audioDetected"
|
||||
motionDetected = "motionDetected"
|
||||
@@ -1924,6 +1954,7 @@ class ScryptedInterfaceMethods(str, Enum):
|
||||
setColorTemperature = "setColorTemperature"
|
||||
setRgb = "setRgb"
|
||||
setHsv = "setHsv"
|
||||
pressButton = "pressButton"
|
||||
sendNotification = "sendNotification"
|
||||
start = "start"
|
||||
stop = "stop"
|
||||
@@ -2178,6 +2209,14 @@ class DeviceState:
|
||||
def hsv(self, value: ColorHsv):
|
||||
self.setScryptedProperty("hsv", value)
|
||||
|
||||
@property
|
||||
def buttons(self) -> list[str]:
|
||||
return self.getScryptedProperty("buttons")
|
||||
|
||||
@buttons.setter
|
||||
def buttons(self, value: list[str]):
|
||||
self.setScryptedProperty("buttons", value)
|
||||
|
||||
@property
|
||||
def running(self) -> bool:
|
||||
return self.getScryptedProperty("running")
|
||||
@@ -2242,6 +2281,30 @@ class DeviceState:
|
||||
def audioVolumes(self, value: AudioVolumes):
|
||||
self.setScryptedProperty("audioVolumes", value)
|
||||
|
||||
@property
|
||||
def fontSize(self) -> float:
|
||||
return self.getScryptedProperty("fontSize")
|
||||
|
||||
@fontSize.setter
|
||||
def fontSize(self, value: float):
|
||||
self.setScryptedProperty("fontSize", value)
|
||||
|
||||
@property
|
||||
def origin(self) -> Point:
|
||||
return self.getScryptedProperty("origin")
|
||||
|
||||
@origin.setter
|
||||
def origin(self, value: Point):
|
||||
self.setScryptedProperty("origin", value)
|
||||
|
||||
@property
|
||||
def text(self) -> str:
|
||||
return self.getScryptedProperty("text")
|
||||
|
||||
@text.setter
|
||||
def text(self, value: str):
|
||||
self.setScryptedProperty("text", value)
|
||||
|
||||
@property
|
||||
def recordingActive(self) -> bool:
|
||||
return self.getScryptedProperty("recordingActive")
|
||||
@@ -2338,6 +2401,14 @@ class DeviceState:
|
||||
def tampered(self, value: TamperState):
|
||||
self.setScryptedProperty("tampered", value)
|
||||
|
||||
@property
|
||||
def sleeping(self) -> bool:
|
||||
return self.getScryptedProperty("sleeping")
|
||||
|
||||
@sleeping.setter
|
||||
def sleeping(self, value: bool):
|
||||
self.setScryptedProperty("sleeping", value)
|
||||
|
||||
@property
|
||||
def powerDetected(self) -> bool:
|
||||
return self.getScryptedProperty("powerDetected")
|
||||
@@ -2612,6 +2683,20 @@ ScryptedInterfaceDescriptors = {
|
||||
"hsv"
|
||||
]
|
||||
},
|
||||
"Buttons": {
|
||||
"name": "Buttons",
|
||||
"methods": [],
|
||||
"properties": [
|
||||
"buttons"
|
||||
]
|
||||
},
|
||||
"PressButtons": {
|
||||
"name": "PressButtons",
|
||||
"methods": [
|
||||
"pressButton"
|
||||
],
|
||||
"properties": []
|
||||
},
|
||||
"Notifier": {
|
||||
"name": "Notifier",
|
||||
"methods": [
|
||||
@@ -2722,6 +2807,15 @@ ScryptedInterfaceDescriptors = {
|
||||
],
|
||||
"properties": []
|
||||
},
|
||||
"VideoTextOverlay": {
|
||||
"name": "VideoTextOverlay",
|
||||
"methods": [],
|
||||
"properties": [
|
||||
"fontSize",
|
||||
"origin",
|
||||
"text"
|
||||
]
|
||||
},
|
||||
"VideoRecorder": {
|
||||
"name": "VideoRecorder",
|
||||
"methods": [
|
||||
@@ -2938,6 +3032,13 @@ ScryptedInterfaceDescriptors = {
|
||||
"tampered"
|
||||
]
|
||||
},
|
||||
"Sleep": {
|
||||
"name": "Sleep",
|
||||
"methods": [],
|
||||
"properties": [
|
||||
"sleeping"
|
||||
]
|
||||
},
|
||||
"PowerSensor": {
|
||||
"name": "PowerSensor",
|
||||
"methods": [],
|
||||
|
||||
@@ -214,6 +214,14 @@ export interface ColorHsv {
|
||||
v?: number;
|
||||
}
|
||||
|
||||
export interface Buttons {
|
||||
buttons?: ('doorbell' | string)[];
|
||||
}
|
||||
|
||||
export interface PressButtons {
|
||||
pressButton(button: string): Promise<void>;
|
||||
}
|
||||
|
||||
export interface NotificationAction {
|
||||
action: string;
|
||||
icon?: string;
|
||||
@@ -234,6 +242,7 @@ export interface NotifierOptions {
|
||||
renotify?: boolean;
|
||||
requireInteraction?: boolean;
|
||||
silent?: boolean;
|
||||
critical?: boolean;
|
||||
/**
|
||||
* Collapse key/id.
|
||||
*/
|
||||
@@ -953,6 +962,20 @@ export interface VideoCameraMask {
|
||||
setPrivacyMasks(masks: PrivacyMasks): Promise<void>;
|
||||
}
|
||||
|
||||
export interface VideoTextOverlay {
|
||||
/**
|
||||
* The top left position of the overlay in the image, normalized to 0-1.
|
||||
*/
|
||||
origin?: Point;
|
||||
fontSize?: number;
|
||||
text?: string;
|
||||
}
|
||||
|
||||
export interface VideoTextOverlays {
|
||||
getVideoTextOverlays(): Promise<Record<string, string>>;
|
||||
setVideoTextOverlay(id: string, value: VideoTextOverlay): Promise<void>;
|
||||
}
|
||||
|
||||
export enum PanTiltZoomMovement {
|
||||
Absolute = "Absolute",
|
||||
Relative = "Relative",
|
||||
@@ -1259,6 +1282,10 @@ export interface Charger {
|
||||
chargeState?: ChargeState;
|
||||
}
|
||||
|
||||
export interface Sleep {
|
||||
sleeping?: boolean;
|
||||
}
|
||||
|
||||
export interface Reboot {
|
||||
reboot(): Promise<void>;
|
||||
}
|
||||
@@ -2255,6 +2282,8 @@ export enum ScryptedInterface {
|
||||
ColorSettingTemperature = "ColorSettingTemperature",
|
||||
ColorSettingRgb = "ColorSettingRgb",
|
||||
ColorSettingHsv = "ColorSettingHsv",
|
||||
Buttons = "Buttons",
|
||||
PressButtons = "PressButtons",
|
||||
Notifier = "Notifier",
|
||||
StartStop = "StartStop",
|
||||
Pause = "Pause",
|
||||
@@ -2268,6 +2297,7 @@ export enum ScryptedInterface {
|
||||
Display = "Display",
|
||||
VideoCamera = "VideoCamera",
|
||||
VideoCameraMask = "VideoCameraMask",
|
||||
VideoTextOverlay = "VideoTextOverlay",
|
||||
VideoRecorder = "VideoRecorder",
|
||||
VideoRecorderManagement = "VideoRecorderManagement",
|
||||
PanTiltZoom = "PanTiltZoom",
|
||||
@@ -2294,6 +2324,7 @@ export enum ScryptedInterface {
|
||||
Settings = "Settings",
|
||||
BinarySensor = "BinarySensor",
|
||||
TamperSensor = "TamperSensor",
|
||||
Sleep = "Sleep",
|
||||
PowerSensor = "PowerSensor",
|
||||
AudioSensor = "AudioSensor",
|
||||
MotionSensor = "MotionSensor",
|
||||
|
||||
5
server/.vscode/launch.json
vendored
5
server/.vscode/launch.json
vendored
@@ -99,11 +99,6 @@
|
||||
"env": {
|
||||
"SCRYPTED_PYTHON310_PATH": "/opt/homebrew/bin/python3.10",
|
||||
"DYLD_LIBRARY_PATH": "/usr/local/lib",
|
||||
"SCRYPTED_CLUSTER_WORKER_NAME": "Macaroni 2",
|
||||
"SCRYPTED_CLUSTER_LABELS": "@scrypted/coreml,@scrypted/tensorflow-lite,compute,compute.preferred",
|
||||
"SCRYPTED_CLUSTER_MODE": "client",
|
||||
"SCRYPTED_CLUSTER_SERVER": "192.168.2.130",
|
||||
"SCRYPTED_CLUSTER_SECRET": "swordfish",
|
||||
"SCRYPTED_CAN_RESTART": "true",
|
||||
"SCRYPTED_VOLUME": "/Users/koush/.scrypted-cluster/volume-client",
|
||||
}
|
||||
|
||||
12
server/package-lock.json
generated
12
server/package-lock.json
generated
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"name": "@scrypted/server",
|
||||
"version": "0.125.3",
|
||||
"version": "0.129.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/server",
|
||||
"version": "0.125.3",
|
||||
"version": "0.129.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/ffmpeg-static": "^6.1.0-build3",
|
||||
"@scrypted/node-pty": "^1.0.22",
|
||||
"@scrypted/types": "^0.3.92",
|
||||
"@scrypted/types": "^0.3.100",
|
||||
"adm-zip": "^0.5.16",
|
||||
"body-parser": "^1.20.3",
|
||||
"cookie-parser": "^1.4.7",
|
||||
@@ -557,9 +557,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@scrypted/types": {
|
||||
"version": "0.3.92",
|
||||
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.3.92.tgz",
|
||||
"integrity": "sha512-/M1Lg42/yoFWusj5+Lyp2S0JCiWDDWcmsjiUnTf1DahZ6/M2oZ3bwR/0KX3D9vJE79owWST1Gm0+Rdvpxuil9A==",
|
||||
"version": "0.3.100",
|
||||
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.3.100.tgz",
|
||||
"integrity": "sha512-s/07QCxjMWqODgWj2UpLehzeo2cGFrCA9X8mvpG3owT/+q+sb8v/UUcw9TLHGSN6yIriNhceg3i9WO07kEIT6A==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@types/adm-zip": {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "@scrypted/server",
|
||||
"version": "0.127.1",
|
||||
"version": "0.130.3",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"@scrypted/ffmpeg-static": "^6.1.0-build3",
|
||||
"@scrypted/node-pty": "^1.0.22",
|
||||
"@scrypted/types": "^0.3.92",
|
||||
"@scrypted/types": "^0.3.100",
|
||||
"adm-zip": "^0.5.16",
|
||||
"body-parser": "^1.20.3",
|
||||
"cookie-parser": "^1.4.7",
|
||||
|
||||
@@ -144,7 +144,11 @@ class ClusterSetup:
|
||||
m = hashlib.sha256()
|
||||
m.update(
|
||||
bytes(
|
||||
f"{o['id']}{o.get('address', '')}{o['port']}{o.get('sourceKey', '')}{o['proxyId']}{self.clusterSecret}",
|
||||
# The use of ` o.get(key, None) or '' ` is to ensure that optional fields
|
||||
# are omitted from the hash, matching the JS implementation. Otherwise, since
|
||||
# the dict may contain the keys initialized to None, ` o.get(key, '') ` would
|
||||
# return None instead of ''.
|
||||
f"{o['id']}{o.get('address', None) or ''}{o['port']}{o.get('sourceKey', None) or ''}{o['proxyId']}{self.clusterSecret}",
|
||||
"utf8",
|
||||
)
|
||||
)
|
||||
|
||||
@@ -4,7 +4,6 @@ import fs from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import process from 'process';
|
||||
import semver from 'semver';
|
||||
import { ensurePluginVolume } from "./plugin-volume";
|
||||
|
||||
export function defaultNpmExec(args: string[], options: child_process.SpawnOptions) {
|
||||
@@ -26,8 +25,16 @@ export function setNpmExecFunction(f: typeof npmExecFunction) {
|
||||
|
||||
export function getPluginNodePath(name: string) {
|
||||
const pluginVolume = ensurePluginVolume(name);
|
||||
const nodeMajorVersion = semver.parse(process.version).major;
|
||||
let nodeVersionedDirectory = `node${nodeMajorVersion}-${process.platform}-${process.arch}`;
|
||||
|
||||
const abi = process.versions.modules;
|
||||
let runtime = process.env.npm_config_runtime;
|
||||
if (!runtime && process.versions.electron)
|
||||
runtime = 'electron';
|
||||
if (!runtime)
|
||||
runtime = 'node';
|
||||
const { platform, arch } = process;
|
||||
let nodeVersionedDirectory = `n-${runtime}-v${abi}-${platform}-${arch}`;
|
||||
|
||||
const scryptedBase = process.env.SCRYPTED_BASE_VERSION;
|
||||
if (scryptedBase)
|
||||
nodeVersionedDirectory += '-' + scryptedBase;
|
||||
@@ -98,7 +105,7 @@ export async function installOptionalDependencies(console: Console, packageJson:
|
||||
if (!de.isDirectory())
|
||||
return;
|
||||
if (de.name.startsWith('linux') || de.name.startsWith('darwin') || de.name.startsWith('win32')
|
||||
|| de.name.startsWith('python') || de.name.startsWith('node')) {
|
||||
|| de.name.startsWith('python') || de.name.startsWith('node') || de.name.startsWith('n-')) {
|
||||
console.log('Removing old dependencies:', filePath);
|
||||
try {
|
||||
await fs.promises.rm(filePath, {
|
||||
|
||||
@@ -24,6 +24,7 @@ import { NodeThreadWorker } from './runtime/node-thread-worker';
|
||||
import { prepareZip } from './runtime/node-worker-common';
|
||||
import { getBuiltinRuntimeHosts } from './runtime/runtime-host';
|
||||
import { RuntimeWorker, RuntimeWorkerOptions } from './runtime/runtime-worker';
|
||||
import { Deferred } from '../deferred';
|
||||
|
||||
const serverVersion = require('../../package.json').version;
|
||||
|
||||
@@ -291,6 +292,14 @@ export function startPluginRemote(mainFilename: string, pluginId: string, peerSe
|
||||
forkPeer = Promise.resolve(localPeer);
|
||||
}
|
||||
|
||||
const exitDeferred = new Deferred<string>();
|
||||
runtimeWorker.on('exit', () => {
|
||||
exitDeferred.resolve('worker exited');
|
||||
});
|
||||
runtimeWorker.on('error', e => {
|
||||
exitDeferred.resolve('worker error' + e);
|
||||
});
|
||||
|
||||
// thread workers inherit main console. pipe anything else.
|
||||
if (!(runtimeWorker instanceof NodeThreadWorker)) {
|
||||
const console = options?.id ? getMixinConsole(options.id, options.nativeId) : undefined;
|
||||
@@ -299,6 +308,9 @@ export function startPluginRemote(mainFilename: string, pluginId: string, peerSe
|
||||
|
||||
const result = (async () => {
|
||||
const threadPeer = await forkPeer;
|
||||
exitDeferred.promise.then(reason => {
|
||||
threadPeer.kill(reason);
|
||||
});
|
||||
|
||||
// todo: handle nested forks and skip wrap. this is probably buggy.
|
||||
class PluginForkAPI extends PluginAPIProxy {
|
||||
@@ -319,13 +331,7 @@ export function startPluginRemote(mainFilename: string, pluginId: string, peerSe
|
||||
|
||||
const remote = await setupPluginRemote(threadPeer, forkApi, pluginId, { serverVersion }, () => systemManager.getSystemState());
|
||||
forks.add(remote);
|
||||
runtimeWorker.on('exit', () => {
|
||||
threadPeer.kill('worker exited');
|
||||
forkApi.removeListeners();
|
||||
forks.delete(remote);
|
||||
});
|
||||
runtimeWorker.on('error', e => {
|
||||
threadPeer.kill('worker error ' + e);
|
||||
exitDeferred.promise.then(reason => {
|
||||
forkApi.removeListeners();
|
||||
forks.delete(remote);
|
||||
});
|
||||
|
||||
@@ -48,7 +48,12 @@ export class NodeForkWorker extends ChildProcessWorker {
|
||||
this.pluginId
|
||||
];
|
||||
|
||||
const nodePaths: string[] = [path.resolve(__dirname, '..', '..', '..', 'node_modules')];
|
||||
const nodePaths: string[] = [
|
||||
// /server/node_modules/@scrypted/server/node_modules
|
||||
path.resolve(__dirname, '..', '..', '..', 'node_modules'),
|
||||
// /server/node_modules
|
||||
path.resolve(process.cwd(), 'node_modules'),
|
||||
];
|
||||
if (env?.NODE_PATH)
|
||||
nodePaths.push(env.NODE_PATH);
|
||||
if (process.env.NODE_PATH)
|
||||
|
||||
Reference in New Issue
Block a user