diff --git a/plugins/dummy-switch/.gitignore b/plugins/dummy-switch/.gitignore new file mode 100644 index 000000000..9cdb546bf --- /dev/null +++ b/plugins/dummy-switch/.gitignore @@ -0,0 +1,4 @@ +.DS_Store +out/ +node_modules/ +dist/ diff --git a/plugins/dummy-switch/.npmignore b/plugins/dummy-switch/.npmignore new file mode 100644 index 000000000..ff2824293 --- /dev/null +++ b/plugins/dummy-switch/.npmignore @@ -0,0 +1,8 @@ +.DS_Store +out/ +node_modules/ +*.map +fs +src +.vscode +dist/*.js diff --git a/plugins/dummy-switch/.vscode/launch.json b/plugins/dummy-switch/.vscode/launch.json new file mode 100644 index 000000000..0669f79b4 --- /dev/null +++ b/plugins/dummy-switch/.vscode/launch.json @@ -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": [ + "/**" + ], + "preLaunchTask": "scrypted: deploy+debug", + "sourceMaps": true, + "localRoot": "${workspaceFolder}/out", + "remoteRoot": "/plugin/", + "type": "pwa-node" + } + ] +} \ No newline at end of file diff --git a/plugins/dummy-switch/.vscode/settings.json b/plugins/dummy-switch/.vscode/settings.json new file mode 100644 index 000000000..77ccdbd6d --- /dev/null +++ b/plugins/dummy-switch/.vscode/settings.json @@ -0,0 +1,4 @@ + +{ + "scrypted.debugHost": "127.0.0.1", +} \ No newline at end of file diff --git a/plugins/dummy-switch/.vscode/tasks.json b/plugins/dummy-switch/.vscode/tasks.json new file mode 100644 index 000000000..4d922a539 --- /dev/null +++ b/plugins/dummy-switch/.vscode/tasks.json @@ -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}", + }, + ] +} diff --git a/plugins/dummy-switch/README.md b/plugins/dummy-switch/README.md new file mode 100644 index 000000000..587eee071 --- /dev/null +++ b/plugins/dummy-switch/README.md @@ -0,0 +1,21 @@ +# @scrypted/dummy-switch + +## Usage + +This dummy switch plugin lets you create dummy switches +that can be synced with HomeKit or Google Home. The switch +can be programmed to run a script or shell script when turned on or off. + +## npm commands + * npm run scrypted-webpack + * npm run scrypted-deploy + * npm run scrypted-debug + +## 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. diff --git a/plugins/dummy-switch/package-lock.json b/plugins/dummy-switch/package-lock.json new file mode 100644 index 000000000..ec25e30ec --- /dev/null +++ b/plugins/dummy-switch/package-lock.json @@ -0,0 +1,205 @@ +{ + "name": "@scrypted/dummy-motion-sensor", + "version": "0.0.2", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "@scrypted/dummy-motion-sensor", + "version": "0.0.2", + "dependencies": { + "@types/node": "^16.6.1", + "axios": "^0.19.0" + }, + "devDependencies": { + "@scrypted/sdk": "file:../../sdk" + } + }, + "../../sdk": { + "name": "@scrypted/sdk", + "version": "0.0.72", + "dev": true, + "license": "ISC", + "dependencies": { + "@babel/core": "^7.2.2", + "@babel/plugin-proposal-class-properties": "^7.4.4", + "@babel/plugin-proposal-optional-chaining": "^7.13.12", + "@babel/plugin-transform-modules-commonjs": "^7.2.0", + "@babel/plugin-transform-typescript": "^7.15.0", + "@babel/polyfill": "^7.2.5", + "@babel/preset-env": "^7.2.3", + "@babel/preset-typescript": "^7.15.0", + "@types/node": "^16.6.1", + "adm-zip": "^0.4.13", + "axios": "^0.21.1", + "babel-loader": "^8.0.4", + "babel-plugin-const-enum": "^1.1.0", + "babel-plugin-minify-dead-code-elimination": "^0.5.1", + "babel-polyfill": "^6.26.0", + "babel-template": "^6.26.0", + "browserify-buffertools": "^1.0.2", + "bytebuffer": "^5.0.1", + "chalk": "^2.4.2", + "clean-webpack-plugin": "^3.0.0", + "engine.io-client": "^3.3.2", + "event-target-shim": "^5.0.1", + "events": "^3.0.0", + "long": "^4.0.0", + "node-cmd": "^3.0.0", + "node-ip": "^0.1.2", + "raw-loader": "^1.0.0", + "terser": "^3.14.1", + "ts-loader": "^5.4.5", + "tsconfig-paths-webpack-plugin": "^3.2.0", + "typescript": "^4.3.5", + "webpack": "^4.28.1", + "webpack-cli": "^3.1.2", + "webpack-inject-plugin": "^1.0.2" + }, + "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" + } + }, + "../sdk": { + "extraneous": true + }, + "node_modules/@scrypted/sdk": { + "resolved": "../../sdk", + "link": true + }, + "node_modules/@types/node": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.6.1.tgz", + "integrity": "sha512-Sr7BhXEAer9xyGuCN3Ek9eg9xPviCF2gfu9kTfuU2HkTVAMYSDeX40fvpmo72n5nansg3nsBjuQBrsS28r+NUw==" + }, + "node_modules/axios": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.0.tgz", + "integrity": "sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ==", + "dependencies": { + "follow-redirects": "1.5.10", + "is-buffer": "^2.0.2" + } + }, + "node_modules/follow-redirects": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", + "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", + "dependencies": { + "debug": "=3.1.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/follow-redirects/node_modules/debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/follow-redirects/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "node_modules/is-buffer": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz", + "integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==", + "engines": { + "node": ">=4" + } + } + }, + "dependencies": { + "@scrypted/sdk": { + "version": "file:../../sdk", + "requires": { + "@babel/core": "^7.2.2", + "@babel/plugin-proposal-class-properties": "^7.4.4", + "@babel/plugin-proposal-optional-chaining": "^7.13.12", + "@babel/plugin-transform-modules-commonjs": "^7.2.0", + "@babel/plugin-transform-typescript": "^7.15.0", + "@babel/polyfill": "^7.2.5", + "@babel/preset-env": "^7.2.3", + "@babel/preset-typescript": "^7.15.0", + "@types/node": "^16.6.1", + "adm-zip": "^0.4.13", + "axios": "^0.21.1", + "babel-loader": "^8.0.4", + "babel-plugin-const-enum": "^1.1.0", + "babel-plugin-minify-dead-code-elimination": "^0.5.1", + "babel-polyfill": "^6.26.0", + "babel-template": "^6.26.0", + "browserify-buffertools": "^1.0.2", + "bytebuffer": "^5.0.1", + "chalk": "^2.4.2", + "clean-webpack-plugin": "^3.0.0", + "engine.io-client": "^3.3.2", + "event-target-shim": "^5.0.1", + "events": "^3.0.0", + "long": "^4.0.0", + "node-cmd": "^3.0.0", + "node-ip": "^0.1.2", + "raw-loader": "^1.0.0", + "terser": "^3.14.1", + "ts-loader": "^5.4.5", + "tsconfig-paths-webpack-plugin": "^3.2.0", + "typescript": "^4.3.5", + "webpack": "^4.28.1", + "webpack-cli": "^3.1.2", + "webpack-inject-plugin": "^1.0.2" + } + }, + "@types/node": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.6.1.tgz", + "integrity": "sha512-Sr7BhXEAer9xyGuCN3Ek9eg9xPviCF2gfu9kTfuU2HkTVAMYSDeX40fvpmo72n5nansg3nsBjuQBrsS28r+NUw==" + }, + "axios": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.0.tgz", + "integrity": "sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ==", + "requires": { + "follow-redirects": "1.5.10", + "is-buffer": "^2.0.2" + } + }, + "follow-redirects": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", + "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", + "requires": { + "debug": "=3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "is-buffer": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz", + "integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==" + } + } +} diff --git a/plugins/dummy-switch/package.json b/plugins/dummy-switch/package.json new file mode 100644 index 000000000..3a621dab0 --- /dev/null +++ b/plugins/dummy-switch/package.json @@ -0,0 +1,38 @@ +{ + "name": "@scrypted/dummy-switch", + "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", + "dummy", + "switch", + "shell", + "script" + ], + "scrypted": { + "name": "Dummy Switch Plugin", + "type": "DeviceProvider", + "interfaces": [ + "DeviceProvider", + "Settings" + ] + }, + "dependencies": { + "@types/node": "^16.6.1", + "axios": "^0.19.0" + }, + "devDependencies": { + "@scrypted/sdk": "file:../../sdk" + }, + "version": "0.0.2" +} diff --git a/plugins/dummy-switch/src/main.ts b/plugins/dummy-switch/src/main.ts new file mode 100644 index 000000000..29bd170ac --- /dev/null +++ b/plugins/dummy-switch/src/main.ts @@ -0,0 +1,125 @@ +import { DeviceProvider, OnOff, Scriptable, ScriptSource, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, Setting, Settings } from '@scrypted/sdk'; +import sdk from '@scrypted/sdk'; +import { createMonacoEvalDefaults, scryptedEval } from '../../../common/src/scrypted-eval'; +import child_process from 'child_process'; + +const { log, deviceManager } = sdk; + +class DummySwitch extends ScryptedDeviceBase implements OnOff, Scriptable { + language: string; + constructor(nativeId: string) { + super(nativeId); + + if (nativeId.startsWith('typescript:')) + this.language = 'typescript'; + else + this.language = 'shell'; + } + async turnOff(): Promise { + this.on = false; + const source = JSON.parse(this.storage.getItem('source')); + this.eval(source); + } + async turnOn(): Promise { + this.on = true; + const source = JSON.parse(this.storage.getItem('source')); + this.eval(source); + } + async saveScript(script: ScriptSource): Promise { + this.storage.setItem('source', JSON.stringify(script)); + } + async loadScripts(): Promise<{ [filename: string]: ScriptSource; }> { + const filename = this.language === 'typescript' ? 'dummy-switch-script.ts' : 'dummy-switch-script.sh'; + const ret: { [filename: string]: ScriptSource; } = { + }; + + try { + const source = JSON.parse(this.storage.getItem('source')); + ret[filename] = source; + } + catch (e) { + ret[filename] = { + script: '', + } + } + Object.assign(ret[filename], { + language: this.language, + name: 'Switch Script', + monacoEvalDefaults: this.language === 'typescript' ? createMonacoEvalDefaults({}) : undefined, + }); + return ret; + } + async eval(source: ScriptSource, variables?: { [name: string]: any; }): Promise { + if (this.language === 'typescript') + return scryptedEval(this, source.script, {}, {}); + const cp = child_process.spawn('sh', { + env: { + DUMMY_ON: (!!this.on).toString(), + }, + }); + cp.stdin.write(source.script); + cp.stdout.on('data', data => this.console.log(data.toString())); + cp.stderr.on('data', data => this.console.log(data.toString())); + } +} + +class DummySwitchProvider extends ScryptedDeviceBase implements DeviceProvider, Settings { + devices = new Map(); + + constructor(nativeId?: string) { + super(nativeId); + + for (const camId of deviceManager.getNativeIds()) { + if (camId) + this.getDevice(camId); + } + + } + async getSettings(): Promise { + return [ + { + key: 'shell:', + title: 'Add Switch (Shell Script)', + placeholder: 'Switch Name', + }, + { + key: 'typescript:', + title: 'Add Switch (Typescript)', + placeholder: 'Switch Name', + }, + ] + } + + async putSetting(key: string, value: string | number) { + // generate a random id + const nativeId = key + Math.random().toString(); + const name = value.toString(); + + deviceManager.onDeviceDiscovered({ + nativeId, + name, + interfaces: [ScryptedInterface.OnOff, ScryptedInterface.Scriptable], + type: ScryptedDeviceType.Switch, + }); + + + var text = `New Switch ${name} ready. Check the notification area to complete setup.`; + log.a(text); + log.clearAlert(text); + } + + async discoverDevices(duration: number) { + } + + getDevice(nativeId: string) { + let ret = this.devices.get(nativeId); + if (!ret) { + ret = new DummySwitch(nativeId); + if (ret) + this.devices.set(nativeId, ret); + } + return ret; + } +} + +export default new DummySwitchProvider(); diff --git a/plugins/dummy-switch/src/scrypted-eval.ts b/plugins/dummy-switch/src/scrypted-eval.ts new file mode 100644 index 000000000..2c2c4c860 --- /dev/null +++ b/plugins/dummy-switch/src/scrypted-eval.ts @@ -0,0 +1,6 @@ +import { ScryptedDeviceBase } from "@scrypted/sdk"; +import { scryptedEval as scryptedEvalBase } from "../../../common/src/scrypted-eval"; + +export async function scryptedEval(device: ScryptedDeviceBase, script: string, params: { [name: string]: any }) { + return scryptedEvalBase(device, script, {}, params); +}