homekit/core/sdk: use global setting for server address and transcoding

This commit is contained in:
Koushik Dutta
2022-11-25 23:26:17 -08:00
parent 115143e304
commit 619ce43fcd
20 changed files with 312 additions and 99 deletions

View File

@@ -1,3 +1,3 @@
{
"scrypted.debugHost": "koushik-ubuntu",
"scrypted.debugHost": "127.0.0.1",
}

View File

@@ -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.

View File

@@ -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"
}
},

View File

@@ -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"
}
}
}

View File

@@ -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<string, Automation>();
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<string> {
return "Create custom smart home actions that trigger on specific events.";
}
async getCreateDeviceSettings(): Promise<Setting[]> {
return [
{
key: 'name',
title: 'Name',
description: 'The name or description of the new automation.',
},
]
}
async createDevice(settings: DeviceCreatorSettings): Promise<string> {
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);
}
}

View File

@@ -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<string> {
return 'Add Scrypted Plugins or Devices to the Scrypted launch screen for quick access.';
}
async canMixin(type: ScryptedDeviceType, interfaces: string[]): Promise<string[]> {
if (interfaces.includes("@scrypted/launcher-ignore"))
return;
if (type === ScryptedDeviceType.Builtin || type === ScryptedDeviceType.API)
return;
return [
ScryptedInterface.LauncherApplication,
];

View File

@@ -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<string, Automation>();
automationCore: AutomationCore;
aggregate = new Map<string, AggregateDevice>();
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<Setting[]> {
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<void> {
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);
}

View File

@@ -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')

View File

@@ -11,8 +11,8 @@ export const ScriptCoreNativeId = 'scriptcore';
export class ScriptCore extends ScryptedDeviceBase implements DeviceProvider, DeviceCreator, Readme {
scripts = new Map<string, Promise<Script>>();
constructor(nativeId: string) {
super(nativeId);
constructor() {
super(ScriptCoreNativeId);
for (const nativeId of deviceManager.getNativeIds()) {
if (nativeId?.startsWith('script:')) {

View File

@@ -1,7 +1,7 @@
{
"compilerOptions": {
"resolveJsonModule": true,
"moduleResolution": "node",
"moduleResolution": "Node16",
"target": "esnext",
"esModuleInterop": true,
},

View File

@@ -185,7 +185,7 @@
<LogCard :rows="15" :logRoute="`/device/${id}/`"></LogCard>
</v-flex>
<v-flex xs12 v-if="!device.interfaces.includes(ScryptedInterface.Settings) && (availableMixins.length || !device.interfaces.includes(ScryptedInterface.ScryptedPlugin))">
<v-flex xs12 v-if="!device.interfaces.includes(ScryptedInterface.Settings) && (availableMixins.length || deviceIsEditable(device))">
<Settings :device="device"></Settings>
</v-flex>
</v-layout>
@@ -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,

View File

@@ -1,6 +1,7 @@
<template>
<v-layout>
<v-layout row wrap>
<v-flex xs12 md6 lg4>
<Settings :device="coreDevice" class="mb-2"></Settings>
<v-card v-if="updateAvailable">
<v-toolbar>
<v-toolbar-title>Update Available </v-toolbar-title>
@@ -101,10 +102,16 @@
</template>
<script>
import { checkUpdate } from "../plugin/plugin";
import Settings from "../../interfaces/Settings.vue"
import {createSystemSettingsDevice} from './system-settings';
export default {
components: {
Settings,
},
data() {
return {
coreDevice: createSystemSettingsDevice(this.$scrypted.systemManager),
currentVersion: null,
updateAvailable: null,
canUpdate: false,

View File

@@ -0,0 +1,64 @@
import { ScryptedDevice, ScryptedDeviceType, ScryptedInterface, Settings, SystemManager } from "@scrypted/types";
export function createSystemSettingsDevice(systemManager: SystemManager): ScryptedDevice & Settings {
const core = systemManager.getDeviceByName<Settings>('@scrypted/core');
let transcode: ScryptedDevice & Settings;
for (const id of Object.keys(systemManager.getSystemState())) {
const device = systemManager.getDeviceById<Settings>(id);
if (device.nativeId === 'transcode' && device.pluginId === '@scrypted/prebuffer-mixin') {
transcode = device;
break;
}
}
return {
name: 'Settings',
type: ScryptedDeviceType.Builtin,
interfaces: [
ScryptedInterface.Settings,
],
async setName(name) {
},
async setRoom() {
},
async setType() {
},
async probe() {
return true;
},
listen(event, callback) {
const cl = core.listen(event, callback);
const tl = transcode?.listen(event, callback);
return {
removeListener() {
cl.removeListener();
tl?.removeListener();
},
}
},
async getSettings() {
return [
...(await core.getSettings()).map(s => ({
...s,
key: 'core:' + s.key,
group: 'Network Settings',
})),
...(await transcode?.getSettings() || []).map(s => ({
...s,
key: 'transcode:' + s.key,
group: 'Transcoding',
})),
];
},
async putSetting(key, value) {
if (key.startsWith('core:')) {
await core.putSetting(key.substring(5), value);
}
else if (key.startsWith('transcode:')) {
await core.putSetting(key.substring(10), value);
}
},
}
}

View File

@@ -1,4 +1,15 @@
import { ScryptedDeviceType, ScryptedInterface } from "@scrypted/types";
import { ScryptedDevice, ScryptedDeviceType, ScryptedInterface } from "@scrypted/types";
export function deviceIsEditable(device: ScryptedDevice) {
if (!device)
return;
if (device.interfaces.includes(ScryptedInterface.ScryptedPlugin))
return;
if (device.type === ScryptedDeviceType.Builtin || device.type === ScryptedDeviceType.API)
return;
return true;
}
export function typeToIcon(type) {
switch (type) {
@@ -30,6 +41,7 @@ export function typeToIcon(type) {
case ScryptedDeviceType.Irrigation: return "fa-faucet";
case ScryptedDeviceType.Person: return "fa-user";
case ScryptedDeviceType.SecuritySystem: return "fa-shield-alt";
case ScryptedDeviceType.Builtin: return "fa-server";
}
return "fa-toggle-on";

View File

@@ -41,15 +41,15 @@
</v-card>
</template>
<script>
import { ScryptedInterface } from "@scrypted/types";
import Vue from 'vue';
import AvailableMixins from "../components/AvailableMixins.vue";
import CardTitle from "../components/CardTitle.vue";
import { deviceIsEditable, hasFixedPhysicalLocation, inferTypesFromInterfaces } from "../components/helpers";
import Mixin from "../components/Mixin.vue";
import RPCInterface from "./RPCInterface.vue";
import Setting from "./Setting.vue";
import SettingMultiple from "./SettingMultiple.vue";
import CardTitle from "../components/CardTitle.vue";
import AvailableMixins from "../components/AvailableMixins.vue";
import Mixin from "../components/Mixin.vue";
import { ScryptedInterface } from "@scrypted/types";
import { hasFixedPhysicalLocation, inferTypesFromInterfaces } from "../components/helpers";
import Vue from 'vue';
export default {
components: {
@@ -93,7 +93,7 @@ export default {
let addAt = 0;
if (this.device && !this.device.interfaces.includes(ScryptedInterface.ScryptedPlugin)) {
if (deviceIsEditable(this.device)) {
const inferredTypes = inferTypesFromInterfaces(
this.device.type,
this.device.providedType,

View File

@@ -0,0 +1,13 @@
import sdk from '@scrypted/sdk';
export async function getAddressOverride(legacy: string) {
try {
const service = await sdk.systemManager.getComponent('addresses');
const addresses = await service.getLocalAddresses();
const address = addresses?.[0];
return address || legacy;
}
catch (e) {
return legacy;
}
}

View File

@@ -107,6 +107,19 @@ export class HomeKitPlugin extends ScryptedDeviceBase implements MixinProvider,
}
async getSettings(): Promise<Setting[]> {
try {
this.storageSettings.settings.addressOverride.hide = false;
const service = await sdk.systemManager.getComponent('addresses');
if (service) {
if (this.storageSettings.values.addressOverride) {
await service.setLocalAddresses([this.storageSettings.values.addressOverride]);
this.storageSettings.values.addressOverride = undefined;
}
this.storageSettings.settings.addressOverride.hide = true;;
}
}
catch (e) {
}
return this.storageSettings.getSettings();
}

View File

@@ -9,6 +9,7 @@ import sdk, { Camera, FFmpegInput, Intercom, MediaStreamFeedback, MediaStreamOpt
import dgram, { SocketType } from 'dgram';
import { once } from 'events';
import os from 'os';
import { getAddressOverride } from '../../address-override';
import { AudioStreamingCodecType, CameraController, CameraStreamingDelegate, PrepareStreamCallback, PrepareStreamRequest, PrepareStreamResponse, StartStreamRequest, StreamingRequest, StreamRequestCallback, StreamRequestTypes } from '../../hap';
import type { HomeKitPlugin } from "../../main";
import { startRtpSink } from '../../rtp/rtp-ffmpeg-input';
@@ -57,12 +58,14 @@ export function createCameraStreamingDelegate(device: ScryptedDevice & VideoCame
});
const socketType = request.addressVersion === 'ipv6' ? 'udp6' : 'udp4';
let addressOverride = homekitPlugin.storageSettings.values.addressOverride || undefined;
let addressOverride = await getAddressOverride(homekitPlugin.storageSettings.values.addressOverride || undefined);
if (addressOverride) {
const infos = Object.values(os.networkInterfaces()).flat().map(i => i?.address);
if (!infos.find(address => address === addressOverride)) {
console.error('The provided Scrypted Server Address was not found in the list of network addresses and may be invalid and will not be used (DHCP assignment change?): ' + addressOverride);
const error = 'The provided Scrypted Server Address was not found in the list of network addresses and may be invalid and will not be used (DHCP assignment change?): ' + addressOverride;
console.error(error);
sdk.log.a(error);
addressOverride = undefined;
}
}

View File

@@ -372,6 +372,7 @@ class HumiditySettingStatus(TypedDict):
class LauncherApplicationInfo(TypedDict):
description: str
href: str
icon: str
name: str
pass

View File

@@ -1606,6 +1606,7 @@ export interface LauncherApplicationInfo {
*/
icon?: string;
description?: string;
href?: string;
}
export interface LauncherApplication {