mirror of
https://github.com/koush/scrypted.git
synced 2026-03-20 16:40:24 +00:00
console and repl
This commit is contained in:
4
plugins/core/package-lock.json
generated
4
plugins/core/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.0.50",
|
||||
"version": "0.0.51",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.0.50",
|
||||
"version": "0.0.51",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.0.50",
|
||||
"version": "0.0.51",
|
||||
"description": "Scrypted Core plugin. Provides the UI, websocket, and engine.io APIs.",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -16,6 +16,7 @@ import fs from 'fs';
|
||||
import { sendJSON } from './http-helpers';
|
||||
import { Automation } from './automation';
|
||||
import { AggregateDevice, createAggregateDevice } from './aggregate';
|
||||
import net from 'net';
|
||||
|
||||
const indexHtml = fs.readFileSync('dist/index.html').toString();
|
||||
|
||||
@@ -132,9 +133,30 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Eng
|
||||
async discoverDevices(duration: number) {
|
||||
}
|
||||
|
||||
async checkService(request: HttpRequest, ws: WebSocket, name: string): Promise<boolean> {
|
||||
const check = `/endpoint/@scrypted/core/engine.io/${name}/`;
|
||||
if (!request.url.startsWith(check))
|
||||
return false;
|
||||
const deviceId = request.url.substr(check.length).split('/')[0];
|
||||
const plugins = await systemManager.getComponent('plugins');
|
||||
const { nativeId, pluginId } = await plugins.getDeviceInfo(deviceId);
|
||||
const port = await plugins.getRemoteServicePort(pluginId, name);
|
||||
const socket = net.connect(port);
|
||||
socket.on('data', data => ws.send(data));
|
||||
socket.resume();
|
||||
socket.write(nativeId?.toString() || 'undefined');
|
||||
ws.onclose = () => socket.destroy();
|
||||
ws.onmessage = message => socket.write(message.data);
|
||||
return true;
|
||||
}
|
||||
|
||||
async onConnection(request: HttpRequest, webSocketUrl: string): Promise<void> {
|
||||
const ws = new WebSocket(webSocketUrl);
|
||||
|
||||
if (await this.checkService(request, ws, 'console') || await this.checkService(request, ws, 'repl')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.isPublicEndpoint) {
|
||||
ws.close();
|
||||
return;
|
||||
|
||||
55
plugins/core/ui/package-lock.json
generated
55
plugins/core/ui/package-lock.json
generated
@@ -38,7 +38,8 @@
|
||||
"vue2-google-maps": "^0.10.7",
|
||||
"vuetify": "^2.5.8",
|
||||
"vuex": "^3.6.2",
|
||||
"xterm": "^4.13.0"
|
||||
"xterm": "^4.13.0",
|
||||
"xterm-addon-fit": "^0.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-class-properties": "^7.13.0",
|
||||
@@ -49,6 +50,8 @@
|
||||
"@babel/preset-typescript": "^7.13.0",
|
||||
"@types/blob-stream": "^0.1.30",
|
||||
"@types/dom-webcodecs": "^0.1.0",
|
||||
"@types/engine.io": "^3.1.7",
|
||||
"@types/engine.io-client": "^3.1.5",
|
||||
"@types/lodash": "^4.14.172",
|
||||
"@types/mkdirp": "^1.0.1",
|
||||
"@types/node": "^16.0.0",
|
||||
@@ -2155,6 +2158,24 @@
|
||||
"integrity": "sha512-e5DuY7mZedrV8rrgEnm1gPmy4h2x11ryUTnkjmbdR6SWGCS3Qvz1ZJudtBIrbPNEhnpGM634SL7ZHQDz7dsBCg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/engine.io": {
|
||||
"version": "3.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/engine.io/-/engine.io-3.1.7.tgz",
|
||||
"integrity": "sha512-qNjVXcrp+1sS8YpRUa714r0pgzOwESdW5UjHL7D/2ZFdBX0BXUXtg1LUrp+ylvqbvMcMWUy73YpRoxPN2VoKAQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/engine.io-client": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/engine.io-client/-/engine.io-client-3.1.5.tgz",
|
||||
"integrity": "sha512-h2BKMLNVMQGQ0Q7WaQCJ/63LooRQhItnoFNXhbNVnjhNheWEO+sdpTFjZpzWP7TDl4M5lI3fsEAk3PFZfV67Yg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/express": {
|
||||
"version": "4.17.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz",
|
||||
@@ -18742,6 +18763,14 @@
|
||||
"resolved": "https://registry.npmjs.org/xterm/-/xterm-4.13.0.tgz",
|
||||
"integrity": "sha512-HVW1gdoLOTnkMaqQCr2r3mQy4fX9iSa5gWxKZ2UTYdLa4iqavv7QxJ8n1Ypse32shPVkhTYPLS6vHEFZp5ghzw=="
|
||||
},
|
||||
"node_modules/xterm-addon-fit": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/xterm-addon-fit/-/xterm-addon-fit-0.5.0.tgz",
|
||||
"integrity": "sha512-DsS9fqhXHacEmsPxBJZvfj2la30Iz9xk+UKjhQgnYNkrUIN5CYLbw7WEfz117c7+S86S/tpHPfvNxJsF5/G8wQ==",
|
||||
"peerDependencies": {
|
||||
"xterm": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
|
||||
@@ -20492,6 +20521,24 @@
|
||||
"integrity": "sha512-e5DuY7mZedrV8rrgEnm1gPmy4h2x11ryUTnkjmbdR6SWGCS3Qvz1ZJudtBIrbPNEhnpGM634SL7ZHQDz7dsBCg==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/engine.io": {
|
||||
"version": "3.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/engine.io/-/engine.io-3.1.7.tgz",
|
||||
"integrity": "sha512-qNjVXcrp+1sS8YpRUa714r0pgzOwESdW5UjHL7D/2ZFdBX0BXUXtg1LUrp+ylvqbvMcMWUy73YpRoxPN2VoKAQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/engine.io-client": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/engine.io-client/-/engine.io-client-3.1.5.tgz",
|
||||
"integrity": "sha512-h2BKMLNVMQGQ0Q7WaQCJ/63LooRQhItnoFNXhbNVnjhNheWEO+sdpTFjZpzWP7TDl4M5lI3fsEAk3PFZfV67Yg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/express": {
|
||||
"version": "4.17.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz",
|
||||
@@ -33900,6 +33947,12 @@
|
||||
"resolved": "https://registry.npmjs.org/xterm/-/xterm-4.13.0.tgz",
|
||||
"integrity": "sha512-HVW1gdoLOTnkMaqQCr2r3mQy4fX9iSa5gWxKZ2UTYdLa4iqavv7QxJ8n1Ypse32shPVkhTYPLS6vHEFZp5ghzw=="
|
||||
},
|
||||
"xterm-addon-fit": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/xterm-addon-fit/-/xterm-addon-fit-0.5.0.tgz",
|
||||
"integrity": "sha512-DsS9fqhXHacEmsPxBJZvfj2la30Iz9xk+UKjhQgnYNkrUIN5CYLbw7WEfz117c7+S86S/tpHPfvNxJsF5/G8wQ==",
|
||||
"requires": {}
|
||||
},
|
||||
"y18n": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
|
||||
|
||||
@@ -40,7 +40,8 @@
|
||||
"vue2-google-maps": "^0.10.7",
|
||||
"vuetify": "^2.5.8",
|
||||
"vuex": "^3.6.2",
|
||||
"xterm": "^4.13.0"
|
||||
"xterm": "^4.13.0",
|
||||
"xterm-addon-fit": "^0.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-class-properties": "^7.13.0",
|
||||
@@ -51,6 +52,8 @@
|
||||
"@babel/preset-typescript": "^7.13.0",
|
||||
"@types/blob-stream": "^0.1.30",
|
||||
"@types/dom-webcodecs": "^0.1.0",
|
||||
"@types/engine.io": "^3.1.7",
|
||||
"@types/engine.io-client": "^3.1.5",
|
||||
"@types/lodash": "^4.14.172",
|
||||
"@types/mkdirp": "^1.0.1",
|
||||
"@types/node": "^16.0.0",
|
||||
|
||||
39
plugins/core/ui/src/components/ConsoleCard.vue
Normal file
39
plugins/core/ui/src/components/ConsoleCard.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<v-card raised>
|
||||
<v-toolbar dark color="blue"> Console </v-toolbar>
|
||||
<div ref="terminal"></div>
|
||||
</v-card>
|
||||
</template>
|
||||
<script>
|
||||
import { Terminal } from "xterm";
|
||||
import { FitAddon } from 'xterm-addon-fit';
|
||||
import eio from "engine.io-client";
|
||||
|
||||
export default {
|
||||
props: ["deviceId"],
|
||||
socket: null,
|
||||
mounted() {
|
||||
const term = new Terminal({
|
||||
convertEol: true,
|
||||
disableStdin: true,
|
||||
});
|
||||
const fitAddon = new FitAddon();
|
||||
term.loadAddon(fitAddon);
|
||||
term.open(this.$refs.terminal);
|
||||
fitAddon.fit();
|
||||
|
||||
const endpointPath = `/endpoint/@scrypted/core`;
|
||||
|
||||
const options = {
|
||||
path: `${endpointPath}/engine.io/console/${this.deviceId}`,
|
||||
};
|
||||
const rootLocation = `${window.location.protocol}//${window.location.host}`;
|
||||
this.socket = eio(rootLocation, options);
|
||||
|
||||
this.socket.on('message', data => term.write(new Uint8Array(data)));
|
||||
},
|
||||
destroyed() {
|
||||
this.socket?.close();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -3,208 +3,213 @@
|
||||
<v-flex xs12 md6 v-if="name != null">
|
||||
<v-layout row wrap>
|
||||
<v-flex xs12>
|
||||
<v-flex>
|
||||
<div v-if="deviceAlerts.length" class="pb-5">
|
||||
<v-alert
|
||||
dismissible
|
||||
@input="removeAlert(alert)"
|
||||
v-for="alert in deviceAlerts"
|
||||
:key="alert.id"
|
||||
xs12
|
||||
md6
|
||||
lg6
|
||||
outlined
|
||||
text
|
||||
color="primary"
|
||||
icon="mdi-vuetify"
|
||||
border="left"
|
||||
<div v-if="deviceAlerts.length" class="pb-5">
|
||||
<v-alert
|
||||
dismissible
|
||||
@input="removeAlert(alert)"
|
||||
v-for="alert in deviceAlerts"
|
||||
:key="alert.id"
|
||||
xs12
|
||||
md6
|
||||
lg6
|
||||
outlined
|
||||
text
|
||||
color="primary"
|
||||
icon="mdi-vuetify"
|
||||
border="left"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-icon class="white--text mr-3" size="sm" color="#a9afbb">{{
|
||||
getAlertIcon(alert)
|
||||
}}</v-icon>
|
||||
</template>
|
||||
<div class="caption">{{ alert.title }}</div>
|
||||
<div
|
||||
v-linkified:options="{ className: 'alert-link' }"
|
||||
v-html="alert.message"
|
||||
style="color: white"
|
||||
></div>
|
||||
</v-alert>
|
||||
</div>
|
||||
|
||||
<v-card raised>
|
||||
<v-card-title class="orange-gradient subtitle-1 font-weight-light">
|
||||
{{ name || "No Device Name" }}
|
||||
<v-layout
|
||||
row
|
||||
justify-end
|
||||
align-center
|
||||
v-if="cardHeaderInterfaces.length"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-icon
|
||||
class="white--text mr-3"
|
||||
size="sm"
|
||||
color="#a9afbb"
|
||||
>{{ getAlertIcon(alert) }}</v-icon>
|
||||
</template>
|
||||
<div class="caption">{{ alert.title }}</div>
|
||||
<div
|
||||
v-linkified:options="{ className: 'alert-link' }"
|
||||
v-html="alert.message"
|
||||
style="color: white"
|
||||
></div>
|
||||
</v-alert>
|
||||
</div>
|
||||
|
||||
<v-card raised >
|
||||
<v-card-title
|
||||
class="orange-gradient subtitle-1 font-weight-light"
|
||||
>
|
||||
{{ name || "No Device Name" }}
|
||||
<v-layout row justify-end align-center v-if="cardHeaderInterfaces.length">
|
||||
<component
|
||||
:value="deviceState"
|
||||
:device="device"
|
||||
:is="iface"
|
||||
v-for="iface in cardHeaderInterfaces"
|
||||
:key="iface"
|
||||
></component>
|
||||
</v-layout>
|
||||
</v-card-title>
|
||||
|
||||
<v-flex v-if="cardButtonInterfaces.length">
|
||||
<v-layout align-center justify-center>
|
||||
<component
|
||||
v-for="iface in cardButtonInterfaces"
|
||||
:key="iface"
|
||||
:value="deviceState"
|
||||
:device="device"
|
||||
:is="iface"
|
||||
></component>
|
||||
</v-layout>
|
||||
</v-flex>
|
||||
|
||||
<v-form>
|
||||
<v-container>
|
||||
<v-layout>
|
||||
<v-flex xs12>
|
||||
<v-text-field
|
||||
v-model="name"
|
||||
label="Name"
|
||||
required
|
||||
></v-text-field>
|
||||
<v-select
|
||||
v-if="inferredTypes.length > 1"
|
||||
:items="inferredTypes"
|
||||
label="Type"
|
||||
outlined
|
||||
v-model="type"
|
||||
></v-select>
|
||||
<v-combobox
|
||||
v-if="
|
||||
hasFixedPhysicalLocation(type, deviceState.interfaces)
|
||||
"
|
||||
:items="existingRooms"
|
||||
outlined
|
||||
v-model="room"
|
||||
label="Room"
|
||||
required
|
||||
></v-combobox>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-container>
|
||||
</v-form>
|
||||
|
||||
<v-card-actions>
|
||||
<component
|
||||
v-for="iface in cardActionInterfaces"
|
||||
:value="deviceState"
|
||||
:device="device"
|
||||
:is="iface"
|
||||
v-for="iface in cardHeaderInterfaces"
|
||||
:key="iface"
|
||||
></component>
|
||||
</v-layout>
|
||||
</v-card-title>
|
||||
|
||||
<v-flex v-if="cardButtonInterfaces.length">
|
||||
<v-layout align-center justify-center>
|
||||
<component
|
||||
v-for="iface in cardButtonInterfaces"
|
||||
:key="iface"
|
||||
:value="deviceState"
|
||||
:device="device"
|
||||
:is="iface"
|
||||
></component>
|
||||
<v-spacer></v-spacer>
|
||||
</v-layout>
|
||||
</v-flex>
|
||||
|
||||
<v-btn color="info" text @click="openLogs" v-if="!loading"
|
||||
>Logs</v-btn
|
||||
>
|
||||
<v-form>
|
||||
<v-container>
|
||||
<v-layout>
|
||||
<v-flex xs12>
|
||||
<v-text-field
|
||||
v-model="name"
|
||||
label="Name"
|
||||
required
|
||||
></v-text-field>
|
||||
<v-select
|
||||
v-if="inferredTypes.length > 1"
|
||||
:items="inferredTypes"
|
||||
label="Type"
|
||||
outlined
|
||||
v-model="type"
|
||||
></v-select>
|
||||
<v-combobox
|
||||
v-if="
|
||||
hasFixedPhysicalLocation(type, deviceState.interfaces)
|
||||
"
|
||||
:items="existingRooms"
|
||||
outlined
|
||||
v-model="room"
|
||||
label="Room"
|
||||
required
|
||||
></v-combobox>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-container>
|
||||
</v-form>
|
||||
|
||||
<v-dialog v-if="!loading" v-model="showDelete" width="500">
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-btn color="error" text v-on="on">Delete</v-btn>
|
||||
</template>
|
||||
<v-card-actions>
|
||||
<component
|
||||
v-for="iface in cardActionInterfaces"
|
||||
:key="iface"
|
||||
:value="deviceState"
|
||||
:device="device"
|
||||
:is="iface"
|
||||
></component>
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<v-card>
|
||||
<v-card-title
|
||||
style="margin-bottom: 8px"
|
||||
class="red font-weight-light white--text"
|
||||
primary-title
|
||||
>Delete Device</v-card-title
|
||||
<v-btn color="info" text @click="openConsole" v-if="!loading"
|
||||
>Console</v-btn
|
||||
>
|
||||
|
||||
<v-btn color="info" text @click="openRepl" v-if="!loading"
|
||||
>REPL</v-btn
|
||||
>
|
||||
|
||||
<v-btn color="info" text @click="openLogs" v-if="!loading"
|
||||
>Logs</v-btn
|
||||
>
|
||||
|
||||
<v-dialog v-if="!loading" v-model="showDelete" width="500">
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-btn color="error" text v-on="on">Delete</v-btn>
|
||||
</template>
|
||||
|
||||
<v-card>
|
||||
<v-card-title
|
||||
style="margin-bottom: 8px"
|
||||
class="red font-weight-light white--text"
|
||||
primary-title
|
||||
>Delete Device</v-card-title
|
||||
>
|
||||
|
||||
<v-card-text
|
||||
>This will permanently delete the device. It can not be
|
||||
undone.</v-card-text
|
||||
>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="primary" text @click="showDelete = false"
|
||||
>Cancel</v-btn
|
||||
>
|
||||
|
||||
<v-card-text
|
||||
>This will permanently delete the device. It can not be
|
||||
undone.</v-card-text
|
||||
<v-btn color="red" text @click="remove"
|
||||
>Delete Device</v-btn
|
||||
>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="primary" text @click="showDelete = false"
|
||||
>Cancel</v-btn
|
||||
>
|
||||
<v-btn color="red" text @click="remove"
|
||||
>Delete Device</v-btn
|
||||
>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<v-btn color="primary" v-if="!loading" text @click="save"
|
||||
>Save</v-btn
|
||||
>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
<v-alert
|
||||
outlined
|
||||
v-model="showSave"
|
||||
dismissible
|
||||
close-text="Close Alert"
|
||||
type="success"
|
||||
>Saved.</v-alert
|
||||
>
|
||||
<v-alert
|
||||
outlined
|
||||
v-model="showSaveError"
|
||||
dismissible
|
||||
close-text="Close Alert"
|
||||
type="success"
|
||||
>There was an error while saving. Please check the logs.</v-alert
|
||||
>
|
||||
</v-flex>
|
||||
<v-btn color="primary" v-if="!loading" text @click="save"
|
||||
>Save</v-btn
|
||||
>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
<v-alert
|
||||
outlined
|
||||
v-model="showSave"
|
||||
dismissible
|
||||
close-text="Close Alert"
|
||||
type="success"
|
||||
>Saved.</v-alert
|
||||
>
|
||||
<v-alert
|
||||
outlined
|
||||
v-model="showSaveError"
|
||||
dismissible
|
||||
close-text="Close Alert"
|
||||
type="success"
|
||||
>There was an error while saving. Please check the logs.</v-alert
|
||||
>
|
||||
</v-flex>
|
||||
|
||||
<v-flex xs12 v-if="!ownerDevice && pluginData">
|
||||
<v-flex>
|
||||
<v-card raised >
|
||||
<v-card-title
|
||||
class="green-gradient subtitle-1 text--white font-weight-light"
|
||||
<v-card raised>
|
||||
<v-card-title
|
||||
class="green-gradient subtitle-1 text--white font-weight-light"
|
||||
>
|
||||
<font-awesome-icon size="sm" icon="database" />
|
||||
Plugin Management
|
||||
</v-card-title>
|
||||
<v-card-text></v-card-text>
|
||||
<v-container>
|
||||
<v-layout>
|
||||
<v-flex>
|
||||
<v-btn outlined color="blue" @click="reloadPlugin"
|
||||
>Reload Plugin</v-btn
|
||||
>
|
||||
</v-flex>
|
||||
</v-layout></v-container
|
||||
>
|
||||
<v-card-actions>
|
||||
<v-btn text color="primary" @click="showStorage = !showStorage"
|
||||
>Storage</v-btn
|
||||
>
|
||||
<font-awesome-icon size="sm" icon="database" />
|
||||
Plugin Management
|
||||
</v-card-title>
|
||||
<v-card-text></v-card-text>
|
||||
<v-container>
|
||||
<v-layout>
|
||||
<v-flex>
|
||||
<v-btn outlined color="blue" @click="reloadPlugin"
|
||||
>Reload Plugin</v-btn
|
||||
>
|
||||
</v-flex>
|
||||
</v-layout></v-container
|
||||
>
|
||||
<v-card-actions>
|
||||
<v-btn text color="primary" @click="showStorage = !showStorage"
|
||||
>Storage</v-btn
|
||||
>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
v-if="!pluginData.updateAvailable"
|
||||
text
|
||||
color="blue"
|
||||
@click="openNpm"
|
||||
xs4
|
||||
>{{ pluginData.packageJson.name }}@{{
|
||||
pluginData.packageJson.version
|
||||
}}</v-btn
|
||||
>
|
||||
<v-btn v-else color="orange" @click="doInstall" dark
|
||||
>Install Update {{ pluginData.updateAvailable }}</v-btn
|
||||
>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-flex>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
v-if="!pluginData.updateAvailable"
|
||||
text
|
||||
color="blue"
|
||||
@click="openNpm"
|
||||
xs4
|
||||
>{{ pluginData.packageJson.name }}@{{
|
||||
pluginData.packageJson.version
|
||||
}}</v-btn
|
||||
>
|
||||
<v-btn v-else color="orange" @click="doInstall" dark
|
||||
>Install Update {{ pluginData.updateAvailable }}</v-btn
|
||||
>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-flex>
|
||||
|
||||
<v-flex xs12 v-if="deviceComponent">
|
||||
@@ -217,113 +222,98 @@
|
||||
</v-flex>
|
||||
|
||||
<v-flex xs12 v-if="ownerDevice && pluginData">
|
||||
<v-flex>
|
||||
<v-card raised >
|
||||
<v-card-title
|
||||
class="green-gradient subtitle-1 text--white font-weight-light"
|
||||
<v-card raised>
|
||||
<v-card-title
|
||||
class="green-gradient subtitle-1 text--white font-weight-light"
|
||||
>
|
||||
<font-awesome-icon size="sm" icon="server" />
|
||||
Managed Device
|
||||
</v-card-title>
|
||||
<v-card-text></v-card-text>
|
||||
<v-card-text>
|
||||
<b>Native ID:</b>
|
||||
{{ pluginData.nativeId }}
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn text color="primary" @click="showStorage = !showStorage"
|
||||
>Storage</v-btn
|
||||
>
|
||||
<font-awesome-icon size="sm" icon="server" />
|
||||
Managed Device
|
||||
</v-card-title>
|
||||
<v-card-text></v-card-text>
|
||||
<v-card-text>
|
||||
<b>Native ID:</b>
|
||||
{{ pluginData.nativeId }}
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn text color="primary" @click="showStorage = !showStorage"
|
||||
>Storage</v-btn
|
||||
>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn text color="blue" :to="`/device/${ownerDevice.id}`">{{
|
||||
ownerDevice.name
|
||||
}}</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-flex>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn text color="blue" :to="`/device/${ownerDevice.id}`">{{
|
||||
ownerDevice.name
|
||||
}}</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-flex>
|
||||
|
||||
<v-flex xs12 v-if="availableMixins.length">
|
||||
<v-flex>
|
||||
<v-card raised >
|
||||
<v-card-title
|
||||
class="green-gradient subtitle-1 text--white font-weight-light"
|
||||
<v-card raised>
|
||||
<v-card-title
|
||||
class="green-gradient subtitle-1 text--white font-weight-light"
|
||||
>
|
||||
<font-awesome-icon size="sm" icon="puzzle-piece" />
|
||||
Integrations and Extensions
|
||||
</v-card-title>
|
||||
|
||||
<v-list-item-group>
|
||||
<v-list-item
|
||||
@click="
|
||||
mixin.enabled = !mixin.enabled;
|
||||
toggleMixin(mixin);
|
||||
"
|
||||
v-for="mixin in availableMixins"
|
||||
:key="mixin.id"
|
||||
inactive
|
||||
>
|
||||
<font-awesome-icon size="sm" icon="puzzle-piece" />
|
||||
Integrations and Extensions
|
||||
</v-card-title>
|
||||
<v-list-item-action>
|
||||
<v-checkbox
|
||||
@click.stop
|
||||
@change="toggleMixin(mixin)"
|
||||
v-model="mixin.enabled"
|
||||
color="primary"
|
||||
></v-checkbox>
|
||||
</v-list-item-action>
|
||||
|
||||
<v-list-item-group>
|
||||
<v-list-item
|
||||
@click="
|
||||
mixin.enabled = !mixin.enabled;
|
||||
toggleMixin(mixin);
|
||||
"
|
||||
v-for="mixin in availableMixins"
|
||||
:key="mixin.id"
|
||||
inactive
|
||||
>
|
||||
<v-list-item-action>
|
||||
<v-checkbox
|
||||
@click.stop
|
||||
@change="toggleMixin(mixin)"
|
||||
v-model="mixin.enabled"
|
||||
color="primary"
|
||||
></v-checkbox>
|
||||
</v-list-item-action>
|
||||
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>{{ mixin.name }}</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list-item-group>
|
||||
</v-card>
|
||||
</v-flex>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>{{ mixin.name }}</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list-item-group>
|
||||
</v-card>
|
||||
</v-flex>
|
||||
|
||||
<v-flex xs12 v-if="showStorage">
|
||||
<v-flex>
|
||||
<v-card raised >
|
||||
<v-card-title
|
||||
class="green-gradient subtitle-1 text--white font-weight-light"
|
||||
>Storage</v-card-title
|
||||
>
|
||||
<v-form>
|
||||
<v-container>
|
||||
<v-layout>
|
||||
<v-flex xs12>
|
||||
<Storage
|
||||
v-model="pluginData.storage"
|
||||
@input="onChange"
|
||||
></Storage>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-container>
|
||||
</v-form>
|
||||
</v-card>
|
||||
</v-flex>
|
||||
<v-card raised>
|
||||
<v-card-title
|
||||
class="green-gradient subtitle-1 text--white font-weight-light"
|
||||
>Storage</v-card-title
|
||||
>
|
||||
<v-form>
|
||||
<v-container>
|
||||
<v-layout>
|
||||
<v-flex xs12>
|
||||
<Storage
|
||||
v-model="pluginData.storage"
|
||||
@input="onChange"
|
||||
></Storage>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-container>
|
||||
</v-form>
|
||||
</v-card>
|
||||
</v-flex>
|
||||
|
||||
<v-flex xs12>
|
||||
<v-flex>
|
||||
<v-card
|
||||
raised
|
||||
|
||||
v-for="iface in cardUnderInterfaces"
|
||||
:key="iface"
|
||||
>
|
||||
<v-card-title
|
||||
class="orange-gradient subtitle-1 font-weight-light"
|
||||
>
|
||||
{{ iface }}
|
||||
</v-card-title>
|
||||
<component
|
||||
:value="deviceState"
|
||||
:device="device"
|
||||
:is="iface"
|
||||
></component>
|
||||
</v-card>
|
||||
</v-flex>
|
||||
<v-card raised v-for="iface in cardUnderInterfaces" :key="iface">
|
||||
<v-card-title class="orange-gradient subtitle-1 font-weight-light">
|
||||
{{ iface }}
|
||||
</v-card-title>
|
||||
<component
|
||||
:value="deviceState"
|
||||
:device="device"
|
||||
:is="iface"
|
||||
></component>
|
||||
</v-card>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-flex>
|
||||
@@ -331,18 +321,15 @@
|
||||
<v-flex xs12 md6 lg6>
|
||||
<v-layout row wrap>
|
||||
<v-flex xs12 v-for="iface in noCardInterfaces" :key="iface">
|
||||
<v-flex v-if="name != null">
|
||||
<component
|
||||
<component v-if="name != null"
|
||||
:value="deviceState"
|
||||
:device="device"
|
||||
:is="iface"
|
||||
></component>
|
||||
</v-flex>
|
||||
</v-flex>
|
||||
|
||||
<v-flex xs12 v-for="iface in cardInterfaces" :key="iface">
|
||||
<v-flex v-if="name != null">
|
||||
<v-card>
|
||||
<v-card v-if="name != null">
|
||||
<v-card-title
|
||||
class="red-gradient white--text subtitle-1 font-weight-light"
|
||||
>{{ iface }}</v-card-title
|
||||
@@ -353,15 +340,18 @@
|
||||
:is="iface"
|
||||
></component>
|
||||
</v-card>
|
||||
</v-flex>
|
||||
</v-flex>
|
||||
|
||||
<v-flex ref="logsEl">
|
||||
<LogCard
|
||||
v-if="showLogs"
|
||||
:rows="15"
|
||||
:logRoute="`/device/${id}/`"
|
||||
></LogCard>
|
||||
<v-flex v-if="showLogs" ref="logsEl">
|
||||
<LogCard :rows="15" :logRoute="`/device/${id}/`"></LogCard>
|
||||
</v-flex>
|
||||
|
||||
<v-flex xs12 v-if="showConsole" ref="consoleEl">
|
||||
<ConsoleCard :deviceId="id"></ConsoleCard>
|
||||
</v-flex>
|
||||
|
||||
<v-flex xs12 v-if="showRepl" ref="replEl">
|
||||
<REPLCard :deviceId="id"></REPLCard>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-flex>
|
||||
@@ -372,6 +362,8 @@ import VueSlider from "vue-slider-component";
|
||||
import "vue-slider-component/theme/material.css";
|
||||
|
||||
import LogCard from "./builtin/LogCard.vue";
|
||||
import ConsoleCard from "./ConsoleCard.vue";
|
||||
import REPLCard from "./REPLCard.vue";
|
||||
import {
|
||||
inferTypesFromInterfaces,
|
||||
getComponentWebPath,
|
||||
@@ -411,6 +403,7 @@ import Storage from "../common/Storage.vue";
|
||||
import { checkUpdate, installNpm, getNpmPath } from "./script/plugin";
|
||||
import AggregateDevice from "./aggregate/AggregateDevice.vue";
|
||||
import Automation from "./automation/Automation.vue";
|
||||
import Vue from "vue";
|
||||
|
||||
const cardHeaderInterfaces = [
|
||||
ScryptedInterface.OccupancySensor,
|
||||
@@ -424,9 +417,7 @@ const cardHeaderInterfaces = [
|
||||
|
||||
const cardUnderInterfaces = [ScryptedInterface.DeviceProvider];
|
||||
|
||||
const noCardInterfaces = [
|
||||
ScryptedInterface.Settings,
|
||||
]
|
||||
const noCardInterfaces = [ScryptedInterface.Settings];
|
||||
|
||||
const cardInterfaces = [
|
||||
ScryptedInterface.Brightness,
|
||||
@@ -464,7 +455,7 @@ function filterInterfaces(interfaces) {
|
||||
);
|
||||
|
||||
if (ret.includes(ScryptedInterface.Camera)) {
|
||||
ret = ret.filter(iface => iface !== ScryptedInterface.VideoCamera);
|
||||
ret = ret.filter((iface) => iface !== ScryptedInterface.VideoCamera);
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
@@ -505,6 +496,8 @@ export default {
|
||||
|
||||
VueSlider,
|
||||
LogCard,
|
||||
ConsoleCard,
|
||||
REPLCard,
|
||||
|
||||
Storage,
|
||||
|
||||
@@ -544,6 +537,8 @@ export default {
|
||||
initialState() {
|
||||
return {
|
||||
showLogs: false,
|
||||
showConsole: false,
|
||||
showRepl: false,
|
||||
showDelete: false,
|
||||
showSave: false,
|
||||
showSaveError: false,
|
||||
@@ -566,9 +561,24 @@ export default {
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">");
|
||||
},
|
||||
openLogs() {
|
||||
async openConsole() {
|
||||
this.showConsole = !this.showConsole;
|
||||
if (this.showConsole) {
|
||||
await Vue.nextTick();
|
||||
this.$vuetify.goTo(this.$refs.consoleEl);
|
||||
}
|
||||
},
|
||||
async openRepl() {
|
||||
this.showRepl = !this.showRepl;
|
||||
if (this.showRepl) {
|
||||
await Vue.nextTick();
|
||||
this.$vuetify.goTo(this.$refs.replEl);
|
||||
}
|
||||
},
|
||||
async openLogs() {
|
||||
this.showLogs = !this.showLogs;
|
||||
if (this.showLogs) {
|
||||
await Vue.nextTick();
|
||||
this.$vuetify.goTo(this.$refs.logsEl);
|
||||
}
|
||||
},
|
||||
@@ -614,7 +624,7 @@ export default {
|
||||
);
|
||||
this.pluginData = pluginData;
|
||||
checkUpdate(pluginData.pluginId, pluginData.packageJson.version).then(
|
||||
updateAvailable => (pluginData.updateAvailable = updateAvailable)
|
||||
(updateAvailable) => (pluginData.updateAvailable = updateAvailable)
|
||||
);
|
||||
|
||||
const device = this.device;
|
||||
|
||||
48
plugins/core/ui/src/components/REPLCard.vue
Normal file
48
plugins/core/ui/src/components/REPLCard.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<v-card raised>
|
||||
<v-toolbar dark color="blue"> JavaScript REPL </v-toolbar>
|
||||
<div ref="terminal"></div>
|
||||
</v-card>
|
||||
</template>
|
||||
<script>
|
||||
import { Terminal } from "xterm";
|
||||
import { FitAddon } from 'xterm-addon-fit';
|
||||
import eio from "engine.io-client";
|
||||
|
||||
export default {
|
||||
props: ["deviceId"],
|
||||
socket: null,
|
||||
mounted() {
|
||||
const term = new Terminal({
|
||||
convertEol: true,
|
||||
});
|
||||
const fitAddon = new FitAddon();
|
||||
term.loadAddon(fitAddon);
|
||||
term.open(this.$refs.terminal);
|
||||
fitAddon.fit();
|
||||
|
||||
const endpointPath = `/endpoint/@scrypted/core`;
|
||||
|
||||
const options = {
|
||||
path: `${endpointPath}/engine.io/repl/${this.deviceId}`,
|
||||
};
|
||||
const rootLocation = `${window.location.protocol}//${window.location.host}`;
|
||||
this.socket = eio(rootLocation, options);
|
||||
|
||||
this.socket.on('message', data => {
|
||||
term.write(new Uint8Array(data));
|
||||
});
|
||||
|
||||
term.onData(data => {
|
||||
this.socket.send(data);
|
||||
});
|
||||
|
||||
term.onBinary(data => {
|
||||
this.socket.send(data);
|
||||
});
|
||||
},
|
||||
destroyed() {
|
||||
this.socket?.close();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -1,3 +1,4 @@
|
||||
import "xterm/css/xterm.css";
|
||||
import '@fortawesome/fontawesome-free/css/all.css'
|
||||
import Vue from 'vue'
|
||||
import Vuetify, {
|
||||
|
||||
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.4",
|
||||
"version": "0.0.5",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/google-home",
|
||||
"version": "0.0.4",
|
||||
"version": "0.0.5",
|
||||
"dependencies": {
|
||||
"actions-on-google": "^2.13.0",
|
||||
"axios": "^0.21.1",
|
||||
|
||||
@@ -35,5 +35,5 @@
|
||||
"@types/mdns": "^0.0.34",
|
||||
"@types/url-parse": "^1.4.3"
|
||||
},
|
||||
"version": "0.0.4"
|
||||
"version": "0.0.5"
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ class GoogleHome extends ScryptedDeviceBase implements HttpRequestHandler, Engin
|
||||
this.plugins = systemManager.getComponent('plugins');
|
||||
|
||||
this.localEndpoint = new http.Server((req, res) => {
|
||||
this.log.i('got request');
|
||||
this.console.log('got request');
|
||||
res.writeHead(404);
|
||||
res.end();
|
||||
});
|
||||
@@ -91,7 +91,7 @@ class GoogleHome extends ScryptedDeviceBase implements HttpRequestHandler, Engin
|
||||
|
||||
endpointManager.getInsecurePublicLocalEndpoint().then(endpoint => {
|
||||
const url = new URL(endpoint);
|
||||
this.log.i(endpoint);
|
||||
this.console.log(endpoint);
|
||||
const ad = mdns.createAdvertisement(mdns.tcp('scrypted-gh'), parseInt(url.port));
|
||||
ad.start();
|
||||
});
|
||||
@@ -237,7 +237,7 @@ class GoogleHome extends ScryptedDeviceBase implements HttpRequestHandler, Engin
|
||||
for (const queryDevice of input.payload.devices) {
|
||||
const device = systemManager.getDeviceById(queryDevice.id);
|
||||
if (!device) {
|
||||
this.log.e(`query for missing device ${queryDevice.id}`);
|
||||
this.console.error(`query for missing device ${queryDevice.id}`);
|
||||
ret.payload.devices[queryDevice.id] = {
|
||||
online: false,
|
||||
};
|
||||
@@ -247,7 +247,7 @@ class GoogleHome extends ScryptedDeviceBase implements HttpRequestHandler, Engin
|
||||
const { type } = device;
|
||||
const supportedType = supportedTypes[type];
|
||||
if (!supportedType) {
|
||||
this.log.e(`query for unsupported type ${type}`);
|
||||
this.console.error(`query for unsupported type ${type}`);
|
||||
ret.payload.devices[queryDevice.id] = {
|
||||
online: false,
|
||||
};
|
||||
@@ -264,7 +264,7 @@ class GoogleHome extends ScryptedDeviceBase implements HttpRequestHandler, Engin
|
||||
}, status);
|
||||
}
|
||||
catch (e) {
|
||||
this.log.e(`query failure for ${device.name}`);
|
||||
this.console.error(`query failure for ${device.name}`);
|
||||
ret.payload.devices[queryDevice.id] = {
|
||||
status: 'ERROR',
|
||||
online: false,
|
||||
@@ -289,6 +289,7 @@ class GoogleHome extends ScryptedDeviceBase implements HttpRequestHandler, Engin
|
||||
for (const commandDevice of command.devices) {
|
||||
const device = systemManager.getDeviceById(commandDevice.id);
|
||||
if (!device) {
|
||||
this.log.e(`execute failed, device not found ${JSON.stringify(commandDevice)}`);
|
||||
const error: SmartHomeV1ExecuteResponseCommands = {
|
||||
ids: [commandDevice.id],
|
||||
status: 'ERROR',
|
||||
@@ -298,9 +299,12 @@ class GoogleHome extends ScryptedDeviceBase implements HttpRequestHandler, Engin
|
||||
continue;
|
||||
}
|
||||
|
||||
this.log.i(`executing command on ${device.name}`);
|
||||
|
||||
for (const execution of command.execution) {
|
||||
const commandHandler = commandHandlers[execution.command]
|
||||
if (!commandHandler) {
|
||||
this.log.e(`execute failed, command not supported ${JSON.stringify(execution)}`);
|
||||
const error: SmartHomeV1ExecuteResponseCommands = {
|
||||
ids: [commandDevice.id],
|
||||
status: 'ERROR',
|
||||
@@ -315,6 +319,7 @@ class GoogleHome extends ScryptedDeviceBase implements HttpRequestHandler, Engin
|
||||
ret.payload.commands.push(result);
|
||||
}
|
||||
catch (e) {
|
||||
this.log.e(`execution failed ${e}`);
|
||||
const error: SmartHomeV1ExecuteResponseCommands = {
|
||||
ids: [commandDevice.id],
|
||||
status: 'ERROR',
|
||||
@@ -375,12 +380,12 @@ class GoogleHome extends ScryptedDeviceBase implements HttpRequestHandler, Engin
|
||||
if (!Object.keys(report.payload.devices.states).length)
|
||||
return;
|
||||
|
||||
this.log.i('reporting state:');
|
||||
this.log.i(JSON.stringify(report, undefined, 2));
|
||||
this.console.log('reporting state:');
|
||||
this.console.log(JSON.stringify(report, undefined, 2));
|
||||
if (this.app.jwt) {
|
||||
const result = await this.app.reportState(report);
|
||||
this.log.i('report state result:')
|
||||
this.log.i(result);
|
||||
this.console.log('report state result:')
|
||||
this.console.log(result);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -397,8 +402,8 @@ class GoogleHome extends ScryptedDeviceBase implements HttpRequestHandler, Engin
|
||||
Authorization: `Bearer ${token_info}`
|
||||
},
|
||||
});
|
||||
this.log.i('report state result:');
|
||||
this.log.i(JSON.stringify(response.data));
|
||||
this.console.log('report state result:');
|
||||
this.console.log(JSON.stringify(response.data));
|
||||
}
|
||||
|
||||
async requestSync() {
|
||||
@@ -420,8 +425,8 @@ class GoogleHome extends ScryptedDeviceBase implements HttpRequestHandler, Engin
|
||||
Authorization: `Bearer ${token_info}`
|
||||
}
|
||||
});
|
||||
this.log.i('request sync result:');
|
||||
this.log.i(JSON.stringify(response.data));
|
||||
this.console.log('request sync result:');
|
||||
this.console.log(JSON.stringify(response.data));
|
||||
}
|
||||
|
||||
async onRequest(request: HttpRequest, response: HttpResponse): Promise<void> {
|
||||
@@ -432,19 +437,19 @@ class GoogleHome extends ScryptedDeviceBase implements HttpRequestHandler, Engin
|
||||
return;
|
||||
}
|
||||
|
||||
this.log.i(request.body);
|
||||
this.console.log(request.body);
|
||||
const body = JSON.parse(request.body);
|
||||
try {
|
||||
const result = await this.app.handler(body, request.headers as Headers);
|
||||
const res = JSON.stringify(result.body);
|
||||
this.log.i(res);
|
||||
this.console.log(res);
|
||||
response.send(res, {
|
||||
headers: result.headers,
|
||||
code: result.status,
|
||||
});
|
||||
}
|
||||
catch (e) {
|
||||
this.log.e(`request error ${e}`);
|
||||
this.console.error(`request error ${e}`);
|
||||
response.send(e.message, {
|
||||
code: 500,
|
||||
});
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
const { Console } = require('console');
|
||||
const { PassThrough } = require('stream');
|
||||
|
||||
class ScryptedDeviceBase {
|
||||
constructor(nativeId) {
|
||||
this.nativeId = nativeId;
|
||||
@@ -17,6 +20,14 @@ class ScryptedDeviceBase {
|
||||
return this._log;
|
||||
}
|
||||
|
||||
get console() {
|
||||
if (!this._console) {
|
||||
this._console = deviceManager.getDeviceConsole(this.nativeId);
|
||||
}
|
||||
|
||||
return this._console;
|
||||
}
|
||||
|
||||
_lazyLoadDeviceState() {
|
||||
if (!this._deviceState) {
|
||||
if (this.nativeId) {
|
||||
@@ -51,33 +62,33 @@ class MixinDeviceBase {
|
||||
}
|
||||
}
|
||||
|
||||
(function() {
|
||||
function _createGetState(state) {
|
||||
return function() {
|
||||
this._lazyLoadDeviceState();
|
||||
return this._deviceState[state];
|
||||
};
|
||||
}
|
||||
(function () {
|
||||
function _createGetState(state) {
|
||||
return function () {
|
||||
this._lazyLoadDeviceState();
|
||||
return this._deviceState[state];
|
||||
};
|
||||
}
|
||||
|
||||
function _createSetState(state) {
|
||||
return function(value) {
|
||||
this._lazyLoadDeviceState();
|
||||
this._deviceState[state] = value;
|
||||
};
|
||||
}
|
||||
function _createSetState(state) {
|
||||
return function (value) {
|
||||
this._lazyLoadDeviceState();
|
||||
this._deviceState[state] = value;
|
||||
};
|
||||
}
|
||||
|
||||
var fields = ["id","interfaces","metadata","name","providedInterfaces","providedName","providedRoom","providedType","providerId","room","type","on","brightness","colorTemperature","rgb","hsv","running","paused","docked","temperature","temperatureUnit","humidity","thermostatAvailableModes","thermostatMode","thermostatSetpoint","thermostatSetpointHigh","thermostatSetpointLow","lockState","entryOpen","batteryLevel","online","updateAvailable","fromMimeType","toMimeType","binaryState","intrusionDetected","powerDetected","motionDetected","occupied","flooded","ultraviolet","luminance","position",
|
||||
];
|
||||
for (var field of fields) {
|
||||
Object.defineProperty(ScryptedDeviceBase.prototype, field, {
|
||||
set: _createSetState(field),
|
||||
get: _createGetState(field),
|
||||
});
|
||||
Object.defineProperty(MixinDeviceBase.prototype, field, {
|
||||
set: _createSetState(field),
|
||||
get: _createGetState(field),
|
||||
});
|
||||
}
|
||||
var fields = ["id", "interfaces", "metadata", "name", "providedInterfaces", "providedName", "providedRoom", "providedType", "providerId", "room", "type", "on", "brightness", "colorTemperature", "rgb", "hsv", "running", "paused", "docked", "temperature", "temperatureUnit", "humidity", "thermostatAvailableModes", "thermostatMode", "thermostatSetpoint", "thermostatSetpointHigh", "thermostatSetpointLow", "lockState", "entryOpen", "batteryLevel", "online", "updateAvailable", "fromMimeType", "toMimeType", "binaryState", "intrusionDetected", "powerDetected", "motionDetected", "occupied", "flooded", "ultraviolet", "luminance", "position",
|
||||
];
|
||||
for (var field of fields) {
|
||||
Object.defineProperty(ScryptedDeviceBase.prototype, field, {
|
||||
set: _createSetState(field),
|
||||
get: _createGetState(field),
|
||||
});
|
||||
Object.defineProperty(MixinDeviceBase.prototype, field, {
|
||||
set: _createSetState(field),
|
||||
get: _createGetState(field),
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
|
||||
|
||||
39
sdk/index.js
39
sdk/index.js
@@ -1,48 +1,13 @@
|
||||
var sdk = require('./index.generated.js');
|
||||
try {
|
||||
var mediaManagerProxy;
|
||||
try {
|
||||
mediaManagerProxy = mediaManager;
|
||||
}
|
||||
catch (e) {
|
||||
var mediaManagerApply = function(target, prop, argumentsList) {
|
||||
var copy = [];
|
||||
if (argumentsList) {
|
||||
for (var i in argumentsList) {
|
||||
copy.push(NativeBuffer.from(argumentsList[i]));
|
||||
}
|
||||
}
|
||||
var ret = mediaManager[prop].apply(mediaManager, copy);
|
||||
var p = global['Promise'];
|
||||
if (!p || (!prop.startsWith('convert'))) {
|
||||
return ret;
|
||||
}
|
||||
// convert the promise to the globally available Promise.
|
||||
return new p((resolve, reject) => {
|
||||
// todo: dont use native buffer as a return value
|
||||
ret.then(r => NativeBuffer.toBuffer(resolve(r)))
|
||||
.catch(e => reject(e));
|
||||
});
|
||||
};
|
||||
|
||||
mediaManagerProxy = new Proxy(function(){}, {
|
||||
get: function(target, prop) {
|
||||
return function() {
|
||||
return mediaManagerApply(target, prop, arguments)
|
||||
}
|
||||
},
|
||||
apply: mediaManagerApply,
|
||||
})
|
||||
}
|
||||
|
||||
sdk = Object.assign(sdk, {
|
||||
log,
|
||||
|
||||
android,
|
||||
deviceManager,
|
||||
endpointManager,
|
||||
mediaManager: mediaManagerProxy,
|
||||
mediaManager,
|
||||
systemManager,
|
||||
pluginHostAPI,
|
||||
});
|
||||
}
|
||||
catch (e) {
|
||||
|
||||
4
sdk/package-lock.json
generated
4
sdk/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.0.64",
|
||||
"version": "0.0.67",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.0.64",
|
||||
"version": "0.0.67",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.2.2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.0.65",
|
||||
"version": "0.0.67",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
39
sdk/types.d.ts
vendored
39
sdk/types.d.ts
vendored
@@ -1,4 +1,3 @@
|
||||
|
||||
/**
|
||||
* DeviceState is returned by DeviceManager.getDeviceState, and allows getting/setting of a device provided by a DeviceProvider.
|
||||
*/
|
||||
@@ -662,14 +661,14 @@ export interface DeviceManager {
|
||||
getDeviceLogger(nativeId: string): Logger;
|
||||
|
||||
/**
|
||||
* Get the device state maintained by Scrypted. Setting properties on this state will update the state in Scrypted.
|
||||
* Get the console for the device given a native id.
|
||||
*/
|
||||
getDeviceState(): DeviceState;
|
||||
getDeviceConsole?(nativeId?: string): Console;
|
||||
|
||||
/**
|
||||
* Get the device state maintained by Scrypted. Setting properties on this state will update the state in Scrypted.
|
||||
*/
|
||||
getDeviceState(nativeId: string): DeviceState;
|
||||
getDeviceState(nativeId?: string): DeviceState;
|
||||
|
||||
/**
|
||||
* Get the per script Storage object.
|
||||
@@ -835,22 +834,6 @@ export interface SystemManager {
|
||||
*/
|
||||
removeDevice(id: string): Promise<void>;
|
||||
|
||||
}
|
||||
/**
|
||||
* Android provides limited access to the Android system, to send Intents to other applications, such as Tasker. See Android SDK documentation for more information.
|
||||
*/
|
||||
export interface Android {
|
||||
/**
|
||||
* Create a new Intent. Use one of the send methods to send broadcasts, start activities, or start services.
|
||||
*/
|
||||
newIntent(): Intent;
|
||||
|
||||
sendBroadcast(intent: Intent): Promise<void>;
|
||||
|
||||
startActivity(intent: Intent): Promise<void>;
|
||||
|
||||
startService(intent: Intent): Promise<void>;
|
||||
|
||||
}
|
||||
/**
|
||||
* MixinProviders can add and intercept interfaces to other devices to add or augment their behavior.
|
||||
@@ -996,7 +979,6 @@ export enum ScryptedInterface {
|
||||
MediaSource = "MediaSource",
|
||||
MessagingEndpoint = "MessagingEndpoint",
|
||||
OauthClient = "OauthClient",
|
||||
Android = "Android",
|
||||
MixinProvider = "MixinProvider",
|
||||
HttpRequestHandler = "HttpRequestHandler",
|
||||
EngineIOHandler = "EngineIOHandler",
|
||||
@@ -1066,16 +1048,6 @@ export enum ScryptedMimeTypes {
|
||||
RTCAVAnswer = 'x-scrypted/x-rtc-av-answer',
|
||||
}
|
||||
|
||||
// export interface ZwaveManagerDevice extends ZwaveManager, ScryptedDevice {
|
||||
// }
|
||||
|
||||
/**
|
||||
* Android Intent.
|
||||
* See: https://developer.android.com/reference/android/content/Intent
|
||||
*/
|
||||
interface Intent {
|
||||
}
|
||||
|
||||
export interface ScryptedInterfaceDescriptor {
|
||||
name: string;
|
||||
properties: ScryptedInterfaceProperty[];
|
||||
@@ -1092,14 +1064,13 @@ export interface ScryptedStatic {
|
||||
* @deprecated
|
||||
*/
|
||||
log?: Logger,
|
||||
scriptSettings?: Settings,
|
||||
|
||||
android?: Android,
|
||||
deviceManager?: DeviceManager,
|
||||
endpointManager?: EndpointManager,
|
||||
mediaManager?: MediaManager,
|
||||
systemManager: SystemManager,
|
||||
// zwaveManager?: ZwaveManagerDevice,
|
||||
|
||||
pluginHostAPI?: any;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -104,7 +104,6 @@ module.exports.ScryptedInterface = {
|
||||
MediaSource: "MediaSource",
|
||||
MessagingEndpoint: "MessagingEndpoint",
|
||||
OauthClient: "OauthClient",
|
||||
Android: "Android",
|
||||
MixinProvider: "MixinProvider",
|
||||
HttpRequestHandler: "HttpRequestHandler",
|
||||
EngineIOHandler: "EngineIOHandler",
|
||||
@@ -501,17 +500,6 @@ module.exports.ScryptedInterfaceDescriptors = {
|
||||
"onOauthCallback",
|
||||
]
|
||||
},
|
||||
Android: {
|
||||
name: "Android",
|
||||
properties: [
|
||||
],
|
||||
methods: [
|
||||
"newIntent",
|
||||
"sendBroadcast",
|
||||
"startActivity",
|
||||
"startService",
|
||||
]
|
||||
},
|
||||
MixinProvider: {
|
||||
name: "MixinProvider",
|
||||
properties: [
|
||||
|
||||
@@ -75,4 +75,8 @@ export class PluginComponent {
|
||||
id: this.scrypted.findPluginDevice(pluginId),
|
||||
}
|
||||
}
|
||||
|
||||
async getRemoteServicePort(pluginId: string, name: string): Promise<number> {
|
||||
return this.scrypted.plugins[pluginId].remote.getServicePort(name);
|
||||
}
|
||||
}
|
||||
15
server/src/plugin/cluster-helper.ts
Normal file
15
server/src/plugin/cluster-helper.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import net from 'net';
|
||||
import { once } from 'events';
|
||||
|
||||
export async function listenZeroCluster(server: net.Server) {
|
||||
while (true) {
|
||||
const port = 10000 + Math.round(Math.random() * 30000);
|
||||
server.listen(port);
|
||||
try {
|
||||
await once(server, 'listening');
|
||||
return (server.address() as net.AddressInfo).port;
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,7 @@ import { once } from 'events';
|
||||
import fs from 'fs';
|
||||
import tmp from 'tmp';
|
||||
import net from 'net';
|
||||
import { sleep } from "../sleep";
|
||||
import { AddressInfo } from "ws";
|
||||
import { listenZeroCluster } from "./cluster-helper";
|
||||
|
||||
const wrtc = require('wrtc');
|
||||
Object.assign(global, wrtc);
|
||||
@@ -22,18 +21,6 @@ interface RTCSession {
|
||||
|
||||
const rtcSessions: { [id: string]: RTCSession } = {};
|
||||
|
||||
async function listenZeroCluster(server: net.Server) {
|
||||
while (true) {
|
||||
const port = 10000 + Math.round(Math.random() * 30000);
|
||||
server.listen(port);
|
||||
try {
|
||||
await once(server, 'listening');
|
||||
return (server.address() as AddressInfo).port;
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addBuiltins(converters: BufferConverter[]) {
|
||||
converters.push({
|
||||
|
||||
@@ -43,6 +43,8 @@ export interface PluginRemote {
|
||||
ioEvent(id: string, event: string, message?: any): Promise<void>;
|
||||
|
||||
createDeviceState(id: string, setState: (property: string, value: any) => Promise<void>): Promise<any>;
|
||||
|
||||
getServicePort(name: string): Promise<number>;
|
||||
}
|
||||
|
||||
export interface MediaObjectRemote extends MediaObject {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import cluster from 'cluster';
|
||||
import { RpcMessage, RpcPeer } from '../rpc';
|
||||
import AdmZip from 'adm-zip';
|
||||
import { ScryptedDevice, Device, DeviceManifest, EventDetails, EventListenerOptions, EventListenerRegister, EngineIOHandler, ScryptedInterfaceProperty, MediaManager, SystemDeviceState } from '@scrypted/sdk/types'
|
||||
import { ScryptedDevice, Device, DeviceManifest, EventDetails, EventListenerOptions, EventListenerRegister, EngineIOHandler, ScryptedInterfaceProperty, MediaManager, SystemDeviceState, ScryptedStatic } from '@scrypted/sdk/types'
|
||||
import { ScryptedRuntime } from '../runtime';
|
||||
import { Plugin } from '../db-types';
|
||||
import io from 'engine.io';
|
||||
@@ -10,7 +10,14 @@ import { PluginAPI, PluginRemote } from './plugin-api';
|
||||
import { Logger } from '../logger';
|
||||
import { MediaManagerImpl } from './media';
|
||||
import { getState } from '../state';
|
||||
import WebSocket from 'ws';
|
||||
import WebSocket, { EventEmitter } from 'ws';
|
||||
import { listenZeroCluster } from './cluster-helper';
|
||||
import { Server } from 'net';
|
||||
import repl, { REPLServer } from 'repl';
|
||||
import { once } from 'events';
|
||||
import { PassThrough } from 'stream';
|
||||
import { Console } from 'console'
|
||||
import util from 'util';
|
||||
|
||||
export class PluginHost {
|
||||
worker: cluster.Worker;
|
||||
@@ -58,6 +65,10 @@ export class PluginHost {
|
||||
|
||||
if (true) {
|
||||
this.worker = cluster.fork();
|
||||
this.worker.process.stdout.on('data', data => {
|
||||
process.stdout.write(data);
|
||||
});
|
||||
this.worker.process.stderr.on('data', data => process.stderr.write(data));
|
||||
|
||||
let connected = true;
|
||||
this.worker.on('disconnect', () => {
|
||||
@@ -107,7 +118,9 @@ export class PluginHost {
|
||||
}
|
||||
});
|
||||
|
||||
attachPluginRemote(remote, async (systemManager) => new MediaManagerImpl(systemManager));
|
||||
attachPluginRemote(remote, {
|
||||
createMediaManager: async (systemManager) => new MediaManagerImpl(systemManager),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -151,6 +164,7 @@ export class PluginHost {
|
||||
const device = scrypted.findPluginDevice(plugin._id, nativeId);
|
||||
return self.scrypted.getDeviceLogger(device);
|
||||
}
|
||||
|
||||
getComponent(id: string): Promise<any> {
|
||||
return self.scrypted.getComponent(id);
|
||||
}
|
||||
@@ -283,7 +297,149 @@ export class PluginHost {
|
||||
}
|
||||
}
|
||||
|
||||
export async function startPluginCluster() {
|
||||
async function createConsoleServer(events: EventEmitter): Promise<number> {
|
||||
const outputs = new Map<string, Buffer[]>();
|
||||
const appendOutput = (data: Buffer, nativeId: string) => {
|
||||
if (!nativeId)
|
||||
nativeId = undefined;
|
||||
let buffers = outputs.get(nativeId);
|
||||
if (!buffers) {
|
||||
buffers = [];
|
||||
outputs.set(nativeId, buffers);
|
||||
}
|
||||
buffers.push(data);
|
||||
};
|
||||
events.on('stdout', appendOutput);
|
||||
events.on('stderr', appendOutput);
|
||||
|
||||
const server = new Server(async (socket) => {
|
||||
let [filter] = await once(socket, 'data');
|
||||
filter = filter.toString().trim();
|
||||
if (filter === 'undefined')
|
||||
filter = undefined;
|
||||
|
||||
const buffers = outputs.get(filter);
|
||||
if (buffers) {
|
||||
const concat = Buffer.concat(buffers);
|
||||
outputs.set(filter, [concat]);
|
||||
socket.write(concat);
|
||||
}
|
||||
|
||||
const cb = (data: Buffer, nativeId: string) => {
|
||||
if (nativeId !== filter)
|
||||
return;
|
||||
socket.write(data);
|
||||
};
|
||||
events.on('stdout', cb)
|
||||
events.on('stderr', cb)
|
||||
|
||||
const cleanup = () => {
|
||||
events.removeListener('stdout', cb);
|
||||
events.removeListener('stderr', cb);
|
||||
};
|
||||
|
||||
socket.on('close', cleanup);
|
||||
socket.on('error', cleanup);
|
||||
socket.on('end', cleanup);
|
||||
});
|
||||
return listenZeroCluster(server);
|
||||
}
|
||||
|
||||
async function createREPLServer(events: EventEmitter): Promise<number> {
|
||||
const [[scrypted], [params], [plugin]] = await Promise.all([once(events, 'scrypted'), once(events, 'params'), once(events, 'plugin')]);
|
||||
const { deviceManager, systemManager } = scrypted;
|
||||
const server = new Server(async (socket) => {
|
||||
let [filter] = await once(socket, 'data');
|
||||
filter = filter.toString().trim();
|
||||
if (filter === 'undefined')
|
||||
filter = undefined;
|
||||
|
||||
const chain: string[] = [];
|
||||
const nativeIds: Map<string, any> = deviceManager.nativeIds;
|
||||
const reversed = new Map<string, string>();
|
||||
for (const nativeId of nativeIds.keys()) {
|
||||
reversed.set(nativeIds.get(nativeId).id, nativeId);
|
||||
}
|
||||
|
||||
while (filter) {
|
||||
const { id } = nativeIds.get(filter);
|
||||
const d = await systemManager.getDeviceById(id);
|
||||
chain.push(filter);
|
||||
filter = reversed.get(d.providerId);
|
||||
}
|
||||
|
||||
chain.reverse();
|
||||
let device = plugin;
|
||||
for (const c of chain) {
|
||||
device = await device.getDevice(c);
|
||||
}
|
||||
|
||||
const r = repl.start({
|
||||
terminal: true,
|
||||
input: socket,
|
||||
output: socket,
|
||||
// writer(this: REPLServer, obj: any) {
|
||||
// const ret = util.inspect(obj, {
|
||||
// colors: true,
|
||||
// });
|
||||
// return ret;//.replaceAll('\n', '\r\n');
|
||||
// },
|
||||
preview: false,
|
||||
});
|
||||
|
||||
const ctx = Object.assign(params, {
|
||||
device
|
||||
});
|
||||
delete ctx.console;
|
||||
Object.assign(r.context, ctx);
|
||||
|
||||
const cleanup = () => {
|
||||
r.close();
|
||||
};
|
||||
|
||||
socket.on('close', cleanup);
|
||||
socket.on('error', cleanup);
|
||||
socket.on('end', cleanup);
|
||||
});
|
||||
return listenZeroCluster(server);
|
||||
}
|
||||
|
||||
export function startPluginClusterWorker() {
|
||||
const events = new EventEmitter();
|
||||
|
||||
const getDeviceConsole = (nativeId?: string) => {
|
||||
const stdout = new PassThrough();
|
||||
const stderr = new PassThrough();
|
||||
|
||||
stdout.on('data', data => events.emit('stdout', data, nativeId));
|
||||
stderr.on('data', data => this.events.emit('stderr', data, nativeId));
|
||||
|
||||
const ret = new Console(stdout, stderr);
|
||||
|
||||
const methods = [
|
||||
'log', 'warn',
|
||||
'dir', 'time',
|
||||
'timeEnd', 'timeLog',
|
||||
'trace', 'assert',
|
||||
'clear', 'count',
|
||||
'countReset', 'group',
|
||||
'groupEnd', 'table',
|
||||
'debug', 'info',
|
||||
'dirxml', 'error',
|
||||
'groupCollapsed',
|
||||
];
|
||||
|
||||
for (const m of methods) {
|
||||
const old = (ret as any)[m].bind(ret);
|
||||
(ret as any)[m] = (...args: any[]) => {
|
||||
(console as any)[m](...args);
|
||||
old(...args);
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
const peer = new RpcPeer((message, reject) => process.send(message, undefined, {
|
||||
swallowErrors: !reject,
|
||||
}, e => {
|
||||
@@ -291,16 +447,34 @@ export async function startPluginCluster() {
|
||||
reject(e);
|
||||
}));
|
||||
process.on('message', message => peer.handleMessage(message as RpcMessage));
|
||||
const scrypted = await attachPluginRemote(peer, async (systemManager) => new MediaManagerImpl(systemManager));
|
||||
process.on('uncaughtException', e => {
|
||||
scrypted.log.e('uncaughtException');
|
||||
scrypted.log.e(e.toString());
|
||||
scrypted.log.e(e.stack);
|
||||
});
|
||||
process.on('unhandledRejection', e => {
|
||||
scrypted.log.e('unhandledRejection');
|
||||
scrypted.log.e(e.toString());
|
||||
});
|
||||
|
||||
const consolePort = createConsoleServer(events);
|
||||
const replPort = createREPLServer(events);
|
||||
|
||||
attachPluginRemote(peer, {
|
||||
createMediaManager: async (systemManager) => new MediaManagerImpl(systemManager),
|
||||
events,
|
||||
getDeviceConsole,
|
||||
async getServicePort(name) {
|
||||
if (name === 'repl')
|
||||
return replPort;
|
||||
if (name === 'console')
|
||||
return consolePort;
|
||||
throw new Error(`unknown service ${name}`);
|
||||
}
|
||||
}).then(scrypted => {
|
||||
events.emit('scrypted', scrypted);
|
||||
|
||||
process.on('uncaughtException', e => {
|
||||
scrypted.log.e('uncaughtException');
|
||||
scrypted.log.e(e.toString());
|
||||
scrypted.log.e(e.stack);
|
||||
});
|
||||
process.on('unhandledRejection', e => {
|
||||
scrypted.log.e('unhandledRejection');
|
||||
scrypted.log.e(e.toString());
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
class LazyRemote implements PluginRemote {
|
||||
@@ -331,4 +505,9 @@ class LazyRemote implements PluginRemote {
|
||||
async createDeviceState(id: string, setState: (property: string, value: any) => Promise<void>): Promise<any> {
|
||||
return (await this.init).createDeviceState(id, setState);
|
||||
}
|
||||
|
||||
async getServicePort(name: string): Promise<number> {
|
||||
return (await this.init).getServicePort(name);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -7,13 +7,16 @@ import { PluginAPI, PluginLogger, PluginRemote } from './plugin-api';
|
||||
import { SystemManagerImpl } from './system';
|
||||
import { RpcPeer } from '../rpc';
|
||||
import { BufferSerializer } from './buffer-serializer';
|
||||
import { Console } from 'console';
|
||||
import { EventEmitter, PassThrough } from 'stream';
|
||||
import { Writable } from 'node:stream';
|
||||
|
||||
class DeviceLogger implements Logger {
|
||||
nativeId: string;
|
||||
api: PluginAPI;
|
||||
logger: Promise<PluginLogger>;
|
||||
|
||||
constructor(api: PluginAPI, nativeId: string) {
|
||||
constructor(api: PluginAPI, nativeId: string, public console: any) {
|
||||
this.api = api;
|
||||
this.nativeId = nativeId;
|
||||
}
|
||||
@@ -25,7 +28,7 @@ class DeviceLogger implements Logger {
|
||||
}
|
||||
|
||||
async log(level: string, message: string) {
|
||||
console.log(message);
|
||||
this.console.log(message);
|
||||
(await this.ensureLogger()).log(level, message);
|
||||
}
|
||||
|
||||
@@ -135,12 +138,12 @@ class DeviceManagerImpl implements DeviceManager {
|
||||
nativeIds = new Map<string, DeviceManagerDevice>();
|
||||
systemManager: SystemManagerImpl;
|
||||
|
||||
constructor(systemManager: SystemManagerImpl) {
|
||||
constructor(systemManager: SystemManagerImpl, public events?: EventEmitter, public getDeviceConsole?: (nativeId?: string) => Console) {
|
||||
this.systemManager = systemManager;
|
||||
}
|
||||
|
||||
getDeviceLogger(nativeId?: string): Logger {
|
||||
return new DeviceLogger(this.api, nativeId);
|
||||
return new DeviceLogger(this.api, nativeId, this.getDeviceConsole?.(nativeId) || console);
|
||||
}
|
||||
|
||||
getDeviceState(nativeId?: any): DeviceState {
|
||||
@@ -172,27 +175,6 @@ class DeviceManagerImpl implements DeviceManager {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class PushManagerImpl implements PushManager {
|
||||
getSubscription(): Promise<PushSubscription> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
permissionState(options?: PushSubscriptionOptionsInit): Promise<PushPermissionState> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
subscribe(options?: PushSubscriptionOptionsInit): Promise<PushSubscription> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
getRegistrationId(): string {
|
||||
return 'no-registration-id-fix-this';
|
||||
}
|
||||
|
||||
getSenderId(): string {
|
||||
return 'no-sender-id-fix-this';
|
||||
}
|
||||
}
|
||||
|
||||
class StorageImpl implements Storage {
|
||||
api: PluginAPI;
|
||||
|
||||
@@ -297,19 +279,25 @@ export async function setupPluginRemote(peer: RpcPeer, api: PluginAPI, pluginId:
|
||||
return ret;
|
||||
}
|
||||
|
||||
export async function attachPluginRemote(peer: RpcPeer, createMediaManager?: (systemManager: SystemManager) => Promise<MediaManager>): Promise<ScryptedStatic> {
|
||||
export interface PluginRemoteAttachOptions {
|
||||
createMediaManager?: (systemManager: SystemManager) => Promise<MediaManager>;
|
||||
getServicePort?: (name: string) => Promise<number>;
|
||||
getDeviceConsole?: (nativeId?: string) => Console;
|
||||
events?: EventEmitter;
|
||||
}
|
||||
|
||||
export function attachPluginRemote(peer: RpcPeer, options?: PluginRemoteAttachOptions): Promise<ScryptedStatic> {
|
||||
const { createMediaManager, getServicePort, events, getDeviceConsole } = options || {};
|
||||
|
||||
peer.addSerializer(Buffer, 'Buffer', new BufferSerializer());
|
||||
|
||||
let done: any;
|
||||
const retPromise = new Promise<ScryptedStatic>((resolve, reject) => {
|
||||
done = resolve;
|
||||
});
|
||||
let done: (scrypted: ScryptedStatic) => void;
|
||||
const retPromise = new Promise<ScryptedStatic>(resolve => done = resolve);
|
||||
|
||||
peer.params.getRemote = async (api: PluginAPI, pluginId: string) => {
|
||||
const systemManager = new SystemManagerImpl();
|
||||
const deviceManager = new DeviceManagerImpl(systemManager);
|
||||
const deviceManager = new DeviceManagerImpl(systemManager, events, getDeviceConsole);
|
||||
const endpointManager = new EndpointManagerImpl();
|
||||
const pushManager = new PushManagerImpl();
|
||||
const ioSockets: { [id: string]: WebSocketCallbacks } = {};
|
||||
const mediaManager = await api.getMediaManager() || await createMediaManager(systemManager);
|
||||
|
||||
@@ -336,6 +324,7 @@ export async function attachPluginRemote(peer: RpcPeer, createMediaManager?: (sy
|
||||
const localStorage = new StorageImpl(deviceManager, undefined);
|
||||
|
||||
const remote: PluginRemote = {
|
||||
getServicePort,
|
||||
createDeviceState(id: string, setState: (property: string, value: any) => Promise<void>) {
|
||||
const handler = new DeviceStateProxyHandler(deviceManager, id, setState);
|
||||
return new Proxy(handler, handler);
|
||||
@@ -378,7 +367,7 @@ export async function attachPluginRemote(peer: RpcPeer, createMediaManager?: (sy
|
||||
}
|
||||
},
|
||||
|
||||
async notify(id: string, eventTime: number, eventInterface: string, property: string, value: SystemDeviceState|any, changed?: boolean) {
|
||||
async notify(id: string, eventTime: number, eventInterface: string, property: string, value: SystemDeviceState | any, changed?: boolean) {
|
||||
if (property) {
|
||||
const state = systemManager.state?.[id];
|
||||
if (!state) {
|
||||
@@ -416,7 +405,7 @@ export async function attachPluginRemote(peer: RpcPeer, createMediaManager?: (sy
|
||||
volume.writeFileSync(name, entry.getData());
|
||||
}
|
||||
|
||||
const params = {
|
||||
const params: any = {
|
||||
// legacy
|
||||
android: {},
|
||||
|
||||
@@ -429,7 +418,7 @@ export async function attachPluginRemote(peer: RpcPeer, createMediaManager?: (sy
|
||||
error,
|
||||
end
|
||||
};
|
||||
|
||||
|
||||
connect(undefined, {
|
||||
close: () => api.ioClose(id),
|
||||
}, (message: string) => api.ioSend(id, message));
|
||||
@@ -442,7 +431,7 @@ export async function attachPluginRemote(peer: RpcPeer, createMediaManager?: (sy
|
||||
error,
|
||||
end
|
||||
};
|
||||
|
||||
|
||||
connect(undefined, {
|
||||
close: () => api.ioClose(id),
|
||||
}, (message: string) => api.ioSend(id, message));
|
||||
@@ -463,18 +452,24 @@ export async function attachPluginRemote(peer: RpcPeer, createMediaManager?: (sy
|
||||
const module = require(name);
|
||||
return module;
|
||||
},
|
||||
pushManager,
|
||||
deviceManager,
|
||||
systemManager,
|
||||
mediaManager,
|
||||
endpointManager,
|
||||
log,
|
||||
localStorage,
|
||||
zwaveManager: null as any,
|
||||
pluginHostAPI: api,
|
||||
};
|
||||
|
||||
if (getDeviceConsole) {
|
||||
params.console = getDeviceConsole(undefined);
|
||||
}
|
||||
|
||||
events?.emit('params', params);
|
||||
|
||||
try {
|
||||
peer.evalLocal(script, '/plugin/main.nodejs.js', params);
|
||||
events?.emit('plugin', exports.default);
|
||||
return exports.default;
|
||||
}
|
||||
catch (e) {
|
||||
|
||||
@@ -170,9 +170,23 @@ export class ScryptedRuntime {
|
||||
async endpointHandler(req: Request, res: Response, isPublicEndpoint: boolean, isEngineIOEndpoint: boolean,
|
||||
handler: (req: Request, res: Response, endpointRequest: HttpRequest, pluginHost: PluginHost, pluginDevice: PluginDevice) => void) {
|
||||
|
||||
const isUpgrade = !!(req as any).upgradeHead;
|
||||
|
||||
const end = (code: number, message: string) => {
|
||||
if (isUpgrade) {
|
||||
const socket = res.socket;
|
||||
socket.write(`HTTP/1.1 ${code} ${message}\r\n` +
|
||||
'\r\n');
|
||||
socket.destroy();
|
||||
}
|
||||
else {
|
||||
res.status(code);
|
||||
res.send(message);
|
||||
}
|
||||
};
|
||||
|
||||
if (!isPublicEndpoint && !res.locals.username) {
|
||||
res.status(401);
|
||||
res.send('Not logged in');
|
||||
end(401, 'Not Authorized');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -192,17 +206,17 @@ export class ScryptedRuntime {
|
||||
}
|
||||
|
||||
const pluginDevice = this.findPluginDevice(endpoint) ?? this.findPluginDeviceById(endpoint);
|
||||
if (!pluginHost || !pluginDevice) {
|
||||
if (req.headers.connection?.toLowerCase() === 'upgrade' && (req.headers.upgrade?.toLowerCase() !== 'websocket' || !pluginDevice?.state.interfaces.value.includes(ScryptedInterface.EngineIOHandler))) {
|
||||
const socket = res.socket;
|
||||
socket.write('HTTP/1.1 404 Not Found\r\n' +
|
||||
'\r\n');
|
||||
socket.destroy();
|
||||
|
||||
// check if upgrade requests can be handled. must be websocket.
|
||||
if (isUpgrade) {
|
||||
if (req.headers.upgrade?.toLowerCase() !== 'websocket' || !pluginDevice?.state.interfaces.value.includes(ScryptedInterface.EngineIOHandler)) {
|
||||
end(404, 'Not Found');
|
||||
return;
|
||||
}
|
||||
else if (!pluginDevice?.state.interfaces.value.includes(ScryptedInterface.HttpRequestHandler)) {
|
||||
res.status(404);
|
||||
res.send()
|
||||
}
|
||||
else {
|
||||
if (!pluginDevice?.state.interfaces.value.includes(ScryptedInterface.HttpRequestHandler)) {
|
||||
end(404, 'Not Found');
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -300,7 +314,7 @@ export class ScryptedRuntime {
|
||||
endpointRequest,
|
||||
pluginDevice,
|
||||
};
|
||||
if (req.headers.upgrade)
|
||||
if ((req as any).upgradeHead)
|
||||
pluginHost.io.handleUpgrade(req, res.socket, (req as any).upgradeHead)
|
||||
else
|
||||
pluginHost.io.handleRequest(req, res);
|
||||
|
||||
@@ -8,7 +8,7 @@ import express from 'express';
|
||||
import bodyParser from 'body-parser';
|
||||
import cluster from 'cluster';
|
||||
import net from 'net';
|
||||
import { startPluginCluster } from './plugin/plugin-host';
|
||||
import { startPluginClusterWorker as startPluginRemoteClusterWorker } from './plugin/plugin-host';
|
||||
import { ScryptedRuntime } from './runtime';
|
||||
import level from './level';
|
||||
import { Plugin, ScryptedUser, Settings } from './db-types';
|
||||
@@ -28,7 +28,7 @@ process.on('unhandledRejection', error => {
|
||||
|
||||
|
||||
if (!cluster.isMaster) {
|
||||
startPluginCluster();
|
||||
startPluginRemoteClusterWorker();
|
||||
}
|
||||
else {
|
||||
let workerInspectPort: number = undefined;
|
||||
|
||||
Reference in New Issue
Block a user