mirror of
https://github.com/koush/scrypted.git
synced 2026-02-07 16:02:13 +00:00
Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec2e4d64fd | ||
|
|
44644448f5 | ||
|
|
0a86d5c4ea | ||
|
|
20282e05ea | ||
|
|
9d6b405fa9 | ||
|
|
b82ce5ff45 | ||
|
|
f461198e1e | ||
|
|
7505e6907a | ||
|
|
c1046d5706 | ||
|
|
a61c06b607 | ||
|
|
d3df5742e6 | ||
|
|
68ac42ca46 | ||
|
|
bb7c6ef8b9 | ||
|
|
446e8ed61e | ||
|
|
80372b35f2 | ||
|
|
57eff2f296 | ||
|
|
d996088041 | ||
|
|
04be70019b | ||
|
|
51732d0dcd | ||
|
|
e40bc3ddee | ||
|
|
3f4409e1c3 | ||
|
|
63b7616ab3 | ||
|
|
29059691ce | ||
|
|
531a9d28dc | ||
|
|
3314b4d9ca | ||
|
|
37df9810c8 | ||
|
|
47c1cbba3c | ||
|
|
ded7e549bb | ||
|
|
abb2b85cec | ||
|
|
7d157d2882 | ||
|
|
c6c0a225dd | ||
|
|
276fc386ec | ||
|
|
0b21afd193 | ||
|
|
1032e58e3b | ||
|
|
4987b01167 | ||
|
|
28bb8c5b3c | ||
|
|
2160170c3a | ||
|
|
c0eac9053b | ||
|
|
d57501dd42 | ||
|
|
264cb0404f | ||
|
|
dc9f4b39a8 | ||
|
|
653eeceaf2 | ||
|
|
3d8711947a | ||
|
|
38038d5f30 | ||
|
|
e21d9c3a0c | ||
|
|
7b8c014b3b | ||
|
|
55a30864fd | ||
|
|
4f419ff75c | ||
|
|
638a4f28ad | ||
|
|
8970154b8f | ||
|
|
c96debaaed | ||
|
|
fe7b479235 | ||
|
|
aa1486e641 |
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -11,9 +11,6 @@
|
||||
[submodule "external/werift"]
|
||||
path = external/werift
|
||||
url = ../../koush/werift-webrtc
|
||||
[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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Home Assistant Addon Configuration
|
||||
name: Scrypted
|
||||
version: "v0.118.0-jammy-full"
|
||||
version: "v0.120.0-jammy-full"
|
||||
slug: scrypted
|
||||
description: Scrypted is a high performance home video integration and automation platform
|
||||
url: "https://github.com/koush/scrypted"
|
||||
|
||||
@@ -30,8 +30,8 @@ apt-get -y install intel-media-va-driver-non-free &&
|
||||
apt-get -y dist-upgrade;
|
||||
|
||||
# manual installation
|
||||
# https://github.com/intel/compute-runtime/releases/tag/24.13.29138.7
|
||||
|
||||
# https://github.com/intel/compute-runtime/releases/tag/24.35.30872.22
|
||||
# these debs are seemingly ubuntu 22.04 only.
|
||||
|
||||
rm -rf /tmp/gpu && mkdir -p /tmp/gpu && cd /tmp/gpu
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ fi
|
||||
UBUNTU_22_04=$(lsb_release -r | grep "22.04")
|
||||
UBUNTU_24_04=$(lsb_release -r | grep "24.04")
|
||||
|
||||
if [ -z "$UBUNTU_22_04" ]
|
||||
if [ -z "$UBUNTU_22_04" ] && [ -z "$UBUNTU_24_04" ]
|
||||
then
|
||||
# proxmox is compatible with ubuntu 22.04, check for /etc/pve directory
|
||||
if [ -d "/etc/pve" ]
|
||||
@@ -23,6 +23,13 @@ then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ -n "$UBUNTU_22_04" ]
|
||||
then
|
||||
distro="22.04_amd64"
|
||||
else
|
||||
distro="24.04_amd64"
|
||||
fi
|
||||
|
||||
dpkg --purge --force-remove-reinstreq intel-driver-compiler-npu intel-fw-npu intel-level-zero-npu
|
||||
|
||||
# no errors beyond this point
|
||||
@@ -30,27 +37,23 @@ set -e
|
||||
|
||||
rm -rf /tmp/npu && mkdir -p /tmp/npu && cd /tmp/npu
|
||||
|
||||
# different npu downloads for ubuntu versions
|
||||
if [ -n "$UBUNTU_22_04" ]
|
||||
then
|
||||
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v1.8.0/intel-driver-compiler-npu_1.8.0.20240916-10885588273_ubuntu22.04_amd64.deb
|
||||
# firmware can only be installed on host. will cause problems inside container.
|
||||
if [ -n "$INTEL_FW_NPU" ]
|
||||
then
|
||||
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v1.8.0/intel-fw-npu_1.8.0.20240916-10885588273_ubuntu22.04_amd64.deb
|
||||
fi
|
||||
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v1.8.0/intel-level-zero-npu_1.8.0.20240916-10885588273_ubuntu22.04_amd64.deb
|
||||
else
|
||||
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v1.8.0/intel-driver-compiler-npu_1.8.0.20240916-10885588273_ubuntu24.04_amd64.deb
|
||||
if [ -n "$INTEL_FW_NPU" ]
|
||||
then
|
||||
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v1.8.0/intel-fw-npu_1.8.0.20240916-10885588273_ubuntu24.04_amd64.deb
|
||||
fi
|
||||
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v1.8.0/intel-level-zero-npu_1.8.0.20240916-10885588273_ubuntu24.04_amd64.deb
|
||||
fi
|
||||
# level zero must also be installed
|
||||
LEVEL_ZERO_VERSION=1.18.3
|
||||
# https://github.com/oneapi-src/level-zero
|
||||
curl -O -L https://github.com/oneapi-src/level-zero/releases/download/v"$LEVEL_ZERO_VERSION"/level-zero_"$LEVEL_ZERO_VERSION"+u$distro.deb
|
||||
curl -O -L https://github.com/oneapi-src/level-zero/releases/download/v"$LEVEL_ZERO_VERSION"/level-zero-devel_"$LEVEL_ZERO_VERSION"+u$distro.deb
|
||||
|
||||
curl -O -L https://github.com/oneapi-src/level-zero/releases/download/v1.17.6/level-zero_1.17.6+u22.04_amd64.deb
|
||||
curl -O -L https://github.com/oneapi-src/level-zero/releases/download/v1.17.6/level-zero-devel_1.17.6+u22.04_amd64.deb
|
||||
# npu driver
|
||||
# https://github.com/intel/linux-npu-driver
|
||||
NPU_VERSION=1.8.0
|
||||
NPU_VERSION_DATE=20240916-10885588273
|
||||
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v"$NPU_VERSION"/intel-driver-compiler-npu_$NPU_VERSION."$NPU_VERSION_DATE"_ubuntu$distro.deb
|
||||
# firmware can only be installed on host. will cause problems inside container.
|
||||
if [ -n "$INTEL_FW_NPU" ]
|
||||
then
|
||||
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v"$NPU_VERSION"/intel-fw-npu_$NPU_VERSION."$NPU_VERSION_DATE"_ubuntu$distro.deb
|
||||
fi
|
||||
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v"$NPU_VERSION"/intel-level-zero-npu_$NPU_VERSION."$NPU_VERSION_DATE"_ubuntu$distro.deb
|
||||
|
||||
apt -y update
|
||||
apt -y install libtbb12
|
||||
|
||||
@@ -40,8 +40,6 @@ echo "Installing Scrypted dependencies..."
|
||||
RUN_IGNORE xcode-select --install
|
||||
RUN brew update
|
||||
RUN_IGNORE brew install node@20
|
||||
# snapshot plugin and others
|
||||
RUN brew install libvips
|
||||
# dlib
|
||||
RUN brew install cmake
|
||||
|
||||
|
||||
@@ -10,14 +10,15 @@ export DEBIAN_FRONTEND=noninteractive
|
||||
if [ -e "volume/.pull" ]
|
||||
then
|
||||
rm -rf volume/.pull
|
||||
docker compose pull && docker container prune -f && docker image prune -a -f
|
||||
PULL="--pull"
|
||||
(sleep 300 && docker container prune -f && docker image prune -a -f) &
|
||||
else
|
||||
# always background pull in case there's a broken image.
|
||||
(docker compose pull && docker container prune -f && docker image prune -a -f) &
|
||||
(sleep 300 && docker compose pull && docker container prune -f && docker image prune -a -f) &
|
||||
fi
|
||||
|
||||
# do not daemonize, when it exits, systemd will restart it.
|
||||
# force a recreate as .env may have changed.
|
||||
# furthermore force recreate gets the container back into a known state
|
||||
# which is preferable in case the user has made manual changes and then restarts.
|
||||
WATCHTOWER_HTTP_API_TOKEN=$(echo $RANDOM | md5sum | head -c 32) docker compose up --force-recreate --abort-on-container-exit
|
||||
WATCHTOWER_HTTP_API_TOKEN=$(echo $RANDOM | md5sum | head -c 32) docker compose up --force-recreate --abort-on-container-exit $PULL
|
||||
|
||||
@@ -18,17 +18,30 @@ function readyn() {
|
||||
}
|
||||
|
||||
cd /tmp
|
||||
SCRYPTED_VERSION=v0.116.0
|
||||
SCRYPTED_VERSION=v0.120.0
|
||||
SCRYPTED_TAR_ZST=scrypted-$SCRYPTED_VERSION.tar.zst
|
||||
if [ -z "$VMID" ]
|
||||
then
|
||||
VMID=10443
|
||||
fi
|
||||
|
||||
SCRYPTED_BACKUP_VMID=10445
|
||||
if [ -n "$SCRYPTED_RESTORE" ]
|
||||
then
|
||||
pct config $VMID 2>&1 > /dev/null
|
||||
if [ "$?" != "0" ]
|
||||
then
|
||||
echo "VMID $VMID not found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# append existing mac address.
|
||||
HWADDR=",hwaddr=$(pct config $VMID | grep -oE 'hwaddr=[A-Z0-9:]+' | cut -d '=' -f 2)"
|
||||
RESTORE_HOSTNAME=$(pct config $VMID | grep -oE 'hostname: [^[:space:]]+' | cut -d ':' -f 2- | tr -d ' ')
|
||||
|
||||
pct destroy $SCRYPTED_BACKUP_VMID 2>&1 > /dev/null
|
||||
RESTORE_VMID=$VMID
|
||||
VMID=10444
|
||||
VMID=$SCRYPTED_BACKUP_VMID
|
||||
pct destroy $VMID 2>&1 > /dev/null
|
||||
fi
|
||||
|
||||
@@ -39,18 +52,49 @@ then
|
||||
mv scrypted.tar.zst $SCRYPTED_TAR_ZST
|
||||
fi
|
||||
|
||||
echo "Checking for existing container."
|
||||
pct config $VMID
|
||||
if [ "$?" == "0" ]
|
||||
if [[ "$@" =~ "--force" ]]
|
||||
then
|
||||
echo ""
|
||||
echo "Existing container $VMID found. Run this script with --force to overwrite the existing container."
|
||||
echo "This will wipe all existing data. Clone the existing container to retain the data, then reassign the owner of the scrypted volume after installation is complete."
|
||||
echo ""
|
||||
echo "bash $0 --force"
|
||||
echo ""
|
||||
IGNORE_EXISTING=true
|
||||
fi
|
||||
|
||||
if [ -n "$SCRYPTED_RESTORE" ]
|
||||
then
|
||||
IGNORE_EXISTING=true
|
||||
fi
|
||||
|
||||
if [ -z "$IGNORE_EXISTING" ]
|
||||
then
|
||||
echo "Checking for existing container."
|
||||
pct config $VMID
|
||||
if [ "$?" == "0" ]
|
||||
then
|
||||
echo ""
|
||||
echo "==============================================================="
|
||||
echo "Existing container $VMID found."
|
||||
echo "Please choose from the following options to resolve this error."
|
||||
echo "==============================================================="
|
||||
echo ""
|
||||
echo "1. To reinstall and reset Scrypted, run this script with --force to overwrite the existing container."
|
||||
echo "THIS WILL WIPE THE EXISTING CONFIGURATION:"
|
||||
echo ""
|
||||
echo "VMID=$VMID bash $0 --force"
|
||||
echo ""
|
||||
echo "2. To reinstall Scrypted and and retain existing configuration, run this script with the environment variable SCRYPTED_RESTORE=true."
|
||||
echo "This preserves existing data. Creating a backup within Scrypted is highly recommended in case the reset fails."
|
||||
echo "THIS WILL WIPE ADDITIONAL VOLUMES SUCH AS NVR STORAGE. NVR volumes will need to be readded after the restore:"
|
||||
echo ""
|
||||
echo "SCRYPTED_RESTORE=true VMID=$VMID bash $0"
|
||||
echo ""
|
||||
echo "3. To install and run multiple Scrypted containers, run this script with the environment variable specifying"
|
||||
echo "the new VMID=<number>. For example, to create a new LXC with VMID 12345:"
|
||||
echo ""
|
||||
echo "VMID=12345 bash $0"
|
||||
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
pct stop $VMID 2>&1 > /dev/null
|
||||
pct restore $VMID $SCRYPTED_TAR_ZST $@
|
||||
|
||||
if [ "$?" != "0" ]
|
||||
@@ -73,7 +117,7 @@ then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
pct set $VMID -net0 name=eth0,bridge=vmbr0,ip=dhcp,ip6=auto
|
||||
pct set $VMID -net0 name=eth0,bridge=vmbr0,ip=dhcp,ip6=auto$HWADDR
|
||||
if [ "$?" != "0" ]
|
||||
then
|
||||
echo ""
|
||||
@@ -82,6 +126,18 @@ then
|
||||
echo "Ignoring... Please verify your container's network settings."
|
||||
fi
|
||||
|
||||
if [ -n "$RESTORE_HOSTNAME" ]
|
||||
then
|
||||
pct set $VMID --hostname $RESTORE_HOSTNAME
|
||||
if [ "$?" != "0" ]
|
||||
then
|
||||
echo ""
|
||||
echo "pct hostname restore failed"
|
||||
echo ""
|
||||
echo "Ignoring... Please verify your container's dns settings."
|
||||
fi
|
||||
fi
|
||||
|
||||
CONF=/etc/pve/lxc/$VMID.conf
|
||||
if [ -f "$CONF" ]
|
||||
then
|
||||
@@ -92,23 +148,89 @@ fi
|
||||
|
||||
if [ -n "$SCRYPTED_RESTORE" ]
|
||||
then
|
||||
readyn "Running this script will reset Scrypted to a factory state while preserving existing data. IT IS RECOMMENDED TO CREATE A BACKUP FIRST. Are you sure you want to continue?"
|
||||
echo ""
|
||||
echo ""
|
||||
echo "Running this script will reset the Scrypted container to a factory state while preserving existing data."
|
||||
echo "IT IS RECOMMENDED TO CREATE A BACKUP INSIDE SCRYPTED FIRST."
|
||||
readyn "Are you sure you want to continue?"
|
||||
if [ "$yn" != "y" ]
|
||||
then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Stopping scrypted..."
|
||||
pct stop $RESTORE_VMID 2>&1 > /dev/null
|
||||
|
||||
echo "Preparing rootfs reset..."
|
||||
# this copies the
|
||||
pct set 10444 --delete mp0 && pct set 10444 --delete unused0 && pct move-volume $RESTORE_VMID mp0 --target-vmid 10444 --target-volume mp0
|
||||
|
||||
# remove the empty data volume from the downloaded image.
|
||||
pct set $SCRYPTED_BACKUP_VMID --delete mp0 && pct set $SCRYPTED_BACKUP_VMID --delete unused0
|
||||
if [ "$?" != "0" ]
|
||||
then
|
||||
echo "Failed to remove data volume from image."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# create a backup that contains only the root disk.
|
||||
rm *.tar
|
||||
vzdump 10444 --dumpdir /tmp
|
||||
VMID=$RESTORE_VMID
|
||||
echo "Moving data volume to backup..."
|
||||
pct restore $VMID *.tar $@
|
||||
vzdump $SCRYPTED_BACKUP_VMID --dumpdir /tmp
|
||||
|
||||
pct destroy 10444
|
||||
# this moves the data volume from the current scrypted instance to the backup target to preserve it during
|
||||
# the restore.
|
||||
pct move-volume $RESTORE_VMID mp0 --target-vmid $SCRYPTED_BACKUP_VMID --target-volume mp0
|
||||
if [ "$?" != "0" ]
|
||||
then
|
||||
echo "Failed to move data volume to backup."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# arguments: from to mp hide-warning
|
||||
function move_volume() {
|
||||
HAS_VOLUME=$(pct config $1 | grep $3:)
|
||||
if [ -n "$HAS_VOLUME" ]
|
||||
then
|
||||
echo "Moving $3..."
|
||||
# this may error and there may be recording loss. bailing at ths point is already too late.
|
||||
pct move-volume $1 $3 --target-vmid $2 --target-volume $3
|
||||
|
||||
# volume must be inside /mnt to get into docker container
|
||||
INSIDE_MNT=$(echo $HAS_VOLUME | grep /mnt)
|
||||
if [ -z "$INSIDE_MNT" -a -z "$4" ]
|
||||
then
|
||||
echo "##################################################################"
|
||||
echo "The following mount point is not visible to the"
|
||||
echo "Scrypted docker container within the LXC:"
|
||||
echo ""
|
||||
echo "$HAS_VOLUME"
|
||||
echo ""
|
||||
echo "This recordings directory will be unavailable."
|
||||
echo "The mount point must be updated to a path within /mnt."
|
||||
echo "https://docs.scrypted.app/scrypted-nvr/recording-storage.html#proxmox-ve-mount-point"
|
||||
echo "##################################################################"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# try moving 5 volumes, any more than that seems unlikely
|
||||
move_volume $RESTORE_VMID $SCRYPTED_BACKUP_VMID mp1 hide-warning
|
||||
move_volume $RESTORE_VMID $SCRYPTED_BACKUP_VMID mp2 hide-warning
|
||||
move_volume $RESTORE_VMID $SCRYPTED_BACKUP_VMID mp3 hide-warning
|
||||
move_volume $RESTORE_VMID $SCRYPTED_BACKUP_VMID mp4 hide-warning
|
||||
move_volume $RESTORE_VMID $SCRYPTED_BACKUP_VMID mp5 hide-warning
|
||||
|
||||
VMID=$RESTORE_VMID
|
||||
echo "Restoring with reset image..."
|
||||
pct restore --force 1 $VMID *.tar $@
|
||||
|
||||
echo "Restoring volumes..."
|
||||
move_volume $SCRYPTED_BACKUP_VMID $VMID mp0 hide-warning
|
||||
move_volume $SCRYPTED_BACKUP_VMID $VMID mp1
|
||||
move_volume $SCRYPTED_BACKUP_VMID $VMID mp2
|
||||
move_volume $SCRYPTED_BACKUP_VMID $VMID mp3
|
||||
move_volume $SCRYPTED_BACKUP_VMID $VMID mp4
|
||||
move_volume $SCRYPTED_BACKUP_VMID $VMID mp5
|
||||
|
||||
pct destroy $SCRYPTED_BACKUP_VMID
|
||||
fi
|
||||
|
||||
readyn "Add udev rule for hardware acceleration? This may conflict with existing rules."
|
||||
@@ -143,4 +265,5 @@ then
|
||||
fi
|
||||
|
||||
echo "Scrypted setup is complete and the container resources can be started."
|
||||
echo "Scrypted NVR users should provide at least 4 cores and 16GB RAM prior to starting."
|
||||
echo ""
|
||||
echo "Scrypted NVR servers should run the disk setup script in the documentation to add storage prior to starting the container."
|
||||
|
||||
5
plugins/alexa/package-lock.json
generated
5
plugins/alexa/package-lock.json
generated
@@ -1,12 +1,11 @@
|
||||
{
|
||||
"name": "@scrypted/alexa",
|
||||
"version": "0.3.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/alexa",
|
||||
"version": "0.3.3",
|
||||
"version": "0.3.4",
|
||||
"dependencies": {
|
||||
"axios": "^1.3.4",
|
||||
"uuid": "^9.0.0"
|
||||
@@ -203,4 +202,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/alexa",
|
||||
"version": "0.3.3",
|
||||
"version": "0.3.4",
|
||||
"scripts": {
|
||||
"scrypted-setup-project": "scrypted-setup-project",
|
||||
"prescrypted-setup-project": "scrypted-package-json",
|
||||
|
||||
@@ -53,6 +53,12 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
|
||||
title: "Pairing Key",
|
||||
description: "The pairing key used to validate requests from Alexa. Clear this key or delete the plugin to allow pairing with a different Alexa login.",
|
||||
},
|
||||
disableAutoAdd: {
|
||||
title: "Disable auto add",
|
||||
description: "Disable automatic enablement of devices.",
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
},
|
||||
});
|
||||
|
||||
accessToken: Promise<string>;
|
||||
@@ -116,6 +122,10 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
|
||||
if (!supportedTypes.has(device.type))
|
||||
return DeviceMixinStatus.NotSupported;
|
||||
|
||||
if (this.storageSettings.values.disableAutoAdd) {
|
||||
return DeviceMixinStatus.Skip;
|
||||
}
|
||||
|
||||
mixins.push(this.id);
|
||||
|
||||
const plugins = await systemManager.getComponent('plugins');
|
||||
@@ -671,7 +681,8 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
|
||||
enum DeviceMixinStatus {
|
||||
NotSupported = 0,
|
||||
Setup = 1,
|
||||
AlreadySetup = 2
|
||||
AlreadySetup = 2,
|
||||
Skip = 3,
|
||||
}
|
||||
|
||||
class HttpResponseLoggingImpl implements AlexaHttpResponse {
|
||||
|
||||
4
plugins/diagnostics/package-lock.json
generated
4
plugins/diagnostics/package-lock.json
generated
@@ -13,7 +13,7 @@
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.5.4"
|
||||
},
|
||||
"version": "0.0.17"
|
||||
"version": "0.0.18"
|
||||
},
|
||||
"../../common": {
|
||||
"name": "@scrypted/common",
|
||||
@@ -844,5 +844,5 @@
|
||||
"dev": true
|
||||
}
|
||||
},
|
||||
"version": "0.0.17"
|
||||
"version": "0.0.18"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/diagnostics",
|
||||
"version": "0.0.17",
|
||||
"version": "0.0.18",
|
||||
"scripts": {
|
||||
"scrypted-setup-project": "scrypted-setup-project",
|
||||
"prescrypted-setup-project": "scrypted-package-json",
|
||||
|
||||
@@ -128,6 +128,9 @@ class DiagnosticsPlugin extends ScryptedDeviceBase implements Settings {
|
||||
await device.sendNotification('Scrypted Diagnostics', {
|
||||
body: 'Body',
|
||||
subtitle: 'Subtitle',
|
||||
android: {
|
||||
channel: 'diagnostics',
|
||||
}
|
||||
}, mo);
|
||||
|
||||
this.warnStep(console, 'Check the device for the notification.');
|
||||
@@ -512,6 +515,7 @@ class DiagnosticsPlugin extends ScryptedDeviceBase implements Settings {
|
||||
|
||||
await this.validate(this.console, 'Deprecated Plugins', async () => {
|
||||
const defunctPlugins = [
|
||||
'@scrypted/electron-core',
|
||||
'@scrypted/opencv',
|
||||
'@scrypted/python-codecs',
|
||||
'@scrypted/pam-diff',
|
||||
|
||||
@@ -111,6 +111,12 @@ export class HomeKitPlugin extends ScryptedDeviceBase implements MixinProvider,
|
||||
hide: true,
|
||||
description: 'The last home hub to request a recording. Internally used to determine if a streaming request is coming from remote wifi.',
|
||||
},
|
||||
autoAdd: {
|
||||
title: "Auto enable",
|
||||
description: "Automatically enable this mixin on new devices.",
|
||||
type: 'boolean',
|
||||
defaultValue: true,
|
||||
},
|
||||
});
|
||||
mergedDevices = new Set<string>();
|
||||
|
||||
@@ -218,7 +224,8 @@ export class HomeKitPlugin extends ScryptedDeviceBase implements MixinProvider,
|
||||
|
||||
try {
|
||||
const mixins = (device.mixins || []).slice();
|
||||
if (!mixins.includes(this.id)) {
|
||||
const autoAdd = this.storageSettings.values.autoAdd ?? true;
|
||||
if (!mixins.includes(this.id) && autoAdd) {
|
||||
// don't sync this by default, as it's solely for automations
|
||||
if (device.type === ScryptedDeviceType.Notifier)
|
||||
continue;
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"scrypted.debugHost": "127.0.0.1",
|
||||
"scrypted.debugHost": "scrypted-nvr",
|
||||
}
|
||||
4
plugins/prebuffer-mixin/package-lock.json
generated
4
plugins/prebuffer-mixin/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/prebuffer-mixin",
|
||||
"version": "0.10.36",
|
||||
"version": "0.10.37",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/prebuffer-mixin",
|
||||
"version": "0.10.36",
|
||||
"version": "0.10.37",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/prebuffer-mixin",
|
||||
"version": "0.10.36",
|
||||
"version": "0.10.37",
|
||||
"description": "Video Stream Rebroadcast, Prebuffer, and Management Plugin for Scrypted.",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -196,13 +196,33 @@ class PrebufferSession {
|
||||
return;
|
||||
this.console.log(this.streamName, 'prebuffer session started');
|
||||
this.parserSessionPromise = this.startPrebufferSession();
|
||||
this.parserSessionPromise.then(pso => pso.killed.finally(() => {
|
||||
this.console.error(this.streamName, 'prebuffer session ended');
|
||||
this.parserSessionPromise = undefined;
|
||||
}))
|
||||
let active = false;
|
||||
this.parserSessionPromise.then(pso => {
|
||||
pso.once('rtsp', () => {
|
||||
active = true;
|
||||
if (!this.mixin.online)
|
||||
this.mixin.online = true;
|
||||
});
|
||||
|
||||
pso.killed.finally(() => {
|
||||
this.console.error(this.streamName, 'prebuffer session ended');
|
||||
this.parserSessionPromise = undefined;
|
||||
});
|
||||
})
|
||||
.catch(e => {
|
||||
this.console.error(this.streamName, 'prebuffer session ended with error', e);
|
||||
this.parserSessionPromise = undefined;
|
||||
|
||||
if (!active) {
|
||||
// find sessions that arent this one, and check their prebuffers to see if any data has been received.
|
||||
// if there's no data, then consider this camera offline.
|
||||
const others = [...this.mixin.sessions.values()].filter(s => s !== this);
|
||||
if (others.length) {
|
||||
const hasData = others.some(s => s.rtspPrebuffer.length);
|
||||
if (!hasData && this.mixin.online)
|
||||
this.mixin.online = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1415,11 +1435,6 @@ class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera> implements Vid
|
||||
}
|
||||
}
|
||||
|
||||
if (!enabledIds.length)
|
||||
this.online = true;
|
||||
|
||||
let active = 0;
|
||||
|
||||
// figure out the default stream and streams that may have been removed due to
|
||||
// a config change.
|
||||
const toRemove = new Set(this.sessions.keys());
|
||||
@@ -1462,23 +1477,13 @@ class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera> implements Vid
|
||||
}
|
||||
|
||||
session.ensurePrebufferSession();
|
||||
let wasActive = false;
|
||||
try {
|
||||
this.console.log(name, 'prebuffer session starting');
|
||||
const ps = await session.parserSessionPromise;
|
||||
active++;
|
||||
wasActive = true;
|
||||
this.online = !!active;
|
||||
await ps.killed;
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
finally {
|
||||
if (wasActive)
|
||||
active--;
|
||||
wasActive = false;
|
||||
this.online = !!active;
|
||||
}
|
||||
this.console.log(this.name, 'restarting prebuffer session in 5 seconds');
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
}
|
||||
|
||||
6
plugins/reolink/package-lock.json
generated
6
plugins/reolink/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/reolink",
|
||||
"version": "0.0.96",
|
||||
"version": "0.0.98",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/reolink",
|
||||
"version": "0.0.96",
|
||||
"version": "0.0.98",
|
||||
"license": "Apache",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
@@ -35,7 +35,7 @@
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.3.65",
|
||||
"version": "0.3.67",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/reolink",
|
||||
"version": "0.0.97",
|
||||
"version": "0.0.98",
|
||||
"description": "Reolink Plugin for Scrypted",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache",
|
||||
|
||||
@@ -52,11 +52,13 @@ class ReolinkCameraSiren extends ScryptedDeviceBase implements OnOff {
|
||||
|
||||
class ReolinkCamera extends RtspSmartCamera implements Camera, DeviceProvider, Reboot, Intercom, ObjectDetector, PanTiltZoom {
|
||||
client: ReolinkCameraClient;
|
||||
clientWithToken: ReolinkCameraClient;
|
||||
onvifClient: OnvifCameraAPI;
|
||||
onvifIntercom = new OnvifIntercom(this);
|
||||
videoStreamOptions: Promise<UrlMediaStreamOptions[]>;
|
||||
motionTimeout: NodeJS.Timeout;
|
||||
siren: ReolinkCameraSiren;
|
||||
batteryTimeout: NodeJS.Timeout;
|
||||
|
||||
storageSettings = new StorageSettings(this, {
|
||||
doorbell: {
|
||||
@@ -180,6 +182,7 @@ class ReolinkCamera extends RtspSmartCamera implements Camera, DeviceProvider, R
|
||||
}
|
||||
const api = this.getClient();
|
||||
const deviceInfo = await api.getDeviceInfo();
|
||||
this.console.log('deviceInfo', JSON.stringify(deviceInfo));
|
||||
this.storageSettings.values.deviceInfo = deviceInfo;
|
||||
await this.updateAbilities();
|
||||
await this.updateDevice();
|
||||
@@ -217,7 +220,13 @@ class ReolinkCamera extends RtspSmartCamera implements Camera, DeviceProvider, R
|
||||
|
||||
async updateAbilities() {
|
||||
const api = this.getClient();
|
||||
const abilities = await api.getAbility();
|
||||
const apiWithToken = this.getClientWithToken();
|
||||
let abilities;
|
||||
try {
|
||||
abilities = await api.getAbility();
|
||||
} catch (e) {
|
||||
abilities = await apiWithToken.getAbility();
|
||||
}
|
||||
this.storageSettings.values.abilities = abilities;
|
||||
this.console.log('getAbility', JSON.stringify(abilities));
|
||||
}
|
||||
@@ -286,6 +295,11 @@ class ReolinkCamera extends RtspSmartCamera implements Camera, DeviceProvider, R
|
||||
&& this.storageSettings.values.abilities?.value?.Ability?.supportAudioAlarm?.ver !== 0;
|
||||
}
|
||||
|
||||
hasBattery() {
|
||||
const batteryConfigVer = this.storageSettings.values.abilities?.value?.Ability?.abilityChn?.[this.getRtspChannel()]?.battery?.ver ?? 0;
|
||||
return batteryConfigVer > 0;
|
||||
}
|
||||
|
||||
async updateDevice() {
|
||||
const interfaces = this.provider.getInterfaces();
|
||||
let type = ScryptedDeviceType.Camera;
|
||||
@@ -311,8 +325,31 @@ class ReolinkCamera extends RtspSmartCamera implements Camera, DeviceProvider, R
|
||||
}
|
||||
if (this.hasSiren())
|
||||
interfaces.push(ScryptedInterface.DeviceProvider);
|
||||
if (this.hasBattery()) {
|
||||
interfaces.push(ScryptedInterface.Battery, ScryptedInterface.Online);
|
||||
this.startBatteryCheckInterval();
|
||||
}
|
||||
|
||||
await this.provider.updateDevice(this.nativeId, name, interfaces, type);
|
||||
await this.provider.updateDevice(this.nativeId, this.name ?? name, interfaces, type);
|
||||
}
|
||||
|
||||
startBatteryCheckInterval() {
|
||||
if (this.batteryTimeout) {
|
||||
clearInterval(this.batteryTimeout);
|
||||
}
|
||||
|
||||
this.batteryTimeout = setInterval(async () => {
|
||||
const api = this.getClientWithToken();
|
||||
|
||||
try {
|
||||
const { batteryPercent, sleep } = await api.getBatteryInfo();
|
||||
this.batteryLevel = batteryPercent;
|
||||
this.online = !sleep;
|
||||
}
|
||||
catch (e) {
|
||||
this.console.log('Error in getting battery info', e);
|
||||
}
|
||||
}, 1000 * 60 * 30);
|
||||
}
|
||||
|
||||
async reboot() {
|
||||
@@ -341,6 +378,12 @@ class ReolinkCamera extends RtspSmartCamera implements Camera, DeviceProvider, R
|
||||
return this.client;
|
||||
}
|
||||
|
||||
getClientWithToken() {
|
||||
if (!this.clientWithToken)
|
||||
this.clientWithToken = new ReolinkCameraClient(this.getHttpAddress(), this.getUsername(), this.getPassword(), this.getRtspChannel(), this.console, true);
|
||||
return this.clientWithToken;
|
||||
}
|
||||
|
||||
async getOnvifClient() {
|
||||
if (!this.onvifClient)
|
||||
this.onvifClient = await this.createOnvifClient();
|
||||
@@ -612,7 +655,7 @@ class ReolinkCamera extends RtspSmartCamera implements Camera, DeviceProvider, R
|
||||
// 1: support main/extern/sub stream
|
||||
// 2: support main/sub stream
|
||||
|
||||
const live = this.storageSettings.values.abilities?.value?.Ability?.abilityChn?.[0].live?.ver;
|
||||
const live = this.storageSettings.values.abilities?.value?.Ability?.abilityChn?.[this.getRtspChannel()].live?.ver;
|
||||
const [rtmpMain, rtmpExt, rtmpSub, rtspMain, rtspSub] = streams;
|
||||
streams.splice(0, streams.length);
|
||||
|
||||
@@ -621,7 +664,7 @@ class ReolinkCamera extends RtspSmartCamera implements Camera, DeviceProvider, R
|
||||
// 1: main stream enc type is H265
|
||||
|
||||
// anecdotally, encoders of type h265 do not have a working RTMP main stream.
|
||||
const mainEncType = this.storageSettings.values.abilities?.value?.Ability?.abilityChn?.[0].mainEncType?.ver;
|
||||
const mainEncType = this.storageSettings.values.abilities?.value?.Ability?.abilityChn?.[this.getRtspChannel()].mainEncType?.ver;
|
||||
|
||||
if (live === 2) {
|
||||
if (mainEncType === 1) {
|
||||
@@ -790,6 +833,7 @@ class ReolinkProvider extends RtspProvider {
|
||||
const rtspChannel = parseInt(settings.rtspChannel?.toString()) || 0;
|
||||
if (!skipValidate) {
|
||||
const api = new ReolinkCameraClient(httpAddress, username, password, rtspChannel, this.console);
|
||||
const apiWithToken = new ReolinkCameraClient(httpAddress, username, password, rtspChannel, this.console, true);
|
||||
try {
|
||||
await api.jpegSnapshot();
|
||||
}
|
||||
@@ -803,7 +847,11 @@ class ReolinkProvider extends RtspProvider {
|
||||
doorbell = deviceInfo.type === 'BELL';
|
||||
name = deviceInfo.name ?? 'Reolink Camera';
|
||||
ai = await api.getAiState();
|
||||
abilities = await api.getAbility();
|
||||
try {
|
||||
abilities = await api.getAbility();
|
||||
} catch (e) {
|
||||
abilities = await apiWithToken.getAbility();
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
this.console.error('Reolink camera does not support AI events', e);
|
||||
|
||||
@@ -46,25 +46,27 @@ async function getDeviceInfo(host: string, username: string, password: string):
|
||||
return response.body?.[0]?.value?.DevInfo;
|
||||
}
|
||||
|
||||
export async function getLoginParameters(host: string, username: string, password: string) {
|
||||
try {
|
||||
await getDeviceInfo(host, username, password);
|
||||
return {
|
||||
parameters: {
|
||||
user: username,
|
||||
password,
|
||||
},
|
||||
leaseTimeSeconds: Infinity,
|
||||
export async function getLoginParameters(host: string, username: string, password: string, forceToken?: boolean) {
|
||||
if (!forceToken) {
|
||||
try {
|
||||
await getDeviceInfo(host, username, password);
|
||||
return {
|
||||
parameters: {
|
||||
user: username,
|
||||
password,
|
||||
},
|
||||
leaseTimeSeconds: Infinity,
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(`http://${host}/api.cgi`);
|
||||
const params = url.searchParams;
|
||||
params.set('cmd', 'Login');
|
||||
|
||||
|
||||
const response = await httpFetch({
|
||||
url,
|
||||
method: 'POST',
|
||||
@@ -83,7 +85,7 @@ export async function getLoginParameters(host: string, username: string, passwor
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
const token = response.body?.[0]?.value?.Token?.name || response.body?.value?.Token?.name;
|
||||
if (!token)
|
||||
throw new Error('unable to login');
|
||||
|
||||
@@ -49,7 +49,7 @@ export class ReolinkCameraClient {
|
||||
parameters: Record<string, string>;
|
||||
tokenLease: number;
|
||||
|
||||
constructor(public host: string, public username: string, public password: string, public channelId: number, public console: Console) {
|
||||
constructor(public host: string, public username: string, public password: string, public channelId: number, public console: Console, public readonly forceToken?: boolean) {
|
||||
this.credential = {
|
||||
username,
|
||||
password,
|
||||
@@ -80,7 +80,7 @@ export class ReolinkCameraClient {
|
||||
|
||||
this.console.log(`token expired at ${this.tokenLease}, renewing...`);
|
||||
|
||||
const { parameters, leaseTimeSeconds } = await getLoginParameters(this.host, this.username, this.password);
|
||||
const { parameters, leaseTimeSeconds } = await getLoginParameters(this.host, this.username, this.password, this.forceToken);
|
||||
this.parameters = parameters
|
||||
this.tokenLease = Date.now() + 1000 * leaseTimeSeconds;
|
||||
}
|
||||
@@ -153,15 +153,37 @@ export class ReolinkCameraClient {
|
||||
const params = url.searchParams;
|
||||
params.set('cmd', 'GetAbility');
|
||||
params.set('channel', this.channelId.toString());
|
||||
const response = await this.requestWithLogin({
|
||||
let response = await this.requestWithLogin({
|
||||
url,
|
||||
responseType: 'json',
|
||||
});
|
||||
const error = response.body?.[0]?.error;
|
||||
let error = response.body?.[0]?.error;
|
||||
if (error) {
|
||||
this.console.error('error during call to getAbility', error);
|
||||
throw new Error('error during call to getAbility');
|
||||
this.console.error('error during call to getAbility GET, Trying with POST', error);
|
||||
|
||||
url.search = '';
|
||||
|
||||
const body = [
|
||||
{
|
||||
cmd: "GetAbility",
|
||||
action: 0,
|
||||
param: { User: { userName: this.username } }
|
||||
}
|
||||
];
|
||||
|
||||
response = await this.requestWithLogin({
|
||||
url,
|
||||
responseType: 'json',
|
||||
method: 'POST',
|
||||
}, this.createReadable(body));
|
||||
|
||||
error = response.body?.[0]?.error;
|
||||
if (error) {
|
||||
this.console.error('error during call to getAbility GET, Trying with POST', error);
|
||||
throw new Error('error during call to getAbility');
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
value: response.body?.[0]?.value || response.body?.value,
|
||||
data: response.body,
|
||||
@@ -210,7 +232,46 @@ export class ReolinkCameraClient {
|
||||
this.console.error('error during call to getDeviceInfo', error);
|
||||
throw new Error('error during call to getDeviceInfo');
|
||||
}
|
||||
return response.body?.[0]?.value?.DevInfo;
|
||||
|
||||
const deviceInfo: DevInfo = await response.body?.[0]?.value?.DevInfo;
|
||||
|
||||
// Will need to check if it's valid for NVR and NVR_WIFI
|
||||
if (!['HOMEHUB', 'NVR', 'NVR_WIFI'].includes(deviceInfo.exactType)) {
|
||||
return deviceInfo;
|
||||
}
|
||||
|
||||
// If the device is listed as homehub, fetch the channel specific information
|
||||
url.search = '';
|
||||
const body = [
|
||||
{ cmd: "GetChnTypeInfo", action: 0, param: { channel: this.channelId } },
|
||||
{ cmd: "GetChannelstatus", action: 0, param: {} },
|
||||
]
|
||||
|
||||
const additionalInfoResponse = await this.requestWithLogin({
|
||||
url,
|
||||
method: 'POST',
|
||||
responseType: 'json'
|
||||
}, this.createReadable(body));
|
||||
|
||||
const chnTypeInfo = additionalInfoResponse?.body?.find(elem => elem.cmd === 'GetChnTypeInfo');
|
||||
const chnStatus = additionalInfoResponse?.body?.find(elem => elem.cmd === 'GetChannelstatus');
|
||||
|
||||
if (chnTypeInfo?.value) {
|
||||
deviceInfo.firmVer = chnTypeInfo.value.firmVer;
|
||||
deviceInfo.model = chnTypeInfo.value.typeInfo;
|
||||
deviceInfo.pakSuffix = chnTypeInfo.value.pakSuffix;
|
||||
}
|
||||
|
||||
if (chnStatus?.value) {
|
||||
const specificChannelStatus = chnStatus.value?.status?.find(elem => elem.channel === this.channelId);
|
||||
|
||||
if (specificChannelStatus) {
|
||||
deviceInfo.name = specificChannelStatus.name;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return deviceInfo;
|
||||
}
|
||||
|
||||
async getPtzPresets(): Promise<PtzPreset[]> {
|
||||
@@ -369,4 +430,39 @@ export class ReolinkCameraClient {
|
||||
data: response.body,
|
||||
};
|
||||
}
|
||||
|
||||
async getBatteryInfo() {
|
||||
const url = new URL(`http://${this.host}/api.cgi`);
|
||||
|
||||
const body = [
|
||||
{
|
||||
cmd: "GetBatteryInfo",
|
||||
action: 0,
|
||||
param: { channel: this.channelId }
|
||||
},
|
||||
{
|
||||
cmd: "GetChannelstatus",
|
||||
}
|
||||
];
|
||||
|
||||
const response = await this.requestWithLogin({
|
||||
url,
|
||||
responseType: 'json',
|
||||
method: 'POST',
|
||||
}, this.createReadable(body));
|
||||
|
||||
const error = response.body?.find(elem => elem.error)?.error;
|
||||
if (error) {
|
||||
this.console.error('error during call to getBatteryInfo', error);
|
||||
}
|
||||
|
||||
const batteryInfoEntry = response.body.find(entry => entry.cmd === 'GetBatteryInfo')?.value?.Battery;
|
||||
const channelStatusEntry = response.body.find(entry => entry.cmd === 'GetChannelstatus')?.value?.status
|
||||
?.find(chStatus => chStatus.channel === this.channelId)
|
||||
|
||||
return {
|
||||
batteryPercent: batteryInfoEntry?.batteryPercent,
|
||||
sleep: channelStatusEntry?.sleep === 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
2
plugins/ring/.vscode/settings.json
vendored
2
plugins/ring/.vscode/settings.json
vendored
@@ -1,4 +1,4 @@
|
||||
|
||||
{
|
||||
"scrypted.debugHost": "127.0.0.1",
|
||||
"scrypted.debugHost": "scrypted-nvr",
|
||||
}
|
||||
214
sdk/README.md
214
sdk/README.md
@@ -1,213 +1 @@
|
||||
# Table of Contents
|
||||
* [Getting Started](#getting-started)
|
||||
* [Typescript Sample Setup](#typescript-sample-setup)
|
||||
* [Creating a Switch](#creating-a-switch)
|
||||
* [Core Concepts](#core-concepts)
|
||||
* [Interfaces](#interfaces)
|
||||
* [Events](#events)
|
||||
* [Creating Multiple Devices](#creating-multiple-devices)
|
||||
* [Full Reference](/modules)
|
||||
* [Sample Plugins](https://github.com/koush/scrypted/tree/main/plugins)
|
||||
* [Camera Provider Sample](https://github.com/koush/scrypted-sample-cameraprovider)
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
# Getting Started
|
||||
|
||||
The quickest way to get started is to check out the the [Typescript sample](https://github.com/koush/scrypted-vscode-typescript) and open it in Visual Studio Code. The setup instructions can be found in the readme for the [project](https://github.com/koush/scrypted-vscode-typescript).
|
||||
|
||||
<br/>
|
||||
|
||||
## Typescript Sample Setup
|
||||
|
||||
These instructions can be followed on your preferred development machine, and do not need to be run on the Scrypted Server itself. The Scrypted SDK can deploy and **debug** plugins running on a remote server. For example, the VS Code development environment can be running on a Mac, while the server is running on a Raspberry Pi.
|
||||
|
||||
1. npm install
|
||||
2. Open this plugin director yin VS Code.
|
||||
3. Edit `.vscode/settings.json` to point to the IP address of your Scrypted server. The default is `127.0.0.1`, your local machine.
|
||||
4. Press Launch (green arrow button in the Run and Debug sidebar) to start debugging.
|
||||
* The VS Code `Terminal` area may show an authentication failure and prompt you to log in to the Scrypted Management Console with `npx scrypted login`. You will only need to do this once. You can then relaunch afterwards.
|
||||
|
||||
<p align="center">
|
||||
<img width="538" alt="image" src="https://user-images.githubusercontent.com/73924/151676616-c730eb56-26dd-466d-b7f5-25783300b3bc.png">
|
||||
</p>
|
||||
<br/>
|
||||
|
||||
## Creating a Switch
|
||||
|
||||
The aforementioned sample will create a single switch device.
|
||||
|
||||
```typescript
|
||||
import axios from 'axios';
|
||||
import { OnOff, ScryptedDeviceBase } from '@scrypted/sdk';
|
||||
|
||||
console.log('Hello World. This will create a virtual OnOff device.');
|
||||
// OnOff is a simple binary switch. See "interfaces" in package.json
|
||||
// to add support for more capabilities, like Brightness or Lock.
|
||||
|
||||
class TypescriptLight extends ScryptedDeviceBase implements OnOff {
|
||||
constructor() {
|
||||
super();
|
||||
this.on = this.on || false;
|
||||
}
|
||||
async turnOff() {
|
||||
this.console.log('turnOff was called!');
|
||||
this.on = false;
|
||||
}
|
||||
async turnOn() {
|
||||
// set a breakpoint here.
|
||||
this.console.log('turnOn was called!');
|
||||
|
||||
this.console.log("Let's pretend to perform a web request on an API that would turn on a light.");
|
||||
const ip = await axios.get('http://jsonip.com');
|
||||
this.console.log(`my ip: ${ip.data.ip}`);
|
||||
|
||||
this.on = true;
|
||||
}
|
||||
}
|
||||
|
||||
export default TypescriptLight;
|
||||
```
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
# Core Concepts
|
||||
|
||||
Devices the core entry points and objects within Scrypted. A device can be a physical device, a virtual device, a provider of other devices (like a hub), a webhook, etc. Devices have two primary properties: Interfaces and Events.
|
||||
<br/>
|
||||
|
||||
## Interfaces
|
||||
|
||||
Interfaces are how devices expose their capabilities to Scrypted. An OnOff interface represents a binary switch. The Brightness interface represents a light that can be dimmed. The ColorSettingRgb interface indicates the light can change color. A device may expose multiple different interfaces to describe its functionality.
|
||||
|
||||
For example, given the following devices, the interfaces they would implement:
|
||||
|
||||
Outlet: OnOff,
|
||||
Dimmer Switch: OnOff, Brightness,
|
||||
Color Bulb: OnOff, Brightness, ColorSettingRgb
|
||||
Interfaces aren't only used represent characteristics of physical devices. As mentioned, they provide ways to hook into Scrypted. The HttpRequestHandler lets you add a web hook to handle incoming web requests. EventListener lets you create handlers that respond to events. DeviceProvider acts as a controller platform (like Hue or Lifx) for exposing multiple other devices to Scrypted.
|
||||
|
||||
Interfaces also provide a way to query the device state. Such as checking whether an outlet is on or off, the current brightness level, or the current color.
|
||||
|
||||
```typescript
|
||||
// Interfaces describe how the current state of a device, and can be used to modify that state.
|
||||
if (light.on) {
|
||||
light.turnOff();
|
||||
}
|
||||
else {
|
||||
light.turnOn();
|
||||
}
|
||||
```
|
||||
<br/>
|
||||
|
||||
## Events
|
||||
|
||||
Scrypted maintains the state of all connected devices. Whenever the state of an interface is updated on a device, an Event will be triggered for that particular interface.
|
||||
|
||||
For example, when a light turns on, the Light device would send an OnOff event. If a Slack message is received, the Slack device would send a MessagingEndpoint event. Setting a schedule for sunrise on weekdays would send an Alarm event on that schedule.
|
||||
|
||||
Automations subscribe to these events in your smart home setup and react accordingly.
|
||||
|
||||
```
|
||||
// Events are triggered by the device on update, and can be observed.
|
||||
light.listen('OnOff', (eventSource: ScryptedDevice, eventDetails: EventDetails, eventData: object) => {
|
||||
if (eventData) {
|
||||
log.i('The light was turned on.');
|
||||
}
|
||||
else {
|
||||
log.i('The light was turned off.');
|
||||
}
|
||||
});
|
||||
```
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
# Creating Multiple Devices
|
||||
|
||||
Most plugins will want to create multiple devices. This is done by implementing the DeviceProvider interface.
|
||||
|
||||
To do this, thep project `package.json` needs to update the `scrypted` section that describes the plugin:
|
||||
|
||||
```json
|
||||
"scrypted": {
|
||||
"name": "TypeScript Light Provider",
|
||||
"type": "DeviceProvider",
|
||||
"interfaces": [
|
||||
"DeviceProvider"
|
||||
]
|
||||
},
|
||||
```
|
||||
|
||||
Then, the code is updated to support multiple lights:
|
||||
|
||||
```typescript
|
||||
import axios from 'axios';
|
||||
import sdk, { DeviceProvider, OnOff, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface } from '@scrypted/sdk';
|
||||
|
||||
class TypescriptLight extends ScryptedDeviceBase implements OnOff {
|
||||
constructor(nativeId?: string) {
|
||||
super(nativeId);
|
||||
this.on = this.on || false;
|
||||
}
|
||||
async turnOff() {
|
||||
this.console.log('turnOff was called!');
|
||||
this.on = false;
|
||||
}
|
||||
async turnOn() {
|
||||
// set a breakpoint here.
|
||||
this.console.log('turnOn was called!');
|
||||
|
||||
this.console.log("Let's pretend to perform a web request on an API that would turn on a light.");
|
||||
const ip = await axios.get('http://jsonip.com');
|
||||
this.console.log(`my ip: ${ip.data.ip}`);
|
||||
|
||||
this.on = true;
|
||||
}
|
||||
}
|
||||
|
||||
class MyDeviceProvider extends ScryptedDeviceBase implements DeviceProvider {
|
||||
constructor(nativeId?: string) {
|
||||
super(nativeId);
|
||||
|
||||
this.prepareDevices();
|
||||
}
|
||||
|
||||
async prepareDevices() {
|
||||
// "Discover" the lights provided by this provider to Scrypted.
|
||||
await sdk.deviceManager.onDevicesChanged({
|
||||
devices: [
|
||||
{
|
||||
// the native id is the unique identifier for this light within
|
||||
// your plugin.
|
||||
nativeId: 'light1',
|
||||
name: 'Light 1',
|
||||
type: ScryptedDeviceType.Light,
|
||||
interfaces: [
|
||||
ScryptedInterface.OnOff,
|
||||
]
|
||||
},
|
||||
{
|
||||
nativeId: 'light2',
|
||||
name: 'Light 1',
|
||||
type: ScryptedDeviceType.Light,
|
||||
interfaces: [
|
||||
ScryptedInterface.OnOff,
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
// After the lights are discovered, Scrypted will request the plugin create the
|
||||
// instance that can be used to control and query the light.
|
||||
getDevice(nativeId: string) {
|
||||
return new TypescriptLight(nativeId);
|
||||
}
|
||||
}
|
||||
|
||||
// Export the provider from the plugin, rather than the individual light.
|
||||
export default MyDeviceProvider;
|
||||
```
|
||||
|
||||
Running the sample will then create 3 devices: the plugin/hub and the 2 lights it controls.
|
||||
[Scrypted SDK](https://developer.scrypted.app)
|
||||
Submodule sdk/developer.scrypted.app deleted from 285ba01d8d
1915
sdk/package-lock.json
generated
1915
sdk/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.3.67",
|
||||
"version": "0.3.68",
|
||||
"description": "",
|
||||
"main": "dist/src/index.js",
|
||||
"exports": {
|
||||
@@ -12,8 +12,6 @@
|
||||
"scripts": {
|
||||
"prepublishOnly": "npm run build && cd types && npm version patch && npm publish",
|
||||
"prebuild": "cd types && npm run build",
|
||||
"predocs": "npm run build",
|
||||
"docs": "typedoc && cp developer.scrypted.app/CNAME developer.scrypted.app/docs",
|
||||
"build": "rimraf dist && tsc",
|
||||
"webpack": "webpack-cli --config webpack.config.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
@@ -30,10 +28,10 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.24.7",
|
||||
"adm-zip": "^0.5.14",
|
||||
"axios": "^1.7.3",
|
||||
"babel-loader": "^9.1.3",
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"adm-zip": "^0.5.16",
|
||||
"axios": "^1.7.7",
|
||||
"babel-loader": "^9.2.1",
|
||||
"babel-plugin-const-enum": "^1.2.0",
|
||||
"ncp": "^2.0.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
@@ -41,15 +39,15 @@
|
||||
"tmp": "^0.2.3",
|
||||
"ts-loader": "^9.5.1",
|
||||
"typescript": "^5.5.4",
|
||||
"webpack": "^5.93.0",
|
||||
"webpack": "^5.95.0",
|
||||
"webpack-bundle-analyzer": "^4.10.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.1.0",
|
||||
"@types/node": "^22.8.1",
|
||||
"@types/stringify-object": "^4.0.5",
|
||||
"stringify-object": "^3.3.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typedoc": "^0.26.5"
|
||||
"typedoc": "^0.26.10"
|
||||
},
|
||||
"types": "dist/src/index.d.ts"
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"entryPoints": [
|
||||
"./src"
|
||||
],
|
||||
"sort": ["source-order"],
|
||||
"name": "Scrypted Documentation",
|
||||
"tsconfig": "./tsconfig.json",
|
||||
"out": "./developer.scrypted.app/docs",
|
||||
"categorizeByGroup": false,
|
||||
"defaultCategory": "Device Interfaces Reference",
|
||||
"excludePrivate": true,
|
||||
"disableSources": true,
|
||||
"categoryOrder": [
|
||||
"Core Reference",
|
||||
"Device Provider Reference",
|
||||
"Media Reference",
|
||||
"Webhook and Push Reference",
|
||||
"Mixin Reference",
|
||||
"WebRTC Reference"
|
||||
],
|
||||
"customCss": "./developer.scrypted.app/docs.css",
|
||||
"readme": "./README.md"
|
||||
}
|
||||
4
sdk/types/package-lock.json
generated
4
sdk/types/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/types",
|
||||
"version": "0.3.62",
|
||||
"version": "0.3.63",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/types",
|
||||
"version": "0.3.62",
|
||||
"version": "0.3.63",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.1.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/types",
|
||||
"version": "0.3.62",
|
||||
"version": "0.3.63",
|
||||
"description": "",
|
||||
"main": "dist/index.js",
|
||||
"author": "",
|
||||
|
||||
@@ -71,8 +71,8 @@ class PanTiltZoomMovement(str, Enum):
|
||||
|
||||
class ScryptedDeviceType(str, Enum):
|
||||
|
||||
API = "API"
|
||||
AirPurifier = "AirPurifier"
|
||||
API = "API"
|
||||
Automation = "Automation"
|
||||
Builtin = "Builtin"
|
||||
Camera = "Camera"
|
||||
@@ -117,9 +117,9 @@ class ScryptedInterface(str, Enum):
|
||||
BinarySensor = "BinarySensor"
|
||||
Brightness = "Brightness"
|
||||
BufferConverter = "BufferConverter"
|
||||
CO2Sensor = "CO2Sensor"
|
||||
Camera = "Camera"
|
||||
Charger = "Charger"
|
||||
CO2Sensor = "CO2Sensor"
|
||||
ColorSettingHsv = "ColorSettingHsv"
|
||||
ColorSettingRgb = "ColorSettingRgb"
|
||||
ColorSettingTemperature = "ColorSettingTemperature"
|
||||
@@ -147,8 +147,8 @@ class ScryptedInterface(str, Enum):
|
||||
Microphone = "Microphone"
|
||||
MixinProvider = "MixinProvider"
|
||||
MotionSensor = "MotionSensor"
|
||||
NOXSensor = "NOXSensor"
|
||||
Notifier = "Notifier"
|
||||
NOXSensor = "NOXSensor"
|
||||
OauthClient = "OauthClient"
|
||||
ObjectDetection = "ObjectDetection"
|
||||
ObjectDetectionGenerator = "ObjectDetectionGenerator"
|
||||
@@ -156,22 +156,22 @@ class ScryptedInterface(str, Enum):
|
||||
ObjectDetector = "ObjectDetector"
|
||||
ObjectTracker = "ObjectTracker"
|
||||
OccupancySensor = "OccupancySensor"
|
||||
OnOff = "OnOff"
|
||||
Online = "Online"
|
||||
PM10Sensor = "PM10Sensor"
|
||||
PM25Sensor = "PM25Sensor"
|
||||
OnOff = "OnOff"
|
||||
PanTiltZoom = "PanTiltZoom"
|
||||
PasswordStore = "PasswordStore"
|
||||
Pause = "Pause"
|
||||
PM10Sensor = "PM10Sensor"
|
||||
PM25Sensor = "PM25Sensor"
|
||||
PositionSensor = "PositionSensor"
|
||||
PowerSensor = "PowerSensor"
|
||||
Program = "Program"
|
||||
PushHandler = "PushHandler"
|
||||
RTCSignalingChannel = "RTCSignalingChannel"
|
||||
RTCSignalingClient = "RTCSignalingClient"
|
||||
Readme = "Readme"
|
||||
Reboot = "Reboot"
|
||||
Refresh = "Refresh"
|
||||
RTCSignalingChannel = "RTCSignalingChannel"
|
||||
RTCSignalingClient = "RTCSignalingClient"
|
||||
Scene = "Scene"
|
||||
Scriptable = "Scriptable"
|
||||
ScryptedDevice = "ScryptedDevice"
|
||||
@@ -185,13 +185,12 @@ class ScryptedInterface(str, Enum):
|
||||
Settings = "Settings"
|
||||
StartStop = "StartStop"
|
||||
StreamService = "StreamService"
|
||||
TTY = "TTY"
|
||||
TTYSettings = "TTYSettings"
|
||||
TamperSensor = "TamperSensor"
|
||||
TemperatureSetting = "TemperatureSetting"
|
||||
Thermometer = "Thermometer"
|
||||
TTY = "TTY"
|
||||
TTYSettings = "TTYSettings"
|
||||
UltravioletSensor = "UltravioletSensor"
|
||||
VOCSensor = "VOCSensor"
|
||||
VideoCamera = "VideoCamera"
|
||||
VideoCameraConfiguration = "VideoCameraConfiguration"
|
||||
VideoCameraMask = "VideoCameraMask"
|
||||
@@ -199,6 +198,7 @@ class ScryptedInterface(str, Enum):
|
||||
VideoFrameGenerator = "VideoFrameGenerator"
|
||||
VideoRecorder = "VideoRecorder"
|
||||
VideoRecorderManagement = "VideoRecorderManagement"
|
||||
VOCSensor = "VOCSensor"
|
||||
|
||||
class ScryptedMimeTypes(str, Enum):
|
||||
|
||||
@@ -211,11 +211,11 @@ class ScryptedMimeTypes(str, Enum):
|
||||
MediaStreamFeedback = "x-scrypted/x-media-stream-feedback"
|
||||
MediaStreamUrl = "text/x-media-url"
|
||||
PushEndpoint = "text/x-push-endpoint"
|
||||
RequestMediaObject = "x-scrypted/x-scrypted-request-media-object"
|
||||
RequestMediaStream = "x-scrypted/x-scrypted-request-stream"
|
||||
RTCConnectionManagement = "x-scrypted/x-scrypted-rtc-connection-management"
|
||||
RTCSignalingChannel = "x-scrypted/x-scrypted-rtc-signaling-channel"
|
||||
RTCSignalingSession = "x-scrypted/x-scrypted-rtc-signaling-session"
|
||||
RequestMediaObject = "x-scrypted/x-scrypted-request-media-object"
|
||||
RequestMediaStream = "x-scrypted/x-scrypted-request-stream"
|
||||
SchemePrefix = "x-scrypted/x-scrypted-scheme-"
|
||||
ServerId = "text/x-server-id"
|
||||
Url = "text/x-uri"
|
||||
@@ -543,20 +543,6 @@ class EventListenerOptions(TypedDict):
|
||||
mixinId: str # The EventListener will listen to events and property changes from a device or mixin that is suppressed by a mixin.
|
||||
watch: bool # This EventListener will passively watch for events, and not initiate polling.
|
||||
|
||||
class FFmpegInput(TypedDict):
|
||||
|
||||
container: str
|
||||
destinationVideoBitrate: float
|
||||
env: Any # Environment variables to set when launching FFmpeg.
|
||||
ffmpegPath: str # Path to a custom FFmpeg binary.
|
||||
h264EncoderArguments: list[str]
|
||||
h264FilterArguments: list[str]
|
||||
inputArguments: list[str]
|
||||
mediaStreamOptions: ResponseMediaStreamOptions
|
||||
url: str # The media url for this FFmpegInput.
|
||||
urls: list[str] # Alternate media urls for this FFmpegInput.
|
||||
videoDecoderArguments: list[str]
|
||||
|
||||
class FanState(TypedDict):
|
||||
|
||||
counterClockwise: bool
|
||||
@@ -574,6 +560,20 @@ class FanStatus(TypedDict):
|
||||
speed: float # Rotations per minute, if available, otherwise 0 or 1.
|
||||
swing: bool
|
||||
|
||||
class FFmpegInput(TypedDict):
|
||||
|
||||
container: str
|
||||
destinationVideoBitrate: float
|
||||
env: Any # Environment variables to set when launching FFmpeg.
|
||||
ffmpegPath: str # Path to a custom FFmpeg binary.
|
||||
h264EncoderArguments: list[str]
|
||||
h264FilterArguments: list[str]
|
||||
inputArguments: list[str]
|
||||
mediaStreamOptions: ResponseMediaStreamOptions
|
||||
url: str # The media url for this FFmpegInput.
|
||||
urls: list[str] # Alternate media urls for this FFmpegInput.
|
||||
videoDecoderArguments: list[str]
|
||||
|
||||
class HttpRequest(TypedDict):
|
||||
|
||||
aclId: str
|
||||
@@ -643,9 +643,13 @@ class MediaStreamOptions(TypedDict):
|
||||
tool: MediaStreamTool # The tool was used to write the container or will be used to read teh container. Ie, scrypted, the ffmpeg tools, gstreamer.
|
||||
video: VideoStreamOptions
|
||||
|
||||
class AndroidNotificationOptions(TypedDict):
|
||||
channel: str
|
||||
|
||||
class NotifierOptions(TypedDict):
|
||||
|
||||
actions: list[NotificationAction]
|
||||
android: AndroidNotificationOptions
|
||||
badge: str
|
||||
body: str
|
||||
bodyWithSubtitle: str
|
||||
@@ -982,10 +986,6 @@ class BufferConverter:
|
||||
pass
|
||||
|
||||
|
||||
class CO2Sensor:
|
||||
|
||||
co2ppm: float
|
||||
|
||||
class Camera:
|
||||
"""Camera devices can take still photos."""
|
||||
|
||||
@@ -1001,6 +1001,10 @@ class Charger:
|
||||
|
||||
chargeState: ChargeState
|
||||
|
||||
class CO2Sensor:
|
||||
|
||||
co2ppm: float
|
||||
|
||||
class ColorSettingHsv:
|
||||
"""ColorSettingHsv sets the color of a colored light using the HSV representation."""
|
||||
|
||||
@@ -1218,10 +1222,6 @@ class MotionSensor:
|
||||
|
||||
motionDetected: bool
|
||||
|
||||
class NOXSensor:
|
||||
|
||||
noxDensity: float
|
||||
|
||||
class Notifier:
|
||||
"""Notifier can be any endpoint that can receive messages, such as speakers, phone numbers, messaging clients, etc. The messages may optionally contain media."""
|
||||
|
||||
@@ -1229,6 +1229,10 @@ class Notifier:
|
||||
pass
|
||||
|
||||
|
||||
class NOXSensor:
|
||||
|
||||
noxDensity: float
|
||||
|
||||
class OauthClient:
|
||||
"""The OauthClient can be implemented to perform the browser based Oauth process from within a plugin."""
|
||||
|
||||
@@ -1283,6 +1287,11 @@ class OccupancySensor:
|
||||
|
||||
occupied: bool
|
||||
|
||||
class Online:
|
||||
"""Online denotes whether the device is online or unresponsive. It may be unresponsive due to being unplugged, network error, etc."""
|
||||
|
||||
online: bool
|
||||
|
||||
class OnOff:
|
||||
"""OnOff is a basic binary switch."""
|
||||
|
||||
@@ -1294,19 +1303,6 @@ class OnOff:
|
||||
pass
|
||||
|
||||
|
||||
class Online:
|
||||
"""Online denotes whether the device is online or unresponsive. It may be unresponsive due to being unplugged, network error, etc."""
|
||||
|
||||
online: bool
|
||||
|
||||
class PM10Sensor:
|
||||
|
||||
pm10Density: float
|
||||
|
||||
class PM25Sensor:
|
||||
|
||||
pm25Density: float
|
||||
|
||||
class PanTiltZoom:
|
||||
|
||||
ptzCapabilities: PanTiltZoomCapabilities
|
||||
@@ -1337,6 +1333,14 @@ class Pause:
|
||||
pass
|
||||
|
||||
|
||||
class PM10Sensor:
|
||||
|
||||
pm10Density: float
|
||||
|
||||
class PM25Sensor:
|
||||
|
||||
pm25Density: float
|
||||
|
||||
class PositionSensor:
|
||||
|
||||
position: Position
|
||||
@@ -1510,19 +1514,6 @@ class StreamService:
|
||||
pass
|
||||
|
||||
|
||||
class TTY:
|
||||
"""TTY connection offered by a remote device that can be connected to by an interactive terminal interface. Implementors should also implement StreamService to handle the actual data transfer."""
|
||||
|
||||
|
||||
pass
|
||||
|
||||
class TTYSettings:
|
||||
"""TTYSettings allows TTY backends to query plugins for modifications to the (non-)interactive terminal environment."""
|
||||
|
||||
async def getTTYSettings(self) -> Any:
|
||||
pass
|
||||
|
||||
|
||||
class TamperSensor:
|
||||
|
||||
tampered: TamperState
|
||||
@@ -1543,14 +1534,23 @@ class Thermometer:
|
||||
pass
|
||||
|
||||
|
||||
class TTY:
|
||||
"""TTY connection offered by a remote device that can be connected to by an interactive terminal interface. Implementors should also implement StreamService to handle the actual data transfer."""
|
||||
|
||||
|
||||
pass
|
||||
|
||||
class TTYSettings:
|
||||
"""TTYSettings allows TTY backends to query plugins for modifications to the (non-)interactive terminal environment."""
|
||||
|
||||
async def getTTYSettings(self) -> Any:
|
||||
pass
|
||||
|
||||
|
||||
class UltravioletSensor:
|
||||
|
||||
ultraviolet: float
|
||||
|
||||
class VOCSensor:
|
||||
|
||||
vocDensity: float
|
||||
|
||||
class VideoCamera:
|
||||
"""VideoCamera devices can capture video streams."""
|
||||
|
||||
@@ -1581,10 +1581,10 @@ class VideoClips:
|
||||
async def getVideoClip(self, videoId: str) -> MediaObject:
|
||||
pass
|
||||
|
||||
async def getVideoClipThumbnail(self, thumbnailId: str, options: VideoClipThumbnailOptions = None) -> MediaObject:
|
||||
async def getVideoClips(self, options: VideoClipOptions = None) -> list[VideoClip]:
|
||||
pass
|
||||
|
||||
async def getVideoClips(self, options: VideoClipOptions = None) -> list[VideoClip]:
|
||||
async def getVideoClipThumbnail(self, thumbnailId: str, options: VideoClipThumbnailOptions = None) -> MediaObject:
|
||||
pass
|
||||
|
||||
async def removeVideoClips(self, videoClipIds: list[str]) -> None:
|
||||
@@ -1622,6 +1622,10 @@ class VideoRecorderManagement:
|
||||
pass
|
||||
|
||||
|
||||
class VOCSensor:
|
||||
|
||||
vocDensity: float
|
||||
|
||||
class Logger:
|
||||
"""Logger is exposed via log.* to allow writing to the Scrypted log."""
|
||||
|
||||
@@ -1911,8 +1915,8 @@ class ScryptedInterfaceMethods(str, Enum):
|
||||
ptzCommand = "ptzCommand"
|
||||
getRecordedEvents = "getRecordedEvents"
|
||||
getVideoClip = "getVideoClip"
|
||||
getVideoClipThumbnail = "getVideoClipThumbnail"
|
||||
getVideoClips = "getVideoClips"
|
||||
getVideoClipThumbnail = "getVideoClipThumbnail"
|
||||
removeVideoClips = "removeVideoClips"
|
||||
setVideoStreamOptions = "setVideoStreamOptions"
|
||||
startIntercom = "startIntercom"
|
||||
@@ -2721,8 +2725,8 @@ ScryptedInterfaceDescriptors = {
|
||||
"name": "VideoClips",
|
||||
"methods": [
|
||||
"getVideoClip",
|
||||
"getVideoClipThumbnail",
|
||||
"getVideoClips",
|
||||
"getVideoClipThumbnail",
|
||||
"removeVideoClips"
|
||||
],
|
||||
"properties": []
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import stringifyObject from 'stringify-object';
|
||||
import { ScryptedInterface, ScryptedInterfaceDescriptor } from "./types.input";
|
||||
import path from 'path';
|
||||
import fs from "fs";
|
||||
import { DeclarationReflection, ProjectReflection, ReflectionKind, SomeType } from 'typedoc';
|
||||
import path from 'path';
|
||||
import stringifyObject from 'stringify-object';
|
||||
import { DeclarationReflection, ProjectReflection, ReflectionKind } from 'typedoc';
|
||||
import { ScryptedInterface, ScryptedInterfaceDescriptor } from "./types.input";
|
||||
|
||||
const schema = JSON.parse(fs.readFileSync(path.join(__dirname, '../gen/schema.json')).toString()) as ProjectReflection;
|
||||
const packageJson = require('../package.json');
|
||||
|
||||
@@ -225,6 +225,9 @@ export interface NotifierOptions {
|
||||
badge?: string;
|
||||
bodyWithSubtitle?: string;
|
||||
body?: string;
|
||||
android?: {
|
||||
channel?: string;
|
||||
}
|
||||
data?: any;
|
||||
dir?: NotificationDirection;
|
||||
lang?: string;
|
||||
@@ -1991,7 +1994,6 @@ export interface EndpointManager {
|
||||
|
||||
/**
|
||||
* Get an URL that can be externally accessed by anyone with the link. Plugin implementation is responsible for authentication.
|
||||
* @deprecated
|
||||
*/
|
||||
getCloudEndpoint(nativeId?: ScryptedNativeId, options?: {
|
||||
/**
|
||||
|
||||
38
server/package-lock.json
generated
38
server/package-lock.json
generated
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"name": "@scrypted/server",
|
||||
"version": "0.119.3",
|
||||
"version": "0.121.5",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/server",
|
||||
"version": "0.119.3",
|
||||
"version": "0.121.5",
|
||||
"hasInstallScript": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/ffmpeg-static": "^6.1.0-build1",
|
||||
"@scrypted/node-pty": "^1.0.18",
|
||||
"@scrypted/types": "^0.3.62",
|
||||
"@scrypted/types": "^0.3.63",
|
||||
"adm-zip": "^0.5.16",
|
||||
"body-parser": "^1.20.3",
|
||||
"cookie-parser": "^1.4.7",
|
||||
@@ -40,13 +40,13 @@
|
||||
"scrypted-serve": "bin/scrypted-serve"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/adm-zip": "^0.5.5",
|
||||
"@types/adm-zip": "^0.5.6",
|
||||
"@types/cookie-parser": "^1.4.7",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/follow-redirects": "^1.14.4",
|
||||
"@types/http-auth": "^4.1.4",
|
||||
"@types/lodash": "^4.17.10",
|
||||
"@types/node": "^22.7.6",
|
||||
"@types/lodash": "^4.17.12",
|
||||
"@types/node": "^22.8.4",
|
||||
"@types/node-dijkstra": "^2.5.6",
|
||||
"@types/node-forge": "^1.3.11",
|
||||
"@types/semver": "^7.5.8",
|
||||
@@ -557,14 +557,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@scrypted/types": {
|
||||
"version": "0.3.62",
|
||||
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.3.62.tgz",
|
||||
"integrity": "sha512-tJHpCS8B0K7h33plkbajOAHUfOpqrEJOwYAojdOTc/DGf6+xKOUMKXl+wnEHmtjtUmlSNjeBQJxJ8ua813gS6Q=="
|
||||
"version": "0.3.63",
|
||||
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.3.63.tgz",
|
||||
"integrity": "sha512-9oYF/tsSXl5v9uOjuoRZvzn6drpMSw+5sspOlBM6OfT91HewCHR2IgQV/h7lSwQK1xzXkdbuyJI1Rsp0Z/cl2A=="
|
||||
},
|
||||
"node_modules/@types/adm-zip": {
|
||||
"version": "0.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/adm-zip/-/adm-zip-0.5.5.tgz",
|
||||
"integrity": "sha512-YCGstVMjc4LTY5uK9/obvxBya93axZOVOyf2GSUulADzmLhYE45u2nAssCs/fWBs1Ifq5Vat75JTPwd5XZoPJw==",
|
||||
"version": "0.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/adm-zip/-/adm-zip-0.5.6.tgz",
|
||||
"integrity": "sha512-lRlcSLg5Yoo7C2H2AUiAoYlvifWoCx/se7iUNiCBTfEVVYFVn+Tr9ZGed4K73tYgLe9O4PjdJvbxlkdAOx/qiw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
@@ -654,9 +654,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/lodash": {
|
||||
"version": "4.17.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.10.tgz",
|
||||
"integrity": "sha512-YpS0zzoduEhuOWjAotS6A5AVCva7X4lVlYLF0FYHAY9sdraBfnatttHItlWeZdGhuEkf+OzMNg2ZYAx8t+52uQ==",
|
||||
"version": "4.17.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.12.tgz",
|
||||
"integrity": "sha512-sviUmCE8AYdaF/KIHLDJBQgeYzPBI0vf/17NaYehBJfYD1j6/L95Slh07NlyK2iNyBNaEkb3En2jRt+a8y3xZQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/mime": {
|
||||
@@ -666,11 +666,11 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.7.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.6.tgz",
|
||||
"integrity": "sha512-/d7Rnj0/ExXDMcioS78/kf1lMzYk4BZV8MZGTBKzTGZ6/406ukkbYlIsZmMPhcR5KlkunDHQLrtAVmSq7r+mSw==",
|
||||
"version": "22.8.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.4.tgz",
|
||||
"integrity": "sha512-SpNNxkftTJOPk0oN+y2bIqurEXHTA2AOZ3EJDDKeJ5VzkvvORSvmQXGQarcOzWV1ac7DCaPBEdMDxBsM+d8jWw==",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.19.2"
|
||||
"undici-types": "~6.19.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node-dijkstra": {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "@scrypted/server",
|
||||
"version": "0.119.4",
|
||||
"version": "0.121.5",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"@scrypted/ffmpeg-static": "^6.1.0-build1",
|
||||
"@scrypted/node-pty": "^1.0.18",
|
||||
"@scrypted/types": "^0.3.62",
|
||||
"@scrypted/types": "^0.3.63",
|
||||
"adm-zip": "^0.5.16",
|
||||
"body-parser": "^1.20.3",
|
||||
"cookie-parser": "^1.4.7",
|
||||
@@ -30,13 +30,13 @@
|
||||
"ws": "^8.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/adm-zip": "^0.5.5",
|
||||
"@types/adm-zip": "^0.5.6",
|
||||
"@types/cookie-parser": "^1.4.7",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/follow-redirects": "^1.14.4",
|
||||
"@types/http-auth": "^4.1.4",
|
||||
"@types/lodash": "^4.17.10",
|
||||
"@types/node": "^22.7.6",
|
||||
"@types/lodash": "^4.17.12",
|
||||
"@types/node": "^22.8.4",
|
||||
"@types/node-dijkstra": "^2.5.6",
|
||||
"@types/node-forge": "^1.3.11",
|
||||
"@types/semver": "^7.5.8",
|
||||
|
||||
@@ -13,11 +13,10 @@ class BufferTransfer implements RpcSerializer {
|
||||
if (!serializationContext)
|
||||
return this.bufferSerializer.serialize(value);
|
||||
|
||||
// node may used pooled buffers for Buffer.allocUnsafe, Buffer.from, and other calls.
|
||||
// these buffers will be smaller than Buffer.poolSize.
|
||||
// In these instances, do not transfer the buffer, as it may not be transferible.
|
||||
// create a copy so it can be safely transfered.
|
||||
if (value.buffer.byteLength <= Buffer.poolSize) {
|
||||
// allow transfer of the buffer only if it sets the __rpc_transferable property.
|
||||
// this is the only safe way to do this, since call sites may return the same buffer
|
||||
// multiple times (like an image/jpeg MediaObject).
|
||||
if ((value as any).__rpc_transferable !== true) {
|
||||
const ab = value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength);
|
||||
value = Buffer.from(ab);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user