Compare commits

..

64 Commits

Author SHA1 Message Date
Koushik Dutta
801bd46730 server: default auth should be last available option 2024-01-31 12:10:25 -08:00
Koushik Dutta
e5a764d82f reolink: use onvif events on doorbell 2024-01-31 12:10:08 -08:00
Koushik Dutta
7c9cd9f112 postrelease 2024-01-30 10:03:47 -08:00
Koushik Dutta
59e1391fae fix restore on lxc 2024-01-30 10:03:31 -08:00
Koushik Dutta
e0a6e66e8a core: fix lag with terminal input 2024-01-29 22:17:08 -08:00
Koushik Dutta
fa7071b335 Merge branch 'main' of github.com:koush/scrypted 2024-01-29 21:02:05 -08:00
Koushik Dutta
c28e60d875 tensorflow-lite: switch default model to efficientdet_lite0_320_ptq 2024-01-29 21:02:00 -08:00
Koushik Dutta
62cbb88207 install: ensure local/docker services cant run concurrently 2024-01-29 18:21:14 -08:00
Brett Jia
4b6a858f2b ui: add client-side flow control (#1290) 2024-01-29 13:59:51 -08:00
Matthew Lieder
97a254b5d2 synology-ss: make login more resilient (#1289)
Fixes #1266
2024-01-28 15:27:02 -08:00
Koushik Dutta
0ada6286e7 openvino: update dep 2024-01-25 19:57:16 -08:00
Koushik Dutta
9f12e6dd6e ha: publish 2024-01-25 08:51:41 -08:00
Koushik Dutta
604798e845 docker: fixup /dev/dri enabling 2024-01-25 08:26:40 -08:00
Koushik Dutta
d912266de1 postrelease 2024-01-24 23:47:11 -08:00
Koushik Dutta
f5a32489d7 server: prevent windows from clobbering python path 2024-01-24 23:46:58 -08:00
Koushik Dutta
135ad8e3a8 rebroadcast: add id suffix to rtsp urls to determine ffmpeg usage 2024-01-24 13:52:35 -08:00
Koushik Dutta
3c4021c66b videoanalysis: disable filters for objects that are in detector provided zones 2024-01-23 21:42:36 -08:00
Koushik Dutta
669ab17772 docker: remove nvr storage config prompt 2024-01-23 15:47:48 -08:00
Koushik Dutta
1860d7d8ea Merge branch 'main' of github.com:koush/scrypted 2024-01-23 15:46:53 -08:00
Koushik Dutta
fa266e9dd1 docker: validate the storage directory 2024-01-23 20:10:02 +00:00
Koushik Dutta
4e2f3bf2c7 docker: finish drive setup script 2024-01-23 19:40:16 +00:00
Koushik Dutta
146e27fd13 install: initial pass at disk setup 2024-01-23 19:00:51 +00:00
Koushik Dutta
e4bb50375f postbeta 2024-01-22 20:15:16 -08:00
Koushik Dutta
9686315c02 postbeta 2024-01-22 19:57:24 -08:00
Koushik Dutta
520895f3aa postbeta 2024-01-22 19:50:20 -08:00
Koushik Dutta
ddffc49bcf postbeta 2024-01-22 19:22:20 -08:00
Koushik Dutta
a07f52445d postbeta 2024-01-22 19:13:41 -08:00
Koushik Dutta
5e7b203f11 postbeta 2024-01-22 19:01:13 -08:00
Koushik Dutta
d752298960 postbeta 2024-01-22 17:45:46 -08:00
Koushik Dutta
5253f29831 remove usage of NODE_* env variables which get sanitized by electron. 2024-01-22 17:45:26 -08:00
Koushik Dutta
58d674746d postrelease 2024-01-22 17:22:09 -08:00
Koushik Dutta
8a640758d1 server/cli: fix login issues 2024-01-22 09:11:02 -08:00
Koushik Dutta
9be913af26 sample-cameraprovider: update 2024-01-21 15:39:48 -08:00
Koushik Dutta
da17bee516 sdk: publish 2024-01-21 15:38:28 -08:00
Koushik Dutta
48d9790051 cli: fix https://github.com/koush/scrypted/issues/1277 2024-01-21 14:52:10 -08:00
Koushik Dutta
c43014348d sdk: prevent unnecessary JSON exceptions 2024-01-21 14:42:44 -08:00
Koushik Dutta
cec3a592ba client: add missing dependency 2024-01-21 14:42:24 -08:00
Koushik Dutta
c446ddcdf4 Merge branch 'main' of github.com:koush/scrypted 2024-01-21 12:45:24 -08:00
Koushik Dutta
72f79ea8ef core: fix certificate login error, fix backup/restore in ha 2024-01-21 12:45:19 -08:00
Brett Jia
41988699d0 server: expose backup as a service (#1275)
* server: expose backup as a service

* move restore into new backup service
2024-01-20 21:37:56 -08:00
Koushik Dutta
5151c520d4 Update config.yaml 2024-01-18 19:21:11 -08:00
Koushik Dutta
e1abe717fa postrelease 2024-01-18 13:30:02 -08:00
Koushik Dutta
c7a9ca06be server: use existing service control restart 2024-01-18 13:29:50 -08:00
Koushik Dutta
9827f15f5f server: pass through restart hook 2024-01-18 13:25:22 -08:00
Koushik Dutta
d245a722e2 postrelease 2024-01-18 13:20:25 -08:00
Koushik Dutta
c8e94c0386 Merge branch 'main' of github.com:koush/scrypted 2024-01-18 13:15:41 -08:00
Koushik Dutta
8c6e7b997a ui: implement backup/restore 2024-01-18 13:15:37 -08:00
Johannes Bosecker
9abc7ca139 amcrest: Implemented other intercom codec for Dahua doorbells (G.711A). (#1273) 2024-01-18 12:36:11 -08:00
Koushik Dutta
2a943eb5e0 postbeta 2024-01-18 09:33:13 -08:00
Koushik Dutta
a4fe78a48b ha: publish 2024-01-17 22:07:56 -08:00
Koushik Dutta
50ff0833c9 server: new node min verison 2024-01-17 10:26:30 -08:00
Koushik Dutta
c94085a6c7 zwave: smoke alarm support 2024-01-17 10:25:48 -08:00
Koushik Dutta
c477437456 server: add hook for restart 2024-01-14 15:47:25 -08:00
Koushik Dutta
0da96130fe Merge branch 'main' of github.com:koush/scrypted 2024-01-14 15:36:17 -08:00
Koushik Dutta
fdbf7ab60b server: implement backup/restore 2024-01-14 15:36:08 -08:00
Long Zheng
0cecfb86ff npm-install.sh install auth-fetch package (#1267) 2024-01-14 14:46:23 -08:00
Koushik Dutta
9a195c6207 homekit: fix prune crash 2024-01-14 14:25:30 -08:00
Koushik Dutta
47021a7743 server: reduce deps 2024-01-14 14:25:19 -08:00
Koushik Dutta
01400cf206 core: prep for server fakefs removal 2024-01-14 14:24:43 -08:00
Koushik Dutta
99da29a738 postrelease 2024-01-14 08:06:50 -08:00
Koushik Dutta
6378c5953a server: bump core 2024-01-14 08:06:42 -08:00
Koushik Dutta
846034d7c8 core: fix login 2024-01-14 08:06:25 -08:00
Koushik Dutta
ad47f14922 ha: publish 2024-01-13 22:45:05 -08:00
Koushik Dutta
0066379b1e postrelease 2024-01-13 22:18:49 -08:00
73 changed files with 943 additions and 508 deletions

View File

@@ -3,6 +3,7 @@ import fs from 'fs';
import type { TranspileOptions } from "typescript";
import vm from "vm";
import { ScriptDevice } from "./monaco/script-device";
import path from 'path';
const { systemManager, deviceManager, mediaManager, endpointManager } = sdk;
@@ -22,9 +23,13 @@ export async function tsCompile(source: string, options: TranspileOptions = null
return ts.transpileModule(source, options).outputText;
}
export function readFileAsString(f: string) {
return fs.readFileSync(f).toString();;
}
function getTypeDefs() {
const scryptedTypesDefs = fs.readFileSync('@types/sdk/types.d.ts').toString();
const scryptedIndexDefs = fs.readFileSync('@types/sdk/index.d.ts').toString();
const scryptedTypesDefs = readFileAsString('@types/sdk/types.d.ts');
const scryptedIndexDefs = readFileAsString('@types/sdk/index.d.ts');
return {
scryptedIndexDefs,
scryptedTypesDefs,
@@ -104,7 +109,7 @@ export async function scryptedEval(device: ScryptedDeviceBase, script: string, e
}
export function createMonacoEvalDefaults(extraLibs: { [lib: string]: string }) {
const bufferTypeDefs = fs.readFileSync('@types/node/buffer.d.ts').toString();
const bufferTypeDefs= readFileAsString('@types/node/buffer.d.ts');
const safeLibs = {
bufferTypeDefs,

View File

@@ -1,6 +1,6 @@
# Home Assistant Addon Configuration
name: Scrypted
version: "18-jammy-full.s6-v0.80.0"
version: "18-jammy-full.s6-v0.91.6"
slug: scrypted
description: Scrypted is a high performance home video integration and automation platform
url: "https://github.com/koush/scrypted"

View File

@@ -26,6 +26,10 @@ then
fi
fi
echo "Stopping local service if it is running..."
systemctl stop scrypted.service 2> /dev/null
systemctl disable scrypted.service 2> /dev/null
USER_HOME=$(eval echo ~$SERVICE_USER)
SCRYPTED_HOME=$USER_HOME/.scrypted
mkdir -p $SCRYPTED_HOME
@@ -48,21 +52,12 @@ echo "Created $DOCKER_COMPOSE_YML"
curl -s https://raw.githubusercontent.com/koush/scrypted/main/install/docker/docker-compose.yml | sed s/SET_THIS_TO_SOME_RANDOM_TEXT/"$(echo $RANDOM | md5sum | head -c 32)"/g > $DOCKER_COMPOSE_YML
if [ -d /dev/dri ]
then
sed -i 's/'#' - \/dev\/dri/- \/dev\/dri/g' $DOCKER_COMPOSE_YML
sed -i 's/'#' "\/dev\/dri/"\/dev\/dri/g' $DOCKER_COMPOSE_YML
fi
echo "Setting permissions on $SCRYPTED_HOME"
chown -R $SERVICE_USER $SCRYPTED_HOME
echo "Optional:"
readyn "Edit docker-compose.yml to add external storage for Scrypted NVR?"
if [ "$yn" == "y" ]
then
apt install nano
nano $DOCKER_COMPOSE_YML
fi
set +e
echo "docker compose down"
@@ -85,3 +80,6 @@ echo "Scrypted is now running at: https://localhost:10443/"
echo "Note that it is https and that you'll be asked to approve/ignore the website certificate."
echo
echo
echo "Optional:"
echo "Scrypted NVR Recording storage directory can be configured with an additional script:"
echo "https://docs.scrypted.app/scrypted-nvr/installation.html#docker-volume"

View File

@@ -0,0 +1,140 @@
if [ -z "$SERVICE_USER" ]
then
echo "Scrypted SERVICE_USER environment variable was not specified. NVR Storage can not be configured."
exit 0
fi
if [ "$USER" != "root" ]
then
echo "$USER"
echo "This script must be run as sudo or root."
exit 1
fi
USER_HOME=$(eval echo ~$SERVICE_USER)
SCRYPTED_HOME=$USER_HOME/.scrypted
DOCKER_COMPOSE_YML=$SCRYPTED_HOME/docker-compose.yml
if [ ! -f "$DOCKER_COMPOSE_YML" ]
then
echo "$DOCKER_COMPOSE_YML not found. Install Scrypted first."
exit 1
fi
NVR_MOUNT_LINE=$(cat "$DOCKER_COMPOSE_YML" | grep :/nvr)
if [ -z "$NVR_MOUNT_LINE" ]
then
echo "Unexpected contents in $DOCKER_COMPOSE_YML. Rerun the Scrypted docker compose installer."
exit 1
fi
function backup() {
BACKUP_FILE="$1".scrypted-bak
if [ ! -f "$BACKUP_FILE" ]
then
cp "$1" "$BACKUP_FILE"
fi
}
backup "$DOCKER_COMPOSE_YML"
function readyn() {
while true; do
read -p "$1 (y/n) " yn
case $yn in
[Yy]* ) break;;
[Nn]* ) break;;
* ) echo "Please answer yes or no. (y/n)";;
esac
done
}
if [ -z "$1" ]
then
lsblk
echo ""
echo "Please run the script with an existing mount path or the 'disk' device to format (e.g. sdx)."
exit 1
fi
function stopscrypted() {
cd "$SCRYPTED_HOME"
echo ""
echo "Stopping the Scrypted container. If there are any errors during disk setup, Scrypted will need to be manually restarted with:"
echo "cd $SCRYPTED_HOME && docker compose up -d"
echo ""
docker compose down
}
BLOCK_DEVICE="/dev/$1"
if [ -b "$BLOCK_DEVICE" ]
then
readyn "Format $BLOCK_DEVICE?"
if [ "$yn" == "n" ]
then
exit 1
fi
stopscrypted
umount "$BLOCK_DEVICE"1 2> /dev/null
umount "$BLOCK_DEVICE"2 2> /dev/null
umount /mnt/scrypted-nvr 2> /dev/null
set -e
parted "$BLOCK_DEVICE" --script mklabel gpt
parted -a optimal "$BLOCK_DEVICE" mkpart scrypted-nvr "0%" "100%"
set +e
sync
mkfs -F -t ext4 "$BLOCK_DEVICE"1
sync
# parse/evaluate blkid line as env vars
for attr in $(blkid | grep "$BLOCK_DEVICE")
do
e=$(echo $attr | grep =)
if [ ! -z "$e" ]
then
export "$e"
fi
done
if [ -z "$UUID" ]
then
echo "Error parsing disk UUID."
exit 1
fi
echo "UUID: $UUID"
set -e
backup "/etc/fstab"
grep -v "scrypted-nvr" /etc/fstab > /tmp/fstab && cp /tmp/fstab /etc/fstab
# ensure newline
sed -i -e '$a\' /etc/fstab
mkdir -p /mnt/scrypted-nvr
echo "PARTLABEL=scrypted-nvr /mnt/scrypted-nvr ext4 defaults 0 0" >> /etc/fstab
mount -a
set +e
DIR="/mnt/scrypted-nvr"
else
if [ ! -d "$1" ]
then
echo "$1 is not a valid directory."
exit 1
fi
stopscrypted
DIR="$1"
fi
ESCAPED_DIR=$(echo "$DIR" | sed s/\\//\\\\\\//g)
set -e
sed -i s/'^.*:\/nvr'/" - $ESCAPED_DIR:\/nvr"/ "$DOCKER_COMPOSE_YML"
sed -i s/'^.*SCRYPTED_NVR_VOLUME.*$'/" - SCRYPTED_NVR_VOLUME=\/nvr"/ "$DOCKER_COMPOSE_YML"
set +e
cd "$SCRYPTED_HOME" && docker compose up -d

View File

@@ -89,6 +89,13 @@ USER_HOME=$(eval echo ~$SERVICE_USER)
echo "Setting permissions on $USER_HOME/.scrypted"
chown -R $SERVICE_USER $USER_HOME/.scrypted
echo "Stopping docker service if it exists..."
cd $USER_HOME/.scrypted
echo "docker compose down"
sudo -u $SERVICE_USER docker compose down 2> /dev/null
echo "docker compose rm -rf"
sudo -u $SERVICE_USER docker rm -f /scrypted /scrypted-watchtower 2> /dev/null
echo "Installing Scrypted..."
RUN sudo -u $SERVICE_USER npx -y scrypted@latest install-server

View File

@@ -14,7 +14,7 @@ cd $(dirname $0)
git submodule init
git submodule update
for directory in sdk common server packages/client
for directory in sdk common server packages/client packages/auth-fetch
do
echo "$directory > npm install"
pushd $directory

View File

@@ -21,7 +21,7 @@
],
"preLaunchTask": "npm: build",
"args": [
"shell",
"login",
],
"sourceMaps": true,
"resolveSourceMapLocations": [

View File

@@ -1,12 +1,12 @@
{
"name": "scrypted",
"version": "1.3.6",
"version": "1.3.10",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "scrypted",
"version": "1.3.6",
"version": "1.3.10",
"license": "ISC",
"dependencies": {
"@scrypted/client": "^1.3.3",

View File

@@ -1,6 +1,6 @@
{
"name": "scrypted",
"version": "1.3.6",
"version": "1.3.10",
"description": "",
"main": "./dist/packages/cli/src/main.js",
"bin": {

View File

@@ -4,18 +4,13 @@ import { connectScryptedClient } from '@scrypted/client';
import { FFmpegInput, ScryptedMimeTypes } from '@scrypted/types';
import child_process from 'child_process';
import fs from 'fs';
import https from 'https';
import path from 'path';
import readline from 'readline-sync';
import semver from 'semver';
import { authHttpFetch } from '../../../common/src/http-auth-fetch';
import { httpFetch } from '../../../server/src/fetch/http-fetch';
import { installServe, serveMain } from './service';
import { connectShell } from './shell';
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.')
}
@@ -45,6 +40,12 @@ interface LoginFile {
[host: string]: Login;
}
function basicAuthHeaders(username: string, password: string) {
const headers = new Headers();
headers.set('Authorization', `Basic ${Buffer.from(username + ":" + password).toString('base64')}`);
return headers;
}
async function doLogin(host: string) {
host = toIpAndPort(host);
@@ -54,16 +55,15 @@ async function doLogin(host: string) {
});
const url = `https://${host}/login`;
const response = await authHttpFetch({
const response = await httpFetch({
method: 'GET',
credential: {
username,
password,
},
headers: basicAuthHeaders(username, password),
url,
rejectUnauthorized: false,
responseType: 'json',
});
if (response.body.error)
throw new Error(response.body.error);
fs.mkdirSync(scryptedHome, {
recursive: true,
@@ -81,13 +81,10 @@ async function doLogin(host: string) {
login[host] = response.body;
fs.writeFileSync(loginPath, JSON.stringify(login));
return login;
return login[host];
}
async function getOrDoLogin(host: string): Promise<{
username: string,
token: string,
}> {
async function getOrDoLogin(host: string): Promise<Login> {
let login: LoginFile;
try {
login = JSON.parse(fs.readFileSync(loginPath).toString());
@@ -96,11 +93,12 @@ async function getOrDoLogin(host: string): Promise<{
if (!login[host].username || !login[host].token)
throw new Error();
return login[host];
}
catch (e) {
login = await doLogin(host);
return doLogin(host);
}
return login[host];
}
async function runCommand() {
@@ -150,8 +148,8 @@ async function main() {
}
else if (process.argv[2] === 'login') {
const ip = process.argv[3] || '127.0.0.1';
const token = await doLogin(ip);
console.log('login successful. token:', token);
const login = await doLogin(ip);
console.log('login successful. token:', login.token);
}
else if (process.argv[2] === 'command') {
const { sdk, pendingResult } = await runCommand();
@@ -203,16 +201,15 @@ async function main() {
const login = await getOrDoLogin(ip);
const url = `https://${ip}/web/component/script/install/${pkg}`;
const response = await authHttpFetch({
const response = await httpFetch({
method: 'POST',
credential: {
username: login.username,
password: login.token,
},
headers: basicAuthHeaders(login.username, login.token),
url,
rejectUnauthorized: false,
responseType: 'json',
});
if (response.body.error)
throw new Error(response.body.error);
console.log('install successful. id:', response.body.id);
}
@@ -249,7 +246,6 @@ async function main() {
console.log(' npx scrypted install @scrypted/rtsp');
console.log(' npx scrypted install @scrypted/rtsp/0.0.51');
console.log(' npx scrypted install @scrypted/rtsp/0.0.51 192.168.2.100');
process.exit(1);
}
}

View File

@@ -1,6 +1,6 @@
{
"compilerOptions": {
"module": "commonjs",
"module": "Node16",
"target": "esnext",
"noImplicitAny": true,
"outDir": "./dist",
@@ -8,7 +8,7 @@
"sourceMap": true,
"inlineSources": true,
"declaration": true,
"resolveJsonModule": true,
"moduleResolution": "Node16",
},
"include": [
"src/**/*"

View File

@@ -19,6 +19,8 @@ import { httpFetch } from '../../../server/src/fetch/http-fetch';
let fetcher: typeof httpFetch | typeof domFetch;
try {
if (process.arch === 'browser' as any)
throw new Error();
require('net');
require('events');
fetcher = httpFetch;

View File

@@ -495,6 +495,8 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
return this.onvifIntercom.startIntercom(media);
}
const doorbellType = this.storage.getItem('doorbellType');
// not sure if this all works, since i don't actually have a doorbell.
// good luck!
const channel = this.getRtspChannel() || '1';
@@ -505,12 +507,29 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
const args = ffmpegInput.inputArguments.slice();
args.unshift('-hide_banner');
args.push(
"-vn",
'-acodec', 'aac',
'-f', 'adts',
'pipe:3',
);
let contentType: string;
if (doorbellType == DAHUA_DOORBELL_TYPE) {
args.push(
"-vn",
'-acodec', 'pcm_alaw',
'-ac', '1',
'-ar', '8000',
'-sample_fmt', 's16',
'-f', 'alaw',
'pipe:3',
);
contentType = 'Audio/G.711A';
}
else {
args.push(
"-vn",
'-acodec', 'aac',
'-f', 'adts',
'pipe:3',
);
contentType = 'Audio/AAC';
}
this.console.log('ffmpeg intercom', args);
@@ -533,7 +552,7 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
url,
method: 'POST',
headers: {
'Content-Type': 'Audio/AAC',
'Content-Type': contentType,
'Content-Length': '9999999'
},
responseType: 'readable',

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
import { tsCompile } from '@scrypted/common/src/eval/scrypted-eval';
import { readFileAsString, 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';
@@ -64,8 +64,7 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Eng
constructor() {
super();
this.indexHtml = fs.readFileSync('dist/index.html').toString();
this.indexHtml = readFileAsString('dist/index.html');
(async () => {
await deviceManager.onDeviceDiscovered(

View File

@@ -3,7 +3,6 @@ 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 { deviceManager } = sdk;

View File

@@ -121,7 +121,7 @@
},
"../../../sdk": {
"name": "@scrypted/sdk",
"version": "0.3.4",
"version": "0.3.5",
"license": "ISC",
"dependencies": {
"@babel/preset-typescript": "^7.18.6",
@@ -158,7 +158,7 @@
},
"../../../sdk/types": {
"name": "@scrypted/types",
"version": "0.3.4",
"version": "0.3.5",
"license": "ISC",
"devDependencies": {
"@types/rimraf": "^3.0.2",

View File

@@ -137,7 +137,7 @@ export default {
// cert may need to be reaccepted? Server is down? Go to the
// server root to force the network error to bypass the PWA cache.
if (
e.toString().includes("Network Error") &&
(e.toString().includes("Network Error") || e.toString().includes("Load failed")) &&
window.location.href.startsWith("https:")
) {
window.location = "/";

View File

@@ -21,27 +21,18 @@
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
v-if="!canUpdate"
small
text
href="https://github.com/koush/scrypted#installation"
>More Information</v-btn
>
<v-btn v-if="!canUpdate" small text href="https://github.com/koush/scrypted#installation">More
Information</v-btn>
<v-dialog v-else v-model="updateAndRestart" width="500">
<template v-slot:activator="{ on }">
<v-btn small text color="red" v-on="on"
>Update and Restart Scrypted</v-btn
>
<v-btn small text color="red" v-on="on">Update and Restart Scrypted</v-btn>
</template>
<v-card color="red" dark>
<v-card-title primary-title>Restart Scrypted</v-card-title>
<v-card-text
>Are you sure you want to restart the Scrypted
service?</v-card-text
>
<v-card-text>Are you sure you want to restart the Scrypted
service?</v-card-text>
<v-card-text>{{ restartStatus }}</v-card-text>
<v-divider></v-divider>
@@ -67,10 +58,11 @@
</v-card>
<v-card class="mt-2" v-if="showRestart">
<v-toolbar
><v-toolbar-title>Server Management</v-toolbar-title></v-toolbar
>
<v-toolbar><v-toolbar-title>Server Management</v-toolbar-title></v-toolbar>
<v-card-actions>
<v-btn text :href="backupUrl" color="info">Backup</v-btn>
<v-btn text color="info" @click="restoreClick">Restore</v-btn>
<input type="file" ref="restoreFile" style="display: none;" @change="restore" />
<v-spacer></v-spacer>
<v-dialog v-model="restart" width="500">
<template v-slot:activator="{ on }">
@@ -80,10 +72,8 @@
<v-card color="red" dark>
<v-card-title primary-title>Restart Scrypted</v-card-title>
<v-card-text
>Are you sure you want to restart the Scrypted
service?</v-card-text
>
<v-card-text>Are you sure you want to restart the Scrypted
service?</v-card-text>
<v-card-text>{{ restartStatus }}</v-card-text>
<v-divider></v-divider>
@@ -103,7 +93,8 @@
<script>
import { checkServerUpdate } from "../plugin/plugin";
import Settings from "../../interfaces/Settings.vue"
import {createSystemSettingsDevice} from './system-settings';
import { createSystemSettingsDevice } from './system-settings';
import { combineBaseUrl, getCurrentBaseUrl } from "../../../../../../packages/client/src";
export default {
components: {
@@ -121,11 +112,35 @@ export default {
showRestart: false,
};
},
computed: {
backupUrl() {
const baseUrl = getCurrentBaseUrl();
return combineBaseUrl(baseUrl, 'web/component/backup');
},
},
mounted() {
this.loadEnv();
this.checkUpdateAvailable();
},
methods: {
async restoreClick() {
const restoreFile = this.$refs.restoreFile;
restoreFile.click();
},
async restore() {
const restoreFile = this.$refs.restoreFile;
const file = restoreFile.files[0];
if (!file)
return;
console.log(file);
const fileBlob = new Blob([file]);
const baseUrl = getCurrentBaseUrl();
const restoreUrl = combineBaseUrl(baseUrl, 'web/component/restore');
await fetch(restoreUrl, {
method: 'POST',
body: fileBlob,
});
},
async checkUpdateAvailable() {
const info = await this.$scrypted.systemManager.getComponent("info");
const version = await info.getVersion();

View File

@@ -33,17 +33,56 @@ export default {
const termSvcRaw = this.$scrypted.systemManager.getDeviceByName("@scrypted/core");
const termSvc = await termSvcRaw.getDevice("terminalservice");
const termSvcDirect = await this.$scrypted.connectRPCObject(termSvc);
const queue = createAsyncQueue();
const dataQueue = createAsyncQueue();
const ctrlQueue = createAsyncQueue();
queue.enqueue(JSON.stringify({ interactive: true }));
queue.enqueue(JSON.stringify({ dim: { cols: term.cols, rows: term.rows } }));
ctrlQueue.enqueue({ interactive: true });
ctrlQueue.enqueue({ dim: { cols: term.cols, rows: term.rows } });
term.onData(data => queue.enqueue(Buffer.from(data, 'utf8')));
term.onBinary(data => queue.enqueue(Buffer.from(data, 'binary')));
term.onResize(dim => queue.enqueue(JSON.stringify({ dim })));
let bufferedLength = 0;
const MAX_BUFFERED_LENGTH = 64000;
async function dataQueueEnqueue(data) {
bufferedLength += data.length;
const promise = dataQueue.enqueue(data).then(() => bufferedLength -= data.length);
if (bufferedLength >= MAX_BUFFERED_LENGTH) {
term.setOption("disableStdin", true);
await promise;
if (bufferedLength < MAX_BUFFERED_LENGTH)
term.setOption("disableStdin", false);
}
}
const localGenerator = queue.queue;
const remoteGenerator = await termSvcDirect.connectStream(localGenerator);
term.onData(data => dataQueueEnqueue(Buffer.from(data, 'utf8')));
term.onBinary(data => dataQueueEnqueue(Buffer.from(data, 'binary')));
term.onResize(dim => {
ctrlQueue.enqueue({ dim });
ctrlQueue.enqueue(Buffer.alloc(0));
});
async function* localGenerator() {
while (true) {
const ctrlBuffers = ctrlQueue.clear();
if (ctrlBuffers.length) {
for (const ctrl of ctrlBuffers) {
yield JSON.stringify(ctrl);
}
continue;
}
const dataBuffers = dataQueue.clear();
if (dataBuffers.length === 0) {
const buf = await dataQueue.dequeue();
if (buf.length)
yield buf;
continue;
}
const concat = Buffer.concat(dataBuffers);
if (concat.length)
yield concat;
}
}
const remoteGenerator = await termSvcDirect.connectStream(localGenerator());
for await (const message of remoteGenerator) {
if (!message) {

View File

@@ -42,15 +42,28 @@ export function createSystemSettingsDevice(systemManager: SystemManager): Scrypt
const results = systemSettings.map(async d => {
const settings = await d.getSettings();
for (const setting of settings) {
const subgroup = setting.group;
if (d.pluginId === '@scrypted/core')
setting.group = 'General';
else
setting.group = d.name;
setting.subgroup = subgroup;
setting.key = d.id + ':' + setting.key;
}
return settings;
});
return (await Promise.all(results)).flat();
const ret = (await Promise.all(results)).flat();
ret.sort((a, b) => {
if (a.group === 'General') {
if (b.group === 'General')
return 0;
return -1;
}
if (b.group === 'General')
return 1;
return 0;
});
return ret;
},
async putSetting(key, value) {
const [id, realKey] = key.split(':');

View File

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

View File

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

View File

@@ -1,8 +1,8 @@
import sdk, { FFmpegInput, MediaObject, VideoClip, VideoClipOptions } from '@scrypted/sdk';
import path from 'path';
import fs from 'fs';
import mkdirp from 'mkdirp';
import { mkdirp } from 'mkdirp';
import path from 'path';
const { mediaManager } = sdk;
export const VIDEO_CLIPS_NATIVE_ID = 'save-video-clips';
@@ -98,16 +98,6 @@ export async function getVideoClips(options?: VideoClipOptions, id?: string): Pr
if (options?.endTime)
ret = ret.filter(clip => clip.startTime + clip.duration < options.endTime);
if (options?.reverseOrder)
ret = ret.reverse();
if (options?.startId) {
const startIndex = ret.findIndex(c => c.id === options.startId);
if (startIndex === -1)
throw new Error('startIndex not found');
ret = ret.slice(startIndex);
}
if (options?.count)
ret = ret.slice(0, options.count);

View File

@@ -7,17 +7,17 @@ import { timeoutPromise } from "@scrypted/common/src/promise-utils";
import sdk, { AudioSensor, FFmpegInput, MotionSensor, ScryptedDevice, ScryptedInterface, ScryptedMimeTypes, VideoCamera } from '@scrypted/sdk';
import child_process from "child_process";
import fs from 'fs';
import mkdirp from 'mkdirp';
import { mkdirp } from 'mkdirp';
import net from 'net';
import path from 'path';
import { Duplex, Readable, Writable } from 'stream';
import { } from '../../common';
import { AudioRecordingCodecType, CameraRecordingConfiguration, DataStreamConnection, RecordingPacket } from '../../hap';
import { AudioRecordingCodecType, CameraRecordingConfiguration, RecordingPacket } from '../../hap';
import type { HomeKitPlugin } from "../../main";
import { getCameraRecordingFiles, HksvVideoClip, VIDEO_CLIPS_NATIVE_ID } from './camera-recording-files';
import { checkCompatibleCodec, FORCE_OPUS, transcodingDebugModeWarning } from './camera-utils';
import { NAL_TYPE_DELIMITER, NAL_TYPE_FU_A, NAL_TYPE_IDR, NAL_TYPE_PPS, NAL_TYPE_SEI, NAL_TYPE_SPS, NAL_TYPE_STAP_A } from "./h264-packetizer";
import path from 'path';
import { getDebugMode } from "./camera-debug-mode-storage";
import { HksvVideoClip, VIDEO_CLIPS_NATIVE_ID, getCameraRecordingFiles } from './camera-recording-files';
import { FORCE_OPUS, checkCompatibleCodec, transcodingDebugModeWarning } from './camera-utils';
import { NAL_TYPE_DELIMITER, NAL_TYPE_FU_A, NAL_TYPE_IDR, NAL_TYPE_PPS, NAL_TYPE_SEI, NAL_TYPE_SPS, NAL_TYPE_STAP_A } from "./h264-packetizer";
const { log, mediaManager, deviceManager } = sdk;

View File

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

View File

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

View File

@@ -514,10 +514,14 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
if (!o.boundingBox)
continue;
o.zones = []
const box = normalizeBox(o.boundingBox, detection.inputDimensions);
let included: boolean;
// need a way to explicitly include package zone.
if (o.zones)
included = true;
else
o.zones = [];
for (const [zone, zoneValue] of Object.entries(this.zones)) {
if (zoneValue.length < 3) {
// this.console.warn(zone, 'Zone is unconfigured, skipping.');

View File

@@ -103,6 +103,8 @@ export class OnvifCameraAPI {
const dataValue = event.message.message.data.simpleItem.$.Value;
const eventTopic = stripNamespaces(event.topic._);
ret.emit('onvifEvent', eventTopic, dataValue);
if (eventTopic.includes('MotionAlarm')) {
// ret.emit('event', OnvifEvent.MotionBuggy);
if (dataValue)

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/openvino",
"version": "0.1.48",
"version": "0.1.51",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/openvino",
"version": "0.1.48",
"version": "0.1.51",
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
}

View File

@@ -41,5 +41,5 @@
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
},
"version": "0.1.48"
"version": "0.1.51"
}

View File

@@ -1,4 +1,4 @@
openvino==2023.2.0
openvino==2023.3.0
# pillow-simd is available on x64 linux
# pillow-simd confirmed not building with arm64 linux or apple silicon

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/prebuffer-mixin",
"version": "0.10.11",
"version": "0.10.12",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/prebuffer-mixin",
"version": "0.10.11",
"version": "0.10.12",
"license": "Apache-2.0",
"dependencies": {
"@scrypted/common": "file:../../common",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/prebuffer-mixin",
"version": "0.10.11",
"version": "0.10.12",
"description": "Video Stream Rebroadcast, Prebuffer, and Management Plugin for Scrypted.",
"author": "Scrypted",
"license": "Apache-2.0",

View File

@@ -1,4 +1,4 @@
import path from 'path'
import { AutoenableMixinProvider } from '@scrypted/common/src/autoenable-mixin-provider';
import { getDebugModeH264EncoderArgs, getH264EncoderArgs } from '@scrypted/common/src/ffmpeg-hardware-acceleration';
import { addVideoFilterArguments } from '@scrypted/common/src/ffmpeg-helpers';
@@ -971,6 +971,7 @@ class PrebufferSession {
const clientPromise = await listenSingleRtspClient({
hostname,
pathToken: path.join(crypto.randomBytes(8).toString('hex'), this.mixin.id),
createServer: duplex => {
sdp = addTrackControls(sdp);
server = new FileRtspServer(duplex, sdp);

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/reolink",
"version": "0.0.58",
"version": "0.0.59",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/reolink",
"version": "0.0.58",
"version": "0.0.59",
"license": "Apache",
"dependencies": {
"@scrypted/common": "file:../../common",

View File

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

View File

@@ -3,7 +3,7 @@ import sdk, { Camera, DeviceCreatorSettings, DeviceInformation, Intercom, MediaO
import { StorageSettings } from '@scrypted/sdk/storage-settings';
import { EventEmitter } from "stream";
import { Destroyable, RtspProvider, RtspSmartCamera, UrlMediaStreamOptions } from "../../rtsp/src/rtsp";
import { OnvifCameraAPI, connectCameraAPI } from './onvif-api';
import { OnvifCameraAPI, OnvifEvent, connectCameraAPI } from './onvif-api';
import { listenEvents } from './onvif-events';
import { OnvifIntercom } from './onvif-intercom';
import { AIState, DevInfo, Enc, ReolinkCameraClient } from './reolink-api';
@@ -49,6 +49,10 @@ class ReolinkCamera extends RtspSmartCamera implements Camera, Reboot, Intercom,
await this.updateDevice();
this.updatePtzCaps();
},
},
doorbellUseOnvifDetections: {
hide: true,
defaultValue: true,
}
});
@@ -80,6 +84,12 @@ class ReolinkCamera extends RtspSmartCamera implements Camera, Reboot, Intercom,
}
async getObjectTypes(): Promise<ObjectDetectionTypes> {
if (this.storageSettings.values.doorbell && this.storageSettings.values.doorbellUseOnvifDetections) {
return {
classes: ['person'],
};
}
try {
const ai: AIState = this.storageSettings.values.hasObjectDetector[0]?.value;
const classes: string[] = [];
@@ -228,9 +238,35 @@ class ReolinkCamera extends RtspSmartCamera implements Camera, Reboot, Intercom,
if (this.storageSettings.values.doorbell) {
const ret = await listenEvents(this, await this.createOnvifClient(), this.storageSettings.values.motionTimeout * 1000);
if (!this.storageSettings.values.doorbellUseOnvifDetections) {
startAI(ret, ret.triggerMotion);
}
else {
ret.on('onvifEvent', (eventTopic: string, dataValue: any) => {
if (eventTopic.includes('PeopleDetect')) {
if (dataValue) {
ret.emit('event', OnvifEvent.MotionStart);
const od: ObjectsDetected = {
timestamp: Date.now(),
detections: [
{
className: 'person',
score: 1,
}
],
};
sdk.deviceManager.onDeviceEvent(this.nativeId, ScryptedInterface.ObjectDetector, od);
}
else {
ret.emit('event', OnvifEvent.MotionStop);
}
}
});
}
ret.on('close', () => killed = true);
ret.on('error', () => killed = true);
startAI(ret, ret.triggerMotion);
return ret;
}

View File

@@ -1,30 +1,30 @@
{
"name": "@scrypted/synology-ss",
"version": "0.0.16",
"version": "0.0.17",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/synology-ss",
"version": "0.0.16",
"version": "0.0.17",
"license": "Apache",
"dependencies": {
"axios": "^0.24.0"
"axios": "^1.0.0"
},
"devDependencies": {
"@scrypted/sdk": "file:../../sdk",
"@types/node": "^16.6.1"
"@types/node": "^18.0.0"
}
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.2.86",
"version": "0.3.5",
"dev": true,
"license": "ISC",
"dependencies": {
"@babel/preset-typescript": "^7.18.6",
"adm-zip": "^0.4.13",
"axios": "^0.21.4",
"axios": "^1.6.5",
"babel-loader": "^9.1.0",
"babel-plugin-const-enum": "^1.1.0",
"esbuild": "^0.15.9",
@@ -59,23 +59,52 @@
"link": true
},
"node_modules/@types/node": {
"version": "16.11.7",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.7.tgz",
"integrity": "sha512-QB5D2sqfSjCmTuWcBWyJ+/44bcjO7VbjSbOE0ucoVbAsSNQc4Lt6QkgkVXkTDwkL4z/beecZNDvVX15D4P8Jbw==",
"dev": true
"version": "18.19.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.10.tgz",
"integrity": "sha512-IZD8kAM02AW1HRDTPOlz3npFava678pr8Ie9Vp8uRhBROXAv8MXT2pCnGZZAKYdromsNQLHQcfWQ6EOatVLtqA==",
"dev": true,
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/axios": {
"version": "0.24.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz",
"integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==",
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz",
"integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==",
"dependencies": {
"follow-redirects": "^1.14.4"
"follow-redirects": "^1.15.4",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/follow-redirects": {
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
"version": "1.15.5",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
"integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==",
"funding": [
{
"type": "individual",
@@ -90,6 +119,49 @@
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true
}
},
"dependencies": {
@@ -100,7 +172,7 @@
"@types/node": "^18.11.18",
"@types/stringify-object": "^4.0.0",
"adm-zip": "^0.4.13",
"axios": "^0.21.4",
"axios": "^1.6.5",
"babel-loader": "^9.1.0",
"babel-plugin-const-enum": "^1.1.0",
"esbuild": "^0.15.9",
@@ -118,23 +190,80 @@
}
},
"@types/node": {
"version": "16.11.7",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.7.tgz",
"integrity": "sha512-QB5D2sqfSjCmTuWcBWyJ+/44bcjO7VbjSbOE0ucoVbAsSNQc4Lt6QkgkVXkTDwkL4z/beecZNDvVX15D4P8Jbw==",
"dev": true
},
"axios": {
"version": "0.24.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz",
"integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==",
"version": "18.19.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.10.tgz",
"integrity": "sha512-IZD8kAM02AW1HRDTPOlz3npFava678pr8Ie9Vp8uRhBROXAv8MXT2pCnGZZAKYdromsNQLHQcfWQ6EOatVLtqA==",
"dev": true,
"requires": {
"follow-redirects": "^1.14.4"
"undici-types": "~5.26.4"
}
},
"asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"axios": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz",
"integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==",
"requires": {
"follow-redirects": "^1.15.4",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"requires": {
"delayed-stream": "~1.0.0"
}
},
"delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
},
"follow-redirects": {
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA=="
"version": "1.15.5",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
"integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw=="
},
"form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
}
},
"mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
},
"mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"requires": {
"mime-db": "1.52.0"
}
},
"proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/synology-ss",
"version": "0.0.16",
"version": "0.0.17",
"description": "A Synology Surveillance Station plugin for Scrypted",
"author": "Scrypted",
"license": "Apache",
@@ -36,10 +36,10 @@
]
},
"dependencies": {
"axios": "^0.24.0"
"axios": "^1.0.0"
},
"devDependencies": {
"@scrypted/sdk": "file:../../sdk",
"@types/node": "^16.6.1"
"@types/node": "^18.0.0"
}
}

View File

@@ -164,11 +164,12 @@ export class SynologyApiClient {
const response = await this.client.get<SynologyApiResponse<T>>(url ?? await this.getApiPath(params.api), { params });
if (!response.data?.success) {
if (response.data?.error?.code) {
const errorCode = response.data?.error?.code;
if (errorCode) {
const errorCodeLookup = { ...errorCodeDescriptions, ...extraErrorCodes }
throw new Error(`${errorCodeLookup[response.data.error.code]} (error code ${response.data.error.code})`)
throw new SynologyApiError(`${errorCodeLookup[errorCode]} (error code ${errorCode})`, errorCode)
} else {
throw new Error(`Synology API call failed with status code ${response.status}`);
throw new SynologyApiError(`Synology API call failed with status code ${response.status}`);
}
}
@@ -186,7 +187,17 @@ export interface SynologyApiInfo {
maxVersion: number;
}
export interface SynologyApiError {
export class SynologyApiError extends Error {
code?: string;
constructor(message: string, code?: string) {
super(message);
this.name = 'SynologyApiError';
this.code = code;
}
}
export interface SynologyApiErrorObject {
code: string;
}
@@ -198,7 +209,7 @@ interface SynologyApiRequestParams {
interface SynologyApiResponse<T> {
data?: T;
error?: SynologyApiError;
error?: SynologyApiErrorObject;
success: boolean;
}

View File

@@ -1,6 +1,6 @@
import sdk, { Camera, Device, DeviceProvider, HttpRequest, HttpRequestHandler, HttpResponse, MediaObject, MediaStreamOptions, MediaStreamUrl, MotionSensor, PictureOptions, ResponseMediaStreamOptions, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, Settings, VideoCamera } from "@scrypted/sdk";
import { createInstanceableProviderPlugin, enableInstanceableProviderMode, isInstanceableProviderModeEnabled } from '../../../common/src/provider-plugin';
import { SynologyApiClient, SynologyCamera, SynologyCameraStream } from "./api/synology-api-client";
import { SynologyApiClient, SynologyApiError, SynologyCamera, SynologyCameraStream } from "./api/synology-api-client";
const { deviceManager } = sdk;
@@ -162,10 +162,11 @@ class SynologyCameraDevice extends ScryptedDeviceBase implements Camera, HttpReq
}
class SynologySurveillanceStation extends ScryptedDeviceBase implements Settings, DeviceProvider {
private cameras: SynologyCamera[];
private cameras: SynologyCamera[] = [];
private cameraDevices: Map<string, SynologyCameraDevice> = new Map();
api: SynologyApiClient;
private startup: Promise<void>;
private discovering: boolean;
constructor(nativeId?: string) {
super(nativeId);
@@ -177,66 +178,23 @@ class SynologySurveillanceStation extends ScryptedDeviceBase implements Settings
}
public async discoverDevices(duration: number): Promise<void> {
const url = this.getSetting('url');
const username = this.getSetting('username');
const password = this.getSetting('password');
const otpCode = this.getSetting('otpCode');
const mfaDeviceId = this.getSetting('mfaDeviceId');
if (this.discovering) return;
this.discovering = true;
this.log.clearAlerts();
if (!url) {
this.log.a('Must provide URL.');
return
}
if (!username) {
this.log.a('Must provide username.');
return
}
if (!password) {
this.log.a('Must provide password.');
return
}
if (!this.api || url !== this.api.url) {
this.api = new SynologyApiClient(url);
}
this.console.info(`Fetching list of cameras from Synology server...`);
try {
const newMfaDeviceId = await this.api.login(username, password, otpCode ? parseInt(otpCode) : undefined, !!otpCode, 'Scrypted', mfaDeviceId);
// If a OTP was present, store the device ID to allow us to skip the OTP requirement next login.
if (otpCode) {
this.storage.setItem('mfaDeviceId', newMfaDeviceId);
if (!await this.tryLogin()) {
return;
}
}
catch (e) {
this.log.a(`login error: ${e}`);
this.console.error('login error', e);
// Clear device ID upon login failure, since it's likely useless now
this.storage.removeItem('mfaDeviceId');
return;
}
finally {
// Clear the OTP setting if provided since it's a temporary code
if (otpCode) {
this.storage.removeItem('otpCode');
this.onDeviceEvent(ScryptedInterface.Settings, undefined);
}
}
try {
this.cameras = await this.api.listCameras();
if (!this.cameras) {
this.console.error('Cameras failed to load. Retrying in 10 seconds.');
setTimeout(() => {
this.discoverDevices(0);
}, 100000);
}, 10000);
return;
}
@@ -285,6 +243,8 @@ class SynologySurveillanceStation extends ScryptedDeviceBase implements Settings
catch (e) {
this.log.a(`device discovery error: ${e}`);
this.console.error('device discovery error', e);
} finally {
this.discovering = false;
}
}
@@ -353,7 +313,81 @@ class SynologySurveillanceStation extends ScryptedDeviceBase implements Settings
return;
}
this.storage.setItem(key, value.toString());
this.discoverDevices(0);
// Delaying discover in case user updated multiple settings, so that it doesn't run until all have been set
setTimeout(() => this.discoverDevices(0), 200);
}
private async tryLogin(): Promise<boolean> {
this.console.info('Logging into Synology...');
const url = this.getSetting('url');
const username = this.getSetting('username');
const password = this.getSetting('password');
const otpCode = this.getSetting('otpCode');
const mfaDeviceId = this.getSetting('mfaDeviceId');
this.log.clearAlerts();
if (!url) {
this.log.a('Must provide URL.');
return
}
if (!username) {
this.log.a('Must provide username.');
return
}
if (!password) {
this.log.a('Must provide password.');
return
}
if (!this.api || url !== this.api.url) {
this.api = new SynologyApiClient(url);
}
let successful = false;
for (let attempt=1; attempt<=3; attempt++) {
try {
const newMfaDeviceId = await this.api.login(username, password, otpCode ? parseInt(otpCode) : undefined, !!otpCode, 'Scrypted', mfaDeviceId);
// If a OTP was present, store the device ID to allow us to skip the OTP requirement next login.
if (otpCode) {
this.storage.setItem('mfaDeviceId', newMfaDeviceId);
}
successful = true;
}
catch (e) {
this.log.a(`login error on attempt ${attempt}: ${e}`);
this.console.error(`login error on attempt ${attempt}`, e);
if (e instanceof SynologyApiError) {
break;
} else {
// Retry on failures that aren't Synology-specific, such as timeouts
await new Promise((resolve) => setTimeout(resolve, attempt * 1000));
continue;
}
}
finally {
// Clear the OTP setting if provided since it's a temporary code
if (otpCode) {
this.storage.removeItem('otpCode');
this.onDeviceEvent(ScryptedInterface.Settings, undefined);
}
}
}
if (successful) {
this.console.info(`Successfully logged into Synology`);
} else {
this.console.info(`Failed to log into Synology`);
}
return successful;
}
}

View File

@@ -1,6 +1,6 @@
{
"compilerOptions": {
"module": "commonjs",
"module": "Node16",
"target": "ES2021",
"resolveJsonModule": true,
"moduleResolution": "Node16",

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/tensorflow-lite",
"version": "0.1.45",
"version": "0.1.46",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/tensorflow-lite",
"version": "0.1.45",
"version": "0.1.46",
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
}

View File

@@ -53,5 +53,5 @@
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
},
"version": "0.1.45"
"version": "0.1.46"
}

View File

@@ -70,7 +70,7 @@ class TensorFlowLitePlugin(
nonlocal model
if defaultModel:
model = "yolov8n_full_integer_quant_320"
model = "efficientdet_lite0_320_ptq"
self.yolo = "yolo" in model
self.yolov8 = "yolov8" in model

View File

@@ -1,4 +1,4 @@
{
"scrypted.debugHost": "koushik-ubuntu",
"scrypted.debugHost": "scrypted-server",
}

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/zwave",
"version": "0.1.2",
"version": "0.1.5",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/zwave",
"version": "0.1.2",
"version": "0.1.5",
"license": "Apache",
"dependencies": {
"@scrypted/sdk": "file:../../sdk",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/zwave",
"version": "0.1.2",
"version": "0.1.5",
"description": "Z-Wave USB Controller for Scrypted",
"author": "Scrypted",
"license": "Apache",

View File

@@ -0,0 +1,22 @@
import { CO2Sensor } from "@scrypted/sdk";
import type { ValueID } from "@zwave-js/core";
import { ZWaveNode, ZWaveNodeValueUpdatedArgs } from "zwave-js";
import { Notification } from "./Notification";
import { ZwaveDeviceBase } from "./ZwaveDeviceBase";
export class SmokeAlarmToCO2Sensor extends Notification implements CO2Sensor {
static getInterfaces(node: ZWaveNode, valueId: ValueID): string[] {
if (Notification.checkInterface(node, valueId, 'Smoke detected')
|| Notification.checkInterface(node, valueId, 'Smoke detected (location provided)')) {
return ['CO2Sensor'];
}
return null;
}
static onValueChanged(zwaveDevice: ZwaveDeviceBase, valueId: ZWaveNodeValueUpdatedArgs) {
if (valueId.propertyKey === 'Alarm status') {
const notification = Notification.lookupNotification(zwaveDevice, 'Smoke Alarm');
zwaveDevice.co2ppm = notification.lookupValue(valueId.newValue as number) ? 50 : 0;
}
}
}

View File

@@ -65,6 +65,7 @@ export class ZwaveDeviceBase extends ScryptedDeviceBase implements Refresh, Sett
}
onValueChanged(valueId: ZWaveNodeValueUpdatedArgs) {
this.console.log('value changed', valueId);
var cc = getCommandClassIndex(valueId.commandClass, valueId.property as number);
if (!cc) {
cc = getCommandClass(valueId.commandClass);

View File

@@ -1,26 +1,26 @@
import OnOffToSwitch from './OnOffToSwitch';
import BrightnessToSwitchMultilevel from './BrightnessToSwitchMultilevel';
import { CommandClassHandler as CommandClassHandlerClass } from './ZwaveDeviceBase';
import BinarySensorToStateSensor from './BinarySensorToStateSensor';
import LockToDoorLock from './LockToDoorLock';
import BatteryToBattery from './BatteryToBattery';
import ThermometerToSensorMultilevel from './ThermometerToSensorMultilevel';
import HumidityToSensorMultilevel from './HumiditySensorToSensorMultilevel';
import LuminanceSensorToSensorMultilevel from './LuminanceSensorToSensorMultilevel';
import UltravioletSensorMultilevel from './UltravioletSensorToSensorMultilevel';
import SettingsToConfiguration from './SettingsToConfiguration';
import EntryToBarrierOperator from './EntryToBarrierOperator';
import EntrySensorToBarriorOperator from './EntrySensorToBarrierOperator';
import ColorSettingRgbToColor from './ColorSettingRgbToColor';
import { NotificationType } from './Notification';
import { EntrySensorToAccessControl } from './EntrySensorToAccessControl';
import { FloodSensorToWaterAlarm } from './FloodSensorToWaterAlarm';
import { PasswordStoreToUserCode } from './PasswordStoreToUserCode';
import { TamperSensorToHomeSecurity } from './TamperSensorToHomeSecurity';
import { PowerSensorToPowerManagement } from './PowerSensorToPowerManagement';
import { ZWaveNode } from 'zwave-js';
import {CommandClasses, ValueID} from '@zwave-js/core'
import { ScryptedInterface } from '@scrypted/sdk';
import { CommandClasses, ValueID } from '@zwave-js/core';
import { ZWaveNode } from 'zwave-js';
import BatteryToBattery from './BatteryToBattery';
import BinarySensorToStateSensor from './BinarySensorToStateSensor';
import BrightnessToSwitchMultilevel from './BrightnessToSwitchMultilevel';
import ColorSettingRgbToColor from './ColorSettingRgbToColor';
import { EntrySensorToAccessControl } from './EntrySensorToAccessControl';
import EntrySensorToBarriorOperator from './EntrySensorToBarrierOperator';
import EntryToBarrierOperator from './EntryToBarrierOperator';
import { FloodSensorToWaterAlarm } from './FloodSensorToWaterAlarm';
import HumidityToSensorMultilevel from './HumiditySensorToSensorMultilevel';
import LockToDoorLock from './LockToDoorLock';
import LuminanceSensorToSensorMultilevel from './LuminanceSensorToSensorMultilevel';
import OnOffToSwitch from './OnOffToSwitch';
import { PasswordStoreToUserCode } from './PasswordStoreToUserCode';
import { PowerSensorToPowerManagement } from './PowerSensorToPowerManagement';
import SettingsToConfiguration from './SettingsToConfiguration';
import { SmokeAlarmToCO2Sensor } from './SmokeAlarmToBinarySensor';
import { TamperSensorToHomeSecurity } from './TamperSensorToHomeSecurity';
import ThermometerToSensorMultilevel from './ThermometerToSensorMultilevel';
import UltravioletSensorMultilevel from './UltravioletSensorToSensorMultilevel';
import { CommandClassHandler as CommandClassHandlerClass } from './ZwaveDeviceBase';
var CommandClassMap: {[ccId: string]: CommandClassInfo} = {};
@@ -72,25 +72,26 @@ export function getCommandClassIndex(commandClass: number, index: number): Comma
return CommandClassMap[`${commandClass}#${index}`];
}
addCommandClassIndex(CommandClasses['Binary Switch'], 'currentValue', OnOffToSwitch, 'OnOff');
addCommandClassIndex(CommandClasses['Multilevel Switch'], 'currentValue', BrightnessToSwitchMultilevel, 'Brightness', 'OnOff');
addCommandClassIndex(CommandClasses['Color'], 'currentValue', ColorSettingRgbToColor, 'ColorSettingRgb', 'ColorSettingTemperature');
addCommandClassIndex(CommandClasses['Binary Sensor'], 'Any', BinarySensorToStateSensor, 'BinarySensor');
addCommandClassIndex(CommandClasses['Door Lock'], 'currentMode', LockToDoorLock, 'Lock');
addCommandClassIndex(CommandClasses['Battery'], 'level', BatteryToBattery, 'Battery');
addCommandClassIndex(CommandClasses['Entry Control'], 'currentValue', EntryToBarrierOperator, 'Entry');
addCommandClassIndex(CommandClasses['Multilevel Sensor'], 'Air temperature', ThermometerToSensorMultilevel, 'Thermometer');
addCommandClassIndex(CommandClasses['Multilevel Sensor'], 'Humidity', HumidityToSensorMultilevel, 'HumiditySensor');
addCommandClassIndex(CommandClasses['Multilevel Sensor'], 'Illuminance', LuminanceSensorToSensorMultilevel, 'LuminanceSensor');
addCommandClassIndex(CommandClasses['Multilevel Sensor'], 'Ultraviolet', UltravioletSensorMultilevel, 'UltravioletSensor');
addCommandClassIndex(CommandClasses['Binary Switch'], 'currentValue', OnOffToSwitch, ScryptedInterface.OnOff);
addCommandClassIndex(CommandClasses['Multilevel Switch'], 'currentValue', BrightnessToSwitchMultilevel, ScryptedInterface.Brightness, ScryptedInterface.OnOff);
addCommandClassIndex(CommandClasses['Color'], 'currentValue', ColorSettingRgbToColor, ScryptedInterface.ColorSettingRgb, ScryptedInterface.ColorSettingTemperature);
addCommandClassIndex(CommandClasses['Binary Sensor'], 'Any', BinarySensorToStateSensor, ScryptedInterface.BinarySensor);
addCommandClassIndex(CommandClasses['Door Lock'], 'currentMode', LockToDoorLock, ScryptedInterface.Lock);
addCommandClassIndex(CommandClasses['Battery'], 'level', BatteryToBattery, ScryptedInterface.Battery);
addCommandClassIndex(CommandClasses['Entry Control'], 'currentValue', EntryToBarrierOperator, ScryptedInterface.Entry);
addCommandClassIndex(CommandClasses['Multilevel Sensor'], 'Air temperature', ThermometerToSensorMultilevel, ScryptedInterface.Thermometer);
addCommandClassIndex(CommandClasses['Multilevel Sensor'], 'Humidity', HumidityToSensorMultilevel, ScryptedInterface.HumiditySensor);
addCommandClassIndex(CommandClasses['Multilevel Sensor'], 'Illuminance', LuminanceSensorToSensorMultilevel, ScryptedInterface.LuminanceSensor);
addCommandClassIndex(CommandClasses['Multilevel Sensor'], 'Ultraviolet', UltravioletSensorMultilevel, ScryptedInterface.UltravioletSensor);
addCommandClassIndex(CommandClasses['Notification'], 'Access Control', EntrySensorToAccessControl, 'EntrySensor');
addCommandClassIndex(CommandClasses['Notification'], 'Water Alarm', FloodSensorToWaterAlarm, 'FloodSensor');
addCommandClassIndex(CommandClasses['Notification'], 'Access Control', EntrySensorToAccessControl, ScryptedInterface.EntrySensor);
addCommandClassIndex(CommandClasses['Notification'], 'Water Alarm', FloodSensorToWaterAlarm, ScryptedInterface.FloodSensor);
addCommandClassIndex(CommandClasses['Notification'], 'Home Security', TamperSensorToHomeSecurity, ScryptedInterface.TamperSensor);
addCommandClassIndex(CommandClasses['Notification'], 'Power Management', PowerSensorToPowerManagement, 'PowerSensor');
addCommandClassIndex(CommandClasses['Notification'], 'Power Management', PowerSensorToPowerManagement, ScryptedInterface.PowerSensor);
addCommandClassIndex(CommandClasses['Notification'], 'Smoke Alarm', SmokeAlarmToCO2Sensor, ScryptedInterface.CO2Sensor);
addCommandClassIndex(CommandClasses['Barrier Operator'], 'currentState', EntryToBarrierOperator, 'Entry');
addCommandClassIndex(CommandClasses['Barrier Operator'], 'position', EntrySensorToBarriorOperator, 'EntrySensor');
addCommandClassIndex(CommandClasses['Barrier Operator'], 'currentState', EntryToBarrierOperator, ScryptedInterface.Entry);
addCommandClassIndex(CommandClasses['Barrier Operator'], 'position', EntrySensorToBarriorOperator, ScryptedInterface.EntrySensor);
addCommandClass(CommandClasses['Configuration'], SettingsToConfiguration, 'Settings');
addCommandClass(CommandClasses['User Code'], PasswordStoreToUserCode, 'PasswordStore');
addCommandClass(CommandClasses['Configuration'], SettingsToConfiguration, ScryptedInterface.Settings);
addCommandClass(CommandClasses['User Code'], PasswordStoreToUserCode, ScryptedInterface.PasswordStore);

4
sdk/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/sdk",
"version": "0.3.4",
"version": "0.3.5",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/sdk",
"version": "0.3.4",
"version": "0.3.5",
"license": "ISC",
"dependencies": {
"@babel/preset-typescript": "^7.18.6",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/sdk",
"version": "0.3.4",
"version": "0.3.5",
"description": "",
"main": "dist/src/index.js",
"exports": {

View File

@@ -19,6 +19,8 @@ function parseValue(value: string, setting: StorageSetting, readDefaultValue: ()
return parseInt(value) || readDefaultValue() || 0;
}
if (type === 'array') {
if (!value)
return readDefaultValue() || [];
try {
return JSON.parse(value);
}

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/types",
"version": "0.3.4",
"version": "0.3.5",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/types",
"version": "0.3.4",
"version": "0.3.5",
"license": "ISC",
"devDependencies": {
"@types/rimraf": "^3.0.2",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/types",
"version": "0.3.4",
"version": "0.3.5",
"description": "",
"main": "dist/index.js",
"author": "",

View File

@@ -27,6 +27,7 @@
"${workspaceFolder}/**/*.js"
],
"env": {
"SCRYPTED_CAN_RESTART": "true",
// "SCRYPTED_DEFAULT_AUTHENTICATION": "demo"
// force usage of system python because brew python is 3.11
// which has no wheels for coreml tools or tflite-runtime

152
server/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/server",
"version": "0.83.0",
"version": "0.93.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/server",
"version": "0.83.0",
"version": "0.93.0",
"license": "ISC",
"dependencies": {
"@mapbox/node-pre-gyp": "^1.0.11",
@@ -14,7 +14,6 @@
"adm-zip": "^0.5.10",
"body-parser": "^1.20.2",
"cookie-parser": "^1.4.6",
"debug": "^4.3.4",
"engine.io": "^6.5.4",
"express": "^4.18.2",
"ffmpeg-static": "^5.2.0",
@@ -22,16 +21,13 @@
"http-auth": "^4.2.0",
"ip": "^1.1.8",
"level": "^8.0.0",
"linkfs": "^2.1.0",
"lodash": "^4.17.21",
"memfs": "^4.6.0",
"nan": "^2.18.0",
"node-dijkstra": "^2.5.0",
"node-forge": "^1.3.1",
"node-gyp": "^10.0.1",
"router": "^1.3.8",
"semver": "^7.5.4",
"send": "^0.18.0",
"sharp": "^0.33.2",
"source-map-support": "^0.5.21",
"tar": "^6.2.0",
@@ -46,7 +42,6 @@
"devDependencies": {
"@types/adm-zip": "^0.5.5",
"@types/cookie-parser": "^1.4.6",
"@types/debug": "^4.1.12",
"@types/express": "^4.17.21",
"@types/follow-redirects": "^1.14.4",
"@types/http-auth": "^4.1.4",
@@ -54,7 +49,6 @@
"@types/lodash": "^4.14.202",
"@types/node-dijkstra": "^2.5.6",
"@types/node-forge": "^1.3.11",
"@types/pem": "^1.14.4",
"@types/semver": "^7.5.6",
"@types/source-map-support": "^0.5.10",
"@types/tar": "^6.1.10",
@@ -713,15 +707,6 @@
"@types/node": "*"
}
},
"node_modules/@types/debug": {
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
"integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
"dev": true,
"dependencies": {
"@types/ms": "*"
}
},
"node_modules/@types/express": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz",
@@ -785,12 +770,6 @@
"integrity": "sha512-iJt33IQnVRkqeqC7PzBHPTC6fDlRNRW8vjrgqtScAhrmMwe8c4Eo7+fUGTa+XdWrpEgpyKWMYmi2dIwMAYRzPw==",
"dev": true
},
"node_modules/@types/ms": {
"version": "0.7.31",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz",
"integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==",
"dev": true
},
"node_modules/@types/node": {
"version": "20.2.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.2.5.tgz",
@@ -811,15 +790,6 @@
"@types/node": "*"
}
},
"node_modules/@types/pem": {
"version": "1.14.4",
"resolved": "https://registry.npmjs.org/@types/pem/-/pem-1.14.4.tgz",
"integrity": "sha512-Xt6qY6kX1RD4UmYNhWCCf3OSJrRcwbQIaJ/mQSjjAHxIjXMHx/vHNPOgEU3HdVKS1k/U5CZ6ClQlRo8egkl8xg==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/qs": {
"version": "6.9.7",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
@@ -1018,11 +988,6 @@
"node": ">=10"
}
},
"node_modules/arg": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="
},
"node_modules/array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
@@ -1812,12 +1777,6 @@
"node": ">= 0.8"
}
},
"node_modules/fast-diff": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz",
"integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==",
"peer": true
},
"node_modules/ffmpeg-static": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ffmpeg-static/-/ffmpeg-static-5.2.0.tgz",
@@ -2145,14 +2104,6 @@
"node": ">= 6"
}
},
"node_modules/hyperdyperid": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz",
"integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==",
"engines": {
"node": ">=10.18"
}
},
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@@ -2303,35 +2254,6 @@
"@pkgjs/parseargs": "^0.11.0"
}
},
"node_modules/json-joy": {
"version": "9.4.0",
"resolved": "https://registry.npmjs.org/json-joy/-/json-joy-9.4.0.tgz",
"integrity": "sha512-qSWB6VlyQGOdzhjP5eKABYTqAzNlzFaR+uYPYzYijfbhcOSuqWP9Q6bfU7AVvNMFPnaU79vqFqezHeqFtCPXDA==",
"dependencies": {
"arg": "^5.0.2",
"hyperdyperid": "^1.2.0"
},
"bin": {
"json-pack": "bin/json-pack.js",
"json-pack-test": "bin/json-pack-test.js",
"json-patch": "bin/json-patch.js",
"json-patch-test": "bin/json-patch-test.js",
"json-pointer": "bin/json-pointer.js",
"json-pointer-test": "bin/json-pointer-test.js",
"json-unpack": "bin/json-unpack.js"
},
"engines": {
"node": ">=10.0"
},
"funding": {
"url": "https://github.com/sponsors/streamich"
},
"peerDependencies": {
"quill-delta": "^5",
"rxjs": "7",
"tslib": "2"
}
},
"node_modules/level": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/level/-/level-8.0.0.tgz",
@@ -2368,28 +2290,11 @@
"node": ">=12"
}
},
"node_modules/linkfs": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/linkfs/-/linkfs-2.1.0.tgz",
"integrity": "sha512-kmsGcmpvjStZ0ATjuHycBujtNnXiZR28BTivEu0gAMDTT7GEyodcK6zSRtu6xsrdorrPZEIN380x7BD7xEYkew=="
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/lodash.clonedeep": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
"integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==",
"peer": true
},
"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==",
"peer": true
},
"node_modules/lru-cache": {
"version": "10.0.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.3.tgz",
@@ -2457,25 +2362,6 @@
"node": ">= 0.6"
}
},
"node_modules/memfs": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/memfs/-/memfs-4.6.0.tgz",
"integrity": "sha512-I6mhA1//KEZfKRQT9LujyW6lRbX7RkC24xKododIDO3AGShcaFAMKElv1yFGWX8fD4UaSiwasr3NeQ5TdtHY1A==",
"dependencies": {
"json-joy": "^9.2.0",
"thingies": "^1.11.1"
},
"engines": {
"node": ">= 4.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/streamich"
},
"peerDependencies": {
"tslib": "2"
}
},
"node_modules/merge-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
@@ -3294,20 +3180,6 @@
}
]
},
"node_modules/quill-delta": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-5.1.0.tgz",
"integrity": "sha512-X74oCeRI4/p0ucjb5Ma8adTXd9Scumz367kkMK5V/IatcX6A0vlgLgKbzXWy5nZmCGeNJm2oQX0d2Eqj+ZIlCA==",
"peer": true,
"dependencies": {
"fast-diff": "^1.3.0",
"lodash.clonedeep": "^4.5.0",
"lodash.isequal": "^4.5.0"
},
"engines": {
"node": ">= 12.0.0"
}
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@@ -3437,15 +3309,6 @@
"queue-microtask": "^1.2.2"
}
},
"node_modules/rxjs": {
"version": "7.8.1",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
"peer": true,
"dependencies": {
"tslib": "^2.1.0"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -3906,17 +3769,6 @@
"node": ">=8"
}
},
"node_modules/thingies": {
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/thingies/-/thingies-1.12.0.tgz",
"integrity": "sha512-AiGqfYC1jLmJagbzQGuoZRM48JPsr9yB734a7K6wzr34NMhjUPrWSQrkF7ZBybf3yCerCL2Gcr02kMv4NmaZfA==",
"engines": {
"node": ">=10.18"
},
"peerDependencies": {
"tslib": "^2"
}
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/server",
"version": "0.84.0",
"version": "0.93.0",
"description": "",
"dependencies": {
"@mapbox/node-pre-gyp": "^1.0.11",
@@ -8,7 +8,6 @@
"adm-zip": "^0.5.10",
"body-parser": "^1.20.2",
"cookie-parser": "^1.4.6",
"debug": "^4.3.4",
"engine.io": "^6.5.4",
"express": "^4.18.2",
"ffmpeg-static": "^5.2.0",
@@ -16,16 +15,13 @@
"http-auth": "^4.2.0",
"ip": "^1.1.8",
"level": "^8.0.0",
"linkfs": "^2.1.0",
"lodash": "^4.17.21",
"memfs": "^4.6.0",
"nan": "^2.18.0",
"node-dijkstra": "^2.5.0",
"node-forge": "^1.3.1",
"node-gyp": "^10.0.1",
"router": "^1.3.8",
"semver": "^7.5.4",
"send": "^0.18.0",
"sharp": "^0.33.2",
"source-map-support": "^0.5.21",
"tar": "^6.2.0",
@@ -37,7 +33,6 @@
"devDependencies": {
"@types/adm-zip": "^0.5.5",
"@types/cookie-parser": "^1.4.6",
"@types/debug": "^4.1.12",
"@types/express": "^4.17.21",
"@types/follow-redirects": "^1.14.4",
"@types/http-auth": "^4.1.4",
@@ -45,7 +40,6 @@
"@types/lodash": "^4.14.202",
"@types/node-dijkstra": "^2.5.6",
"@types/node-forge": "^1.3.11",
"@types/pem": "^1.14.4",
"@types/semver": "^7.5.6",
"@types/source-map-support": "^0.5.10",
"@types/tar": "^6.1.10",
@@ -68,7 +62,7 @@
"build": "tsc --outDir dist",
"postbuild": "node test/check-build-output.js",
"beta": "npm publish --tag beta",
"postbeta": "npm version minor && git add package.json && npm run build && git commit -m postbeta",
"postbeta": "npm version patch && git add package.json && npm run build && git commit -m postbeta",
"release": "npm publish",
"prepublishOnly": "npm run build",
"postrelease": "git tag v$npm_package_version && git push origin v$npm_package_version && npm version minor && git add package.json && git commit -m postrelease",

View File

@@ -84,9 +84,11 @@ export class PluginHost {
}
async upsertDevice(upsert: Device) {
const newDevice = !this.scrypted.findPluginDevice(this.pluginId, upsert.nativeId);
const { pluginDevicePromise, interfacesChanged } = this.scrypted.upsertDevice(this.pluginId, upsert);
const pi = await pluginDevicePromise;
await this.remote.setNativeId(pi.nativeId, pi._id, pi.storage || {});
if (newDevice)
await this.remote.setNativeId(pi.nativeId, pi._id, pi.storage || {});
// fetch a new device instance if the descriptor changed.
// plugin may return the same instance.
// this avoids device and mixin churn.

View File

@@ -2,7 +2,6 @@ import { ScryptedStatic, SystemManager } from '@scrypted/types';
import AdmZip from 'adm-zip';
import { once } from 'events';
import fs from 'fs';
import { Volume } from 'memfs';
import net from 'net';
import path from 'path';
import { install as installSourceMapSupport } from 'source-map-support';
@@ -19,7 +18,7 @@ import { DeviceManagerImpl, PluginReader, attachPluginRemote, setupPluginRemote
import { PluginStats, startStatsUpdater } from './plugin-remote-stats';
import { createREPLServer } from './plugin-repl';
import { NodeThreadWorker } from './runtime/node-thread-worker';
const { link } = require('linkfs');
import worker_threads from 'worker_threads';
const serverVersion = require('../../package.json').version;
@@ -196,10 +195,18 @@ export function startPluginRemote(mainFilename: string, pluginId: string, peerSe
}
}
let volume: any;
// let volume: any;
let pluginReader: PluginReader;
if (zipOptions?.unzippedPath && fs.existsSync(zipOptions?.unzippedPath)) {
volume = link(fs, ['', path.join(zipOptions.unzippedPath, 'fs')]);
if (worker_threads.isMainThread) {
const fsDir = path.join(zipOptions.unzippedPath, 'fs')
if (fs.existsSync(fsDir))
process.chdir(fsDir);
else
process.chdir(zipOptions.unzippedPath);
}
// volume = link(fs, ['', path.join(zipOptions.unzippedPath, 'fs')]);
pluginReader = name => {
const filename = path.join(zipOptions.unzippedPath, name);
if (!fs.existsSync(filename))
@@ -208,18 +215,20 @@ export function startPluginRemote(mainFilename: string, pluginId: string, peerSe
};
}
else {
// this code path was used in testing and should be unreachable.
const admZip = new AdmZip(zipData);
volume = new Volume();
for (const entry of admZip.getEntries()) {
if (entry.isDirectory)
continue;
if (!entry.entryName.startsWith('fs/'))
continue;
const name = entry.entryName.substring('fs/'.length);
volume.mkdirpSync(path.dirname(name));
const data = entry.getData();
volume.writeFileSync(name, data);
}
// volume = new Volume();
// for (const entry of admZip.getEntries()) {
// if (entry.isDirectory)
// continue;
// if (!entry.entryName.startsWith('fs/'))
// continue;
// const name = entry.entryName.substring('fs/'.length);
// volume.mkdirpSync(path.dirname(name));
// const data = entry.getData();
// volume.writeFileSync(name, data);
// }
pluginReader = name => {
const entry = admZip.getEntry(name);
@@ -235,9 +244,6 @@ export function startPluginRemote(mainFilename: string, pluginId: string, peerSe
const pnp = getPluginNodePath(pluginId);
pluginConsole?.log('node modules', pnp);
params.require = (name: string) => {
if (name === 'fakefs' || (name === 'fs' && !packageJson.scrypted.realfs)) {
return volume;
}
if (name === 'realfs') {
return require('fs');
}

View File

@@ -7,9 +7,15 @@ export function getScryptedVolume() {
return volumeDir;
}
export function getPluginVolume(pluginId: string) {
export function getPluginsVolume() {
const volume = getScryptedVolume();
const pluginVolume = path.join(volume, 'plugins', pluginId);
const pluginsVolume = path.join(volume, 'plugins');
return pluginsVolume;
}
export function getPluginVolume(pluginId: string) {
const volume = getPluginsVolume();
const pluginVolume = path.join(volume, pluginId);
return pluginVolume;
}

View File

@@ -21,9 +21,7 @@ export class NodeForkWorker extends ChildProcessWorker {
this.worker = child_process.fork(mainFilename, ['child', this.pluginId], {
stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
env: Object.assign({}, process.env, env, {
NODE_PATH: path.join(getPluginNodePath(this.pluginId), 'node_modules'),
}),
env: Object.assign({}, process.env, env),
serialization: 'advanced',
execArgv,
});

View File

@@ -16,9 +16,7 @@ export class NodeThreadWorker extends EventEmitter implements RuntimeWorker {
this.worker = new worker_threads.Worker(mainFilename, {
argv: ['child-thread', this.pluginId],
env: Object.assign({}, process.env, env, {
NODE_PATH: path.join(getPluginNodePath(this.pluginId), 'node_modules'),
}),
env: Object.assign({}, process.env, env),
});
this.worker.on('exit', () => {

View File

@@ -53,10 +53,12 @@ export class PythonRuntimeWorker extends ChildProcessWorker {
const pluginPythonVersion = options.packageJson.scrypted.pythonVersion?.[os.platform()]?.[os.arch()] || options.packageJson.scrypted.pythonVersion?.default;
if (os.platform() === 'win32') {
pythonPath ||= 'py.exe';
const windowsPythonVersion = pluginPythonVersion || process.env.SCRYPTED_WINDOWS_PYTHON_VERSION;
if (windowsPythonVersion)
args.unshift(windowsPythonVersion)
if (!pythonPath) {
pythonPath = 'py.exe';
const windowsPythonVersion = pluginPythonVersion || process.env.SCRYPTED_WINDOWS_PYTHON_VERSION;
if (windowsPythonVersion)
args.unshift(windowsPythonVersion)
}
}
else if (pluginPythonVersion) {
pythonPath = `python${pluginPythonVersion}`;

View File

@@ -46,13 +46,14 @@ import { getNpmPackageInfo, PluginComponent } from './services/plugin';
import { ServiceControl } from './services/service-control';
import { UsersService } from './services/users';
import { getState, ScryptedStateManager, setState } from './state';
import { Backup } from './services/backup';
interface DeviceProxyPair {
handler: PluginDeviceProxyHandler;
proxy: ScryptedDevice;
}
const MIN_SCRYPTED_CORE_VERSION = 'v0.2.4';
const MIN_SCRYPTED_CORE_VERSION = 'v0.2.6';
const PLUGIN_DEVICE_STATE_VERSION = 2;
interface HttpPluginData {
@@ -65,7 +66,6 @@ export type RuntimeHost = (mainFilename: string, pluginId: string, options: Runt
export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
clusterId = crypto.randomBytes(3).toString('hex');
clusterSecret = crypto.randomBytes(16).toString('hex');
datastore: Level;
plugins: { [id: string]: PluginHost } = {};
pluginDevices: { [id: string]: PluginDevice } = {};
devices: { [id: string]: DeviceProxyPair } = {};
@@ -103,12 +103,11 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
addressSettings = new AddressSettings(this);
usersService = new UsersService(this);
info = new Info();
backup = new Backup(this);
pluginHosts = new Map<string, RuntimeHost>();
constructor(public mainFilename: string, datastore: Level, insecure: http.Server, secure: https.Server, app: express.Application) {
constructor(public mainFilename: string, public datastore: Level, insecure: http.Server, secure: https.Server, app: express.Application) {
super(app);
this.datastore = datastore;
this.app = app;
// ensure that all the users are loaded from the db.
this.usersService.getAllUsers();
@@ -446,6 +445,8 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
return this.addressSettings;
case "users":
return this.usersService;
case 'backup':
return this.backup;
}
}

View File

@@ -1,9 +1,10 @@
import v8 from 'v8';
import vm from 'vm';
import dns from 'dns';
import process from 'process';
import semver from 'semver';
import { RPCResultError, startPeriodicGarbageCollection } from './rpc';
import v8 from 'v8';
import vm from 'vm';
import { PluginError } from './plugin/plugin-error';
import { RPCResultError, startPeriodicGarbageCollection } from './rpc';
import type { Runtime } from './scrypted-server-main';
export function isChildProcess() {
@@ -26,7 +27,7 @@ function start(mainFilename: string, options?: {
// This causes issues with clients that are on "IPv6" networks that are
// actually busted and fail to connect to npm's IPv6 address.
// The workaround is to favor IPv4.
process.env['NODE_OPTIONS'] = '--dns-result-order=ipv4first';
dns.setDefaultResultOrder('ipv4first');
startPeriodicGarbageCollection();

View File

@@ -1,11 +1,16 @@
import { startPluginRemote } from "./plugin/plugin-remote-worker";
import { RpcMessage } from "./rpc";
import worker_threads from "worker_threads";
import v8 from 'v8';
import net from 'net';
import v8 from 'v8';
import worker_threads from "worker_threads";
import { getPluginNodePath } from "./plugin/plugin-npm-dependencies";
import { startPluginRemote } from "./plugin/plugin-remote-worker";
import { SidebandSocketSerializer } from "./plugin/socket-serializer";
import { RpcMessage } from "./rpc";
function start(mainFilename: string) {
const pluginId = process.argv[3];
console.log('starting plugin', pluginId);
module.paths.push(getPluginNodePath(pluginId));
if (process.argv[2] === 'child-thread') {
const peer = startPluginRemote(mainFilename, process.argv[3], (message, reject) => {
try {

View File

@@ -26,11 +26,12 @@ import { getNpmPackageInfo } from './services/plugin';
import { setScryptedUserPassword, UsersService } from './services/users';
import { sleep } from './sleep';
import { ONE_DAY_MILLISECONDS, UserToken } from './usertoken';
import AdmZip from 'adm-zip';
export type Runtime = ScryptedRuntime;
if (!semver.gte(process.version, '16.0.0')) {
throw new Error('"node" version out of date. Please update node to v16 or higher.')
if (!semver.gte(process.version, '18.0.0')) {
throw new Error('"node" version out of date. Please update node to v18 or higher.')
}
process.on('unhandledRejection', error => {
@@ -148,6 +149,10 @@ async function start(mainFilename: string, options?: {
realm: 'Scrypted',
}, async (username, password, callback) => {
const user = await db.tryGet(ScryptedUser, username);
if (!user) {
callback(false);
return;
}
const salted = user.salt + password;
const hash = crypto.createHash('sha256');
@@ -215,14 +220,6 @@ async function start(mainFilename: string, options?: {
}
app.use(async (req, res, next) => {
const defaultAuthentication = getDefaultAuthentication(req);
if (defaultAuthentication) {
res.locals.username = defaultAuthentication._id;
res.locals.aclId = defaultAuthentication.aclId;
next();
return;
}
// the remote address may be ipv6 prefixed so use a fuzzy match.
// eg ::ffff:192.168.2.124
if (process.env.SCRYPTED_ADMIN_USERNAME
@@ -302,6 +299,15 @@ async function start(mainFilename: string, options?: {
else if (req.query['scryptedToken']) {
checkToken(req.query.scryptedToken.toString());
}
if (!res.locals.username) {
const defaultAuthentication = getDefaultAuthentication(req);
if (defaultAuthentication) {
res.locals.username = defaultAuthentication._id;
res.locals.aclId = defaultAuthentication.aclId;
}
}
next();
});
@@ -335,6 +341,36 @@ async function start(mainFilename: string, options?: {
await options?.onRuntimeCreated?.(scrypted);
await scrypted.start();
app.post('/web/component/restore', async (req, res) => {
const buffers: Buffer[] = [];
req.on('data', b => buffers.push(b));
try {
await once(req, 'end');
await scrypted.backup.restore(Buffer.concat(buffers))
}
catch (e) {
res.send({
error: "Error during restore.",
});
return;
}
});
app.get('/web/component/backup', async (req, res) => {
try {
const zipBuffer = await scrypted.backup.createBackup();
// the file is a normal zip file, but an extension is added to prevent safari, etc, from unzipping it automatically.
res.header('Content-Disposition', 'attachment; filename="scrypted.zip.backup"')
res.send(zipBuffer);
}
catch (e) {
console.error('Backup error', e);
res.status(500);
res.send('Internal Error');
}
});
app.get(['/web/component/script/npm/:pkg', '/web/component/script/npm/@:owner/:pkg'], async (req, res) => {
const { owner, pkg } = req.params;
let endpoint = pkg;
@@ -602,22 +638,6 @@ async function start(mainFilename: string, options?: {
return;
}
// env based anon user login
const defaultAuthentication = getDefaultAuthentication(req);
if (defaultAuthentication) {
const userToken = new UserToken(defaultAuthentication._id, defaultAuthentication.aclId, Date.now());
res.send({
...createTokens(userToken),
expiration: ONE_DAY_MILLISECONDS,
username: defaultAuthentication,
// TODO: do not return the token from a short term auth mechanism?
token: defaultAuthentication?.token,
...alternateAddresses,
hostname,
});
return;
}
// basic auth
if (req.protocol === 'https' && req.headers.authorization) {
const username = await new Promise(resolve => {
@@ -662,6 +682,22 @@ async function start(mainFilename: string, options?: {
})
}
catch (e) {
// env based anon user login
const defaultAuthentication = getDefaultAuthentication(req);
if (defaultAuthentication) {
const userToken = new UserToken(defaultAuthentication._id, defaultAuthentication.aclId, Date.now());
res.send({
...createTokens(userToken),
expiration: ONE_DAY_MILLISECONDS,
username: defaultAuthentication,
// TODO: do not return the token from a short term auth mechanism?
token: defaultAuthentication?.token,
...alternateAddresses,
hostname,
});
return;
}
res.send({
error: e?.message || 'Unknown Error.',
hasLogin,

View File

@@ -0,0 +1,68 @@
import fs from 'fs';
import path from 'path';
import Level from '../level';
import { sleep } from '../sleep';
import { getPluginsVolume, getScryptedVolume } from '../plugin/plugin-volume';
import AdmZip from 'adm-zip';
import { ScryptedRuntime } from '../runtime';
import { getPluginNodePath } from '../plugin/plugin-npm-dependencies';
export class Backup {
constructor(public runtime: ScryptedRuntime) {}
async createBackup(): Promise<Buffer> {
const volumeDir = getScryptedVolume();
const backupDbPath = path.join(volumeDir, 'backup.db');
await fs.promises.rm(backupDbPath, {
recursive: true,
force: true,
});
const backupDb = new Level(backupDbPath);
await backupDb.open();
for await (const [key, value] of this.runtime.datastore.iterator()) {
await backupDb.put(key, value);
}
await backupDb.close();
const backupZip = path.join(volumeDir, 'backup.zip');
await fs.promises.rm(backupZip, {
recursive: true,
force: true,
});
const zip = new AdmZip();
await zip.addLocalFolderPromise(backupDbPath, {});
return zip.toBufferPromise();
}
async restore(b: Buffer): Promise<void> {
const volumeDir = getScryptedVolume();
const dbPath = path.join(volumeDir, 'scrypted.db');
const zip = new AdmZip(b);
if (!zip.test())
throw new Error('backup zip test failed.');
this.runtime.kill();
await sleep(5000);
await this.runtime.datastore.close();
// nuke the existing database path
await fs.promises.rm(dbPath, {
recursive: true,
force: true,
});
// nuke all the plugins and associated files downloaded by thhem.
// first run after restore will reinstall everything.
await fs.promises.rm(getPluginsVolume(), {
recursive: true,
force: true,
});
zip.extractAllTo(dbPath, true);
this.runtime.serviceControl.restart();
}
}