Skip to content

Commit

Permalink
Start working on Matter Socket
Browse files Browse the repository at this point in the history
  • Loading branch information
GermanBluefox committed Oct 19, 2023
1 parent a3a0b97 commit aea4e73
Show file tree
Hide file tree
Showing 8 changed files with 522 additions and 204 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"@alcalzone/release-script-plugin-iobroker": "^3.6.0",
"@alcalzone/release-script-plugin-license": "^3.5.9",
"@types/iobroker": "^5.0.6",
"@types/node": "^20.8.6",
"@types/node": "^20.8.7",
"@typescript-eslint/eslint-plugin": "^6.8.0",
"chai": "^4.3.10",
"eslint": "^8.51.0",
Expand Down
16 changes: 9 additions & 7 deletions src-admin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
"private": true,
"dependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@iobroker/adapter-react-v5": "^4.6.6",
"@mui/icons-material": "^5.14.13",
"@mui/material": "^5.14.13",
"@mui/styles": "^5.14.13",
"@sentry/browser": "^7.74.0",
"@sentry/integrations": "^7.74.0",
"@iobroker/adapter-react-v5": "^4.6.7",
"@mui/icons-material": "^5.14.14",
"@mui/material": "^5.14.14",
"@mui/styles": "^5.14.14",
"@sentry/browser": "^7.74.1",
"@sentry/integrations": "^7.74.1",
"babel-eslint": "^10.1.0",
"eslint": "^8.51.0",
"eslint-config-airbnb": "^19.0.4",
Expand All @@ -19,11 +19,13 @@
"eslint-plugin-only-warn": "^1.1.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"iobroker.type-detector": "^2.0.4",
"iobroker.type-detector": "^2.0.6",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-icons": "^4.11.0",
"react-inlinesvg": "^4.0.6",
"react-native-svg": "^13.14.0",
"react-qr-code": "^2.0.12",
"react-scripts": "^5.0.1",
"uuid": "^9.0.1"
},
Expand Down
94 changes: 92 additions & 2 deletions src-admin/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,14 @@ import {
AppBar,
Tabs,
Tab,
IconButton,
} from '@mui/material';

import {
SignalWifiConnectedNoInternet4 as IconNoConnection,
SignalCellularOff as IconNotAlive,
} from '@mui/icons-material';

import GenericApp from '@iobroker/adapter-react-v5/GenericApp';
import { I18n, Loader, AdminConnection } from '@iobroker/adapter-react-v5';

Expand Down Expand Up @@ -68,9 +74,31 @@ class App extends GenericApp {
super(props, extendedProps);

this.state.selectedTab = window.localStorage.getItem(`${this.adapterName}.${this.instance}.selectedTab`) || 'controller';
this.state.alive = false;
this.state.backendRunning = false;
this.state.nodeStates = {};

this.state.detectedDevices = null;
this.configHandler = null;
this.intervalSubscribe = null;
}

refreshBackendSubscription() {
this.refreshTimer && clearTimeout(this.refreshTimer);
this.refreshTimer = setTimeout(() => {
this.refreshTimer = null;
this.refreshBackendSubscription();
}, 60000);

this.socket.subscribeOnInstance(`matter.${this.instance}`, 'gui', null, this.onBackendUpdates)
.then(result => {
if (typeof result === 'object' && result.accepted === false) {
console.error('Subscribe is not accepted');
this.setState({ backendRunning: !!result.accepted });
} else if (!this.state.backendRunning) {
this.setState({ backendRunning: true });
}
});
}

async onConnectionReady() {
Expand All @@ -87,16 +115,59 @@ class App extends GenericApp {
matter.bridges = matter.bridges.list;
}

this.setState({ matter, changed: this.configHandler.isChanged(matter), ready: true });
this.socket.subscribeState(`system.adapter.matter.${this.instance}.alive`, this.onAlive);
const alive = await this.socket.getState(`system.adapter.matter.${this.instance}.alive`);

if (alive?.val) {
this.refreshBackendSubscription();
}

this.setState({
matter,
changed: this.configHandler.isChanged(matter),
ready: true,
alive: !!(alive?.val),
});
}

onAlive = (id, state) => {
if (state?.val && !this.state.alive) {
this.setState({ alive: true });
this.refreshBackendSubscription();
} else if (!state?.val && this.state.alive) {
this.refreshTimer && clearTimeout(this.refreshTimer);
this.refreshTimer = null;
this.setState({ alive: false });
}
};

onBackendUpdates = update => {
if (update.uuid) {
const nodeStates = JSON.parse(JSON.stringify(this.state.nodeStates));
nodeStates[update.uuid] = update;
this.setState({ nodeStates });
} else {
console.log(`Unknown update: ${JSON.stringify(update)}`);
}
};

