mirror of
https://github.com/Koenkk/zigbee2mqtt.git
synced 2026-08-28 13:34:24 +00:00
Dynamically expose attributes (#10132)
* Dynamically expose attributes * Rollback options in configure step Use event instead, as proposed in https://github.com/Koenkk/zigbee2mqtt/pull/10132/files#r771801648 * Simplify code * Switch to already existing event * Fix all unit tests * Remove redundant code * Perfect rollback * Fix typescript types * Updates * Updates * Updates * updates * Improve Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
This commit is contained in:
@@ -688,7 +688,7 @@ export default class Bridge extends Extension {
|
||||
model: device.definition.model,
|
||||
vendor: device.definition.vendor,
|
||||
description: device.definition.description,
|
||||
exposes: device.definition.exposes,
|
||||
exposes: device.exposes(),
|
||||
supports_ota: !!device.definition.ota,
|
||||
options: device.definition.options,
|
||||
icon,
|
||||
|
||||
+11
-10
@@ -13,17 +13,17 @@ const topicRegex =
|
||||
const legacyTopicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/group/(.+)/(remove|add|remove_all)$`);
|
||||
const legacyTopicRegexRemoveAll = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/group/remove_all$`);
|
||||
|
||||
const stateProperties: {[s: string]: (value: string, definition: zhc.Definition) => boolean} = {
|
||||
const stateProperties: {[s: string]: (value: string, exposes: zhc.DefinitionExpose[]) => boolean} = {
|
||||
'state': () => true,
|
||||
'brightness': (value, definition) =>
|
||||
!!definition.exposes.find((e) => e.type === 'light' && e.features.find((f) => f.name === 'brightness')),
|
||||
'color_temp': (value, definition) =>
|
||||
!!definition.exposes.find((e) => e.type === 'light' && e.features.find((f) => f.name === 'color_temp')),
|
||||
'color': (value, definition) =>
|
||||
!!definition.exposes.find((e) => e.type === 'light' &&
|
||||
'brightness': (value, exposes) =>
|
||||
!!exposes.find((e) => e.type === 'light' && e.features.find((f) => f.name === 'brightness')),
|
||||
'color_temp': (value, exposes) =>
|
||||
!!exposes.find((e) => e.type === 'light' && e.features.find((f) => f.name === 'color_temp')),
|
||||
'color': (value, exposes) =>
|
||||
!!exposes.find((e) => e.type === 'light' &&
|
||||
e.features.find((f) => f.name === 'color_xy' || f.name === 'color_hs')),
|
||||
'color_mode': (value, definition) =>
|
||||
!!definition.exposes.find((e) => e.type === 'light' && (
|
||||
'color_mode': (value, exposes) =>
|
||||
!!exposes.find((e) => e.type === 'light' && (
|
||||
(e.features.find((f) => f.name === `color_${value}`)) ||
|
||||
(value === 'color_temp' && e.features.find((f) => f.name === 'color_temp')) )),
|
||||
};
|
||||
@@ -143,9 +143,10 @@ export default class Groups extends Extension {
|
||||
const groupsToPublish: Set<Group> = new Set();
|
||||
for (const member of entity.zh.members) {
|
||||
const device = this.zigbee.resolveEntity(member.getDevice()) as Device;
|
||||
const exposes = device.exposes();
|
||||
const memberPayload: KeyValue = {};
|
||||
Object.keys(payload).forEach((key) => {
|
||||
if (stateProperties[key](payload[key], device.definition)) {
|
||||
if (stateProperties[key](payload[key], exposes)) {
|
||||
memberPayload[key] = payload[key];
|
||||
}
|
||||
});
|
||||
|
||||
@@ -107,7 +107,7 @@ export default class HomeAssistant extends Extension {
|
||||
}
|
||||
|
||||
private exposeToConfig(exposes: zhc.DefinitionExpose[], entityType: 'device' | 'group',
|
||||
definition?: zhc.Definition): DiscoveryEntry[] {
|
||||
definition?: zhc.Definition, definitionExposes?: zhc.DefinitionExpose[]): DiscoveryEntry[] {
|
||||
// For groups an array of exposes (of the same type) is passed, this is to determine e.g. what features
|
||||
// to use for a bulb (e.g. color_xy/color_temp)
|
||||
assert(entityType === 'group' || exposes.length === 1, 'Multiple exposes for device not allowed');
|
||||
@@ -162,7 +162,7 @@ export default class HomeAssistant extends Extension {
|
||||
discoveryEntry.discovery_payload.min_mireds = min;
|
||||
}
|
||||
|
||||
const effect = definition && definition.exposes.find((e) => e.type === 'enum' && e.name === 'effect');
|
||||
const effect = definitionExposes?.find((e) => e.type === 'enum' && e.name === 'effect');
|
||||
if (effect) {
|
||||
discoveryEntry.discovery_payload.effect = true;
|
||||
discoveryEntry.discovery_payload.effect_list = effect.values;
|
||||
@@ -879,8 +879,8 @@ export default class HomeAssistant extends Extension {
|
||||
|
||||
let configs: DiscoveryEntry[] = [];
|
||||
if (isDevice) {
|
||||
for (const expose of entity.definition.exposes) {
|
||||
configs.push(...this.exposeToConfig([expose], 'device', entity.definition));
|
||||
for (const expose of entity.exposes()) {
|
||||
configs.push(...this.exposeToConfig([expose], 'device', entity.definition, entity.exposes()));
|
||||
}
|
||||
|
||||
for (const mapping of legacyMapping) {
|
||||
@@ -898,20 +898,21 @@ export default class HomeAssistant extends Extension {
|
||||
} else { // group
|
||||
const exposesByType: {[s: string]: zhc.DefinitionExpose[]} = {};
|
||||
|
||||
entity.membersDefinitions().forEach((definition) => {
|
||||
for (const expose of definition.exposes.filter((e) => groupSupportedTypes.includes(e.type))) {
|
||||
let key = expose.type;
|
||||
if (['switch', 'lock', 'cover'].includes(expose.type) && expose.endpoint) {
|
||||
// A device can have multiple of these types which have to discovered seperately.
|
||||
// e.g. switch with property state and valve_detection.
|
||||
const state = expose.features.find((f) => f.name === 'state');
|
||||
key += featurePropertyWithoutEndpoint(state);
|
||||
}
|
||||
entity.zh.members.map((e) => this.zigbee.resolveEntity(e.getDevice()) as Device)
|
||||
.filter((d) => d.definition).forEach((device) => {
|
||||
for (const expose of device.exposes().filter((e) => groupSupportedTypes.includes(e.type))) {
|
||||
let key = expose.type;
|
||||
if (['switch', 'lock', 'cover'].includes(expose.type) && expose.endpoint) {
|
||||
// A device can have multiple of these types which have to discovered seperately.
|
||||
// e.g. switch with property state and valve_detection.
|
||||
const state = expose.features.find((f) => f.name === 'state');
|
||||
key += featurePropertyWithoutEndpoint(state);
|
||||
}
|
||||
|
||||
if (!exposesByType[key]) exposesByType[key] = [];
|
||||
exposesByType[key].push(expose);
|
||||
}
|
||||
});
|
||||
if (!exposesByType[key]) exposesByType[key] = [];
|
||||
exposesByType[key].push(expose);
|
||||
}
|
||||
});
|
||||
|
||||
configs = [].concat(...Object.values(exposesByType)
|
||||
.map((exposes) => this.exposeToConfig(exposes, 'group')));
|
||||
|
||||
@@ -267,7 +267,7 @@ export default class NetworkMap extends Extension {
|
||||
model: device.definition.model,
|
||||
vendor: device.definition.vendor,
|
||||
description: device.definition.description,
|
||||
supports: Array.from(new Set((device.definition.exposes).map((e) => {
|
||||
supports: Array.from(new Set((device.exposes()).map((e) => {
|
||||
return e.hasOwnProperty('name') ? e.name :
|
||||
`${e.type} (${e.features.map((f) => f.name).join(', ')})`;
|
||||
}))).join(', '),
|
||||
|
||||
@@ -23,6 +23,15 @@ export default class Device {
|
||||
this.zh = device;
|
||||
}
|
||||
|
||||
exposes(): zhc.DefinitionExpose[] {
|
||||
/* istanbul ignore if */
|
||||
if (typeof this.definition.exposes == 'function') {
|
||||
return this.definition.exposes(this.zh, this.settings);
|
||||
} else {
|
||||
return this.definition.exposes;
|
||||
}
|
||||
}
|
||||
|
||||
ensureInSettings(): void {
|
||||
if (this.zh.type !== 'Coordinator' && !settings.getDevice(this.zh.ieeeAddr)) {
|
||||
settings.addDevice(this.zh.ieeeAddr);
|
||||
|
||||
Vendored
+1
-1
@@ -114,7 +114,7 @@ declare global {
|
||||
description: string
|
||||
options: zhc.DefinitionExpose[],
|
||||
vendor: string
|
||||
exposes: DefinitionExpose[]
|
||||
exposes: DefinitionExpose[] | ((device: zh.Device, options: KeyValue) => DefinitionExpose[])
|
||||
configure?: (device: zh.Device, coordinatorEndpoint: zh.Endpoint, logger: Logger) => Promise<void>;
|
||||
onEvent?: (type: string, data: KeyValue, device: zh.Device, settings: KeyValue) => Promise<void>;
|
||||
ota?: {
|
||||
|
||||
+6
-6
@@ -1129,31 +1129,31 @@ describe('Bridge', () => {
|
||||
const svg_icon = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDo';
|
||||
const icon_link = 'https://www.zigbee2mqtt.io/images/devices/ZNCZ02LM.jpg';
|
||||
definition.icon = icon_link;
|
||||
let payload = bridge.getDefinitionPayload({...device, zh: device, definition, settings: {}});
|
||||
let payload = bridge.getDefinitionPayload({...device, zh: device, definition, exposes: () => definition.exposes, settings: {}});
|
||||
expect(payload).not.toBeUndefined()
|
||||
expect(payload['icon']).not.toBeUndefined()
|
||||
expect(payload.icon).toBe(icon_link);
|
||||
|
||||
definition.icon = icon_link;
|
||||
payload = bridge.getDefinitionPayload({...device, zh: device, definition, settings: {icon: svg_icon}});
|
||||
payload = bridge.getDefinitionPayload({...device, zh: device, definition, exposes: () => definition.exposes, settings: {icon: svg_icon}});
|
||||
expect(payload).not.toBeUndefined()
|
||||
expect(payload['icon']).not.toBeUndefined()
|
||||
expect(payload.icon).toBe(svg_icon);
|
||||
|
||||
definition.icon = '_${model}_';
|
||||
payload = bridge.getDefinitionPayload({...device, zh: device, definition, settings: {}});
|
||||
payload = bridge.getDefinitionPayload({...device, zh: device, definition, exposes: () => definition.exposes, settings: {}});
|
||||
expect(payload).not.toBeUndefined()
|
||||
expect(payload['icon']).not.toBeUndefined()
|
||||
expect(payload.icon).toBe('_lumi.plug_');
|
||||
|
||||
definition.icon = '_${model}_${zigbeeModel}_';
|
||||
payload = bridge.getDefinitionPayload({...device, zh: device, definition, settings: {}});
|
||||
payload = bridge.getDefinitionPayload({...device, zh: device, definition, exposes: () => definition.exposes, settings: {}});
|
||||
expect(payload).not.toBeUndefined()
|
||||
expect(payload['icon']).not.toBeUndefined()
|
||||
expect(payload.icon).toBe('_lumi.plug_lumi.plug_');
|
||||
|
||||
definition.icon = svg_icon;
|
||||
payload = bridge.getDefinitionPayload({...device, zh: device, definition, settings: {}});
|
||||
payload = bridge.getDefinitionPayload({...device, zh: device, definition, exposes: () => definition.exposes, settings: {}});
|
||||
expect(payload).not.toBeUndefined()
|
||||
expect(payload['icon']).not.toBeUndefined()
|
||||
expect(payload.icon).toBe(svg_icon);
|
||||
@@ -1161,7 +1161,7 @@ describe('Bridge', () => {
|
||||
device.modelID = '?._Z\\NC+Z02*LM';
|
||||
definition.model = '&&&&*+';
|
||||
definition.icon = '_${model}_${zigbeeModel}_';
|
||||
payload = bridge.getDefinitionPayload({...device, zh: device, definition, settings: {}});
|
||||
payload = bridge.getDefinitionPayload({...device, zh: device, definition, exposes: () => definition.exposes, settings: {}});
|
||||
expect(payload).not.toBeUndefined()
|
||||
expect(payload['icon']).not.toBeUndefined()
|
||||
expect(payload.icon).toBe('_------_-._Z-NC-Z02-LM_');
|
||||
|
||||
@@ -49,7 +49,8 @@ describe('HomeAssistant extension', () => {
|
||||
it('Should not have duplicate type/object_ids in a mapping', () => {
|
||||
const duplicated = [];
|
||||
require('zigbee-herdsman-converters').devices.forEach((d) => {
|
||||
const device = {definition: d, isDevice: () => true, settings: {}};
|
||||
const exposes = typeof d.exposes == 'function' ? d.exposes() : d.exposes;
|
||||
const device = {definition: d, isDevice: () => true, settings: {}, exposes: () => exposes};
|
||||
const configs = extension.getConfigs(device);
|
||||
const cfg_type_object_ids = [];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user