mirror of
https://github.com/koush/scrypted.git
synced 2026-02-03 22:23:27 +00:00
Compare commits
178 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7dc476fe02 | ||
|
|
f5070f1ff1 | ||
|
|
15283e13f0 | ||
|
|
0cde5bf8e7 | ||
|
|
fe3a1a023d | ||
|
|
369dcff2bd | ||
|
|
ed341a12b1 | ||
|
|
00e523e268 | ||
|
|
4e25aedbe7 | ||
|
|
45bd3cbb7c | ||
|
|
8e34bc2130 | ||
|
|
457fc96332 | ||
|
|
e2186401bf | ||
|
|
a19d916ef0 | ||
|
|
42bc7dc644 | ||
|
|
f9d6308154 | ||
|
|
dcb6627fb1 | ||
|
|
1d5c71d617 | ||
|
|
d5157fb868 | ||
|
|
98096845dc | ||
|
|
28ac97f4c9 | ||
|
|
2fc39e3979 | ||
|
|
9c89c3c2b8 | ||
|
|
15c7747f48 | ||
|
|
940d4b7fd4 | ||
|
|
a1c8ce754e | ||
|
|
5e6364850a | ||
|
|
8df52e7595 | ||
|
|
1e004d6700 | ||
|
|
4570f9cd38 | ||
|
|
601cd39ba4 | ||
|
|
923475fab2 | ||
|
|
21ce5dfad4 | ||
|
|
2bd3592aad | ||
|
|
44f083ca23 | ||
|
|
cc7271f0a2 | ||
|
|
11a1a1134d | ||
|
|
70cfa13e67 | ||
|
|
291f90b2b2 | ||
|
|
d0ae7eb841 | ||
|
|
8444102cca | ||
|
|
5a1c052c77 | ||
|
|
fb7eeece54 | ||
|
|
d479bcece9 | ||
|
|
deefac2347 | ||
|
|
53808a04b7 | ||
|
|
a1785c2658 | ||
|
|
601cf46b1e | ||
|
|
6bba1b1cbd | ||
|
|
ab0122420b | ||
|
|
74ae2aab91 | ||
|
|
c5fa131a44 | ||
|
|
8dcf4dda9f | ||
|
|
cd59125ada | ||
|
|
d284eb6738 | ||
|
|
a78cc943cc | ||
|
|
7ddeda1595 | ||
|
|
f02dfa5e14 | ||
|
|
b2a4f20381 | ||
|
|
dec3c354f0 | ||
|
|
2ee581d48d | ||
|
|
d74c3a3fc5 | ||
|
|
405d9f0c09 | ||
|
|
db25c5babe | ||
|
|
d5c90ab8da | ||
|
|
81a5c143d6 | ||
|
|
ebf2176618 | ||
|
|
f435f8eff5 | ||
|
|
f8c16edaae | ||
|
|
ea86065d99 | ||
|
|
ed5c7b126c | ||
|
|
806e015823 | ||
|
|
41c4cbc96c | ||
|
|
143a0b2c41 | ||
|
|
f582db3f11 | ||
|
|
103855ca50 | ||
|
|
70c6fe4c68 | ||
|
|
c85d45050f | ||
|
|
16a39ac76a | ||
|
|
fdc7519db0 | ||
|
|
83af0c5ec7 | ||
|
|
ee22686bff | ||
|
|
7dc1f9736a | ||
|
|
6e2aa37d75 | ||
|
|
fbaa8a31cf | ||
|
|
fa89a5ad24 | ||
|
|
464deaf35e | ||
|
|
9cc8f50ff7 | ||
|
|
c17a1184cc | ||
|
|
b5004739c3 | ||
|
|
d01c0fa72b | ||
|
|
bb9f3d5aab | ||
|
|
b23daa6735 | ||
|
|
bb8b0125b6 | ||
|
|
8e5f44f998 | ||
|
|
9015af4902 | ||
|
|
7902a091a9 | ||
|
|
615357befb | ||
|
|
34b26c81dc | ||
|
|
ea99a54e1b | ||
|
|
f726826391 | ||
|
|
dc5148c856 | ||
|
|
373c11ffee | ||
|
|
bea1f019b4 | ||
|
|
29c98777e9 | ||
|
|
9eb5029128 | ||
|
|
33607796d1 | ||
|
|
f23fa0c335 | ||
|
|
e6cfecfc1a | ||
|
|
44346d5b33 | ||
|
|
19da68884b | ||
|
|
544349de8d | ||
|
|
6f90b1a0e3 | ||
|
|
fbbb9163d7 | ||
|
|
445581eefa | ||
|
|
096c036ea2 | ||
|
|
b2e5801426 | ||
|
|
41061854f1 | ||
|
|
d91e625973 | ||
|
|
ec5b59a00c | ||
|
|
172790b18f | ||
|
|
de0e6ee955 | ||
|
|
69d7ff2ced | ||
|
|
3c237eac91 | ||
|
|
694c195024 | ||
|
|
c1f0281030 | ||
|
|
fa218cbcbd | ||
|
|
a89700acc2 | ||
|
|
82fb24e275 | ||
|
|
eef67a9383 | ||
|
|
1180d9fa2c | ||
|
|
57734f1d3c | ||
|
|
dace750829 | ||
|
|
f359a7167a | ||
|
|
39c0759d1b | ||
|
|
fee90334fb | ||
|
|
80db6e50ab | ||
|
|
1fa6c2d842 | ||
|
|
8b39c4c22c | ||
|
|
4b6fd5b5a8 | ||
|
|
f2d1909b6d | ||
|
|
7917fb96dc | ||
|
|
ad5fae98f1 | ||
|
|
8412eb36fe | ||
|
|
822455383b | ||
|
|
2d4357e4c0 | ||
|
|
6336407f15 | ||
|
|
38e3492137 | ||
|
|
255e426e2d | ||
|
|
fed1cf2a0d | ||
|
|
a5cb8c3fdc | ||
|
|
514d86144f | ||
|
|
21db7934c9 | ||
|
|
14e4b5c0e3 | ||
|
|
6478ad0411 | ||
|
|
81b7d432e9 | ||
|
|
dcbf5094f9 | ||
|
|
69ba181dfa | ||
|
|
88654275c1 | ||
|
|
62f28271ed | ||
|
|
0538130e78 | ||
|
|
95a3a16227 | ||
|
|
bfbf89ff69 | ||
|
|
e630589489 | ||
|
|
99d1e51f36 | ||
|
|
ab42ccd889 | ||
|
|
767af25aa0 | ||
|
|
7575dd82ce | ||
|
|
9307bbd09e | ||
|
|
68a9ec09e6 | ||
|
|
f8a548401f | ||
|
|
26d1f8e58c | ||
|
|
8772e25c8e | ||
|
|
378ac82c8c | ||
|
|
fcb1292ffd | ||
|
|
18112ee40f | ||
|
|
fa8b9dfe99 | ||
|
|
e7dff4edc9 |
9
.gitmodules
vendored
9
.gitmodules
vendored
@@ -19,6 +19,7 @@
|
||||
[submodule "external/ring-client-api"]
|
||||
path = external/ring-client-api
|
||||
url = ../../koush/ring
|
||||
branch = fork
|
||||
[submodule "plugins/vscode-typescript"]
|
||||
path = plugins/vscode-typescript
|
||||
url = ../../koush/scrypted-vscode-typescript/
|
||||
@@ -28,20 +29,14 @@
|
||||
[submodule "plugins/zwave/file-stream-rotator"]
|
||||
path = plugins/zwave/file-stream-rotator
|
||||
url = ../../koush/file-stream-rotator.git
|
||||
[submodule "external/push-receiver"]
|
||||
path = external/push-receiver
|
||||
url = ../../koush/push-receiver.git
|
||||
[submodule "sdk/developer.scrypted.app"]
|
||||
path = sdk/developer.scrypted.app
|
||||
url = ../../koush/developer.scrypted.app
|
||||
[submodule "plugins/sample-cameraprovider"]
|
||||
path = plugins/sample-cameraprovider
|
||||
url = ../../koush/scrypted-sample-cameraprovider
|
||||
[submodule "plugins/objectdetector/node-moving-things-tracker"]
|
||||
path = plugins/objectdetector/node-moving-things-tracker
|
||||
url = ../../koush/node-moving-things-tracker.git
|
||||
[submodule "plugins/tensorflow-lite/sort_oh"]
|
||||
path = plugins/tensorflow-lite/sort_oh
|
||||
path = plugins/sort-tracker/sort_oh
|
||||
url = ../../koush/sort_oh.git
|
||||
[submodule "plugins/cloud/node-nat-upnp"]
|
||||
path = plugins/cloud/node-nat-upnp
|
||||
|
||||
@@ -2,14 +2,15 @@ import type { TranspileOptions } from "typescript";
|
||||
import sdk, { ScryptedDeviceBase, MixinDeviceBase, ScryptedInterface, ScryptedDeviceType } from "@scrypted/sdk";
|
||||
import vm from "vm";
|
||||
import fs from 'fs';
|
||||
import { newThread } from '@scrypted/server/src/threading';
|
||||
import { ScriptDevice } from "./monaco/script-device";
|
||||
import { ScryptedInterfaceDescriptors } from "@scrypted/sdk";
|
||||
import fetch from 'node-fetch-commonjs';
|
||||
import { PluginAPIProxy } from '../../../server/src/plugin/plugin-api';
|
||||
import { SystemManagerImpl } from '../../../server/src/plugin/system';
|
||||
|
||||
const { systemManager, deviceManager, mediaManager, endpointManager } = sdk;
|
||||
|
||||
function tsCompile(source: string, options: TranspileOptions = null): string {
|
||||
export async function tsCompile(source: string, options: TranspileOptions = null): Promise<string> {
|
||||
const ts = require("typescript");
|
||||
const { ScriptTarget } = ts;
|
||||
|
||||
@@ -25,27 +26,6 @@ function tsCompile(source: string, options: TranspileOptions = null): string {
|
||||
return ts.transpileModule(source, options).outputText;
|
||||
}
|
||||
|
||||
async function tsCompileThread(source: string, options: TranspileOptions = null): Promise<string> {
|
||||
return newThread({
|
||||
source, options,
|
||||
customRequire: '__webpack_require__',
|
||||
}, ({ source, options }) => {
|
||||
const ts = global.require("typescript");
|
||||
const { ScriptTarget } = ts;
|
||||
|
||||
// Default options -- you could also perform a merge, or use the project tsconfig.json
|
||||
if (null === options) {
|
||||
options = {
|
||||
compilerOptions: {
|
||||
target: ScriptTarget.ESNext,
|
||||
module: ts.ModuleKind.CommonJS
|
||||
}
|
||||
};
|
||||
}
|
||||
return ts.transpileModule(source, options).outputText;
|
||||
});
|
||||
}
|
||||
|
||||
function getTypeDefs() {
|
||||
const scryptedTypesDefs = fs.readFileSync('@types/sdk/types.d.ts').toString();
|
||||
const scryptedIndexDefs = fs.readFileSync('@types/sdk/index.d.ts').toString();
|
||||
@@ -61,14 +41,27 @@ export async function scryptedEval(device: ScryptedDeviceBase, script: string, e
|
||||
}, extraLibs);
|
||||
const allScripts = Object.values(libs).join('\n').toString() + script;
|
||||
let compiled: string;
|
||||
const worker = sdk.fork<{
|
||||
tsCompile: typeof tsCompile,
|
||||
}>();
|
||||
worker.worker.on('error', () => { })
|
||||
try {
|
||||
compiled = await tsCompileThread(allScripts);
|
||||
const result = await worker.result;
|
||||
compiled = await result.tsCompile(allScripts);
|
||||
}
|
||||
catch (e) {
|
||||
device.log.e('Error compiling typescript.');
|
||||
device.console.error(e);
|
||||
throw e;
|
||||
}
|
||||
finally {
|
||||
worker.worker.terminate();
|
||||
}
|
||||
|
||||
const smProxy = new SystemManagerImpl();
|
||||
smProxy.state = systemManager.getSystemState();
|
||||
const apiProxy = new PluginAPIProxy(sdk.pluginHostAPI);
|
||||
smProxy.api = apiProxy;
|
||||
|
||||
const allParams = Object.assign({}, params, {
|
||||
sdk,
|
||||
@@ -76,7 +69,7 @@ export async function scryptedEval(device: ScryptedDeviceBase, script: string, e
|
||||
fetch,
|
||||
ScryptedDeviceBase,
|
||||
MixinDeviceBase,
|
||||
systemManager,
|
||||
systemManager: smProxy,
|
||||
deviceManager,
|
||||
endpointManager,
|
||||
mediaManager,
|
||||
@@ -111,6 +104,7 @@ export async function scryptedEval(device: ScryptedDeviceBase, script: string, e
|
||||
return {
|
||||
value,
|
||||
defaultExport,
|
||||
apiProxy,
|
||||
};
|
||||
}
|
||||
catch (e) {
|
||||
|
||||
@@ -4,10 +4,10 @@ import { once } from 'events';
|
||||
import { BASIC } from 'http-auth-utils/dist/index';
|
||||
import { parseHTTPHeadersQuotedKeyValueSet } from 'http-auth-utils/dist/utils';
|
||||
import net from 'net';
|
||||
import { Duplex, Readable } from 'stream';
|
||||
import { Duplex, Readable, Writable } from 'stream';
|
||||
import tls from 'tls';
|
||||
import { Deferred } from './deferred';
|
||||
import { closeQuiet, createBindUdp, createBindZero } from './listen-cluster';
|
||||
import { closeQuiet, createBindUdp, createBindZero, listenZeroSingleClient } from './listen-cluster';
|
||||
import { timeoutPromise } from './promise-utils';
|
||||
import { readLength, readLine } from './read-stream';
|
||||
import { MSection, parseSdp } from './sdp-utils';
|
||||
@@ -47,6 +47,29 @@ export async function readMessage(client: Readable): Promise<string[]> {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export async function readBody(client: Readable, response: Headers) {
|
||||
const cl = parseInt(response['content-length']);
|
||||
if (cl)
|
||||
return readLength(client, cl)
|
||||
}
|
||||
|
||||
|
||||
export function writeMessage(client: Writable, messageLine: string, body: Buffer, headers: Headers, console?: Console) {
|
||||
let message = messageLine !== undefined ? `${messageLine}\r\n` : '';
|
||||
if (body)
|
||||
headers['Content-Length'] = body.length.toString();
|
||||
for (const [key, value] of Object.entries(headers)) {
|
||||
message += `${key}: ${value}\r\n`;
|
||||
}
|
||||
message += '\r\n';
|
||||
client.write(message);
|
||||
console?.log('rtsp outgoing message\n', message);
|
||||
console?.log();
|
||||
if (body)
|
||||
client.write(body);
|
||||
}
|
||||
|
||||
// https://yumichan.net/video-processing/video-compression/introduction-to-h264-nal-unit/
|
||||
|
||||
export const H264_NAL_TYPE_RESERVED0 = 0;
|
||||
@@ -284,18 +307,7 @@ export class RtspBase {
|
||||
}
|
||||
|
||||
write(messageLine: string, headers: Headers, body?: Buffer) {
|
||||
let message = `${messageLine}\r\n`;
|
||||
if (body)
|
||||
headers['Content-Length'] = body.length.toString();
|
||||
for (const [key, value] of Object.entries(headers)) {
|
||||
message += `${key}: ${value}\r\n`;
|
||||
}
|
||||
message += '\r\n';
|
||||
this.client.write(message);
|
||||
this.console?.log('rtsp outgoing message\n', message);
|
||||
this.console?.log();
|
||||
if (body)
|
||||
this.client.write(body);
|
||||
writeMessage(this.client, messageLine, body, headers, this.console);
|
||||
}
|
||||
|
||||
async readMessage(): Promise<string[]> {
|
||||
@@ -590,9 +602,7 @@ export class RtspClient extends RtspBase {
|
||||
}
|
||||
|
||||
async readBody(response: Headers) {
|
||||
const cl = parseInt(response['content-length']);
|
||||
if (cl)
|
||||
return readLength(this.client, cl)
|
||||
return readBody(this.client, response);
|
||||
}
|
||||
|
||||
async request(method: string, headers?: Headers, path?: string, body?: Buffer, authenticating?: boolean): Promise<RtspServerResponse> {
|
||||
@@ -1053,3 +1063,33 @@ export class RtspServer {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function listenSingleRtspClient<T extends RtspServer>(options?: {
|
||||
hostname?: string,
|
||||
pathToken?: string,
|
||||
createServer?(duplex: Duplex): T,
|
||||
}) {
|
||||
const pathToken = options?.pathToken || crypto.randomBytes(8).toString('hex');
|
||||
let { url, clientPromise, server } = await listenZeroSingleClient(options?.hostname);
|
||||
|
||||
const rtspServerPath = '/' + pathToken;
|
||||
url = url.replace('tcp:', 'rtsp:') + rtspServerPath;
|
||||
|
||||
const rtspServerPromise = clientPromise.then(client => {
|
||||
const createServer = options?.createServer || (duplex => new RtspServer(duplex));
|
||||
|
||||
const rtspServer = createServer(client);
|
||||
rtspServer.checkRequest = async (method, url, headers, message) => {
|
||||
rtspServer.checkRequest = undefined;
|
||||
const u = new URL(url);
|
||||
return u.pathname === rtspServerPath;
|
||||
};
|
||||
return rtspServer as T;
|
||||
});
|
||||
|
||||
return {
|
||||
url,
|
||||
rtspServerPromise,
|
||||
server,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,20 +31,18 @@ RUN apt-get -y install \
|
||||
build-essential \
|
||||
cmake \
|
||||
gcc \
|
||||
gir1.2-gtk-3.0 \
|
||||
libcairo2-dev \
|
||||
libgirepository1.0-dev \
|
||||
libglib2.0-dev \
|
||||
libjpeg-dev \
|
||||
libgif-dev \
|
||||
libopenjp2-7 \
|
||||
libpango1.0-dev \
|
||||
librsvg2-dev \
|
||||
pkg-config
|
||||
|
||||
# ffmpeg
|
||||
RUN apt-get -y install \
|
||||
ffmpeg
|
||||
|
||||
# gstreamer native https://gstreamer.freedesktop.org/documentation/installing/on-linux.html?gi-language=c#install-gstreamer-on-ubuntu-or-debian
|
||||
RUN apt-get -y install \
|
||||
gstreamer1.0-tools libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libgstreamer-plugins-bad1.0-dev gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav gstreamer1.0-alsa
|
||||
gstreamer1.0-tools libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libgstreamer-plugins-bad1.0-dev gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav gstreamer1.0-alsa \
|
||||
gstreamer1.0-vaapi
|
||||
|
||||
# python native
|
||||
RUN apt-get -y install \
|
||||
@@ -64,7 +62,6 @@ RUN apt-get -y install \
|
||||
# python pip
|
||||
RUN python3 -m pip install --upgrade pip
|
||||
RUN python3 -m pip install aiofiles debugpy typing_extensions typing psutil
|
||||
RUN python3 -m pip install dlib
|
||||
|
||||
################################################################
|
||||
# End section generated from template/Dockerfile.full.header
|
||||
|
||||
@@ -20,6 +20,11 @@ RUN apt-get -y install \
|
||||
libglib2.0-dev \
|
||||
pkg-config
|
||||
|
||||
# ffmpeg
|
||||
RUN apt-get -y install \
|
||||
ffmpeg
|
||||
ENV SCRYPTED_FFMPEG_PATH=ffmpeg
|
||||
|
||||
# python native
|
||||
RUN apt-get -y install \
|
||||
python3 \
|
||||
|
||||
@@ -19,6 +19,11 @@ RUN apt-get -y install \
|
||||
libglib2.0-dev \
|
||||
pkg-config
|
||||
|
||||
# ffmpeg
|
||||
RUN apt-get -y install \
|
||||
ffmpeg
|
||||
ENV SCRYPTED_FFMPEG_PATH=ffmpeg
|
||||
|
||||
ENV SCRYPTED_DOCKER_SERVE="true"
|
||||
ENV SCRYPTED_CAN_RESTART="true"
|
||||
ENV SCRYPTED_VOLUME="/server/volume"
|
||||
|
||||
@@ -38,6 +38,8 @@ services:
|
||||
# - /dev/ttyACM0:/dev/ttyACM0
|
||||
# all usb devices, such as coral tpu
|
||||
# - /dev/bus/usb:/dev/bus/usb
|
||||
# intel hardware accelerated video decoding
|
||||
# - /dev/dri:/dev/dri
|
||||
|
||||
volumes:
|
||||
- ~/.scrypted/volume:/server/volume
|
||||
|
||||
@@ -26,21 +26,20 @@ RUN apt-get -y upgrade
|
||||
# base development stuff
|
||||
RUN apt-get -y install \
|
||||
build-essential \
|
||||
cmake \
|
||||
gcc \
|
||||
gir1.2-gtk-3.0 \
|
||||
libcairo2-dev \
|
||||
libgirepository1.0-dev \
|
||||
libglib2.0-dev \
|
||||
libjpeg-dev \
|
||||
libgif-dev \
|
||||
libopenjp2-7 \
|
||||
libpango1.0-dev \
|
||||
librsvg2-dev \
|
||||
pkg-config
|
||||
|
||||
# ffmpeg
|
||||
RUN apt-get -y install \
|
||||
ffmpeg
|
||||
|
||||
# gstreamer native https://gstreamer.freedesktop.org/documentation/installing/on-linux.html?gi-language=c#install-gstreamer-on-ubuntu-or-debian
|
||||
RUN apt-get -y install \
|
||||
gstreamer1.0-tools libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libgstreamer-plugins-bad1.0-dev gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav gstreamer1.0-alsa
|
||||
gstreamer1.0-tools libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libgstreamer-plugins-bad1.0-dev gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav gstreamer1.0-alsa \
|
||||
gstreamer1.0-vaapi
|
||||
|
||||
# python native
|
||||
RUN apt-get -y install \
|
||||
|
||||
1
external/push-receiver
vendored
1
external/push-receiver
vendored
Submodule external/push-receiver deleted from d054e083d6
2
external/ring-client-api
vendored
2
external/ring-client-api
vendored
Submodule external/ring-client-api updated: 7ef505251f...d571cdfc00
2
external/werift
vendored
2
external/werift
vendored
Submodule external/werift updated: 3d58683cdf...140faa891d
8
packages/cli/.vscode/launch.json
vendored
8
packages/cli/.vscode/launch.json
vendored
@@ -6,7 +6,7 @@
|
||||
"configurations": [
|
||||
{
|
||||
"console": "integratedTerminal",
|
||||
"type": "pwa-node",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Launch Program",
|
||||
"skipFiles": [
|
||||
@@ -20,8 +20,10 @@
|
||||
"ts-node/register"
|
||||
],
|
||||
"args": [
|
||||
"install",
|
||||
"@scrypted/google-device-access"
|
||||
"ffplay",
|
||||
"Kitchen",
|
||||
"getRecordingStream",
|
||||
"{\"startTime\":1677699495709}"
|
||||
],
|
||||
"sourceMaps": true,
|
||||
"resolveSourceMapLocations": [
|
||||
|
||||
717
packages/cli/package-lock.json
generated
717
packages/cli/package-lock.json
generated
@@ -1,119 +1,183 @@
|
||||
{
|
||||
"name": "scrypted",
|
||||
"version": "1.0.57",
|
||||
"lockfileVersion": 2,
|
||||
"version": "1.0.67",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "scrypted",
|
||||
"version": "1.0.57",
|
||||
"version": "1.0.67",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/types": "^0.0.6",
|
||||
"adm-zip": "^0.5.9",
|
||||
"@scrypted/client": "^1.1.43",
|
||||
"@scrypted/types": "^0.2.66",
|
||||
"adm-zip": "^0.5.10",
|
||||
"axios": "^0.21.4",
|
||||
"engine.io-client": "^5.2.0",
|
||||
"ip": "^1.1.8",
|
||||
"linkfs": "^2.1.0",
|
||||
"memfs": "^3.4.1",
|
||||
"mkdirp": "^1.0.4",
|
||||
"readline-sync": "^1.4.10",
|
||||
"rimraf": "^3.0.2",
|
||||
"semver": "^7.3.5",
|
||||
"tslib": "^2.3.1"
|
||||
"semver": "^7.3.8",
|
||||
"tslib": "^2.5.0"
|
||||
},
|
||||
"bin": {
|
||||
"scrypted": "dist/packages/cli/src/main.js"
|
||||
"scrypted": "dist/main.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mkdirp": "^1.0.2",
|
||||
"@types/node": "^18.14.2",
|
||||
"@types/readline-sync": "^1.4.4",
|
||||
"@types/rimraf": "^3.0.2",
|
||||
"@types/semver": "^7.3.9",
|
||||
"ts-node": "^10.2.1",
|
||||
"typescript": "^4.8.2"
|
||||
}
|
||||
},
|
||||
"../../common": {
|
||||
"name": "@scrypted/common",
|
||||
"version": "1.0.1",
|
||||
"extraneous": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/sdk": "file:../sdk",
|
||||
"@scrypted/server": "file:../server",
|
||||
"http-auth-utils": "^3.0.2",
|
||||
"node-fetch-commonjs": "^3.1.1",
|
||||
"typescript": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-consumer": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz",
|
||||
"integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
"@types/semver": "^7.3.13",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.9.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz",
|
||||
"integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==",
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@cspotcode/source-map-consumer": "0.8.0"
|
||||
"@jridgewell/trace-mapping": "0.3.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
|
||||
"integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.4.14",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
|
||||
"integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
|
||||
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.0.3",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@scrypted/client": {
|
||||
"version": "1.1.43",
|
||||
"resolved": "https://registry.npmjs.org/@scrypted/client/-/client-1.1.43.tgz",
|
||||
"integrity": "sha512-qpeGdqFga/Fx51MoF3E0iBPCjE/SDEIVdGh8Ws5dqw38bxUJD264c9NsNyCguLKyYguErKTAWnQkzqhO0bUbaA==",
|
||||
"dependencies": {
|
||||
"@scrypted/types": "^0.2.66",
|
||||
"axios": "^0.25.0",
|
||||
"engine.io-client": "^6.4.0",
|
||||
"rimraf": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@scrypted/client/node_modules/axios": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.25.0.tgz",
|
||||
"integrity": "sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.14.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@scrypted/client/node_modules/engine.io-client": {
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.4.0.tgz",
|
||||
"integrity": "sha512-GyKPDyoEha+XZ7iEqam49vz6auPnNJ9ZBfy89f+rMMas8AuiMWOZ9PVzu8xb9ZC6rafUqiGHSCfu22ih66E+1g==",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.1",
|
||||
"engine.io-parser": "~5.0.3",
|
||||
"ws": "~8.11.0",
|
||||
"xmlhttprequest-ssl": "~2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@scrypted/client/node_modules/engine.io-parser": {
|
||||
"version": "5.0.6",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.6.tgz",
|
||||
"integrity": "sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@scrypted/client/node_modules/ws": {
|
||||
"version": "8.11.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
|
||||
"integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": "^5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@scrypted/types": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.0.6.tgz",
|
||||
"integrity": "sha512-r/attybPcJvBNll3g+k8i2jQwQiu0izoBazZ+Kvsdeayr3Mbzm1NaBkwbUPICroWJKY+jlfoaZSQt4eGTX+vog=="
|
||||
"version": "0.2.66",
|
||||
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.2.66.tgz",
|
||||
"integrity": "sha512-AL2iD7OmpqZlQMlpZKUBHpzL7H1IHhwKOi9uhRbVwG7EIDwenTspqtziH2Hyu0+XeCLf+gN69uQB6Qlz+QPf9A=="
|
||||
},
|
||||
"node_modules/@socket.io/component-emitter": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
|
||||
"integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg=="
|
||||
},
|
||||
"node_modules/@tsconfig/node10": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz",
|
||||
"integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==",
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
|
||||
"integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@tsconfig/node12": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz",
|
||||
"integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==",
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
|
||||
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@tsconfig/node14": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz",
|
||||
"integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
|
||||
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@tsconfig/node16": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz",
|
||||
"integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz",
|
||||
"integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/glob": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz",
|
||||
"integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==",
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-8.1.0.tgz",
|
||||
"integrity": "sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/minimatch": "*",
|
||||
"@types/minimatch": "^5.1.2",
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/minimatch": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz",
|
||||
"integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==",
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz",
|
||||
"integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/mkdirp": {
|
||||
@@ -126,9 +190,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "16.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.1.tgz",
|
||||
"integrity": "sha512-PYGcJHL9mwl1Ek3PLiYgyEKtwTMmkMw4vbiyz/ps3pfdRYLVv+SN7qHVAImrjdAXxgluDEw6Ph4lyv+m9UpRmA==",
|
||||
"version": "18.14.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.14.2.tgz",
|
||||
"integrity": "sha512-1uEQxww3DaghA0RxqHx0O0ppVlo43pJhepY51OxuQIKHpjbnYLA7vcdwioNPzIqmC2u3I/dmylcqjlh0e7AyUA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/readline-sync": {
|
||||
@@ -148,15 +212,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/semver": {
|
||||
"version": "7.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.9.tgz",
|
||||
"integrity": "sha512-L/TMpyURfBkf+o/526Zb6kd/tchUP3iBDEPjqjb+U2MAJhVRxxrmr2fwpe08E7QsV7YLcpq0tUaQ9O9x97ZIxQ==",
|
||||
"version": "7.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz",
|
||||
"integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.5.0.tgz",
|
||||
"integrity": "sha512-yXbYeFy+jUuYd3/CDcg2NkIYE991XYX/bje7LmjJigUciaeO1JR4XxXgCIV1/Zc/dRuFEyw1L0pbA+qynJkW5Q==",
|
||||
"version": "8.8.2",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz",
|
||||
"integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
@@ -175,9 +239,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/adm-zip": {
|
||||
"version": "0.5.9",
|
||||
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.9.tgz",
|
||||
"integrity": "sha512-s+3fXLkeeLjZ2kLjCBwQufpI5fuN+kIGBxu6530nVQZGVol0d7Y/M88/xw9HGGUcJjKf8LutN3VPRUBq6N7Ajg==",
|
||||
"version": "0.5.10",
|
||||
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.10.tgz",
|
||||
"integrity": "sha512-x0HvcHqVJNTPk/Bw8JbLWlWoo6Wwnsug0fnYYro1HBrjxZ3G7/AZk7Ahv8JwDe1uIcz8eBqvu86FuF1POiG7vQ==",
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
}
|
||||
@@ -204,7 +268,7 @@
|
||||
"node_modules/base64-arraybuffer": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz",
|
||||
"integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=",
|
||||
"integrity": "sha512-a1eIFi4R9ySrbiMuyTGx5e92uRH5tQY6kArNcFaKBUleIoLjdjBg7Zxm3Mqm3Kmkf27HLR/1fnxX9q8GQ7Iavg==",
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
@@ -226,7 +290,7 @@
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
|
||||
},
|
||||
"node_modules/create-require": {
|
||||
"version": "1.1.1",
|
||||
@@ -235,9 +299,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.3.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
|
||||
"integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
|
||||
"dependencies": {
|
||||
"ms": "2.1.2"
|
||||
},
|
||||
@@ -288,9 +352,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.14.8",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz",
|
||||
"integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==",
|
||||
"version": "1.15.2",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
|
||||
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -306,25 +370,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fs-monkey": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz",
|
||||
"integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q=="
|
||||
},
|
||||
"node_modules/fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
|
||||
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
|
||||
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
|
||||
"version": "7.2.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||
"dependencies": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.0.4",
|
||||
"minimatch": "^3.1.1",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
},
|
||||
@@ -338,12 +397,12 @@
|
||||
"node_modules/has-cors": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz",
|
||||
"integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk="
|
||||
"integrity": "sha512-g5VNKdkFuUuVCP9gYfDJHjK2nqdQJ7aDLTnycnc2+RvsOQbuLdF5pm7vuE5J76SEBIQjs4kQY/BWq74JUmjbXA=="
|
||||
},
|
||||
"node_modules/inflight": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
|
||||
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
|
||||
"dependencies": {
|
||||
"once": "^1.3.0",
|
||||
"wrappy": "1"
|
||||
@@ -359,11 +418,6 @@
|
||||
"resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz",
|
||||
"integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg=="
|
||||
},
|
||||
"node_modules/linkfs": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/linkfs/-/linkfs-2.1.0.tgz",
|
||||
"integrity": "sha512-kmsGcmpvjStZ0ATjuHycBujtNnXiZR28BTivEu0gAMDTT7GEyodcK6zSRtu6xsrdorrPZEIN380x7BD7xEYkew=="
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
@@ -381,17 +435,6 @@
|
||||
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/memfs": {
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.1.tgz",
|
||||
"integrity": "sha512-1c9VPVvW5P7I85c35zAdEr1TD5+F11IToIHIlrVIcflfnzPkJa0ZoYEoEdYDP8KgPFoSZ/opDrUsAoZWym3mtw==",
|
||||
"dependencies": {
|
||||
"fs-monkey": "1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
@@ -422,7 +465,7 @@
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
@@ -440,7 +483,7 @@
|
||||
"node_modules/path-is-absolute": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
|
||||
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -468,9 +511,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.3.5",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
|
||||
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
|
||||
"version": "7.3.8",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
|
||||
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
|
||||
"dependencies": {
|
||||
"lru-cache": "^6.0.0"
|
||||
},
|
||||
@@ -482,12 +525,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ts-node": {
|
||||
"version": "10.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.3.0.tgz",
|
||||
"integrity": "sha512-RYIy3i8IgpFH45AX4fQHExrT8BxDeKTdC83QFJkNzkvt8uFB6QJ8XMyhynYiKMLxt9a7yuXaDBZNOYS3XjDcYw==",
|
||||
"version": "10.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz",
|
||||
"integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@cspotcode/source-map-support": "0.7.0",
|
||||
"@cspotcode/source-map-support": "^0.8.0",
|
||||
"@tsconfig/node10": "^1.0.7",
|
||||
"@tsconfig/node12": "^1.0.7",
|
||||
"@tsconfig/node14": "^1.0.0",
|
||||
@@ -498,11 +541,13 @@
|
||||
"create-require": "^1.1.0",
|
||||
"diff": "^4.0.1",
|
||||
"make-error": "^1.1.1",
|
||||
"v8-compile-cache-lib": "^3.0.1",
|
||||
"yn": "3.1.1"
|
||||
},
|
||||
"bin": {
|
||||
"ts-node": "dist/bin.js",
|
||||
"ts-node-cwd": "dist/bin-cwd.js",
|
||||
"ts-node-esm": "dist/bin-esm.js",
|
||||
"ts-node-script": "dist/bin-script.js",
|
||||
"ts-node-transpile-only": "dist/bin-transpile.js",
|
||||
"ts-script": "dist/bin-script-deprecated.js"
|
||||
@@ -523,14 +568,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
|
||||
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz",
|
||||
"integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg=="
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "4.8.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.2.tgz",
|
||||
"integrity": "sha512-C0I1UsrrDHo2fYI5oaCGbSejwX4ch+9Y5jTQELvovfmFkK3HHSZJB8MSJcWLmCUBzQBchCrZ9rMRV6GuNrvGtw==",
|
||||
"version": "4.9.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
||||
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
@@ -540,10 +585,16 @@
|
||||
"node": ">=4.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/v8-compile-cache-lib": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "7.4.6",
|
||||
@@ -581,7 +632,7 @@
|
||||
"node_modules/yeast": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",
|
||||
"integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk="
|
||||
"integrity": "sha512-8HFIh676uyGYP6wP13R/j6OJ/1HwJ46snpvzE7aHAN3Ryqh2yX6Xox2B4CUmTwwOIzlG3Bs7ocsP5dZH/R1Qbg=="
|
||||
},
|
||||
"node_modules/yn": {
|
||||
"version": "3.1.1",
|
||||
@@ -592,413 +643,5 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@cspotcode/source-map-consumer": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz",
|
||||
"integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==",
|
||||
"dev": true
|
||||
},
|
||||
"@cspotcode/source-map-support": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz",
|
||||
"integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@cspotcode/source-map-consumer": "0.8.0"
|
||||
}
|
||||
},
|
||||
"@scrypted/types": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.0.6.tgz",
|
||||
"integrity": "sha512-r/attybPcJvBNll3g+k8i2jQwQiu0izoBazZ+Kvsdeayr3Mbzm1NaBkwbUPICroWJKY+jlfoaZSQt4eGTX+vog=="
|
||||
},
|
||||
"@tsconfig/node10": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz",
|
||||
"integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==",
|
||||
"dev": true
|
||||
},
|
||||
"@tsconfig/node12": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz",
|
||||
"integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==",
|
||||
"dev": true
|
||||
},
|
||||
"@tsconfig/node14": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz",
|
||||
"integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==",
|
||||
"dev": true
|
||||
},
|
||||
"@tsconfig/node16": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz",
|
||||
"integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/glob": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz",
|
||||
"integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/minimatch": "*",
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/minimatch": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz",
|
||||
"integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/mkdirp": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/mkdirp/-/mkdirp-1.0.2.tgz",
|
||||
"integrity": "sha512-o0K1tSO0Dx5X6xlU5F1D6625FawhC3dU3iqr25lluNv/+/QIVH8RLNEiVokgIZo+mz+87w/3Mkg/VvQS+J51fQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "16.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.1.tgz",
|
||||
"integrity": "sha512-PYGcJHL9mwl1Ek3PLiYgyEKtwTMmkMw4vbiyz/ps3pfdRYLVv+SN7qHVAImrjdAXxgluDEw6Ph4lyv+m9UpRmA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/readline-sync": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/readline-sync/-/readline-sync-1.4.4.tgz",
|
||||
"integrity": "sha512-cFjVIoiamX7U6zkO2VPvXyTxbFDdiRo902IarJuPVxBhpDnXhwSaVE86ip+SCuyWBbEioKCkT4C88RNTxBM1Dw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/rimraf": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/rimraf/-/rimraf-3.0.2.tgz",
|
||||
"integrity": "sha512-F3OznnSLAUxFrCEu/L5PY8+ny8DtcFRjx7fZZ9bycvXRi3KPTRS9HOitGZwvPg0juRhXFWIeKX58cnX5YqLohQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/glob": "*",
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/semver": {
|
||||
"version": "7.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.9.tgz",
|
||||
"integrity": "sha512-L/TMpyURfBkf+o/526Zb6kd/tchUP3iBDEPjqjb+U2MAJhVRxxrmr2fwpe08E7QsV7YLcpq0tUaQ9O9x97ZIxQ==",
|
||||
"dev": true
|
||||
},
|
||||
"acorn": {
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.5.0.tgz",
|
||||
"integrity": "sha512-yXbYeFy+jUuYd3/CDcg2NkIYE991XYX/bje7LmjJigUciaeO1JR4XxXgCIV1/Zc/dRuFEyw1L0pbA+qynJkW5Q==",
|
||||
"dev": true
|
||||
},
|
||||
"acorn-walk": {
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz",
|
||||
"integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==",
|
||||
"dev": true
|
||||
},
|
||||
"adm-zip": {
|
||||
"version": "0.5.9",
|
||||
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.9.tgz",
|
||||
"integrity": "sha512-s+3fXLkeeLjZ2kLjCBwQufpI5fuN+kIGBxu6530nVQZGVol0d7Y/M88/xw9HGGUcJjKf8LutN3VPRUBq6N7Ajg=="
|
||||
},
|
||||
"arg": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
|
||||
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
|
||||
"dev": true
|
||||
},
|
||||
"axios": {
|
||||
"version": "0.21.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
|
||||
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
|
||||
"requires": {
|
||||
"follow-redirects": "^1.14.0"
|
||||
}
|
||||
},
|
||||
"balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
|
||||
},
|
||||
"base64-arraybuffer": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz",
|
||||
"integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI="
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"component-emitter": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
|
||||
"integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg=="
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
|
||||
},
|
||||
"create-require": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
|
||||
"dev": true
|
||||
},
|
||||
"debug": {
|
||||
"version": "4.3.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
|
||||
"integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
|
||||
"requires": {
|
||||
"ms": "2.1.2"
|
||||
}
|
||||
},
|
||||
"diff": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
|
||||
"dev": true
|
||||
},
|
||||
"engine.io-client": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-5.2.0.tgz",
|
||||
"integrity": "sha512-BcIBXGBkT7wKecwnfrSV79G2X5lSUSgeAGgoo60plXf8UsQEvCQww/KMwXSMhVjb98fFYNq20CC5eo8IOAPqsg==",
|
||||
"requires": {
|
||||
"base64-arraybuffer": "0.1.4",
|
||||
"component-emitter": "~1.3.0",
|
||||
"debug": "~4.3.1",
|
||||
"engine.io-parser": "~4.0.1",
|
||||
"has-cors": "1.1.0",
|
||||
"parseqs": "0.0.6",
|
||||
"parseuri": "0.0.6",
|
||||
"ws": "~7.4.2",
|
||||
"xmlhttprequest-ssl": "~2.0.0",
|
||||
"yeast": "0.1.2"
|
||||
}
|
||||
},
|
||||
"engine.io-parser": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-4.0.3.tgz",
|
||||
"integrity": "sha512-xEAAY0msNnESNPc00e19y5heTPX4y/TJ36gr8t1voOaNmTojP9b3oK3BbJLFufW2XFPQaaijpFewm2g2Um3uqA==",
|
||||
"requires": {
|
||||
"base64-arraybuffer": "0.1.4"
|
||||
}
|
||||
},
|
||||
"follow-redirects": {
|
||||
"version": "1.14.8",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz",
|
||||
"integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA=="
|
||||
},
|
||||
"fs-monkey": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz",
|
||||
"integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q=="
|
||||
},
|
||||
"fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
|
||||
},
|
||||
"glob": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
|
||||
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
|
||||
"requires": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.0.4",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"has-cors": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz",
|
||||
"integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk="
|
||||
},
|
||||
"inflight": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
|
||||
"requires": {
|
||||
"once": "^1.3.0",
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"ip": {
|
||||
"version": "1.1.8",
|
||||
"resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz",
|
||||
"integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg=="
|
||||
},
|
||||
"linkfs": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/linkfs/-/linkfs-2.1.0.tgz",
|
||||
"integrity": "sha512-kmsGcmpvjStZ0ATjuHycBujtNnXiZR28BTivEu0gAMDTT7GEyodcK6zSRtu6xsrdorrPZEIN380x7BD7xEYkew=="
|
||||
},
|
||||
"lru-cache": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||
"requires": {
|
||||
"yallist": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"make-error": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
||||
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
||||
"dev": true
|
||||
},
|
||||
"memfs": {
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.1.tgz",
|
||||
"integrity": "sha512-1c9VPVvW5P7I85c35zAdEr1TD5+F11IToIHIlrVIcflfnzPkJa0ZoYEoEdYDP8KgPFoSZ/opDrUsAoZWym3mtw==",
|
||||
"requires": {
|
||||
"fs-monkey": "1.0.3"
|
||||
}
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
},
|
||||
"mkdirp": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
|
||||
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
|
||||
"requires": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"parseqs": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz",
|
||||
"integrity": "sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w=="
|
||||
},
|
||||
"parseuri": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.6.tgz",
|
||||
"integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow=="
|
||||
},
|
||||
"path-is-absolute": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
|
||||
},
|
||||
"readline-sync": {
|
||||
"version": "1.4.10",
|
||||
"resolved": "https://registry.npmjs.org/readline-sync/-/readline-sync-1.4.10.tgz",
|
||||
"integrity": "sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw=="
|
||||
},
|
||||
"rimraf": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
|
||||
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
|
||||
"requires": {
|
||||
"glob": "^7.1.3"
|
||||
}
|
||||
},
|
||||
"semver": {
|
||||
"version": "7.3.5",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
|
||||
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
|
||||
"requires": {
|
||||
"lru-cache": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"ts-node": {
|
||||
"version": "10.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.3.0.tgz",
|
||||
"integrity": "sha512-RYIy3i8IgpFH45AX4fQHExrT8BxDeKTdC83QFJkNzkvt8uFB6QJ8XMyhynYiKMLxt9a7yuXaDBZNOYS3XjDcYw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@cspotcode/source-map-support": "0.7.0",
|
||||
"@tsconfig/node10": "^1.0.7",
|
||||
"@tsconfig/node12": "^1.0.7",
|
||||
"@tsconfig/node14": "^1.0.0",
|
||||
"@tsconfig/node16": "^1.0.2",
|
||||
"acorn": "^8.4.1",
|
||||
"acorn-walk": "^8.1.1",
|
||||
"arg": "^4.1.0",
|
||||
"create-require": "^1.1.0",
|
||||
"diff": "^4.0.1",
|
||||
"make-error": "^1.1.1",
|
||||
"yn": "3.1.1"
|
||||
}
|
||||
},
|
||||
"tslib": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
|
||||
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
|
||||
},
|
||||
"typescript": {
|
||||
"version": "4.8.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.2.tgz",
|
||||
"integrity": "sha512-C0I1UsrrDHo2fYI5oaCGbSejwX4ch+9Y5jTQELvovfmFkK3HHSZJB8MSJcWLmCUBzQBchCrZ9rMRV6GuNrvGtw==",
|
||||
"dev": true
|
||||
},
|
||||
"wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
|
||||
},
|
||||
"ws": {
|
||||
"version": "7.4.6",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
|
||||
"integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==",
|
||||
"requires": {}
|
||||
},
|
||||
"xmlhttprequest-ssl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz",
|
||||
"integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A=="
|
||||
},
|
||||
"yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||
},
|
||||
"yeast": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",
|
||||
"integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk="
|
||||
},
|
||||
"yn": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
||||
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"name": "scrypted",
|
||||
"version": "1.0.57",
|
||||
"version": "1.0.67",
|
||||
"description": "",
|
||||
"main": "./dist/packages/cli/src/main.js",
|
||||
"main": "./dist/main.js",
|
||||
"bin": {
|
||||
"scrypted": "./dist/packages/cli/src/main.js"
|
||||
"scrypted": "./dist/main.js"
|
||||
},
|
||||
"scripts": {
|
||||
"prebuild": "rimraf dist",
|
||||
@@ -16,25 +16,25 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/types": "^0.0.6",
|
||||
"adm-zip": "^0.5.9",
|
||||
"@scrypted/client": "^1.1.43",
|
||||
"@scrypted/types": "^0.2.66",
|
||||
"adm-zip": "^0.5.10",
|
||||
"axios": "^0.21.4",
|
||||
"engine.io-client": "^5.2.0",
|
||||
"ip": "^1.1.8",
|
||||
"linkfs": "^2.1.0",
|
||||
"memfs": "^3.4.1",
|
||||
"mkdirp": "^1.0.4",
|
||||
"readline-sync": "^1.4.10",
|
||||
"rimraf": "^3.0.2",
|
||||
"semver": "^7.3.5",
|
||||
"tslib": "^2.3.1"
|
||||
"semver": "^7.3.8",
|
||||
"tslib": "^2.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mkdirp": "^1.0.2",
|
||||
"@types/node": "^18.14.2",
|
||||
"@types/readline-sync": "^1.4.4",
|
||||
"@types/rimraf": "^3.0.2",
|
||||
"@types/semver": "^7.3.9",
|
||||
"ts-node": "^10.2.1",
|
||||
"typescript": "^4.8.2"
|
||||
"@types/semver": "^7.3.13",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.9.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,11 +7,15 @@ import readline from 'readline-sync';
|
||||
import https from 'https';
|
||||
import mkdirp from 'mkdirp';
|
||||
import { installServe, serveMain } from './service';
|
||||
import { connectScryptedClient } from '../../client/src/index';
|
||||
import { ScryptedMimeTypes, FFMpegInput } from '@scrypted/types';
|
||||
import { connectScryptedClient } from '@scrypted/client';
|
||||
import { ScryptedMimeTypes, FFmpegInput } from '@scrypted/types';
|
||||
import semver from 'semver';
|
||||
import child_process from 'child_process';
|
||||
|
||||
const httpsAgent = new https.Agent({
|
||||
rejectUnauthorized: false,
|
||||
});
|
||||
|
||||
if (!semver.gte(process.version, '16.0.0')) {
|
||||
throw new Error('"node" version out of date. Please update node to v16 or higher.')
|
||||
}
|
||||
@@ -57,6 +61,7 @@ async function doLogin(host: string) {
|
||||
password,
|
||||
},
|
||||
url,
|
||||
httpsAgent,
|
||||
}, axiosConfig));
|
||||
|
||||
mkdirp.sync(scryptedHome);
|
||||
@@ -112,13 +117,16 @@ async function runCommand() {
|
||||
pluginId: '@scrypted/core',
|
||||
username: login.username,
|
||||
password: login.token,
|
||||
axiosConfig: {
|
||||
httpsAgent,
|
||||
}
|
||||
});
|
||||
|
||||
const device: any = sdk.systemManager.getDeviceById(idOrName) || sdk.systemManager.getDeviceByName(idOrName);
|
||||
if (!device)
|
||||
throw new Error('device not found: ' + idOrName);
|
||||
const method = process.argv[4];
|
||||
const args = process.argv.slice(5).map(arg => () => {
|
||||
const args = process.argv.slice(5).map(arg => {
|
||||
try {
|
||||
return JSON.parse(arg);
|
||||
}
|
||||
@@ -157,9 +165,15 @@ async function main() {
|
||||
}
|
||||
else if (process.argv[2] === 'ffplay') {
|
||||
const { sdk, pendingResult } = await runCommand();
|
||||
const ffinput = await sdk.mediaManager.convertMediaObjectToJSON<FFMpegInput>(await pendingResult, ScryptedMimeTypes.FFmpegInput);
|
||||
console.log(ffinput);
|
||||
child_process.spawn('ffplay', ffinput.inputArguments, {
|
||||
const ffmpegInput = await sdk.mediaManager.convertMediaObjectToJSON<FFmpegInput>(await pendingResult, ScryptedMimeTypes.FFmpegInput);
|
||||
if (ffmpegInput.url && ffmpegInput.urls?.[0]) {
|
||||
const url = new URL(ffmpegInput.url);
|
||||
if (url.hostname === '127.0.0.1' && ffmpegInput.urls?.[0]) {
|
||||
ffmpegInput.inputArguments = ffmpegInput.inputArguments.map(i => i === ffmpegInput.url ? ffmpegInput.urls?.[0] : i);
|
||||
}
|
||||
}
|
||||
console.log('ffplay', ...ffmpegInput.inputArguments);
|
||||
child_process.spawn('ffplay', ffmpegInput.inputArguments, {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
sdk.disconnect();
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"resolveJsonModule": true,
|
||||
"module": "commonjs",
|
||||
"target": "ESNext",
|
||||
"noImplicitAny": true,
|
||||
"outDir": "./dist",
|
||||
"esModuleInterop": true,
|
||||
"sourceMap": true,
|
||||
"declaration": true
|
||||
"module": "commonjs",
|
||||
"target": "esnext",
|
||||
"noImplicitAny": true,
|
||||
"outDir": "./dist",
|
||||
"esModuleInterop": true,
|
||||
"sourceMap": true,
|
||||
"inlineSources": true,
|
||||
"declaration": true,
|
||||
"resolveJsonModule": true,
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
"src/**/*"
|
||||
],
|
||||
}
|
||||
285
packages/client/package-lock.json
generated
285
packages/client/package-lock.json
generated
@@ -1,58 +1,29 @@
|
||||
{
|
||||
"name": "@scrypted/client",
|
||||
"version": "1.1.38",
|
||||
"lockfileVersion": 2,
|
||||
"version": "1.1.43",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/client",
|
||||
"version": "1.1.38",
|
||||
"version": "1.1.43",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/types": "^0.2.64",
|
||||
"@scrypted/types": "^0.2.76",
|
||||
"axios": "^0.25.0",
|
||||
"engine.io-client": "^6.2.2",
|
||||
"engine.io-client": "^6.4.0",
|
||||
"rimraf": "^3.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/ip": "^1.1.0",
|
||||
"@types/node": "^17.0.17",
|
||||
"typescript": "^4.7.4"
|
||||
"@types/node": "^18.14.2",
|
||||
"typescript": "^4.9.5"
|
||||
}
|
||||
},
|
||||
"../../common": {
|
||||
"name": "@scrypted/common",
|
||||
"version": "1.0.1",
|
||||
"extraneous": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/sdk": "file:../sdk",
|
||||
"@scrypted/server": "file:../server",
|
||||
"http-auth-utils": "^3.0.2",
|
||||
"node-fetch-commonjs": "^3.1.1",
|
||||
"typescript": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.9.0"
|
||||
}
|
||||
},
|
||||
"../../sdk/types": {
|
||||
"name": "@scrypted/types",
|
||||
"version": "0.0.9",
|
||||
"extraneous": true,
|
||||
"license": "ISC",
|
||||
"devDependencies": {}
|
||||
},
|
||||
"../common": {
|
||||
"extraneous": true
|
||||
},
|
||||
"../sdk/types": {
|
||||
"extraneous": true
|
||||
},
|
||||
"node_modules/@scrypted/types": {
|
||||
"version": "0.2.64",
|
||||
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.2.64.tgz",
|
||||
"integrity": "sha512-8x+EVlsJ5MGJ5HxPcVxV5p5RakP9zivqhTkzgEUUbfGDUXUmv1BYlNy/AESkSNKR26idEiZrKD1VfE67hPIH8A=="
|
||||
"version": "0.2.76",
|
||||
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.2.76.tgz",
|
||||
"integrity": "sha512-/7n8ICkXj8TGba4cHvckLCgSNsOmOGQ8I+Jd8fX9sxkthgsZhF5At8PHhHdkCDS+yfSmfXHkcqluZZOfYPkpAg=="
|
||||
},
|
||||
"node_modules/@socket.io/component-emitter": {
|
||||
"version": "3.1.0",
|
||||
@@ -69,9 +40,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "17.0.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.18.tgz",
|
||||
"integrity": "sha512-eKj4f/BsN/qcculZiRSujogjvp5O/k4lOW5m35NopjZM/QwLOR075a8pJW5hD+Rtdm2DaCVPENS6KtSQnUD6BA==",
|
||||
"version": "18.14.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.14.2.tgz",
|
||||
"integrity": "sha512-1uEQxww3DaghA0RxqHx0O0ppVlo43pJhepY51OxuQIKHpjbnYLA7vcdwioNPzIqmC2u3I/dmylcqjlh0e7AyUA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/axios": {
|
||||
@@ -99,12 +70,12 @@
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.3.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
|
||||
"integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
|
||||
"dependencies": {
|
||||
"ms": "2.1.2"
|
||||
},
|
||||
@@ -118,29 +89,29 @@
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-client": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.2.2.tgz",
|
||||
"integrity": "sha512-8ZQmx0LQGRTYkHuogVZuGSpDqYZtCM/nv8zQ68VZ+JkOpazJ7ICdsSpaO6iXwvaU30oFg5QJOJWj8zWqhbKjkQ==",
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.4.0.tgz",
|
||||
"integrity": "sha512-GyKPDyoEha+XZ7iEqam49vz6auPnNJ9ZBfy89f+rMMas8AuiMWOZ9PVzu8xb9ZC6rafUqiGHSCfu22ih66E+1g==",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.1",
|
||||
"engine.io-parser": "~5.0.3",
|
||||
"ws": "~8.2.3",
|
||||
"ws": "~8.11.0",
|
||||
"xmlhttprequest-ssl": "~2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-parser": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.4.tgz",
|
||||
"integrity": "sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg==",
|
||||
"version": "5.0.6",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.6.tgz",
|
||||
"integrity": "sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.14.8",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz",
|
||||
"integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==",
|
||||
"version": "1.15.2",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
|
||||
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -159,17 +130,17 @@
|
||||
"node_modules/fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
|
||||
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
|
||||
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
|
||||
"version": "7.2.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||
"dependencies": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.0.4",
|
||||
"minimatch": "^3.1.1",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
},
|
||||
@@ -183,7 +154,7 @@
|
||||
"node_modules/inflight": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
|
||||
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
|
||||
"dependencies": {
|
||||
"once": "^1.3.0",
|
||||
"wrappy": "1"
|
||||
@@ -213,7 +184,7 @@
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
@@ -221,7 +192,7 @@
|
||||
"node_modules/path-is-absolute": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
|
||||
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -241,9 +212,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "4.7.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
|
||||
"integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==",
|
||||
"version": "4.9.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
||||
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
@@ -256,12 +227,12 @@
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.2.3",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz",
|
||||
"integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==",
|
||||
"version": "8.11.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
|
||||
"integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
@@ -286,177 +257,5 @@
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@scrypted/types": {
|
||||
"version": "0.2.64",
|
||||
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.2.64.tgz",
|
||||
"integrity": "sha512-8x+EVlsJ5MGJ5HxPcVxV5p5RakP9zivqhTkzgEUUbfGDUXUmv1BYlNy/AESkSNKR26idEiZrKD1VfE67hPIH8A=="
|
||||
},
|
||||
"@socket.io/component-emitter": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
|
||||
"integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg=="
|
||||
},
|
||||
"@types/ip": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/ip/-/ip-1.1.0.tgz",
|
||||
"integrity": "sha512-dwNe8gOoF70VdL6WJBwVHtQmAX4RMd62M+mAB9HQFjG1/qiCLM/meRy95Pd14FYBbEDwCq7jgJs89cHpLBu4HQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "17.0.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.18.tgz",
|
||||
"integrity": "sha512-eKj4f/BsN/qcculZiRSujogjvp5O/k4lOW5m35NopjZM/QwLOR075a8pJW5hD+Rtdm2DaCVPENS6KtSQnUD6BA==",
|
||||
"dev": true
|
||||
},
|
||||
"axios": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.25.0.tgz",
|
||||
"integrity": "sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==",
|
||||
"requires": {
|
||||
"follow-redirects": "^1.14.7"
|
||||
}
|
||||
},
|
||||
"balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
|
||||
},
|
||||
"debug": {
|
||||
"version": "4.3.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
|
||||
"integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
|
||||
"requires": {
|
||||
"ms": "2.1.2"
|
||||
}
|
||||
},
|
||||
"engine.io-client": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.2.2.tgz",
|
||||
"integrity": "sha512-8ZQmx0LQGRTYkHuogVZuGSpDqYZtCM/nv8zQ68VZ+JkOpazJ7ICdsSpaO6iXwvaU30oFg5QJOJWj8zWqhbKjkQ==",
|
||||
"requires": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.1",
|
||||
"engine.io-parser": "~5.0.3",
|
||||
"ws": "~8.2.3",
|
||||
"xmlhttprequest-ssl": "~2.0.0"
|
||||
}
|
||||
},
|
||||
"engine.io-parser": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.4.tgz",
|
||||
"integrity": "sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg=="
|
||||
},
|
||||
"follow-redirects": {
|
||||
"version": "1.14.8",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz",
|
||||
"integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA=="
|
||||
},
|
||||
"fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
|
||||
},
|
||||
"glob": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
|
||||
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
|
||||
"requires": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.0.4",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"inflight": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
|
||||
"requires": {
|
||||
"once": "^1.3.0",
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
|
||||
"requires": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"path-is-absolute": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
|
||||
},
|
||||
"rimraf": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
|
||||
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
|
||||
"requires": {
|
||||
"glob": "^7.1.3"
|
||||
}
|
||||
},
|
||||
"typescript": {
|
||||
"version": "4.7.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
|
||||
"integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==",
|
||||
"dev": true
|
||||
},
|
||||
"wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
|
||||
},
|
||||
"ws": {
|
||||
"version": "8.2.3",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz",
|
||||
"integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==",
|
||||
"requires": {}
|
||||
},
|
||||
"xmlhttprequest-ssl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz",
|
||||
"integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/client",
|
||||
"version": "1.1.38",
|
||||
"version": "1.1.43",
|
||||
"description": "",
|
||||
"main": "dist/packages/client/src/index.js",
|
||||
"scripts": {
|
||||
@@ -13,13 +13,13 @@
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/ip": "^1.1.0",
|
||||
"@types/node": "^17.0.17",
|
||||
"typescript": "^4.7.4"
|
||||
"@types/node": "^18.14.2",
|
||||
"typescript": "^4.9.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@scrypted/types": "^0.2.64",
|
||||
"@scrypted/types": "^0.2.76",
|
||||
"axios": "^0.25.0",
|
||||
"engine.io-client": "^6.2.2",
|
||||
"engine.io-client": "^6.4.0",
|
||||
"rimraf": "^3.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { RTCConnectionManagement, RTCSignalingSession, ScryptedStatic } from "@scrypted/types";
|
||||
import { MediaObjectOptions, RTCConnectionManagement, RTCSignalingSession, ScryptedStatic } from "@scrypted/types";
|
||||
import axios, { AxiosRequestConfig } from 'axios';
|
||||
import * as eio from 'engine.io-client';
|
||||
import { SocketOptions } from 'engine.io-client';
|
||||
@@ -49,6 +49,7 @@ export interface ScryptedClientStatic extends ScryptedStatic {
|
||||
connectionType: ScryptedClientConnectionType;
|
||||
authorization?: string;
|
||||
queryToken?: { [parameter: string]: string };
|
||||
rpcPeer: RpcPeer,
|
||||
}
|
||||
|
||||
export interface ScryptedConnectionOptions {
|
||||
@@ -503,7 +504,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
} = scrypted;
|
||||
console.log('api attached', Date.now() - start);
|
||||
|
||||
mediaManager.createMediaObject = async (data, mimeType, options) => {
|
||||
mediaManager.createMediaObject = async<T extends MediaObjectOptions>(data: any, mimeType: string, options: T) => {
|
||||
const mo: MediaObjectRemote & {
|
||||
[RpcPeer.PROPERTY_PROXY_PROPERTIES]: any,
|
||||
[RpcPeer.PROPERTY_JSON_DISABLE_SERIALIZATION]: true,
|
||||
@@ -519,7 +520,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
return data;
|
||||
},
|
||||
};
|
||||
return mo;
|
||||
return mo as any;
|
||||
}
|
||||
|
||||
const { browserSignalingSession, connectionManagementId, updateSessionId } = rpcPeer.params;
|
||||
@@ -587,6 +588,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
browserSignalingSession,
|
||||
authorization,
|
||||
queryToken,
|
||||
rpcPeer,
|
||||
}
|
||||
|
||||
socket.on('close', () => {
|
||||
|
||||
2
plugins/alexa/.vscode/settings.json
vendored
2
plugins/alexa/.vscode/settings.json
vendored
@@ -1,4 +1,4 @@
|
||||
|
||||
{
|
||||
"scrypted.debugHost": "127.0.0.1",
|
||||
"scrypted.debugHost": "10.10.0.50",
|
||||
}
|
||||
3111
plugins/alexa/package-lock.json
generated
3111
plugins/alexa/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/alexa",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"scripts": {
|
||||
"scrypted-setup-project": "scrypted-setup-project",
|
||||
"prescrypted-setup-project": "scrypted-package-json",
|
||||
@@ -21,11 +21,12 @@
|
||||
"amazon"
|
||||
],
|
||||
"scrypted": {
|
||||
"name": "Alexa Plugin",
|
||||
"name": "Alexa",
|
||||
"type": "API",
|
||||
"interfaces": [
|
||||
"HttpRequestHandler",
|
||||
"MixinProvider"
|
||||
"MixinProvider",
|
||||
"Settings"
|
||||
],
|
||||
"pluginDependencies": [
|
||||
"@scrypted/cloud",
|
||||
@@ -33,14 +34,11 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node": "^16.6.1",
|
||||
"alexa-smarthome-ts": "^0.0.1",
|
||||
"axios": "^1.3.4",
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"@scrypted/server": "file:../../server"
|
||||
"@types/node": "^18.4.2",
|
||||
"@scrypted/sdk": "^0.2.70"
|
||||
}
|
||||
}
|
||||
|
||||
221
plugins/alexa/src/alexa.ts
Normal file
221
plugins/alexa/src/alexa.ts
Normal file
@@ -0,0 +1,221 @@
|
||||
export declare type DisplayCategory = 'ACTIVITY_TRIGGER' | 'CAMERA' | 'CONTACT_SENSOR' | 'DOOR' | 'DOORBELL' | 'GARAGE_DOOR' | 'LIGHT' | 'MICROWAVE' | 'MOTION_SENSOR' | 'OTHER' | 'SCENE_TRIGGER' | 'SECURITY_PANEL' | 'SMARTLOCK' | 'SMARTPLUG' | 'SPEAKER' | 'SWITCH' | 'TEMPERATURE_SENSOR' | 'THERMOSTAT' | 'TV';
|
||||
|
||||
/*
|
||||
COMMON DIRECTIVES AND RESPONSES
|
||||
*/
|
||||
|
||||
export interface AddOrUpdateReport {
|
||||
event: {
|
||||
header: Header<"Alexa.Discovery", "AddOrUpdateReport">;
|
||||
payload: AddOrUpdateReportPayload;
|
||||
}
|
||||
}
|
||||
|
||||
export interface DeleteReport {
|
||||
event: {
|
||||
header: Header<"Alexa.Discovery", "DeleteReport">;
|
||||
payload: DeleteReportPayload;
|
||||
}
|
||||
}
|
||||
|
||||
export interface StateReport extends Report<"Alexa", "StateReport"> { }
|
||||
|
||||
export interface ChangeReport extends Report<"Alexa", "ChangeReport", ChangePayload> { }
|
||||
|
||||
export interface Response {
|
||||
event: Event<"Alexa", "Response">;
|
||||
context?: Context;
|
||||
}
|
||||
|
||||
export interface DeferredResponse {
|
||||
event: Event<"Alexa", "DeferredResponse", DeferredPayload>;
|
||||
}
|
||||
|
||||
export interface ErrorResponse {
|
||||
event: Event<"Alexa", "ErrorResponse", ErrorPayload>;
|
||||
}
|
||||
|
||||
/*
|
||||
DEVICE EVENTS
|
||||
*/
|
||||
|
||||
export interface WebRTCAnswerGeneratedForSessionEvent extends Report<"Alexa.RTCSessionController", "AnswerGeneratedForSession", WebRTCAnswerGeneratedForSessionPayload> { }
|
||||
|
||||
export interface WebRTCSessionConnectedEvent extends Report<"Alexa.RTCSessionController", "SessionConnected", WebRTCSessionPayload> { }
|
||||
|
||||
export interface WebRTCSessionDisconnectedEvent extends Report<"Alexa.RTCSessionController", "SessionDisconnected", WebRTCSessionPayload> { }
|
||||
|
||||
export interface ObjectDetectionEvent extends Report<"Alexa.SmartVision.ObjectDetectionSensor", "ObjectDetection", ObjectDetectionPayload> { }
|
||||
|
||||
export interface DoorbellPressEvent extends Report<"Alexa.DoorbellEventSource", "DoorbellPress", DoorbellPressPayload> { }
|
||||
|
||||
/*
|
||||
IMPLIMENTATION TYPES
|
||||
*/
|
||||
|
||||
|
||||
export interface Header<NS = string, N = string> {
|
||||
namespace: NS;
|
||||
name: N;
|
||||
messageId: string;
|
||||
correlationToken?: string;
|
||||
payloadVersion: string;
|
||||
}
|
||||
|
||||
export interface Scope {
|
||||
type: string;
|
||||
token: string;
|
||||
partition?: string;
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
export interface Endpoint {
|
||||
endpointId: string;
|
||||
scope?: Scope;
|
||||
cookie?: any;
|
||||
}
|
||||
|
||||
export interface Payload { }
|
||||
|
||||
export interface Directive<NS = string, N = string, P = Payload> {
|
||||
header: Header<NS, N>;
|
||||
endpoint: Endpoint;
|
||||
payload: P;
|
||||
}
|
||||
|
||||
export interface Event<NS = string, N = string, P = Payload> {
|
||||
header: Header<NS, N>;
|
||||
endpoint: Endpoint;
|
||||
payload: P;
|
||||
}
|
||||
|
||||
export interface Property {
|
||||
namespace: string;
|
||||
instance?: string;
|
||||
name: string;
|
||||
value: any;
|
||||
timeOfSample: string;
|
||||
uncertaintyInMilliseconds?: number;
|
||||
}
|
||||
|
||||
export interface Context {
|
||||
properties: Property[];
|
||||
}
|
||||
|
||||
export interface Report<NS = string, N = string, P = Payload> {
|
||||
event: Event<NS, N, P>;
|
||||
context: Context;
|
||||
}
|
||||
|
||||
export interface DeferredPayload {
|
||||
estimatedDeferralInSeconds: number;
|
||||
}
|
||||
|
||||
export interface ErrorPayload {
|
||||
type: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface ChangePayload {
|
||||
change: {
|
||||
cause: {
|
||||
type: "APP_INTERACTION" | "PERIODIC_POLL" | "PHYSICAL_INTERACTION" | "VOICE_INTERACTION" | "RULE_TRIGGER";
|
||||
},
|
||||
properties: Property[];
|
||||
}
|
||||
}
|
||||
|
||||
export interface WebRTCSessionPayload {
|
||||
sessionId: string;
|
||||
}
|
||||
|
||||
export interface WebRTCAnswerGeneratedForSessionPayload {
|
||||
answer: {
|
||||
format: string;
|
||||
value: string;
|
||||
}
|
||||
}
|
||||
|
||||
export interface ObjectDetectionPayloadEvent {
|
||||
eventIdenifier: string;
|
||||
imageNetClass: string;
|
||||
timeOfSample: string;
|
||||
uncertaintyInMilliseconds: number;
|
||||
objectIdentifier: string;
|
||||
frameImageUri: string;
|
||||
croppedImageUri: string;
|
||||
}
|
||||
|
||||
export interface ObjectDetectionPayload {
|
||||
events: ObjectDetectionPayloadEvent[]
|
||||
}
|
||||
|
||||
|
||||
export interface DoorbellPressPayload {
|
||||
cause: {
|
||||
type: "APP_INTERACTION" | "PERIODIC_POLL" | "PHYSICAL_INTERACTION" | "VOICE_INTERACTION";
|
||||
},
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export interface DiscoveryProperty {
|
||||
supported: any[];
|
||||
proactivelyReported: boolean;
|
||||
retrievable: boolean;
|
||||
}
|
||||
|
||||
export interface DiscoveryCapability {
|
||||
type: string;
|
||||
interface: string;
|
||||
instance?: string;
|
||||
version: string;
|
||||
properties?: DiscoveryProperty;
|
||||
capabilityResources?: any;
|
||||
configuration?: any;
|
||||
semantics?: any;
|
||||
}
|
||||
|
||||
export interface DiscoveryEndpoint {
|
||||
endpointId: string;
|
||||
manufacturerName: string;
|
||||
description: string;
|
||||
friendlyName: string;
|
||||
displayCategories: DisplayCategory[];
|
||||
additionalAttributes?: {
|
||||
"manufacturer"?: string;
|
||||
"model"?: string;
|
||||
"serialNumber"?: string;
|
||||
"firmwareVersion"? : string;
|
||||
"softwareVersion"?: string;
|
||||
"customIdentifier"?: string;
|
||||
};
|
||||
capabilities?: DiscoveryCapability[];
|
||||
connections?: any[];
|
||||
relationships?: any;
|
||||
cookie?: any;
|
||||
}
|
||||
|
||||
export interface DiscoverPayload {
|
||||
endpoints: DiscoveryEndpoint[]
|
||||
}
|
||||
|
||||
export interface Discovery {
|
||||
event: {
|
||||
header: Header<"Alexa.Discovery", "Discover.Response">;
|
||||
payload: DiscoverPayload;
|
||||
}
|
||||
}
|
||||
|
||||
export interface AddOrUpdateReportPayload {
|
||||
endpoints: DiscoveryEndpoint[]
|
||||
scope: Scope;
|
||||
}
|
||||
|
||||
export interface DeleteReportEndpoint {
|
||||
endpointId: string;
|
||||
}
|
||||
|
||||
export interface DeleteReportPayload {
|
||||
endpoints: DeleteReportEndpoint[]
|
||||
scope: Scope;
|
||||
}
|
||||
131
plugins/alexa/src/common.ts
Normal file
131
plugins/alexa/src/common.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { Battery, Online, PowerSensor, ScryptedDevice, ScryptedInterface, HttpResponse } from "@scrypted/sdk";
|
||||
import { v4 as createMessageId } from 'uuid';
|
||||
|
||||
export interface AlexaHttpResponse extends HttpResponse {
|
||||
send(body: any, options?: any): void;
|
||||
}
|
||||
|
||||
export function addOnline(data: any, device: ScryptedDevice & Online) : any {
|
||||
if (!device.interfaces.includes(ScryptedInterface.Online))
|
||||
return data;
|
||||
|
||||
if (data.context === undefined)
|
||||
data.context = {};
|
||||
|
||||
if (data.context.properties === undefined)
|
||||
data.context.properties = [];
|
||||
|
||||
data.context.properties.push(
|
||||
{
|
||||
"namespace": "Alexa.EndpointHealth",
|
||||
"name": "connectivity",
|
||||
"value": {
|
||||
"value": device.online ? "OK" : "UNREACHABLE",
|
||||
"reason": device.online ? undefined : "INTERNET_UNREACHABLE"
|
||||
},
|
||||
"timeOfSample": new Date().toISOString(),
|
||||
"uncertaintyInMilliseconds": 0
|
||||
}
|
||||
);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export function addBattery(data: any, device: ScryptedDevice & Battery) : any {
|
||||
if (!device.interfaces.includes(ScryptedInterface.Battery))
|
||||
return data;
|
||||
|
||||
if (data.context === undefined)
|
||||
data.context = {};
|
||||
|
||||
if (data.context.properties === undefined)
|
||||
data.context.properties = [];
|
||||
|
||||
const lowPower = device.batteryLevel < 20;
|
||||
let health = undefined;
|
||||
|
||||
if (lowPower) {
|
||||
health = {
|
||||
"state": "WARNING",
|
||||
"reasons": [
|
||||
"LOW_CHARGE"
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
data.context.properties.push(
|
||||
{
|
||||
"namespace": "Alexa.EndpointHealth",
|
||||
"name": "battery",
|
||||
"value": {
|
||||
health,
|
||||
"levelPercentage": device.batteryLevel,
|
||||
},
|
||||
"timeOfSample": new Date().toISOString(),
|
||||
"uncertaintyInMilliseconds": 0
|
||||
}
|
||||
);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export function authErrorResponse(errorType: string, errorMessage: string, directive: any): any {
|
||||
const { header } = directive;
|
||||
const data = {
|
||||
"event": {
|
||||
header,
|
||||
"payload": {
|
||||
"type": errorType,
|
||||
"message": errorMessage
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
data.event.header.name = "ErrorResponse";
|
||||
data.event.header.messageId = createMessageId();
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// https://developer.amazon.com/en-US/docs/alexa/device-apis/alexa-errorresponse.html#error-types
|
||||
export function deviceErrorResponse (errorType: string, errorMessage: string, directive: any): any{
|
||||
const { header, endpoint } = directive;
|
||||
const data = {
|
||||
"event": {
|
||||
header,
|
||||
endpoint,
|
||||
"payload": {
|
||||
"type": errorType,
|
||||
"message": errorMessage
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
data.event.header.name = "ErrorResponse";
|
||||
data.event.header.messageId = createMessageId();
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export function mirroredResponse (directive: any): any {
|
||||
const { header, endpoint, payload } = directive;
|
||||
const data = {
|
||||
"event": {
|
||||
header,
|
||||
endpoint,
|
||||
payload
|
||||
}
|
||||
};
|
||||
|
||||
data.event.header.name = "Response";
|
||||
data.event.header.messageId = createMessageId();
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export function sendDeviceResponse(data: any, response: any, device: ScryptedDevice) {
|
||||
data = addBattery(data, device);
|
||||
data = addOnline(data, device);
|
||||
|
||||
response.send(data);
|
||||
}
|
||||
34
plugins/alexa/src/handlers.ts
Normal file
34
plugins/alexa/src/handlers.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { HttpRequest, ScryptedDevice } from "@scrypted/sdk";
|
||||
import { AlexaHttpResponse, sendDeviceResponse } from "./common";
|
||||
import { supportedTypes } from "./types";
|
||||
import { v4 as createMessageId } from 'uuid';
|
||||
import { Directive, StateReport } from "./alexa";
|
||||
|
||||
export type AlexaHandler = (request: HttpRequest, response: AlexaHttpResponse, directive: Directive) => Promise<void>
|
||||
export type AlexaDeviceHandler<T> = (request: HttpRequest, response: AlexaHttpResponse, directive: Directive, device: ScryptedDevice & T) => Promise<void>
|
||||
|
||||
export const alexaDeviceHandlers = new Map<string, AlexaDeviceHandler<any>>();
|
||||
export const alexaHandlers = new Map<string, AlexaHandler>();
|
||||
|
||||
alexaDeviceHandlers.set('Alexa/ReportState', async (request, response, directive: any, device: ScryptedDevice) => {
|
||||
const supportedType = supportedTypes.get(device.type);
|
||||
if (!supportedType)
|
||||
return;
|
||||
|
||||
const { header, endpoint, payload } = directive;
|
||||
const report = await supportedType.sendReport(device);
|
||||
|
||||
let data = {
|
||||
"event": {
|
||||
header,
|
||||
endpoint,
|
||||
payload
|
||||
},
|
||||
context: report?.context
|
||||
} as StateReport;
|
||||
|
||||
data.event.header.name = "StateReport";
|
||||
data.event.header.messageId = createMessageId();
|
||||
|
||||
sendDeviceResponse(data, response, device);
|
||||
});
|
||||
@@ -1,18 +1,21 @@
|
||||
import axios from 'axios';
|
||||
import sdk, { HttpRequest, HttpRequestHandler, HttpResponse, MixinProvider, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, Setting, SettingValue, Settings } from '@scrypted/sdk';
|
||||
import sdk, { HttpRequest, HttpRequestHandler, MixinProvider, ScryptedDevice, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, EventDetails, Setting, SettingValue, Settings, HttpResponseOptions, HttpResponse } from '@scrypted/sdk';
|
||||
import { StorageSettings } from '@scrypted/sdk/storage-settings';
|
||||
import { AutoenableMixinProvider } from '@scrypted/common/src/autoenable-mixin-provider';
|
||||
import { isSupported } from './types';
|
||||
import { DiscoveryEndpoint, DiscoverEvent } from 'alexa-smarthome-ts';
|
||||
import { AlexaHandler, addBattery, addOnline, addPowerSensor, capabilityHandlers, supportedTypes } from './types/common';
|
||||
import { createMessageId } from './message';
|
||||
import { addBattery, addOnline, deviceErrorResponse, mirroredResponse, authErrorResponse, AlexaHttpResponse } from './common';
|
||||
import { supportedTypes } from './types';
|
||||
import { v4 as createMessageId } from 'uuid';
|
||||
import { ChangeReport, Discovery, DiscoveryEndpoint } from './alexa';
|
||||
import { alexaHandlers, alexaDeviceHandlers } from './handlers';
|
||||
|
||||
const { systemManager, deviceManager } = sdk;
|
||||
|
||||
const client_id = "amzn1.application-oa2-client.3283807e04d8408eb44a698c10f9dd13";
|
||||
const client_secret = "bed445e2b26730acd818b90e175b275f6b67b18ff8645e571c5b3e311fa75ee9";
|
||||
const includeToken = 4;
|
||||
|
||||
class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler, MixinProvider, Settings {
|
||||
export let DEBUG = false;
|
||||
|
||||
class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, MixinProvider, Settings {
|
||||
storageSettings = new StorageSettings(this, {
|
||||
tokenInfo: {
|
||||
hide: true,
|
||||
@@ -22,6 +25,10 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
|
||||
multiple: true,
|
||||
hide: true
|
||||
},
|
||||
defaultIncluded: {
|
||||
hide: true,
|
||||
json: true
|
||||
},
|
||||
apiEndpoint: {
|
||||
title: 'Alexa Endpoint',
|
||||
description: 'This is the endpoint Alexa will use to send events to. This is set after you login.',
|
||||
@@ -30,88 +37,169 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
|
||||
}
|
||||
});
|
||||
|
||||
handlers = new Map<string, AlexaHandler>();
|
||||
accessToken: Promise<string>;
|
||||
validAuths = new Set<string>();
|
||||
devices = new Map<string, ScryptedDevice>();
|
||||
|
||||
constructor(nativeId?: string) {
|
||||
super(nativeId);
|
||||
|
||||
this.handlers.set('Alexa.Authorization', this.alexaAuthorization);
|
||||
this.handlers.set('Alexa.Discovery', this.alexaDiscovery);
|
||||
alexaHandlers.set('Alexa.Authorization/AcceptGrant', this.onAlexaAuthorization);
|
||||
alexaHandlers.set('Alexa.Discovery/Discover', this.onDiscoverEndpoints);
|
||||
|
||||
this.syncDevices();
|
||||
this.start();
|
||||
}
|
||||
|
||||
systemManager.listen(async (eventSource, eventDetails, eventData) => {
|
||||
if (!eventSource)
|
||||
return;
|
||||
async start() {
|
||||
|
||||
if (!this.storageSettings.values.syncedDevices.includes(eventSource.id))
|
||||
return;
|
||||
for (const id of Object.keys(systemManager.getSystemState())) {
|
||||
const device = systemManager.getDeviceById(id);
|
||||
await this.tryEnableMixin(device);
|
||||
}
|
||||
|
||||
const supportedType = supportedTypes.get(eventSource.type);
|
||||
if (!supportedType) {
|
||||
this.console.warn(`${eventSource.name} no longer supported type?`);
|
||||
return;
|
||||
systemManager.listen((async (eventSource: ScryptedDevice | undefined, eventDetails: EventDetails, eventData: any) => {
|
||||
const status = await this.tryEnableMixin(eventSource);
|
||||
|
||||
// sync new devices when added or removed
|
||||
if (status === DeviceMixinStatus.Setup)
|
||||
await this.syncEndpoints();
|
||||
|
||||
if (status === DeviceMixinStatus.Setup || status === DeviceMixinStatus.AlreadySetup) {
|
||||
|
||||
if (!this.devices.has(eventSource.id)) {
|
||||
this.devices.set(eventSource.id, eventSource);
|
||||
eventSource.listen(ScryptedInterface.ObjectDetector, this.deviceListen.bind(this));
|
||||
}
|
||||
|
||||
this.deviceListen(eventSource, eventDetails, eventData);
|
||||
}
|
||||
}).bind(this));
|
||||
|
||||
const report = await supportedType.sendEvent(eventSource, eventDetails, eventData);
|
||||
let data = {
|
||||
"event": {
|
||||
"header": {
|
||||
"messageId": createMessageId(),
|
||||
"namespace": report?.namespace ?? "Alexa",
|
||||
"name": report?.name ?? "ChangeReport",
|
||||
"payloadVersion": "3"
|
||||
},
|
||||
"endpoint": {
|
||||
"endpointId": eventSource.id,
|
||||
"scope": undefined
|
||||
},
|
||||
"payload": report?.payload,
|
||||
await this.syncEndpoints();
|
||||
}
|
||||
|
||||
private async tryEnableMixin(device: ScryptedDevice): Promise<DeviceMixinStatus> {
|
||||
if (!device)
|
||||
return DeviceMixinStatus.NotSupported;
|
||||
|
||||
const mixins = (device.mixins || []).slice();
|
||||
if (mixins.includes(this.id))
|
||||
return DeviceMixinStatus.AlreadySetup;
|
||||
|
||||
const defaultIncluded = this.storageSettings.values.defaultIncluded || {};
|
||||
if (defaultIncluded[device.id] === includeToken)
|
||||
return DeviceMixinStatus.AlreadySetup;
|
||||
|
||||
if (!supportedTypes.has(device.type))
|
||||
return DeviceMixinStatus.NotSupported;
|
||||
|
||||
mixins.push(this.id);
|
||||
|
||||
const plugins = await systemManager.getComponent('plugins');
|
||||
await plugins.setMixins(device.id, mixins);
|
||||
|
||||
defaultIncluded[device.id] = includeToken;
|
||||
this.storageSettings.values.defaultIncluded = defaultIncluded;
|
||||
|
||||
return DeviceMixinStatus.Setup;
|
||||
}
|
||||
|
||||
async canMixin(type: ScryptedDeviceType, interfaces: string[]): Promise<string[]> {
|
||||
const available = supportedTypes.has(type);
|
||||
|
||||
if (available)
|
||||
return [];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
async getMixin(device: ScryptedDevice, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: { [key: string]: any }): Promise<any> {
|
||||
return device;
|
||||
}
|
||||
|
||||
async releaseMixin(id: string, mixinDevice: any): Promise<void> {
|
||||
const device = systemManager.getDeviceById(id);
|
||||
const mixins = (device.mixins || []).slice();
|
||||
if (mixins.includes(this.id))
|
||||
return;
|
||||
|
||||
this.log.i(`Device removed from Alexa: ${device.name}. Requesting sync.`);
|
||||
await this.syncEndpoints();
|
||||
}
|
||||
|
||||
async deviceListen(eventSource: ScryptedDevice | undefined, eventDetails: EventDetails, eventData: any) : Promise<void> {
|
||||
if (!eventSource)
|
||||
return;
|
||||
|
||||
if (!this.storageSettings.values.syncedDevices.includes(eventSource.id))
|
||||
return;
|
||||
|
||||
if (eventDetails.eventInterface === ScryptedInterface.ScryptedDevice)
|
||||
return;
|
||||
|
||||
const supportedType = supportedTypes.get(eventSource.type);
|
||||
if (!supportedType)
|
||||
return;
|
||||
|
||||
const report = await supportedType.sendEvent(eventSource, eventDetails, eventData);
|
||||
if (!report) {
|
||||
this.console.warn(`${eventDetails.eventInterface}.${eventDetails.property} not supported for device ${eventSource.type}`);
|
||||
return;
|
||||
}
|
||||
|
||||
let data = {
|
||||
"event": {
|
||||
"header": {
|
||||
"messageId": createMessageId(),
|
||||
"namespace": report?.event?.header?.namespace ?? "Alexa",
|
||||
"name": report?.event?.header?.name ?? "ChangeReport",
|
||||
"payloadVersion": "3"
|
||||
},
|
||||
"context": report?.context
|
||||
}
|
||||
"endpoint": {
|
||||
"endpointId": eventSource.id,
|
||||
},
|
||||
payload: report?.event?.payload
|
||||
},
|
||||
context: report?.context
|
||||
} as ChangeReport;
|
||||
|
||||
data = addOnline(data, eventSource);
|
||||
data = addBattery(data, eventSource);
|
||||
data = addPowerSensor(data, eventSource);
|
||||
data = addOnline(data, eventSource);
|
||||
data = addBattery(data, eventSource);
|
||||
|
||||
// nothing to report
|
||||
if (data.context === undefined && data.event.payload === undefined)
|
||||
return;
|
||||
|
||||
const accessToken = await this.getAccessToken();
|
||||
data.event.endpoint.scope = {
|
||||
"type": "BearerToken",
|
||||
"token": accessToken,
|
||||
};
|
||||
// nothing to report
|
||||
if (data.context === undefined && data.event.payload === undefined)
|
||||
return;
|
||||
|
||||
data = await this.addAccessToken(data);
|
||||
|
||||
await this.postEvent(data);
|
||||
});
|
||||
await this.postEvent(data);
|
||||
}
|
||||
|
||||
private async addAccessToken(data: any) : Promise<any> {
|
||||
const accessToken = await this.getAccessToken();
|
||||
|
||||
if (data.event === undefined)
|
||||
data.event = {};
|
||||
|
||||
if (data.event.endpoint === undefined)
|
||||
data.event.endpoint = [];
|
||||
|
||||
data.event.endpoint.scope = {
|
||||
"type": "BearerToken",
|
||||
"token": accessToken,
|
||||
};
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
getSettings(): Promise<Setting[]> {
|
||||
return this.storageSettings.getSettings();
|
||||
}
|
||||
|
||||
putSetting(key: string, value: SettingValue): Promise<void> {
|
||||
return this.storageSettings.putSetting(key, value);
|
||||
}
|
||||
|
||||
async getMixin(mixinDevice: any, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: { [key: string]: any; }): Promise<any> {
|
||||
return mixinDevice;
|
||||
}
|
||||
|
||||
async releaseMixin(id: string, mixinDevice: any): Promise<void> {
|
||||
const device = systemManager.getDeviceById(id);
|
||||
if (device.mixins?.includes(this.id)) {
|
||||
return;
|
||||
}
|
||||
this.console.log('release mixin', id);
|
||||
this.log.a(`${device.name} was removed. The Alexa plugin will reload momentarily.`);
|
||||
deviceManager.requestRestart();
|
||||
}
|
||||
|
||||
readonly endpoints: string[] = [
|
||||
'api.amazonalexa.com',
|
||||
'api.eu.amazonalexa.com',
|
||||
@@ -146,6 +234,8 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
|
||||
const endpoint = await this.getAlexaEndpoint();
|
||||
const self = this;
|
||||
|
||||
this.console.assert(!DEBUG, `event:`, data);
|
||||
|
||||
return axios.post(`https://${endpoint}/v3/events`, data, {
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + accessToken,
|
||||
@@ -160,25 +250,59 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
|
||||
});
|
||||
}
|
||||
|
||||
async syncDevices() {
|
||||
const endpoints = await this.addOrUpdateReport();
|
||||
async getEndpoints() : Promise<DiscoveryEndpoint[]> {
|
||||
const endpoints: DiscoveryEndpoint[] = [];
|
||||
|
||||
for (const id of Object.keys(systemManager.getSystemState())) {
|
||||
const device = systemManager.getDeviceById(id);
|
||||
|
||||
if (!device.mixins?.includes(this.id))
|
||||
continue;
|
||||
|
||||
const endpoint = await this.getEndpointForDevice(device);
|
||||
if (endpoint)
|
||||
endpoints.push(endpoint);
|
||||
}
|
||||
|
||||
return endpoints;
|
||||
}
|
||||
|
||||
async onDiscoverEndpoints(request: HttpRequest, response: AlexaHttpResponse, directive: any) {
|
||||
const endpoints = await this.getEndpoints();
|
||||
|
||||
const data = {
|
||||
"event": {
|
||||
"header": {
|
||||
"namespace": 'Alexa.Discovery',
|
||||
"name": 'Discover.Response',
|
||||
"payloadVersion": '3',
|
||||
"messageId": createMessageId()
|
||||
},
|
||||
"payload": {
|
||||
endpoints
|
||||
}
|
||||
}
|
||||
} as Discovery;
|
||||
|
||||
response.send(data);
|
||||
|
||||
await this.saveEndpoints(endpoints);
|
||||
}
|
||||
|
||||
async addOrUpdateReport() {
|
||||
const endpoints = this.getDiscoveryEndpoints();
|
||||
async syncEndpoints() {
|
||||
const endpoints = await this.getEndpoints();
|
||||
|
||||
if (!endpoints.length)
|
||||
return [];
|
||||
return [];
|
||||
|
||||
const accessToken = await this.getAccessToken();
|
||||
await this.postEvent({
|
||||
const data = {
|
||||
"event": {
|
||||
"header": {
|
||||
"namespace": "Alexa.Discovery",
|
||||
"name": "AddOrUpdateReport",
|
||||
"payloadVersion": "3",
|
||||
"messageId": createMessageId(),
|
||||
"messageId": createMessageId()
|
||||
},
|
||||
"payload": {
|
||||
endpoints,
|
||||
@@ -188,12 +312,35 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return endpoints;
|
||||
await this.postEvent(data);
|
||||
|
||||
await this.saveEndpoints(endpoints);
|
||||
}
|
||||
|
||||
async deleteReport(...ids: string[]) {
|
||||
async saveEndpoints(endpoints: DiscoveryEndpoint[]) {
|
||||
const existingEndpoints: string[] = this.storageSettings.values.syncedDevices;
|
||||
const newEndpoints = endpoints.map(endpoint => endpoint.endpointId);
|
||||
const deleted = new Set(existingEndpoints);
|
||||
|
||||
for (const id of newEndpoints) {
|
||||
deleted.delete(id);
|
||||
}
|
||||
|
||||
const all = new Set([...existingEndpoints, ...newEndpoints]);
|
||||
|
||||
// save all the endpoints
|
||||
this.storageSettings.values.syncedDevices = [...all];
|
||||
|
||||
// delete leftover endpoints
|
||||
await this.deleteEndpoints(...deleted);
|
||||
|
||||
// prune if the delete report completed successfully
|
||||
this.storageSettings.values.syncedDevices = newEndpoints;
|
||||
}
|
||||
|
||||
async deleteEndpoints(...ids: string[]) {
|
||||
if (!ids.length)
|
||||
return;
|
||||
|
||||
@@ -219,17 +366,6 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
|
||||
})
|
||||
}
|
||||
|
||||
async canMixin(type: ScryptedDeviceType, interfaces: string[]): Promise<string[]> {
|
||||
const discovery = isSupported({
|
||||
type,
|
||||
interfaces,
|
||||
} as any);
|
||||
|
||||
if (!discovery)
|
||||
return;
|
||||
return [];
|
||||
}
|
||||
|
||||
getAccessToken(): Promise<string> {
|
||||
if (this.accessToken)
|
||||
return this.accessToken;
|
||||
@@ -306,9 +442,8 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
|
||||
return this.accessToken;
|
||||
}
|
||||
|
||||
async alexaAuthorization(request: HttpRequest, response: HttpResponse) {
|
||||
const json = JSON.parse(request.body);
|
||||
const { grant } = json.directive.payload;
|
||||
async onAlexaAuthorization(request: HttpRequest, response: AlexaHttpResponse, directive: any) {
|
||||
const { grant } = directive.payload;
|
||||
this.storageSettings.values.tokenInfo = grant;
|
||||
this.storageSettings.values.apiEndpoint = undefined;
|
||||
this.accessToken = undefined;
|
||||
@@ -321,27 +456,14 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
|
||||
this.storageSettings.values.apiEndpoint = undefined;
|
||||
this.accessToken = undefined;
|
||||
|
||||
response.send(JSON.stringify({
|
||||
"event": {
|
||||
"header": {
|
||||
"namespace": "Alexa.Authorization",
|
||||
"name": "ErrorResponse",
|
||||
"messageId": createMessageId(),
|
||||
"payloadVersion": "3"
|
||||
},
|
||||
"payload": {
|
||||
"type": "ACCEPT_GRANT_FAILED",
|
||||
"message": `Failed to handle the AcceptGrant directive because ${reason}`
|
||||
}
|
||||
}
|
||||
}));
|
||||
response.send(authErrorResponse("ACCEPT_GRANT_FAILED", `Failed to handle the AcceptGrant directive because ${reason}`, directive));
|
||||
|
||||
return undefined;
|
||||
});
|
||||
|
||||
if (accessToken !== undefined) {
|
||||
try {
|
||||
response.send(JSON.stringify({
|
||||
response.send({
|
||||
"event": {
|
||||
"header": {
|
||||
"namespace": "Alexa.Authorization",
|
||||
@@ -351,7 +473,7 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
|
||||
},
|
||||
"payload": {}
|
||||
}
|
||||
}));
|
||||
});
|
||||
} catch (error) {
|
||||
this.console.error(`AcceptGrant.Response failed because ${error}`);
|
||||
|
||||
@@ -363,14 +485,15 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
|
||||
}
|
||||
}
|
||||
|
||||
createEndpoint(device: ScryptedDevice): DiscoveryEndpoint<any> {
|
||||
async getEndpointForDevice(device: ScryptedDevice) : Promise<DiscoveryEndpoint> {
|
||||
if (!device)
|
||||
return;
|
||||
const discovery = isSupported(device);
|
||||
|
||||
const discovery = await supportedTypes.get(device.type)?.discover(device);
|
||||
if (!discovery)
|
||||
return;
|
||||
|
||||
const ret = Object.assign({
|
||||
const data: DiscoveryEndpoint = {
|
||||
endpointId: device.id,
|
||||
manufacturerName: "Scrypted",
|
||||
description: `${device.info?.manufacturer ?? 'Unknown'} ${device.info?.model ?? `device of type ${device.type}`}, connected via Scrypted`,
|
||||
@@ -380,13 +503,20 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
|
||||
model: device.info?.model || undefined,
|
||||
serialNumber: device.info?.serialNumber || undefined,
|
||||
firmwareVersion: device.info?.firmware || undefined,
|
||||
//softwareVersion: device.info?.version || undefined
|
||||
}
|
||||
}, discovery);
|
||||
softwareVersion: device.info?.version || undefined
|
||||
},
|
||||
displayCategories: discovery.displayCategories,
|
||||
capabilities: discovery.capabilities
|
||||
};
|
||||
|
||||
let supportedEndpointHealths: any[] = [];
|
||||
|
||||
if (device.interfaces.includes(ScryptedInterface.Online)) {
|
||||
supportedEndpointHealths.push({
|
||||
"name": "connectivity"
|
||||
});
|
||||
}
|
||||
|
||||
let supportedEndpointHealths = [{
|
||||
"name": "connectivity"
|
||||
}];
|
||||
// {
|
||||
// "name": "radioDiagnostics"
|
||||
// },
|
||||
@@ -400,17 +530,22 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
|
||||
})
|
||||
}
|
||||
|
||||
ret.capabilities.push(
|
||||
{
|
||||
"type": "AlexaInterface",
|
||||
"interface": "Alexa.EndpointHealth",
|
||||
"version": "3.2" as any,
|
||||
"properties": {
|
||||
"supported": supportedEndpointHealths,
|
||||
"proactivelyReported": true,
|
||||
"retrievable": true
|
||||
if (supportedEndpointHealths.length > 0) {
|
||||
data.capabilities.push(
|
||||
{
|
||||
"type": "AlexaInterface",
|
||||
"interface": "Alexa.EndpointHealth",
|
||||
"version": "3.2",
|
||||
"properties": {
|
||||
"supported": supportedEndpointHealths,
|
||||
"proactivelyReported": true,
|
||||
"retrievable": true
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
data.capabilities.push(
|
||||
{
|
||||
"type": "AlexaInterface",
|
||||
"interface": "Alexa",
|
||||
@@ -418,76 +553,20 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
|
||||
}
|
||||
);
|
||||
|
||||
//if (device.info?.mac !== undefined)
|
||||
// ret.connections.push(
|
||||
// {
|
||||
// "type": "TCP_IP",
|
||||
// "macAddress": device.info?.mac || undefined
|
||||
// }
|
||||
// );
|
||||
|
||||
return ret as any;
|
||||
}
|
||||
|
||||
async saveEndpoints(endpoints: DiscoveryEndpoint<any>[]) {
|
||||
const existingEndpoints: string[] = this.storageSettings.values.syncedDevices;
|
||||
const newEndpoints = endpoints.map(endpoint => endpoint.endpointId);
|
||||
const deleted = new Set(existingEndpoints);
|
||||
|
||||
for (const id of newEndpoints) {
|
||||
deleted.delete(id);
|
||||
}
|
||||
|
||||
const all = new Set([...existingEndpoints, ...newEndpoints]);
|
||||
|
||||
// save all the endpoints
|
||||
this.storageSettings.values.syncedDevices = [...all];
|
||||
|
||||
// delete leftover endpoints
|
||||
await this.deleteReport(...deleted);
|
||||
|
||||
// prune if the delete report completed successfully
|
||||
this.storageSettings.values.syncedDevices = newEndpoints;
|
||||
}
|
||||
|
||||
getDiscoveryEndpoints() {
|
||||
const endpoints: DiscoveryEndpoint<any>[] = [];
|
||||
|
||||
for (const id of Object.keys(systemManager.getSystemState())) {
|
||||
const device = systemManager.getDeviceById(id);
|
||||
|
||||
if (!device.mixins?.includes(this.id))
|
||||
continue;
|
||||
const endpoint = this.createEndpoint(device);
|
||||
if (endpoint)
|
||||
endpoints.push(endpoint);
|
||||
}
|
||||
return endpoints;
|
||||
}
|
||||
|
||||
async alexaDiscovery(request: HttpRequest, response: HttpResponse) {
|
||||
const endpoints = this.getDiscoveryEndpoints();
|
||||
|
||||
const ret: DiscoverEvent<any> = {
|
||||
event: {
|
||||
header: {
|
||||
namespace: 'Alexa.Discovery',
|
||||
name: 'Discover.Response',
|
||||
messageId: createMessageId(),
|
||||
payloadVersion: '3',
|
||||
},
|
||||
payload: {
|
||||
endpoints,
|
||||
if (device.info?.mac !== undefined)
|
||||
data.connections = [
|
||||
{
|
||||
"type": "TCP_IP",
|
||||
"macAddress": device.info.mac
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
response.send(JSON.stringify(ret));
|
||||
|
||||
this.saveEndpoints(endpoints);
|
||||
return data as any;
|
||||
}
|
||||
|
||||
async onRequest(request: HttpRequest, response: HttpResponse) {
|
||||
async onRequest(request: HttpRequest, rawResponse: HttpResponse) {
|
||||
const response = new HttpResponseLoggingImpl(rawResponse, this.console);
|
||||
|
||||
const { authorization } = request.headers;
|
||||
if (!this.validAuths.has(authorization)) {
|
||||
try {
|
||||
@@ -501,42 +580,81 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
|
||||
catch (e) {
|
||||
this.console.error(`request failed due to invalid authorization`, e);
|
||||
response.send(e.message, {
|
||||
code: 500
|
||||
code: 500,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const body = JSON.parse(request.body);
|
||||
const { directive } = body;
|
||||
const { namespace } = directive.header;
|
||||
const handler = this.handlers.get(namespace);
|
||||
if (handler)
|
||||
return handler.apply(this, arguments);
|
||||
const body = JSON.parse(request.body);
|
||||
const { directive } = body;
|
||||
const { namespace, name } = directive.header;
|
||||
|
||||
const capHandler = capabilityHandlers.get(namespace);
|
||||
if (capHandler) {
|
||||
const device = systemManager.getDeviceById(directive.endpoint.endpointId);
|
||||
if (!device) {
|
||||
response.send('Not Found', {
|
||||
code: 404,
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.console.assert(!DEBUG, `request: ${namespace}/${name}`);
|
||||
|
||||
return capHandler.apply(this, [request, response, directive, device]);
|
||||
const mapName = `${namespace}/${name}`;
|
||||
const handler = alexaHandlers.get(mapName);
|
||||
|
||||
if (handler)
|
||||
return handler.apply(this, [request, response, directive]);
|
||||
|
||||
const deviceHandler = alexaDeviceHandlers.get(mapName);
|
||||
|
||||
if (deviceHandler) {
|
||||
const device = systemManager.getDeviceById(directive.endpoint.endpointId);
|
||||
if (!device) {
|
||||
response.send(deviceErrorResponse("NO_SUCH_ENDPOINT", "The device doesn't exist in Scrypted", directive));
|
||||
return;
|
||||
}
|
||||
|
||||
response.send('Not Found', {
|
||||
code: 404,
|
||||
});
|
||||
}
|
||||
catch (e) {
|
||||
response.send(e.message, {
|
||||
code: 500,
|
||||
});
|
||||
return deviceHandler.apply(this, [request, response, directive, device]);
|
||||
} else {
|
||||
this.console.error(`no handler for: ${mapName}`);
|
||||
}
|
||||
|
||||
// it is better to send a non-specific response than an error, as the API might get rate throttled
|
||||
response.send(mirroredResponse(directive));
|
||||
}
|
||||
}
|
||||
|
||||
enum DeviceMixinStatus {
|
||||
NotSupported = 0,
|
||||
Setup = 1,
|
||||
AlreadySetup = 2
|
||||
}
|
||||
|
||||
class HttpResponseLoggingImpl implements AlexaHttpResponse {
|
||||
constructor(private response: HttpResponse, private console: Console) {
|
||||
}
|
||||
|
||||
send(body: string): void;
|
||||
send(body: string, options: HttpResponseOptions): void;
|
||||
send(body: Buffer): void;
|
||||
send(body: Buffer, options: HttpResponseOptions): void;
|
||||
send(body: any, options?: any): void {
|
||||
if (!options)
|
||||
options = {};
|
||||
|
||||
if (!options.code)
|
||||
options.code = 200;
|
||||
|
||||
if (options.code !== 200)
|
||||
this.console.error(`response error ${options.code}:`, body);
|
||||
else
|
||||
this.console.assert(!DEBUG, `response ${options.code}:`, body);
|
||||
|
||||
if (typeof body === 'object')
|
||||
body = JSON.stringify(body);
|
||||
|
||||
this.response.send(body, options);
|
||||
}
|
||||
sendFile(path: string): void;
|
||||
sendFile(path: string, options: HttpResponseOptions): void;
|
||||
sendFile(path: any, options?: any): void {
|
||||
this.response.sendFile(path, options);
|
||||
}
|
||||
sendSocket(socket: any, options: HttpResponseOptions): void {
|
||||
this.response.sendSocket(socket, options);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import {v4 as uuidv4} from 'uuid';
|
||||
|
||||
export function createMessageId() {
|
||||
return uuidv4();
|
||||
}
|
||||
@@ -1,190 +1,24 @@
|
||||
import { HttpResponse, MotionSensor, RTCAVSignalingSetup, RTCSignalingChannel, RTCSignalingOptions, RTCSignalingSendIceCandidate, RTCSignalingSession, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, VideoCamera } from "@scrypted/sdk";
|
||||
import { addSupportedType, AlexaCapabilityHandler, capabilityHandlers, EventReport, StateReport } from "./common";
|
||||
import { createMessageId } from "../message";
|
||||
import { Capability } from "alexa-smarthome-ts/lib/skill/Capability";
|
||||
import { DisplayCategory } from "alexa-smarthome-ts";
|
||||
import { MotionSensor, ObjectDetector, ScryptedDevice, ScryptedDeviceType, ScryptedInterface } from "@scrypted/sdk";
|
||||
import { DiscoveryEndpoint, Report } from "../alexa";
|
||||
import { getCameraCapabilities, reportCameraState, sendCameraEvent } from "./camera/capabilities";
|
||||
import { supportedTypes } from ".";
|
||||
|
||||
export function getCameraCapabilities(device: ScryptedDevice): Capability<any>[] {
|
||||
const capabilities: Capability<any>[] = [
|
||||
{
|
||||
"type": "AlexaInterface",
|
||||
"interface": "Alexa.RTCSessionController",
|
||||
"version": "3",
|
||||
"configuration": {
|
||||
isFullDuplexAudioSupported: true,
|
||||
}
|
||||
} as any,
|
||||
];
|
||||
|
||||
if (device.interfaces.includes(ScryptedInterface.MotionSensor)) {
|
||||
capabilities.push(
|
||||
{
|
||||
"type": "AlexaInterface",
|
||||
"interface": "Alexa.MotionSensor",
|
||||
"version": "3",
|
||||
"properties": {
|
||||
"supported": [
|
||||
{
|
||||
"name": "detectionState"
|
||||
}
|
||||
],
|
||||
"proactivelyReported": true,
|
||||
"retrievable": true
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return capabilities;
|
||||
}
|
||||
|
||||
addSupportedType(ScryptedDeviceType.Camera, {
|
||||
probe(device) {
|
||||
supportedTypes.set(ScryptedDeviceType.Camera, {
|
||||
async discover(device: ScryptedDevice): Promise<Partial<DiscoveryEndpoint>> {
|
||||
if (!device.interfaces.includes(ScryptedInterface.RTCSignalingChannel))
|
||||
return;
|
||||
|
||||
const capabilities = getCameraCapabilities(device);
|
||||
const capabilities = await getCameraCapabilities(device);
|
||||
|
||||
return {
|
||||
displayCategories: ['CAMERA'],
|
||||
capabilities
|
||||
}
|
||||
},
|
||||
async reportState(device: ScryptedDevice & MotionSensor): Promise<StateReport> {
|
||||
return {
|
||||
type: 'state',
|
||||
namespace: 'Alexa',
|
||||
name: 'StateReport',
|
||||
context: {
|
||||
"properties": [
|
||||
{
|
||||
"namespace": "Alexa.MotionSensor",
|
||||
"name": "detectionState",
|
||||
"value": device.motionDetected ? "DETECTED" : "NOT_DETECTED",
|
||||
"timeOfSample": new Date().toISOString(),
|
||||
"uncertaintyInMilliseconds": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
sendReport(device: ScryptedDevice & MotionSensor & ObjectDetector): Promise<Partial<Report>>{
|
||||
return reportCameraState(device);
|
||||
},
|
||||
async sendEvent(eventSource: ScryptedDevice & MotionSensor, eventDetails, eventData): Promise<EventReport> {
|
||||
if (eventDetails.eventInterface !== ScryptedInterface.MotionSensor)
|
||||
return undefined;
|
||||
|
||||
return {
|
||||
type: 'event',
|
||||
namespace: 'Alexa',
|
||||
name: 'ChangeReport',
|
||||
payload: {
|
||||
change: {
|
||||
cause: {
|
||||
type: "PHYSICAL_INTERACTION"
|
||||
},
|
||||
properties: [
|
||||
{
|
||||
"namespace": "Alexa.MotionSensor",
|
||||
"name": "detectionState",
|
||||
"value": eventData ? "DETECTED" : "NOT_DETECTED",
|
||||
"timeOfSample": new Date().toISOString(),
|
||||
"uncertaintyInMilliseconds": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
};
|
||||
sendEvent(eventSource: ScryptedDevice & MotionSensor & ObjectDetector, eventDetails, eventData): Promise<Partial<Report>> {
|
||||
return sendCameraEvent(eventSource, eventDetails, eventData);
|
||||
}
|
||||
});
|
||||
|
||||
export const rtcHandlers = new Map<string, AlexaCapabilityHandler<any>>();
|
||||
|
||||
export class AlexaSignalingSession implements RTCSignalingSession {
|
||||
constructor(public response: HttpResponse, public directive: any) {
|
||||
}
|
||||
|
||||
async getOptions(): Promise<RTCSignalingOptions> {
|
||||
return {
|
||||
proxy: true,
|
||||
offer: {
|
||||
type: 'offer',
|
||||
sdp: this.directive.payload.offer.value,
|
||||
},
|
||||
disableTrickle: true,
|
||||
// this could be a low resolution screen, no way of knowing, so never send a
|
||||
// 1080p+ stream.
|
||||
screen: {
|
||||
devicePixelRatio: 1, // TODO: get this from the device
|
||||
width: 1280,
|
||||
height: 720,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async createLocalDescription(type: "offer" | "answer", setup: RTCAVSignalingSetup, sendIceCandidate: RTCSignalingSendIceCandidate): Promise<RTCSessionDescriptionInit> {
|
||||
if (type !== 'offer')
|
||||
throw new Error('Alexa only supports RTC offer');
|
||||
if (sendIceCandidate)
|
||||
throw new Error("Alexa does not support trickle ICE");
|
||||
return {
|
||||
type: 'offer',
|
||||
sdp: this.directive.payload.offer.value,
|
||||
}
|
||||
}
|
||||
|
||||
async addIceCandidate(candidate: RTCIceCandidateInit): Promise<void> {
|
||||
throw new Error("Alexa does not support trickle ICE");
|
||||
}
|
||||
|
||||
async setRemoteDescription(description: RTCSessionDescriptionInit, setup: RTCAVSignalingSetup): Promise<void> {
|
||||
this.response.send(JSON.stringify({
|
||||
"event": {
|
||||
"header": {
|
||||
"namespace": "Alexa.RTCSessionController",
|
||||
"name": "AnswerGeneratedForSession",
|
||||
"messageId": createMessageId(),
|
||||
"payloadVersion": "3"
|
||||
},
|
||||
"payload": {
|
||||
"answer": {
|
||||
"format": "SDP",
|
||||
"value": description.sdp,
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
rtcHandlers.set('InitiateSessionWithOffer', async (request, response, directive: any,
|
||||
device: ScryptedDevice & RTCSignalingChannel) => {
|
||||
const session = new AlexaSignalingSession(response, directive);
|
||||
const control = await device.startRTCSignalingSession(session);
|
||||
control.setPlayback({
|
||||
audio: true,
|
||||
video: false,
|
||||
})
|
||||
});
|
||||
|
||||
capabilityHandlers.set('Alexa.RTCSessionController', async (request, response, directive: any, device: ScryptedDevice & VideoCamera) => {
|
||||
const { name } = directive.header;
|
||||
const handler = rtcHandlers.get(name);
|
||||
if (handler)
|
||||
return handler.apply(this, [request, response, directive, device]);
|
||||
|
||||
const { sessionId } = directive.payload;
|
||||
const body = {
|
||||
"event": {
|
||||
"header": {
|
||||
"namespace": "Alexa.RTCSessionController",
|
||||
name,
|
||||
"messageId": createMessageId(),
|
||||
"payloadVersion": "3"
|
||||
},
|
||||
"payload": {
|
||||
sessionId,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
response.send(JSON.stringify(body));
|
||||
});
|
||||
|
||||
194
plugins/alexa/src/types/camera/capabilities.ts
Normal file
194
plugins/alexa/src/types/camera/capabilities.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
import sdk, { MediaObject, MotionSensor, ObjectDetector, ScryptedDevice, ScryptedInterface } from "@scrypted/sdk";
|
||||
import { ChangeReport, DiscoveryCapability, ObjectDetectionEvent, Report, StateReport, Property } from "../../alexa";
|
||||
|
||||
const { mediaManager } = sdk;
|
||||
|
||||
export async function reportCameraState(device: ScryptedDevice & MotionSensor & ObjectDetector): Promise<Partial<Report>>{
|
||||
let data = {
|
||||
context: {
|
||||
properties: []
|
||||
}
|
||||
|
||||
} as Partial<StateReport>;
|
||||
|
||||
if (device.interfaces.includes(ScryptedInterface.ObjectDetector)) {
|
||||
const detectionTypes = await (device as any as ObjectDetector).getObjectTypes();
|
||||
const classNames = detectionTypes.classes.filter(t => t !== 'ring' && t !== 'motion').map(type => type.toLowerCase());
|
||||
|
||||
data.context.properties.push({
|
||||
"namespace": "Alexa.SmartVision.ObjectDetectionSensor",
|
||||
"name": "objectDetectionClasses",
|
||||
"value": classNames.map(type => ({
|
||||
"imageNetClass": type
|
||||
})),
|
||||
"timeOfSample": new Date().toISOString(),
|
||||
"uncertaintyInMilliseconds": 0
|
||||
});
|
||||
}
|
||||
|
||||
if (device.interfaces.includes(ScryptedInterface.MotionSensor)) {
|
||||
data.context.properties.push({
|
||||
"namespace": "Alexa.MotionSensor",
|
||||
"name": "detectionState",
|
||||
"value": device.motionDetected ? "DETECTED" : "NOT_DETECTED",
|
||||
"timeOfSample": new Date().toISOString(),
|
||||
"uncertaintyInMilliseconds": 0
|
||||
});
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
export async function sendCameraEvent (eventSource: ScryptedDevice & MotionSensor & ObjectDetector, eventDetails, eventData): Promise<Partial<Report>> {
|
||||
if (eventDetails.eventInterface === ScryptedInterface.ObjectDetector) {
|
||||
|
||||
// ring and motion are not valid objects
|
||||
if (eventData.detections.has('ring') || eventData.detections.has('motion'))
|
||||
return undefined;
|
||||
|
||||
console.debug('ObjectDetector event', eventData);
|
||||
|
||||
let mediaObj: MediaObject = undefined;
|
||||
let frameImageUri: string = undefined;
|
||||
|
||||
try {
|
||||
mediaObj = await eventSource.getDetectionInput(eventData.detectionId, eventData.eventId);
|
||||
frameImageUri = await mediaManager.convertMediaObjectToUrl(mediaObj, 'image/jpeg');
|
||||
} catch (e) { }
|
||||
|
||||
let data = {
|
||||
event: {
|
||||
header: {
|
||||
namespace: 'Alexa.SmartVision.ObjectDetectionSensor',
|
||||
name: 'ObjectDetection'
|
||||
},
|
||||
payload: {
|
||||
"events": [eventData.detections.map(detection => {
|
||||
let event = {
|
||||
"eventIdentifier": eventData.eventId,
|
||||
"imageNetClass": detection.className,
|
||||
"timeOfSample": new Date(eventData.timestamp).toISOString(),
|
||||
"uncertaintyInMilliseconds": 500
|
||||
};
|
||||
|
||||
if (detection.id) {
|
||||
event["objectIdentifier"] = detection.id;
|
||||
}
|
||||
|
||||
if (frameImageUri) {
|
||||
event["frameImageUri"] = frameImageUri;
|
||||
}
|
||||
|
||||
return event;
|
||||
})]
|
||||
}
|
||||
}
|
||||
} as Partial<ObjectDetectionEvent>;
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
if (eventDetails.eventInterface === ScryptedInterface.MotionSensor)
|
||||
return {
|
||||
event: {
|
||||
payload: {
|
||||
change: {
|
||||
cause: {
|
||||
type: "PHYSICAL_INTERACTION"
|
||||
},
|
||||
properties: [
|
||||
{
|
||||
"namespace": "Alexa.MotionSensor",
|
||||
"name": "detectionState",
|
||||
"value": eventData ? "DETECTED" : "NOT_DETECTED",
|
||||
"timeOfSample": new Date(eventDetails.eventTime).toISOString(),
|
||||
"uncertaintyInMilliseconds": 500
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
}
|
||||
} as Partial<ChangeReport>;
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export async function getCameraCapabilities(device: ScryptedDevice): Promise<DiscoveryCapability[]> {
|
||||
const capabilities = [
|
||||
{
|
||||
"type": "AlexaInterface",
|
||||
"interface": "Alexa.RTCSessionController",
|
||||
"version": "3",
|
||||
"configuration": {
|
||||
isFullDuplexAudioSupported: true,
|
||||
}
|
||||
} as DiscoveryCapability
|
||||
];
|
||||
|
||||
if (device.interfaces.includes(ScryptedInterface.ObjectDetector)) {
|
||||
const detectionTypes = await (device as any as ObjectDetector).getObjectTypes();
|
||||
const classNames = detectionTypes.classes.filter(t => t !== 'ring' && t !== 'motion').map(type => type.toLowerCase());
|
||||
|
||||
capabilities.push(
|
||||
{
|
||||
"type": "AlexaInterface",
|
||||
"interface": "Alexa.SmartVision.ObjectDetectionSensor",
|
||||
"version": "1.0",
|
||||
"properties": {
|
||||
"supported": [{
|
||||
"name": "objectDetectionClasses"
|
||||
}],
|
||||
"proactivelyReported": true,
|
||||
"retrievable": true
|
||||
},
|
||||
"configuration": {
|
||||
"objectDetectionConfiguration": classNames.map(type => ({
|
||||
"imageNetClass": type
|
||||
}))
|
||||
}
|
||||
} as DiscoveryCapability
|
||||
);
|
||||
|
||||
capabilities.push(
|
||||
{
|
||||
"type": "AlexaInterface",
|
||||
"interface": "Alexa.DataController",
|
||||
"instance": "Camera.SmartVisionData",
|
||||
"version": "1.0",
|
||||
"properties": undefined,
|
||||
"configuration": {
|
||||
"targetCapability": {
|
||||
"name": "Alexa.SmartVision.ObjectDetectionSensor",
|
||||
"version": "1.0"
|
||||
},
|
||||
"dataRetrievalSchema": {
|
||||
"type": "JSON",
|
||||
"schema": "SmartVisionData"
|
||||
},
|
||||
"supportedAccess": ["BY_IDENTIFIER", "BY_TIMESTAMP_RANGE"]
|
||||
}
|
||||
} as DiscoveryCapability
|
||||
);
|
||||
}
|
||||
|
||||
if (device.interfaces.includes(ScryptedInterface.MotionSensor)) {
|
||||
capabilities.push(
|
||||
{
|
||||
"type": "AlexaInterface",
|
||||
"interface": "Alexa.MotionSensor",
|
||||
"version": "3",
|
||||
"properties": {
|
||||
"supported": [
|
||||
{
|
||||
"name": "detectionState"
|
||||
}
|
||||
],
|
||||
"proactivelyReported": true,
|
||||
"retrievable": true
|
||||
}
|
||||
} as DiscoveryCapability
|
||||
);
|
||||
}
|
||||
|
||||
return capabilities;
|
||||
};
|
||||
152
plugins/alexa/src/types/camera/handlers.ts
Normal file
152
plugins/alexa/src/types/camera/handlers.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import { ObjectDetector, RTCAVSignalingSetup, RTCSessionControl, RTCSignalingChannel, RTCSignalingOptions, RTCSignalingSendIceCandidate, RTCSignalingSession, ScryptedDevice } from "@scrypted/sdk";
|
||||
import { supportedTypes } from "..";
|
||||
import { v4 as createMessageId } from 'uuid';
|
||||
import { AlexaHttpResponse, sendDeviceResponse } from "../../common";
|
||||
import { alexaDeviceHandlers } from "../../handlers";
|
||||
import { Response, WebRTCAnswerGeneratedForSessionEvent, WebRTCSessionConnectedEvent, WebRTCSessionDisconnectedEvent } from '../../alexa'
|
||||
|
||||
export class AlexaSignalingSession implements RTCSignalingSession {
|
||||
constructor(public response: AlexaHttpResponse, public directive: any) {
|
||||
}
|
||||
|
||||
async getOptions(): Promise<RTCSignalingOptions> {
|
||||
return {
|
||||
proxy: true,
|
||||
offer: {
|
||||
type: 'offer',
|
||||
sdp: this.directive.payload.offer.value,
|
||||
},
|
||||
disableTrickle: true,
|
||||
}
|
||||
}
|
||||
|
||||
async createLocalDescription(type: "offer" | "answer", setup: RTCAVSignalingSetup, sendIceCandidate: RTCSignalingSendIceCandidate): Promise<RTCSessionDescriptionInit> {
|
||||
if (type !== 'offer')
|
||||
throw new Error('Alexa only supports RTC offer');
|
||||
|
||||
if (sendIceCandidate)
|
||||
throw new Error("Alexa does not support trickle ICE");
|
||||
|
||||
return {
|
||||
type: type,
|
||||
sdp: this.directive.payload.offer.value,
|
||||
}
|
||||
}
|
||||
|
||||
async addIceCandidate(candidate: RTCIceCandidateInit): Promise<void> {
|
||||
throw new Error("Alexa does not support trickle ICE");
|
||||
}
|
||||
|
||||
async setRemoteDescription(description: RTCSessionDescriptionInit, setup: RTCAVSignalingSetup): Promise<void> {
|
||||
|
||||
const { header, endpoint, payload } = this.directive;
|
||||
|
||||
const data: WebRTCAnswerGeneratedForSessionEvent = {
|
||||
"event": {
|
||||
header,
|
||||
endpoint,
|
||||
payload
|
||||
},
|
||||
context: undefined
|
||||
};
|
||||
|
||||
data.event.header.name = "AnswerGeneratedForSession";
|
||||
data.event.header.messageId = createMessageId();
|
||||
|
||||
data.event.payload.answer = {
|
||||
format: 'SDP',
|
||||
value: description.sdp,
|
||||
};
|
||||
|
||||
this.response.send(data);
|
||||
}
|
||||
}
|
||||
|
||||
const sessionCache = new Map<string, RTCSessionControl>();
|
||||
|
||||
alexaDeviceHandlers.set('Alexa.RTCSessionController/InitiateSessionWithOffer', async (request, response, directive: any, device: ScryptedDevice & RTCSignalingChannel) => {
|
||||
const { header, endpoint, payload } = directive;
|
||||
const { sessionId } = payload;
|
||||
|
||||
const session = new AlexaSignalingSession(response, directive);
|
||||
const control = await device.startRTCSignalingSession(session);
|
||||
control.setPlayback({
|
||||
audio: true,
|
||||
video: false,
|
||||
})
|
||||
|
||||
sessionCache.set(sessionId, control);
|
||||
});
|
||||
|
||||
alexaDeviceHandlers.set('Alexa.RTCSessionController/SessionConnected', async (request, response, directive: any, device: ScryptedDevice) => {
|
||||
const { header, endpoint, payload } = directive;
|
||||
const data: WebRTCSessionConnectedEvent = {
|
||||
"event": {
|
||||
header,
|
||||
endpoint,
|
||||
payload
|
||||
},
|
||||
context: undefined
|
||||
};
|
||||
|
||||
data.event.header.messageId = createMessageId();
|
||||
|
||||
response.send(data);
|
||||
});
|
||||
|
||||
alexaDeviceHandlers.set('Alexa.RTCSessionController/SessionDisconnected', async (request, response, directive: any, device: ScryptedDevice) => {
|
||||
const { header, endpoint, payload } = directive;
|
||||
const { sessionId } = payload;
|
||||
|
||||
const session = sessionCache.get(sessionId);
|
||||
if (session) {
|
||||
sessionCache.delete(sessionId);
|
||||
await session.endSession();
|
||||
}
|
||||
|
||||
const data: WebRTCSessionDisconnectedEvent = {
|
||||
"event": {
|
||||
header,
|
||||
endpoint,
|
||||
payload
|
||||
},
|
||||
context: undefined
|
||||
};
|
||||
|
||||
data.event.header.messageId = createMessageId();
|
||||
|
||||
response.send(data);
|
||||
});
|
||||
|
||||
alexaDeviceHandlers.set('Alexa.SmartVision.ObjectDetectionSensor/SetObjectDetectionClasses', async (request, response, directive: any, device: ScryptedDevice & ObjectDetector) => {
|
||||
const supportedType = supportedTypes.get(device.type);
|
||||
if (!supportedType)
|
||||
return;
|
||||
|
||||
const { header, endpoint, payload } = directive;
|
||||
const detectionTypes = await device.getObjectTypes();
|
||||
|
||||
const data: Response = {
|
||||
"event": {
|
||||
header,
|
||||
endpoint,
|
||||
payload: {}
|
||||
},
|
||||
"context": {
|
||||
"properties": [{
|
||||
"namespace": "Alexa.SmartVision.ObjectDetectionSensor",
|
||||
"name": "objectDetectionClasses",
|
||||
"value": detectionTypes.classes.map(type => ({
|
||||
"imageNetClass": type
|
||||
})),
|
||||
timeOfSample: new Date().toISOString(),
|
||||
uncertaintyInMilliseconds: 0
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
data.event.header.name = "Response";
|
||||
data.event.header.messageId = createMessageId();
|
||||
|
||||
sendDeviceResponse(data, response, device);
|
||||
});
|
||||
@@ -1,180 +0,0 @@
|
||||
import { Battery, EventDetails, HttpRequest, HttpResponse, Online, PowerSensor, ScryptedDevice, ScryptedDeviceType, ScryptedInterface } from "@scrypted/sdk";
|
||||
import {DiscoveryEndpoint, Directive} from 'alexa-smarthome-ts';
|
||||
import { createMessageId } from "../message";
|
||||
|
||||
export type AlexaHandler = (request: HttpRequest, response: HttpResponse, directive: Directive) => Promise<void>
|
||||
export type AlexaCapabilityHandler<T> = (request: HttpRequest, response: HttpResponse, directive: Directive, device: ScryptedDevice & T) => Promise<void>
|
||||
|
||||
export const supportedTypes = new Map<ScryptedDeviceType, SupportedType>();
|
||||
export const capabilityHandlers = new Map<string, AlexaCapabilityHandler<any>>();
|
||||
export const alexaHandlers = new Map<string, AlexaCapabilityHandler<any>>();
|
||||
|
||||
export interface EventReport {
|
||||
type: 'event';
|
||||
payload?: any;
|
||||
context?: any;
|
||||
namespace?: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface StateReport {
|
||||
type: 'state';
|
||||
payload?: any;
|
||||
context?: any;
|
||||
namespace?: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface SupportedType {
|
||||
probe(device: ScryptedDevice): Partial<DiscoveryEndpoint<any>>;
|
||||
sendEvent(eventSource: ScryptedDevice, eventDetails: EventDetails, eventData: any): Promise<EventReport>;
|
||||
reportState(device: ScryptedDevice): Promise<StateReport>;
|
||||
}
|
||||
|
||||
export function addSupportedType(type: ScryptedDeviceType, supportedType: SupportedType) {
|
||||
supportedTypes.set(type, supportedType);
|
||||
}
|
||||
|
||||
export function isSupported(device: ScryptedDevice) {
|
||||
return supportedTypes.get(device.type)?.probe(device);
|
||||
}
|
||||
|
||||
export function addOnline(data: any, device: ScryptedDevice & Online) : any {
|
||||
if (!device.interfaces.includes(ScryptedInterface.Online))
|
||||
return data;
|
||||
|
||||
if (data.context === undefined)
|
||||
data.context = {};
|
||||
|
||||
if (data.context.properties === undefined)
|
||||
data.context.properties = [];
|
||||
|
||||
data.context.properties.push(
|
||||
{
|
||||
"namespace": "Alexa.EndpointHealth",
|
||||
"name": "connectivity",
|
||||
"value": {
|
||||
"value": device.online ? "OK" : "UNREACHABLE",
|
||||
},
|
||||
"timeOfSample": new Date().toISOString(),
|
||||
"uncertaintyInMilliseconds": 0
|
||||
}
|
||||
);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export function addPowerSensor(data: any, device: ScryptedDevice & PowerSensor) : any {
|
||||
if (!device.interfaces.includes(ScryptedInterface.PowerSensor))
|
||||
return data;
|
||||
|
||||
if (data.context === undefined)
|
||||
data.context = {};
|
||||
|
||||
if (data.context.properties === undefined)
|
||||
data.context.properties = [];
|
||||
|
||||
data.context.properties.push(
|
||||
{
|
||||
"namespace": "Alexa.PowerController",
|
||||
"name": "powerState",
|
||||
"value": device.powerDetected ? "ON" : "OFF",
|
||||
"timeOfSample": new Date().toISOString(),
|
||||
"uncertaintyInMilliseconds": 0
|
||||
}
|
||||
);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export function addBattery(data: any, device: ScryptedDevice & Battery) : any {
|
||||
if (!device.interfaces.includes(ScryptedInterface.Battery))
|
||||
return data;
|
||||
|
||||
if (data.context === undefined)
|
||||
data.context = {};
|
||||
|
||||
if (data.context.properties === undefined)
|
||||
data.context.properties = [];
|
||||
|
||||
const lowPower = device.batteryLevel < 20;
|
||||
let health = undefined;
|
||||
|
||||
if (lowPower) {
|
||||
health = {
|
||||
"state": "WARNING",
|
||||
"reasons": [
|
||||
"LOW_CHARGE"
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
data.context.properties.push(
|
||||
{
|
||||
"namespace": "Alexa.EndpointHealth",
|
||||
"name": "battery",
|
||||
"value": {
|
||||
health,
|
||||
"levelPercentage": device.batteryLevel,
|
||||
},
|
||||
"timeOfSample": new Date().toISOString(),
|
||||
"uncertaintyInMilliseconds": 0
|
||||
}
|
||||
);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function sendResponse(data: any, response: any, device: ScryptedDevice) {
|
||||
data = addBattery(data, device);
|
||||
data = addOnline(data, device);
|
||||
data = addPowerSensor(data, device);
|
||||
|
||||
response.send(JSON.stringify(data));
|
||||
}
|
||||
|
||||
alexaHandlers.set('ReportState', async (request, response, directive: any, device: ScryptedDevice) => {
|
||||
const supportedType = supportedTypes.get(device.type);
|
||||
if (!supportedType)
|
||||
return;
|
||||
|
||||
const { header, endpoint } = directive;
|
||||
|
||||
const report = await supportedType.reportState(device);
|
||||
if (report.type === 'state') {
|
||||
const data = {
|
||||
"event": {
|
||||
header,
|
||||
endpoint,
|
||||
payload: report.payload,
|
||||
},
|
||||
"context": report.context
|
||||
};
|
||||
|
||||
data.event.header.name = "StateReport";
|
||||
data.event.header.messageId = createMessageId();
|
||||
|
||||
sendResponse(data, response, device);
|
||||
}
|
||||
});
|
||||
|
||||
capabilityHandlers.set('Alexa', async (request, response, directive: any, device: ScryptedDevice) => {
|
||||
const { name } = directive.header;
|
||||
let handler = alexaHandlers.get(name);
|
||||
if (handler)
|
||||
return handler.apply(this, [request, response, directive, device]);
|
||||
|
||||
const { header, endpoint, payload } = directive;
|
||||
const data = {
|
||||
"event": {
|
||||
header,
|
||||
endpoint,
|
||||
payload
|
||||
}
|
||||
};
|
||||
|
||||
data.event.header.name = "Response";
|
||||
data.event.header.messageId = createMessageId();
|
||||
|
||||
sendResponse(data, response, device);
|
||||
});
|
||||
@@ -1,81 +1,57 @@
|
||||
import { BinarySensor, MotionSensor, ScryptedDevice, ScryptedDeviceType, ScryptedInterface } from "@scrypted/sdk";
|
||||
import { getCameraCapabilities } from "./camera";
|
||||
import { addSupportedType, EventReport, StateReport } from "./common";
|
||||
import { DisplayCategory } from "alexa-smarthome-ts";
|
||||
import { MotionSensor, ObjectDetector, ScryptedDevice, ScryptedDeviceType, ScryptedInterface } from "@scrypted/sdk";
|
||||
import { getCameraCapabilities, reportCameraState, sendCameraEvent } from "./camera/capabilities";
|
||||
import { DiscoveryEndpoint, DisplayCategory, Report, DoorbellPressEvent } from "../alexa";
|
||||
import { supportedTypes } from ".";
|
||||
|
||||
addSupportedType(ScryptedDeviceType.Doorbell, {
|
||||
probe(device) {
|
||||
if (!device.interfaces.includes(ScryptedInterface.RTCSignalingChannel) || !device.interfaces.includes(ScryptedInterface.BinarySensor))
|
||||
return;
|
||||
supportedTypes.set(ScryptedDeviceType.Doorbell, {
|
||||
async discover(device: ScryptedDevice): Promise<Partial<DiscoveryEndpoint>> {
|
||||
let capabilities: any[] = [];
|
||||
let category: DisplayCategory = 'DOORBELL';
|
||||
|
||||
const capabilities = getCameraCapabilities(device);
|
||||
capabilities.push(
|
||||
{
|
||||
"type": "AlexaInterface",
|
||||
"interface": "Alexa.DoorbellEventSource",
|
||||
"version": "3",
|
||||
"proactivelyReported": true
|
||||
} as any,
|
||||
);
|
||||
|
||||
return {
|
||||
displayCategories: ['CAMERA'],
|
||||
capabilities
|
||||
if (device.interfaces.includes(ScryptedInterface.RTCSignalingChannel)) {
|
||||
capabilities = await getCameraCapabilities(device);
|
||||
category = 'CAMERA';
|
||||
}
|
||||
},
|
||||
async reportState(device: ScryptedDevice & MotionSensor): Promise<StateReport>{
|
||||
|
||||
if (device.interfaces.includes(ScryptedInterface.BinarySensor)) {
|
||||
capabilities.push(
|
||||
{
|
||||
"type": "AlexaInterface",
|
||||
"interface": "Alexa.DoorbellEventSource",
|
||||
"version": "3",
|
||||
"proactivelyReported": true
|
||||
} as any,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'state',
|
||||
namespace: 'Alexa',
|
||||
name: 'StateReport',
|
||||
context: {
|
||||
"properties": [
|
||||
{
|
||||
"namespace": "Alexa.MotionSensor",
|
||||
"name": "detectionState",
|
||||
"value": device.motionDetected ? "DETECTED" : "NOT_DETECTED",
|
||||
"timeOfSample": new Date().toISOString(),
|
||||
"uncertaintyInMilliseconds": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
displayCategories: [category],
|
||||
capabilities
|
||||
};
|
||||
},
|
||||
async sendEvent(eventSource: ScryptedDevice, eventDetails, eventData): Promise<EventReport> {
|
||||
if (eventDetails.eventInterface === ScryptedInterface.MotionSensor)
|
||||
return {
|
||||
type: 'event',
|
||||
namespace: 'Alexa',
|
||||
name: 'ChangeReport',
|
||||
payload: {
|
||||
change: {
|
||||
cause: {
|
||||
type: "PHYSICAL_INTERACTION"
|
||||
},
|
||||
properties: [
|
||||
{
|
||||
"namespace": "Alexa.MotionSensor",
|
||||
"name": "detectionState",
|
||||
"value": eventData ? "DETECTED" : "NOT_DETECTED",
|
||||
"timeOfSample": new Date().toISOString(),
|
||||
"uncertaintyInMilliseconds": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
};
|
||||
sendReport(device: ScryptedDevice & MotionSensor & ObjectDetector): Promise<Partial<Report>>{
|
||||
return reportCameraState(device);
|
||||
},
|
||||
async sendEvent(eventSource: ScryptedDevice & MotionSensor & ObjectDetector, eventDetails, eventData): Promise<Partial<Report>> {
|
||||
let response = await sendCameraEvent(eventSource, eventDetails, eventData);
|
||||
|
||||
if (response)
|
||||
return response;
|
||||
|
||||
if (eventDetails.eventInterface === ScryptedInterface.BinarySensor && eventData === true)
|
||||
return {
|
||||
type: 'event',
|
||||
namespace: 'Alexa.DoorbellEventSource',
|
||||
name: 'DoorbellPress',
|
||||
payload: {
|
||||
"cause": {
|
||||
"type": "PHYSICAL_INTERACTION"
|
||||
event: {
|
||||
header: {
|
||||
namespace: 'Alexa.DoorbellEventSource',
|
||||
name: 'DoorbellPress'
|
||||
},
|
||||
"timestamp": new Date().toISOString(),
|
||||
}
|
||||
};
|
||||
payload: {
|
||||
"cause": {
|
||||
"type": "PHYSICAL_INTERACTION"
|
||||
},
|
||||
"timestamp": new Date(eventDetails.eventTime).toISOString(),
|
||||
}
|
||||
}
|
||||
} as Partial<DoorbellPressEvent>;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { BinarySensor, Entry, EntrySensor, ScryptedDevice, ScryptedDeviceType, ScryptedInterface } from "@scrypted/sdk";
|
||||
import { getCameraCapabilities } from "./camera";
|
||||
import { addSupportedType, EventReport, StateReport } from "./common";
|
||||
import { DisplayCategory } from "alexa-smarthome-ts";
|
||||
import { Entry, EntrySensor, ScryptedDevice, ScryptedDeviceType, ScryptedInterface } from "@scrypted/sdk";
|
||||
import { DiscoveryEndpoint, DiscoveryCapability, ChangeReport, Report } from "../alexa";
|
||||
import { supportedTypes } from ".";
|
||||
|
||||
addSupportedType(ScryptedDeviceType.Garage, {
|
||||
probe(device) {
|
||||
if (!device.interfaces.includes(ScryptedInterface.EntrySensor))
|
||||
supportedTypes.set(ScryptedDeviceType.Garage, {
|
||||
async discover(device: ScryptedDevice): Promise<Partial<DiscoveryEndpoint>> {
|
||||
if (!device.interfaces.includes(ScryptedInterface.EntrySensor))
|
||||
return;
|
||||
|
||||
const capabilities = getCameraCapabilities(device);
|
||||
const capabilities: DiscoveryCapability[] = [];
|
||||
capabilities.push(
|
||||
{
|
||||
"type": "AlexaInterface",
|
||||
@@ -115,19 +114,16 @@ addSupportedType(ScryptedDeviceType.Garage, {
|
||||
}
|
||||
]
|
||||
}
|
||||
} as any,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
displayCategories: ['GARAGE_DOOR'] as any,
|
||||
displayCategories: ['GARAGE_DOOR'],
|
||||
capabilities
|
||||
}
|
||||
},
|
||||
async reportState(eventSource: ScryptedDevice & EntrySensor): Promise<StateReport> {
|
||||
async sendReport(eventSource: ScryptedDevice & EntrySensor): Promise<Partial<Report>> {
|
||||
return {
|
||||
type: 'state',
|
||||
namespace: 'Alexa',
|
||||
name: 'StateReport',
|
||||
context: {
|
||||
"properties": [
|
||||
{
|
||||
@@ -142,14 +138,12 @@ addSupportedType(ScryptedDeviceType.Garage, {
|
||||
}
|
||||
};
|
||||
},
|
||||
async sendEvent(eventSource: ScryptedDevice & EntrySensor, eventDetails, eventData): Promise<EventReport> {
|
||||
async sendEvent(eventSource: ScryptedDevice & Entry & EntrySensor, eventDetails, eventData): Promise<Partial<Report>> {
|
||||
if (eventDetails.eventInterface !== ScryptedInterface.EntrySensor)
|
||||
return undefined;
|
||||
|
||||
return {
|
||||
type: 'event',
|
||||
namespace: 'Alexa',
|
||||
name: 'ChangeReport',
|
||||
event: {
|
||||
payload: {
|
||||
change: {
|
||||
cause: {
|
||||
@@ -167,6 +161,30 @@ addSupportedType(ScryptedDeviceType.Garage, {
|
||||
]
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
} as Partial<ChangeReport>;
|
||||
},
|
||||
async setState(eventSource: ScryptedDevice & Entry & EntrySensor, payload: any): Promise<Partial<Report>> {
|
||||
if (payload.mode === 'Position.Up') {
|
||||
await eventSource.openEntry();
|
||||
}
|
||||
else if (payload.mode === 'Position.Down') {
|
||||
await eventSource.closeEntry();
|
||||
}
|
||||
|
||||
return {
|
||||
context: {
|
||||
"properties": [
|
||||
{
|
||||
"namespace": "Alexa.ModeController",
|
||||
"instance": "GarageDoor.Position",
|
||||
"name": "mode",
|
||||
"value": payload.mode,
|
||||
"timeOfSample": new Date().toISOString(),
|
||||
"uncertaintyInMilliseconds": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
31
plugins/alexa/src/types/garagedoor/handlers.ts
Normal file
31
plugins/alexa/src/types/garagedoor/handlers.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { ScryptedDevice } from "@scrypted/sdk";
|
||||
import { supportedTypes } from "..";
|
||||
import { sendDeviceResponse } from "../../common";
|
||||
import { alexaDeviceHandlers } from "../../handlers";
|
||||
import { v4 as createMessageId } from 'uuid';
|
||||
import { Response } from "../../alexa";
|
||||
|
||||
async function sendResponse (request, response, directive: any, device: ScryptedDevice) {
|
||||
const supportedType = supportedTypes.get(device.type);
|
||||
if (!supportedType)
|
||||
return;
|
||||
|
||||
const { header, endpoint, payload } = directive;
|
||||
const report = await supportedType.setState(device, payload);
|
||||
const data = {
|
||||
"event": {
|
||||
header,
|
||||
endpoint,
|
||||
payload
|
||||
},
|
||||
context: report?.context
|
||||
} as Response;
|
||||
|
||||
data.event.header.name = "Response";
|
||||
data.event.header.messageId = createMessageId();
|
||||
|
||||
sendDeviceResponse(data, response, device);
|
||||
}
|
||||
|
||||
alexaDeviceHandlers.set('Alexa.ModeController/SetMode', sendResponse);
|
||||
alexaDeviceHandlers.set('Alexa.ModeController/AdjustMode', sendResponse);
|
||||
@@ -1,6 +1,21 @@
|
||||
import { ScryptedDeviceType, ScryptedDevice, EventDetails } from '@scrypted/sdk';
|
||||
import { DiscoveryEndpoint, Report } from '../alexa';
|
||||
|
||||
export interface SupportedType {
|
||||
discover(device: ScryptedDevice): Promise<Partial<DiscoveryEndpoint>>;
|
||||
sendEvent(device: ScryptedDevice, eventDetails: EventDetails, eventData: any): Promise<Partial<Report>>;
|
||||
sendReport(device: ScryptedDevice): Promise<Partial<Report>>;
|
||||
setState?(device: ScryptedDevice, payload: any): Promise<Partial<Report>>;
|
||||
}
|
||||
|
||||
export const supportedTypes = new Map<ScryptedDeviceType, SupportedType>();
|
||||
|
||||
import '../handlers';
|
||||
import './camera';
|
||||
import './camera/handlers';
|
||||
import './doorbell';
|
||||
import './garagedoor';
|
||||
|
||||
export { isSupported} from './common';
|
||||
|
||||
import './switch';
|
||||
import './switch/handlers';
|
||||
import './sensor';
|
||||
import './securitysystem';
|
||||
179
plugins/alexa/src/types/securitysystem.ts
Normal file
179
plugins/alexa/src/types/securitysystem.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
import { EventDetails, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, SecuritySystem, SecuritySystemMode } from "@scrypted/sdk";
|
||||
import { DiscoveryEndpoint, DiscoveryCapability, ChangeReport, Report, StateReport, DisplayCategory, ChangePayload, Property } from "../alexa";
|
||||
import { supportedTypes } from ".";
|
||||
|
||||
function getArmState(mode: SecuritySystemMode): string {
|
||||
switch(mode) {
|
||||
case SecuritySystemMode.AwayArmed:
|
||||
return 'ARMED_AWAY';
|
||||
case SecuritySystemMode.HomeArmed:
|
||||
return 'ARMED_STAY';
|
||||
case SecuritySystemMode.NightArmed:
|
||||
return 'ARMED_NIGHT';
|
||||
case SecuritySystemMode.Disarmed:
|
||||
return 'DISARMED';
|
||||
}
|
||||
}
|
||||
|
||||
supportedTypes.set(ScryptedDeviceType.SecuritySystem, {
|
||||
async discover(device: ScryptedDevice & SecuritySystem): Promise<Partial<DiscoveryEndpoint>> {
|
||||
const capabilities: DiscoveryCapability[] = [];
|
||||
const displayCategories: DisplayCategory[] = [];
|
||||
|
||||
if (device.interfaces.includes(ScryptedInterface.SecuritySystem)) {
|
||||
const supportedModes = device.securitySystemState.supportedModes;
|
||||
|
||||
capabilities.push(
|
||||
{
|
||||
"type": "AlexaInterface",
|
||||
"interface": "Alexa.SecurityPanelController",
|
||||
"version": "3",
|
||||
"properties": {
|
||||
"supported": [
|
||||
{
|
||||
"name": "armState"
|
||||
},
|
||||
{
|
||||
"name": "burglaryAlarm"
|
||||
},
|
||||
//{
|
||||
// "name": "waterAlarm"
|
||||
//},
|
||||
//{
|
||||
// "name": "fireAlarm"
|
||||
//},
|
||||
//{
|
||||
// "name": "carbonMonoxideAlarm"
|
||||
//}
|
||||
],
|
||||
"proactivelyReported": true,
|
||||
"retrievable": true
|
||||
},
|
||||
"configuration": {
|
||||
"supportedArmStates": supportedModes.map(mode => {
|
||||
return {
|
||||
"value": getArmState(mode)
|
||||
}
|
||||
}),
|
||||
"supportedAuthorizationTypes": [
|
||||
{
|
||||
"type": "FOUR_DIGIT_PIN"
|
||||
}
|
||||
]
|
||||
}
|
||||
} as DiscoveryCapability
|
||||
);
|
||||
|
||||
displayCategories.push('SECURITY_PANEL');
|
||||
}
|
||||
|
||||
if (capabilities.length === 0)
|
||||
return;
|
||||
|
||||
return {
|
||||
displayCategories,
|
||||
capabilities
|
||||
}
|
||||
},
|
||||
async sendReport(eventSource: ScryptedDevice & SecuritySystem): Promise<Partial<Report>> {
|
||||
let data = {
|
||||
context: {
|
||||
properties: []
|
||||
}
|
||||
|
||||
} as Partial<StateReport>;
|
||||
|
||||
if (eventSource.interfaces.includes(ScryptedInterface.SecuritySystem)) {
|
||||
data.context.properties.push({
|
||||
"namespace": "Alexa.SecurityPanelController",
|
||||
"name": "armState",
|
||||
"value": getArmState(eventSource.securitySystemState.mode),
|
||||
"timeOfSample": new Date().toISOString(),
|
||||
"uncertaintyInMilliseconds": 0
|
||||
} as Property);
|
||||
|
||||
data.context.properties.push({
|
||||
"namespace": "Alexa.SecurityPanelController",
|
||||
"name": "burglaryAlarm",
|
||||
"value": {
|
||||
"value": eventSource.securitySystemState.triggered ? "ALARM" : "OK",
|
||||
},
|
||||
"timeOfSample": new Date().toISOString(),
|
||||
"uncertaintyInMilliseconds": 0
|
||||
} as Property);
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
async sendEvent(eventSource: ScryptedDevice & SecuritySystem, eventDetails: EventDetails, eventData): Promise<Partial<Report>> {
|
||||
if (eventDetails.eventInterface === ScryptedInterface.SecuritySystem && eventDetails.property === "mode") {
|
||||
return {
|
||||
event: {
|
||||
payload: {
|
||||
change: {
|
||||
cause: {
|
||||
type: "PHYSICAL_INTERACTION"
|
||||
},
|
||||
properties: [
|
||||
{
|
||||
"namespace": "Alexa.SecurityPanelController",
|
||||
"name": "armState",
|
||||
"value": getArmState(eventData),
|
||||
"timeOfSample": new Date().toISOString(),
|
||||
"uncertaintyInMilliseconds": 0
|
||||
} as Property
|
||||
]
|
||||
}
|
||||
} as ChangePayload,
|
||||
},
|
||||
context: {
|
||||
properties: [{
|
||||
"namespace": "Alexa.SecurityPanelController",
|
||||
"name": "burglaryAlarm",
|
||||
"value": {
|
||||
"value": eventSource.securitySystemState.triggered ? "ALARM" : "OK",
|
||||
},
|
||||
"timeOfSample": new Date().toISOString(),
|
||||
"uncertaintyInMilliseconds": 0
|
||||
} as Property]
|
||||
}
|
||||
} as Partial<ChangeReport>;
|
||||
}
|
||||
|
||||
if (eventDetails.eventInterface === ScryptedInterface.SecuritySystem && eventDetails.property === "triggered") {
|
||||
return {
|
||||
event: {
|
||||
payload: {
|
||||
change: {
|
||||
cause: {
|
||||
type: "RULE_TRIGGER"
|
||||
},
|
||||
properties: [
|
||||
{
|
||||
"namespace": "Alexa.SecurityPanelController",
|
||||
"name": "burglaryAlarm",
|
||||
"value": {
|
||||
"value": eventData ? "ALARM" : "OK"
|
||||
},
|
||||
"timeOfSample": new Date().toISOString(),
|
||||
"uncertaintyInMilliseconds": 0
|
||||
} as Property
|
||||
]
|
||||
}
|
||||
} as ChangePayload,
|
||||
},
|
||||
context: {
|
||||
properties: [{
|
||||
"namespace": "Alexa.SecurityPanelController",
|
||||
"name": "armState",
|
||||
"value": getArmState(eventSource.securitySystemState.mode),
|
||||
"timeOfSample": new Date().toISOString(),
|
||||
"uncertaintyInMilliseconds": 0
|
||||
} as Property]
|
||||
}
|
||||
} as Partial<ChangeReport>;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
196
plugins/alexa/src/types/sensor.ts
Normal file
196
plugins/alexa/src/types/sensor.ts
Normal file
@@ -0,0 +1,196 @@
|
||||
import { EntrySensor, MotionSensor, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, Thermometer } from "@scrypted/sdk";
|
||||
import { DiscoveryEndpoint, DiscoveryCapability, ChangeReport, Report, StateReport, DisplayCategory, ChangePayload, Property } from "../alexa";
|
||||
import { supportedTypes } from ".";
|
||||
|
||||
supportedTypes.set(ScryptedDeviceType.Sensor, {
|
||||
async discover(device: ScryptedDevice): Promise<Partial<DiscoveryEndpoint>> {
|
||||
const capabilities: DiscoveryCapability[] = [];
|
||||
const displayCategories: DisplayCategory[] = [];
|
||||
|
||||
if (device.interfaces.includes(ScryptedInterface.Thermometer)) {
|
||||
capabilities.push(
|
||||
{
|
||||
"type": "AlexaInterface",
|
||||
"interface": "Alexa.TemperatureSensor",
|
||||
"version": "3",
|
||||
"properties": {
|
||||
"supported": [
|
||||
{
|
||||
"name": "temperature"
|
||||
}
|
||||
],
|
||||
"proactivelyReported": true,
|
||||
"retrievable": true
|
||||
}
|
||||
} as DiscoveryCapability
|
||||
);
|
||||
|
||||
displayCategories.push('TEMPERATURE_SENSOR');
|
||||
}
|
||||
|
||||
if (device.interfaces.includes(ScryptedInterface.EntrySensor)) {
|
||||
capabilities.push(
|
||||
{
|
||||
"type": "AlexaInterface",
|
||||
"interface": "Alexa.ContactSensor",
|
||||
"version": "3",
|
||||
"properties": {
|
||||
"supported": [
|
||||
{
|
||||
"name": "detectionState"
|
||||
}
|
||||
],
|
||||
"proactivelyReported": true,
|
||||
"retrievable": true
|
||||
}
|
||||
} as DiscoveryCapability
|
||||
);
|
||||
|
||||
displayCategories.push('CONTACT_SENSOR');
|
||||
}
|
||||
|
||||
if (device.interfaces.includes(ScryptedInterface.MotionSensor)) {
|
||||
capabilities.push(
|
||||
{
|
||||
"type": "AlexaInterface",
|
||||
"interface": "Alexa.MotionSensor",
|
||||
"version": "3",
|
||||
"properties": {
|
||||
"supported": [
|
||||
{
|
||||
"name": "detectionState"
|
||||
}
|
||||
],
|
||||
"proactivelyReported": true,
|
||||
"retrievable": true
|
||||
}
|
||||
} as DiscoveryCapability
|
||||
);
|
||||
|
||||
displayCategories.push('MOTION_SENSOR');
|
||||
}
|
||||
|
||||
if (capabilities.length === 0)
|
||||
return;
|
||||
|
||||
return {
|
||||
displayCategories: displayCategories,
|
||||
capabilities
|
||||
}
|
||||
},
|
||||
async sendReport(eventSource: ScryptedDevice & MotionSensor & EntrySensor & Thermometer): Promise<Partial<Report>> {
|
||||
let data = {
|
||||
context: {
|
||||
properties: []
|
||||
}
|
||||
|
||||
} as Partial<StateReport>;
|
||||
|
||||
if (eventSource.interfaces.includes(ScryptedInterface.Thermometer)) {
|
||||
data.context.properties.push({
|
||||
"namespace": "Alexa.TemperatureSensor",
|
||||
"name": "temperature",
|
||||
"value": {
|
||||
"value": eventSource.temperature,
|
||||
"scale": "CELSIUS"
|
||||
},
|
||||
"timeOfSample": new Date().toISOString(),
|
||||
"uncertaintyInMilliseconds": 0
|
||||
});
|
||||
}
|
||||
|
||||
if (eventSource.interfaces.includes(ScryptedInterface.EntrySensor)) {
|
||||
data.context.properties.push({
|
||||
"namespace": "Alexa.ContactSensor",
|
||||
"name": "detectionState",
|
||||
"value": eventSource.entryOpen ? "DETECTED" : "NOT_DETECTED",
|
||||
"timeOfSample": new Date().toISOString(),
|
||||
"uncertaintyInMilliseconds": 0
|
||||
});
|
||||
}
|
||||
|
||||
if (eventSource.interfaces.includes(ScryptedInterface.MotionSensor)) {
|
||||
data.context.properties.push({
|
||||
"namespace": "Alexa.MotionSensor",
|
||||
"name": "detectionState",
|
||||
"value": eventSource.motionDetected ? "DETECTED" : "NOT_DETECTED",
|
||||
"timeOfSample": new Date().toISOString(),
|
||||
"uncertaintyInMilliseconds": 0
|
||||
});
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
async sendEvent(eventSource: ScryptedDevice & MotionSensor & EntrySensor & Thermometer, eventDetails, eventData): Promise<Partial<Report>> {
|
||||
if (eventDetails.eventInterface === ScryptedInterface.MotionSensor)
|
||||
return {
|
||||
event: {
|
||||
payload: {
|
||||
change: {
|
||||
cause: {
|
||||
type: "PHYSICAL_INTERACTION"
|
||||
},
|
||||
properties: [
|
||||
{
|
||||
"namespace": "Alexa.MotionSensor",
|
||||
"name": "detectionState",
|
||||
"value": eventData ? "DETECTED" : "NOT_DETECTED",
|
||||
"timeOfSample": new Date(eventDetails.eventTime).toISOString(),
|
||||
"uncertaintyInMilliseconds": 0
|
||||
} as Property
|
||||
]
|
||||
}
|
||||
} as ChangePayload,
|
||||
}
|
||||
} as Partial<ChangeReport>;
|
||||
|
||||
if (eventDetails.eventInterface === ScryptedInterface.EntrySensor)
|
||||
return {
|
||||
event: {
|
||||
payload: {
|
||||
change: {
|
||||
cause: {
|
||||
type: "PHYSICAL_INTERACTION"
|
||||
},
|
||||
properties: [
|
||||
{
|
||||
"namespace": "Alexa.ContactSensor",
|
||||
"name": "detectionState",
|
||||
"value": eventData ? "DETECTED" : "NOT_DETECTED",
|
||||
"timeOfSample": new Date(eventDetails.eventTime).toISOString(),
|
||||
"uncertaintyInMilliseconds": 0
|
||||
} as Property
|
||||
]
|
||||
}
|
||||
} as ChangePayload,
|
||||
}
|
||||
} as Partial<ChangeReport>;
|
||||
|
||||
if (eventDetails.eventInterface === ScryptedInterface.Thermometer)
|
||||
return {
|
||||
event: {
|
||||
payload: {
|
||||
change: {
|
||||
cause: {
|
||||
type: "PERIODIC_POLL"
|
||||
},
|
||||
properties: [
|
||||
{
|
||||
"namespace": "Alexa.TemperatureSensor",
|
||||
"name": "temperature",
|
||||
"value": {
|
||||
"value": eventSource.temperature,
|
||||
"scale": "CELSIUS"
|
||||
},
|
||||
"timeOfSample": new Date(eventDetails.eventTime).toISOString(),
|
||||
"uncertaintyInMilliseconds": 0
|
||||
} as Property
|
||||
]
|
||||
}
|
||||
} as ChangePayload,
|
||||
}
|
||||
} as Partial<ChangeReport>;
|
||||
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
71
plugins/alexa/src/types/switch.ts
Normal file
71
plugins/alexa/src/types/switch.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { OnOff, ScryptedDevice, ScryptedDeviceType, ScryptedInterface } from "@scrypted/sdk";
|
||||
import { DiscoveryEndpoint, ChangeReport, Report, Property, ChangePayload, DiscoveryCapability } from "../alexa";
|
||||
import { supportedTypes } from ".";
|
||||
|
||||
supportedTypes.set(ScryptedDeviceType.Switch, {
|
||||
async discover(device: ScryptedDevice): Promise<Partial<DiscoveryEndpoint>> {
|
||||
if (!device.interfaces.includes(ScryptedInterface.OnOff))
|
||||
return;
|
||||
|
||||
const capabilities: DiscoveryCapability[] = [];
|
||||
capabilities.push({
|
||||
"type": "AlexaInterface",
|
||||
"interface": "Alexa.PowerController",
|
||||
"version": "3",
|
||||
"properties": {
|
||||
"supported": [
|
||||
{
|
||||
"name": "powerState"
|
||||
}
|
||||
],
|
||||
"proactivelyReported": true,
|
||||
"retrievable": true
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
displayCategories: ['SWITCH'],
|
||||
capabilities
|
||||
}
|
||||
},
|
||||
async sendReport(eventSource: ScryptedDevice & OnOff): Promise<Partial<Report>> {
|
||||
return {
|
||||
context: {
|
||||
"properties": [
|
||||
{
|
||||
"namespace": "Alexa.PowerController",
|
||||
"name": "powerState",
|
||||
"value": eventSource.on ? "ON" : "OFF",
|
||||
"timeOfSample": new Date().toISOString(),
|
||||
"uncertaintyInMilliseconds": 0
|
||||
} as Property
|
||||
]
|
||||
}
|
||||
};
|
||||
},
|
||||
async sendEvent(eventSource: ScryptedDevice & OnOff, eventDetails, eventData): Promise<Partial<Report>> {
|
||||
if (eventDetails.eventInterface !== ScryptedInterface.OnOff)
|
||||
return undefined;
|
||||
|
||||
return {
|
||||
event: {
|
||||
payload: {
|
||||
change: {
|
||||
cause: {
|
||||
type: "PHYSICAL_INTERACTION"
|
||||
},
|
||||
properties: [
|
||||
{
|
||||
"namespace": "Alexa.PowerController",
|
||||
"name": "powerState",
|
||||
"value": eventData ? "ON" : "OFF",
|
||||
"timeOfSample": new Date().toISOString(),
|
||||
"uncertaintyInMilliseconds": 0
|
||||
} as Property
|
||||
]
|
||||
}
|
||||
} as ChangePayload,
|
||||
}
|
||||
} as Partial<ChangeReport>;
|
||||
}
|
||||
});
|
||||
55
plugins/alexa/src/types/switch/handlers.ts
Normal file
55
plugins/alexa/src/types/switch/handlers.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { OnOff, ScryptedDevice } from "@scrypted/sdk";
|
||||
import { supportedTypes } from "..";
|
||||
import { sendDeviceResponse } from "../../common";
|
||||
import { v4 as createMessageId } from 'uuid';
|
||||
import { alexaDeviceHandlers } from "../../handlers";
|
||||
import { Directive, Response } from "../../alexa";
|
||||
|
||||
function commonResponse(header, endpoint, payload, response, device: ScryptedDevice & OnOff) {
|
||||
const data : Response = {
|
||||
"event": {
|
||||
header,
|
||||
endpoint,
|
||||
payload
|
||||
},
|
||||
"context": {
|
||||
"properties": [
|
||||
{
|
||||
"namespace": "Alexa.PowerController",
|
||||
"name": "powerState",
|
||||
"value": device.on ? "ON" : "OFF",
|
||||
"timeOfSample": new Date().toISOString(),
|
||||
"uncertaintyInMilliseconds": 500
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
data.event.header.namespace = "Alexa";
|
||||
data.event.header.name = "Response";
|
||||
data.event.header.messageId = createMessageId();
|
||||
|
||||
sendDeviceResponse(data, response, device);
|
||||
}
|
||||
|
||||
alexaDeviceHandlers.set('Alexa.PowerController/TurnOn', async (request, response, directive: Directive, device: ScryptedDevice & OnOff) => {
|
||||
const supportedType = supportedTypes.get(device.type);
|
||||
if (!supportedType)
|
||||
return;
|
||||
|
||||
const { header, endpoint, payload } = directive;
|
||||
await device.turnOn();
|
||||
|
||||
commonResponse(header, endpoint, payload, response, device);
|
||||
});
|
||||
|
||||
alexaDeviceHandlers.set('Alexa.PowerController/TurnOff', async (request, response, directive: Directive, device: ScryptedDevice & OnOff) => {
|
||||
const supportedType = supportedTypes.get(device.type);
|
||||
if (!supportedType)
|
||||
return;
|
||||
|
||||
const { header, endpoint, payload } = directive;
|
||||
await device.turnOff();
|
||||
|
||||
commonResponse(header, endpoint, payload, response, device);
|
||||
});
|
||||
4
plugins/amcrest/package-lock.json
generated
4
plugins/amcrest/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/amcrest",
|
||||
"version": "0.0.115",
|
||||
"version": "0.0.119",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/amcrest",
|
||||
"version": "0.0.115",
|
||||
"version": "0.0.119",
|
||||
"license": "Apache",
|
||||
"dependencies": {
|
||||
"@koush/axios-digest-auth": "^0.8.5",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/amcrest",
|
||||
"version": "0.0.115",
|
||||
"version": "0.0.119",
|
||||
"description": "Amcrest Plugin for Scrypted",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache",
|
||||
|
||||
@@ -2,6 +2,7 @@ import AxiosDigestAuth from '@koush/axios-digest-auth';
|
||||
import { Readable } from 'stream';
|
||||
import https from 'https';
|
||||
import { IncomingMessage } from 'http';
|
||||
import { amcrestHttpsAgent, getDeviceInfo } from './probe';
|
||||
|
||||
export enum AmcrestEvent {
|
||||
MotionStart = "Code=VideoMotion;action=Start",
|
||||
@@ -17,12 +18,10 @@ export enum AmcrestEvent {
|
||||
PhoneCallDetectStop = "Code=PhoneCallDetect;action=Stop",
|
||||
DahuaTalkInvite = "Code=CallNoAnswered;action=Start",
|
||||
DahuaTalkHangup = "Code=PassiveHungup;action=Start",
|
||||
DahuaCallDeny = "Code=HungupPhone;action=Pulse",
|
||||
DahuaTalkPulse = "Code=_CallNoAnswer_;action=Pulse",
|
||||
}
|
||||
|
||||
export const amcrestHttpsAgent = new https.Agent({
|
||||
rejectUnauthorized: false,
|
||||
});
|
||||
|
||||
export class AmcrestCameraClient {
|
||||
digestAuth: AxiosDigestAuth;
|
||||
@@ -34,6 +33,16 @@ export class AmcrestCameraClient {
|
||||
});
|
||||
}
|
||||
|
||||
async checkTwoWayAudio() {
|
||||
const response = await this.digestAuth.request({
|
||||
httpsAgent: amcrestHttpsAgent,
|
||||
method: "GET",
|
||||
responseType: 'text',
|
||||
url: `http://${this.ip}/cgi-bin/devAudioOutput.cgi?action=getCollect`,
|
||||
});
|
||||
return (response.data as string).includes('result=1');
|
||||
}
|
||||
|
||||
// appAutoStart=true
|
||||
// deviceType=IP4M-1041B
|
||||
// hardwareVersion=1.00
|
||||
@@ -42,30 +51,7 @@ export class AmcrestCameraClient {
|
||||
// updateSerial=IPC-AW46WN-S2
|
||||
// updateSerialCloudUpgrade=IPC-AW46WN-.....
|
||||
async getDeviceInfo() {
|
||||
const response = await this.digestAuth.request({
|
||||
httpsAgent: amcrestHttpsAgent,
|
||||
method: "GET",
|
||||
responseType: 'text',
|
||||
url: `http://${this.ip}/cgi-bin/magicBox.cgi?action=getSystemInfo`,
|
||||
});
|
||||
const lines = (response.data as string).split('\n');
|
||||
const vals: {
|
||||
[key: string]: string,
|
||||
} = {};
|
||||
for (const line of lines) {
|
||||
let index = line.indexOf('=');
|
||||
if (index === -1)
|
||||
index = line.length;
|
||||
const k = line.substring(0, index);
|
||||
const v = line.substring(index + 1);
|
||||
vals[k] = v.trim();
|
||||
}
|
||||
|
||||
return {
|
||||
deviceType: vals.deviceType,
|
||||
hardwareVersion: vals.hardwareVersion,
|
||||
serialNumber: vals.serialNumber,
|
||||
}
|
||||
return getDeviceInfo(this.digestAuth, this.ip);
|
||||
}
|
||||
|
||||
async jpegSnapshot(): Promise<Buffer> {
|
||||
|
||||
@@ -5,7 +5,8 @@ import child_process, { ChildProcess } from 'child_process';
|
||||
import { PassThrough, Readable, Stream } from "stream";
|
||||
import { OnvifIntercom } from "../../onvif/src/onvif-intercom";
|
||||
import { RtspProvider, RtspSmartCamera, UrlMediaStreamOptions } from "../../rtsp/src/rtsp";
|
||||
import { AmcrestCameraClient, AmcrestEvent, amcrestHttpsAgent } from "./amcrest-api";
|
||||
import { AmcrestCameraClient, AmcrestEvent } from "./amcrest-api";
|
||||
import { amcrestHttpsAgent } from './probe';
|
||||
|
||||
const { mediaManager } = sdk;
|
||||
|
||||
@@ -37,19 +38,6 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
}
|
||||
|
||||
this.updateDeviceInfo();
|
||||
this.updateManagementUrl();
|
||||
}
|
||||
|
||||
updateManagementUrl() {
|
||||
const ip = this.storage.getItem('ip');
|
||||
if (!ip)
|
||||
return;
|
||||
const info = this.info || {};
|
||||
const managementUrl = `http://${ip}`;
|
||||
if (info.managementUrl !== managementUrl) {
|
||||
info.managementUrl = managementUrl;
|
||||
this.info = info;
|
||||
}
|
||||
}
|
||||
|
||||
getRecordingStreamCurrentTime(recordingStream: MediaObject): Promise<number> {
|
||||
@@ -80,9 +68,16 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
}
|
||||
|
||||
async updateDeviceInfo(): Promise<void> {
|
||||
if (this.info)
|
||||
const ip = this.storage.getItem('ip');
|
||||
if (!ip)
|
||||
return;
|
||||
const deviceInfo = {};
|
||||
|
||||
const managementUrl = `http://${ip}`;
|
||||
const deviceInfo: DeviceInformation = {
|
||||
...this.info,
|
||||
ip,
|
||||
managementUrl,
|
||||
};
|
||||
|
||||
const deviceParameters = [
|
||||
{ action: "getVendor", replace: "vendor=", parameter: "manufacturer" },
|
||||
@@ -161,6 +156,8 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
const client = new AmcrestCameraClient(this.getHttpAddress(), this.getUsername(), this.getPassword(), this.console);
|
||||
const events = await client.listenEvents();
|
||||
const doorbellType = this.storage.getItem('doorbellType');
|
||||
const callerId = this.storage.getItem('callerID');
|
||||
const multipleCallIds = this.storage.getItem('multipleCallIds') === 'true';
|
||||
|
||||
let pulseTimeout: NodeJS.Timeout;
|
||||
|
||||
@@ -187,11 +184,21 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
|| event === AmcrestEvent.PhoneCallDetectStart
|
||||
|| event === AmcrestEvent.AlarmIPCStart
|
||||
|| event === AmcrestEvent.DahuaTalkInvite) {
|
||||
this.binaryState = true;
|
||||
if (event === AmcrestEvent.DahuaTalkInvite && payload && multipleCallIds)
|
||||
{
|
||||
if (payload.includes(callerId))
|
||||
{
|
||||
this.binaryState = true;
|
||||
}
|
||||
} else
|
||||
{
|
||||
this.binaryState = true;
|
||||
}
|
||||
}
|
||||
else if (event === AmcrestEvent.TalkHangup
|
||||
|| event === AmcrestEvent.PhoneCallDetectStop
|
||||
|| event === AmcrestEvent.AlarmIPCStop
|
||||
|| event === AmcrestEvent.DahuaCallDeny
|
||||
|| event === AmcrestEvent.DahuaTalkHangup) {
|
||||
this.binaryState = false;
|
||||
}
|
||||
@@ -246,6 +253,36 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
|
||||
if (!twoWayAudio)
|
||||
twoWayAudio = isDoorbell ? 'Amcrest' : 'None';
|
||||
|
||||
|
||||
if (doorbellType == DAHUA_DOORBELL_TYPE)
|
||||
{
|
||||
ret.push(
|
||||
{
|
||||
title: 'Multiple Call Buttons',
|
||||
key: 'multipleCallIds',
|
||||
description: 'Some Dahua Doorbells integrate multiple Call Buttons for apartment buildings.',
|
||||
type: 'boolean',
|
||||
value: (this.storage.getItem('multipleCallIds') === 'true').toString(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const multipleCallIds = this.storage.getItem('multipleCallIds');
|
||||
|
||||
if (multipleCallIds)
|
||||
{
|
||||
ret.push(
|
||||
{
|
||||
title: 'Caller ID',
|
||||
key: 'callerID',
|
||||
description: 'Caller ID',
|
||||
type: 'number',
|
||||
value: this.storage.getItem('callerID'),
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
ret.push(
|
||||
{
|
||||
@@ -266,7 +303,11 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
);
|
||||
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
async takeSmartCameraPicture(option?: PictureOptions): Promise<MediaObject> {
|
||||
return this.createMediaObject(await this.getClient().jpegSnapshot(), 'image/jpeg');
|
||||
@@ -441,7 +482,7 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
interfaces.push(ScryptedInterface.VideoRecorder);
|
||||
this.provider.updateDevice(this.nativeId, this.name, interfaces, type);
|
||||
|
||||
this.updateManagementUrl();
|
||||
this.updateDeviceInfo();
|
||||
}
|
||||
|
||||
async startIntercom(media: MediaObject): Promise<void> {
|
||||
@@ -550,9 +591,10 @@ class AmcrestProvider extends RtspProvider {
|
||||
const username = settings.username?.toString();
|
||||
const password = settings.password?.toString();
|
||||
const skipValidate = settings.skipValidate === 'true';
|
||||
let twoWayAudio: string;
|
||||
if (!skipValidate) {
|
||||
const api = new AmcrestCameraClient(httpAddress, username, password, this.console);
|
||||
try {
|
||||
const api = new AmcrestCameraClient(httpAddress, username, password, this.console);
|
||||
const deviceInfo = await api.getDeviceInfo();
|
||||
|
||||
settings.newCamera = deviceInfo.deviceType;
|
||||
@@ -563,6 +605,16 @@ class AmcrestProvider extends RtspProvider {
|
||||
this.console.error('Error adding Amcrest camera', e);
|
||||
throw e;
|
||||
}
|
||||
|
||||
try {
|
||||
if (await api.checkTwoWayAudio()) {
|
||||
// onvif seems to work better than Amcrest, except for AD110.
|
||||
twoWayAudio = 'ONVIF';
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
this.console.warn('Error probing two way audio', e);
|
||||
}
|
||||
}
|
||||
settings.newCamera ||= 'Hikvision Camera';
|
||||
|
||||
@@ -574,6 +626,8 @@ class AmcrestProvider extends RtspProvider {
|
||||
device.putSetting('password', password);
|
||||
device.setIPAddress(settings.ip?.toString());
|
||||
device.setHttpPortOverride(settings.httpPort?.toString());
|
||||
if (twoWayAudio)
|
||||
device.putSetting('twoWayAudio', twoWayAudio);
|
||||
return nativeId;
|
||||
}
|
||||
|
||||
|
||||
42
plugins/amcrest/src/probe.ts
Normal file
42
plugins/amcrest/src/probe.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import https from 'https';
|
||||
|
||||
export const amcrestHttpsAgent = new https.Agent({
|
||||
rejectUnauthorized: false,
|
||||
});
|
||||
|
||||
// appAutoStart=true
|
||||
// deviceType=IP4M-1041B
|
||||
// hardwareVersion=1.00
|
||||
// processor=SSC327DE
|
||||
// serialNumber=12345
|
||||
// updateSerial=IPC-AW46WN-S2
|
||||
|
||||
import AxiosDigestAuth from "@koush/axios-digest-auth";
|
||||
|
||||
// updateSerialCloudUpgrade=IPC-AW46WN-.....
|
||||
export async function getDeviceInfo(digestAuth: AxiosDigestAuth, address: string) {
|
||||
const response = await digestAuth.request({
|
||||
httpsAgent: amcrestHttpsAgent,
|
||||
method: "GET",
|
||||
responseType: 'text',
|
||||
url: `http://${address}/cgi-bin/magicBox.cgi?action=getSystemInfo`,
|
||||
});
|
||||
const lines = (response.data as string).split('\n');
|
||||
const vals: {
|
||||
[key: string]: string,
|
||||
} = {};
|
||||
for (const line of lines) {
|
||||
let index = line.indexOf('=');
|
||||
if (index === -1)
|
||||
index = line.length;
|
||||
const k = line.substring(0, index);
|
||||
const v = line.substring(index + 1);
|
||||
vals[k] = v.trim();
|
||||
}
|
||||
|
||||
return {
|
||||
deviceType: vals.deviceType,
|
||||
hardwareVersion: vals.hardwareVersion,
|
||||
serialNumber: vals.serialNumber,
|
||||
}
|
||||
}
|
||||
6
plugins/arlo/package-lock.json
generated
6
plugins/arlo/package-lock.json
generated
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"name": "@scrypted/arlo",
|
||||
"version": "0.6.5",
|
||||
"version": "0.6.7",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/arlo",
|
||||
"version": "0.6.5",
|
||||
"version": "0.6.7",
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
}
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.2.63",
|
||||
"version": "0.2.78",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/arlo",
|
||||
"version": "0.6.5",
|
||||
"version": "0.6.7",
|
||||
"description": "Arlo Plugin for Scrypted",
|
||||
"keywords": [
|
||||
"scrypted",
|
||||
|
||||
@@ -27,7 +27,7 @@ from .request import Request
|
||||
from .mqtt_stream_async import MQTTStream
|
||||
from .sse_stream_async import EventStream
|
||||
from .logging import logger
|
||||
|
||||
|
||||
# Import all of the other stuff.
|
||||
from datetime import datetime
|
||||
|
||||
@@ -227,7 +227,7 @@ class Arlo(object):
|
||||
when subsequent calls to /notify are made.
|
||||
"""
|
||||
async def heartbeat(self, basestations, interval=30):
|
||||
while self.event_stream and self.event_stream.connected:
|
||||
while self.event_stream and self.event_stream.active:
|
||||
for basestation in basestations:
|
||||
try:
|
||||
self.Ping(basestation)
|
||||
@@ -378,7 +378,9 @@ class Arlo(object):
|
||||
return None
|
||||
return stop
|
||||
|
||||
return asyncio.get_event_loop().create_task(self.HandleEvents(basestation, resource, ['is'], callbackwrapper))
|
||||
return asyncio.get_event_loop().create_task(
|
||||
self.HandleEvents(basestation, resource, [('is', 'motionDetected')], callbackwrapper)
|
||||
)
|
||||
|
||||
def SubscribeToBatteryEvents(self, basestation, camera, callback):
|
||||
"""
|
||||
@@ -403,7 +405,9 @@ class Arlo(object):
|
||||
return None
|
||||
return stop
|
||||
|
||||
return asyncio.get_event_loop().create_task(self.HandleEvents(basestation, resource, ['is'], callbackwrapper))
|
||||
return asyncio.get_event_loop().create_task(
|
||||
self.HandleEvents(basestation, resource, [('is', 'batteryLevel')], callbackwrapper)
|
||||
)
|
||||
|
||||
def SubscribeToDoorbellEvents(self, basestation, doorbell, callback):
|
||||
"""
|
||||
@@ -437,7 +441,9 @@ class Arlo(object):
|
||||
return None
|
||||
return stop
|
||||
|
||||
return asyncio.get_event_loop().create_task(self.HandleEvents(basestation, resource, ['is'], callbackwrapper))
|
||||
return asyncio.get_event_loop().create_task(
|
||||
self.HandleEvents(basestation, resource, [('is', 'buttonPressed')], callbackwrapper)
|
||||
)
|
||||
|
||||
def SubscribeToSDPAnswers(self, basestation, camera, callback):
|
||||
"""
|
||||
@@ -456,14 +462,16 @@ class Arlo(object):
|
||||
|
||||
def callbackwrapper(self, event):
|
||||
properties = event.get("properties", {})
|
||||
stop = None
|
||||
stop = None
|
||||
if properties.get("type") == "answerSdp":
|
||||
stop = callback(properties.get("data"))
|
||||
if not stop:
|
||||
return None
|
||||
return stop
|
||||
|
||||
return asyncio.get_event_loop().create_task(self.HandleEvents(basestation, resource, ['pushToTalk'], callbackwrapper))
|
||||
return asyncio.get_event_loop().create_task(
|
||||
self.HandleEvents(basestation, resource, ['pushToTalk'], callbackwrapper)
|
||||
)
|
||||
|
||||
def SubscribeToCandidateAnswers(self, basestation, camera, callback):
|
||||
"""
|
||||
@@ -482,14 +490,16 @@ class Arlo(object):
|
||||
|
||||
def callbackwrapper(self, event):
|
||||
properties = event.get("properties", {})
|
||||
stop = None
|
||||
stop = None
|
||||
if properties.get("type") == "answerCandidate":
|
||||
stop = callback(properties.get("data"))
|
||||
if not stop:
|
||||
return None
|
||||
return stop
|
||||
|
||||
return asyncio.get_event_loop().create_task(self.HandleEvents(basestation, resource, ['pushToTalk'], callbackwrapper))
|
||||
return asyncio.get_event_loop().create_task(
|
||||
self.HandleEvents(basestation, resource, ['pushToTalk'], callbackwrapper)
|
||||
)
|
||||
|
||||
async def HandleEvents(self, basestation, resource, actions, callback):
|
||||
"""
|
||||
@@ -502,9 +512,17 @@ class Arlo(object):
|
||||
await self.Subscribe()
|
||||
|
||||
async def loop_action_listener(action):
|
||||
# in this function, action can either be a tuple or a string
|
||||
# if it is a tuple, we expect there to be a property key in the tuple
|
||||
property = None
|
||||
if isinstance(action, tuple):
|
||||
action, property = action
|
||||
if not isinstance(action, str):
|
||||
raise Exception('Actions must be either a tuple or a str')
|
||||
|
||||
seen_events = {}
|
||||
while self.event_stream.active:
|
||||
event, _ = await self.event_stream.get(resource, [action], seen_events)
|
||||
event, _ = await self.event_stream.get(resource, action, property, seen_events)
|
||||
|
||||
if event is None or self.event_stream is None \
|
||||
or self.event_stream.event_stream_stop_event.is_set():
|
||||
@@ -514,7 +532,7 @@ class Arlo(object):
|
||||
response = callback(self, event.item)
|
||||
|
||||
# always requeue so other listeners can see the event too
|
||||
self.event_stream.requeue(event, resource, action)
|
||||
self.event_stream.requeue(event, resource, action, property)
|
||||
|
||||
if response is not None:
|
||||
return response
|
||||
@@ -606,7 +624,13 @@ class Arlo(object):
|
||||
return nl.stream_url_dict['url'].replace("rtsp://", "rtsps://")
|
||||
return None
|
||||
|
||||
return await self.TriggerAndHandleEvent(basestation, resource, ["is"], trigger, callback)
|
||||
return await self.TriggerAndHandleEvent(
|
||||
basestation,
|
||||
resource,
|
||||
[("is", "activityState")],
|
||||
trigger,
|
||||
callback,
|
||||
)
|
||||
|
||||
def StartPushToTalk(self, basestation, camera):
|
||||
url = f'https://{self.BASE_URL}/hmsweb/users/devices/{self.user_id}_{camera.get("deviceId")}/pushtotalk'
|
||||
@@ -644,8 +668,6 @@ class Arlo(object):
|
||||
async def TriggerFullFrameSnapshot(self, basestation, camera):
|
||||
"""
|
||||
This function causes the camera to record a fullframe snapshot.
|
||||
The presignedFullFrameSnapshotUrl url is returned.
|
||||
Use DownloadSnapshot() to download the actual image file.
|
||||
"""
|
||||
resource = f"cameras/{camera.get('deviceId')}"
|
||||
|
||||
@@ -676,4 +698,14 @@ class Arlo(object):
|
||||
return url
|
||||
return None
|
||||
|
||||
return await self.TriggerAndHandleEvent(basestation, resource, ["fullFrameSnapshotAvailable", "lastImageSnapshotAvailable", "is"], trigger, callback)
|
||||
return await self.TriggerAndHandleEvent(
|
||||
basestation,
|
||||
resource,
|
||||
[
|
||||
(action, property)
|
||||
for action in ["fullFrameSnapshotAvailable", "lastImageSnapshotAvailable", "is"]
|
||||
for property in ["presignedFullFrameSnapshotUrl", "presignedLastImageUrl"]
|
||||
],
|
||||
trigger,
|
||||
callback,
|
||||
)
|
||||
|
||||
@@ -28,7 +28,7 @@ from .logging import logger
|
||||
|
||||
class Stream:
|
||||
"""This class provides a queue-based EventStream object."""
|
||||
def __init__(self, arlo, expire=10):
|
||||
def __init__(self, arlo, expire=5):
|
||||
self.event_stream = None
|
||||
self.initializing = True
|
||||
self.connected = False
|
||||
@@ -43,7 +43,7 @@ class Stream:
|
||||
self.event_loop = asyncio.get_event_loop()
|
||||
self.event_loop.create_task(self._clean_queues())
|
||||
self.event_loop.create_task(self._refresh_interval())
|
||||
|
||||
|
||||
def __del__(self):
|
||||
self.disconnect()
|
||||
|
||||
@@ -83,11 +83,16 @@ class Stream:
|
||||
self.refresh_loop_signal.put_nowait(object())
|
||||
|
||||
async def _clean_queues(self):
|
||||
interval = self.expire * 2
|
||||
interval = self.expire * 4
|
||||
|
||||
await asyncio.sleep(interval)
|
||||
while not self.event_stream_stop_event.is_set():
|
||||
for key, q in self.queues.items():
|
||||
# since we interrupt the cleanup loop after every queue, there's
|
||||
# a chance the self.queues dict is modified during iteration.
|
||||
# so, we first make a copy of all the items of the dict and any
|
||||
# new queues will be processed on the next loop through
|
||||
queue_items = [i for i in self.queues.items()]
|
||||
for key, q in queue_items:
|
||||
if q.empty():
|
||||
continue
|
||||
|
||||
@@ -114,81 +119,47 @@ class Stream:
|
||||
if num_dropped > 0:
|
||||
logger.debug(f"Cleaned {num_dropped} events from queue {key}")
|
||||
|
||||
# cleanup is not urgent, so give other tasks a chance
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
await asyncio.sleep(interval)
|
||||
|
||||
async def get(self, resource, actions, skip_uuids={}):
|
||||
if len(actions) == 1:
|
||||
action = actions[0]
|
||||
async def get(self, resource, action, property=None, skip_uuids={}):
|
||||
if not property:
|
||||
key = f"{resource}/{action}"
|
||||
|
||||
if key not in self.queues:
|
||||
q = self.queues[key] = asyncio.Queue()
|
||||
else:
|
||||
q = self.queues[key]
|
||||
|
||||
first_requeued = None
|
||||
while True:
|
||||
event = await q.get()
|
||||
q.task_done()
|
||||
|
||||
if not event:
|
||||
# exit signal received
|
||||
return None, action
|
||||
|
||||
if first_requeued is not None and first_requeued is event:
|
||||
# if we reach here, we've cycled through the whole queue
|
||||
# and found nothing for us, so sleep and give the next
|
||||
# subscriber a chance
|
||||
q.put_nowait(event)
|
||||
await asyncio.sleep(random.uniform(0, 0.01))
|
||||
continue
|
||||
|
||||
if event.expired:
|
||||
continue
|
||||
elif event.uuid in skip_uuids:
|
||||
q.put_nowait(event)
|
||||
if first_requeued is None:
|
||||
first_requeued = event
|
||||
else:
|
||||
return event, action
|
||||
else:
|
||||
while True:
|
||||
for action in actions:
|
||||
key = f"{resource}/{action}"
|
||||
key = f"{resource}/{action}/{property}"
|
||||
|
||||
if key not in self.queues:
|
||||
q = self.queues[key] = asyncio.Queue()
|
||||
else:
|
||||
q = self.queues[key]
|
||||
if key not in self.queues:
|
||||
q = self.queues[key] = asyncio.Queue()
|
||||
else:
|
||||
q = self.queues[key]
|
||||
|
||||
if q.empty():
|
||||
continue
|
||||
first_requeued = None
|
||||
while True:
|
||||
event = await q.get()
|
||||
q.task_done()
|
||||
|
||||
first_requeued = None
|
||||
while not q.empty():
|
||||
event = q.get_nowait()
|
||||
q.task_done()
|
||||
if not event:
|
||||
# exit signal received
|
||||
return None, action
|
||||
|
||||
if not event:
|
||||
# exit signal received
|
||||
return None, action
|
||||
|
||||
if first_requeued is not None and first_requeued is event:
|
||||
# if we reach here, we've cycled through the whole queue
|
||||
# and found nothing for us, so go to the next queue
|
||||
q.put_nowait(event)
|
||||
break
|
||||
|
||||
if event.expired:
|
||||
continue
|
||||
elif event.uuid in skip_uuids:
|
||||
q.put_nowait(event)
|
||||
|
||||
if first_requeued is None:
|
||||
first_requeued = event
|
||||
else:
|
||||
return event, action
|
||||
if first_requeued is not None and first_requeued is event:
|
||||
# if we reach here, we've cycled through the whole queue
|
||||
# and found nothing for us, so sleep and give the next
|
||||
# subscriber a chance
|
||||
q.put_nowait(event)
|
||||
await asyncio.sleep(random.uniform(0, 0.01))
|
||||
continue
|
||||
|
||||
if event.expired:
|
||||
continue
|
||||
elif event.uuid in skip_uuids:
|
||||
q.put_nowait(event)
|
||||
if first_requeued is None:
|
||||
first_requeued = event
|
||||
else:
|
||||
return event, action
|
||||
|
||||
async def start(self):
|
||||
raise NotImplementedError()
|
||||
@@ -203,15 +174,31 @@ class Stream:
|
||||
resource = response.get('resource')
|
||||
action = response.get('action')
|
||||
key = f"{resource}/{action}"
|
||||
|
||||
now = time.time()
|
||||
event = StreamEvent(response, now, now + self.expire)
|
||||
|
||||
if key not in self.queues:
|
||||
q = self.queues[key] = asyncio.Queue()
|
||||
else:
|
||||
q = self.queues[key]
|
||||
now = time.time()
|
||||
q.put_nowait(StreamEvent(response, now, now + self.expire))
|
||||
q.put_nowait(event)
|
||||
|
||||
def requeue(self, event, resource, action):
|
||||
key = f"{resource}/{action}"
|
||||
# for optimized lookups, notify listeners of individual properties
|
||||
properties = response.get('properties', {})
|
||||
for property in properties.keys():
|
||||
key = f"{resource}/{action}/{property}"
|
||||
if key not in self.queues:
|
||||
q = self.queues[key] = asyncio.Queue()
|
||||
else:
|
||||
q = self.queues[key]
|
||||
q.put_nowait(event)
|
||||
|
||||
def requeue(self, event, resource, action, property=None):
|
||||
if not property:
|
||||
key = f"{resource}/{action}"
|
||||
else:
|
||||
key = f"{resource}/{action}/{property}"
|
||||
self.queues[key].put_nowait(event)
|
||||
|
||||
def disconnect(self):
|
||||
|
||||
307
plugins/cloud/package-lock.json
generated
307
plugins/cloud/package-lock.json
generated
@@ -1,26 +1,28 @@
|
||||
{
|
||||
"name": "@scrypted/cloud",
|
||||
"version": "0.1.11",
|
||||
"version": "0.1.13",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/cloud",
|
||||
"version": "0.1.11",
|
||||
"version": "0.1.13",
|
||||
"dependencies": {
|
||||
"@eneris/push-receiver": "../../external/push-receiver",
|
||||
"@eneris/push-receiver": "^3.1.4",
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"axios": "^0.25.0",
|
||||
"bpmux": "^8.1.3",
|
||||
"debug": "^4.3.1",
|
||||
"http-proxy": "^1.18.1",
|
||||
"lodash": "^4.17.21",
|
||||
"nat-upnp": "file:./node-nat-upnp",
|
||||
"query-string": "^6.14.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/debug": "^4.1.5",
|
||||
"@types/http-proxy": "^1.17.5",
|
||||
"@types/lodash": "^4.14.191",
|
||||
"@types/nat-upnp": "^1.1.2",
|
||||
"@types/node": "^18.11.18"
|
||||
}
|
||||
@@ -40,39 +42,9 @@
|
||||
"@types/node": "^16.9.0"
|
||||
}
|
||||
},
|
||||
"../../external/push-receiver": {
|
||||
"name": "@eneris/push-receiver",
|
||||
"version": "3.0.2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^0.27.1",
|
||||
"http_ece": "^1.0.5",
|
||||
"long": "^5.2.0",
|
||||
"protobufjs": "^6.11.2",
|
||||
"request-promise": "^4.2.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^28.1.0",
|
||||
"@types/long": "^4.0.1",
|
||||
"@types/node": "^17.0.29",
|
||||
"@typescript-eslint/eslint-plugin": "^5.21.0",
|
||||
"@typescript-eslint/parser": "^5.21.0",
|
||||
"eslint": "^8.14.0",
|
||||
"eslint-plugin-jest": "^26.4.6",
|
||||
"http-proxy": "^1.16.2",
|
||||
"husky": "^7.0.4",
|
||||
"jest": "^28.0.2",
|
||||
"ts-jest": "^28.0.4",
|
||||
"typescript": "^4.4.3",
|
||||
"yargs": "^17.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.2.56",
|
||||
"version": "0.2.82",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
@@ -108,8 +80,82 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eneris/push-receiver": {
|
||||
"resolved": "../../external/push-receiver",
|
||||
"link": true
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@eneris/push-receiver/-/push-receiver-3.1.4.tgz",
|
||||
"integrity": "sha512-KgSydrAmPwcc/xpvRmkvImUMts8uDl+4sUaGypPmD/kn3jhGuDVjzqhnxbSbdycm61rHZRM8NhUZrYUTEZgYlg==",
|
||||
"dependencies": {
|
||||
"axios": "^1.2.1",
|
||||
"http_ece": "^1.0.5",
|
||||
"long": "^5.2.1",
|
||||
"protobufjs": "^7.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@eneris/push-receiver/node_modules/axios": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.3.4.tgz",
|
||||
"integrity": "sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.0",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@protobufjs/aspromise": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
|
||||
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="
|
||||
},
|
||||
"node_modules/@protobufjs/base64": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
|
||||
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="
|
||||
},
|
||||
"node_modules/@protobufjs/codegen": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
|
||||
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="
|
||||
},
|
||||
"node_modules/@protobufjs/eventemitter": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
|
||||
"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="
|
||||
},
|
||||
"node_modules/@protobufjs/fetch": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
|
||||
"integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
|
||||
"dependencies": {
|
||||
"@protobufjs/aspromise": "^1.1.1",
|
||||
"@protobufjs/inquire": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@protobufjs/float": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
|
||||
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="
|
||||
},
|
||||
"node_modules/@protobufjs/inquire": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
|
||||
"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="
|
||||
},
|
||||
"node_modules/@protobufjs/path": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
|
||||
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="
|
||||
},
|
||||
"node_modules/@protobufjs/pool": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
|
||||
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="
|
||||
},
|
||||
"node_modules/@protobufjs/utf8": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
|
||||
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
|
||||
},
|
||||
"node_modules/@scrypted/common": {
|
||||
"resolved": "../../common",
|
||||
@@ -137,6 +183,12 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/lodash": {
|
||||
"version": "4.14.191",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz",
|
||||
"integrity": "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/ms": {
|
||||
"version": "0.7.31",
|
||||
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz",
|
||||
@@ -155,8 +207,7 @@
|
||||
"node_modules/@types/node": {
|
||||
"version": "18.11.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
|
||||
"integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA=="
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "1.1.1",
|
||||
@@ -730,6 +781,17 @@
|
||||
"he": "bin/he"
|
||||
}
|
||||
},
|
||||
"node_modules/http_ece": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/http_ece/-/http_ece-1.1.0.tgz",
|
||||
"integrity": "sha512-bptAfCDdPJxOs5zYSe7Y3lpr772s1G346R4Td5LgRUeCwIGpCGDUTJxRrhTNcAXbx37spge0kWEIH7QAYWNTlA==",
|
||||
"dependencies": {
|
||||
"urlsafe-base64": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/http-proxy": {
|
||||
"version": "1.18.1",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
|
||||
@@ -1160,6 +1222,11 @@
|
||||
"integrity": "sha512-L4/arjjuq4noiUJpt3yS6KIKDtJwNe2fIYgMqyYYKoeIfV1iEqvPwhCx23o+R9dzouGihDAPN1dTIRWa7zk8tw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/long": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz",
|
||||
"integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A=="
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
@@ -1432,6 +1499,29 @@
|
||||
"node": ">= 0.6.6"
|
||||
}
|
||||
},
|
||||
"node_modules/protobufjs": {
|
||||
"version": "7.2.2",
|
||||
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.2.tgz",
|
||||
"integrity": "sha512-++PrQIjrom+bFDPpfmqXfAGSQs40116JRrqqyf53dymUMvvb5d/LMRyicRoF1AUKoXVS1/IgJXlEgcpr4gTF3Q==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@protobufjs/aspromise": "^1.1.2",
|
||||
"@protobufjs/base64": "^1.1.2",
|
||||
"@protobufjs/codegen": "^2.0.4",
|
||||
"@protobufjs/eventemitter": "^1.1.0",
|
||||
"@protobufjs/fetch": "^1.1.0",
|
||||
"@protobufjs/float": "^1.0.2",
|
||||
"@protobufjs/inquire": "^1.1.0",
|
||||
"@protobufjs/path": "^1.1.2",
|
||||
"@protobufjs/pool": "^1.1.0",
|
||||
"@protobufjs/utf8": "^1.1.0",
|
||||
"@types/node": ">=13.7.0",
|
||||
"long": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
@@ -1636,6 +1726,11 @@
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/urlsafe-base64": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/urlsafe-base64/-/urlsafe-base64-1.0.0.tgz",
|
||||
"integrity": "sha512-RtuPeMy7c1UrHwproMZN9gN6kiZ0SvJwRaEzwZY0j9MypEkFqyBaKv176jvlPtg58Zh36bOkS0NFABXMHvvGCA=="
|
||||
},
|
||||
"node_modules/utile": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/utile/-/utile-0.2.1.tgz",
|
||||
@@ -1889,28 +1984,82 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@eneris/push-receiver": {
|
||||
"version": "file:../../external/push-receiver",
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@eneris/push-receiver/-/push-receiver-3.1.4.tgz",
|
||||
"integrity": "sha512-KgSydrAmPwcc/xpvRmkvImUMts8uDl+4sUaGypPmD/kn3jhGuDVjzqhnxbSbdycm61rHZRM8NhUZrYUTEZgYlg==",
|
||||
"requires": {
|
||||
"@types/jest": "^28.1.0",
|
||||
"@types/long": "^4.0.1",
|
||||
"@types/node": "^17.0.29",
|
||||
"@typescript-eslint/eslint-plugin": "^5.21.0",
|
||||
"@typescript-eslint/parser": "^5.21.0",
|
||||
"axios": "^0.27.1",
|
||||
"eslint": "^8.14.0",
|
||||
"eslint-plugin-jest": "^26.4.6",
|
||||
"axios": "^1.2.1",
|
||||
"http_ece": "^1.0.5",
|
||||
"http-proxy": "^1.16.2",
|
||||
"husky": "^7.0.4",
|
||||
"jest": "^28.0.2",
|
||||
"long": "^5.2.0",
|
||||
"protobufjs": "^6.11.2",
|
||||
"request-promise": "^4.2.6",
|
||||
"ts-jest": "^28.0.4",
|
||||
"typescript": "^4.4.3",
|
||||
"yargs": "^17.2.1"
|
||||
"long": "^5.2.1",
|
||||
"protobufjs": "^7.1.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.3.4.tgz",
|
||||
"integrity": "sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==",
|
||||
"requires": {
|
||||
"follow-redirects": "^1.15.0",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@protobufjs/aspromise": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
|
||||
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="
|
||||
},
|
||||
"@protobufjs/base64": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
|
||||
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="
|
||||
},
|
||||
"@protobufjs/codegen": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
|
||||
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="
|
||||
},
|
||||
"@protobufjs/eventemitter": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
|
||||
"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="
|
||||
},
|
||||
"@protobufjs/fetch": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
|
||||
"integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
|
||||
"requires": {
|
||||
"@protobufjs/aspromise": "^1.1.1",
|
||||
"@protobufjs/inquire": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"@protobufjs/float": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
|
||||
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="
|
||||
},
|
||||
"@protobufjs/inquire": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
|
||||
"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="
|
||||
},
|
||||
"@protobufjs/path": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
|
||||
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="
|
||||
},
|
||||
"@protobufjs/pool": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
|
||||
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="
|
||||
},
|
||||
"@protobufjs/utf8": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
|
||||
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
|
||||
},
|
||||
"@scrypted/common": {
|
||||
"version": "file:../../common",
|
||||
"requires": {
|
||||
@@ -1964,6 +2113,12 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/lodash": {
|
||||
"version": "4.14.191",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz",
|
||||
"integrity": "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/ms": {
|
||||
"version": "0.7.31",
|
||||
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz",
|
||||
@@ -1982,8 +2137,7 @@
|
||||
"@types/node": {
|
||||
"version": "18.11.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
|
||||
"integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA=="
|
||||
},
|
||||
"ansi-regex": {
|
||||
"version": "1.1.1",
|
||||
@@ -2399,6 +2553,14 @@
|
||||
"integrity": "sha512-z/GDPjlRMNOa2XJiB4em8wJpuuBfrFOlYKTZxtpkdr1uPdibHI8rYA3MY0KDObpVyaes0e/aunid/t88ZI2EKA==",
|
||||
"dev": true
|
||||
},
|
||||
"http_ece": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/http_ece/-/http_ece-1.1.0.tgz",
|
||||
"integrity": "sha512-bptAfCDdPJxOs5zYSe7Y3lpr772s1G346R4Td5LgRUeCwIGpCGDUTJxRrhTNcAXbx37spge0kWEIH7QAYWNTlA==",
|
||||
"requires": {
|
||||
"urlsafe-base64": "~1.0.0"
|
||||
}
|
||||
},
|
||||
"http-proxy": {
|
||||
"version": "1.18.1",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
|
||||
@@ -2737,6 +2899,11 @@
|
||||
"integrity": "sha512-L4/arjjuq4noiUJpt3yS6KIKDtJwNe2fIYgMqyYYKoeIfV1iEqvPwhCx23o+R9dzouGihDAPN1dTIRWa7zk8tw==",
|
||||
"dev": true
|
||||
},
|
||||
"long": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz",
|
||||
"integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A=="
|
||||
},
|
||||
"mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
@@ -2966,6 +3133,25 @@
|
||||
"winston": "0.8.x"
|
||||
}
|
||||
},
|
||||
"protobufjs": {
|
||||
"version": "7.2.2",
|
||||
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.2.tgz",
|
||||
"integrity": "sha512-++PrQIjrom+bFDPpfmqXfAGSQs40116JRrqqyf53dymUMvvb5d/LMRyicRoF1AUKoXVS1/IgJXlEgcpr4gTF3Q==",
|
||||
"requires": {
|
||||
"@protobufjs/aspromise": "^1.1.2",
|
||||
"@protobufjs/base64": "^1.1.2",
|
||||
"@protobufjs/codegen": "^2.0.4",
|
||||
"@protobufjs/eventemitter": "^1.1.0",
|
||||
"@protobufjs/fetch": "^1.1.0",
|
||||
"@protobufjs/float": "^1.0.2",
|
||||
"@protobufjs/inquire": "^1.1.0",
|
||||
"@protobufjs/path": "^1.1.2",
|
||||
"@protobufjs/pool": "^1.1.0",
|
||||
"@protobufjs/utf8": "^1.1.0",
|
||||
"@types/node": ">=13.7.0",
|
||||
"long": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
@@ -3109,6 +3295,11 @@
|
||||
"integrity": "sha512-OHbMkscHFRcNWEcW80fYhCrzAjheSIBwJChpFaBqA6zEz53nxumqi6ukciRb/UA0/v2nDNMk28ce/uBbYRDsng==",
|
||||
"dev": true
|
||||
},
|
||||
"urlsafe-base64": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/urlsafe-base64/-/urlsafe-base64-1.0.0.tgz",
|
||||
"integrity": "sha512-RtuPeMy7c1UrHwproMZN9gN6kiZ0SvJwRaEzwZY0j9MypEkFqyBaKv176jvlPtg58Zh36bOkS0NFABXMHvvGCA=="
|
||||
},
|
||||
"utile": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/utile/-/utile-0.2.1.tgz",
|
||||
|
||||
@@ -37,21 +37,23 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@eneris/push-receiver": "^3.1.4",
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"@eneris/push-receiver": "../../external/push-receiver",
|
||||
"axios": "^0.25.0",
|
||||
"bpmux": "^8.1.3",
|
||||
"debug": "^4.3.1",
|
||||
"http-proxy": "^1.18.1",
|
||||
"lodash": "^4.17.21",
|
||||
"nat-upnp": "file:./node-nat-upnp",
|
||||
"query-string": "^6.14.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/debug": "^4.1.5",
|
||||
"@types/http-proxy": "^1.17.5",
|
||||
"@types/lodash": "^4.14.191",
|
||||
"@types/nat-upnp": "^1.1.2",
|
||||
"@types/node": "^18.11.18"
|
||||
},
|
||||
"version": "0.1.11"
|
||||
"version": "0.1.13"
|
||||
}
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import sdk, { BufferConverter, DeviceProvider, HttpRequest, HttpRequestHandler, HttpResponse, OauthClient, PushHandler, ScryptedDevice, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, Settings } from "@scrypted/sdk";
|
||||
import sdk, { BufferConverter, DeviceProvider, HttpRequest, HttpRequestHandler, HttpResponse, OauthClient, PushHandler, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, Settings } from "@scrypted/sdk";
|
||||
import { StorageSettings } from "@scrypted/sdk/storage-settings";
|
||||
import axios from 'axios';
|
||||
import bpmux from 'bpmux';
|
||||
import crypto from 'crypto';
|
||||
import { once } from 'events';
|
||||
import http from 'http';
|
||||
import https from 'https';
|
||||
import HttpProxy from 'http-proxy';
|
||||
import https from 'https';
|
||||
import throttle from "lodash/throttle";
|
||||
import upnp from 'nat-upnp';
|
||||
import net from 'net';
|
||||
import net, { AddressInfo } from 'net';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import qs from 'query-string';
|
||||
import { Duplex } from 'stream';
|
||||
import tls from 'tls';
|
||||
import Url from 'url';
|
||||
import type { CORSControlLegacy } from '../../../server/src/services/cors';
|
||||
import { createSelfSignedCertificate } from '../../../server/src/cert';
|
||||
import { PushManager } from './push';
|
||||
import tls from 'tls';
|
||||
|
||||
const { deviceManager, endpointManager, systemManager } = sdk;
|
||||
|
||||
@@ -547,6 +547,8 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
};
|
||||
|
||||
const handler = async (req: http.IncomingMessage, res: http.ServerResponse) => {
|
||||
this.console.log(req.socket?.remoteAddress, req.url);
|
||||
|
||||
const url = Url.parse(req.url);
|
||||
if (url.path.startsWith('/web/oauth/callback') && url.query) {
|
||||
const query = qs.parse(url.query);
|
||||
@@ -620,7 +622,7 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
});
|
||||
this.proxy.on('error', () => { });
|
||||
this.proxy.on('proxyRes', (res, req) => {
|
||||
res.headers['X-Scrypted-Cloud'] = 'true';
|
||||
res.headers['X-Scrypted-Cloud'] = req.headers['x-scrypted-cloud'];
|
||||
res.headers['X-Scrypted-Direct-Address'] = req.headers['x-scrypted-direct-address'];
|
||||
res.headers['Access-Control-Expose-Headers'] = 'X-Scrypted-Cloud, X-Scrypted-Direct-Address';
|
||||
});
|
||||
|
||||
4
plugins/core/package-lock.json
generated
4
plugins/core/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.1.94",
|
||||
"version": "0.1.102",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.1.94",
|
||||
"version": "0.1.102",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.1.94",
|
||||
"version": "0.1.102",
|
||||
"description": "Scrypted Core plugin. Provides the UI, websocket, and engine.io APIs.",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -47,12 +47,11 @@ export class Scheduler {
|
||||
throw new Error('sunrise/sunset clock not supported');
|
||||
}
|
||||
|
||||
|
||||
|
||||
const ret: ScryptedDevice = {
|
||||
async setName() { },
|
||||
async setType() { },
|
||||
async setRoom() { },
|
||||
async setMixins() { },
|
||||
async probe() { return true },
|
||||
listen(event: EventListenerOptions, callback, source?: ScryptedDeviceBase) {
|
||||
function reschedule(): Date {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { BufferConverter, BufferConvertorOptions, HttpRequest, HttpRequestHandler, HttpResponse, HttpResponseOptions, MediaObject, RequestMediaObject, ScryptedDeviceBase, ScryptedMimeTypes } from "@scrypted/sdk";
|
||||
import sdk from "@scrypted/sdk";
|
||||
import sdk, { BufferConverter, HttpRequest, HttpRequestHandler, HttpResponse, HttpResponseOptions, MediaObject, RequestMediaObject, ScryptedDeviceBase, ScryptedMimeTypes } from "@scrypted/sdk";
|
||||
import crypto from 'crypto';
|
||||
import mime from "mime/lite";
|
||||
import path from 'path';
|
||||
import crypto from 'crypto';
|
||||
|
||||
const { endpointManager } = sdk;
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { tsCompile } from '@scrypted/common/src/eval/scrypted-eval';
|
||||
import sdk, { DeviceProvider, EngineIOHandler, HttpRequest, HttpRequestHandler, HttpResponse, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, Setting, Settings, SettingValue } from '@scrypted/sdk';
|
||||
import { StorageSettings } from "@scrypted/sdk/storage-settings";
|
||||
import fs from 'fs';
|
||||
@@ -237,3 +238,9 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Eng
|
||||
}
|
||||
|
||||
export default ScryptedCore;
|
||||
|
||||
export async function fork() {
|
||||
return {
|
||||
tsCompile,
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,13 @@ import { scryptedEval } from "./scrypted-eval";
|
||||
import { monacoEvalDefaults } from "./monaco";
|
||||
import { createScriptDevice, ScriptDeviceImpl } from "@scrypted/common/src/eval/scrypted-eval";
|
||||
import { ScriptCoreNativeId } from "./script-core";
|
||||
import { PluginAPIProxy } from "../../../server/src/plugin/plugin-api";
|
||||
|
||||
const { log, deviceManager, systemManager } = sdk;
|
||||
|
||||
export class Script extends ScryptedDeviceBase implements Scriptable, Program, ScriptDeviceImpl {
|
||||
apiProxy: PluginAPIProxy;
|
||||
|
||||
constructor(nativeId: string) {
|
||||
super(nativeId);
|
||||
}
|
||||
@@ -67,6 +70,8 @@ export class Script extends ScryptedDeviceBase implements Scriptable, Program, S
|
||||
}
|
||||
|
||||
prepareScript() {
|
||||
this.apiProxy?.removeListeners();
|
||||
|
||||
Object.assign(this, createScriptDevice([
|
||||
ScryptedInterface.Scriptable,
|
||||
ScryptedInterface.Program,
|
||||
@@ -79,10 +84,12 @@ export class Script extends ScryptedDeviceBase implements Scriptable, Program, S
|
||||
try {
|
||||
const data = JSON.parse(this.storage.getItem('data'));
|
||||
|
||||
const { value, defaultExport } = await scryptedEval(this, data['script.ts'], Object.assign({
|
||||
const { value, defaultExport, apiProxy } = await scryptedEval(this, data['script.ts'], Object.assign({
|
||||
device: this,
|
||||
}, variables));
|
||||
|
||||
this.apiProxy = apiProxy;
|
||||
|
||||
await this.postRunScript(defaultExport);
|
||||
return value;
|
||||
}
|
||||
@@ -95,10 +102,12 @@ export class Script extends ScryptedDeviceBase implements Scriptable, Program, S
|
||||
async eval(source: ScriptSource, variables?: { [name: string]: any }) {
|
||||
this.prepareScript();
|
||||
|
||||
const { value, defaultExport } = await scryptedEval(this, source.script, Object.assign({
|
||||
const { value, defaultExport, apiProxy } = await scryptedEval(this, source.script, Object.assign({
|
||||
device: this,
|
||||
}, variables));
|
||||
|
||||
this.apiProxy = apiProxy;
|
||||
|
||||
await this.postRunScript(defaultExport);
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -3,14 +3,20 @@ import { addAccessControlsForInterface } from "@scrypted/sdk/acl";
|
||||
import { StorageSettings } from "@scrypted/sdk/storage-settings";
|
||||
export const UsersNativeId = 'users';
|
||||
|
||||
type DBUser = { username: string, aclId: string };
|
||||
type DBUser = { username: string, admin: boolean };
|
||||
|
||||
export class User extends ScryptedDeviceBase implements Settings, ScryptedUser {
|
||||
storageSettings = new StorageSettings(this, {
|
||||
devices: {
|
||||
title: 'Devices',
|
||||
description: 'The devices this user can access. Admin users can access all devices. Scrypted NVR users should use NVR Permissions to grant access to the NVR and associated cameras.',
|
||||
type: 'device',
|
||||
defaultAccess: {
|
||||
title: 'Default Access',
|
||||
description: 'Grant access to @scrypted/core and @scrypted/webrtc',
|
||||
defaultValue: true,
|
||||
type: 'boolean',
|
||||
},
|
||||
interfaces: {
|
||||
title: 'Interfaces',
|
||||
description: 'The interfaces this user can access. Admin users can access all interfaces on all devices. Scrypted NVR users should use NVR Permissions to grant access to the NVR and associated cameras.',
|
||||
type: 'interface',
|
||||
multiple: true,
|
||||
defaultValue: [],
|
||||
},
|
||||
@@ -19,26 +25,27 @@ export class User extends ScryptedDeviceBase implements Settings, ScryptedUser {
|
||||
async getScryptedUserAccessControl(): Promise<ScryptedUserAccessControl> {
|
||||
const self = sdk.deviceManager.getDeviceState(this.nativeId);
|
||||
|
||||
const ret: ScryptedUserAccessControl = {
|
||||
const ret: ScryptedUserAccessControl = {
|
||||
devicesAccessControls: [
|
||||
addAccessControlsForInterface(self.id, ScryptedInterface.ScryptedDevice),
|
||||
addAccessControlsForInterface(sdk.systemManager.getDeviceByName('@scrypted/webrtc').id,
|
||||
ScryptedInterface.ScryptedDevice,
|
||||
ScryptedInterface.EngineIOHandler),
|
||||
addAccessControlsForInterface(sdk.systemManager.getDeviceByName('@scrypted/core').id,
|
||||
ScryptedInterface.ScryptedDevice,
|
||||
ScryptedInterface.EngineIOHandler),
|
||||
...this.storageSettings.values.devices.map((id: string) => ({
|
||||
id,
|
||||
})),
|
||||
...this.storageSettings.values.defaultAccess
|
||||
? [
|
||||
// grant this? not sure.
|
||||
addAccessControlsForInterface(self.id, ScryptedInterface.ScryptedDevice),
|
||||
addAccessControlsForInterface(sdk.systemManager.getDeviceByName('@scrypted/webrtc').id,
|
||||
ScryptedInterface.ScryptedDevice,
|
||||
ScryptedInterface.EngineIOHandler),
|
||||
addAccessControlsForInterface(sdk.systemManager.getDeviceByName('@scrypted/core').id,
|
||||
ScryptedInterface.ScryptedDevice,
|
||||
ScryptedInterface.EngineIOHandler),
|
||||
]
|
||||
: [],
|
||||
...this.storageSettings.values.interfaces.map((deviceInterface: string) => {
|
||||
const [id, scryptedInterface] = deviceInterface.split('#');
|
||||
return addAccessControlsForInterface(id, ScryptedInterface.ScryptedDevice, scryptedInterface as ScryptedInterface);
|
||||
}),
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
if (self) {
|
||||
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -72,7 +79,19 @@ export class User extends ScryptedDeviceBase implements Settings, ScryptedUser {
|
||||
const user = users.find(user => user.username === this.username);
|
||||
if (!user)
|
||||
return;
|
||||
await usersService.addUser(user.username, value.toString(), user.aclId);
|
||||
const { username, admin } = user;
|
||||
const nativeId = `user:${username}`;
|
||||
const aclId = await sdk.deviceManager.onDeviceDiscovered({
|
||||
providerNativeId: this.nativeId,
|
||||
name: username.toString(),
|
||||
nativeId,
|
||||
interfaces: [
|
||||
ScryptedInterface.ScryptedUser,
|
||||
ScryptedInterface.Settings,
|
||||
],
|
||||
type: ScryptedDeviceType.Person,
|
||||
})
|
||||
await usersService.addUser(user.username, value.toString(), admin ? undefined : aclId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
color="primary" icon="mdi-vuetify" border="left">
|
||||
<template v-slot:prepend>
|
||||
<v-icon class="white--text mr-3" size="sm" color="#a9afbb">{{
|
||||
getAlertIcon(alert)
|
||||
getAlertIcon(alert)
|
||||
}}</v-icon>
|
||||
</template>
|
||||
<div class="caption">{{ alert.title }}</div>
|
||||
@@ -185,7 +185,8 @@
|
||||
<LogCard :rows="15" :logRoute="`/device/${id}/`"></LogCard>
|
||||
</v-flex>
|
||||
|
||||
<v-flex xs12 v-if="!device.interfaces.includes(ScryptedInterface.Settings) && (availableMixins.length || deviceIsEditable(device))">
|
||||
<v-flex xs12
|
||||
v-if="!device.interfaces.includes(ScryptedInterface.Settings) && (availableMixins.length || deviceIsEditable(device))">
|
||||
<Settings :device="device"></Settings>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
@@ -240,6 +241,7 @@ import Scene from "../interfaces/Scene.vue";
|
||||
import TemperatureSetting from "../interfaces/TemperatureSetting.vue";
|
||||
import PositionSensor from "../interfaces/sensors/PositionSensor.vue";
|
||||
import DeviceProvider from "../interfaces/DeviceProvider.vue";
|
||||
import ObjectDetection from "../interfaces/ObjectDetection.vue";
|
||||
import MixinProvider from "../interfaces/MixinProvider.vue";
|
||||
import Readme from "../interfaces/Readme.vue";
|
||||
import Scriptable from "../interfaces/automation/Scriptable.vue";
|
||||
@@ -286,7 +288,9 @@ const leftInterfaces = [
|
||||
ScryptedInterface.DeviceProvider,
|
||||
ScryptedInterface.Readme,
|
||||
];
|
||||
const leftAboveInterfaces = [ScryptedInterface.Camera];
|
||||
const leftAboveInterfaces = [
|
||||
ScryptedInterface.Camera,
|
||||
];
|
||||
|
||||
const noCardInterfaces = [
|
||||
ScryptedInterface.Camera,
|
||||
@@ -294,7 +298,10 @@ const noCardInterfaces = [
|
||||
ScryptedInterface.Scriptable,
|
||||
];
|
||||
|
||||
const aboveInterfaces = [ScryptedInterface.Scriptable];
|
||||
const aboveInterfaces = [
|
||||
ScryptedInterface.ObjectDetection,
|
||||
ScryptedInterface.Scriptable
|
||||
];
|
||||
|
||||
const cardActionInterfaces = [
|
||||
ScryptedInterface.OauthClient,
|
||||
@@ -379,6 +386,8 @@ export default {
|
||||
Automation,
|
||||
Program,
|
||||
Scriptable,
|
||||
|
||||
ObjectDetection,
|
||||
},
|
||||
mixins: [Mixin],
|
||||
data() {
|
||||
|
||||
@@ -23,6 +23,9 @@ export function createSystemSettingsDevice(systemManager: SystemManager): Scrypt
|
||||
},
|
||||
async probe() {
|
||||
return true;
|
||||
},
|
||||
async setMixins() {
|
||||
|
||||
},
|
||||
listen(event, callback) {
|
||||
let listeners = systemSettings.map(d => d.listen(event, callback));
|
||||
|
||||
@@ -24,22 +24,45 @@
|
||||
Devices</v-btn>
|
||||
</v-card-actions>
|
||||
|
||||
<v-simple-table v-if="discoveredDevices && discoveredDevices.length">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 10px;"></th>
|
||||
<th>Discovered</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody v-if="discoveredDevices.length">
|
||||
<tr v-for="device in discoveredDevices" :key="device.id">
|
||||
<td>
|
||||
<v-btn x-small outlined fab color="info" @click="openDeviceAdoptionDialog(device)">
|
||||
<v-icon>fa-solid
|
||||
fa-plus</v-icon>
|
||||
</v-btn>
|
||||
</td>
|
||||
<td>
|
||||
{{ device.name }}
|
||||
</td>
|
||||
<td> {{ device.description }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-else>
|
||||
<td></td>
|
||||
<td>None found.</td>
|
||||
<td></td>
|
||||
</tbody>
|
||||
</v-simple-table>
|
||||
|
||||
|
||||
<v-card-text>These things were created by {{ device.name }}.</v-card-text>
|
||||
<v-text-field v-model="search" append-icon="search" label="Search" single-line hide-details></v-text-field>
|
||||
<v-data-table :headers="headers" :items="providerDevices.devices" :items-per-page="10" :search="search">
|
||||
<v-text-field v-if="managedDevices.devices.length > 10" v-model="search" append-icon="search" label="Search"
|
||||
single-line hide-details></v-text-field>
|
||||
<v-data-table v-if="managedDevices.devices.length > 10" :headers="headers" :items="managedDevices.devices"
|
||||
:items-per-page="10" :search="search">
|
||||
<template v-slot:[`item.icon`]="{ item }">
|
||||
<v-icon v-if="!item.nativeId" x-small color="grey">
|
||||
<v-icon x-small color="grey">
|
||||
{{ typeToIcon(item.type) }}
|
||||
</v-icon>
|
||||
|
||||
<v-tooltip bottom v-else>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-btn x-small outlined fab v-on="on" color="info" @click="openDeviceAdoptionDialog(item)"><v-icon>fa-solid
|
||||
fa-plus</v-icon></v-btn>
|
||||
</template>
|
||||
<span>Add Discovered Device</span>
|
||||
</v-tooltip>
|
||||
|
||||
</template>
|
||||
<template v-slot:[`item.name`]="{ item }">
|
||||
<a v-if="!item.nativeId" link :href="'#' + getDeviceViewPath(item.id)">{{ item.name }}</a>
|
||||
@@ -48,7 +71,7 @@
|
||||
</template>
|
||||
</v-data-table>
|
||||
|
||||
<!-- <DeviceGroup v-else :deviceGroup="providerDevices"></DeviceGroup> -->
|
||||
<DeviceGroup v-else :deviceGroup="managedDevices"></DeviceGroup>
|
||||
</v-flex>
|
||||
</template>
|
||||
<script>
|
||||
@@ -167,8 +190,8 @@ export default {
|
||||
});
|
||||
return ret;
|
||||
},
|
||||
providerDevices() {
|
||||
const currentDevices = this.$store.state.scrypted.devices
|
||||
managedDevices() {
|
||||
const devices = this.$store.state.scrypted.devices
|
||||
.filter(
|
||||
(id) =>
|
||||
this.$store.state.systemState[id].providerId.value ===
|
||||
@@ -181,7 +204,7 @@ export default {
|
||||
}));
|
||||
|
||||
return {
|
||||
devices: [...this.discoveredDevices || [], ...currentDevices],
|
||||
devices,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
91
plugins/core/ui/src/interfaces/ObjectDetection.vue
Normal file
91
plugins/core/ui/src/interfaces/ObjectDetection.vue
Normal file
@@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<v-sheet :height="600" width="100%" class="d-flex align-center justify-center flex-wrap text-center mx-auto"
|
||||
@drop="onDrop" @dragover="allowDrop">
|
||||
<div v-if="!img">Drag and Drop a JPG or PNG to analyze.</div>
|
||||
<div v-else style="position: relative; height: 100%;">
|
||||
<img :src="img" style="height: 100%">
|
||||
|
||||
<svg v-if="lastDetection" :viewBox="`0 0 ${svgWidth} ${svgHeight}`" ref="svg" style="
|
||||
top: 0;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
" v-html="svgContents"></svg>
|
||||
|
||||
|
||||
</div>
|
||||
</v-sheet>
|
||||
</template>
|
||||
<script>
|
||||
import RPCInterface from "./RPCInterface.vue";
|
||||
export default {
|
||||
mixins: [RPCInterface],
|
||||
data() {
|
||||
return {
|
||||
img: null,
|
||||
lastDetection: null,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
},
|
||||
computed: {
|
||||
svgWidth() {
|
||||
return this.lastDetection?.inputDimensions?.[0] || 1920;
|
||||
},
|
||||
svgHeight() {
|
||||
return this.lastDetection?.inputDimensions?.[1] || 1080;
|
||||
},
|
||||
svgContents() {
|
||||
if (!this.lastDetection) return "";
|
||||
|
||||
let contents = "";
|
||||
|
||||
for (const detection of this.lastDetection.detections || []) {
|
||||
if (!detection.boundingBox) continue;
|
||||
const svgScale = this.svgWidth / 1080;
|
||||
const sw = 6 * svgScale;
|
||||
const s = "red";
|
||||
const x = detection.boundingBox[0];
|
||||
const y = detection.boundingBox[1];
|
||||
const w = detection.boundingBox[2];
|
||||
const h = detection.boundingBox[3];
|
||||
let t = ``;
|
||||
let toffset = 0;
|
||||
if (detection.score && detection.className !== 'motion') {
|
||||
t += `<tspan x='${x}' dy='${toffset}em'>${Math.round(detection.score * 100) / 100}</tspan>`
|
||||
toffset -= 1.2;
|
||||
}
|
||||
const tname = detection.className + (detection.id ? `: ${detection.id}` : '')
|
||||
t += `<tspan x='${x}' dy='${toffset}em'>${tname}</tspan>`
|
||||
|
||||
const fs = 30 * svgScale;
|
||||
|
||||
const box = `<rect x="${x}" y="${y}" width="${w}" height="${h}" stroke="${s}" stroke-width="${sw}" fill="none" />
|
||||
<text x="${x}" y="${y - 5}" font-size="${fs}" dx="0.05em" dy="0.05em" fill="black">${t}</text>
|
||||
<text x="${x}" y="${y - 5}" font-size="${fs}" fill="white">${t}</text>
|
||||
`;
|
||||
contents += box;
|
||||
}
|
||||
|
||||
return contents;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async onDrop(ev) {
|
||||
ev.preventDefault()
|
||||
const file = ev.dataTransfer.files[0];
|
||||
this.img = URL.createObjectURL(file);
|
||||
const buffer = Buffer.from(await file.arrayBuffer());
|
||||
const mediaManager = this.$scrypted.mediaManager;
|
||||
const mo = await mediaManager.createMediaObject(buffer, 'image/*');
|
||||
const detected = await this.rpc().detectObjects(mo);
|
||||
this.lastDetection = detected;
|
||||
},
|
||||
allowDrop(ev) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -102,6 +102,7 @@ export default {
|
||||
},
|
||||
set(value) {
|
||||
this.rawSettingsGroupName = value;
|
||||
this.rawSettingsSubgroupName = undefined;
|
||||
},
|
||||
},
|
||||
settingsSubgroupName: {
|
||||
@@ -110,7 +111,7 @@ export default {
|
||||
return;
|
||||
if (this.settingsSubgroups.findIndex(sg => sg === this.rawSettingsSubgroupName) !== -1)
|
||||
return this.rawSettingsSubgroupName;
|
||||
return Object.keys(this.settingsSubgroups)?.[0];
|
||||
return Object.values(this.settingsSubgroups)?.[0];
|
||||
},
|
||||
set(value) {
|
||||
this.rawSettingsSubgroupName = value;
|
||||
|
||||
6
plugins/coreml/package-lock.json
generated
6
plugins/coreml/package-lock.json
generated
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"name": "@scrypted/coreml",
|
||||
"version": "0.0.21",
|
||||
"version": "0.0.24",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/coreml",
|
||||
"version": "0.0.21",
|
||||
"version": "0.0.24",
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
}
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.2.57",
|
||||
"version": "0.2.85",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
|
||||
@@ -41,5 +41,5 @@
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
},
|
||||
"version": "0.0.21"
|
||||
"version": "0.0.24"
|
||||
}
|
||||
|
||||
@@ -3,8 +3,3 @@ Pillow>=5.4.1
|
||||
PyGObject>=3.30.4
|
||||
coremltools~=6.1
|
||||
av>=10.0.0; sys_platform != 'linux' or platform_machine == 'x86_64' or platform_machine == 'aarch64'
|
||||
|
||||
# sort_oh
|
||||
scipy
|
||||
filterpy
|
||||
numpy
|
||||
|
||||
@@ -7,4 +7,5 @@ src
|
||||
.vscode
|
||||
dist/*.js
|
||||
dist/*.txt
|
||||
face-api.js
|
||||
HAP-NodeJS
|
||||
.gitattributes
|
||||
@@ -10,6 +10,7 @@
|
||||
"port": 10081,
|
||||
"request": "attach",
|
||||
"skipFiles": [
|
||||
"**/plugin-remote-worker.*",
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"preLaunchTask": "scrypted: deploy+debug",
|
||||
@@ -19,4 +20,4 @@
|
||||
"type": "pwa-node"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"scrypted.debugHost": "127.0.0.1",
|
||||
}
|
||||
}
|
||||
1
plugins/eufy/README.md
Normal file
1
plugins/eufy/README.md
Normal file
@@ -0,0 +1 @@
|
||||
# Eufy Plugin for Scrypted
|
||||
1333
plugins/eufy/package-lock.json
generated
Normal file
1333
plugins/eufy/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
40
plugins/eufy/package.json
Normal file
40
plugins/eufy/package.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "@scrypted/eufy",
|
||||
"description": "Eufy Plugin for Scrypted",
|
||||
"version": "0.0.1",
|
||||
"keywords": [
|
||||
"scrypted",
|
||||
"plugin",
|
||||
"eufy",
|
||||
"camera"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "scrypted-webpack",
|
||||
"prepublishOnly": "NODE_ENV=production scrypted-webpack",
|
||||
"prescrypted-vscode-launch": "scrypted-webpack",
|
||||
"scrypted-vscode-launch": "scrypted-deploy-debug",
|
||||
"scrypted-deploy-debug": "scrypted-deploy-debug",
|
||||
"scrypted-debug": "scrypted-debug",
|
||||
"scrypted-deploy": "scrypted-deploy",
|
||||
"scrypted-readme": "scrypted-readme",
|
||||
"scrypted-package-json": "scrypted-package-json",
|
||||
"scrypted-webpack": "scrypted-webpack"
|
||||
},
|
||||
"scrypted": {
|
||||
"name": "Eufy",
|
||||
"type": "DeviceProvider",
|
||||
"interfaces": [
|
||||
"DeviceProvider",
|
||||
"Settings"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/h264-repacketizer": "file:../../packages/h264-repacketizer ",
|
||||
"@types/node": "^18.14.6"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"eufy-security-client": "^2.4.2"
|
||||
}
|
||||
}
|
||||
319
plugins/eufy/src/main.ts
Normal file
319
plugins/eufy/src/main.ts
Normal file
@@ -0,0 +1,319 @@
|
||||
import { listenSingleRtspClient } from '@scrypted/common/src/rtsp-server';
|
||||
import { addTrackControls, parseSdp } from '@scrypted/common/src/sdp-utils';
|
||||
import sdk, { Battery, Camera, Device, DeviceProvider, FFmpegInput, MediaObject, MotionSensor, RequestMediaStreamOptions, RequestPictureOptions, ResponseMediaStreamOptions, ResponsePictureOptions, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, Setting, Settings, SettingValue, VideoCamera } from '@scrypted/sdk';
|
||||
import { StorageSettings } from '@scrypted/sdk/storage-settings';
|
||||
import eufy, { CaptchaOptions, EufySecurity } from 'eufy-security-client';
|
||||
import { startRtpForwarderProcess } from '../../webrtc/src/rtp-forwarders';
|
||||
|
||||
import { Deferred } from '@scrypted/common/src/deferred';
|
||||
import { Writable } from 'stream';
|
||||
import { LocalLivestreamManager } from './stream';
|
||||
|
||||
const { deviceManager, mediaManager, systemManager } = sdk;
|
||||
|
||||
class EufyCamera extends ScryptedDeviceBase implements Camera, VideoCamera, Battery, MotionSensor {
|
||||
client: EufySecurity;
|
||||
device: eufy.Camera;
|
||||
livestreamManager: LocalLivestreamManager
|
||||
|
||||
constructor(nativeId: string, client: EufySecurity, device: eufy.Camera) {
|
||||
super(nativeId);
|
||||
this.client = client;
|
||||
this.device = device;
|
||||
this.livestreamManager = new LocalLivestreamManager(this.client, this.device, this.console);
|
||||
this.batteryLevel = this.device.getBatteryValue() as number;
|
||||
this.setupMotionDetection();
|
||||
}
|
||||
|
||||
setupMotionDetection() {
|
||||
const handle = (device: eufy.Device, state: boolean) => {
|
||||
this.motionDetected = state;
|
||||
};
|
||||
this.device.on('motion detected', handle);
|
||||
this.device.on('person detected', handle);
|
||||
this.device.on('pet detected', handle);
|
||||
this.device.on('vehicle detected', handle);
|
||||
this.device.on('dog detected', handle);
|
||||
this.device.on('radar motion detected', handle);
|
||||
}
|
||||
|
||||
async takePicture(options?: RequestPictureOptions): Promise<MediaObject> {
|
||||
// if this stream is prebuffered, its safe to use the prebuffer to generate an image
|
||||
const realDevice = systemManager.getDeviceById<VideoCamera>(this.id);
|
||||
try {
|
||||
const msos = await realDevice.getVideoStreamOptions();
|
||||
const prebuffered: RequestMediaStreamOptions = msos.find(mso => mso.prebuffer);
|
||||
if (prebuffered) {
|
||||
prebuffered.refresh = false;
|
||||
return realDevice.getVideoStream(prebuffered);
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
// try to fetch the cloud image if one exists
|
||||
const url = this.device.getLastCameraImageURL();
|
||||
if (url) {
|
||||
return mediaManager.createMediaObjectFromUrl(url.toString());
|
||||
}
|
||||
|
||||
throw new Error("snapshot unavailable");
|
||||
}
|
||||
|
||||
getPictureOptions(): Promise<ResponsePictureOptions[]> {
|
||||
return;
|
||||
}
|
||||
|
||||
getVideoStream(options?: ResponseMediaStreamOptions): Promise<MediaObject> {
|
||||
return this.createVideoStream(options);
|
||||
}
|
||||
|
||||
async getVideoStreamOptions(): Promise<ResponseMediaStreamOptions[]> {
|
||||
return [
|
||||
{
|
||||
container: 'rtsp',
|
||||
id: 'p2p',
|
||||
name: 'P2P',
|
||||
video: {
|
||||
codec: 'h264',
|
||||
},
|
||||
audio: {
|
||||
codec: 'aac',
|
||||
},
|
||||
tool: 'scrypted',
|
||||
userConfigurable: false,
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
async createVideoStream(options?: ResponseMediaStreamOptions): Promise<MediaObject> {
|
||||
const kill = new Deferred<void>();
|
||||
kill.promise.finally(() => {
|
||||
this.console.log('video stream exited');
|
||||
this.livestreamManager.stopLocalLiveStream();
|
||||
});
|
||||
|
||||
const rtspServer = await listenSingleRtspClient();
|
||||
rtspServer.rtspServerPromise.then(async rtsp => {
|
||||
kill.promise.finally(() => rtsp.client.destroy());
|
||||
rtsp.client.on('close', () => kill.resolve());
|
||||
try {
|
||||
const process = await startRtpForwarderProcess(this.console, {
|
||||
inputArguments: [
|
||||
'-f', 'h264', '-i', 'pipe:4',
|
||||
'-f', 'aac', '-i', 'pipe:5',
|
||||
]
|
||||
}, {
|
||||
video: {
|
||||
onRtp: rtp => {
|
||||
if (videoTrack)
|
||||
rtsp.sendTrack(videoTrack.control, rtp, false);
|
||||
},
|
||||
encoderArguments: [
|
||||
'-vcodec', 'copy',
|
||||
]
|
||||
},
|
||||
audio: {
|
||||
onRtp: rtp => {
|
||||
if (audioTrack)
|
||||
rtsp.sendTrack(audioTrack.control, rtp, false);
|
||||
},
|
||||
encoderArguments: [
|
||||
'-acodec', 'copy',
|
||||
'-rtpflags', 'latm',
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
process.killPromise.finally(() => kill.resolve());
|
||||
kill.promise.finally(() => process.kill());
|
||||
|
||||
let parsedSdp: ReturnType<typeof parseSdp>;
|
||||
let videoTrack: typeof parsedSdp.msections[0]
|
||||
let audioTrack: typeof parsedSdp.msections[0]
|
||||
process.sdpContents.then(async sdp => {
|
||||
sdp = addTrackControls(sdp);
|
||||
rtsp.sdp = sdp;
|
||||
parsedSdp = parseSdp(sdp);
|
||||
videoTrack = parsedSdp.msections.find(msection => msection.type === 'video');
|
||||
audioTrack = parsedSdp.msections.find(msection => msection.type === 'audio');
|
||||
await rtsp.handlePlayback();
|
||||
});
|
||||
|
||||
const proxyStream = await this.livestreamManager.getLocalLivestream();
|
||||
proxyStream.videostream.pipe(process.cp.stdio[4] as Writable);
|
||||
proxyStream.audiostream.pipe((process.cp.stdio as any)[5] as Writable);
|
||||
}
|
||||
catch (e) {
|
||||
rtsp.client.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
const input: FFmpegInput = {
|
||||
url: rtspServer.url,
|
||||
mediaStreamOptions: options,
|
||||
inputArguments: [
|
||||
'-i', rtspServer.url,
|
||||
]
|
||||
};
|
||||
|
||||
return mediaManager.createFFmpegMediaObject(input);
|
||||
}
|
||||
}
|
||||
|
||||
class EufyPlugin extends ScryptedDeviceBase implements DeviceProvider, Settings {
|
||||
client: EufySecurity;
|
||||
devices = new Map<string, any>();
|
||||
|
||||
storageSettings = new StorageSettings(this, {
|
||||
country: {
|
||||
title: 'Country',
|
||||
defaultValue: 'US',
|
||||
},
|
||||
email: {
|
||||
title: 'Email',
|
||||
onPut: async () => this.tryLogin(),
|
||||
},
|
||||
password: {
|
||||
title: 'Password',
|
||||
type: 'password',
|
||||
onPut: async () => this.tryLogin(),
|
||||
},
|
||||
twoFactorCode: {
|
||||
title: 'Two Factor Code',
|
||||
description: 'Optional: If 2FA is enabled on your account, enter the code sent to your email or phone number.',
|
||||
onPut: async (oldValue, newValue) => {
|
||||
await this.tryLogin(newValue);
|
||||
},
|
||||
noStore: true,
|
||||
},
|
||||
captcha: {
|
||||
title: 'Captcha',
|
||||
description: 'Optional: If a captcha request is recieved, enter the code in the image.',
|
||||
onPut: async (oldValue, newValue) => {
|
||||
await this.tryLogin(undefined, newValue);
|
||||
},
|
||||
noStore: true,
|
||||
},
|
||||
captchaId: {
|
||||
title: 'Captcha Id',
|
||||
hide: true,
|
||||
}
|
||||
});
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.tryLogin()
|
||||
}
|
||||
|
||||
getSettings(): Promise<Setting[]> {
|
||||
return this.storageSettings.getSettings();
|
||||
}
|
||||
|
||||
putSetting(key: string, value: SettingValue): Promise<void> {
|
||||
return this.storageSettings.putSetting(key, value);
|
||||
}
|
||||
|
||||
async tryLogin(twoFactorCode?: string, captchaCode?: string) {
|
||||
this.log.clearAlerts();
|
||||
|
||||
if (!this.storageSettings.values.email || !this.storageSettings.values.email) {
|
||||
this.log.a('Enter your Eufy email and password to complete setup.');
|
||||
throw new Error('Eufy email and password are missing.');
|
||||
}
|
||||
|
||||
await this.initializeClient();
|
||||
|
||||
var captchaOptions: CaptchaOptions = undefined
|
||||
if (captchaCode) {
|
||||
captchaOptions = {
|
||||
captchaCode: captchaCode,
|
||||
captchaId: this.storageSettings.values.captchaId,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
await this.client.connect({ verifyCode: twoFactorCode, captcha: captchaOptions, force: false });
|
||||
}
|
||||
|
||||
private async initializeClient() {
|
||||
const config = {
|
||||
username: this.storageSettings.values.email,
|
||||
password: this.storageSettings.values.password,
|
||||
country: this.storageSettings.values.country,
|
||||
language: 'en',
|
||||
p2pConnectionSetup: 2,
|
||||
pollingIntervalMinutes: 10,
|
||||
eventDurationSeconds: 10
|
||||
}
|
||||
this.client = await EufySecurity.initialize(config);
|
||||
this.client.on('device added', this.deviceAdded.bind(this));
|
||||
this.client.on('station added', this.stationAdded.bind(this));
|
||||
|
||||
this.client.on('tfa request', () => {
|
||||
this.log.a('Login failed: 2FA is enabled, check your email or texts for your code, then enter it into the Two Factor Code setting to conplete login.');
|
||||
});
|
||||
this.client.on('captcha request', (id, captcha) => {
|
||||
this.log.a(`Login failed: Captcha was requested, fill out the Captcha setting to conplete login. </br> <img src="${captcha}" />`);
|
||||
this.storageSettings.putSetting('captchaId', id);
|
||||
});
|
||||
this.client.on('connect', () => {
|
||||
this.console.debug(`[${this.name}] (${new Date().toLocaleString()}) Client connected.`);
|
||||
this.log.clearAlerts();
|
||||
});
|
||||
this.client.on('push connect', () => {
|
||||
this.console.log(`[${this.name}] (${new Date().toLocaleString()}) Push Connected.`);
|
||||
});
|
||||
this.client.on('push close', () => {
|
||||
this.console.log(`[${this.name}] (${new Date().toLocaleString()}) Push Closed.`);
|
||||
});
|
||||
}
|
||||
|
||||
private async deviceAdded(eufyDevice: eufy.Device) {
|
||||
if (!eufyDevice.isCamera) {
|
||||
this.console.info(`[${this.name}] (${new Date().toLocaleString()}) Ignoring unsupported discovered device: `, eufyDevice.getName(), eufyDevice.getModel());
|
||||
return;
|
||||
}
|
||||
this.console.info(`[${this.name}] (${new Date().toLocaleString()}) Device discovered: `, eufyDevice.getName(), eufyDevice.getModel());
|
||||
|
||||
const nativeId = eufyDevice.getSerial();
|
||||
|
||||
const interfaces = [
|
||||
ScryptedInterface.Camera,
|
||||
ScryptedInterface.VideoCamera
|
||||
];
|
||||
if (eufyDevice.hasBattery())
|
||||
interfaces.push(ScryptedInterface.Battery);
|
||||
if (eufyDevice.hasProperty('motionDetection'))
|
||||
interfaces.push(ScryptedInterface.MotionSensor);
|
||||
|
||||
const device: Device = {
|
||||
info: {
|
||||
model: eufyDevice.getModel(),
|
||||
manufacturer: 'Eufy',
|
||||
firmware: eufyDevice.getSoftwareVersion(),
|
||||
serialNumber: nativeId
|
||||
},
|
||||
nativeId,
|
||||
name: eufyDevice.getName(),
|
||||
type: ScryptedDeviceType.Camera,
|
||||
interfaces,
|
||||
};
|
||||
|
||||
this.devices.set(nativeId, new EufyCamera(nativeId, this.client, eufyDevice as eufy.Camera))
|
||||
await deviceManager.onDeviceDiscovered(device);
|
||||
}
|
||||
|
||||
private async stationAdded(station: eufy.Station) {
|
||||
this.console.info(`[${this.name}] (${new Date().toLocaleString()}) Station discovered: `, station.getName(), station.getModel(), `but stations are not currently supported.`);
|
||||
}
|
||||
|
||||
async getDevice(nativeId: string): Promise<any> {
|
||||
return this.devices.get(nativeId);
|
||||
}
|
||||
|
||||
async releaseDevice(id: string, nativeId: string) {
|
||||
this.console.info(`[${this.name}] (${new Date().toLocaleString()}) Device with id '${nativeId}' was removed.`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default new EufyPlugin();
|
||||
129
plugins/eufy/src/stream.ts
Normal file
129
plugins/eufy/src/stream.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
// Based off of https://github.com/homebridge-eufy-security/plugin/blob/master/src/plugin/controller/LocalLivestreamManager.ts
|
||||
|
||||
import { EventEmitter, Readable } from 'stream';
|
||||
import { Station, Device, StreamMetadata, Camera, EufySecurity } from 'eufy-security-client';
|
||||
|
||||
type StationStream = {
|
||||
station: Station;
|
||||
device: Device;
|
||||
metadata: StreamMetadata;
|
||||
videostream: Readable;
|
||||
audiostream: Readable;
|
||||
createdAt: number;
|
||||
};
|
||||
|
||||
export class LocalLivestreamManager extends EventEmitter {
|
||||
private stationStream: StationStream | null;
|
||||
private console: Console;
|
||||
|
||||
private livestreamStartedAt: number | null;
|
||||
private livestreamIsStarting = false;
|
||||
|
||||
private readonly client: EufySecurity;
|
||||
private readonly device: Camera;
|
||||
|
||||
constructor(client: EufySecurity, device: Camera, console: Console) {
|
||||
super();
|
||||
|
||||
this.console = console;
|
||||
this.client = client;
|
||||
this.device = device;
|
||||
|
||||
this.stationStream = null;
|
||||
this.livestreamStartedAt = null;
|
||||
|
||||
this.initialize();
|
||||
|
||||
this.client.on('station livestream stop', (station: Station, device: Device) => {
|
||||
this.onStationLivestreamStop(station, device);
|
||||
});
|
||||
this.client.on('station livestream start',
|
||||
(station: Station, device: Device, metadata: StreamMetadata, videostream: Readable, audiostream: Readable) => {
|
||||
this.onStationLivestreamStart(station, device, metadata, videostream, audiostream);
|
||||
});
|
||||
}
|
||||
|
||||
private initialize() {
|
||||
if (this.stationStream) {
|
||||
this.stationStream.audiostream.unpipe();
|
||||
this.stationStream.audiostream.destroy();
|
||||
this.stationStream.videostream.unpipe();
|
||||
this.stationStream.videostream.destroy();
|
||||
}
|
||||
this.stationStream = null;
|
||||
this.livestreamStartedAt = null;
|
||||
}
|
||||
|
||||
public async getLocalLivestream(): Promise<StationStream> {
|
||||
this.console.debug(this.device.getName(), 'New instance requests livestream.');
|
||||
if (this.stationStream) {
|
||||
const runtime = (Date.now() - this.livestreamStartedAt!) / 1000;
|
||||
this.console.debug(this.device.getName(), 'Using livestream that was started ' + runtime + ' seconds ago.');
|
||||
return this.stationStream;
|
||||
} else {
|
||||
return await this.startAndGetLocalLiveStream();
|
||||
}
|
||||
}
|
||||
|
||||
private async startAndGetLocalLiveStream(): Promise<StationStream> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.console.debug(this.device.getName(), 'Start new station livestream (P2P Session)...');
|
||||
if (!this.livestreamIsStarting) { // prevent multiple stream starts from eufy station
|
||||
this.livestreamIsStarting = true;
|
||||
this.client.startStationLivestream(this.device.getSerial());
|
||||
} else {
|
||||
this.console.debug(this.device.getName(), 'stream is already starting. waiting...');
|
||||
}
|
||||
|
||||
this.once('livestream start', async () => {
|
||||
if (this.stationStream !== null) {
|
||||
this.console.debug(this.device.getName(), 'New livestream started.');
|
||||
this.livestreamIsStarting = false;
|
||||
resolve(this.stationStream);
|
||||
} else {
|
||||
reject('no started livestream found');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public stopLocalLiveStream(): void {
|
||||
this.console.debug(this.device.getName(), 'Stopping station livestream.');
|
||||
this.client.stopStationLivestream(this.device.getSerial());
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
private onStationLivestreamStop(station: Station, device: Device) {
|
||||
if (device.getSerial() === this.device.getSerial()) {
|
||||
this.console.info(station.getName() + ' station livestream for ' + device.getName() + ' has stopped.');
|
||||
this.initialize();
|
||||
}
|
||||
}
|
||||
|
||||
private async onStationLivestreamStart(
|
||||
station: Station,
|
||||
device: Device,
|
||||
metadata: StreamMetadata,
|
||||
videostream: Readable,
|
||||
audiostream: Readable,
|
||||
) {
|
||||
if (device.getSerial() === this.device.getSerial()) {
|
||||
if (this.stationStream) {
|
||||
const diff = (Date.now() - this.stationStream.createdAt) / 1000;
|
||||
if (diff < 5) {
|
||||
this.console.warn(this.device.getName(), 'Second livestream was started from station. Ignore.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.initialize(); // important to prevent unwanted behaviour when the eufy station emits the 'livestream start' event multiple times
|
||||
|
||||
this.console.info(station.getName() + ' station livestream (P2P session) for ' + device.getName() + ' has started.');
|
||||
this.livestreamStartedAt = Date.now();
|
||||
const createdAt = Date.now();
|
||||
this.stationStream = {station, device, metadata, videostream, audiostream, createdAt};
|
||||
this.console.debug(this.device.getName(), 'Stream metadata: ' + JSON.stringify(this.stationStream.metadata));
|
||||
|
||||
this.emit('livestream start');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,4 +10,4 @@
|
||||
"include": [
|
||||
"src/**/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -174,7 +174,7 @@ export abstract class CameraProviderBase<T extends ResponseMediaStreamOptions> e
|
||||
|
||||
async createDevice(settings: DeviceCreatorSettings, nativeId?: ScryptedNativeId): Promise<string> {
|
||||
nativeId ||= randomBytes(4).toString('hex');
|
||||
const name = settings.newCamera.toString();
|
||||
const name = settings.newCamera?.toString() || 'New Camera';
|
||||
await this.updateDevice(nativeId, name, this.getInterfaces());
|
||||
return nativeId;
|
||||
}
|
||||
@@ -208,6 +208,7 @@ export abstract class CameraProviderBase<T extends ResponseMediaStreamOptions> e
|
||||
name,
|
||||
interfaces,
|
||||
type: type || ScryptedDeviceType.Camera,
|
||||
info: deviceManager.getNativeIds().includes(nativeId) ? deviceManager.getDeviceState(nativeId)?.info : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
# Google Cloud Text to Speech plugin
|
||||
|
||||
## npm commands
|
||||
* npm run scrypted-webpack
|
||||
* npm run scrypted-deploy <ipaddress>
|
||||
* npm run scrypted-debug <ipaddress>
|
||||
|
||||
## scrypted distribution via npm
|
||||
1. Ensure package.json is set up properly for publishing on npm.
|
||||
2. npm publish
|
||||
|
||||
## Visual Studio Code configuration
|
||||
|
||||
* If using a remote server, edit [.vscode/settings.json](blob/master/.vscode/settings.json) to specify the IP Address of the Scrypted server.
|
||||
* Launch Scrypted Debugger from the launch menu.
|
||||
141
plugins/google-cloud-tts/package-lock.json
generated
141
plugins/google-cloud-tts/package-lock.json
generated
@@ -1,141 +0,0 @@
|
||||
{
|
||||
"name": "@scrypted/google-cloud-tts",
|
||||
"version": "0.0.21",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/google-cloud-tts",
|
||||
"version": "0.0.21",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"axios": "^0.24.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"@types/node": "^17.0.8"
|
||||
}
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.0.199",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.16.7",
|
||||
"adm-zip": "^0.4.13",
|
||||
"axios": "^0.21.4",
|
||||
"babel-loader": "^8.2.3",
|
||||
"babel-plugin-const-enum": "^1.1.0",
|
||||
"esbuild": "^0.13.8",
|
||||
"ncp": "^2.0.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"tmp": "^0.2.1",
|
||||
"webpack": "^5.59.0"
|
||||
},
|
||||
"bin": {
|
||||
"scrypted-debug": "bin/scrypted-debug.js",
|
||||
"scrypted-deploy": "bin/scrypted-deploy.js",
|
||||
"scrypted-deploy-debug": "bin/scrypted-deploy-debug.js",
|
||||
"scrypted-package-json": "bin/scrypted-package-json.js",
|
||||
"scrypted-readme": "bin/scrypted-readme.js",
|
||||
"scrypted-setup-project": "bin/scrypted-setup-project.js",
|
||||
"scrypted-webpack": "bin/scrypted-webpack.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.11.1",
|
||||
"@types/stringify-object": "^4.0.0",
|
||||
"stringify-object": "^3.3.0",
|
||||
"ts-node": "^10.4.0",
|
||||
"typedoc": "^0.22.8",
|
||||
"typescript-json-schema": "^0.50.1",
|
||||
"webpack-bundle-analyzer": "^4.5.0"
|
||||
}
|
||||
},
|
||||
"../sdk": {
|
||||
"extraneous": true
|
||||
},
|
||||
"node_modules/@scrypted/sdk": {
|
||||
"resolved": "../../sdk",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "17.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.8.tgz",
|
||||
"integrity": "sha512-YofkM6fGv4gDJq78g4j0mMuGMkZVxZDgtU0JRdx6FgiJDG+0fY0GKVolOV8WqVmEhLCXkQRjwDdKyPxJp/uucg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz",
|
||||
"integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.14.4"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.14.7",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.7.tgz",
|
||||
"integrity": "sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@scrypted/sdk": {
|
||||
"version": "file:../../sdk",
|
||||
"requires": {
|
||||
"@babel/preset-typescript": "^7.16.7",
|
||||
"@types/node": "^16.11.1",
|
||||
"@types/stringify-object": "^4.0.0",
|
||||
"adm-zip": "^0.4.13",
|
||||
"axios": "^0.21.4",
|
||||
"babel-loader": "^8.2.3",
|
||||
"babel-plugin-const-enum": "^1.1.0",
|
||||
"esbuild": "^0.13.8",
|
||||
"ncp": "^2.0.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"stringify-object": "^3.3.0",
|
||||
"tmp": "^0.2.1",
|
||||
"ts-node": "^10.4.0",
|
||||
"typedoc": "^0.22.8",
|
||||
"typescript-json-schema": "^0.50.1",
|
||||
"webpack": "^5.59.0",
|
||||
"webpack-bundle-analyzer": "^4.5.0"
|
||||
}
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "17.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.8.tgz",
|
||||
"integrity": "sha512-YofkM6fGv4gDJq78g4j0mMuGMkZVxZDgtU0JRdx6FgiJDG+0fY0GKVolOV8WqVmEhLCXkQRjwDdKyPxJp/uucg==",
|
||||
"dev": true
|
||||
},
|
||||
"axios": {
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz",
|
||||
"integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==",
|
||||
"requires": {
|
||||
"follow-redirects": "^1.14.4"
|
||||
}
|
||||
},
|
||||
"follow-redirects": {
|
||||
"version": "1.14.7",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.7.tgz",
|
||||
"integrity": "sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ=="
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
// https://developer.scrypted.app/#getting-started
|
||||
import axios from 'axios';
|
||||
import sdk, { BufferConverter, ScryptedDeviceBase, Settings, Setting } from "@scrypted/sdk";
|
||||
|
||||
class GoogleCloudTts extends ScryptedDeviceBase implements BufferConverter, Settings {
|
||||
constructor() {
|
||||
super();
|
||||
this.fromMimeType = 'text/plain';
|
||||
this.toMimeType = 'audio/mpeg';
|
||||
|
||||
if (!this.getApiKey())
|
||||
this.log.a('API key missing.');
|
||||
}
|
||||
getApiKey() {
|
||||
const apiKey = this.storage.getItem('api_key');
|
||||
return apiKey;
|
||||
}
|
||||
async convert(data: string | Buffer, fromMimeType: string): Promise<Buffer> {
|
||||
const voice_name = this.storage.getItem("voice_name") || "en-GB-Standard-A";
|
||||
const voice_gender = this.storage.getItem("voice_gender") || "FEMALE";
|
||||
const voice_language_code = this.storage.getItem("voice_language_code") || "en-GB";
|
||||
|
||||
const from = Buffer.from(data);
|
||||
var json = {
|
||||
"input": {
|
||||
"text": from.toString()
|
||||
},
|
||||
"voice": {
|
||||
"languageCode": voice_language_code,
|
||||
"name": voice_name,
|
||||
"ssmlGender": voice_gender
|
||||
},
|
||||
"audioConfig": {
|
||||
"audioEncoding": "MP3"
|
||||
}
|
||||
};
|
||||
|
||||
var result = await axios.post(`https://texttospeech.googleapis.com/v1/text:synthesize?key=${this.getApiKey()}`, json);
|
||||
console.log(JSON.stringify(result.data, null, 2));
|
||||
const buffer = Buffer.from(result.data.audioContent, 'base64');
|
||||
return buffer;
|
||||
}
|
||||
|
||||
voices: any;
|
||||
async getSettings(): Promise<Setting[]> {
|
||||
const ret: Setting[] = [
|
||||
{
|
||||
title: 'API Key',
|
||||
description: 'API Key used by Google Cloud TTS.',
|
||||
key: 'api_key',
|
||||
value: this.storage.getItem('api_key'),
|
||||
}
|
||||
];
|
||||
|
||||
if (!this.getApiKey())
|
||||
return ret;
|
||||
|
||||
try {
|
||||
if (!this.voices) {
|
||||
const response = await axios.get(`https://texttospeech.googleapis.com/v1/voices?key=${this.getApiKey()}`)
|
||||
this.voices = response.data;
|
||||
}
|
||||
|
||||
}
|
||||
catch (e) {
|
||||
this.log.a('Error retrieving settings from Google Cloud Text to Speech. Is your API Key correct?');
|
||||
return ret;
|
||||
}
|
||||
ret.push({
|
||||
title: "Voice",
|
||||
choices: this.voices.voices.map(voice => voice.name),
|
||||
key: "voice",
|
||||
value: this.storage.getItem("voice_name"),
|
||||
});
|
||||
return ret;
|
||||
}
|
||||
async putSetting(key: string, value: string | number | boolean) {
|
||||
if (key !== 'voice') {
|
||||
this.storage.setItem(key, value.toString());
|
||||
return;
|
||||
}
|
||||
|
||||
const found = this.voices.voices.find((voice: any) => voice.name === value);
|
||||
if (!found) {
|
||||
console.error('Voice not found.');
|
||||
return;
|
||||
}
|
||||
|
||||
localStorage.setItem('voice_name', found.name);
|
||||
localStorage.setItem('voice_language_code', found.languageCodes[0]);
|
||||
localStorage.setItem('voice_gender', found.ssmlGender);
|
||||
}
|
||||
}
|
||||
|
||||
export default new GoogleCloudTts();
|
||||
762
plugins/google-device-access/package-lock.json
generated
762
plugins/google-device-access/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -39,18 +39,15 @@
|
||||
"dependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@googleapis/smartdevicemanagement": "^0.2.0",
|
||||
"axios": "^0.21.1",
|
||||
"@googleapis/smartdevicemanagement": "^1.0.0",
|
||||
"axios": "^1.3.4",
|
||||
"client-oauth2": "^4.3.3",
|
||||
"lodash": "^4.17.21",
|
||||
"query-string": "^7.0.0",
|
||||
"url-parse": "^1.5.1"
|
||||
"lodash": "^4.17.21"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/debug": "^4.1.5",
|
||||
"@types/lodash": "^4.14.168",
|
||||
"@types/node": "^14.17.11",
|
||||
"@types/url-parse": "^1.4.3"
|
||||
"@types/debug": "^4.1.7",
|
||||
"@types/lodash": "^4.14.191",
|
||||
"@types/node": "^18.14.1"
|
||||
},
|
||||
"version": "0.0.95"
|
||||
"version": "0.0.96"
|
||||
}
|
||||
|
||||
2942
plugins/google-device-access/pubsub-server/package-lock.json
generated
2942
plugins/google-device-access/pubsub-server/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "appengine-hello-world",
|
||||
"description": "Simple Hello World Node.js sample for Google App Engine Standard Environment.",
|
||||
"version": "0.0.3",
|
||||
"version": "0.0.4",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"author": "Google Inc.",
|
||||
@@ -10,24 +10,24 @@
|
||||
"url": "https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"start": "node dist/app.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google-cloud/datastore": "^6.5.0",
|
||||
"axios": "^0.21.1",
|
||||
"@google-cloud/datastore": "^7.3.2",
|
||||
"axios": "^1.3.4",
|
||||
"client-oauth2": "^4.3.3",
|
||||
"express": "^4.17.1",
|
||||
"typescript": "^4.4.2"
|
||||
"express": "^4.18.2",
|
||||
"typescript": "^4.9.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/node": "^16.7.10",
|
||||
"mocha": "^9.0.0",
|
||||
"supertest": "^6.0.0",
|
||||
"ts-node": "^10.2.1"
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/node": "^18.14.1",
|
||||
"mocha": "^10.2.0",
|
||||
"supertest": "^6.3.3",
|
||||
"ts-node": "^10.9.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import sdk, { ScryptedDeviceBase, DeviceManifest, DeviceProvider, HttpRequest, HttpRequestHandler, HttpResponse, HumiditySensor, MediaObject, MotionSensor, OauthClient, Refresh, ScryptedDeviceType, ScryptedInterface, Setting, Settings, TemperatureSetting, TemperatureUnit, Thermometer, ThermostatMode, VideoCamera, BinarySensor, DeviceInformation, RTCAVSignalingSetup, Camera, PictureOptions, ObjectsDetected, ObjectDetector, ObjectDetectionTypes, FFmpegInput, RequestMediaStreamOptions, Readme, RTCSignalingChannel, RTCSessionControl, RTCSignalingSession, ResponseMediaStreamOptions, RTCSignalingSendIceCandidate, ScryptedMimeTypes, MediaStreamUrl, TemperatureCommand, OnOff } from '@scrypted/sdk';
|
||||
import { connectRTCSignalingClients } from '@scrypted/common/src/rtc-signaling';
|
||||
import { sleep } from '@scrypted/common/src/sleep';
|
||||
import sdk, { BinarySensor, Camera, DeviceInformation, DeviceManifest, DeviceProvider, FFmpegInput, HttpRequest, HttpRequestHandler, HttpResponse, HumiditySensor, MediaObject, MediaStreamUrl, MotionSensor, OauthClient, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, PictureOptions, Readme, Refresh, RequestMediaStreamOptions, ResponseMediaStreamOptions, RTCAVSignalingSetup, RTCSessionControl, RTCSignalingChannel, RTCSignalingSendIceCandidate, RTCSignalingSession, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, Settings, TemperatureCommand, TemperatureSetting, TemperatureUnit, Thermometer, ThermostatMode, VideoCamera } from '@scrypted/sdk';
|
||||
import axios from 'axios';
|
||||
import ClientOAuth2 from 'client-oauth2';
|
||||
import { randomBytes } from 'crypto';
|
||||
import fs from 'fs';
|
||||
import throttle from 'lodash/throttle';
|
||||
import qs from 'query-string';
|
||||
import querystring from "querystring";
|
||||
import { URL } from 'url';
|
||||
|
||||
const { deviceManager, mediaManager, endpointManager, systemManager } = sdk;
|
||||
@@ -334,11 +334,12 @@ for (const [k, v] of setpointMap.entries()) {
|
||||
setpointReverseMap.set(v, k);
|
||||
}
|
||||
|
||||
class NestThermostat extends ScryptedDeviceBase implements HumiditySensor, Thermometer, TemperatureSetting, Settings, Refresh {
|
||||
class NestThermostat extends ScryptedDeviceBase implements HumiditySensor, Thermometer, TemperatureSetting, Settings, Refresh, OnOff {
|
||||
device: any;
|
||||
provider: GoogleSmartDeviceAccess;
|
||||
executeCommandSetMode: any = undefined;
|
||||
executeCommandSetCelsius: any = undefined;
|
||||
executeCommandSetTimer: any = undefined;
|
||||
|
||||
executeThrottle = throttle(async () => {
|
||||
if (this.executeCommandSetCelsius) {
|
||||
@@ -365,6 +366,12 @@ class NestThermostat extends ScryptedDeviceBase implements HumiditySensor, Therm
|
||||
this.console.log('executeCommandSetCelsius', command);
|
||||
return this.provider.authPost(`/devices/${this.nativeId}:executeCommand`, command);
|
||||
}
|
||||
if (this.executeCommandSetTimer) {
|
||||
const command = this.executeCommandSetTimer;
|
||||
this.executeCommandSetTimer = undefined;
|
||||
this.console.log('executeCommandSetTimer', command);
|
||||
return this.provider.authPost(`/devices/${this.nativeId}:executeCommand`, command);
|
||||
}
|
||||
}, 12000)
|
||||
|
||||
constructor(provider: GoogleSmartDeviceAccess, device: any) {
|
||||
@@ -425,6 +432,33 @@ class NestThermostat extends ScryptedDeviceBase implements HumiditySensor, Therm
|
||||
// not supported by API. throw?
|
||||
}
|
||||
|
||||
async turnOff(): Promise<void> {
|
||||
// You can't turn the fan off when the HVAC unit is currently running.
|
||||
if (this.thermostatActiveMode !== ThermostatMode.Off) {
|
||||
this.on = false;
|
||||
await this.refresh(null, true); // Refresh the state to turn the fan switch back to active.
|
||||
return;
|
||||
}
|
||||
this.executeCommandSetTimer = {
|
||||
command: 'sdm.devices.commands.Fan.SetTimer',
|
||||
params: {
|
||||
timerMode: 'OFF',
|
||||
},
|
||||
}
|
||||
await this.executeThrottle();
|
||||
await this.refresh(null, true);
|
||||
}
|
||||
async turnOn(): Promise<void> {
|
||||
this.executeCommandSetTimer = {
|
||||
command: 'sdm.devices.commands.Fan.SetTimer',
|
||||
params: {
|
||||
timerMode: 'ON',
|
||||
},
|
||||
}
|
||||
await this.executeThrottle();
|
||||
await this.refresh(null, true);
|
||||
}
|
||||
|
||||
reload() {
|
||||
const device = this.device;
|
||||
|
||||
@@ -478,6 +512,9 @@ class NestThermostat extends ScryptedDeviceBase implements HumiditySensor, Therm
|
||||
setpoint,
|
||||
availableModes: modes,
|
||||
}
|
||||
|
||||
// Set Fan Status
|
||||
this.on = this.thermostatActiveMode !== ThermostatMode.Off || device.traits?.['sdm.devices.traits.Fan']?.timerMode === "ON";
|
||||
}
|
||||
|
||||
async refresh(refreshInterface: string, userInitiated: boolean): Promise<void> {
|
||||
@@ -764,7 +801,7 @@ export class GoogleSmartDeviceAccess extends ScryptedDeviceBase implements Oauth
|
||||
response_type: 'code',
|
||||
scope: 'https://www.googleapis.com/auth/sdm.service',
|
||||
}
|
||||
return `${this.authorizationUri}?${qs.stringify(params)}`;
|
||||
return `${this.authorizationUri}?${querystring.stringify(params)}`;
|
||||
}
|
||||
async onOauthCallback(callbackUrl: string) {
|
||||
const cb = new URL(callbackUrl);
|
||||
@@ -833,6 +870,7 @@ export class GoogleSmartDeviceAccess extends ScryptedDeviceBase implements Oauth
|
||||
ScryptedInterface.HumiditySensor,
|
||||
ScryptedInterface.Thermometer,
|
||||
ScryptedInterface.Settings,
|
||||
ScryptedInterface.OnOff
|
||||
],
|
||||
info,
|
||||
})
|
||||
|
||||
4
plugins/hikvision/package-lock.json
generated
4
plugins/hikvision/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/hikvision",
|
||||
"version": "0.0.120",
|
||||
"version": "0.0.124",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/hikvision",
|
||||
"version": "0.0.120",
|
||||
"version": "0.0.124",
|
||||
"license": "Apache",
|
||||
"dependencies": {
|
||||
"@koush/axios-digest-auth": "^0.8.5",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/hikvision",
|
||||
"version": "0.0.120",
|
||||
"version": "0.0.124",
|
||||
"description": "Hikvision Plugin for Scrypted",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache",
|
||||
@@ -38,16 +38,10 @@
|
||||
"@koush/axios-digest-auth": "^0.8.5",
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"@types/highland": "^2.12.14",
|
||||
"@types/lodash": "^4.14.172",
|
||||
"@types/multiparty": "^0.0.33",
|
||||
"@types/node": "^16.9.1",
|
||||
"@types/xml2js": "^0.4.9",
|
||||
"axios": "^0.23.0",
|
||||
"highland": "^2.13.5",
|
||||
"lodash": "^4.17.21",
|
||||
"multiparty": "^4.2.2",
|
||||
"net-keepalive": "^3.0.0",
|
||||
"xml2js": "^0.4.23"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import AxiosDigestAuth from '@koush/axios-digest-auth';
|
||||
import { IncomingMessage } from 'http';
|
||||
import https from 'https';
|
||||
|
||||
export const hikvisionHttpsAgent = new https.Agent({
|
||||
rejectUnauthorized: false,
|
||||
});
|
||||
import { getDeviceInfo, hikvisionHttpsAgent } from './probe';
|
||||
|
||||
export function getChannel(channel: string) {
|
||||
return channel || '101';
|
||||
@@ -44,31 +40,18 @@ export class HikvisionCameraAPI {
|
||||
}
|
||||
|
||||
async getDeviceInfo() {
|
||||
try {
|
||||
const response = await this.digestAuth.request({
|
||||
httpsAgent: hikvisionHttpsAgent,
|
||||
method: "GET",
|
||||
responseType: 'text',
|
||||
url: `http://${this.ip}/ISAPI/System/deviceInfo`,
|
||||
});
|
||||
const deviceModel = response.data.match(/>(.*?)<\/model>/)?.[1];
|
||||
const deviceName = response.data.match(/>(.*?)<\/deviceName>/)?.[1];
|
||||
const serialNumber = response.data.match(/>(.*?)<\/serialNumber>/)?.[1];
|
||||
const macAddress = response.data.match(/>(.*?)<\/macAddress>/)?.[1];
|
||||
const firmwareVersion = response.data.match(/>(.*?)<\/firmwareVersion>/)?.[1];
|
||||
return {
|
||||
deviceModel,
|
||||
deviceName,
|
||||
serialNumber,
|
||||
macAddress,
|
||||
firmwareVersion,
|
||||
};
|
||||
}
|
||||
catch (e) {
|
||||
if (e?.response?.data?.includes('notActivated'))
|
||||
throw new Error(`Camera must be first be activated at http://${this.ip}.`)
|
||||
throw e;
|
||||
}
|
||||
return getDeviceInfo(this.digestAuth, this.ip);
|
||||
}
|
||||
|
||||
async checkTwoWayAudio() {
|
||||
const response = await this.digestAuth.request({
|
||||
httpsAgent: hikvisionHttpsAgent,
|
||||
method: "GET",
|
||||
responseType: 'text',
|
||||
url: `http://${this.ip}/ISAPI/System/TwoWayAudio/channels`,
|
||||
});
|
||||
|
||||
return (response.data as string).includes('Speaker');
|
||||
}
|
||||
|
||||
async checkDeviceModel(): Promise<string> {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { ffmpegLogInitialOutput } from '@scrypted/common/src/media-helpers';
|
||||
import { readLength } from '@scrypted/common/src/read-stream';
|
||||
import sdk, { Camera, DeviceCreatorSettings, DeviceInformation, FFmpegInput, Intercom, MediaObject, MediaStreamOptions, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, VideoStreamOptions } from "@scrypted/sdk";
|
||||
import sdk, { Camera, DeviceCreatorSettings, DeviceInformation, FFmpegInput, Intercom, MediaObject, MediaStreamOptions, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting } from "@scrypted/sdk";
|
||||
import child_process, { ChildProcess } from 'child_process';
|
||||
import { PassThrough, Readable } from "stream";
|
||||
import { sleep } from "../../../common/src/sleep";
|
||||
import xml2js from 'xml2js';
|
||||
import { OnvifIntercom } from "../../onvif/src/onvif-intercom";
|
||||
import { RtspProvider, RtspSmartCamera, UrlMediaStreamOptions } from "../../rtsp/src/rtsp";
|
||||
import { getChannel, HikvisionCameraAPI, HikvisionCameraEvent, hikvisionHttpsAgent } from "./hikvision-camera-api";
|
||||
import xml2js from 'xml2js';
|
||||
import { HikvisionCameraAPI, HikvisionCameraEvent } from "./hikvision-camera-api";
|
||||
import { hikvisionHttpsAgent } from './probe';
|
||||
|
||||
const { mediaManager } = sdk;
|
||||
|
||||
@@ -26,19 +26,29 @@ class HikvisionCamera extends RtspSmartCamera implements Camera, Intercom {
|
||||
constructor(nativeId: string, provider: RtspProvider) {
|
||||
super(nativeId, provider);
|
||||
|
||||
this.updateManagementUrl();
|
||||
this.updateDeviceInfo();
|
||||
}
|
||||
|
||||
updateManagementUrl() {
|
||||
async updateDeviceInfo() {
|
||||
const ip = this.storage.getItem('ip');
|
||||
if (!ip)
|
||||
return;
|
||||
const info = this.info || {};
|
||||
const managementUrl = `http://${ip}`;
|
||||
if (info.managementUrl !== managementUrl) {
|
||||
info.managementUrl = managementUrl;
|
||||
this.info = info;
|
||||
const info: DeviceInformation = {
|
||||
...this.info,
|
||||
managementUrl,
|
||||
ip,
|
||||
manufacturer: 'Hikvision',
|
||||
};
|
||||
const client = this.getClient();
|
||||
const deviceInfo = await client.getDeviceInfo().catch(() => { });
|
||||
if (deviceInfo) {
|
||||
info.model = deviceInfo.deviceModel;
|
||||
info.mac = deviceInfo.macAddress;
|
||||
info.firmware = deviceInfo.firmwareVersion;
|
||||
info.serialNumber = deviceInfo.serialNumber;
|
||||
}
|
||||
this.info = info;
|
||||
}
|
||||
|
||||
async listenEvents() {
|
||||
@@ -284,7 +294,7 @@ class HikvisionCamera extends RtspSmartCamera implements Camera, Intercom {
|
||||
|
||||
this.provider.updateDevice(this.nativeId, this.name, interfaces, type);
|
||||
|
||||
this.updateManagementUrl();
|
||||
this.updateDeviceInfo();
|
||||
}
|
||||
|
||||
async getOtherSettings(): Promise<Setting[]> {
|
||||
@@ -526,9 +536,10 @@ class HikvisionProvider extends RtspProvider {
|
||||
const username = settings.username?.toString();
|
||||
const password = settings.password?.toString();
|
||||
const skipValidate = settings.skipValidate === 'true';
|
||||
let twoWayAudio: string;
|
||||
if (!skipValidate) {
|
||||
const api = new HikvisionCameraAPI(httpAddress, username, password, this.console);
|
||||
try {
|
||||
const api = new HikvisionCameraAPI(httpAddress, username, password, this.console);
|
||||
const deviceInfo = await api.getDeviceInfo();
|
||||
|
||||
settings.newCamera = deviceInfo.deviceName;
|
||||
@@ -542,6 +553,15 @@ class HikvisionProvider extends RtspProvider {
|
||||
this.console.error('Error adding Hikvision camera', e);
|
||||
throw e;
|
||||
}
|
||||
|
||||
try {
|
||||
if (await api.checkTwoWayAudio()) {
|
||||
twoWayAudio = 'Hikvision';
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
this.console.warn('Error probing two way audio', e);
|
||||
}
|
||||
}
|
||||
settings.newCamera ||= 'Hikvision Camera';
|
||||
|
||||
@@ -553,6 +573,8 @@ class HikvisionProvider extends RtspProvider {
|
||||
device.putSetting('password', password);
|
||||
device.setIPAddress(settings.ip?.toString());
|
||||
device.setHttpPortOverride(settings.httpPort?.toString());
|
||||
if (twoWayAudio)
|
||||
device.putSetting('twoWayAudio', twoWayAudio);
|
||||
return nativeId;
|
||||
}
|
||||
|
||||
|
||||
34
plugins/hikvision/src/probe.ts
Normal file
34
plugins/hikvision/src/probe.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import https from 'https';
|
||||
import AxiosDigestAuth from '@koush/axios-digest-auth';
|
||||
|
||||
export const hikvisionHttpsAgent = new https.Agent({
|
||||
rejectUnauthorized: false,
|
||||
});
|
||||
|
||||
export async function getDeviceInfo(digestAuth: AxiosDigestAuth, address: string) {
|
||||
try {
|
||||
const response = await digestAuth.request({
|
||||
httpsAgent: hikvisionHttpsAgent,
|
||||
method: "GET",
|
||||
responseType: 'text',
|
||||
url: `http://${address}/ISAPI/System/deviceInfo`,
|
||||
});
|
||||
const deviceModel = response.data.match(/>(.*?)<\/model>/)?.[1];
|
||||
const deviceName = response.data.match(/>(.*?)<\/deviceName>/)?.[1];
|
||||
const serialNumber = response.data.match(/>(.*?)<\/serialNumber>/)?.[1];
|
||||
const macAddress = response.data.match(/>(.*?)<\/macAddress>/)?.[1];
|
||||
const firmwareVersion = response.data.match(/>(.*?)<\/firmwareVersion>/)?.[1];
|
||||
return {
|
||||
deviceModel,
|
||||
deviceName,
|
||||
serialNumber,
|
||||
macAddress,
|
||||
firmwareVersion,
|
||||
};
|
||||
}
|
||||
catch (e) {
|
||||
if (e?.response?.data?.includes('notActivated'))
|
||||
throw new Error(`Camera must be first be activated at http://${address}.`)
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,11 @@ export function nextSequenceNumber(current: number, increment = 1) {
|
||||
return (current + increment + 0x10000) % 0x10000;
|
||||
}
|
||||
|
||||
const maxRtpTimestamp = BigInt(0xFFFFFFFF);
|
||||
export function addRtpTimestamp(current: number, adjust: number) {
|
||||
return Number(maxRtpTimestamp & (BigInt(current) + BigInt(adjust)));
|
||||
}
|
||||
|
||||
export function isNextSequenceNumber(current: number, next: number) {
|
||||
return nextSequenceNumber(current) === next;
|
||||
}
|
||||
|
||||
2
plugins/objectdetector/.vscode/launch.json
vendored
2
plugins/objectdetector/.vscode/launch.json
vendored
@@ -10,7 +10,7 @@
|
||||
"port": 10081,
|
||||
"request": "attach",
|
||||
"skipFiles": [
|
||||
"**/plugin-remote-worker.*",
|
||||
"**/plugin-console.*",
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"preLaunchTask": "scrypted: deploy+debug",
|
||||
|
||||
Submodule plugins/objectdetector/node-moving-things-tracker deleted from a71d4f6307
294
plugins/objectdetector/package-lock.json
generated
294
plugins/objectdetector/package-lock.json
generated
@@ -1,19 +1,18 @@
|
||||
{
|
||||
"name": "@scrypted/objectdetector",
|
||||
"version": "0.0.92",
|
||||
"version": "0.0.102",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/objectdetector",
|
||||
"version": "0.0.92",
|
||||
"version": "0.0.102",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"jpeg-js": "^0.4.3",
|
||||
"lodash": "^4.17.21",
|
||||
"node-moving-things-tracker": "file:./node-moving-things-tracker",
|
||||
"point-inside-polygon": "^1.0.3",
|
||||
"polygon-overlap": "^1.0.5",
|
||||
"semver": "^7.3.8"
|
||||
@@ -42,7 +41,7 @@
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.2.51",
|
||||
"version": "0.2.71",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
@@ -61,11 +60,11 @@
|
||||
"webpack-bundle-analyzer": "^4.5.0"
|
||||
},
|
||||
"bin": {
|
||||
"scrypted-changelog": "bin/scrypted-changelog.js",
|
||||
"scrypted-debug": "bin/scrypted-debug.js",
|
||||
"scrypted-deploy": "bin/scrypted-deploy.js",
|
||||
"scrypted-deploy-debug": "bin/scrypted-deploy-debug.js",
|
||||
"scrypted-package-json": "bin/scrypted-package-json.js",
|
||||
"scrypted-readme": "bin/scrypted-readme.js",
|
||||
"scrypted-setup-project": "bin/scrypted-setup-project.js",
|
||||
"scrypted-webpack": "bin/scrypted-webpack.js"
|
||||
},
|
||||
@@ -191,28 +190,6 @@
|
||||
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/create-require": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||
@@ -228,67 +205,6 @@
|
||||
"node": ">=0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "7.2.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.1.1",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/inflight": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"once": "^1.3.0",
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/jasmine": {
|
||||
"version": "3.99.0",
|
||||
"resolved": "https://registry.npmjs.org/jasmine/-/jasmine-3.99.0.tgz",
|
||||
"integrity": "sha512-YIThBuHzaIIcjxeuLmPD40SjxkEcc8i//sGMDKCgkRMVgIwRJf5qyExtlJpQeh7pkeoBSOe6lQEdg+/9uKg9mw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"glob": "^7.1.6",
|
||||
"jasmine-core": "~3.99.0"
|
||||
},
|
||||
"bin": {
|
||||
"jasmine": "bin/jasmine.js"
|
||||
}
|
||||
},
|
||||
"node_modules/jasmine-core": {
|
||||
"version": "3.99.1",
|
||||
"resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.99.1.tgz",
|
||||
"integrity": "sha512-Hu1dmuoGcZ7AfyynN3LsfruwMbxMALMka+YtZeGoLuDEySVmVAPaonkNoBRIw/ectu8b9tVQCJNgp4a4knp+tg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/jpeg-js": {
|
||||
"version": "0.4.4",
|
||||
"resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz",
|
||||
@@ -304,11 +220,6 @@
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"node_modules/lodash.isequal": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
|
||||
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
@@ -326,53 +237,6 @@
|
||||
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz",
|
||||
"integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/munkres-js": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/munkres-js/-/munkres-js-1.2.2.tgz",
|
||||
"integrity": "sha512-0oF4tBDvzx20CYzQ44tTJMfwTBJWXe7cE73Sa/u7Mz7X8jRtyOXOGE9kJBhCfX7Akku3Iy/WHa0sRgqLRq2xaQ=="
|
||||
},
|
||||
"node_modules/node-moving-things-tracker": {
|
||||
"resolved": "node-moving-things-tracker",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/path-is-absolute": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/point-inside-polygon": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/point-inside-polygon/-/point-inside-polygon-1.0.3.tgz",
|
||||
@@ -463,27 +327,12 @@
|
||||
"node": ">=4.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
|
||||
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
|
||||
"deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.",
|
||||
"bin": {
|
||||
"uuid": "bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/v8-compile-cache-lib": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
@@ -500,6 +349,7 @@
|
||||
},
|
||||
"node-moving-things-tracker": {
|
||||
"version": "0.9.1",
|
||||
"extraneous": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lodash.isequal": "^4.5.0",
|
||||
@@ -642,28 +492,6 @@
|
||||
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
|
||||
"dev": true
|
||||
},
|
||||
"balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||
"dev": true
|
||||
},
|
||||
"create-require": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||
@@ -676,58 +504,6 @@
|
||||
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
|
||||
"dev": true
|
||||
},
|
||||
"fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
|
||||
"dev": true
|
||||
},
|
||||
"glob": {
|
||||
"version": "7.2.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.1.1",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"inflight": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"once": "^1.3.0",
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"dev": true
|
||||
},
|
||||
"jasmine": {
|
||||
"version": "3.99.0",
|
||||
"resolved": "https://registry.npmjs.org/jasmine/-/jasmine-3.99.0.tgz",
|
||||
"integrity": "sha512-YIThBuHzaIIcjxeuLmPD40SjxkEcc8i//sGMDKCgkRMVgIwRJf5qyExtlJpQeh7pkeoBSOe6lQEdg+/9uKg9mw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"glob": "^7.1.6",
|
||||
"jasmine-core": "~3.99.0"
|
||||
}
|
||||
},
|
||||
"jasmine-core": {
|
||||
"version": "3.99.1",
|
||||
"resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.99.1.tgz",
|
||||
"integrity": "sha512-Hu1dmuoGcZ7AfyynN3LsfruwMbxMALMka+YtZeGoLuDEySVmVAPaonkNoBRIw/ectu8b9tVQCJNgp4a4knp+tg==",
|
||||
"dev": true
|
||||
},
|
||||
"jpeg-js": {
|
||||
"version": "0.4.4",
|
||||
"resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz",
|
||||
@@ -743,11 +519,6 @@
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"lodash.isequal": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
|
||||
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="
|
||||
},
|
||||
"lru-cache": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
@@ -762,50 +533,6 @@
|
||||
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
||||
"dev": true
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
},
|
||||
"minimist": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz",
|
||||
"integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g=="
|
||||
},
|
||||
"munkres-js": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/munkres-js/-/munkres-js-1.2.2.tgz",
|
||||
"integrity": "sha512-0oF4tBDvzx20CYzQ44tTJMfwTBJWXe7cE73Sa/u7Mz7X8jRtyOXOGE9kJBhCfX7Akku3Iy/WHa0sRgqLRq2xaQ=="
|
||||
},
|
||||
"node-moving-things-tracker": {
|
||||
"version": "file:node-moving-things-tracker",
|
||||
"requires": {
|
||||
"jasmine": "^3.6.1",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"minimist": "^1.2.0",
|
||||
"munkres-js": "^1.2.2",
|
||||
"uuid": "^3.2.1"
|
||||
}
|
||||
},
|
||||
"once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"path-is-absolute": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
|
||||
"dev": true
|
||||
},
|
||||
"point-inside-polygon": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/point-inside-polygon/-/point-inside-polygon-1.0.3.tgz",
|
||||
@@ -863,23 +590,12 @@
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"uuid": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
|
||||
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
|
||||
},
|
||||
"v8-compile-cache-lib": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
|
||||
"dev": true
|
||||
},
|
||||
"wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"dev": true
|
||||
},
|
||||
"yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/objectdetector",
|
||||
"version": "0.0.92",
|
||||
"version": "0.0.102",
|
||||
"description": "Scrypted Video Analysis Plugin. Installed alongside a detection service like OpenCV or TensorFlow.",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache-2.0",
|
||||
@@ -35,6 +35,7 @@
|
||||
"name": "Video Analysis Plugin",
|
||||
"type": "API",
|
||||
"interfaces": [
|
||||
"Settings",
|
||||
"MixinProvider"
|
||||
],
|
||||
"realfs": true
|
||||
@@ -42,9 +43,7 @@
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"jpeg-js": "^0.4.3",
|
||||
"lodash": "^4.17.21",
|
||||
"node-moving-things-tracker": "file:./node-moving-things-tracker",
|
||||
"point-inside-polygon": "^1.0.3",
|
||||
"polygon-overlap": "^1.0.5",
|
||||
"semver": "^7.3.8"
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
const Tracker = require('node-moving-things-tracker').Tracker;
|
||||
|
||||
export class DenoisedDetectionEntry<T> {
|
||||
id?: string;
|
||||
boundingBox?: [number, number, number, number];
|
||||
@@ -24,75 +22,18 @@ export interface DenoisedDetectionOptions<T> {
|
||||
now?: number;
|
||||
}
|
||||
|
||||
export interface TrackerItem<T> {
|
||||
x: number,
|
||||
y: number,
|
||||
w: number,
|
||||
h: number,
|
||||
confidence: number,
|
||||
name: string,
|
||||
};
|
||||
|
||||
export interface TrackedItem<T> extends TrackerItem<T> {
|
||||
id: string;
|
||||
isZombie: boolean;
|
||||
bearing: number;
|
||||
frameUnmatchedLeftBeforeDying: number;
|
||||
velocity: {
|
||||
dx: number,
|
||||
dy: number,
|
||||
}
|
||||
}
|
||||
|
||||
export interface DenoisedDetectionState<T> {
|
||||
previousDetections?: DenoisedDetectionEntry<T>[];
|
||||
tracker?: any;
|
||||
tracked?: TrackedItem<T>[];
|
||||
frameCount?: number;
|
||||
lastDetection?: number;
|
||||
// id to time
|
||||
externallyTracked?: Map<string, DenoisedDetectionEntry<T>>;
|
||||
}
|
||||
|
||||
type Rectangle = {
|
||||
xmin: number;
|
||||
xmax: number;
|
||||
ymin: number;
|
||||
ymax: number;
|
||||
};
|
||||
|
||||
function intersect_area(a: Rectangle, b: Rectangle) {
|
||||
const dx = Math.min(a.xmax, b.xmax) - Math.max(a.xmin, b.xmin)
|
||||
const dy = Math.min(a.ymax, b.ymax) - Math.max(a.ymin, b.ymin)
|
||||
if (dx >= 0 && dy >= 0)
|
||||
return dx * dy
|
||||
}
|
||||
|
||||
function trackedItemToRectangle(item: TrackedItem<any>): Rectangle {
|
||||
return {
|
||||
xmin: item.x,
|
||||
xmax: item.x + item.w,
|
||||
ymin: item.y,
|
||||
ymax: item.y + item.h,
|
||||
};
|
||||
}
|
||||
|
||||
export function denoiseDetections<T>(state: DenoisedDetectionState<T>,
|
||||
currentDetections: DenoisedDetectionEntry<T>[],
|
||||
options?: DenoisedDetectionOptions<T>
|
||||
) {
|
||||
if (!state.tracker) {
|
||||
state.frameCount = 0;
|
||||
const tracker = Tracker.newTracker();
|
||||
tracker.reset();
|
||||
tracker.setParams({
|
||||
fastDelete: true,
|
||||
unMatchedFramesTolerance: Number.MAX_SAFE_INTEGER,
|
||||
iouLimit: 0.05
|
||||
});
|
||||
state.tracker = tracker;
|
||||
}
|
||||
|
||||
if (!state.previousDetections)
|
||||
state.previousDetections = [];
|
||||
|
||||
@@ -100,157 +41,52 @@ export function denoiseDetections<T>(state: DenoisedDetectionState<T>,
|
||||
const lastDetection = state.lastDetection || now;
|
||||
const sinceLastDetection = now - lastDetection;
|
||||
|
||||
const externallyTracked = currentDetections.filter(d => d.id);
|
||||
if (externallyTracked.length) {
|
||||
if (!state.externallyTracked)
|
||||
state.externallyTracked = new Map();
|
||||
if (!state.externallyTracked)
|
||||
state.externallyTracked = new Map();
|
||||
|
||||
for (const tracked of currentDetections) {
|
||||
tracked.durationGone = 0;
|
||||
tracked.lastSeen = now;
|
||||
tracked.lastBox = tracked.boundingBox;
|
||||
for (const tracked of currentDetections) {
|
||||
tracked.durationGone = 0;
|
||||
tracked.lastSeen = now;
|
||||
tracked.lastBox = tracked.boundingBox;
|
||||
|
||||
if (!tracked.id) {
|
||||
const id = tracked.id = `untracked-${tracked.name}`;
|
||||
if (!state.externallyTracked.get(id)) {
|
||||
// crappy track untracked objects for 1 minute.
|
||||
setTimeout(() => state.externallyTracked.delete(id), 60000);
|
||||
}
|
||||
}
|
||||
|
||||
let previous = state.externallyTracked.get(tracked.id);
|
||||
if (previous) {
|
||||
state.externallyTracked.delete(tracked.id);
|
||||
tracked.firstSeen = previous.firstSeen;
|
||||
tracked.firstBox = previous.firstBox;
|
||||
|
||||
previous.durationGone = 0;
|
||||
previous.lastSeen = now;
|
||||
previous.lastBox = tracked.boundingBox;
|
||||
options?.retained(tracked, previous);
|
||||
}
|
||||
else {
|
||||
tracked.firstSeen = now;
|
||||
tracked.firstBox = tracked.lastBox = tracked.boundingBox;
|
||||
options?.added(tracked);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
for (const previous of state.externallyTracked.values()) {
|
||||
if (now - previous.lastSeen) {
|
||||
previous.durationGone += sinceLastDetection;
|
||||
if (previous.durationGone >= options.timeout) {
|
||||
options?.expiring(previous);
|
||||
}
|
||||
if (!tracked.id) {
|
||||
const id = tracked.id = `untracked-${tracked.name}`;
|
||||
if (!state.externallyTracked.get(id)) {
|
||||
// crappy track untracked objects for 1 minute.
|
||||
setTimeout(() => state.externallyTracked.delete(id), 60000);
|
||||
}
|
||||
}
|
||||
|
||||
for (const tracked of currentDetections) {
|
||||
state.externallyTracked.set(tracked.id, tracked);
|
||||
}
|
||||
}
|
||||
let previous = state.externallyTracked.get(tracked.id);
|
||||
if (previous) {
|
||||
state.externallyTracked.delete(tracked.id);
|
||||
tracked.firstSeen = previous.firstSeen;
|
||||
tracked.firstBox = previous.firstBox;
|
||||
|
||||
if (state.externallyTracked)
|
||||
return;
|
||||
|
||||
const { tracker, previousDetections } = state;
|
||||
|
||||
const items: TrackerItem<T>[] = currentDetections.filter(cd => cd.boundingBox).map(cd => {
|
||||
const [x, y, w, h] = cd.boundingBox;
|
||||
return {
|
||||
x, y, w, h,
|
||||
confidence: cd.score,
|
||||
name: cd.name,
|
||||
}
|
||||
});
|
||||
|
||||
tracker.updateTrackedItemsWithNewFrame(items, state.frameCount);
|
||||
// console.log(tracker.getAllTrackedItems());
|
||||
const trackedObjects: TrackedItem<T>[] = [...tracker.getTrackedItems().values()];
|
||||
// for (const to of trackedObjects) {
|
||||
// console.log(to.velocity);
|
||||
// }
|
||||
|
||||
|
||||
const previousCopy = previousDetections.slice();
|
||||
previousDetections.splice(0, previousDetections.length);
|
||||
const map = new Map<string, DenoisedDetectionEntry<T>>();
|
||||
for (const pd of previousCopy) {
|
||||
map.set(pd.id, pd);
|
||||
}
|
||||
|
||||
for (const trackedObject of trackedObjects) {
|
||||
map.delete(trackedObject.id);
|
||||
|
||||
const previous = previousCopy.find(d => d.id === trackedObject.id);
|
||||
const current = currentDetections.find(d => {
|
||||
const [x, y, w, h] = d.boundingBox;
|
||||
return !d.id && x === trackedObject.x && y === trackedObject.y && w === trackedObject.w && h === trackedObject.h;
|
||||
});
|
||||
|
||||
if (current) {
|
||||
current.id = trackedObject.id;
|
||||
current.lastSeen = now;
|
||||
current.durationGone = 0;
|
||||
if (previous) {
|
||||
previous.lastSeen = now;
|
||||
current.firstSeen = previous.firstSeen;
|
||||
current.firstBox = previous.firstBox;
|
||||
current.lastBox = previous.boundingBox;
|
||||
previous.lastBox = current.boundingBox;
|
||||
previous.durationGone = 0;
|
||||
options.retained?.(current, previous);
|
||||
}
|
||||
else {
|
||||
current.firstSeen = now;
|
||||
current.firstBox = current.boundingBox;
|
||||
current.lastBox = current.boundingBox;
|
||||
options.added?.(current);
|
||||
}
|
||||
|
||||
previousDetections.push(current);
|
||||
}
|
||||
else if (previous) {
|
||||
previous.durationGone += sinceLastDetection;
|
||||
if (previous.durationGone >= options.timeout) {
|
||||
let foundContainer = false;
|
||||
// the detector may combine multiple detections into one.
|
||||
// handle that scenario by not expiring the individual detections that
|
||||
// are globbed into a larger one.
|
||||
for (const other of trackedObjects) {
|
||||
if (other === trackedObject || other.isZombie)
|
||||
continue;
|
||||
const area = intersect_area(trackedItemToRectangle(trackedObject), trackedItemToRectangle(other));
|
||||
if (area) {
|
||||
const trackedObjectArea = trackedObject.w * trackedObject.h;
|
||||
if (area / trackedObjectArea > .5) {
|
||||
foundContainer = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!foundContainer)
|
||||
trackedObject.frameUnmatchedLeftBeforeDying = -1;
|
||||
// else
|
||||
// console.log('globbed!');
|
||||
}
|
||||
else {
|
||||
options.expiring?.(previous);
|
||||
previousDetections.push(previous);
|
||||
}
|
||||
previous.durationGone = 0;
|
||||
previous.lastSeen = now;
|
||||
previous.lastBox = tracked.boundingBox;
|
||||
options?.retained(tracked, previous);
|
||||
}
|
||||
else {
|
||||
// console.warn('unprocessed denoised detection?', trackedObject);
|
||||
tracked.firstSeen = now;
|
||||
tracked.firstBox = tracked.lastBox = tracked.boundingBox;
|
||||
options?.added(tracked);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
for (const previous of state.externallyTracked.values()) {
|
||||
if (now - previous.lastSeen) {
|
||||
previous.durationGone += sinceLastDetection;
|
||||
if (previous.durationGone >= options.timeout) {
|
||||
options?.expiring(previous);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// should never reach here?
|
||||
for (const r of map.values()) {
|
||||
options.removed?.(r)
|
||||
for (const tracked of currentDetections) {
|
||||
state.externallyTracked.set(tracked.id, tracked);
|
||||
}
|
||||
|
||||
state.tracked = trackedObjects;
|
||||
state.lastDetection = now;
|
||||
state.frameCount++;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import sdk, { Camera, DeviceState, EventListenerRegister, MediaObject, MixinDeviceBase, MixinProvider, MotionSensor, ObjectDetection, ObjectDetectionCallbacks, ObjectDetectionModel, ObjectDetectionResult, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, ScryptedNativeId, Setting, Settings, VideoCamera } from '@scrypted/sdk';
|
||||
import sdk, { VideoFrameGenerator, Camera, DeviceState, EventListenerRegister, MediaObject, MixinDeviceBase, MixinProvider, MotionSensor, ObjectDetection, ObjectDetectionCallbacks, ObjectDetectionModel, ObjectDetectionResult, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, ScryptedNativeId, Setting, Settings, SettingValue, VideoCamera } from '@scrypted/sdk';
|
||||
import { StorageSettings } from '@scrypted/sdk/storage-settings';
|
||||
import crypto from 'crypto';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
@@ -7,7 +7,7 @@ import { SettingsMixinDeviceBase } from "../../../common/src/settings-mixin";
|
||||
import { DenoisedDetectionEntry, DenoisedDetectionState, denoiseDetections } from './denoise';
|
||||
import { serverSupportsMixinEventMasking } from './server-version';
|
||||
import { sleep } from './sleep';
|
||||
import { safeParseJson } from './util';
|
||||
import { getAllDevices, safeParseJson } from './util';
|
||||
|
||||
const polygonOverlap = require('polygon-overlap');
|
||||
const insidePolygon = require('point-inside-polygon');
|
||||
@@ -24,6 +24,8 @@ const defaultSecondScoreThreshold = .7;
|
||||
const BUILTIN_MOTION_SENSOR_ASSIST = 'Assist';
|
||||
const BUILTIN_MOTION_SENSOR_REPLACE = 'Replace';
|
||||
|
||||
const objectDetectionPrefix = `${ScryptedInterface.ObjectDetection}:`;
|
||||
|
||||
type ClipPath = [number, number][];
|
||||
type Zones = { [zone: string]: ClipPath };
|
||||
interface ZoneInfo {
|
||||
@@ -126,7 +128,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
analyzeStop = 0;
|
||||
lastDetectionInput = 0;
|
||||
|
||||
constructor(mixinDevice: VideoCamera & Camera & MotionSensor & ObjectDetector & Settings, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: { [key: string]: any }, providerNativeId: string, public objectDetection: ObjectDetection & ScryptedDevice, modelName: string, group: string, public hasMotionType: boolean, public settings: Setting[]) {
|
||||
constructor(public plugin: ObjectDetectionPlugin, mixinDevice: VideoCamera & Camera & MotionSensor & ObjectDetector & Settings, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: { [key: string]: any }, providerNativeId: string, public objectDetection: ObjectDetection & ScryptedDevice, modelName: string, group: string, public hasMotionType: boolean, public settings: Setting[]) {
|
||||
super({
|
||||
mixinDevice, mixinDeviceState,
|
||||
mixinProviderNativeId: providerNativeId,
|
||||
@@ -187,16 +189,19 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
|| setting.value;
|
||||
}
|
||||
|
||||
if (this.hasMotionType)
|
||||
ret['motionAsObjects'] = this.storageSettings.values.motionAsObjects;
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
async snapshotDetection() {
|
||||
const picture = await this.cameraDevice.takePicture();
|
||||
const detections = await this.objectDetection.detectObjects(picture, {
|
||||
let detections = await this.objectDetection.detectObjects(picture, {
|
||||
detectionId: this.detectionId,
|
||||
settings: this.getCurrentSettings(),
|
||||
});
|
||||
this.trackObjects(detections, true);
|
||||
detections = await this.trackObjects(detections, true);
|
||||
this.reportObjectDetections(detections);
|
||||
}
|
||||
|
||||
@@ -212,6 +217,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
this.detectorRunning = false;
|
||||
this.objectDetection?.detectObjects(undefined, {
|
||||
detectionId: this.detectionId,
|
||||
settings: this.getCurrentSettings(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -308,8 +314,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
async handleDetectionEvent(detection: ObjectsDetected, redetect?: (boundingBox: [number, number, number, number]) => Promise<ObjectDetectionResult[]>, mediaObject?: MediaObject) {
|
||||
this.detectorRunning = detection.running;
|
||||
|
||||
// track the objects on a pre-zoned set.
|
||||
this.trackObjects(detection);
|
||||
detection = await this.trackObjects(detection);
|
||||
|
||||
// apply the zones to the detections and get a shallow copy list of detections after
|
||||
// exclusion zones have applied
|
||||
@@ -347,7 +352,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
}
|
||||
// retain passing the second pass threshold for first time.
|
||||
if (d.bestSecondPassScore < this.secondScoreThreshold && secondPassScore >= this.secondScoreThreshold) {
|
||||
this.console.log('improved', d.id, d.bestSecondPassScore, d.score);
|
||||
this.console.log('improved', d.id, secondPassScore, d.score);
|
||||
better = true;
|
||||
retainImage = true;
|
||||
}
|
||||
@@ -401,7 +406,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
if (this.lastDetectionInput + this.storageSettings.values.detectionTimeout * 1000 < Date.now())
|
||||
retainImage = true;
|
||||
|
||||
if (retainImage) {
|
||||
if (retainImage && mediaObject) {
|
||||
this.lastDetectionInput = now;
|
||||
this.console.log('retaining detection image');
|
||||
this.setDetection(detection, mediaObject);
|
||||
@@ -436,6 +441,8 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
return;
|
||||
|
||||
this.detectorRunning = true;
|
||||
this.analyzeStop = Date.now() + this.getDetectionDuration();
|
||||
|
||||
while (this.detectorRunning) {
|
||||
const now = Date.now();
|
||||
if (now > this.analyzeStop)
|
||||
@@ -445,12 +452,13 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
reason: 'event',
|
||||
});
|
||||
const found = await this.objectDetection.detectObjects(mo, {
|
||||
detectionId: this.detectionId,
|
||||
duration: this.getDetectionDuration(),
|
||||
settings: this.getCurrentSettings(),
|
||||
});
|
||||
found.running = this.detectorRunning;
|
||||
this.handleDetectionEvent(found, undefined, mo);
|
||||
}, this);
|
||||
}
|
||||
catch (e) {
|
||||
this.console.error('snapshot detection error', e);
|
||||
}
|
||||
// cameras tend to only refresh every 1s at best.
|
||||
// maybe get this value from somewhere? or sha the jpeg?
|
||||
@@ -458,17 +466,82 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
if (diff > 0)
|
||||
await sleep(diff);
|
||||
}
|
||||
this.detectorRunning = false;
|
||||
this.handleDetectionEvent({
|
||||
detectionId: this.detectionId,
|
||||
running: false,
|
||||
detections: [],
|
||||
timestamp: Date.now(),
|
||||
}, undefined, undefined);
|
||||
this.endObjectDetection();
|
||||
}
|
||||
|
||||
async startPipelineAnalysis() {
|
||||
if (this.detectorRunning)
|
||||
return;
|
||||
|
||||
const stream = await this.cameraDevice.getVideoStream({
|
||||
destination: 'local-recorder',
|
||||
// ask rebroadcast to mute audio, not needed.
|
||||
audio: null,
|
||||
});
|
||||
|
||||
this.detectorRunning = true;
|
||||
this.analyzeStop = Date.now() + this.getDetectionDuration();
|
||||
|
||||
const videoFrameGenerator = this.newPipeline as VideoFrameGenerator;
|
||||
|
||||
try {
|
||||
const start = Date.now();
|
||||
let detections = 0;
|
||||
for await (const detected
|
||||
of await this.objectDetection.generateObjectDetections(await videoFrameGenerator.generateVideoFrames(stream), {
|
||||
settings: this.getCurrentSettings(),
|
||||
})) {
|
||||
if (!this.detectorRunning) {
|
||||
break;
|
||||
}
|
||||
const now = Date.now();
|
||||
if (now > this.analyzeStop) {
|
||||
break;
|
||||
}
|
||||
|
||||
// apply the zones to the detections and get a shallow copy list of detections after
|
||||
// exclusion zones have applied
|
||||
const zonedDetections = this.applyZones(detected.detected);
|
||||
const filteredDetections = zonedDetections
|
||||
.filter(d => {
|
||||
if (!d.zones?.length)
|
||||
return d.score >= this.scoreThreshold;
|
||||
|
||||
for (const zone of d.zones || []) {
|
||||
const zi = this.zoneInfos[zone];
|
||||
const scoreThreshold = zi?.scoreThreshold || this.scoreThreshold;
|
||||
if (d.score >= scoreThreshold)
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
detected.detected.detections = filteredDetections;
|
||||
|
||||
detections++;
|
||||
// this.console.warn('dps', detections / (Date.now() - start) * 1000);
|
||||
|
||||
if (detected.detected.detectionId) {
|
||||
const jpeg = await detected.videoFrame.toBuffer({
|
||||
format: 'jpg',
|
||||
});
|
||||
const mo = await sdk.mediaManager.createMediaObject(jpeg, 'image/jpeg');
|
||||
this.setDetection(detected.detected, mo);
|
||||
// this.console.log('image saved', detected.detected.detections);
|
||||
}
|
||||
this.reportObjectDetections(detected.detected);
|
||||
// this.handleDetectionEvent(detected.detected);
|
||||
}
|
||||
}
|
||||
finally {
|
||||
this.endObjectDetection();
|
||||
}
|
||||
}
|
||||
|
||||
async startStreamAnalysis() {
|
||||
if (!this.hasMotionType && this.storageSettings.values.captureMode === 'Snapshot') {
|
||||
if (this.newPipeline) {
|
||||
await this.startPipelineAnalysis();
|
||||
}
|
||||
else if (!this.hasMotionType && this.storageSettings.values.captureMode === 'Snapshot') {
|
||||
await this.startSnapshotAnalysis();
|
||||
}
|
||||
else {
|
||||
@@ -487,6 +560,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
await this.objectDetection?.detectObjects(undefined, {
|
||||
detectionId: this.detectionId,
|
||||
duration: this.getDetectionDuration(),
|
||||
settings: this.getCurrentSettings(),
|
||||
}, this);
|
||||
}
|
||||
catch (e) {
|
||||
@@ -545,7 +619,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
applyZones(detection: ObjectsDetected) {
|
||||
// determine zones of the objects, if configured.
|
||||
if (!detection.detections)
|
||||
return;
|
||||
return [];
|
||||
let copy = detection.detections.slice();
|
||||
for (const o of detection.detections) {
|
||||
if (!o.boundingBox)
|
||||
@@ -641,15 +715,15 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
this.onDeviceEvent(ScryptedInterface.ObjectDetector, detection);
|
||||
}
|
||||
|
||||
trackObjects(detectionResult: ObjectsDetected, showAll?: boolean) {
|
||||
async trackObjects(detectionResult: ObjectsDetected, showAll?: boolean) {
|
||||
// do not denoise
|
||||
if (this.hasMotionType) {
|
||||
return;
|
||||
return detectionResult;
|
||||
}
|
||||
|
||||
if (!detectionResult?.detections) {
|
||||
// detection session ended.
|
||||
return;
|
||||
return detectionResult;
|
||||
}
|
||||
|
||||
const { detections } = detectionResult;
|
||||
@@ -719,6 +793,8 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
if (found.length || showAll) {
|
||||
this.console.log('current detections:', this.detectionState.previousDetections.map(d => `${d.detection.className} (${d.detection.score}, ${d.detection.boundingBox?.join(', ')})`).join(', '));
|
||||
}
|
||||
|
||||
return detectionResult;
|
||||
}
|
||||
|
||||
setDetection(detection: ObjectsDetected, detectionInput: MediaObject) {
|
||||
@@ -772,9 +848,19 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
return BUILTIN_MOTION_SENSOR_REPLACE;
|
||||
}
|
||||
|
||||
get newPipeline() {
|
||||
return this.plugin.storageSettings.values.newPipeline;
|
||||
}
|
||||
|
||||
async getMixinSettings(): Promise<Setting[]> {
|
||||
const settings: Setting[] = [];
|
||||
|
||||
try {
|
||||
this.settings = (await this.objectDetection.getDetectionModel(this.getCurrentSettings())).settings;
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
|
||||
if (this.settings) {
|
||||
settings.push(...this.settings.map(setting =>
|
||||
Object.assign({}, setting, {
|
||||
@@ -785,15 +871,15 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
);
|
||||
}
|
||||
|
||||
settings.push(...await this.storageSettings.getSettings());
|
||||
|
||||
this.storageSettings.settings.motionSensorSupplementation.hide = !this.hasMotionType || !this.mixinDeviceInterfaces.includes(ScryptedInterface.MotionSensor);
|
||||
this.storageSettings.settings.captureMode.hide = this.hasMotionType;
|
||||
this.storageSettings.settings.captureMode.hide = this.hasMotionType || !!this.newPipeline;
|
||||
this.storageSettings.settings.detectionDuration.hide = this.hasMotionType;
|
||||
this.storageSettings.settings.detectionTimeout.hide = this.hasMotionType;
|
||||
this.storageSettings.settings.motionDuration.hide = !this.hasMotionType;
|
||||
this.storageSettings.settings.motionAsObjects.hide = !this.hasMotionType;
|
||||
|
||||
settings.push(...await this.storageSettings.getSettings());
|
||||
|
||||
let hideThreshold = true;
|
||||
if (!this.hasMotionType) {
|
||||
let hasInclusionZone = false;
|
||||
@@ -829,7 +915,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
settings.push({
|
||||
subgroup,
|
||||
key: `zone-${name}`,
|
||||
title: `Edit Zone`,
|
||||
title: `Open Zone Editor`,
|
||||
type: 'clippath',
|
||||
value: JSON.stringify(value),
|
||||
});
|
||||
@@ -987,8 +1073,10 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
}
|
||||
|
||||
class ObjectDetectorMixin extends MixinDeviceBase<ObjectDetection> implements MixinProvider {
|
||||
constructor(mixinDevice: ObjectDetection, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: DeviceState, mixinProviderNativeId: ScryptedNativeId, public model: ObjectDetectionModel) {
|
||||
super({ mixinDevice, mixinDeviceInterfaces, mixinDeviceState, mixinProviderNativeId });
|
||||
currentMixins = new Set<ObjectDetectionMixin>();
|
||||
|
||||
constructor(mixinDevice: ObjectDetection, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: DeviceState, public plugin: ObjectDetectionPlugin, public model: ObjectDetectionModel) {
|
||||
super({ mixinDevice, mixinDeviceInterfaces, mixinDeviceState, mixinProviderNativeId: plugin.nativeId });
|
||||
|
||||
// trigger mixin creation. todo: fix this to not be stupid hack.
|
||||
for (const id of Object.keys(systemManager.getSystemState())) {
|
||||
@@ -1000,55 +1088,94 @@ class ObjectDetectorMixin extends MixinDeviceBase<ObjectDetection> implements Mi
|
||||
}
|
||||
|
||||
async canMixin(type: ScryptedDeviceType, interfaces: string[]): Promise<string[]> {
|
||||
// filter out
|
||||
for (const iface of interfaces) {
|
||||
if (iface.startsWith(`${ScryptedInterface.ObjectDetection}:`)) {
|
||||
const deviceMatch = this.mixinDeviceInterfaces.find(miface => miface.startsWith(iface));
|
||||
if (deviceMatch)
|
||||
continue;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
const hasMotionType = this.model.classes.includes('motion');
|
||||
const prefix = `${objectDetectionPrefix}${hasMotionType}`;
|
||||
const thisPrefix = `${prefix}:${this.id}`;
|
||||
|
||||
const found = interfaces.find(iface => iface.startsWith(prefix) && iface !== thisPrefix);
|
||||
if (found)
|
||||
return;
|
||||
// this.console.log('found', found);
|
||||
|
||||
if ((type === ScryptedDeviceType.Camera || type === ScryptedDeviceType.Doorbell) && (interfaces.includes(ScryptedInterface.VideoCamera) || interfaces.includes(ScryptedInterface.Camera))) {
|
||||
const ret: string[] = [ScryptedInterface.ObjectDetector, ScryptedInterface.Settings];
|
||||
const ret: string[] = [
|
||||
ScryptedInterface.ObjectDetector,
|
||||
ScryptedInterface.Settings,
|
||||
thisPrefix,
|
||||
];
|
||||
const model = await this.mixinDevice.getDetectionModel();
|
||||
if (model.classes?.includes('motion')) {
|
||||
// const vamotion = 'mixin:@scrypted/objectdetector:motion';
|
||||
// if (interfaces.includes(vamotion))
|
||||
// return;
|
||||
|
||||
if (model.classes?.includes('motion')) {
|
||||
ret.push(
|
||||
ScryptedInterface.MotionSensor,
|
||||
// vamotion,
|
||||
);
|
||||
}
|
||||
return ret;
|
||||
|
||||
return ret;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async getMixin(mixinDevice: any, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: { [key: string]: any }) {
|
||||
let objectDetection = systemManager.getDeviceById<ObjectDetection>(this.id);
|
||||
const group = objectDetection.name.replace('Plugin', '').trim();
|
||||
|
||||
const hasMotionType = this.model.classes.includes('motion');
|
||||
const group = hasMotionType ? 'Motion Detection' : 'Object Detection';
|
||||
// const group = objectDetection.name.replace('Plugin', '').trim();
|
||||
|
||||
const settings = this.model.settings;
|
||||
|
||||
return new ObjectDetectionMixin(mixinDevice, mixinDeviceInterfaces, mixinDeviceState, this.mixinProviderNativeId, objectDetection, this.model.name, group, hasMotionType, settings);
|
||||
const ret = new ObjectDetectionMixin(this.plugin, mixinDevice, mixinDeviceInterfaces, mixinDeviceState, this.mixinProviderNativeId, objectDetection, this.model.name, group, hasMotionType, settings);
|
||||
this.currentMixins.add(ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
async releaseMixin(id: string, mixinDevice: any) {
|
||||
this.currentMixins.delete(mixinDevice);
|
||||
return mixinDevice.release();
|
||||
}
|
||||
}
|
||||
|
||||
class ObjectDetectionPlugin extends AutoenableMixinProvider {
|
||||
class ObjectDetectionPlugin extends AutoenableMixinProvider implements Settings {
|
||||
currentMixins = new Set<ObjectDetectorMixin>();
|
||||
|
||||
storageSettings = new StorageSettings(this, {
|
||||
newPipeline: {
|
||||
title: 'New Video Pipeline',
|
||||
description: 'WARNING! DO NOT ENABLE: Use the new video pipeline. Leave blank to use the legacy pipeline.',
|
||||
type: 'device',
|
||||
deviceFilter: `interfaces.includes('${ScryptedInterface.VideoFrameGenerator}')`,
|
||||
},
|
||||
activeMotionDetections: {
|
||||
title: 'Active Motion Detection Sessions',
|
||||
readonly: true,
|
||||
mapGet: () => {
|
||||
return [...this.currentMixins.values()]
|
||||
.reduce((c1, v1) => c1 + [...v1.currentMixins.values()]
|
||||
.reduce((c2, v2) => c2 + (v2.hasMotionType && v2.detectorRunning ? 1 : 0), 0), 0);
|
||||
}
|
||||
},
|
||||
activeObjectDetections: {
|
||||
title: 'Active Object Detection Sessions',
|
||||
readonly: true,
|
||||
mapGet: () => {
|
||||
return [...this.currentMixins.values()]
|
||||
.reduce((c1, v1) => c1 + [...v1.currentMixins.values()]
|
||||
.reduce((c2, v2) => c2 + (!v2.hasMotionType && v2.detectorRunning ? 1 : 0), 0), 0);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
constructor(nativeId?: ScryptedNativeId) {
|
||||
super(nativeId);
|
||||
}
|
||||
|
||||
getSettings(): Promise<Setting[]> {
|
||||
return this.storageSettings.getSettings();
|
||||
}
|
||||
|
||||
putSetting(key: string, value: SettingValue): Promise<void> {
|
||||
return this.storageSettings.putSetting(key, value);
|
||||
}
|
||||
|
||||
async canMixin(type: ScryptedDeviceType, interfaces: string[]): Promise<string[]> {
|
||||
if (!interfaces.includes(ScryptedInterface.ObjectDetection))
|
||||
return;
|
||||
@@ -1057,12 +1184,15 @@ class ObjectDetectionPlugin extends AutoenableMixinProvider {
|
||||
|
||||
async getMixin(mixinDevice: ObjectDetection, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: { [key: string]: any; }): Promise<any> {
|
||||
const model = await mixinDevice.getDetectionModel();
|
||||
return new ObjectDetectorMixin(mixinDevice, mixinDeviceInterfaces, mixinDeviceState, this.nativeId, model);
|
||||
const ret = new ObjectDetectorMixin(mixinDevice, mixinDeviceInterfaces, mixinDeviceState, this, model);
|
||||
this.currentMixins.add(ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
async releaseMixin(id: string, mixinDevice: any): Promise<void> {
|
||||
// what does this mean to make a mixin provider no longer available?
|
||||
// just ignore it until reboot?
|
||||
this.currentMixins.delete(mixinDevice);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import sdk from '@scrypted/sdk';
|
||||
|
||||
export function safeParseJson(value: string) {
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
@@ -5,3 +7,7 @@ export function safeParseJson(value: string) {
|
||||
catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
export function getAllDevices() {
|
||||
return Object.keys(sdk.systemManager.getSystemState()).map(id => sdk.systemManager.getDeviceById(id));
|
||||
}
|
||||
|
||||
4
plugins/onvif/package-lock.json
generated
4
plugins/onvif/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/onvif",
|
||||
"version": "0.0.109",
|
||||
"version": "0.0.117",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/onvif",
|
||||
"version": "0.0.109",
|
||||
"version": "0.0.117",
|
||||
"license": "Apache",
|
||||
"dependencies": {
|
||||
"@koush/axios-digest-auth": "^0.8.5",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user