fix(ignore): Use zhc types (#20006)

* fix(ignore): Use zhc TS types

* updates
This commit is contained in:
Koen Kanters
2023-12-04 10:05:05 +01:00
committed by GitHub
parent 2fa8afe37f
commit dd1e0db407
17 changed files with 128 additions and 166 deletions
+8 -2
View File
@@ -4,6 +4,7 @@ import utils from '../util/utils';
import * as settings from '../util/settings';
import debounce from 'debounce';
import bind from 'bind-decorator';
import * as zhc from 'zigbee-herdsman-converters';
const retrieveOnReconnect = [
{keys: ['state']},
@@ -199,8 +200,13 @@ export default class Availability extends Extension {
for (const item of retrieveOnReconnect) {
if (item.condition && this.state.get(device) && !item.condition(this.state.get(device))) continue;
const converter = device.definition.toZigbee.find((c) => c.key.find((k) => item.keys.includes(k)));
await converter?.convertGet?.(device.endpoint(), item.keys[0],
{message: this.state.get(device), mapped: device.definition})
const options: KeyValue = device.options;
const state = this.state.get(device);
const meta: zhc.Tz.Meta = {
message: this.state.get(device), mapped: device.definition, logger, endpoint_name: null,
options, state, device: device.zh,
};
await converter?.convertGet?.(device.endpoint(), item.keys[0], meta)
.catch((e) => {
logger.error(`Failed to read state of '${device.name}' after reconnect (${e.message})`);
});
+6 -3
View File
@@ -12,12 +12,13 @@ import Group from '../model/group';
import data from '../util/data';
import JSZip from 'jszip';
import fs from 'fs';
import type * as zhc from 'zigbee-herdsman-converters';
const requestRegex = new RegExp(`${settings.get().mqtt.base_topic}/bridge/request/(.*)`);
type DefinitionPayload = {
model: string, vendor: string, description: string, exposes: zhc.DefinitionExpose[], supports_ota:
boolean, icon: string, options: zhc.DefinitionExpose[],
model: string, vendor: string, description: string, exposes: zhc.Expose[], supports_ota:
boolean, icon: string, options: zhc.Expose[],
};
export default class Bridge extends Extension {
@@ -687,7 +688,9 @@ export default class Bridge extends Extension {
getDefinitionPayload(device: Device): DefinitionPayload {
if (!device.definition) return null;
let icon = device.options.icon ? device.options.icon : device.definition.icon;
// @ts-expect-error icon is valid for external definitions
const definitionIcon = device.definition.icon;
let icon = device.options.icon ?? definitionIcon;
if (icon) {
icon = icon.replace('${zigbeeModel}', utils.sanitizeImageParameter(device.zh.modelID));
icon = icon.replace('${model}', utils.sanitizeImageParameter(device.definition.model));
+2 -3
View File
@@ -2,7 +2,7 @@ import * as settings from '../util/settings';
import utils from '../util/utils';
import logger from '../util/logger';
import stringify from 'json-stable-stringify-without-jsonify';
import zhc from 'zigbee-herdsman-converters';
import * as zhc from 'zigbee-herdsman-converters';
import Extension from './extension';
import bind from 'bind-decorator';
import Device from '../model/device';
@@ -114,8 +114,7 @@ export default class Configure extends Extension {
logger.info(`Configuring '${device.name}'`);
try {
await device.definition.configure(device.zh, this.zigbee.firstCoordinatorEndpoint(), logger,
device.options);
await device.definition.configure(device.zh, this.zigbee.firstCoordinatorEndpoint(), logger);
logger.info(`Successfully configured '${device.name}'`);
device.zh.meta.configured = zhc.getConfigureKey(device.definition);
device.zh.save();
+2 -2
View File
@@ -1,4 +1,4 @@
import zhc from 'zigbee-herdsman-converters';
import * as zhc from 'zigbee-herdsman-converters';
import * as settings from '../util/settings';
import utils from '../util/utils';
import Extension from './extension';
@@ -12,7 +12,7 @@ export default class ExternalConverters extends Extension {
for (const definition of utils.getExternalConvertersDefinitions(settings.get())) {
const toAdd = {...definition};
delete toAdd['homeassistant'];
zhc.addDeviceDefinition(toAdd);
zhc.addDefinition(toAdd);
}
}
}
+2 -1
View File
@@ -7,13 +7,14 @@ import bind from 'bind-decorator';
import Extension from './extension';
import Device from '../model/device';
import Group from '../model/group';
import * as zhc from 'zigbee-herdsman-converters';
const topicRegex =
new RegExp(`^${settings.get().mqtt.base_topic}/bridge/request/group/members/(remove|add|remove_all)$`);
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, exposes: zhc.DefinitionExpose[]) => boolean} = {
const stateProperties: {[s: string]: (value: string, exposes: zhc.Expose[]) => boolean} = {
'state': () => true,
'brightness': (value, exposes) =>
!!exposes.find((e) => e.type === 'light' && e.features.find((f) => f.name === 'brightness')),
+35 -29
View File
@@ -1,10 +1,11 @@
import * as settings from '../util/settings';
import logger from '../util/logger';
import utils from '../util/utils';
import utils, {isNumericExposeFeature, isBinaryExposeFeature, isEnumExposeFeature} from '../util/utils';
import stringify from 'json-stable-stringify-without-jsonify';
import assert from 'assert';
import Extension from './extension';
import bind from 'bind-decorator';
import * as zhc from 'zigbee-herdsman-converters';
interface MockProperty {property: string, value: KeyValue | string}
@@ -52,7 +53,7 @@ const legacyMapping = [
},
];
const featurePropertyWithoutEndpoint = (feature: zhc.DefinitionExposeFeature): string => {
const featurePropertyWithoutEndpoint = (feature: zhc.Feature): string => {
if (feature.endpoint) {
return feature.property.slice(0, -1 + -1 * feature.endpoint.length);
} else {
@@ -115,8 +116,8 @@ export default class HomeAssistant extends Extension {
this.eventBus.emitPublishAvailability();
}
private exposeToConfig(exposes: zhc.DefinitionExpose[], entityType: 'device' | 'group',
allExposes: zhc.DefinitionExpose[], definition?: zhc.Definition): DiscoveryEntry[] {
private exposeToConfig(exposes: zhc.Expose[], entityType: 'device' | 'group',
allExposes: zhc.Expose[], definition?: zhc.Definition): 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');
@@ -126,7 +127,7 @@ export default class HomeAssistant extends Extension {
const discoveryEntries: DiscoveryEntry[] = [];
const endpoint = entityType === 'device' ? exposes[0].endpoint : undefined;
const getProperty = (feature: zhc.DefinitionExposeFeature): string => entityType === 'group' ?
const getProperty = (feature: zhc.Feature): string => entityType === 'group' ?
featurePropertyWithoutEndpoint(feature) : feature.property;
/* istanbul ignore else */
@@ -169,8 +170,8 @@ export default class HomeAssistant extends Extension {
}
if (hasColorTemp) {
const colorTemps = exposes.map((expose) => expose.features.find((e) => e.name === 'color_temp'))
.filter((e) => e);
const colorTemps = exposes.map((expose) => expose.features
.filter(isNumericExposeFeature).find((e) => e.name === 'color_temp'));
const max = Math.min(...colorTemps.map((e) => e.value_max));
const min = Math.max(...colorTemps.map((e) => e.value_min));
discoveryEntry.discovery_payload.max_mireds = max;
@@ -178,7 +179,7 @@ export default class HomeAssistant extends Extension {
}
const effects = utils.arrayUnique(utils.flatten(
allExposes.filter((e) => e.type === 'enum' && e.name === 'effect').map((e) => e.values)));
allExposes.filter(isEnumExposeFeature).filter((e) => e.name === 'effect').map((e) => e.values)));
if (effects.length) {
discoveryEntry.discovery_payload.effect = true;
discoveryEntry.discovery_payload.effect_list = effects;
@@ -186,7 +187,7 @@ export default class HomeAssistant extends Extension {
discoveryEntries.push(discoveryEntry);
} else if (firstExpose.type === 'switch') {
const state = firstExpose.features.find((f) => f.name === 'state');
const state = firstExpose.features.filter(isBinaryExposeFeature).find((f) => f.name === 'state');
const property = getProperty(state);
const discoveryEntry: DiscoveryEntry = {
type: 'switch',
@@ -218,7 +219,8 @@ export default class HomeAssistant extends Extension {
discoveryEntries.push(discoveryEntry);
} else if (firstExpose.type === 'climate') {
const setpointProperties = ['occupied_heating_setpoint', 'current_heating_setpoint'];
const setpoint = firstExpose.features.find((f) => setpointProperties.includes(f.name));
const setpoint = firstExpose.features.filter(isNumericExposeFeature)
.find((f) => setpointProperties.includes(f.name));
assert(setpoint, 'No setpoint found');
const temperature = firstExpose.features.find((f) => f.name === 'local_temperature');
assert(temperature, 'No temperature found');
@@ -243,7 +245,7 @@ export default class HomeAssistant extends Extension {
},
};
const mode = firstExpose.features.find((f) => f.name === 'system_mode');
const mode = firstExpose.features.filter(isEnumExposeFeature).find((f) => f.name === 'system_mode');
if (mode) {
if (mode.values.includes('sleep')) {
// 'sleep' is not supported by Home Assistant, but is valid according to ZCL
@@ -283,7 +285,8 @@ export default class HomeAssistant extends Extension {
discoveryEntry.discovery_payload.temperature_state_topic = true;
}
const fanMode = firstExpose.features.find((f) => f.name === 'fan_mode');
const fanMode = firstExpose.features.filter(isEnumExposeFeature)
.find((f) => f.name === 'fan_mode');
if (fanMode) {
discoveryEntry.discovery_payload.fan_modes = fanMode.values;
discoveryEntry.discovery_payload.fan_mode_command_topic = true;
@@ -292,7 +295,8 @@ export default class HomeAssistant extends Extension {
discoveryEntry.discovery_payload.fan_mode_state_topic = true;
}
const swingMode = firstExpose.features.find((f) => f.name === 'swing_mode');
const swingMode = firstExpose.features.filter(isEnumExposeFeature)
.find((f) => f.name === 'swing_mode');
if (swingMode) {
discoveryEntry.discovery_payload.swing_modes = swingMode.values;
discoveryEntry.discovery_payload.swing_mode_command_topic = true;
@@ -301,7 +305,7 @@ export default class HomeAssistant extends Extension {
discoveryEntry.discovery_payload.swing_mode_state_topic = true;
}
const preset = firstExpose.features.find((f) => f.name === 'preset');
const preset = firstExpose.features.filter(isEnumExposeFeature).find((f) => f.name === 'preset');
if (preset) {
discoveryEntry.discovery_payload.preset_modes = preset.values;
discoveryEntry.discovery_payload.preset_mode_command_topic = 'preset';
@@ -310,7 +314,8 @@ export default class HomeAssistant extends Extension {
discoveryEntry.discovery_payload.preset_mode_state_topic = true;
}
const tempCalibration = firstExpose.features.find((f) => f.name === 'local_temperature_calibration');
const tempCalibration = firstExpose.features.filter(isNumericExposeFeature)
.find((f) => f.name === 'local_temperature_calibration');
if (tempCalibration) {
const discoveryEntry: DiscoveryEntry = {
type: 'number',
@@ -337,7 +342,8 @@ export default class HomeAssistant extends Extension {
discoveryEntries.push(discoveryEntry);
}
const piHeatingDemand = firstExpose.features.find((f) => f.name === 'pi_heating_demand');
const piHeatingDemand = firstExpose.features.filter(isNumericExposeFeature)
.find((f) => f.name === 'pi_heating_demand');
if (piHeatingDemand) {
const discoveryEntry: DiscoveryEntry = {
type: 'sensor',
@@ -358,7 +364,7 @@ export default class HomeAssistant extends Extension {
discoveryEntries.push(discoveryEntry);
} else if (firstExpose.type === 'lock') {
assert(!endpoint, `Endpoint not supported for lock type`);
const state = firstExpose.features.find((f) => f.name === 'state');
const state = firstExpose.features.filter(isBinaryExposeFeature).find((f) => f.name === 'state');
assert(state, 'No state found');
const discoveryEntry: DiscoveryEntry = {
type: 'lock',
@@ -404,8 +410,8 @@ export default class HomeAssistant extends Extension {
?.features.find((f) => f.name === 'position');
const tilt = exposes.find((expose) => expose.features.find((e) => e.name === 'tilt'))
?.features.find((f) => f.name === 'tilt');
const motorState = allExposes?.find((e) => e.type === 'enum' &&
['motor_state', 'moving'].includes(e.name) && e.access === ACCESS_STATE);
const motorState = allExposes?.filter(isEnumExposeFeature)
.find((e) => ['motor_state', 'moving'].includes(e.name) && e.access === ACCESS_STATE);
const running = allExposes?.find((e) => e.type === 'binary' && e.name === 'running');
const discoveryEntry: DiscoveryEntry = {
@@ -436,9 +442,9 @@ export default class HomeAssistant extends Extension {
const closingLookup = ['closing', 'close', 'backward', 'back', 'reverse', 'down', 'declining'];
const stoppedLookup = ['stopped', 'stop', 'pause', 'paused'];
const openingState = motorState.values.find((s) => openingLookup.includes(s.toLowerCase()));
const closingState = motorState.values.find((s) => closingLookup.includes(s.toLowerCase()));
const stoppedState = motorState.values.find((s) => stoppedLookup.includes(s.toLowerCase()));
const openingState = motorState.values.find((s) => openingLookup.includes(s.toString().toLowerCase()));
const closingState = motorState.values.find((s) => closingLookup.includes(s.toString().toLowerCase()));
const stoppedState = motorState.values.find((s) => stoppedLookup.includes(s.toString().toLowerCase()));
if (openingState && closingState && stoppedState) {
discoveryEntry.discovery_payload.state_opening = openingState;
@@ -496,7 +502,7 @@ export default class HomeAssistant extends Extension {
},
};
const speed = firstExpose.features.find((e) => e.name === 'mode');
const speed = firstExpose.features.filter(isEnumExposeFeature).find((e) => e.name === 'mode');
if (speed) {
// A fan entity in Home Assistant 2021.3 and above may have a speed,
// controlled by a percentage from 1 to 100, and/or non-speed presets.
@@ -524,7 +530,7 @@ export default class HomeAssistant extends Extension {
}
const allowed = [...speeds, ...presets];
speed.values.forEach((s) => assert(allowed.includes(s)));
speed.values.forEach((s) => assert(allowed.includes(s.toString())));
const percentValues = speeds.map((s, i) => `'${s}':${i}`).join(', ');
const percentCommands = speeds.map((s, i) => `${i}:'${s}'`).join(', ');
const presetList = presets.map((s) => `'${s}'`).join(', ');
@@ -547,7 +553,7 @@ export default class HomeAssistant extends Extension {
}
discoveryEntries.push(discoveryEntry);
} else if (firstExpose.type === 'binary') {
} else if (isBinaryExposeFeature(firstExpose)) {
const lookup: {[s: string]: KeyValue}= {
battery_low: {entity_category: 'diagnostic', device_class: 'battery'},
button_lock: {entity_category: 'config', icon: 'mdi:lock'},
@@ -636,7 +642,7 @@ export default class HomeAssistant extends Extension {
discoveryEntries.push(discoveryEntry);
}
} else if (firstExpose.type === 'numeric') {
} else if (isNumericExposeFeature(firstExpose)) {
const lookup: {[s: string]: KeyValue} = {
ac_frequency: {device_class: 'frequency', enabled_by_default: false, entity_category: 'diagnostic',
state_class: 'measurement'},
@@ -847,7 +853,7 @@ export default class HomeAssistant extends Extension {
discoveryEntries.push(discoveryEntry);
}
} else if (firstExpose.type === 'enum') {
} else if (isEnumExposeFeature(firstExpose)) {
const lookup: {[s: string]: KeyValue} = {
action: {icon: 'mdi:gesture-double-tap'},
alarm_humidity: {entity_category: 'config', icon: 'mdi:water-percent-alert'},
@@ -1107,8 +1113,8 @@ export default class HomeAssistant extends Extension {
configs.push(entity.definition.homeassistant);
}
} else { // group
const exposesByType: {[s: string]: zhc.DefinitionExpose[]} = {};
const allExposes: zhc.DefinitionExpose[] = [];
const exposesByType: {[s: string]: zhc.Expose[]} = {};
const allExposes: zhc.Expose[] = [];
entity.zh.members.map((e) => this.zigbee.resolveEntity(e.getDevice()) as Device)
.filter((d) => d.definition).forEach((device) => {
+2 -2
View File
@@ -1,6 +1,6 @@
import * as settings from '../../util/settings';
import logger from '../../util/logger';
import zigbeeHerdsmanConverters from 'zigbee-herdsman-converters';
import * as zhc from 'zigbee-herdsman-converters';
import utils from '../../util/utils';
import assert from 'assert';
import Extension from '../extension';
@@ -140,7 +140,7 @@ export default class BridgeLegacy extends Extension {
};
if (device.zh.type !== 'Coordinator') {
const definition = zigbeeHerdsmanConverters.findByDevice(device.zh);
const definition = zhc.findByDevice(device.zh);
payload.model = definition ? definition.model : device.zh.modelID;
payload.vendor = definition ? definition.vendor : '-';
payload.description = definition ? definition.description : '-';
+7 -7
View File
@@ -1,4 +1,4 @@
import zigbeeHerdsmanConverters from 'zigbee-herdsman-converters';
import * as zhc from 'zigbee-herdsman-converters';
import logger from '../../util/logger';
import * as settings from '../../util/settings';
import Extension from '../extension';
@@ -7,14 +7,14 @@ const defaultConfiguration = {
minimumReportInterval: 3, maximumReportInterval: 300, reportableChange: 1,
};
const ZNLDP12LM = zigbeeHerdsmanConverters.definitions.find((d) => d.model === 'ZNLDP12LM');
const ZNLDP12LM = zhc.definitions.find((d) => d.model === 'ZNLDP12LM');
const devicesNotSupportingReporting = [
zigbeeHerdsmanConverters.definitions.find((d) => d.model === 'CC2530.ROUTER'),
zigbeeHerdsmanConverters.definitions.find((d) => d.model === 'BASICZBR3'),
zigbeeHerdsmanConverters.definitions.find((d) => d.model === 'ZM-CSW032-D'),
zigbeeHerdsmanConverters.definitions.find((d) => d.model === 'TS0001'),
zigbeeHerdsmanConverters.definitions.find((d) => d.model === 'TS0115'),
zhc.definitions.find((d) => d.model === 'CC2530.ROUTER'),
zhc.definitions.find((d) => d.model === 'BASICZBR3'),
zhc.definitions.find((d) => d.model === 'ZM-CSW032-D'),
zhc.definitions.find((d) => d.model === 'TS0001'),
zhc.definitions.find((d) => d.model === 'TS0115'),
];
const reportKey = 1;
+5 -4
View File
@@ -1,4 +1,4 @@
import zhc from 'zigbee-herdsman-converters';
import * as zhc from 'zigbee-herdsman-converters';
import Extension from './extension';
/**
@@ -39,12 +39,13 @@ export default class OnEvent extends Extension {
}
}
private async callOnEvent(device: Device, type: string, data: KeyValue): Promise<void> {
private async callOnEvent(device: Device, type: zhc.OnEventType, data: KeyValue): Promise<void> {
const state = this.state.get(device);
zhc.onEvent(type, data, device.zh, device.options, state);
zhc.onEvent(type, data, device.zh);
if (device.definition?.onEvent) {
await device.definition.onEvent(type, data, device.zh, device.options, state);
const options: KeyValue = device.options;
await device.definition.onEvent(type, data, device.zh, options, state);
}
}
}
+8 -7
View File
@@ -2,14 +2,13 @@ import * as settings from '../util/settings';
import logger from '../util/logger';
import stringify from 'json-stable-stringify-without-jsonify';
import utils from '../util/utils';
import * as tradfriOTA from 'zigbee-herdsman-converters/lib/ota/tradfri';
import * as zigbeeOTA from 'zigbee-herdsman-converters/lib/ota/zigbeeOTA';
import Extension from './extension';
import bind from 'bind-decorator';
import Device from '../model/device';
import dataDir from '../util/data';
import * as URI from 'uri-js';
import path from 'path';
import * as zhc from 'zigbee-herdsman-converters';
function isValidUrl(url: string): boolean {
let parsed;
@@ -45,7 +44,7 @@ export default class OTAUpdate extends Extension {
this.eventBus.onMQTTMessage(this, this.onMQTTMessage);
this.eventBus.onDeviceMessage(this, this.onZigbeeEvent);
if (settings.get().ota.ikea_ota_use_test_url) {
tradfriOTA.useTestURL();
zhc.ota.tradfri.useTestURL();
}
// Let zigbeeOTA module know if the override index file is provided
@@ -56,11 +55,11 @@ export default class OTAUpdate extends Extension {
overrideOTAIndex = dataDir.joinPath(overrideOTAIndex);
}
zigbeeOTA.useIndexOverride(overrideOTAIndex);
zhc.ota.zigbeeOTA.useIndexOverride(overrideOTAIndex);
}
// In order to support local firmware files we need to let zigbeeOTA know where the data directory is
zigbeeOTA.setDataDir(dataDir.getPath());
zhc.ota.zigbeeOTA.setDataDir(dataDir.getPath());
// In case Zigbee2MQTT is restared during an update, progress and remaining values are still in state.
// remove them.
@@ -93,7 +92,9 @@ export default class OTAUpdate extends Extension {
this.lastChecked[data.device.ieeeAddr] = Date.now();
let availableResult: zhc.OtaUpdateAvailableResult = null;
try {
availableResult = await data.device.definition.ota.isUpdateAvailable(data.device.zh, logger, data.data);
// @ts-expect-error typing guaranteed by data.type
const dataData: zhc.ota.ImageInfo = data.data;
availableResult = await data.device.definition.ota.isUpdateAvailable(data.device.zh, logger, dataData);
} catch (e) {
supportsOTA = false;
logger.debug(`Failed to check if update available for '${data.device.name}' (${e.message})`);
@@ -201,7 +202,7 @@ export default class OTAUpdate extends Extension {
}
try {
const availableResult = await device.definition.ota.isUpdateAvailable(device.zh, logger);
const availableResult = await device.definition.ota.isUpdateAvailable(device.zh, logger, null);
const msg = `${availableResult.available ? 'Update' : 'No update'} available for '${device.name}'`;
logger.info(msg);
+23 -22
View File
@@ -1,6 +1,6 @@
import * as settings from '../util/settings';
import zigbeeHerdsmanConverters from 'zigbee-herdsman-converters';
import * as zhc from 'zigbee-herdsman-converters';
import * as philips from 'zigbee-herdsman-converters/lib/philips';
import logger from '../util/logger';
import utils from '../util/utils';
@@ -17,19 +17,19 @@ const sceneConverterKeys = ['scene_store', 'scene_add', 'scene_remove', 'scene_r
// Legacy: don't provide default converters anymore, this is required by older z2m installs not saving group members
const defaultGroupConverters = [
zigbeeHerdsmanConverters.toZigbeeConverters.light_onoff_brightness,
zigbeeHerdsmanConverters.toZigbeeConverters.light_color_colortemp,
zhc.toZigbee.light_onoff_brightness,
zhc.toZigbee.light_color_colortemp,
philips.tz.effect, // Support Hue effects for groups
zigbeeHerdsmanConverters.toZigbeeConverters.ignore_transition,
zigbeeHerdsmanConverters.toZigbeeConverters.cover_position_tilt,
zigbeeHerdsmanConverters.toZigbeeConverters.thermostat_occupied_heating_setpoint,
zigbeeHerdsmanConverters.toZigbeeConverters.tint_scene,
zigbeeHerdsmanConverters.toZigbeeConverters.light_brightness_move,
zigbeeHerdsmanConverters.toZigbeeConverters.light_brightness_step,
zigbeeHerdsmanConverters.toZigbeeConverters.light_colortemp_step,
zigbeeHerdsmanConverters.toZigbeeConverters.light_colortemp_move,
zigbeeHerdsmanConverters.toZigbeeConverters.light_hue_saturation_move,
zigbeeHerdsmanConverters.toZigbeeConverters.light_hue_saturation_step,
zhc.toZigbee.ignore_transition,
zhc.toZigbee.cover_position_tilt,
zhc.toZigbee.thermostat_occupied_heating_setpoint,
zhc.toZigbee.tint_scene,
zhc.toZigbee.light_brightness_move,
zhc.toZigbee.light_brightness_step,
zhc.toZigbee.light_colortemp_step,
zhc.toZigbee.light_colortemp_move,
zhc.toZigbee.light_hue_saturation_move,
zhc.toZigbee.light_hue_saturation_step,
];
interface ParsedTopic {ID: string, endpoint: string, attribute: string, type: 'get' | 'set'}
@@ -81,8 +81,8 @@ export default class Publish extends Extension {
}
}
legacyRetrieveState(re: Device | Group, converter: zhc.ToZigbeeConverter, result: zhc.ToZigbeeConverterResult,
target: zh.Endpoint | zh.Group, key: string, meta: zhc.ToZigbeeConverterGetMeta): void {
legacyRetrieveState(re: Device | Group, converter: zhc.Tz.Converter, result: zhc.Tz.ConvertSetResult,
target: zh.Endpoint | zh.Group, key: string, meta: zhc.Tz.Meta): void {
// It's possible for devices to get out of sync when writing an attribute that's not reportable.
// So here we re-read the value after a specified timeout, this timeout could for example be the
// transition time of a color change or for forcing a state read for devices that don't
@@ -140,18 +140,16 @@ export default class Publish extends Extension {
const membersState = re instanceof Group ?
Object.fromEntries(re.zh.members.map((e) => [e.getDevice().ieeeAddr,
this.state.get(this.zigbee.resolveEntity(e.getDevice().ieeeAddr))])) : null;
let converters: zhc.ToZigbeeConverter[];
let converters: zhc.Tz.Converter[];
{
if (Array.isArray(definition)) {
const c = new Set(definition.map((d) => d.toZigbee).flat());
// @ts-expect-error
if (c.size == 0) converters = defaultGroupConverters;
else converters = Array.from(c);
} else if (definition) {
converters = definition.toZigbee;
} else {
converters = [zigbeeHerdsmanConverters.toZigbeeConverters.read,
zigbeeHerdsmanConverters.toZigbeeConverters.write];
converters = [zhc.toZigbee.read, zhc.toZigbee.write];
}
}
@@ -177,7 +175,7 @@ export default class Publish extends Extension {
entries.sort((a) => (['state', 'brightness', 'brightness_percent'].includes(a[0]) ? sorter : sorter * -1));
// For each attribute call the corresponding converter
const usedConverters: {[s: number]: zhc.ToZigbeeConverter[]} = {};
const usedConverters: {[s: number]: zhc.Tz.Converter[]} = {};
const toPublish: {[s: number | string]: KeyValue} = {};
const toPublishEntity: {[s: number | string]: Device | Group} = {};
const addToToPublish = (entity: Device | Group, payload: KeyValue): void => {
@@ -228,8 +226,11 @@ export default class Publish extends Extension {
}
// Converter didn't return a result, skip
const meta = {endpoint_name: endpointName, options: entitySettings, message: {...message}, logger, device,
state: entityState, membersState, mapped: definition};
const entitySettingsKeyValue: KeyValue = entitySettings;
const meta = {
endpoint_name: endpointName, options: entitySettingsKeyValue,
message: {...message}, logger, device, state: entityState, membersState, mapped: definition,
};
// Strip endpoint name from meta.message properties.
if (endpointName) {
+3 -1
View File
@@ -147,8 +147,10 @@ export default class Receive extends Extension {
let payload: KeyValue = {};
for (const converter of converters) {
try {
const convertData = {...data, device: data.device.zh};
const options: KeyValue = data.device.options;
const converted = await converter.convert(
data.device.definition, data, publish, data.device.options, meta);
data.device.definition, convertData, publish, options, meta);
if (converted) {
payload = {...payload, ...converted};
}
+5 -4
View File
@@ -1,6 +1,6 @@
/* eslint-disable brace-style */
import * as settings from '../util/settings';
import zigbeeHerdsmanConverters from 'zigbee-herdsman-converters';
import * as zhc from 'zigbee-herdsman-converters';
export default class Device {
public zh: zh.Device;
@@ -17,7 +17,7 @@ export default class Device {
// Some devices can change modelID, reconsider the definition in that case.
// https://github.com/Koenkk/zigbee-herdsman-converters/issues/3016
if (!this.zh.interviewing && (!this._definition || this._definitionModelID !== this.zh.modelID)) {
this._definition = zigbeeHerdsmanConverters.findByDevice(this.zh);
this._definition = zhc.findByDevice(this.zh);
this._definitionModelID = this.zh.modelID;
}
return this._definition;
@@ -27,10 +27,11 @@ export default class Device {
this.zh = device;
}
exposes(): zhc.DefinitionExpose[] {
exposes(): zhc.Expose[] {
/* istanbul ignore if */
if (typeof this.definition.exposes == 'function') {
return this.definition.exposes(this.zh, this.options);
const options: KeyValue = this.options;
return this.definition.exposes(this.zh, options);
} else {
return this.definition.exposes;
}
+2 -2
View File
@@ -1,6 +1,6 @@
/* eslint-disable brace-style */
import * as settings from '../util/settings';
import zigbeeHerdsmanConverters from 'zigbee-herdsman-converters';
import * as zhc from 'zigbee-herdsman-converters';
export default class Group {
public zh: zh.Group;
@@ -25,7 +25,7 @@ export default class Group {
membersDefinitions(): zhc.Definition[] {
return this.zh.members.map((m) =>
zigbeeHerdsmanConverters.findByDevice(m.getDevice())).filter((d) => d) as zhc.Definition[];
zhc.findByDevice(m.getDevice())).filter((d) => d) as zhc.Definition[];
}
isDevice(): this is Device {return false;}
+4 -67
View File
@@ -21,6 +21,8 @@ import type {
FrameControl as ZHFrameControl,
} from 'zigbee-herdsman/dist/zcl';
import type * as zhc from 'zigbee-herdsman-converters';
import type * as ZHEvents from 'zigbee-herdsman/dist/controller/events';
import type TypeEventBus from 'lib/eventBus';
@@ -44,6 +46,8 @@ declare global {
type Extension = TypeExtension;
// Types
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type ExternalDefinition = zhc.Definition & {homeassistant: any};
interface MQTTResponse {data: KeyValue, status: 'error' | 'ok', error?: string, transaction?: string}
interface MQTTOptions {qos?: QoS, retain?: boolean, properties?: {messageExpiryInterval: number}}
type Scene = {id: number, name: string};
@@ -71,73 +75,6 @@ declare global {
}
}
// zigbee-herdsman-converters
namespace zhc {
interface Logger {
info: (message: string) => void;
warn: (message: string) => void;
error: (message: string) => void;
debug: (message: string) => void;
}
interface ToZigbeeConverterGetMeta {message?: KeyValue, mapped?: Definition | Definition[]}
interface ToZigbeeConverterResult {state: KeyValue,
membersState: {[s: string]: KeyValue}, readAfterWriteTime?: number}
interface ToZigbeeConverter {
key: string[],
convertGet?: (entity: zh.Endpoint | zh.Group, key: string, meta: ToZigbeeConverterGetMeta) => Promise<void>
convertSet?: (entity: zh.Endpoint | zh.Group, key: string, value: KeyValue | string | number,
meta: {state: KeyValue}) => Promise<ToZigbeeConverterResult>
}
interface FromZigbeeConverter {
cluster: string,
type: string[] | string,
convert: (model: Definition, msg: KeyValue, publish: (payload: KeyValue) => void, options: KeyValue,
meta: {state: KeyValue, logger: Logger, device: zh.Device}) => Promise<KeyValue>,
}
interface DefinitionExposeFeature {name: string, label: string, endpoint?: string,
property: string, value_max?: number, value_min?: number, unit?: string,
value_off?: string, value_on?: string, value_step?: number, values: string[], access: number}
interface DefinitionExpose {
type: string, name?: string, label?: string, features?: DefinitionExposeFeature[],
endpoint?: string, values?: string[], value_off?: string, value_on?: string, value_step?: number,
access: number, property: string, unit?: string,
value_min?: number, value_max?: number}
interface OtaUpdateAvailableResult {available: boolean, currentFileVersion: number, otaFileVersion: number}
interface Definition {
model: string,
zigbeeModel: string[],
endpoint?: (device: zh.Device) => {[s: string]: number}
toZigbee: ToZigbeeConverter[]
fromZigbee: FromZigbeeConverter[]
icon?: string
description: string
options: zhc.DefinitionExpose[],
vendor: string
exposes: DefinitionExpose[] | ((device: zh.Device, options: KeyValue) => DefinitionExpose[])
configure?: (device: zh.Device, coordinatorEndpoint: zh.Endpoint, logger: Logger,
options?: DeviceOptions) => Promise<void>;
onEvent?: (type: string, data: KeyValue, device: zh.Device,
settings: KeyValue, state: KeyValue) => Promise<void>;
ota?: {
isUpdateAvailable: (device: zh.Device, logger: Logger, data?: KeyValue)
=> Promise<OtaUpdateAvailableResult>;
updateToLatest: (device: zh.Device, logger: Logger,
onProgress: (progress: number, remaining: number) => void) => Promise<number>;
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type ExternalDefinition = Definition & {homeassistant: any};
}
namespace eventdata {
type EntityRenamed = { entity: Device | Group, homeAssisantRename: boolean, from: string, to: string };
type DeviceRemoved = { ieeeAddr: string, name: string };
-9
View File
@@ -1,9 +0,0 @@
declare module 'zigbee-herdsman-converters' {
export function findByDevice(device: zh.Device): zhc.Definition;
export function getConfigureKey(definition: zhc.Definition): string | number;
export const toZigbeeConverters: {[s: string]: zhc.ToZigbeeConverter};
export const definitions: zhc.Definition[];
export function addDeviceDefinition(definition: zhc.Definition): Promise<void>;
export function onEvent(type: string, data: KeyValue, device: zh.Device, settings: KeyValue,
state: KeyValue): Promise<void>;
}
+14 -1
View File
@@ -6,6 +6,7 @@ import fs from 'fs';
import path from 'path';
import {detailedDiff} from 'deep-object-diff';
import objectAssignDeep from 'object-assign-deep';
import type * as zhc from 'zigbee-herdsman-converters';
// construct a local ISO8601 string (instead of UTC-based)
// Example:
@@ -161,7 +162,7 @@ function loadModuleFromFile(modulePath: string): unknown {
return loadModuleFromText(moduleCode);
}
function* getExternalConvertersDefinitions(settings: Settings): Generator<zhc.ExternalDefinition> {
function* getExternalConvertersDefinitions(settings: Settings): Generator<ExternalDefinition> {
const externalConverters = settings.external_converters;
for (const moduleName of externalConverters) {
@@ -371,6 +372,18 @@ function clone(obj: KeyValue): KeyValue {
return JSON.parse(JSON.stringify(obj));
}
export function isNumericExposeFeature(feature: zhc.Feature): feature is zhc.Numeric {
return feature?.type === 'numeric';
}
export function isEnumExposeFeature(feature: zhc.Feature): feature is zhc.Enum {
return feature?.type === 'enum';
}
export function isBinaryExposeFeature(feature: zhc.Feature): feature is zhc.Binary {
return feature?.type === 'binary';
}
function computeSettingsToChange(current: KeyValue, new_: KeyValue): KeyValue {
const diff: KeyValue = detailedDiff(current, new_);