mirror of
https://github.com/koush/scrypted.git
synced 2026-03-20 16:40:24 +00:00
readmes
This commit is contained in:
@@ -4,7 +4,7 @@ The Google Device Access Plugin allows you to import and control your Nest and G
|
||||
|
||||
## Setup
|
||||
|
||||
Fllow steps at the link below to create your personal Google Device Access developer account Google Cloud developer account:
|
||||
Follow steps at the link below to create your personal Google Device Access developer account Google Cloud developer account:
|
||||
|
||||
* Google Device Access Project aka GDA ($5)
|
||||
* Google Cloud Project aka GCP (might be within the free tier)
|
||||
|
||||
4
plugins/google-device-access/package-lock.json
generated
4
plugins/google-device-access/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/google-device-access",
|
||||
"version": "0.0.42",
|
||||
"version": "0.0.43",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/google-device-access",
|
||||
"version": "0.0.42",
|
||||
"version": "0.0.43",
|
||||
"dependencies": {
|
||||
"@googleapis/smartdevicemanagement": "^0.2.0",
|
||||
"axios": "^0.21.1",
|
||||
|
||||
@@ -43,5 +43,5 @@
|
||||
"@types/node": "^14.17.11",
|
||||
"@types/url-parse": "^1.4.3"
|
||||
},
|
||||
"version": "0.0.42"
|
||||
"version": "0.0.43"
|
||||
}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
# @scrypted/google-home
|
||||
# Google Home Plugin for Scrypted
|
||||
|
||||
## npm commands
|
||||
* npm run scrypted-webpack
|
||||
* npm run scrypted-deploy <ipaddress>
|
||||
* npm run scrypted-debug <ipaddress>
|
||||
The Google Home Plugin lets you control your Scrypted devices from Google Assistant.
|
||||
|
||||
## scrypted distribution via npm
|
||||
1. Ensure package.json is set up properly for publishing on npm.
|
||||
2. npm publish
|
||||
## Setup
|
||||
|
||||
## Visual Studio Code configuration
|
||||
1. Install the Scrypted Cloud plugin (@scrypted/cloud).
|
||||
2. Log in to Scrypted Cloud.
|
||||
3. Install this plugin.
|
||||
4. Open the Google Home app on your mobile device.
|
||||
5. Add Device.
|
||||
6. Click "Works with Google".
|
||||
7. Search for "Scrypted".
|
||||
8. Add Scrypted, and log in to Scrypted with the same account from the step 2.
|
||||
|
||||
* If using a remote server, edit [.vscode/settings.json](blob/master/.vscode/settings.json) to specify the IP Address of the Scrypted server.
|
||||
* Launch Scrypted Debugger from the launch menu.
|
||||
Your devices should now sync with Google Home.
|
||||
|
||||
4
plugins/google-home/package-lock.json
generated
4
plugins/google-home/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/google-home",
|
||||
"version": "0.0.30",
|
||||
"version": "0.0.31",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/google-home",
|
||||
"version": "0.0.30",
|
||||
"version": "0.0.31",
|
||||
"dependencies": {
|
||||
"@googleapis/homegraph": "^2.0.0",
|
||||
"@homebridge/ciao": "^1.1.3",
|
||||
|
||||
@@ -42,5 +42,5 @@
|
||||
"@types/lodash": "^4.14.168",
|
||||
"@types/url-parse": "^1.4.3"
|
||||
},
|
||||
"version": "0.0.30"
|
||||
"version": "0.0.31"
|
||||
}
|
||||
|
||||
4
plugins/gstreamer-camera/.gitignore
vendored
Normal file
4
plugins/gstreamer-camera/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
.DS_Store
|
||||
out/
|
||||
node_modules/
|
||||
dist/
|
||||
8
plugins/gstreamer-camera/.npmignore
Normal file
8
plugins/gstreamer-camera/.npmignore
Normal file
@@ -0,0 +1,8 @@
|
||||
.DS_Store
|
||||
out/
|
||||
node_modules/
|
||||
*.map
|
||||
fs
|
||||
src
|
||||
.vscode
|
||||
dist/*.js
|
||||
22
plugins/gstreamer-camera/.vscode/launch.json
vendored
Normal file
22
plugins/gstreamer-camera/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Scrypted Debugger",
|
||||
"address": "${config:scrypted.debugHost}",
|
||||
"port": 10081,
|
||||
"request": "attach",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"preLaunchTask": "scrypted: deploy+debug",
|
||||
"sourceMaps": true,
|
||||
"localRoot": "${workspaceFolder}/out",
|
||||
"remoteRoot": "/plugin/",
|
||||
"type": "pwa-node"
|
||||
}
|
||||
]
|
||||
}
|
||||
4
plugins/gstreamer-camera/.vscode/settings.json
vendored
Normal file
4
plugins/gstreamer-camera/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
|
||||
{
|
||||
"scrypted.debugHost": "192.168.2.119",
|
||||
}
|
||||
20
plugins/gstreamer-camera/.vscode/tasks.json
vendored
Normal file
20
plugins/gstreamer-camera/.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
// for the documentation about the tasks.json format
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "scrypted: deploy+debug",
|
||||
"type": "shell",
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "silent",
|
||||
"focus": false,
|
||||
"panel": "shared",
|
||||
"showReuseMessage": true,
|
||||
"clear": false
|
||||
},
|
||||
"command": "npm run scrypted-vscode-launch ${config:scrypted.debugHost}",
|
||||
},
|
||||
]
|
||||
}
|
||||
15
plugins/gstreamer-camera/README.md
Normal file
15
plugins/gstreamer-camera/README.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# RTSP Cameras and Streams Plugin
|
||||
|
||||
## npm commands
|
||||
* npm run scrypted-webpack
|
||||
* npm run scrypted-deploy <ipaddress>
|
||||
* npm run scrypted-debug <ipaddress>
|
||||
|
||||
## scrypted distribution via npm
|
||||
1. Ensure package.json is set up properly for publishing on npm.
|
||||
2. npm publish
|
||||
|
||||
## Visual Studio Code configuration
|
||||
|
||||
* If using a remote server, edit [.vscode/settings.json](blob/master/.vscode/settings.json) to specify the IP Address of the Scrypted server.
|
||||
* Launch Scrypted Debugger from the launch menu.
|
||||
264
plugins/gstreamer-camera/package-lock.json
generated
Normal file
264
plugins/gstreamer-camera/package-lock.json
generated
Normal file
@@ -0,0 +1,264 @@
|
||||
{
|
||||
"name": "@scrypted/rtsp",
|
||||
"version": "0.0.3",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/rtsp",
|
||||
"version": "0.0.3",
|
||||
"license": "Apache",
|
||||
"dependencies": {
|
||||
"@koush/axios-digest-auth": "^0.8.5",
|
||||
"axios": "^0.23.0",
|
||||
"url-parse": "^1.4.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"@types/node": "^16.9.6"
|
||||
}
|
||||
},
|
||||
"../../common": {
|
||||
"name": "@scrypted/common",
|
||||
"version": "1.0.1",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/sdk": "file:../sdk",
|
||||
"typescript": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.9.0"
|
||||
}
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.0.123",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/plugin-proposal-class-properties": "^7.14.5",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.14.5",
|
||||
"@babel/plugin-proposal-numeric-separator": "^7.14.5",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.14.5",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.15.4",
|
||||
"@babel/plugin-transform-typescript": "^7.15.8",
|
||||
"@babel/preset-typescript": "^7.15.0",
|
||||
"@types/node": "^16.11.1",
|
||||
"adm-zip": "^0.4.13",
|
||||
"axios": "^0.21.4",
|
||||
"babel-loader": "^8.2.3",
|
||||
"babel-plugin-const-enum": "^1.1.0",
|
||||
"esbuild": "^0.13.8",
|
||||
"ncp": "^2.0.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"stringify-object": "^3.3.0",
|
||||
"tmp": "^0.2.1",
|
||||
"ts-loader": "^9.2.6",
|
||||
"typedoc": "^0.22.8",
|
||||
"typescript-json-schema": "^0.50.1",
|
||||
"webpack": "^5.59.0"
|
||||
},
|
||||
"bin": {
|
||||
"scrypted-debug": "bin/scrypted-debug.js",
|
||||
"scrypted-deploy": "bin/scrypted-deploy.js",
|
||||
"scrypted-deploy-debug": "bin/scrypted-deploy-debug.js",
|
||||
"scrypted-package-json": "bin/scrypted-package-json.js",
|
||||
"scrypted-readme": "bin/scrypted-readme.js",
|
||||
"scrypted-webpack": "bin/scrypted-webpack.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/stringify-object": "^4.0.0",
|
||||
"ts-node": "^10.4.0"
|
||||
}
|
||||
},
|
||||
"../sdk": {
|
||||
"extraneous": true
|
||||
},
|
||||
"node_modules/@koush/axios-digest-auth": {
|
||||
"version": "0.8.5",
|
||||
"resolved": "https://registry.npmjs.org/@koush/axios-digest-auth/-/axios-digest-auth-0.8.5.tgz",
|
||||
"integrity": "sha512-EZMM0gMJ3hMUD4EuUqSwP6UGt5Vmw2TZtY7Ypec55AnxkExSXM0ySgPtqkAcnL43g1R27yAg/dQL7dRTLMqO3Q==",
|
||||
"dependencies": {
|
||||
"auth-header": "^1.0.0",
|
||||
"axios": "^0.21.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@koush/axios-digest-auth/node_modules/axios": {
|
||||
"version": "0.21.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
|
||||
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@scrypted/common": {
|
||||
"resolved": "../../common",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@scrypted/sdk": {
|
||||
"resolved": "../../sdk",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "16.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.6.tgz",
|
||||
"integrity": "sha512-YHUZhBOMTM3mjFkXVcK+WwAcYmyhe1wL4lfqNtzI0b3qAy7yuSetnM7QJazgE5PFmgVTNGiLOgRFfJMqW7XpSQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/auth-header": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/auth-header/-/auth-header-1.0.0.tgz",
|
||||
"integrity": "sha512-CPPazq09YVDUNNVWo4oSPTQmtwIzHusZhQmahCKvIsk0/xH6U3QsMAv3sM+7+Q0B1K2KJ/Q38OND317uXs4NHA=="
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "0.23.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.23.0.tgz",
|
||||
"integrity": "sha512-NmvAE4i0YAv5cKq8zlDoPd1VLKAqX5oLuZKs8xkJa4qi6RGn0uhCYFjWtHHC9EM/MwOwYWOs53W+V0aqEXq1sg==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.14.4"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.14.4",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.4.tgz",
|
||||
"integrity": "sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/querystringify": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.1.1.tgz",
|
||||
"integrity": "sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA=="
|
||||
},
|
||||
"node_modules/requires-port": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8="
|
||||
},
|
||||
"node_modules/url-parse": {
|
||||
"version": "1.5.3",
|
||||
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.3.tgz",
|
||||
"integrity": "sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ==",
|
||||
"dependencies": {
|
||||
"querystringify": "^2.1.1",
|
||||
"requires-port": "^1.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@koush/axios-digest-auth": {
|
||||
"version": "0.8.5",
|
||||
"resolved": "https://registry.npmjs.org/@koush/axios-digest-auth/-/axios-digest-auth-0.8.5.tgz",
|
||||
"integrity": "sha512-EZMM0gMJ3hMUD4EuUqSwP6UGt5Vmw2TZtY7Ypec55AnxkExSXM0ySgPtqkAcnL43g1R27yAg/dQL7dRTLMqO3Q==",
|
||||
"requires": {
|
||||
"auth-header": "^1.0.0",
|
||||
"axios": "^0.21.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": {
|
||||
"version": "0.21.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
|
||||
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
|
||||
"requires": {
|
||||
"follow-redirects": "^1.14.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@scrypted/common": {
|
||||
"version": "file:../../common",
|
||||
"requires": {
|
||||
"@scrypted/sdk": "file:../sdk",
|
||||
"@types/node": "^16.9.0",
|
||||
"typescript": "^4.4.3"
|
||||
}
|
||||
},
|
||||
"@scrypted/sdk": {
|
||||
"version": "file:../../sdk",
|
||||
"requires": {
|
||||
"@babel/plugin-proposal-class-properties": "^7.14.5",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.14.5",
|
||||
"@babel/plugin-proposal-numeric-separator": "^7.14.5",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.14.5",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.15.4",
|
||||
"@babel/plugin-transform-typescript": "^7.15.8",
|
||||
"@babel/preset-typescript": "^7.15.0",
|
||||
"@types/node": "^16.11.1",
|
||||
"@types/stringify-object": "^4.0.0",
|
||||
"adm-zip": "^0.4.13",
|
||||
"axios": "^0.21.4",
|
||||
"babel-loader": "^8.2.3",
|
||||
"babel-plugin-const-enum": "^1.1.0",
|
||||
"esbuild": "^0.13.8",
|
||||
"ncp": "^2.0.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"stringify-object": "^3.3.0",
|
||||
"tmp": "^0.2.1",
|
||||
"ts-loader": "^9.2.6",
|
||||
"ts-node": "^10.4.0",
|
||||
"typedoc": "^0.22.8",
|
||||
"typescript-json-schema": "^0.50.1",
|
||||
"webpack": "^5.59.0"
|
||||
}
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "16.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.6.tgz",
|
||||
"integrity": "sha512-YHUZhBOMTM3mjFkXVcK+WwAcYmyhe1wL4lfqNtzI0b3qAy7yuSetnM7QJazgE5PFmgVTNGiLOgRFfJMqW7XpSQ==",
|
||||
"dev": true
|
||||
},
|
||||
"auth-header": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/auth-header/-/auth-header-1.0.0.tgz",
|
||||
"integrity": "sha512-CPPazq09YVDUNNVWo4oSPTQmtwIzHusZhQmahCKvIsk0/xH6U3QsMAv3sM+7+Q0B1K2KJ/Q38OND317uXs4NHA=="
|
||||
},
|
||||
"axios": {
|
||||
"version": "0.23.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.23.0.tgz",
|
||||
"integrity": "sha512-NmvAE4i0YAv5cKq8zlDoPd1VLKAqX5oLuZKs8xkJa4qi6RGn0uhCYFjWtHHC9EM/MwOwYWOs53W+V0aqEXq1sg==",
|
||||
"requires": {
|
||||
"follow-redirects": "^1.14.4"
|
||||
}
|
||||
},
|
||||
"follow-redirects": {
|
||||
"version": "1.14.4",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.4.tgz",
|
||||
"integrity": "sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g=="
|
||||
},
|
||||
"querystringify": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.1.1.tgz",
|
||||
"integrity": "sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA=="
|
||||
},
|
||||
"requires-port": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8="
|
||||
},
|
||||
"url-parse": {
|
||||
"version": "1.5.3",
|
||||
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.3.tgz",
|
||||
"integrity": "sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ==",
|
||||
"requires": {
|
||||
"querystringify": "^2.1.1",
|
||||
"requires-port": "^1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
40
plugins/gstreamer-camera/package.json
Normal file
40
plugins/gstreamer-camera/package.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "@scrypted/gstreamer-camera",
|
||||
"version": "0.0.3",
|
||||
"description": "GStreamer Camera Plugin for Scrypted",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache",
|
||||
"scripts": {
|
||||
"prepublishOnly": "NODE_ENV=production scrypted-webpack",
|
||||
"prescrypted-vscode-launch": "scrypted-webpack",
|
||||
"scrypted-vscode-launch": "scrypted-deploy-debug",
|
||||
"scrypted-deploy-debug": "scrypted-deploy-debug",
|
||||
"scrypted-debug": "scrypted-debug",
|
||||
"scrypted-deploy": "scrypted-deploy",
|
||||
"scrypted-readme": "scrypted-readme",
|
||||
"scrypted-package-json": "scrypted-package-json",
|
||||
"scrypted-webpack": "scrypted-webpack"
|
||||
},
|
||||
"keywords": [
|
||||
"scrypted",
|
||||
"plugin",
|
||||
"gstreamer",
|
||||
"camera"
|
||||
],
|
||||
"scrypted": {
|
||||
"name": "GStreamer Camera Plugin",
|
||||
"type": "DeviceProvider",
|
||||
"interfaces": [
|
||||
"DeviceProvider",
|
||||
"DeviceCreator"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"@types/node": "^16.9.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"url-parse": "^1.4.7"
|
||||
}
|
||||
}
|
||||
292
plugins/gstreamer-camera/src/common.ts
Normal file
292
plugins/gstreamer-camera/src/common.ts
Normal file
@@ -0,0 +1,292 @@
|
||||
import sdk, { ScryptedDeviceBase, DeviceProvider, Settings, Setting, ScryptedDeviceType, VideoCamera, MediaObject, MediaStreamOptions, ScryptedInterface, FFMpegInput, Camera, PictureOptions, SettingValue, DeviceCreator, DeviceCreatorSettings } from "@scrypted/sdk";
|
||||
import { recommendRebroadcast } from "./recommend";
|
||||
import AxiosDigestAuth from '@koush/axios-digest-auth';
|
||||
import https from 'https';
|
||||
import { randomBytes } from "crypto";
|
||||
|
||||
const { log, deviceManager, mediaManager } = sdk;
|
||||
|
||||
const httpsAgent = new https.Agent({
|
||||
rejectUnauthorized: false
|
||||
});
|
||||
|
||||
export interface UrlMediaStreamOptions extends MediaStreamOptions {
|
||||
url: string;
|
||||
}
|
||||
|
||||
export abstract class CameraBase<T extends MediaStreamOptions> extends ScryptedDeviceBase implements Camera, VideoCamera, Settings {
|
||||
snapshotAuth: AxiosDigestAuth;
|
||||
pendingPicture: Promise<MediaObject>;
|
||||
|
||||
constructor(nativeId: string, public provider: CameraProviderBase<T>) {
|
||||
super(nativeId);
|
||||
}
|
||||
|
||||
getSnapshotUrl() {
|
||||
return this.storage.getItem('snapshotUrl');
|
||||
}
|
||||
|
||||
async takePicture(option?: PictureOptions): Promise<MediaObject> {
|
||||
if (!this.pendingPicture) {
|
||||
this.pendingPicture = this.takePictureThrottled(option);
|
||||
this.pendingPicture.finally(() => this.pendingPicture = undefined);
|
||||
}
|
||||
|
||||
return this.pendingPicture;
|
||||
}
|
||||
|
||||
async takePictureThrottled(option?: PictureOptions): Promise<MediaObject> {
|
||||
const snapshotUrl = this.getSnapshotUrl();
|
||||
if (!snapshotUrl) {
|
||||
throw new Error('Camera has no snapshot URL');
|
||||
}
|
||||
|
||||
if (!this.snapshotAuth) {
|
||||
this.snapshotAuth = new AxiosDigestAuth({
|
||||
username: this.getUsername(),
|
||||
password: this.getPassword(),
|
||||
});
|
||||
}
|
||||
|
||||
const response = await this.snapshotAuth.request({
|
||||
httpsAgent,
|
||||
method: "GET",
|
||||
responseType: 'arraybuffer',
|
||||
url: snapshotUrl,
|
||||
});
|
||||
|
||||
return mediaManager.createMediaObject(Buffer.from(response.data), response.headers['Content-Type'] || 'image/jpeg');
|
||||
}
|
||||
|
||||
async getPictureOptions(): Promise<PictureOptions[]> {
|
||||
return;
|
||||
}
|
||||
|
||||
getDefaultOrderedVideoStreamOptions(vsos: T[]) {
|
||||
if (!vsos || !vsos.length)
|
||||
return vsos;
|
||||
const defaultStream = this.getDefaultStream(vsos);
|
||||
if (!defaultStream)
|
||||
return vsos;
|
||||
vsos = vsos.filter(vso => vso.id !== defaultStream?.id);
|
||||
vsos.unshift(defaultStream);
|
||||
return vsos;
|
||||
}
|
||||
|
||||
async getVideoStreamOptions(): Promise<T[]> {
|
||||
let vsos = this.getRawVideoStreamOptions();
|
||||
return this.getDefaultOrderedVideoStreamOptions(vsos);
|
||||
}
|
||||
|
||||
abstract getRawVideoStreamOptions(): T[];
|
||||
|
||||
isAudioDisabled() {
|
||||
return this.storage.getItem('noAudio') === 'true';
|
||||
}
|
||||
|
||||
async getVideoStream(options?: T): Promise<MediaObject> {
|
||||
const vsos = await this.getVideoStreamOptions();
|
||||
const vso = vsos?.find(s => s.id === options?.id) || this.getDefaultStream(vsos);
|
||||
return this.createVideoStream(vso);
|
||||
}
|
||||
|
||||
abstract createVideoStream(options?: T): Promise<MediaObject>;
|
||||
|
||||
async getSnapshotUrlSettings(): Promise<Setting[]> {
|
||||
return [
|
||||
{
|
||||
key: 'snapshotUrl',
|
||||
title: 'Snapshot URL',
|
||||
placeholder: 'http://192.168.1.100[:80]/snapshot.jpg',
|
||||
value: this.getSnapshotUrl(),
|
||||
description: 'Optional: The snapshot URL that will returns the current JPEG image.'
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async getUrlSettings(): Promise<Setting[]> {
|
||||
return [
|
||||
...await this.getSnapshotUrlSettings(),
|
||||
];
|
||||
}
|
||||
|
||||
getUsername() {
|
||||
return this.storage.getItem('username');
|
||||
}
|
||||
|
||||
getPassword() {
|
||||
return this.storage.getItem('password');
|
||||
}
|
||||
|
||||
async getOtherSettings(): Promise<Setting[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
getDefaultStream(vsos: T[]) {
|
||||
let defaultStreamIndex = vsos?.findIndex(vso => vso.id === this.storage.getItem('defaultStream'));
|
||||
if (defaultStreamIndex === -1)
|
||||
defaultStreamIndex = 0;
|
||||
|
||||
defaultStreamIndex = defaultStreamIndex || 0;
|
||||
return vsos?.[defaultStreamIndex];
|
||||
}
|
||||
|
||||
async getStreamSettings(): Promise<Setting[]> {
|
||||
try {
|
||||
const vsos = await this.getVideoStreamOptions();
|
||||
if (!vsos?.length || vsos?.length === 1)
|
||||
return [];
|
||||
|
||||
|
||||
const defaultStream = this.getDefaultStream(vsos);
|
||||
return [
|
||||
{
|
||||
title: 'Default Stream',
|
||||
key: 'defaultStream',
|
||||
value: defaultStream?.name,
|
||||
choices: vsos.map(vso => vso.name),
|
||||
description: 'The default stream to use when not specified',
|
||||
}
|
||||
];
|
||||
}
|
||||
catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
getUsernameDescription(): string {
|
||||
return 'Optional: Username for snapshot http requests.';
|
||||
}
|
||||
|
||||
getPasswordDescription(): string {
|
||||
return 'Optional: Password for snapshot http requests.';
|
||||
}
|
||||
|
||||
async getSettings(): Promise<Setting[]> {
|
||||
return [
|
||||
{
|
||||
key: 'username',
|
||||
title: 'Username',
|
||||
value: this.getUsername(),
|
||||
description: this.getUsernameDescription(),
|
||||
},
|
||||
{
|
||||
key: 'password',
|
||||
title: 'Password',
|
||||
value: this.getPassword(),
|
||||
type: 'password',
|
||||
description: this.getPasswordDescription(),
|
||||
},
|
||||
...await this.getUrlSettings(),
|
||||
...await this.getStreamSettings(),
|
||||
...await this.getOtherSettings(),
|
||||
{
|
||||
key: 'noAudio',
|
||||
title: 'No Audio',
|
||||
description: 'Enable this setting if the camera does not have audio or to mute audio.',
|
||||
type: 'boolean',
|
||||
value: (this.isAudioDisabled()).toString(),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async putSettingBase(key: string, value: SettingValue) {
|
||||
if (key === 'defaultStream') {
|
||||
const vsos = await this.getVideoStreamOptions();
|
||||
const stream = vsos.find(vso => vso.name === value);
|
||||
this.storage.setItem('defaultStream', stream?.id);
|
||||
}
|
||||
else {
|
||||
this.storage.setItem(key, value.toString());
|
||||
}
|
||||
|
||||
this.snapshotAuth = undefined;
|
||||
|
||||
this.onDeviceEvent(ScryptedInterface.Settings, undefined);
|
||||
}
|
||||
|
||||
async putSetting(key: string, value: SettingValue) {
|
||||
this.putSettingBase(key, value);
|
||||
|
||||
if (key === 'snapshotUrl') {
|
||||
let interfaces = this.providedInterfaces;
|
||||
if (!value)
|
||||
interfaces = interfaces.filter(iface => iface !== ScryptedInterface.Camera)
|
||||
else
|
||||
interfaces.push(ScryptedInterface.Camera);
|
||||
|
||||
this.provider.updateDevice(this.nativeId, this.providedName, interfaces);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class CameraProviderBase<T extends MediaStreamOptions> extends ScryptedDeviceBase implements DeviceProvider, DeviceCreator {
|
||||
devices = new Map<string, any>();
|
||||
|
||||
constructor(nativeId?: string) {
|
||||
super(nativeId);
|
||||
|
||||
for (const camId of deviceManager.getNativeIds()) {
|
||||
if (camId)
|
||||
this.getDevice(camId);
|
||||
}
|
||||
|
||||
recommendRebroadcast();
|
||||
}
|
||||
|
||||
async createDevice(settings: DeviceCreatorSettings): Promise<string> {
|
||||
const nativeId = randomBytes(4).toString('hex');
|
||||
const name = settings.newCamera.toString();
|
||||
await this.updateDevice(nativeId, name, this.getInterfaces());
|
||||
return nativeId;
|
||||
}
|
||||
|
||||
async getCreateDeviceSettings(): Promise<Setting[]> {
|
||||
return [
|
||||
{
|
||||
key: 'newCamera',
|
||||
title: 'Add Camera',
|
||||
placeholder: 'Camera name, e.g.: Back Yard Camera, Baby Camera, etc',
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
getAdditionalInterfaces(): string[] {
|
||||
return [
|
||||
];
|
||||
}
|
||||
|
||||
getInterfaces() {
|
||||
return [ScryptedInterface.VideoCamera,
|
||||
ScryptedInterface.Settings, ...this.getAdditionalInterfaces()];
|
||||
}
|
||||
|
||||
updateDevice(nativeId: string, name: string, interfaces: string[], type?: ScryptedDeviceType) {
|
||||
return deviceManager.onDeviceDiscovered({
|
||||
nativeId,
|
||||
name,
|
||||
interfaces,
|
||||
type: type || ScryptedDeviceType.Camera,
|
||||
});
|
||||
}
|
||||
|
||||
async putSetting(key: string, value: string | number) {
|
||||
// generate a random id
|
||||
const nativeId = randomBytes(4).toString('hex');
|
||||
const name = value.toString();
|
||||
|
||||
this.updateDevice(nativeId, name, this.getInterfaces());
|
||||
}
|
||||
|
||||
abstract createCamera(nativeId: string): CameraBase<T>;
|
||||
|
||||
getDevice(nativeId: string) {
|
||||
let ret = this.devices.get(nativeId);
|
||||
if (!ret) {
|
||||
ret = this.createCamera(nativeId);
|
||||
if (ret)
|
||||
this.devices.set(nativeId, ret);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
139
plugins/gstreamer-camera/src/main.ts
Normal file
139
plugins/gstreamer-camera/src/main.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import sdk, { FFMpegInput, MediaObject, MediaStreamOptions, Setting, SettingValue } from "@scrypted/sdk";
|
||||
import child_process from "child_process";
|
||||
import { CameraProviderBase, CameraBase, UrlMediaStreamOptions } from "./common";
|
||||
// import {} from "../../../common/src/stream-parser"
|
||||
// import {} from "../../../common/src/ffmpeg-rebroadcast"
|
||||
import net from 'net';
|
||||
import {listenZeroCluster} from "../../../common/src/listen-cluster"
|
||||
|
||||
const { log, deviceManager, mediaManager } = sdk;
|
||||
|
||||
class GStreamerCamera extends CameraBase<MediaStreamOptions> {
|
||||
createGStreamerMediaStreamOptions(gstreamerInput: string, index: number): MediaStreamOptions {
|
||||
return {
|
||||
id: `channel${index}`,
|
||||
name: `Stream ${index + 1}`,
|
||||
video: {
|
||||
},
|
||||
audio: this.isAudioDisabled() ? null : {},
|
||||
};
|
||||
}
|
||||
|
||||
getGStreamerInputs() {
|
||||
let gstreamerInputs: string[] = [];
|
||||
try {
|
||||
gstreamerInputs = JSON.parse(this.storage.getItem('gstreamerInputs'));
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
|
||||
return gstreamerInputs;
|
||||
}
|
||||
|
||||
getRawVideoStreamOptions(): MediaStreamOptions[] {
|
||||
const gstreamerInputs = this.getGStreamerInputs();
|
||||
|
||||
// filter out empty strings.
|
||||
const ret = gstreamerInputs
|
||||
.filter(gstreamerInput => !!gstreamerInput)
|
||||
.map((gstreamerInput, index) => this.createGStreamerMediaStreamOptions(gstreamerInput, index));
|
||||
|
||||
if (!ret.length)
|
||||
return;
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
async getGStreamerInputSettings(): Promise<Setting[]> {
|
||||
return [
|
||||
{
|
||||
key: 'gstreamerInputs',
|
||||
title: 'GStreamer Input Stream Arguments',
|
||||
description: 'GStreamer input arguments passed to the command line gst-launch-1.0 tool. A camera may have multiple streams with different bitrates.',
|
||||
placeholder: '-i rtmp://[user:password@]192.168.1.100[:1935]/channel/101',
|
||||
value: this.getGStreamerInputs(),
|
||||
multiple: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async putSettingBase(key: string, value: SettingValue) {
|
||||
if (key === 'gstreamerInputs') {
|
||||
this.putGStreamerInputs(value as string[]);
|
||||
}
|
||||
else {
|
||||
super.putSettingBase(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
async putGStreamerInputs(gstreamerInputs: string[]) {
|
||||
this.storage.setItem('gstreamerInputs', JSON.stringify(gstreamerInputs.filter(url => !!url)));
|
||||
}
|
||||
|
||||
async getUrlSettings(): Promise<Setting[]> {
|
||||
return [
|
||||
...await this.getSnapshotUrlSettings(),
|
||||
...await this.getGStreamerInputSettings(),
|
||||
];
|
||||
}
|
||||
|
||||
async createVideoStream(options?: MediaStreamOptions): Promise<MediaObject> {
|
||||
const index = this.getRawVideoStreamOptions()?.findIndex(vso => vso.id === options.id);
|
||||
const gstreamerInputs = this.getGStreamerInputs();
|
||||
const gstreamerInput = gstreamerInputs[index];
|
||||
|
||||
if (!gstreamerInput)
|
||||
throw new Error('video streams not set up or no longer exists.');
|
||||
|
||||
const server = net.createServer(async (clientSocket) => {
|
||||
clearTimeout(serverTimeout);
|
||||
server.close();
|
||||
|
||||
const gstreamerServer = net.createServer(gstreamerSocket => {
|
||||
clearTimeout(gstreamerTimeout);
|
||||
gstreamerServer.close();
|
||||
clientSocket.pipe(gstreamerSocket).pipe(clientSocket);
|
||||
});
|
||||
const gstreamerTimeout = setTimeout(() => {
|
||||
this.console.log('timed out waiting for gstreamer');
|
||||
gstreamerServer.close();
|
||||
}, 30000);
|
||||
const gstreamerPort = await listenZeroCluster(gstreamerServer);
|
||||
const args = gstreamerInput.split(' ');
|
||||
args.push('!', 'mpegtsmux', '!', 'tcpclientsink', `port=${gstreamerPort}`, 'sync=false');
|
||||
this.console.log(args);
|
||||
const cp = child_process.spawn('gst-launch-1.0', args);
|
||||
cp.stdout.on('data', data => this.console.log(data.toString()));
|
||||
cp.stderr.on('data', data => this.console.log(data.toString()));
|
||||
|
||||
clientSocket.on('close', () => cp.kill());
|
||||
});
|
||||
const serverTimeout = setTimeout(() => {
|
||||
this.console.log('timed out waiting for client');
|
||||
server.close();
|
||||
}, 30000);
|
||||
const port = await listenZeroCluster(server);
|
||||
|
||||
const ret: FFMpegInput = {
|
||||
url: undefined,
|
||||
inputArguments: [
|
||||
'-f',
|
||||
'mpegts',
|
||||
'-i',
|
||||
`tcp://127.0.0.1:${port}`
|
||||
],
|
||||
mediaStreamOptions: options,
|
||||
};
|
||||
|
||||
return mediaManager.createFFmpegMediaObject(ret);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class GStreamerProvider extends CameraProviderBase<MediaStreamOptions> {
|
||||
createCamera(nativeId: string): GStreamerCamera {
|
||||
return new GStreamerCamera(nativeId, this);
|
||||
}
|
||||
}
|
||||
|
||||
export default new GStreamerProvider();
|
||||
7
plugins/gstreamer-camera/src/recommend.ts
Normal file
7
plugins/gstreamer-camera/src/recommend.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { alertRecommendedPlugins } from "@scrypted/common/src/alert-recommended-plugins";
|
||||
|
||||
export async function recommendRebroadcast() {
|
||||
alertRecommendedPlugins({
|
||||
'@scrypted/prebuffer-mixin': 'Rebroadcast',
|
||||
});
|
||||
}
|
||||
310
plugins/thermostat/src/main.ts
Normal file
310
plugins/thermostat/src/main.ts
Normal file
@@ -0,0 +1,310 @@
|
||||
import sdk, { HumidityCommand, HumidityMode, HumiditySensor, HumiditySetting, OnOff, ScryptedDeviceBase, TemperatureSetting, TemperatureUnit, Thermometer, ThermostatMode } from '@scrypted/sdk';
|
||||
const { deviceManager, log, systemManager } = sdk;
|
||||
|
||||
const sensor = systemManager.getDeviceById<Thermometer & HumiditySensor>(localStorage.getItem('sensor'));
|
||||
const heater = systemManager.getDeviceById<OnOff>(localStorage.getItem('heater'));
|
||||
const cooler = systemManager.getDeviceById<OnOff>(localStorage.getItem('cooler'));
|
||||
|
||||
class ThermostatDevice extends ScryptedDeviceBase implements TemperatureSetting, Thermometer, HumiditySensor, HumiditySetting {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.temperature = sensor.temperature;
|
||||
// copy the current state from the sensor.
|
||||
var unit;
|
||||
if (unit = localStorage.getItem('temperatureUnit')) {
|
||||
this.temperatureUnit = unit === 'F' ? TemperatureUnit.F : TemperatureUnit.C;
|
||||
}
|
||||
else {
|
||||
log.a('Please specify temperatureUnit C or F in Script Settings.');
|
||||
this.temperatureUnit = sensor.temperatureUnit;
|
||||
}
|
||||
this.humidity = sensor.humidity;
|
||||
this.humiditySetting = {
|
||||
mode: HumidityMode.Off,
|
||||
setpoint: 50,
|
||||
activeMode: HumidityMode.Off,
|
||||
availableModes: [HumidityMode.Auto, HumidityMode.Humidify, HumidityMode.Dehumidify],
|
||||
};
|
||||
|
||||
var modes: ThermostatMode[] = [];
|
||||
modes.push(ThermostatMode.Off);
|
||||
if (cooler) {
|
||||
modes.push(ThermostatMode.Cool);
|
||||
}
|
||||
if (heater) {
|
||||
modes.push(ThermostatMode.Heat);
|
||||
}
|
||||
if (heater && cooler) {
|
||||
modes.push(ThermostatMode.HeatCool);
|
||||
}
|
||||
modes.push(ThermostatMode.On);
|
||||
this.thermostatAvailableModes = modes;
|
||||
|
||||
try {
|
||||
if (!this.thermostatMode) {
|
||||
this.thermostatMode = ThermostatMode.Off;
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
async setHumidity(humidity: HumidityCommand): Promise<void> {
|
||||
this.humiditySetting = {
|
||||
mode: humidity.mode,
|
||||
setpoint: 50,
|
||||
activeMode: HumidityMode.Off,
|
||||
availableModes: [HumidityMode.Auto, HumidityMode.Humidify, HumidityMode.Dehumidify],
|
||||
}
|
||||
}
|
||||
|
||||
// whenever the temperature changes, or a new command is sent, this updates the current state accordingly.
|
||||
updateState() {
|
||||
var threshold = 2;
|
||||
|
||||
var thermostatMode = this.thermostatMode || ThermostatMode.Off;
|
||||
|
||||
if (!thermostatMode) {
|
||||
log.e('thermostat mode not set');
|
||||
return;
|
||||
}
|
||||
|
||||
// this holds the last known state of the thermostat.
|
||||
// ie, what it decided to do, the last time it updated its state.
|
||||
var thermostatState = localStorage.getItem('thermostatState');
|
||||
|
||||
// set the state before turning any devices on or off.
|
||||
// on/off events will need to be resolved by looking at the state to
|
||||
// determine if it is manual user input.
|
||||
function setState(state) {
|
||||
if (state == thermostatState) {
|
||||
// log.i('Thermostat state unchanged. ' + state)
|
||||
return;
|
||||
}
|
||||
|
||||
log.i('Thermostat state changed. ' + state);
|
||||
localStorage.setItem('thermostatState', state);
|
||||
}
|
||||
|
||||
function manageSetpoint(temperatureDifference, er, other, ing, ed) {
|
||||
if (!er) {
|
||||
log.e('Thermostat mode set to ' + thermostatMode + ', but ' + thermostatMode + 'er variable is not defined.');
|
||||
return;
|
||||
}
|
||||
|
||||
// turn off the other one. if heating, turn off cooler. if cooling, turn off heater.
|
||||
if (other && other.on) {
|
||||
other.turnOff();
|
||||
}
|
||||
|
||||
if (temperatureDifference < 0) {
|
||||
setState(ed);
|
||||
if (er.on) {
|
||||
er.turnOff();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// start cooling/heating if way over threshold, or if it is not in the cooling/heating state
|
||||
if (temperatureDifference > threshold || thermostatState != ing) {
|
||||
setState(ing);
|
||||
if (!er.on) {
|
||||
er.turnOn();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
setState(ed);
|
||||
if (er.on) {
|
||||
er.turnOff();
|
||||
}
|
||||
}
|
||||
|
||||
function allOff() {
|
||||
if (heater && heater.on) {
|
||||
heater.turnOff();
|
||||
}
|
||||
if (cooler && cooler.on) {
|
||||
cooler.turnOff();
|
||||
}
|
||||
}
|
||||
|
||||
if (thermostatMode == 'Off') {
|
||||
setState('Off');
|
||||
allOff();
|
||||
return;
|
||||
|
||||
} else if (thermostatMode == 'Cool') {
|
||||
|
||||
let thermostatSetpoint = this.thermostatSetpoint || sensor.temperature;
|
||||
if (!thermostatSetpoint) {
|
||||
log.e('No thermostat setpoint is defined.');
|
||||
return;
|
||||
}
|
||||
|
||||
var temperatureDifference = sensor.temperature - thermostatSetpoint;
|
||||
manageSetpoint(temperatureDifference, cooler, heater, 'Cooling', 'Cooled');
|
||||
return;
|
||||
|
||||
} else if (thermostatMode == 'Heat') {
|
||||
|
||||
let thermostatSetpoint = this.thermostatSetpoint || sensor.temperature;
|
||||
if (!thermostatSetpoint) {
|
||||
log.e('No thermostat setpoint is defined.');
|
||||
return;
|
||||
}
|
||||
|
||||
var temperatureDifference = thermostatSetpoint - sensor.temperature;
|
||||
manageSetpoint(temperatureDifference, heater, cooler, 'Heating', 'Heated');
|
||||
return;
|
||||
|
||||
} else if (thermostatMode == 'HeatCool') {
|
||||
|
||||
var temperature = sensor.temperature;
|
||||
var thermostatSetpointLow = this.thermostatSetpointLow || sensor.temperature;
|
||||
var thermostatSetpointHigh = this.thermostatSetpointHigh || sensor.temperature;
|
||||
|
||||
if (!thermostatSetpointLow || !thermostatSetpointHigh) {
|
||||
log.e('No thermostat setpoint low/high is defined.');
|
||||
return;
|
||||
}
|
||||
|
||||
// see if this is within HeatCool tolerance. This prevents immediately cooling after heating all the way to the high setpoint.
|
||||
if ((thermostatState == 'HeatCooled' || thermostatState == 'Heated' || thermostatState == 'Cooled')
|
||||
&& temperature > thermostatSetpointLow - threshold
|
||||
&& temperature < thermostatSetpointHigh + threshold) {
|
||||
// normalize the state into HeatCooled
|
||||
setState('HeatCooled');
|
||||
allOff();
|
||||
return;
|
||||
}
|
||||
|
||||
// if already heating or cooling or way out of tolerance, continue doing it until state changes.
|
||||
if (temperature < thermostatSetpointLow || thermostatState == 'Heating') {
|
||||
var temperatureDifference = thermostatSetpointHigh - temperature;
|
||||
manageSetpoint(temperatureDifference, heater, null, 'Heating', 'Heated');
|
||||
return;
|
||||
} else if (temperature > thermostatSetpointHigh || thermostatState == 'Cooling') {
|
||||
var temperatureDifference = temperature - thermostatSetpointLow;
|
||||
manageSetpoint(temperatureDifference, cooler, null, 'Cooling', 'Cooled');
|
||||
return;
|
||||
}
|
||||
|
||||
// temperature is within tolerance, so this is now HeatCooled
|
||||
setState('HeatCooled');
|
||||
allOff();
|
||||
return;
|
||||
}
|
||||
|
||||
log.e('Unknown mode ' + thermostatMode);
|
||||
}
|
||||
// implementation of TemperatureSetting
|
||||
async setThermostatSetpoint(thermostatSetpoint) {
|
||||
log.i('thermostatSetpoint changed ' + thermostatSetpoint);
|
||||
this.thermostatSetpoint = thermostatSetpoint;
|
||||
this.updateState();
|
||||
}
|
||||
async setThermostatSetpointLow(thermostatSetpointLow) {
|
||||
log.i('thermostatSetpointLow changed ' + thermostatSetpointLow);
|
||||
this.thermostatSetpointLow = thermostatSetpointLow;
|
||||
this.updateState();
|
||||
}
|
||||
async setThermostatSetpointHigh(thermostatSetpointHigh) {
|
||||
log.i('thermostatSetpointHigh changed ' + thermostatSetpointHigh);
|
||||
this.thermostatSetpointHigh = thermostatSetpointHigh;
|
||||
this.updateState();
|
||||
}
|
||||
async setThermostatMode(mode) {
|
||||
log.i('thermostat mode set to ' + mode);
|
||||
if (mode == 'On') {
|
||||
mode = localStorage.getItem("lastThermostatMode");
|
||||
}
|
||||
else if (mode != 'Off') {
|
||||
localStorage.setItem("lastThermostatMode", mode);
|
||||
}
|
||||
this.thermostatMode = mode;
|
||||
this.updateState();
|
||||
}
|
||||
// end implementation of TemperatureSetting
|
||||
// If the heater or cooler gets turned on or off manually (or programatically),
|
||||
// make this resolve with the current state. This relies on the state being set
|
||||
// before any devices are turned on or off (as mentioned above) to avoid race
|
||||
// conditions.
|
||||
manageEvent(on, ing) {
|
||||
var state = localStorage.getItem('thermostatState');
|
||||
if (on) {
|
||||
// on implies it must be heating/cooling
|
||||
if (state != ing) {
|
||||
// should this be Heat/Cool?
|
||||
this.setThermostatMode('On');
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// off implies that it must NOT be heating/cooling
|
||||
if (state == ing) {
|
||||
this.setThermostatMode('Off');
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
var thermostatDevice = new ThermostatDevice();
|
||||
|
||||
function alertAndThrow(msg) {
|
||||
log.a(msg);
|
||||
throw new Error(msg);
|
||||
}
|
||||
|
||||
try {
|
||||
if (!sensor)
|
||||
throw new Error();
|
||||
}
|
||||
catch {
|
||||
alertAndThrow('Setup Incomplete: Assign a thermometer and humidity sensor to the "sensor" variable.');
|
||||
}
|
||||
log.clearAlerts();
|
||||
|
||||
if (!heater && !cooler) {
|
||||
alertAndThrow('Setup Incomplete: Assign an OnOff device to the "heater" and/or "cooler" OnOff variables.');
|
||||
}
|
||||
log.clearAlerts();
|
||||
|
||||
// register to listen for temperature change events
|
||||
sensor.listen('Thermometer', function(source, event, data) {
|
||||
thermostatDevice[event.property] = data;
|
||||
if (event.property == 'temperature') {
|
||||
log.i('temperature event: ' + data);
|
||||
thermostatDevice.updateState();
|
||||
}
|
||||
});
|
||||
|
||||
// listen to humidity events too, and pass those along
|
||||
sensor.listen('HumiditySensor', function(source, event, data) {
|
||||
thermostatDevice[event.property] = data;
|
||||
});
|
||||
|
||||
// Watch for on/off events, some of them may be physical
|
||||
// button presses, and those will need to be resolved by
|
||||
// checking the state versus the event.
|
||||
if (heater) {
|
||||
heater.listen('OnOff', function(source, event, on) {
|
||||
thermostatDevice.manageEvent(on, 'Heating');
|
||||
});
|
||||
}
|
||||
if (cooler) {
|
||||
cooler.listen('OnOff', function(source, event, on) {
|
||||
thermostatDevice.manageEvent(on, 'Cooling');
|
||||
});
|
||||
}
|
||||
|
||||
export default thermostatDevice;
|
||||
Reference in New Issue
Block a user