onChanged = newConfig => {
if (this.state.ready) {
this.setState({ matter: newConfig, changed: this.configHandler.isChanged(newConfig) });
}
};

componentWillUnmount() {
async componentWillUnmount() {
this.intervalSubscribe && clearInterval(this.intervalSubscribe);
this.intervalSubscribe = null;

try {
await this.socket.unsubscribeState(`system.adapter.matter.${this.instance}.alive`, this.onAlive);
await this.socket.unsubscribeFromInstance(`matter.${this.instance}`, 'gui', this.onBackendUpdates);
} catch (e) {
// ignore
}

super.componentWillUnmount();
this.configHandler && this.configHandler.destroy();
}
Expand All @@ -119,24 +190,28 @@ class App extends GenericApp {
renderBridges() {
return <Bridges
socket={this.socket}
nodeStates={this.state.nodeStates}
themeType={this.state.themeType}
detectedDevices={this.state.detectedDevices}
setDetectedDevices={detectedDevices => this.setState({ detectedDevices })}
productIDs={productIDs}
matter={this.state.matter}
updateConfig={this.onChanged}
showToast={text => this.showToast(text)}
/>;
}

renderDevices() {
return <Devices
nodeStates={this.state.nodeStates}
socket={this.socket}
themeType={this.state.themeType}
detectedDevices={this.state.detectedDevices}
setDetectedDevices={detectedDevices => this.setState({ detectedDevices })}
productIDs={productIDs}
matter={this.state.matter}
updateConfig={this.onChanged}
showToast={text => this.showToast(text)}
/>;
}

Expand All @@ -162,6 +237,7 @@ class App extends GenericApp {

return <StyledEngineProvider injectFirst>
<ThemeProvider theme={this.state.theme}>
{this.renderToast()}
<div className="App" style={{ background: this.state.theme.palette.background.default, color: this.state.theme.palette.text.primary }}>
<AppBar position="static">
<Tabs
Expand All @@ -176,7 +252,21 @@ class App extends GenericApp {
<Tab classes={{ selected: this.props.classes.selected }} label={I18n.t('Controller')} value="controller" />
<Tab classes={{ selected: this.props.classes.selected }} label={I18n.t('Bridges')} value="bridges" />
<Tab classes={{ selected: this.props.classes.selected }} label={I18n.t('Devices')} value="devices" />
<div style={{ flexGrow: 1 }} />
{this.state.alive ? null : <IconNotAlive
style={{ color: 'orange', padding: 12 }}
/>}
{this.state.backendRunning ? null : <IconButton
onClick={() => {
this.refreshBackendSubscription();
}}
>
<IconNoConnection
style={{ color: 'orange' }}
/>
</IconButton>}
</Tabs>

</AppBar>

<div className={this.isIFrame ? this.props.classes.tabContentIFrame : this.props.classes.tabContent}>
Expand Down
98 changes: 94 additions & 4 deletions src-admin/src/components/Bridges.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,33 @@ import React from 'react';
import PropTypes from 'prop-types';
import { withStyles } from '@mui/styles';
import { v4 as uuidv4 } from 'uuid';
import QRCode from 'react-qr-code';

import {
Button, Checkbox,
Dialog, DialogActions, DialogContent, DialogTitle,
Fab, FormControlLabel, IconButton, MenuItem, Switch, Table,
Fab, FormControlLabel, IconButton, InputAdornment, MenuItem, Switch, Table,
TableBody,
TableCell,
TableRow, TextField,
Tooltip,
} from '@mui/material';
import {
Add, Close, Delete, Edit, KeyboardArrowDown, KeyboardArrowUp, QuestionMark, Save, UnfoldLess, UnfoldMore,
Add,
Close,
ContentCopy,
Delete,
Edit,
KeyboardArrowDown,
KeyboardArrowUp,
QrCode,
QuestionMark,
Save,
UnfoldLess,
UnfoldMore,
} from '@mui/icons-material';

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

import DeviceDialog, { DEVICE_ICONS } from '../DeviceDialog';
import { getText } from '../Utils';
Expand Down Expand Up @@ -55,6 +67,14 @@ const styles = () => ({
marginLeft: 8,
opacity: 0.6,
},
flexGrow: {
flexGrow: 1,
},
bridgeHeader: {
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
}
});

class Bridges extends React.Component {
Expand All @@ -72,6 +92,7 @@ class Bridges extends React.Component {
editDialog: null,
deleteDialog: false,
bridgesOpened,
showQrCode: null,
};
}

Expand Down Expand Up @@ -371,6 +392,70 @@ class Bridges extends React.Component {
</TableRow>;
}

renderQrCodeDialog() {
if (!this.state.showQrCode) {
return null;
}
return <Dialog
onClose={() => this.setState({ showQrCode: null })}
open={!0}
maxWidth="md"
>
<DialogTitle>{I18n.t('QR Code to connect ')}</DialogTitle>
<DialogContent>
<div style={{ background: 'white', padding: 16 }}>
<QRCode value={this.props.nodeStates[this.state.showQrCode.uuid].qrPairingCode} />
</div>
<TextField
value={this.props.nodeStates[this.state.showQrCode.uuid].manualPairingCode}
InputProps={{
readOnly: true,
endAdornment: <InputAdornment position="end">
<IconButton
onClick={() => {
Utils.copyToClipboard(this.props.nodeStates[this.state.showQrCode.uuid].manualPairingCode);
this.props.showToast(I18n.t('Copied to clipboard'));
}}
edge="end"
>
<ContentCopy />
</IconButton>
</InputAdornment>,
}}
fullWidth
label={I18n.t('Manual pairing code')}
variant="standard"
/>
</DialogContent>
<DialogActions>
<Button
onClick={() => this.setState({ showQrCode: null })}
startIcon={<Close />}
color="grey"
variant="contained"
>
{I18n.t('Close')}
</Button>
</DialogActions>
</Dialog>;
}

renderStatus(bridge) {
if (!this.props.nodeStates[bridge.uuid]) {
return null;
}
if (this.props.nodeStates[bridge.uuid].command === 'showQRCode') {
return <Tooltip title={I18n.t('Show QR Code')}>
<IconButton
style={{ height: 40 }}
onClick={() => this.setState({ showQrCode: bridge })}
>
<QrCode />
</IconButton>
</Tooltip>;
}
}

renderBridge(bridge, bridgeIndex) {
const enabledDevices = bridge.list.filter(d => d.enabled).length;
let countText;
Expand Down Expand Up @@ -413,7 +498,7 @@ class Bridges extends React.Component {
</IconButton>
</TableCell>
<TableCell
style={{ cursor: 'pointer' }}
className={this.props.classes.bridgeHeader}
onClick={() => {
const bridgesOpened = JSON.parse(JSON.stringify(this.state.bridgesOpened));
bridgesOpened[bridgeIndex] = !bridgesOpened[bridgeIndex];
Expand All @@ -440,6 +525,8 @@ class Bridges extends React.Component {
<span className={this.props.classes.bridgeValue}>{bridge.productID || ''}</span>
</div>
</div>
<div className={this.props.classes.flexGrow} />
{this.renderStatus(bridge)}
</TableCell>
<TableCell style={{ width: 0 }}>
<Switch
Expand Down Expand Up @@ -529,6 +616,7 @@ class Bridges extends React.Component {
{this.renderDevicesDialog()}
{this.renderDeleteDialog()}
{this.renderEditDialog()}
{this.renderQrCodeDialog()}
<Tooltip title={I18n.t('Add bridge')}>
<Fab
onClick={() => {
Expand Down Expand Up @@ -608,6 +696,8 @@ Bridges.propTypes = {
themeType: PropTypes.string,
detectedDevices: PropTypes.array,
setDetectedDevices: PropTypes.func,
nodeStates: PropTypes.object,
showToast: PropTypes.func,
};

export default withStyles(styles)(Bridges);
8 changes: 6 additions & 2 deletions src-admin/src/components/ConfigHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ class ConfigHandler {
destroy() {
this.onChanged = null;
if (this.socket.isConnected()) {
this.socket.unsubscribeObject(`matter.${this.instance}.*`, this.onObjectChange);
this.socket.unsubscribeObject(`matter.${this.instance}.bridges.`, this.onObjectChange);
this.socket.unsubscribeObject(`matter.${this.instance}.devices.`, this.onObjectChange);
this.socket.unsubscribeObject(`matter.${this.instance}.controller`, this.onObjectChange);
}
this.socket = null;
}
Expand Down Expand Up @@ -195,7 +197,9 @@ class ConfigHandler {
globalThis.changed = false;
window.parent.postMessage('nochange', '*');

this.socket.subscribeObject(`matter.${this.instance}.*`, this.onObjectChange);
this.socket.subscribeObject(`matter.${this.instance}.bridges.*`, this.onObjectChange);
this.socket.subscribeObject(`matter.${this.instance}.devices.*`, this.onObjectChange);
this.socket.subscribeObject(`matter.${this.instance}.controller`, this.onObjectChange);

return JSON.parse(JSON.stringify(this.config));
}
Expand Down
Loading

0 comments on commit aea4e73

Please sign in to comment.