From 09490a709e2f6cd54d6d4ea74d7243f2a276ff30 Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Fri, 18 Feb 2022 21:46:17 -0800 Subject: [PATCH] core: initial refactor to new webrtc signaling --- plugins/core/package-lock.json | 31 +- plugins/core/src/main.ts | 4 +- plugins/core/ui/package-lock.json | 51 +-- plugins/core/ui/package.json | 5 +- plugins/core/ui/src/common/camera.ts | 345 +++++++++--------- plugins/core/ui/src/common/mixin.ts | 2 +- plugins/core/ui/src/components/Device.vue | 2 +- .../src/components/automation/Automation.vue | 2 +- .../src/components/automation/interfaces.ts | 2 +- .../ui/src/components/dashboard/Dashboard.vue | 2 +- .../components/dashboard/DashboardCamera.vue | 5 +- .../dashboard/DashboardMediaPlayer.vue | 2 +- .../dashboard/DashboardThermostat.vue | 2 +- .../components/dashboard/DashboardToggle.vue | 2 +- .../ui/src/components/dashboard/layout.ts | 2 +- plugins/core/ui/src/components/helpers.ts | 2 +- plugins/core/ui/src/interfaces/Camera.vue | 11 +- plugins/core/ui/src/interfaces/Notifier.vue | 2 +- .../ui/src/interfaces/TemperatureSetting.vue | 2 +- .../src/interfaces/automation/Scriptable.vue | 2 +- plugins/unifi-protect/src/camera.ts | 2 - 21 files changed, 222 insertions(+), 258 deletions(-) diff --git a/plugins/core/package-lock.json b/plugins/core/package-lock.json index 850399089..c7291931d 100644 --- a/plugins/core/package-lock.json +++ b/plugins/core/package-lock.json @@ -10,7 +10,6 @@ "license": "Apache-2.0", "dependencies": { "@koush/wrtc": "^0.5.2", - "@scrypted/rpc": "^1.0.3", "@scrypted/sdk": "file:../../sdk", "mime": "^3.0.0", "router": "^1.3.6", @@ -73,16 +72,10 @@ }, "../../sdk": { "name": "@scrypted/sdk", - "version": "0.0.167", + "version": "0.0.169", "license": "ISC", "dependencies": { - "@babel/plugin-proposal-class-properties": "^7.14.5", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.14.5", - "@babel/plugin-proposal-numeric-separator": "^7.14.5", - "@babel/plugin-proposal-optional-chaining": "^7.14.5", - "@babel/plugin-transform-modules-commonjs": "^7.15.4", - "@babel/plugin-transform-typescript": "^7.15.8", - "@babel/preset-typescript": "^7.15.0", + "@babel/preset-typescript": "^7.16.7", "adm-zip": "^0.4.13", "axios": "^0.21.4", "babel-loader": "^8.2.3", @@ -92,7 +85,6 @@ "raw-loader": "^4.0.2", "rimraf": "^3.0.2", "tmp": "^0.2.1", - "ts-loader": "^9.2.6", "webpack": "^5.59.0" }, "bin": { @@ -164,11 +156,6 @@ "node": ">=10" } }, - "node_modules/@scrypted/rpc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@scrypted/rpc/-/rpc-1.0.3.tgz", - "integrity": "sha512-luEigc8gIMoKv26t2123KKUno3W7o4ze6SMv7ZPnRnlpYrgo9CmjDhezptTfQuHm6c/0eiIRPh6qGSBWOjelGw==" - }, "node_modules/@scrypted/sdk": { "resolved": "../../sdk", "link": true @@ -1388,21 +1375,10 @@ } } }, - "@scrypted/rpc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@scrypted/rpc/-/rpc-1.0.3.tgz", - "integrity": "sha512-luEigc8gIMoKv26t2123KKUno3W7o4ze6SMv7ZPnRnlpYrgo9CmjDhezptTfQuHm6c/0eiIRPh6qGSBWOjelGw==" - }, "@scrypted/sdk": { "version": "file:../../sdk", "requires": { - "@babel/plugin-proposal-class-properties": "^7.14.5", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.14.5", - "@babel/plugin-proposal-numeric-separator": "^7.14.5", - "@babel/plugin-proposal-optional-chaining": "^7.14.5", - "@babel/plugin-transform-modules-commonjs": "^7.15.4", - "@babel/plugin-transform-typescript": "^7.15.8", - "@babel/preset-typescript": "^7.15.0", + "@babel/preset-typescript": "^7.16.7", "@types/node": "^16.11.1", "@types/stringify-object": "^4.0.0", "adm-zip": "^0.4.13", @@ -1415,7 +1391,6 @@ "rimraf": "^3.0.2", "stringify-object": "^3.3.0", "tmp": "^0.2.1", - "ts-loader": "^9.2.6", "ts-node": "^10.4.0", "typedoc": "^0.22.8", "typescript-json-schema": "^0.50.1", diff --git a/plugins/core/src/main.ts b/plugins/core/src/main.ts index d75d8cd06..dbc6fc4e0 100644 --- a/plugins/core/src/main.ts +++ b/plugins/core/src/main.ts @@ -12,11 +12,11 @@ import { Automation } from './automation'; import { AggregateDevice, createAggregateDevice } from './aggregate'; import net from 'net'; import { Script } from './script'; -import { addBuiltins } from "../../../common/src/wrtc-convertors"; +import { addBuiltins } from "../../../common/src/ffmpeg-to-wrtc"; import { updatePluginsData } from './update-plugins'; import { MediaCore } from './media-core'; -addBuiltins(console, mediaManager); +addBuiltins(mediaManager); const { pluginHostAPI } = sdk; diff --git a/plugins/core/ui/package-lock.json b/plugins/core/ui/package-lock.json index 67d02b6c4..633dd77a6 100644 --- a/plugins/core/ui/package-lock.json +++ b/plugins/core/ui/package-lock.json @@ -5,7 +5,6 @@ "packages": { "": { "name": "ui", - "hasInstallScript": true, "dependencies": { "@fortawesome/fontawesome-free": "^5.15.4", "@fortawesome/fontawesome-svg-core": "^1.2.34", @@ -14,8 +13,8 @@ "@fortawesome/free-solid-svg-icons": "^5.15.2", "@fortawesome/vue-fontawesome": "^0.1.10", "@radial-color-picker/vue-color-picker": "^2.3.0", - "@scrypted/client": "file:../client", "@scrypted/sdk": "file:../../../sdk", + "@scrypted/types": "file:../../../sdk/types", "apexcharts": "^3.28.3", "axios": "^0.19.2", "core-js": "^2.6.12", @@ -96,16 +95,10 @@ }, "../../../sdk": { "name": "@scrypted/sdk", - "version": "0.0.167", + "version": "0.0.173", "license": "ISC", "dependencies": { - "@babel/plugin-proposal-class-properties": "^7.14.5", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.14.5", - "@babel/plugin-proposal-numeric-separator": "^7.14.5", - "@babel/plugin-proposal-optional-chaining": "^7.14.5", - "@babel/plugin-transform-modules-commonjs": "^7.15.4", - "@babel/plugin-transform-typescript": "^7.15.8", - "@babel/preset-typescript": "^7.15.0", + "@babel/preset-typescript": "^7.16.7", "adm-zip": "^0.4.13", "axios": "^0.21.4", "babel-loader": "^8.2.3", @@ -115,7 +108,6 @@ "raw-loader": "^4.0.2", "rimraf": "^3.0.2", "tmp": "^0.2.1", - "ts-loader": "^9.2.6", "webpack": "^5.59.0" }, "bin": { @@ -136,12 +128,19 @@ "webpack-bundle-analyzer": "^4.5.0" } }, + "../../../sdk/types": { + "name": "@scrypted/types", + "version": "0.0.9", + "license": "ISC", + "devDependencies": {} + }, "../../sdk": { "extraneous": true }, "../client": { "name": "@scrypted/client", "version": "0.0.7", + "extraneous": true, "license": "ISC", "dependencies": { "@scrypted/sdk": "file:../../../sdk", @@ -2160,14 +2159,14 @@ "vue": "^2.5.21" } }, - "node_modules/@scrypted/client": { - "resolved": "../client", - "link": true - }, "node_modules/@scrypted/sdk": { "resolved": "../../../sdk", "link": true }, + "node_modules/@scrypted/types": { + "resolved": "../../../sdk/types", + "link": true + }, "node_modules/@soda/friendly-errors-webpack-plugin": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/@soda/friendly-errors-webpack-plugin/-/friendly-errors-webpack-plugin-1.8.1.tgz", @@ -21064,26 +21063,10 @@ "@radial-color-picker/rotator": "2.1.0" } }, - "@scrypted/client": { - "version": "file:../client", - "requires": { - "@scrypted/sdk": "file:../../../sdk", - "@types/engine.io-client": "^3.1.5", - "@types/lodash.clonedeep": "^4.5.6", - "engine.io-client": "^5.2.0", - "lodash.clonedeep": "^4.5.0" - } - }, "@scrypted/sdk": { "version": "file:../../../sdk", "requires": { - "@babel/plugin-proposal-class-properties": "^7.14.5", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.14.5", - "@babel/plugin-proposal-numeric-separator": "^7.14.5", - "@babel/plugin-proposal-optional-chaining": "^7.14.5", - "@babel/plugin-transform-modules-commonjs": "^7.15.4", - "@babel/plugin-transform-typescript": "^7.15.8", - "@babel/preset-typescript": "^7.15.0", + "@babel/preset-typescript": "^7.16.7", "@types/node": "^16.11.1", "@types/stringify-object": "^4.0.0", "adm-zip": "^0.4.13", @@ -21096,7 +21079,6 @@ "rimraf": "^3.0.2", "stringify-object": "^3.3.0", "tmp": "^0.2.1", - "ts-loader": "^9.2.6", "ts-node": "^10.4.0", "typedoc": "^0.22.8", "typescript-json-schema": "^0.50.1", @@ -21104,6 +21086,9 @@ "webpack-bundle-analyzer": "^4.5.0" } }, + "@scrypted/types": { + "version": "file:../../../sdk/types" + }, "@soda/friendly-errors-webpack-plugin": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/@soda/friendly-errors-webpack-plugin/-/friendly-errors-webpack-plugin-1.8.1.tgz", diff --git a/plugins/core/ui/package.json b/plugins/core/ui/package.json index 4a0151151..ce0546223 100644 --- a/plugins/core/ui/package.json +++ b/plugins/core/ui/package.json @@ -6,8 +6,7 @@ "serve": "vue-cli-service serve --open", "serve-server": "cd ../../../server && npm run serve", "build": "vue-cli-service build --dest ../fs/dist", - "lint": "vue-cli-service lint", - "postinstall": "(cd ../../../server && npm install); (cd ../client && npm install)" + "lint": "vue-cli-service lint" }, "dependencies": { "@fortawesome/fontawesome-free": "^5.15.4", @@ -17,7 +16,7 @@ "@fortawesome/free-solid-svg-icons": "^5.15.2", "@fortawesome/vue-fontawesome": "^0.1.10", "@radial-color-picker/vue-color-picker": "^2.3.0", - "@scrypted/client": "file:../client", + "@scrypted/types": "file:../../../sdk/types", "@scrypted/sdk": "file:../../../sdk", "apexcharts": "^3.28.3", "axios": "^0.19.2", diff --git a/plugins/core/ui/src/common/camera.ts b/plugins/core/ui/src/common/camera.ts index 32ddf821f..ab9c24e53 100644 --- a/plugins/core/ui/src/common/camera.ts +++ b/plugins/core/ui/src/common/camera.ts @@ -1,165 +1,33 @@ -import { ScryptedDevice, ScryptedMimeTypes, RTCAVMessage, MediaManager, VideoCamera, MediaObject, RTCAVSignalingOfferSetup, RequestMediaStreamOptions } from '@scrypted/sdk/types'; +import { RTCSignalingSendIceCandidate, RTCSignalingSession, ScryptedInterface, ScryptedDevice, ScryptedMimeTypes, RTCAVMessage, MediaManager, VideoCamera, MediaObject, RTCAVSignalingSetup, RequestMediaStreamOptions, RTCSignalingChannel } from '@scrypted/types'; -export async function streamCamera(mediaManager: MediaManager, device: ScryptedDevice & VideoCamera, getVideo: () => HTMLVideoElement, createPeerConnection: (configuration: RTCConfiguration) => RTCPeerConnection) { +async function startCameraLegacy(mediaManager: MediaManager, device: ScryptedDevice & VideoCamera & RTCSignalingChannel) { let selectedStream: RequestMediaStreamOptions; try { const streams = await device.getVideoStreamOptions(); - selectedStream = streams.find(stream => stream.container?.startsWith(ScryptedMimeTypes.RTCAVSignalingPrefix)); - if (selectedStream) - selectedStream.directMediaStream = true; - else - selectedStream = streams.find(stream => stream.container === 'rawvideo'); + selectedStream = streams.find(stream => stream.container === 'rawvideo'); } catch (e) { } const videoStream = await device.getVideoStream(selectedStream); - let trickle = true; - let pc: RTCPeerConnection; let json: RTCAVMessage; - let sentSdp = false; - if (videoStream.mimeType.startsWith(ScryptedMimeTypes.RTCAVSignalingPrefix)) { - trickle = false; - const buffer = await mediaManager.convertMediaObjectToBuffer( - videoStream, - videoStream.mimeType, - ); - const avsource: RTCAVSignalingOfferSetup = JSON.parse(buffer.toString()); + const offer = await mediaManager.convertMediaObjectToBuffer( + videoStream, + ScryptedMimeTypes.RTCAVOffer + ); + json = JSON.parse(offer.toString()); + let pc = new RTCPeerConnection(json.configuration); - pc = createPeerConnection({}) - if (avsource.datachannel) - pc.createDataChannel(avsource.datachannel.label, avsource.datachannel.dict); - // it's possible to do talkback to ring. - let useAudioTransceiver = false; - try { - if (avsource.audio?.direction === 'sendrecv') { - // doing sendrecv on safari requires a mic be attached, or it fails to connect. - const mic = await navigator.mediaDevices.getUserMedia({ video: false, audio: true }) - for (const track of mic.getTracks()) { - pc.addTrack(track); - } - } - else { - useAudioTransceiver = true; - } + const processCandidates = (result: Buffer) => { + const message: RTCAVMessage = JSON.parse(result.toString()); + for (const candidate of message.candidates) { + // console.log('remote candidate', candidate); + pc.addIceCandidate(candidate); } - catch (e) { - useAudioTransceiver = true; - } - if (useAudioTransceiver) - pc.addTransceiver("audio", avsource.audio); - pc.addTransceiver("video", avsource.video); - - const offer = await pc.createOffer({ - offerToReceiveVideo: true, - offerToReceiveAudio: true, - }); - await pc.setLocalDescription(offer); - } - else { - const offer = await mediaManager.convertMediaObjectToBuffer( - videoStream, - ScryptedMimeTypes.RTCAVOffer - ); - json = JSON.parse(offer.toString()); - pc = createPeerConnection(json.configuration); - } - try { - - pc.onconnectionstatechange = async () => { - console.log(pc.connectionState); - - const stats = await pc.getStats() - let selectedLocalCandidate - for (const { type, state, localCandidateId } of stats.values()) - if (type === 'candidate-pair' && state === 'succeeded' && localCandidateId) { - selectedLocalCandidate = localCandidateId - break - } - const isLocal = !!selectedLocalCandidate && stats.get(selectedLocalCandidate)?.candidateType === 'relay' - console.log('isLocal', isLocal); - }; - pc.onsignalingstatechange = () => console.log(pc.connectionState); - pc.ontrack = () => { - const mediaStream = new MediaStream( - pc.getReceivers().map((receiver) => receiver.track) - ); - getVideo().srcObject = mediaStream; - const remoteAudio = document.createElement("audio"); - remoteAudio.srcObject = mediaStream; - remoteAudio.play(); - console.log('done tracks'); - }; - - const processCandidates = (result: Buffer) => { - const message: RTCAVMessage = JSON.parse(result.toString()); - for (const candidate of message.candidates) { - // console.log('remote candidate', candidate); - pc.addIceCandidate(candidate); - } - }; - - pc.onicecandidate = async (evt) => { - if (!evt.candidate) { - if (!trickle) { - if (sentSdp) - return; - sentSdp = true; - const offer = await pc.createOffer({ - offerToReceiveAudio: true, - offerToReceiveVideo: true, - }); - console.log('offer', offer); - await pc.setLocalDescription(offer); - - const offerWithCandidates: RTCAVMessage = { - id: undefined, - candidates: [], - description: { - sdp: offer.sdp, - type: 'offer', - }, - configuration: {}, - }; - const mo = await mediaManager.createMediaObject( - Buffer.from(JSON.stringify(offerWithCandidates)), - ScryptedMimeTypes.RTCAVOffer - ); - const result = await mediaManager.convertMediaObjectToBuffer( - mo, - videoStream.mimeType - ); - const answer: RTCAVMessage = JSON.parse(result.toString()) - console.log('answer', answer); - await pc.setRemoteDescription(answer.description); - } - return; - } - if (!trickle) { - return; - } - // console.log('local candidate', evt.candidate); - const candidateObject: RTCAVMessage = { - id: json.id, - candidates: [evt.candidate], - description: null, - configuration: null, - }; - const mo = await mediaManager.createMediaObject( - Buffer.from(JSON.stringify(candidateObject)), - ScryptedMimeTypes.RTCAVAnswer - ); - const result = await mediaManager.convertMediaObjectToBuffer( - mo, - ScryptedMimeTypes.RTCAVOffer - ); - processCandidates(result); - }; - - if (!trickle) - return pc; + }; + (async () => { await pc.setRemoteDescription(json.description); const answer = await pc.createAnswer(); await pc.setLocalDescription(answer); @@ -180,26 +48,169 @@ export async function streamCamera(mediaManager: MediaManager, device: ScryptedD ); processCandidates(result); - (async () => { - const emptyObject: RTCAVMessage = { - id: json.id, - candidates: [], - description: null, - configuration: null, - }; - while (true) { - const mo = await mediaManager.createMediaObject( - Buffer.from(JSON.stringify(emptyObject)), - ScryptedMimeTypes.RTCAVAnswer - ); - const result = await mediaManager.convertMediaObjectToBuffer( - mo, - ScryptedMimeTypes.RTCAVOffer - ); - processCandidates(result); + const emptyObject: RTCAVMessage = { + id: json.id, + candidates: [], + description: null, + configuration: null, + }; + while (true) { + const mo = await mediaManager.createMediaObject( + Buffer.from(JSON.stringify(emptyObject)), + ScryptedMimeTypes.RTCAVAnswer + ); + const result = await mediaManager.convertMediaObjectToBuffer( + mo, + ScryptedMimeTypes.RTCAVOffer + ); + processCandidates(result); + } + })(); + console.log("done av offer"); + + pc.onicecandidate = async (evt) => { + if (!evt.candidate) { + return; + } + // console.log('local candidate', evt.candidate); + const candidateObject: RTCAVMessage = { + id: json.id, + candidates: [evt.candidate], + description: null, + configuration: null, + }; + const mo = await mediaManager.createMediaObject( + Buffer.from(JSON.stringify(candidateObject)), + ScryptedMimeTypes.RTCAVAnswer + ); + const result = await mediaManager.convertMediaObjectToBuffer( + mo, + ScryptedMimeTypes.RTCAVOffer + ); + processCandidates(result); + }; + + return pc; +} + +async function startCameraRtc(mediaManager: MediaManager, device: ScryptedDevice & VideoCamera & RTCSignalingChannel) { + const pc = new RTCPeerConnection(); + const gatheringPromise = new Promise(resolve => pc.onicegatheringstatechange = () => { + if (pc.iceGatheringState === 'complete') + resolve(undefined); + }); + + class SignalingSession implements RTCSignalingSession { + async onIceCandidate(candidate: RTCIceCandidate) { + await pc.addIceCandidate(candidate); + } + async createLocalDescription(type: 'offer' | 'answer', setup: RTCAVSignalingSetup, sendIceCandidate: RTCSignalingSendIceCandidate): Promise { + if (setup.datachannel) + pc.createDataChannel(setup.datachannel.label, setup.datachannel.dict); + // it's possible to do talkback to ring. + let useAudioTransceiver = false; + try { + if (setup.audio?.direction === 'sendrecv') { + // doing sendrecv on safari requires a mic be attached, or it fails to connect. + const mic = await navigator.mediaDevices.getUserMedia({ video: false, audio: true }); + for (const track of mic.getTracks()) { + pc.addTrack(track); + } + } + else { + useAudioTransceiver = true; + } } - })(); - console.log("done av offer"); + catch (e) { + useAudioTransceiver = true; + } + if (useAudioTransceiver) + pc.addTransceiver("audio", setup.audio); + pc.addTransceiver("video", setup.video); + + pc.onicecandidate = ev => { + sendIceCandidate?.(ev.candidate as any); + }; + + const toDescription = (init: RTCSessionDescriptionInit) => { + return { + type: init.type, + sdp: init.sdp, + } + } + + if (type === 'offer') { + let offer = await pc.createOffer({ + offerToReceiveAudio: true, + offerToReceiveVideo: true, + }); + const set = pc.setLocalDescription(offer); + if (sendIceCandidate) + return toDescription(offer); + await set; + await gatheringPromise; + offer = await pc.createOffer({ + offerToReceiveAudio: true, + offerToReceiveVideo: true, + }); + return toDescription(offer); + } + else { + let answer = await pc.createAnswer(); + const set = pc.setLocalDescription(answer); + if (sendIceCandidate) + return toDescription(answer); + await set; + await gatheringPromise; + answer = pc.currentLocalDescription || answer; + return toDescription(answer); + } + } + async setRemoteDescription(description: RTCSessionDescription) { + await pc.setRemoteDescription(description); + } + } + + device.startRTCSignalingSession(new SignalingSession()); + return pc; +} + +export async function streamCamera(mediaManager: MediaManager, device: ScryptedDevice & VideoCamera & RTCSignalingChannel, getVideo: () => HTMLVideoElement) { + let pc: RTCPeerConnection; + + if (device.interfaces.includes(ScryptedInterface.RTCSignalingChannel)) { + pc = await startCameraRtc(mediaManager, device); + } + else { + // todo: stop using the weird buffer convertor as a shim a signaling channel. + pc = await startCameraLegacy(mediaManager, device); + } + + try { + pc.onconnectionstatechange = async () => { + console.log(pc.connectionState); + + const stats = await pc.getStats() + let selectedLocalCandidate + for (const { type, state, localCandidateId } of stats.values()) + if (type === 'candidate-pair' && state === 'succeeded' && localCandidateId) { + selectedLocalCandidate = localCandidateId + break + } + const isLocal = !!selectedLocalCandidate && stats.get(selectedLocalCandidate)?.type === "local-candidate"; + console.log('isLocal', isLocal, stats.get(selectedLocalCandidate)); + }; + pc.onsignalingstatechange = () => console.log(pc.connectionState); + pc.ontrack = () => { + const mediaStream = new MediaStream( + pc.getReceivers().map((receiver) => receiver.track) + ); + getVideo().srcObject = mediaStream; + const remoteAudio = document.createElement("audio"); + remoteAudio.srcObject = mediaStream; + remoteAudio.play(); + console.log('done tracks'); + }; return pc; } diff --git a/plugins/core/ui/src/common/mixin.ts b/plugins/core/ui/src/common/mixin.ts index 3d862d019..e7953b276 100644 --- a/plugins/core/ui/src/common/mixin.ts +++ b/plugins/core/ui/src/common/mixin.ts @@ -1,4 +1,4 @@ -import { MixinProvider, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, SystemManager } from "@scrypted/sdk/types"; +import { MixinProvider, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, SystemManager } from "@scrypted/types"; export async function setMixin(systemManager: SystemManager, device: ScryptedDevice, mixinId: string, enabled: boolean) { const plugins = await systemManager.getComponent( diff --git a/plugins/core/ui/src/components/Device.vue b/plugins/core/ui/src/components/Device.vue index fcd498fe1..0db017798 100644 --- a/plugins/core/ui/src/components/Device.vue +++ b/plugins/core/ui/src/components/Device.vue @@ -398,7 +398,7 @@ import { hasFixedPhysicalLocation, getInterfaceFriendlyName, } from "./helpers"; -import { ScryptedInterface } from "@scrypted/sdk/types"; +import { ScryptedInterface } from "@scrypted/types"; import Notifier from "../interfaces/Notifier.vue"; import OnOff from "../interfaces/OnOff.vue"; import Brightness from "../interfaces/Brightness.vue"; diff --git a/plugins/core/ui/src/components/automation/Automation.vue b/plugins/core/ui/src/components/automation/Automation.vue index 9f57c90da..f84305984 100644 --- a/plugins/core/ui/src/components/automation/Automation.vue +++ b/plugins/core/ui/src/components/automation/Automation.vue @@ -102,7 +102,7 @@