Skip to content

Commit

Permalink
feat(dev-console): add external definition generator (#1866)
Browse files Browse the repository at this point in the history
* feat(dev-console): add external definitino generator

* fix(dev): updates
  • Loading branch information
Koenkk authored Dec 25, 2023
1 parent a75ccbd commit 945af60
Show file tree
Hide file tree
Showing 10 changed files with 113 additions and 14 deletions.
5 changes: 5 additions & 0 deletions src/actions/DeviceApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface DeviceApi {
setDeviceOptions(id: string, options: Record<string, unknown>): Promise<void>;
setDeviceDescription(id: string, description: string): Promise<void>;

generateExternalDefinition(friendlyNameOrIEEEAddress: FriendlyName | IEEEEAddress): Promise<void>;
readDeviceAttributes(friendlyNameOrIEEEAddress: FriendlyName | IEEEEAddress, endpoint: Endpoint, cluster: Cluster, attributes: Attribute[], options: Record<string, unknown>): Promise<void>;
writeDeviceAttributes(friendlyNameOrIEEEAddress: FriendlyName | IEEEEAddress, endpoint: Endpoint, cluster: Cluster, attributes: AttributeInfo[], options: Record<string, unknown>): Promise<void>;
executeCommand(friendlyNameOrIEEEAddress: FriendlyName | IEEEEAddress, endpoint: Endpoint, cluster: Cluster, command: unknown, payload: Record<string, unknown>): Promise<void>;
Expand Down Expand Up @@ -59,6 +60,10 @@ export default {
return api.send(`${toDeviceId(id, endpoint)}/set`, { read: { cluster, attributes, options } });
},

generateExternalDefinition(_state, id: FriendlyName | IEEEEAddress): Promise<void> {
return api.send("bridge/request/device/generate_external_definition", { id });
},

writeDeviceAttributes(_state, id: FriendlyName | IEEEEAddress, endpoint: Endpoint, cluster: Cluster, attributes: AttributeInfo[], options: Record<string, unknown>): Promise<void> {
const payload = {};
attributes.forEach(info => {
Expand Down
54 changes: 50 additions & 4 deletions src/components/device-page/dev-console.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,27 @@
import React, { ChangeEvent, Component } from 'react';
import { Attribute, Cluster, Device, Endpoint } from '../../types';
import ClusterPicker, { PickerType } from '../cluster-picker';
import AceEditor from 'react-ace';

import DataType from 'zigbee-herdsman/dist/zcl/definition/dataType';
import ZclCluster from 'zigbee-herdsman/dist/zcl/definition/cluster';
import AttributePicker, { AttributeDefinition } from '../attribute-picker';
import Button from '../button';
import { DeviceApi } from '../../actions/DeviceApi';
import { LogMessage } from '../../store';
import { GlobalState, LogMessage } from '../../store';
import { WithTranslation, withTranslation } from 'react-i18next';
import EndpointPicker from '../endpoint-picker';
import { getEndpoints } from '../../utils';
import { getEndpoints, supportNewDevicesUrl } from '../../utils';
import { CommandExecutor } from './CommandExecutor';
import { LastLogResult } from './LastLogResult';

interface DevConsoleProps
extends WithTranslation,
Pick<DeviceApi, 'executeCommand' | 'readDeviceAttributes' | 'writeDeviceAttributes'> {
Pick<
DeviceApi,
'executeCommand' | 'readDeviceAttributes' | 'writeDeviceAttributes' | 'generateExternalDefinition'
>,
Pick<GlobalState, 'generatedExternalDefinitions' | 'theme'> {
device: Device;
logs: LogMessage[];
}
Expand Down Expand Up @@ -103,6 +108,10 @@ export class DevConsole extends Component<DevConsoleProps, DevConsoleState> {
{},
);
};
onGenerateExternalDefinitionClick = (): void => {
const { generateExternalDefinition, device } = this.props;
generateExternalDefinition(device.ieee_address);
};

onWriteClick = (): void => {
const { writeDeviceAttributes, device } = this.props;
Expand Down Expand Up @@ -210,12 +219,49 @@ export class DevConsole extends Component<DevConsoleProps, DevConsoleState> {
</>
);
}
renderGenerateExternalDefinition(): JSX.Element {
const { t, generatedExternalDefinitions, device, theme } = this.props;
const externalDefinition = generatedExternalDefinitions[device.ieee_address];
if (externalDefinition) {
const editorTheme = theme === 'light' ? 'github' : 'dracula';
return (
<>
{t('generated_external_definition')} (
<a href={supportNewDevicesUrl} target="_blank" rel="noreferrer">
{t('documentation')}
</a>
)
<AceEditor
setOptions={{ useWorker: false }}
mode="javascript"
readOnly={true}
name="UNIQUE_ID_OF_DIV"
editorProps={{ $blockScrolling: true }}
value={externalDefinition}
width="100%"
maxLines={Infinity}
theme={editorTheme}
showPrintMargin={false}
/>
</>
);
} else {
return (
<Button<void> className="btn btn-primary" onClick={this.onGenerateExternalDefinitionClick}>
{t('generate_external_definition')}
</Button>
);
}
}
render(): JSX.Element {
const { executeCommand, logs, device } = this.props;
const logsFilterFn = (l: LogMessage) =>
logStartingStrings.some((startString) => l.message.startsWith(startString));
return (
<div>
<div className="card">
<div className="card-body">{this.renderGenerateExternalDefinition()}</div>
</div>
<div className="card">
<div className="card-body">
{this.renderRead()}
Expand All @@ -232,4 +278,4 @@ export class DevConsole extends Component<DevConsoleProps, DevConsoleState> {
}
}

export default withTranslation('common')(DevConsole);
export default withTranslation(['devConsole', 'common'])(DevConsole);
23 changes: 19 additions & 4 deletions src/components/device-page/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,26 @@ type UrlParams = {
dev: string;
tab?: TabName;
};
type PropsFromStore = Pick<GlobalState, 'bridgeInfo' | 'devices' | 'logs' | 'deviceStates'>;
type PropsFromStore = Pick<
GlobalState,
'bridgeInfo' | 'devices' | 'logs' | 'deviceStates' | 'generatedExternalDefinitions' | 'theme'
>;

type DevicePageProps = RouteComponentProps<UrlParams> & PropsFromStore & DeviceApi & WithTranslation<'devicePage'>;

function ContentRenderer(props: DevicePageProps): JSX.Element {
const { match, devices, logs } = props;
const { readDeviceAttributes, writeDeviceAttributes, setDeviceOptions, executeCommand, bridgeInfo, deviceStates } =
props;
const {
readDeviceAttributes,
writeDeviceAttributes,
setDeviceOptions,
executeCommand,
generateExternalDefinition,
bridgeInfo,
deviceStates,
generatedExternalDefinitions,
theme,
} = props;
const { tab, dev } = match.params;
const device = devices[dev];
const deviceState = deviceStates[device.friendly_name] ?? {};
Expand Down Expand Up @@ -113,7 +125,10 @@ function ContentRenderer(props: DevicePageProps): JSX.Element {
logs={logs}
readDeviceAttributes={readDeviceAttributes}
writeDeviceAttributes={writeDeviceAttributes}
generateExternalDefinition={generateExternalDefinition}
generatedExternalDefinitions={generatedExternalDefinitions}
executeCommand={executeCommand}
theme={theme}
/>
);
case 'scene':
Expand Down Expand Up @@ -160,7 +175,7 @@ export function DevicePage(props: DevicePageProps): JSX.Element {
);
}
const devicePageWithRouter = withRouter(DevicePage);
const mappedProps = ['devices', 'deviceStates', 'logs', 'bridgeInfo'];
const mappedProps = ['devices', 'deviceStates', 'logs', 'bridgeInfo', 'generatedExternalDefinitions', 'theme'];
const ConnectedDevicePage = withTranslation('devicePage')(
connect<unknown, unknown, GlobalState, unknown>(mappedProps, actions)(devicePageWithRouter),
);
Expand Down
20 changes: 18 additions & 2 deletions src/components/device-page/info.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable react/display-name */
import React, { Component, Fragment } from 'react';
import { BridgeInfo, Device, DeviceState } from '../../types';
import { isDeviceDisabled, toHex } from '../../utils';
import { isDeviceDisabled, supportNewDevicesUrl, toHex } from '../../utils';
import DeviceControlGroup from '../device-control/DeviceControlGroup';
import cx from 'classnames';
import style from './style.module.css';
Expand All @@ -17,6 +17,7 @@ import { DisplayValue } from '../display-value/DisplayValue';
import { Availability } from '../zigbee/Availability';
import { DeviceApi } from '../../actions/DeviceApi';
import actions from '../../actions/actions';
import { TFunction } from 'i18next';

type DeviceInfoProps = {
device: Device;
Expand Down Expand Up @@ -102,7 +103,13 @@ const displayProps = [
},
},
{
render: (device: Device) => (
render: (
device: Device,
state: DeviceState,
bridgeInfo: BridgeInfo,
availability: AvailabilityState,
t: TFunction,
) => (
<dd className="col-12 col-md-7">
<p
className={cx('mb-0', 'font-weight-bold', {
Expand All @@ -111,6 +118,14 @@ const displayProps = [
})}
>
<DisplayValue name="supported" value={device.supported} />
{!device.supported && (
<>
{' '}
<a target="_blank" rel="noopener noreferrer" href={supportNewDevicesUrl}>
({t('how_to_add_support')})
</a>
</>
)}
</p>
</dd>
),
Expand Down Expand Up @@ -209,6 +224,7 @@ export class DeviceInfo extends Component<
deviceState,
bridgeInfo,
availability[device.friendly_name] ?? 'offline',
t,
)
) : (
<dd className="col-12 col-md-7">{get(device, prop.key)}</dd>
Expand Down
3 changes: 2 additions & 1 deletion src/components/vendor-links/vendor-links.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { Fragment } from 'react';
import { useTranslation } from 'react-i18next';
import { Device } from '../../types';
import { supportNewDevicesUrl } from '../../utils';

type VendorProps = {
device: Device;
Expand Down Expand Up @@ -28,8 +29,8 @@ export const VendorLink: React.FunctionComponent<VendorProps> = (props: VendorPr

export const ModelLink: React.FunctionComponent<VendorProps> = (props: VendorProps) => {
const { device, anchor } = props;
let url = 'https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html';
let title = device.model_id;
let url = supportNewDevicesUrl;
if (device.supported && device.definition) {
const detailsAnchor = [
encodeURIComponent(device.definition?.vendor?.toLowerCase()),
Expand Down
13 changes: 10 additions & 3 deletions src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,12 @@
"the_only_endpoint": "The only endpoint",
"unbind": "Unbind",
"write": "Write",
"destination_endpoint": "Destination endpoint"
"destination_endpoint": "Destination endpoint",
"documentation": "Documentation"
},
"devConsole": {
"generate_external_definition": "Generate external definition",
"generated_external_definition": "Generated external definition"
},
"devicePage": {
"about": "About",
Expand Down Expand Up @@ -2247,7 +2252,8 @@
"translate": "Translate",
"stats": "Stats",
"coordinator_ieee_address": "Coordinator IEEE Address",
"request_z2m_backup": "Request Z2m backup",
"request_z2m_backup": "Request Z2M backup",
"download_z2m_backup": "Download Z2M backup",
"add_install_code": "Add install code",
"homeassistant": "Home Assistant integration",
"localise_images": "Localise device images"
Expand Down Expand Up @@ -2326,7 +2332,8 @@
"execute": "Execute",
"command": "Command",
"payload": "Payload",
"save_description": "Save description"
"save_description": "Save description",
"how_to_add_support": "How to add support"
},
"scene": {
"scene_id": "Scene ID",
Expand Down
1 change: 1 addition & 0 deletions src/initialState.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"extensions": [],
"theme": "light",
"missingTranslations": {},
"generatedExternalDefinitions": {},
"availability": {},
"preparingBackup": false,
"backup": ""
Expand Down
1 change: 1 addition & 0 deletions src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export interface GlobalState extends WithDevices, WithDeviceStates, WithGroups,
missingTranslations: Map<string, unknown>;
preparingBackup: boolean;
backup: Base64String;
generatedExternalDefinitions: Map<string, string>;
}

const theme = getCurrentTheme();
Expand Down
2 changes: 2 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,3 +197,5 @@ export const debounceArgs = (fn: (...args: any) => any, options?: Record<string,


export const isIframe = (): boolean => window.location !== window.parent.location;

export const supportNewDevicesUrl = "https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html";
5 changes: 5 additions & 0 deletions src/ws-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,11 @@ class Api {
store.setState({ backup: zip, preparingBackup: false });
break;

case "bridge/response/device/generate_external_definition":
const { data: { source, id } } = data.payload as { data: { source: Base64String, id: string } };
const generatedExternalDefinitions = {...store.getState().generatedExternalDefinitions, [id]: source};
store.setState({ generatedExternalDefinitions });
break;

default:
break;
Expand Down

0 comments on commit 945af60

Please sign in to comment.