From 619ce43fcdb4a83e2ca093e53da60d7915dfe7df Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Fri, 25 Nov 2022 23:26:17 -0800 Subject: [PATCH] homekit/core/sdk: use global setting for server address and transcoding --- plugins/core/.vscode/settings.json | 2 +- plugins/core/README.md | 4 +- plugins/core/package-lock.json | 24 ++-- plugins/core/package.json | 5 +- plugins/core/src/automations-core.ts | 86 ++++++++++++ plugins/core/src/launcher-mixin.ts | 10 +- plugins/core/src/main.ts | 132 +++++++++--------- plugins/core/src/media-core.ts | 2 +- plugins/core/src/script-core.ts | 4 +- plugins/core/tsconfig.json | 2 +- plugins/core/ui/src/components/Device.vue | 4 +- .../components/builtin/SettingsComponent.vue | 9 +- .../src/components/builtin/system-settings.ts | 64 +++++++++ plugins/core/ui/src/components/helpers.ts | 14 +- plugins/core/ui/src/interfaces/Settings.vue | 14 +- plugins/homekit/src/address-override.ts | 13 ++ plugins/homekit/src/main.ts | 13 ++ .../src/types/camera/camera-streaming.ts | 7 +- .../scrypted_python/scrypted_sdk/types.py | 1 + sdk/types/src/types.input.ts | 1 + 20 files changed, 312 insertions(+), 99 deletions(-) create mode 100644 plugins/core/src/automations-core.ts create mode 100644 plugins/core/ui/src/components/builtin/system-settings.ts create mode 100644 plugins/homekit/src/address-override.ts diff --git a/plugins/core/.vscode/settings.json b/plugins/core/.vscode/settings.json index 2b496d57a..79c896063 100644 --- a/plugins/core/.vscode/settings.json +++ b/plugins/core/.vscode/settings.json @@ -1,3 +1,3 @@ { - "scrypted.debugHost": "koushik-ubuntu", + "scrypted.debugHost": "127.0.0.1", } \ No newline at end of file diff --git a/plugins/core/README.md b/plugins/core/README.md index ca2941808..352979971 100644 --- a/plugins/core/README.md +++ b/plugins/core/README.md @@ -1,3 +1,3 @@ -# Scrypted Web UI Plugin +# Scrypted Core Plugin -The Core Plugin provides the web UI for Scrypted. +The Core Plugin provides the UI, Automations, Device Groups, and other core functionality within Scrypted. diff --git a/plugins/core/package-lock.json b/plugins/core/package-lock.json index 3c3082665..63e4fb533 100644 --- a/plugins/core/package-lock.json +++ b/plugins/core/package-lock.json @@ -87,7 +87,7 @@ }, "../../sdk": { "name": "@scrypted/sdk", - "version": "0.0.206", + "version": "0.2.21", "license": "ISC", "dependencies": { "@babel/preset-typescript": "^7.16.7", @@ -95,12 +95,14 @@ "axios": "^0.21.4", "babel-loader": "^8.2.3", "babel-plugin-const-enum": "^1.1.0", - "esbuild": "^0.13.8", + "esbuild": "^0.15.9", "ncp": "^2.0.0", "raw-loader": "^4.0.2", "rimraf": "^3.0.2", "tmp": "^0.2.1", - "webpack": "^5.59.0" + "typescript": "^4.9.3", + "webpack": "^5.74.0", + "webpack-bundle-analyzer": "^4.5.0" }, "bin": { "scrypted-debug": "bin/scrypted-debug.js", @@ -112,13 +114,11 @@ "scrypted-webpack": "bin/scrypted-webpack.js" }, "devDependencies": { - "@types/node": "^16.11.1", + "@types/node": "^18.11.9", "@types/stringify-object": "^4.0.0", "stringify-object": "^3.3.0", "ts-node": "^10.4.0", - "typedoc": "^0.22.8", - "typescript-json-schema": "^0.50.1", - "webpack-bundle-analyzer": "^4.5.0" + "typedoc": "^0.23.21" } }, "node_modules/@scrypted/common": { @@ -250,22 +250,22 @@ "version": "file:../../sdk", "requires": { "@babel/preset-typescript": "^7.16.7", - "@types/node": "^16.11.1", + "@types/node": "^18.11.9", "@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", + "esbuild": "^0.15.9", "ncp": "^2.0.0", "raw-loader": "^4.0.2", "rimraf": "^3.0.2", "stringify-object": "^3.3.0", "tmp": "^0.2.1", "ts-node": "^10.4.0", - "typedoc": "^0.22.8", - "typescript-json-schema": "^0.50.1", - "webpack": "^5.59.0", + "typedoc": "^0.23.21", + "typescript": "^4.9.3", + "webpack": "^5.74.0", "webpack-bundle-analyzer": "^4.5.0" } }, diff --git a/plugins/core/package.json b/plugins/core/package.json index 0f67813fc..487cc4b77 100644 --- a/plugins/core/package.json +++ b/plugins/core/package.json @@ -29,7 +29,8 @@ "@scrypted/launcher-ignore", "HttpRequestHandler", "EngineIOHandler", - "DeviceProvider" + "DeviceProvider", + "Settings" ], "pluginDependencies": [ "@scrypted/webrtc" @@ -46,4 +47,4 @@ "@types/mime": "^2.0.3", "@types/node": "^16.9.0" } -} +} \ No newline at end of file diff --git a/plugins/core/src/automations-core.ts b/plugins/core/src/automations-core.ts new file mode 100644 index 000000000..3b8cf41b8 --- /dev/null +++ b/plugins/core/src/automations-core.ts @@ -0,0 +1,86 @@ +import sdk, { Device, DeviceCreator, DeviceCreatorSettings, DeviceProvider, Readme, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, Setting } from '@scrypted/sdk'; +import { randomBytes } from "crypto"; +import { Automation } from "./automation"; +import { updatePluginsData } from './update-plugins'; + +const { deviceManager } = sdk; +export const AutomationCoreNativeId = 'automationcore'; + +export class AutomationCore extends ScryptedDeviceBase implements DeviceProvider, DeviceCreator, Readme { + automations = new Map(); + + constructor() { + super(AutomationCoreNativeId); + + for (const nativeId of deviceManager.getNativeIds()) { + if (nativeId?.startsWith('automation:')) { + const automation = new Automation(nativeId); + this.automations.set(nativeId, automation); + this.reportAutomation(nativeId, automation.providedName); + } + } + + + (async () => { + const updatePluginsNativeId = 'automation:update-plugins' + let updatePlugins = this.automations.get(updatePluginsNativeId); + if (!updatePlugins) { + await this.reportAutomation(updatePluginsNativeId, 'Autoupdate Plugins'); + updatePlugins = new Automation(updatePluginsNativeId); + updatePlugins.storage.setItem('data', JSON.stringify(updatePluginsData)); + this.automations.set(updatePluginsNativeId, updatePlugins); + } + })(); + + // update the automations devices on storage change. + // todo: make this use setting api + sdk.systemManager.listen((eventSource, eventDetails, eventData) => { + if (eventDetails.eventInterface === 'Storage') { + const ids = [...this.automations.values()].map(a => a.id); + if (ids.includes(eventSource.id)) { + const automation = [...this.automations.values()].find(a => a.id === eventSource.id); + automation.bind(); + } + } + }); + } + + async getReadmeMarkdown(): Promise { + return "Create custom smart home actions that trigger on specific events."; + } + + async getCreateDeviceSettings(): Promise { + return [ + { + key: 'name', + title: 'Name', + description: 'The name or description of the new automation.', + }, + ] + } + + async createDevice(settings: DeviceCreatorSettings): Promise { + const { name, template } = settings; + const nativeId = 'automation:' + randomBytes(8).toString('hex'); + await this.reportAutomation(nativeId, name?.toString()); + const automation = new Automation(nativeId); + this.automations.set(nativeId, automation); + return nativeId; + } + + async reportAutomation(nativeId: string, name?: string) { + const device: Device = { + providerNativeId: AutomationCoreNativeId, + name, + nativeId, + type: ScryptedDeviceType.Automation, + interfaces: [ScryptedInterface.OnOff, ScryptedInterface.Settings] + } + await deviceManager.onDeviceDiscovered(device); + } + + + async getDevice(nativeId: string) { + return this.automations.get(nativeId); + } +} diff --git a/plugins/core/src/launcher-mixin.ts b/plugins/core/src/launcher-mixin.ts index a95ae917f..0bc7f2b9a 100644 --- a/plugins/core/src/launcher-mixin.ts +++ b/plugins/core/src/launcher-mixin.ts @@ -1,10 +1,16 @@ -import { DeviceState, MixinProvider, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface } from "@scrypted/sdk"; +import { DeviceState, MixinProvider, Readme, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface } from "@scrypted/sdk"; import { typeToIcon } from "../ui/src/components/helpers"; -export class LauncherMixin extends ScryptedDeviceBase implements MixinProvider { +export class LauncherMixin extends ScryptedDeviceBase implements MixinProvider, Readme { + async getReadmeMarkdown(): Promise { + return 'Add Scrypted Plugins or Devices to the Scrypted launch screen for quick access.'; + } + async canMixin(type: ScryptedDeviceType, interfaces: string[]): Promise { if (interfaces.includes("@scrypted/launcher-ignore")) return; + if (type === ScryptedDeviceType.Builtin || type === ScryptedDeviceType.API) + return; return [ ScryptedInterface.LauncherApplication, ]; diff --git a/plugins/core/src/main.ts b/plugins/core/src/main.ts index 5e6b58c37..dc0baae7d 100644 --- a/plugins/core/src/main.ts +++ b/plugins/core/src/main.ts @@ -1,39 +1,29 @@ -import { ScryptedDeviceBase, HttpRequestHandler, HttpRequest, HttpResponse, EngineIOHandler, Device, DeviceProvider, ScryptedInterface, ScryptedDeviceType, RTCSignalingChannel, VideoCamera, VideoRecorder } from '@scrypted/sdk'; -import sdk from '@scrypted/sdk'; -import Router from 'router'; -import { UserStorage } from './userStorage'; -import { RpcPeer } from '../../../server/src/rpc'; -import { setupPluginRemote } from '../../../server/src/plugin/plugin-remote'; -import { PluginAPIProxy } from '../../../server/src/plugin/plugin-api'; +import sdk, { Device, DeviceProvider, EngineIOHandler, HttpRequest, HttpRequestHandler, HttpResponse, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, Setting, Settings, SettingValue } from '@scrypted/sdk'; +import { StorageSettings } from "@scrypted/sdk/storage-settings"; import fs from 'fs'; -import { sendJSON } from './http-helpers'; -import { Automation } from './automation'; -import { AggregateDevice, createAggregateDevice } from './aggregate'; import net from 'net'; -import { updatePluginsData } from './update-plugins'; +import Router from 'router'; +import { AggregateDevice, createAggregateDevice } from './aggregate'; +import { AutomationCore, AutomationCoreNativeId } from './automations-core'; +import { sendJSON } from './http-helpers'; +import { LauncherMixin } from './launcher-mixin'; import { MediaCore } from './media-core'; import { ScriptCore, ScriptCoreNativeId } from './script-core'; -import { LauncherMixin } from './launcher-mixin'; +import os from 'os'; -const { pluginHostAPI, systemManager, deviceManager, mediaManager, endpointManager } = sdk; +const { systemManager, deviceManager, endpointManager } = sdk; const indexHtml = fs.readFileSync('dist/index.html').toString(); +export function getAddresses() { + const addresses = Object.entries(os.networkInterfaces()).filter(([iface]) => iface.startsWith('en') || iface.startsWith('eth') || iface.startsWith('wlan')).map(([_, addr]) => addr).flat().map(info => info.address).filter(address => address); + return addresses; +} + interface RoutedHttpRequest extends HttpRequest { params: { [key: string]: string }; } -async function reportAutomation(nativeId: string, name?: string) { - const device: Device = { - name, - nativeId, - type: ScryptedDeviceType.Automation, - interfaces: [ScryptedInterface.OnOff, ScryptedInterface.Settings] - } - await deviceManager.onDeviceDiscovered(device); -} - - async function reportAggregate(nativeId: string, interfaces: string[]) { const device: Device = { name: undefined, @@ -44,14 +34,33 @@ async function reportAggregate(nativeId: string, interfaces: string[]) { await deviceManager.onDeviceDiscovered(device); } -class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, EngineIOHandler, DeviceProvider { +class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, EngineIOHandler, DeviceProvider, Settings { router: any = Router(); publicRouter: any = Router(); mediaCore: MediaCore; launcher: LauncherMixin; scriptCore: ScriptCore; - automations = new Map(); + automationCore: AutomationCore; aggregate = new Map(); + localAddresses: string[]; + storageSettings = new StorageSettings(this, { + localAddresses: { + title: 'Scrypted Server Address', + description: 'The IP address used by the Scrypted server. Set this to the wired IP address to prevent usage of a wireless address.', + combobox: true, + async onGet() { + return { + choices: getAddresses(), + }; + }, + mapGet: () => this.localAddresses?.[0], + onPut: async (oldValue, newValue) => { + this.localAddresses = newValue ? [newValue] : undefined; + const service = await sdk.systemManager.getComponent('addresses'); + service.setLocalAddresses(this.localAddresses); + }, + } + }); constructor() { super(); @@ -62,7 +71,7 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Eng name: 'Media Core', nativeId: 'mediacore', interfaces: [ScryptedInterface.DeviceProvider, ScryptedInterface.BufferConverter, ScryptedInterface.HttpRequestHandler], - type: ScryptedDeviceType.API, + type: ScryptedDeviceType.Builtin, }, ); this.mediaCore = new MediaCore('mediacore'); @@ -73,10 +82,22 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Eng name: 'Scripting Core', nativeId: ScriptCoreNativeId, interfaces: [ScryptedInterface.DeviceProvider, ScryptedInterface.DeviceCreator, ScryptedInterface.Readme], - type: ScryptedDeviceType.API, + type: ScryptedDeviceType.Builtin, }, ); - this.scriptCore = new ScriptCore(ScriptCoreNativeId); + this.scriptCore = new ScriptCore(); + })(); + + (async () => { + await deviceManager.onDeviceDiscovered( + { + name: 'Automation Core', + nativeId: AutomationCoreNativeId, + interfaces: [ScryptedInterface.DeviceProvider, ScryptedInterface.DeviceCreator, ScryptedInterface.Readme], + type: ScryptedDeviceType.Builtin, + }, + ); + this.automationCore = new AutomationCore(); })(); deviceManager.onDeviceDiscovered({ @@ -85,44 +106,19 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Eng interfaces: [ '@scrypted/launcher-ignore', ScryptedInterface.MixinProvider, + ScryptedInterface.Readme, ], type: ScryptedDeviceType.Builtin, }); for (const nativeId of deviceManager.getNativeIds()) { - if (nativeId?.startsWith('automation:')) { - const automation = new Automation(nativeId); - this.automations.set(nativeId, automation); - reportAutomation(nativeId, automation.providedName); - } - else if (nativeId?.startsWith('aggregate:')) { + if (nativeId?.startsWith('aggregate:')) { const aggregate = createAggregateDevice(nativeId); this.aggregate.set(nativeId, aggregate); reportAggregate(nativeId, aggregate.computeInterfaces()); } } - (async () => { - const updatePluginsNativeId = 'automation:update-plugins' - let updatePlugins = this.automations.get(updatePluginsNativeId); - if (!updatePlugins) { - await reportAutomation(updatePluginsNativeId, 'Autoupdate Plugins'); - updatePlugins = new Automation(updatePluginsNativeId); - updatePlugins.storage.setItem('data', JSON.stringify(updatePluginsData)); - this.automations.set(updatePluginsNativeId, updatePlugins); - } - })(); - - this.router.post('/api/new/automation', async (req: RoutedHttpRequest, res: HttpResponse) => { - const nativeId = `automation:${Math.random()}`; - await reportAutomation(nativeId); - const automation = new Automation(nativeId); - this.automations.set(nativeId, automation); - const { id } = automation; - sendJSON(res, { - id, - }); - }); this.router.post('/api/new/aggregate', async (req: RoutedHttpRequest, res: HttpResponse) => { const nativeId = `aggregate:${Math.random()}`; @@ -138,12 +134,7 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Eng // update the automations and grouped devices on storage change. systemManager.listen((eventSource, eventDetails, eventData) => { if (eventDetails.eventInterface === 'Storage') { - let ids = [...this.automations.values()].map(a => a.id); - if (ids.includes(eventSource.id)) { - const automation = [...this.automations.values()].find(a => a.id === eventSource.id); - automation.bind(); - } - ids = [...this.aggregate.values()].map(a => a.id); + const ids = [...this.aggregate.values()].map(a => a.id); if (ids.includes(eventSource.id)) { const aggregate = [...this.aggregate.values()].find(a => a.id === eventSource.id); reportAggregate(aggregate.nativeId, aggregate.computeInterfaces()); @@ -152,6 +143,19 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Eng }); } + async getSettings(): Promise { + try { + const service = await sdk.systemManager.getComponent('addresses'); + this.localAddresses = await service.getLocalAddresses(); + } + catch (e) { + } + return this.storageSettings.getSettings(); + } + async putSetting(key: string, value: SettingValue): Promise { + await this.storageSettings.putSetting(key, value); + } + async getDevice(nativeId: string) { if (nativeId === 'launcher') return new LauncherMixin('launcher'); @@ -159,8 +163,8 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Eng return this.mediaCore; if (nativeId === ScriptCoreNativeId) return this.scriptCore; - if (nativeId?.startsWith('automation:')) - return this.automations.get(nativeId); + if (nativeId === AutomationCoreNativeId) + return this.automationCore; if (nativeId?.startsWith('aggregate:')) return this.aggregate.get(nativeId); } diff --git a/plugins/core/src/media-core.ts b/plugins/core/src/media-core.ts index ac2f1c728..a914ef6bc 100644 --- a/plugins/core/src/media-core.ts +++ b/plugins/core/src/media-core.ts @@ -115,7 +115,7 @@ export class MediaCore extends ScryptedDeviceBase implements DeviceProvider, Buf } } - getDevice(nativeId: string) { + async getDevice(nativeId: string) { if (nativeId === 'http') return this.httpHost; if (nativeId === 'https') diff --git a/plugins/core/src/script-core.ts b/plugins/core/src/script-core.ts index 6296eef40..f9658d3e5 100644 --- a/plugins/core/src/script-core.ts +++ b/plugins/core/src/script-core.ts @@ -11,8 +11,8 @@ export const ScriptCoreNativeId = 'scriptcore'; export class ScriptCore extends ScryptedDeviceBase implements DeviceProvider, DeviceCreator, Readme { scripts = new Map>(); - constructor(nativeId: string) { - super(nativeId); + constructor() { + super(ScriptCoreNativeId); for (const nativeId of deviceManager.getNativeIds()) { if (nativeId?.startsWith('script:')) { diff --git a/plugins/core/tsconfig.json b/plugins/core/tsconfig.json index 22d317309..0dca70a7c 100644 --- a/plugins/core/tsconfig.json +++ b/plugins/core/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "resolveJsonModule": true, - "moduleResolution": "node", + "moduleResolution": "Node16", "target": "esnext", "esModuleInterop": true, }, diff --git a/plugins/core/ui/src/components/Device.vue b/plugins/core/ui/src/components/Device.vue index ef4e8062b..c02d7ca11 100644 --- a/plugins/core/ui/src/components/Device.vue +++ b/plugins/core/ui/src/components/Device.vue @@ -185,7 +185,7 @@ - + @@ -206,6 +206,7 @@ import { getAlertIcon, hasFixedPhysicalLocation, getInterfaceFriendlyName, + deviceIsEditable, } from "./helpers"; import { ScryptedInterface } from "@scrypted/types"; import RTCSignalingClient from "../interfaces/RTCSignalingClient.vue"; @@ -410,6 +411,7 @@ export default { }, }, methods: { + deviceIsEditable, getInterfaceFriendlyName, hasFixedPhysicalLocation, getComponentWebPath, diff --git a/plugins/core/ui/src/components/builtin/SettingsComponent.vue b/plugins/core/ui/src/components/builtin/SettingsComponent.vue index 24ba0e2e6..4f2e78f9d 100644 --- a/plugins/core/ui/src/components/builtin/SettingsComponent.vue +++ b/plugins/core/ui/src/components/builtin/SettingsComponent.vue @@ -1,6 +1,7 @@