diff --git a/src-admin/package-lock.json b/src-admin/package-lock.json index c43fcc6..33320b0 100644 --- a/src-admin/package-lock.json +++ b/src-admin/package-lock.json @@ -9,8 +9,8 @@ "version": "0.4.4", "dependencies": { "@foxriver76/iob-component-lib": "^0.2.0", - "@iobroker/adapter-react-v5": "^7.4.17", - "@iobroker/dm-gui-components": "^7.4.17", + "@iobroker/adapter-react-v5": "^7.4.18", + "@iobroker/dm-gui-components": "^7.4.18", "@iobroker/type-detector": "^4.1.1", "@types/react-dom": "^18.3.5", "@types/uuid": "^10.0.0", @@ -988,9 +988,9 @@ } }, "node_modules/@iobroker/adapter-react-v5": { - "version": "7.4.17", - "resolved": "https://registry.npmjs.org/@iobroker/adapter-react-v5/-/adapter-react-v5-7.4.17.tgz", - "integrity": "sha512-XOCgA4q2D/9omTtXkrzux12QPoF0teTnc1DZ6hLNxwWzPR2r/JKI+0fE3qJ+oWx6HnPZPdwHPJd/EowunTnKTQ==", + "version": "7.4.18", + "resolved": "https://registry.npmjs.org/@iobroker/adapter-react-v5/-/adapter-react-v5-7.4.18.tgz", + "integrity": "sha512-Lll02b7P8YdtEqsQLBE1OtPhC58ZP6JMWsoDlr0InC6RTvzvNtG0YPWI6W3rqqmXtLhF0V+oGTMSUQqz5pC1hw==", "license": "MIT", "dependencies": { "@emotion/react": "^11.14.0", @@ -1119,13 +1119,13 @@ } }, "node_modules/@iobroker/dm-gui-components": { - "version": "7.4.17", - "resolved": "https://registry.npmjs.org/@iobroker/dm-gui-components/-/dm-gui-components-7.4.17.tgz", - "integrity": "sha512-DA93jQ5hHuCnE22eBjM+a+RX3objAuBhWdfYRs+sNtFvHRV+uIr5ULwEQaDPJxc8ohJTDl2So5ulaPlXJ5hG/g==", + "version": "7.4.18", + "resolved": "https://registry.npmjs.org/@iobroker/dm-gui-components/-/dm-gui-components-7.4.18.tgz", + "integrity": "sha512-qJpjyMd/h8Kh5O9p6nDxo0HOkWzgNQfMELcH8ZcAMiN3UtuaJCAjtUlXCgoeNJGAdxazh7FqyjUs2ifklIS75A==", "license": "MIT", "dependencies": { - "@iobroker/adapter-react-v5": "7.4.17", - "@iobroker/json-config": "7.4.17" + "@iobroker/adapter-react-v5": "7.4.18", + "@iobroker/json-config": "7.4.18" } }, "node_modules/@iobroker/js-controller-common": { @@ -1185,11 +1185,11 @@ } }, "node_modules/@iobroker/json-config": { - "version": "7.4.17", - "resolved": "https://registry.npmjs.org/@iobroker/json-config/-/json-config-7.4.17.tgz", - "integrity": "sha512-yFyNhkfhWPFXDOXAiCIeypAq4JWAzBLkmWK8sHNi+iOWQTKdMCzRXzLKsjRzhiH6AOUkP2/tXIScZXOdfSRJQw==", + "version": "7.4.18", + "resolved": "https://registry.npmjs.org/@iobroker/json-config/-/json-config-7.4.18.tgz", + "integrity": "sha512-8FzFubBPLNsRdQ6ker+E+3c8w+ygQQc/EILzCmkEbPPmI/jKI8Xt9lCLx7U0xUWkRdob4Wl21TpBwt4Z/oIMvw==", "dependencies": { - "@iobroker/adapter-react-v5": "7.4.17", + "@iobroker/adapter-react-v5": "7.4.18", "@mui/x-date-pickers": "^7.24.1", "crypto-js": "^4.2.0", "react-ace": "^13.0.0", diff --git a/src-admin/package.json b/src-admin/package.json index 79caf69..feccc80 100644 --- a/src-admin/package.json +++ b/src-admin/package.json @@ -7,8 +7,8 @@ }, "dependencies": { "@foxriver76/iob-component-lib": "^0.2.0", - "@iobroker/adapter-react-v5": "^7.4.17", - "@iobroker/dm-gui-components": "^7.4.17", + "@iobroker/adapter-react-v5": "^7.4.18", + "@iobroker/dm-gui-components": "^7.4.18", "@iobroker/type-detector": "^4.1.1", "@types/react-dom": "^18.3.5", "@types/uuid": "^10.0.0", diff --git a/src-admin/src/App.tsx b/src-admin/src/App.tsx index a350436..7769982 100644 --- a/src-admin/src/App.tsx +++ b/src-admin/src/App.tsx @@ -1,7 +1,7 @@ import { StyledEngineProvider, ThemeProvider } from '@mui/material/styles'; import React from 'react'; -import { IconButton } from '@foxriver76/iob-component-lib'; +import { IconButton as IconButton76 } from '@foxriver76/iob-component-lib'; import { AppBar, Dialog, @@ -13,9 +13,11 @@ import { Tab, Tabs, Tooltip, + Snackbar, + IconButton, } from '@mui/material'; -import { Help as IconHelp, SignalCellularOff as IconNotAlive } from '@mui/icons-material'; +import { Help as IconHelp, SignalCellularOff as IconNotAlive, Close } from '@mui/icons-material'; import { AdminConnection, @@ -107,6 +109,7 @@ interface AppState extends GenericAppState { } | null; welcomeDialogShowed: boolean; updatePassTrigger: number; + identifyUuids: { uuid: string; ts: number }[]; } class App extends GenericApp { @@ -116,6 +119,8 @@ class App extends GenericApp { private refreshTimer: ReturnType | null = null; + private readonly identifyTimers: Record> = {}; + private connectToBackEndInterval: ReturnType | null = null; private connectToBackEndCounter = 0; @@ -175,7 +180,8 @@ class App extends GenericApp { welcomeDialogShowed: false, inProcessing: null, updatePassTrigger: 1, - }); + identifyUuids: [], + } as Partial); this.alert = window.alert; window.alert = text => this.showToast(text); @@ -407,6 +413,39 @@ class App extends GenericApp { this.refreshTimer = null; this.refreshBackendSubscription(); }, 5_000); + } else if (update.command === 'identifyPopup') { + // Some device in ioBroker should be identified + if (update.identifyUuid) { + if (this.identifyTimers[update.identifyUuid]) { + clearTimeout(this.identifyTimers[update.identifyUuid]); + } + const identifyUuids = [...this.state.identifyUuids]; + if (!identifyUuids.find(it => it.uuid === update.identifyUuid)) { + identifyUuids.push({ + uuid: update.identifyUuid, + ts: Date.now() + (update.identifySeconds || 15) * 1000, + }); + } + + this.setState({ identifyUuids }); + + this.identifyTimers[update.identifyUuid || ''] = setTimeout( + () => { + const now = Date.now(); + const identifyUuids = [...this.state.identifyUuids]; + // Delete all outdated identifies + for (let i = identifyUuids.length - 1; i >= 0; i--) { + if (identifyUuids[i].ts <= now) { + identifyUuids.splice(i, 1); + } + } + this.setState({ identifyUuids }); + }, + (update.identifySeconds || 15) * 1000, + ); + } else { + console.warn('No identifyUuid'); + } } else { this.controllerMessageHandler && this.controllerMessageHandler(update); } @@ -451,6 +490,8 @@ class App extends GenericApp { this.refreshTimer = null; } + Object.values(this.identifyTimers).forEach(timer => clearTimeout(timer)); + try { this.socket.unsubscribeState(`system.adapter.matter.${this.instance}.alive`, this.onAlive); await this.socket.unsubscribeFromInstance(`matter.${this.instance}`, 'gui', this.onBackendUpdates); @@ -555,6 +596,7 @@ class App extends GenericApp { checkLicenseOnAdd={(type: 'addBridge' | 'addDevice' | 'addDeviceToBridge', matter: MatterConfig) => this.checkLicenseOnAdd(type, matter) } + identifyUuids={this.state.identifyUuids} /> ); } @@ -590,6 +632,7 @@ class App extends GenericApp { }} showToast={(text: string) => this.showToast(text)} checkLicenseOnAdd={(matter: MatterConfig) => this.checkLicenseOnAdd('addDevice', matter)} + identifyUuids={this.state.identifyUuids} /> ); } @@ -684,6 +727,88 @@ class App extends GenericApp { ); } + renderIdentifyToast(): React.JSX.Element[] | null { + if (!this.state.identifyUuids.length) { + return null; + } + + return this.state.identifyUuids.map(it => { + // Try to find information about this device + let name; + if (this.state.matter?.bridges?.length) { + for (const bridge of this.state.matter.bridges) { + if (bridge.uuid === it.uuid) { + name = bridge.name; + break; + } else if (bridge.list?.length) { + for (const device of bridge.list) { + if (device.uuid === it.uuid) { + name = + typeof device.name === 'object' + ? device.name[I18n.getLanguage()] || device.name.en + : device.name; + break; + } + } + } + } + } + + if (!name && this.state.matter?.devices?.length) { + for (const device of this.state.matter.devices) { + if (device.uuid === it.uuid) { + name = + typeof device.name === 'object' + ? device.name[I18n.getLanguage()] || device.name.en + : device.name; + break; + } + } + } + + name = name || it.uuid; + + return ( + { + const identifyUuids = [...this.state.identifyUuids]; + const i = identifyUuids.findIndex(id => id.uuid === it.uuid); + if (i !== -1) { + identifyUuids.splice(i, 1); + this.setState({ identifyUuids }); + } + }} + message={{I18n.t(`Identifying device %s`, name)}} + action={[ + { + const identifyUuids = [...this.state.identifyUuids]; + const i = identifyUuids.findIndex(id => id.uuid === it.uuid); + if (i !== -1) { + identifyUuids.splice(i, 1); + this.setState({ identifyUuids }); + } + }} + size="large" + > + + , + ]} + /> + ); + }); + } + render(): React.JSX.Element { if (!this.state.ready) { return ( @@ -699,6 +824,7 @@ class App extends GenericApp { {this.renderToast()} + {this.renderIdentifyToast()} {this.renderProgressDialog()} {this.renderWelcomeDialog()}
{ justifyContent: 'center', }} > - { @@ -1364,7 +1365,7 @@ export class Bridges extends BridgesAndDevices { = { +export const STYLES: Record = { vendorIcon: { width: 24, height: 24, @@ -68,6 +68,18 @@ export const STYLES: Record = { tooltip: { pointerEvents: 'none', }, + animation: 'blink .5s linear infinite', + '@keyframes spin': (theme: IobTheme): any => ({ + '0%': { + backgroundColor: theme.palette.background.paper, + }, + '50%': { + backgroundColor: theme.palette.mode === 'dark' ? '#958200' : '#ffe441', + }, + '100%': { + backgroundColor: theme.palette.background.paper, + }, + }), } as const; export interface BridgesAndDevicesProps { @@ -88,6 +100,7 @@ export interface BridgesAndDevicesProps { updateConfig: (config: MatterConfig) => void; updateNodeStates: (states: { [uuid: string]: NodeStateResponse }) => void; inProcessing: Processing; + identifyUuids: { uuid: string; ts: number }[]; } export interface BridgesAndDevicesState { @@ -514,6 +527,26 @@ class BridgesAndDevices | undefined { + return this.props.identifyUuids.find(it => it.uuid === uuid) + ? { + animation: 'bd-blink .5s linear infinite', + '@keyframes bd-blink': { + '0%': { + backgroundColor: this.props.theme.palette.background.paper, + }, + '50%': { + backgroundColor: this.props.theme.palette.mode === 'dark' ? '#958200' : '#ffe441', + }, + '100%': { + backgroundColor: this.props.theme.palette.background.paper, + }, + }, + } + : undefined; + } + getInProcessing(uuid: string): false | 'inQueue' | 'processing' { if (!this.props.inProcessing) { return false; diff --git a/src-admin/src/Tabs/Devices.tsx b/src-admin/src/Tabs/Devices.tsx index 0a10635..a6b9123 100644 --- a/src-admin/src/Tabs/Devices.tsx +++ b/src-admin/src/Tabs/Devices.tsx @@ -683,6 +683,7 @@ class Devices extends BridgesAndDevices { {this.renderProcessOverlay(device.uuid, device.deleted)} diff --git a/src-admin/src/i18n/de.json b/src-admin/src/i18n/de.json index 9009cd8..6bed7d7 100644 --- a/src-admin/src/i18n/de.json +++ b/src-admin/src/i18n/de.json @@ -89,6 +89,7 @@ "General": "Allgemein", "Generate new pairing code": "Neuen Pairing-Code generieren", "Hide unsupported devices": "Nicht unterstützte Geräte ausblenden", + "Identifying device %s": "Identifiziere \"%s\"", "If your device has more then one active network interface and you have issues try limiting it to one interface": "Wenn Ihr Gerät über mehr als eine aktive Netzwerkschnittstelle verfügt und Sie Probleme haben, versuchen Sie, es auf eine Schnittstelle zu beschränken.", "Info about Alexa Bridge": "Bitte beachten: Aufgrund von Einschränkungen des Amazon Alexa-Systems kann pro ioBroker-Host nur eine Bridge oder ein Gerät mit Alexa gekoppelt werden! Diese Auswahl bestimmt, welche Bridge mit dem Alexa-System gekoppelt wird.", "Instance is not alive": "Instanz ist nicht aktiv", diff --git a/src-admin/src/i18n/en.json b/src-admin/src/i18n/en.json index 36bc5aa..f1616a2 100644 --- a/src-admin/src/i18n/en.json +++ b/src-admin/src/i18n/en.json @@ -89,6 +89,7 @@ "General": "General", "Generate new pairing code": "Generate new pairing code", "Hide unsupported devices": "Hide unsupported devices", + "Identifying device %s": "Identifying device \"%s\"", "If your device has more then one active network interface and you have issues try limiting it to one interface": "If your device has more then one active network interface and you have issues try limiting it to one interface.", "Info about Alexa Bridge": "Please note: Due to limitations of the Amazon Alexa system, only one bridge or device can be paired with Alexa per ioBroker host! This selection determines which bridge will be paired with the Alexa system.", "Instance is not alive": "Instance is not alive", diff --git a/src-admin/src/i18n/es.json b/src-admin/src/i18n/es.json index b7db607..0c76284 100644 --- a/src-admin/src/i18n/es.json +++ b/src-admin/src/i18n/es.json @@ -89,6 +89,7 @@ "General": "General", "Generate new pairing code": "Generar nuevo código de emparejamiento", "Hide unsupported devices": "Ocultar dispositivos no compatibles", + "Identifying device %s": "Identificando el dispositivo \"%s\"", "If your device has more then one active network interface and you have issues try limiting it to one interface": "Si su dispositivo tiene más de una interfaz de red activa y tiene problemas, intente limitarlo a una interfaz.", "Info about Alexa Bridge": "Tenga en cuenta: Debido a las limitaciones del sistema Amazon Alexa, solo se puede emparejar un puente o dispositivo con Alexa por host de ioBroker. Esta selección determina qué puente se emparejará con el sistema Alexa.", "Instance is not alive": "La instancia no está viva", diff --git a/src-admin/src/i18n/fr.json b/src-admin/src/i18n/fr.json index 5aeaa38..545a41e 100644 --- a/src-admin/src/i18n/fr.json +++ b/src-admin/src/i18n/fr.json @@ -89,6 +89,7 @@ "General": "Général", "Generate new pairing code": "Générer un nouveau code d'appairage", "Hide unsupported devices": "Masquer les appareils non pris en charge", + "Identifying device %s": "Identification du périphérique \"%s\"", "If your device has more then one active network interface and you have issues try limiting it to one interface": "Si votre appareil dispose de plusieurs interfaces réseau actives et que vous rencontrez des problèmes, essayez de le limiter à une seule interface.", "Info about Alexa Bridge": "Remarque : en raison des limitations du système Amazon Alexa, un seul pont ou appareil peut être associé à Alexa par hôte ioBroker ! Cette sélection détermine quel pont sera associé au système Alexa.", "Instance is not alive": "L'instance n'est pas vivante", diff --git a/src-admin/src/i18n/it.json b/src-admin/src/i18n/it.json index 7dcfdef..5b4245b 100644 --- a/src-admin/src/i18n/it.json +++ b/src-admin/src/i18n/it.json @@ -89,6 +89,7 @@ "General": "Generale", "Generate new pairing code": "Genera un nuovo codice di accoppiamento", "Hide unsupported devices": "Nascondi i dispositivi non supportati", + "Identifying device %s": "Identificazione del dispositivo \"%s\"", "If your device has more then one active network interface and you have issues try limiting it to one interface": "Se il tuo dispositivo ha più di un'interfaccia di rete attiva e riscontri problemi, prova a limitarla a un'unica interfaccia.", "Info about Alexa Bridge": "Nota bene: a causa delle limitazioni del sistema Amazon Alexa, solo un bridge o dispositivo può essere associato ad Alexa per host ioBroker! Questa selezione determina quale bridge verrà associato al sistema Alexa.", "Instance is not alive": "L'istanza non è attiva", diff --git a/src-admin/src/i18n/nl.json b/src-admin/src/i18n/nl.json index cbf03f3..27a508c 100644 --- a/src-admin/src/i18n/nl.json +++ b/src-admin/src/i18n/nl.json @@ -89,6 +89,7 @@ "General": "Algemeen", "Generate new pairing code": "Genereer een nieuwe koppelingscode", "Hide unsupported devices": "Verberg niet-ondersteunde apparaten", + "Identifying device %s": "Identificatie apparaat \"%s\"", "If your device has more then one active network interface and you have issues try limiting it to one interface": "Als uw apparaat meer dan één actieve netwerkinterface heeft en u problemen ondervindt, probeer het dan te beperken tot één interface.", "Info about Alexa Bridge": "Let op: Vanwege beperkingen van het Amazon Alexa-systeem kan er slechts één bridge of apparaat worden gekoppeld aan Alexa per ioBroker-host! Deze selectie bepaalt welke bridge wordt gekoppeld aan het Alexa-systeem.", "Instance is not alive": "Instantie is niet levend", diff --git a/src-admin/src/i18n/pl.json b/src-admin/src/i18n/pl.json index 17c79cd..d64ce95 100644 --- a/src-admin/src/i18n/pl.json +++ b/src-admin/src/i18n/pl.json @@ -89,6 +89,7 @@ "General": "Ogólny", "Generate new pairing code": "Wygeneruj nowy kod parowania", "Hide unsupported devices": "Ukryj nieobsługiwane urządzenia", + "Identifying device %s": "Identyfikowanie urządzenia \"%s\"", "If your device has more then one active network interface and you have issues try limiting it to one interface": "Jeśli Twoje urządzenie ma więcej niż jeden aktywny interfejs sieciowy i masz problemy, spróbuj ograniczyć je do jednego interfejsu.", "Info about Alexa Bridge": "Uwaga: Ze względu na ograniczenia systemu Amazon Alexa, tylko jeden most lub urządzenie może być sparowane z Alexą na hosta ioBroker! Ten wybór określa, który most zostanie sparowany z systemem Alexa.", "Instance is not alive": "Instancja nie jest żywa", diff --git a/src-admin/src/i18n/pt.json b/src-admin/src/i18n/pt.json index 60a66c9..9e08a3e 100644 --- a/src-admin/src/i18n/pt.json +++ b/src-admin/src/i18n/pt.json @@ -89,6 +89,7 @@ "General": "Em geral", "Generate new pairing code": "Gerar novo código de pareamento", "Hide unsupported devices": "Ocultar dispositivos não suportados", + "Identifying device %s": "Identificando o dispositivo \"%s\"", "If your device has more then one active network interface and you have issues try limiting it to one interface": "Se o seu dispositivo tiver mais de uma interface de rede ativa e você tiver problemas, tente limitá-lo a uma interface.", "Info about Alexa Bridge": "Observação: devido às limitações do sistema Amazon Alexa, apenas uma ponte ou dispositivo pode ser pareado com Alexa por host ioBroker! Esta seleção determina qual ponte será pareada com o sistema Alexa.", "Instance is not alive": "A instância não está viva", diff --git a/src-admin/src/i18n/ru.json b/src-admin/src/i18n/ru.json index e72356a..8a348ee 100644 --- a/src-admin/src/i18n/ru.json +++ b/src-admin/src/i18n/ru.json @@ -89,6 +89,7 @@ "General": "Основные", "Generate new pairing code": "Создать новый код сопряжения", "Hide unsupported devices": "Скрыть неподдерживаемые устройства", + "Identifying device %s": "Идентификация устройства \"%s\"", "If your device has more then one active network interface and you have issues try limiting it to one interface": "Если на вашем устройстве имеется более одного активного сетевого интерфейса и у вас возникли проблемы, попробуйте ограничить его одним интерфейсом.", "Info about Alexa Bridge": "Обратите внимание: из-за ограничений системы Amazon Alexa, только один мост или устройство может быть сопряжено с Alexa на один хост ioBroker! Этот выбор определяет, какой мост будет сопряжен с системой Alexa.", "Instance is not alive": "Экземпляр не выполняется", diff --git a/src-admin/src/i18n/uk.json b/src-admin/src/i18n/uk.json index 3244b28..b93ce0c 100644 --- a/src-admin/src/i18n/uk.json +++ b/src-admin/src/i18n/uk.json @@ -89,6 +89,7 @@ "General": "Загальний", "Generate new pairing code": "Створіть новий код сполучення", "Hide unsupported devices": "Приховати непідтримувані пристрої", + "Identifying device %s": "Ідентифікація пристрою \"%s\"", "If your device has more then one active network interface and you have issues try limiting it to one interface": "Якщо ваш пристрій має більше ніж один активний мережевий інтерфейс і у вас є проблеми, спробуйте обмежити його одним інтерфейсом.", "Info about Alexa Bridge": "Будь ласка, зверніть увагу: через обмеження системи Amazon Alexa лише один міст або пристрій можна поєднати з Alexa на одному хості ioBroker! Цей вибір визначає, який міст буде сполучено з системою Alexa.", "Instance is not alive": "Примірник не живий", diff --git a/src-admin/src/i18n/zh-cn.json b/src-admin/src/i18n/zh-cn.json index d7cb2d2..660e28b 100644 --- a/src-admin/src/i18n/zh-cn.json +++ b/src-admin/src/i18n/zh-cn.json @@ -89,6 +89,7 @@ "General": "一般的", "Generate new pairing code": "生成新的配对码", "Hide unsupported devices": "隐藏不支持的设备", + "Identifying device %s": "正在识别设备 \"%s\"", "If your device has more then one active network interface and you have issues try limiting it to one interface": "如果您的设备具有多个活动网络接口并且您遇到问题,请尝试将其限制为一个接口。", "Info about Alexa Bridge": "请注意:由于 Amazon Alexa 系统的限制,每个 ioBroker 主机只能将一个桥接器或设备与 Alexa 配对!此选择决定哪个桥接器将与 Alexa 系统配对。", "Instance is not alive": "实例不存在", diff --git a/src-admin/src/types.d.ts b/src-admin/src/types.d.ts index 2b90a2b..4f82b79 100644 --- a/src-admin/src/types.d.ts +++ b/src-admin/src/types.d.ts @@ -162,11 +162,17 @@ export interface GUIMessage { | 'reconnect' | 'progress' | 'processing' + | 'identifyPopup' | 'updateController'; states?: { [uuid: string]: NodeStateResponse }; device?: CommissionableDevice; processing?: { id: string; inProgress: boolean }[] | null; + /** Used for identify popup */ + identifyUuid?: string; + /** Used for identify popup. How long to blink */ + identifySeconds?: number; + progress?: { close?: boolean; title?: string;