Skip to content

Commit

Permalink
Implemented in GUI "identifyPopup": #264
Browse files Browse the repository at this point in the history
  • Loading branch information
GermanBluefox committed Jan 25, 2025
1 parent 48ebfab commit 567e5b4
Show file tree
Hide file tree
Showing 18 changed files with 201 additions and 23 deletions.
28 changes: 14 additions & 14 deletions src-admin/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions src-admin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
134 changes: 130 additions & 4 deletions src-admin/src/App.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -107,6 +109,7 @@ interface AppState extends GenericAppState {
} | null;
welcomeDialogShowed: boolean;
updatePassTrigger: number;
identifyUuids: { uuid: string; ts: number }[];
}

class App extends GenericApp<GenericAppProps, AppState> {
Expand All @@ -116,6 +119,8 @@ class App extends GenericApp<GenericAppProps, AppState> {

private refreshTimer: ReturnType<typeof setTimeout> | null = null;

private readonly identifyTimers: Record<string, ReturnType<typeof setTimeout>> = {};

private connectToBackEndInterval: ReturnType<typeof setInterval> | null = null;

private connectToBackEndCounter = 0;
Expand Down Expand Up @@ -175,7 +180,8 @@ class App extends GenericApp<GenericAppProps, AppState> {
welcomeDialogShowed: false,
inProcessing: null,
updatePassTrigger: 1,
});
identifyUuids: [],
} as Partial<AppState>);

this.alert = window.alert;
window.alert = text => this.showToast(text);
Expand Down Expand Up @@ -407,6 +413,39 @@ class App extends GenericApp<GenericAppProps, AppState> {
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);
}
Expand Down Expand Up @@ -451,6 +490,8 @@ class App extends GenericApp<GenericAppProps, AppState> {
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);
Expand Down Expand Up @@ -555,6 +596,7 @@ class App extends GenericApp<GenericAppProps, AppState> {
checkLicenseOnAdd={(type: 'addBridge' | 'addDevice' | 'addDeviceToBridge', matter: MatterConfig) =>
this.checkLicenseOnAdd(type, matter)
}
identifyUuids={this.state.identifyUuids}
/>
);
}
Expand Down Expand Up @@ -590,6 +632,7 @@ class App extends GenericApp<GenericAppProps, AppState> {
}}
showToast={(text: string) => this.showToast(text)}
checkLicenseOnAdd={(matter: MatterConfig) => this.checkLicenseOnAdd('addDevice', matter)}
identifyUuids={this.state.identifyUuids}
/>
);
}
Expand Down Expand Up @@ -684,6 +727,88 @@ class App extends GenericApp<GenericAppProps, AppState> {
);
}

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 (
<Snackbar
key={it.uuid}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
open={!0}
onClose={() => {
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={<span>{I18n.t(`Identifying device %s`, name)}</span>}
action={[
<IconButton
key="close"
aria-label="Close"
color="inherit"
className={this.props.classes?.close}
onClick={() => {
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"
>
<Close />
</IconButton>,
]}
/>
);
});
}

render(): React.JSX.Element {
if (!this.state.ready) {
return (
Expand All @@ -699,6 +824,7 @@ class App extends GenericApp<GenericAppProps, AppState> {
<StyledEngineProvider injectFirst>
<ThemeProvider theme={this.state.theme}>
{this.renderToast()}
{this.renderIdentifyToast()}
{this.renderProgressDialog()}
{this.renderWelcomeDialog()}
<div
Expand Down Expand Up @@ -766,7 +892,7 @@ class App extends GenericApp<GenericAppProps, AppState> {
justifyContent: 'center',
}}
>
<IconButton
<IconButton76
iconColor="warning"
noBackground
icon="noConnection"
Expand Down
5 changes: 3 additions & 2 deletions src-admin/src/Tabs/Bridges.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import {
Tooltip,
} from '@mui/material';

import { I18n, SelectID, type IobTheme, IconDeviceType } from '@iobroker/adapter-react-v5';
import { I18n, SelectID, type IobTheme, IconDeviceType, Utils } from '@iobroker/adapter-react-v5';

import InfoBox from '../components/InfoBox';
import DeviceDialog, { SUPPORTED_DEVICES } from '../components/DeviceDialog';
Expand Down Expand Up @@ -1231,6 +1231,7 @@ export class Bridges extends BridgesAndDevices<BridgesProps, BridgesState> {
<TableRow
key={devIndex}
style={{ opacity: device.enabled && bridge.enabled ? 1 : 0.4 }}
sx={this.getBlinkingSx(device.uuid)}
>
<TableCell style={{ border: 0, borderBottomLeftRadius: isLast ? 4 : 0 }} />
<TableCell>
Expand Down Expand Up @@ -1364,7 +1365,7 @@ export class Bridges extends BridgesAndDevices<BridgesProps, BridgesState> {
<React.Fragment key={bridgeIndex}>
<TableRow
style={{ opacity: bridge.enabled ? 1 : 0.4, position: 'relative' }}
sx={styles.bridgeButtonsAndTitle}
sx={Utils.getStyle(this.props.theme, styles.bridgeButtonsAndTitle, this.getBlinkingSx(bridge.uuid))}
>
<TableCell
style={{
Expand Down
35 changes: 34 additions & 1 deletion src-admin/src/Tabs/BridgesAndDevices.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,26 @@ import type {
import { formatPairingCode, getTranslation } from '../Utils';
import type { ActionButton, BackEndCommandJsonFormOptions, JsonFormSchema } from '@iobroker/dm-utils';

export const STYLES: Record<string, React.CSSProperties> = {
export const STYLES: Record<string, any> = {
vendorIcon: {
width: 24,
height: 24,
},
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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -514,6 +527,26 @@ class BridgesAndDevices<TProps extends BridgesAndDevicesProps, TState extends Br
);
}

// eslint-disable-next-line react/no-unused-class-component-methods
getBlinkingSx(uuid: string): Record<string, any> | 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;
Expand Down
1 change: 1 addition & 0 deletions src-admin/src/Tabs/Devices.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -683,6 +683,7 @@ class Devices extends BridgesAndDevices<DevicesProps, DevicesState> {
<TableRow
key={index}
style={{ opacity: device.enabled ? 1 : 0.4, position: 'relative' }}
sx={this.getBlinkingSx(device.uuid)}
>
<TableCell>
{this.renderProcessOverlay(device.uuid, device.deleted)}
Expand Down
1 change: 1 addition & 0 deletions src-admin/src/i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading

0 comments on commit 567e5b4

Please sign in to comment.