diff --git a/lib/extension/availability.ts b/lib/extension/availability.ts index 54521be221..fb0f82df7b 100644 --- a/lib/extension/availability.ts +++ b/lib/extension/availability.ts @@ -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})`); }); diff --git a/lib/extension/bridge.ts b/lib/extension/bridge.ts index d4483f6403..55795ab710 100644 --- a/lib/extension/bridge.ts +++ b/lib/extension/bridge.ts @@ -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)); diff --git a/lib/extension/configure.ts b/lib/extension/configure.ts index e521b7fca5..200b275ea9 100644 --- a/lib/extension/configure.ts +++ b/lib/extension/configure.ts @@ -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(); diff --git a/lib/extension/externalConverters.ts b/lib/extension/externalConverters.ts index 077c0e2f2a..2d8fb69648 100644 --- a/lib/extension/externalConverters.ts +++ b/lib/extension/externalConverters.ts @@ -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); } } } diff --git a/lib/extension/groups.ts b/lib/extension/groups.ts index ac002cc857..3b7166ccbf 100644 --- a/lib/extension/groups.ts +++ b/lib/extension/groups.ts @@ -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')), diff --git a/lib/extension/homeassistant.ts b/lib/extension/homeassistant.ts index 0ea4bf537d..36cb596cbb 100644 --- a/lib/extension/homeassistant.ts +++ b/lib/extension/homeassistant.ts @@ -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) => { diff --git a/lib/extension/legacy/bridgeLegacy.ts b/lib/extension/legacy/bridgeLegacy.ts index 2eaf6cf626..4447f2d89a 100644 --- a/lib/extension/legacy/bridgeLegacy.ts +++ b/lib/extension/legacy/bridgeLegacy.ts @@ -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 : '-'; diff --git a/lib/extension/legacy/report.ts b/lib/extension/legacy/report.ts index 863e8a5b6a..9ac4666eac 100644 --- a/lib/extension/legacy/report.ts +++ b/lib/extension/legacy/report.ts @@ -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; diff --git a/lib/extension/onEvent.ts b/lib/extension/onEvent.ts index f82b8023e8..74979c50cd 100644 --- a/lib/extension/onEvent.ts +++ b/lib/extension/onEvent.ts @@ -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 { + private async callOnEvent(device: Device, type: zhc.OnEventType, data: KeyValue): Promise { 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); } } } diff --git a/lib/extension/otaUpdate.ts b/lib/extension/otaUpdate.ts index 0a833642ec..8556b7e33f 100644 --- a/lib/extension/otaUpdate.ts +++ b/lib/extension/otaUpdate.ts @@ -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); diff --git a/lib/extension/publish.ts b/lib/extension/publish.ts index a7b8b92eb8..38242491ac 100644 --- a/lib/extension/publish.ts +++ b/lib/extension/publish.ts @@ -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) { diff --git a/lib/extension/receive.ts b/lib/extension/receive.ts index 5ca82df174..66ae2f1ff4 100755 --- a/lib/extension/receive.ts +++ b/lib/extension/receive.ts @@ -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}; } diff --git a/lib/model/device.ts b/lib/model/device.ts index 8363b6237e..c4d34a478c 100644 --- a/lib/model/device.ts +++ b/lib/model/device.ts @@ -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; } diff --git a/lib/model/group.ts b/lib/model/group.ts index 2f79eab871..e8075c7726 100644 --- a/lib/model/group.ts +++ b/lib/model/group.ts @@ -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;} diff --git a/lib/types/types.d.ts b/lib/types/types.d.ts index db81bb03b9..f1edc98208 100644 --- a/lib/types/types.d.ts +++ b/lib/types/types.d.ts @@ -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 - convertSet?: (entity: zh.Endpoint | zh.Group, key: string, value: KeyValue | string | number, - meta: {state: KeyValue}) => Promise - } - - 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, - } - - 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; - onEvent?: (type: string, data: KeyValue, device: zh.Device, - settings: KeyValue, state: KeyValue) => Promise; - ota?: { - isUpdateAvailable: (device: zh.Device, logger: Logger, data?: KeyValue) - => Promise; - updateToLatest: (device: zh.Device, logger: Logger, - onProgress: (progress: number, remaining: number) => void) => Promise; - } - } - - // 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 }; diff --git a/lib/types/zigbee-herdsman-converters.d.ts b/lib/types/zigbee-herdsman-converters.d.ts deleted file mode 100644 index 55ea95cc2c..0000000000 --- a/lib/types/zigbee-herdsman-converters.d.ts +++ /dev/null @@ -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; - export function onEvent(type: string, data: KeyValue, device: zh.Device, settings: KeyValue, - state: KeyValue): Promise; -} diff --git a/lib/util/utils.ts b/lib/util/utils.ts index 78d6f32d89..46d5a1082c 100644 --- a/lib/util/utils.ts +++ b/lib/util/utils.ts @@ -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 { +function* getExternalConvertersDefinitions(settings: Settings): Generator { 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_);