From 5cdd8eb7bec290d63fa171d23570c9d143baa92a Mon Sep 17 00:00:00 2001 From: Koen Kanters Date: Wed, 27 Jan 2021 20:51:30 +0100 Subject: [PATCH] Preps for new zigbee-herdsman-converters (build will fail for this commit) --- lib/extension/homeassistant.js | 638 +++++++++--------- lib/extension/networkMap.js | 2 +- .../mock-external-converter-multiple.js | 17 +- test/assets/mock-external-converter.js | 9 +- test/externalConverters.test.js | 26 +- test/homeassistant.test.js | 5 +- test/networkMap.test.js | 8 +- 7 files changed, 363 insertions(+), 342 deletions(-) diff --git a/lib/extension/homeassistant.js b/lib/extension/homeassistant.js index e1d2d7c99..b9b42862c 100644 --- a/lib/extension/homeassistant.js +++ b/lib/extension/homeassistant.js @@ -53,350 +53,346 @@ class HomeAssistant extends Extension { populateMapping() { for (const def of zigbeeHerdsmanConverters.definitions) { - if (def.hasOwnProperty('exposes')) { - this.mapping[def.model] = []; + this.mapping[def.model] = []; - if (['WXKG01LM', 'HS1EB/HS1EB-E', 'ICZB-KPD14S', 'TERNCY-SD01', 'TERNCY-PP01', 'ICZB-KPD18S', - 'E1766', 'ZWallRemote0', 'ptvo.switch', '2AJZ4KPKEY', 'ZGRC-KEY-013', 'HGZB-02S', 'HGZB-045', - 'HGZB-1S', 'AV2010/34', 'IM6001-BTP01', 'WXKG11LM', 'WXKG03LM', 'WXKG02LM_rev1', 'WXKG02LM_rev2', - 'QBKG04LM', 'QBKG03LM', 'QBKG11LM', 'QBKG21LM', 'QBKG22LM', 'WXKG12LM', 'QBKG12LM', - 'E1743'].includes(def.model)) { - // deprecated - this.mapping[def.model].push(sensorClick); - } + if (['WXKG01LM', 'HS1EB/HS1EB-E', 'ICZB-KPD14S', 'TERNCY-SD01', 'TERNCY-PP01', 'ICZB-KPD18S', + 'E1766', 'ZWallRemote0', 'ptvo.switch', '2AJZ4KPKEY', 'ZGRC-KEY-013', 'HGZB-02S', 'HGZB-045', + 'HGZB-1S', 'AV2010/34', 'IM6001-BTP01', 'WXKG11LM', 'WXKG03LM', 'WXKG02LM_rev1', 'WXKG02LM_rev2', + 'QBKG04LM', 'QBKG03LM', 'QBKG11LM', 'QBKG21LM', 'QBKG22LM', 'WXKG12LM', 'QBKG12LM', + 'E1743'].includes(def.model)) { + // deprecated + this.mapping[def.model].push(sensorClick); + } - if (['ICTC-G-1'].includes(def.model)) { - // deprecated - this.mapping[def.model].push({ - type: 'sensor', - object_id: 'brightness', + if (['ICTC-G-1'].includes(def.model)) { + // deprecated + this.mapping[def.model].push({ + type: 'sensor', + object_id: 'brightness', + discovery_payload: { + unit_of_measurement: 'brightness', + icon: 'mdi:brightness-5', + value_template: '{{ value_json.brightness }}', + }, + }); + } + + for (const expose of def.exposes) { + let discoveryEntry = null; + /* istanbul ignore else */ + if (expose.type === 'light') { + const supportsXY = !!expose.features.find((e) => e.name === 'color_xy'); + const supportsHS = !!expose.features.find((e) => e.name === 'color_hs'); + discoveryEntry = { + type: 'light', + object_id: expose.endpoint ? `light_${expose.endpoint}` : 'light', discovery_payload: { - unit_of_measurement: 'brightness', - icon: 'mdi:brightness-5', - value_template: '{{ value_json.brightness }}', + brightness: !!expose.features.find((e) => e.name === 'brightness'), + color_temp: !!expose.features.find((e) => e.name === 'color_temp'), + xy: supportsXY, + hs: !supportsXY && supportsHS, + schema: 'json', + command_topic: true, + brightness_scale: 254, + command_topic_prefix: expose.endpoint ? expose.endpoint : undefined, + state_topic_postfix: expose.endpoint ? expose.endpoint : undefined, }, - }); - } + }; - for (const expose of def.exposes) { - let discoveryEntry = null; - /* istanbul ignore else */ - if (expose.type === 'light') { - const supportsXY = !!expose.features.find((e) => e.name === 'color_xy'); - const supportsHS = !!expose.features.find((e) => e.name === 'color_hs'); - discoveryEntry = { - type: 'light', - object_id: expose.endpoint ? `light_${expose.endpoint}` : 'light', - discovery_payload: { - brightness: !!expose.features.find((e) => e.name === 'brightness'), - color_temp: !!expose.features.find((e) => e.name === 'color_temp'), - xy: supportsXY, - hs: !supportsXY && supportsHS, - schema: 'json', - command_topic: true, - brightness_scale: 254, - command_topic_prefix: expose.endpoint ? expose.endpoint : undefined, - state_topic_postfix: expose.endpoint ? expose.endpoint : undefined, - }, + const effect = def.exposes.find((e) => e.type === 'enum' && e.name === 'effect'); + if (effect) { + discoveryEntry.discovery_payload.effect = true; + discoveryEntry.discovery_payload.effect_list = effect.values; + } + } else if (expose.type === 'switch') { + const state = expose.features.find((f) => f.name === 'state'); + discoveryEntry = { + type: 'switch', + object_id: expose.endpoint ? `switch_${expose.endpoint}` : 'switch', + discovery_payload: { + payload_off: state.value_off, + payload_on: state.value_on, + value_template: `{{ value_json.${state.property} }}`, + command_topic: true, + command_topic_prefix: expose.endpoint ? expose.endpoint : undefined, + }, + }; + + const different = ['valve_detection', 'window_detection', 'auto_lock', 'away_mode']; + if (different.includes(state.property)) { + discoveryEntry.discovery_payload.command_topic_postfix = state.property; + discoveryEntry.discovery_payload.state_off = state.value_off; + discoveryEntry.discovery_payload.state_on = state.value_on; + discoveryEntry.discovery_payload.state_topic = true; + discoveryEntry.object_id = state.property; + + if (state.property === 'window_detection') { + discoveryEntry.discovery_payload.icon = 'mdi:window-open-variant'; + } + } + } else if (expose.type === 'climate') { + const setpointProperties = ['occupied_heating_setpoint', 'current_heating_setpoint']; + const setpoint = expose.features.find((f) => setpointProperties.includes(f.name)); + assert(setpoint, 'No setpoint found'); + const temperature = expose.features.find((f) => f.name === 'local_temperature'); + assert(temperature, 'No temperature found'); + + discoveryEntry = { + type: 'climate', + object_id: expose.endpoint ? `climate_${expose.endpoint}` : 'climate', + discovery_payload: { + // Static + state_topic: false, + temperature_unit: 'C', + // Setpoint + temp_step: setpoint.value_step, + min_temp: setpoint.value_min.toString(), + max_temp: setpoint.value_max.toString(), + // Temperature + current_temperature_topic: true, + current_temperature_template: `{{ value_json.${temperature.property} }}`, + }, + }; + + const mode = expose.features.find((f) => f.name === 'system_mode'); + if (mode) { + discoveryEntry.discovery_payload.mode_state_topic = true; + discoveryEntry.discovery_payload.mode_state_template = `{{ value_json.${mode.property} }}`; + discoveryEntry.discovery_payload.modes = mode.values; + discoveryEntry.discovery_payload.mode_command_topic = true; + } + + const state = expose.features.find((f) => f.name === 'running_state'); + if (state) { + discoveryEntry.discovery_payload.action_topic = true; + discoveryEntry.discovery_payload.action_template = `{% set values = ` + + `{'idle':'off','heat':'heating','cool':'cooling','fan only':'fan'}` + + ` %}{{ values[value_json.${state.property}] }}`; + } + + const coolingSetpoint = expose.features.find((f) => f.name === 'occupied_cooling_setpoint'); + if (coolingSetpoint) { + discoveryEntry.discovery_payload.temperature_low_command_topic = setpoint.name; + discoveryEntry.discovery_payload.temperature_low_state_template = + `{{ value_json.${setpoint.property} }}`; + discoveryEntry.discovery_payload.temperature_low_state_topic = true; + discoveryEntry.discovery_payload.temperature_high_command_topic = coolingSetpoint.name; + discoveryEntry.discovery_payload.temperature_high_state_template = + `{{ value_json.${coolingSetpoint.property} }}`; + discoveryEntry.discovery_payload.temperature_high_state_topic = true; + } else { + discoveryEntry.discovery_payload.temperature_command_topic = setpoint.name; + discoveryEntry.discovery_payload.temperature_state_template = + `{{ value_json.${setpoint.property} }}`; + discoveryEntry.discovery_payload.temperature_state_topic = true; + } + + const fanMode = expose.features.find((f) => f.name === 'fan_mode'); + if (fanMode) { + discoveryEntry.discovery_payload.fan_modes = fanMode.values; + discoveryEntry.discovery_payload.fan_mode_command_topic = true; + discoveryEntry.discovery_payload.fan_mode_state_template = + `{{ value_json.${fanMode.property} }}`; + discoveryEntry.discovery_payload.fan_mode_state_topic = true; + } + + const preset = expose.features.find((f) => f.name === 'preset'); + if (preset) { + discoveryEntry.discovery_payload.hold_modes = preset.values; + discoveryEntry.discovery_payload.hold_command_topic = true; + discoveryEntry.discovery_payload.hold_state_template = + `{{ value_json.${preset.property} }}`; + discoveryEntry.discovery_payload.hold_state_topic = true; + } + + const awayMode = expose.features.find((f) => f.name === 'away_mode'); + if (awayMode) { + discoveryEntry.discovery_payload.away_mode_command_topic = true; + discoveryEntry.discovery_payload.away_mode_state_topic = true; + discoveryEntry.discovery_payload.away_mode_state_template = + `{{ value_json.${awayMode.property} }}`; + } + + if (expose.endpoint) { + discoveryEntry.discovery_payload.state_topic_postfix = expose.endpoint; + } + } else if (expose.type === 'lock') { + assert(!expose.endpoint, `Endpoint not supported for lock type`); + const state = expose.features.find((f) => f.name === 'state'); + assert(state, 'No state found'); + discoveryEntry = { + type: 'lock', + object_id: 'lock', + discovery_payload: { + command_topic: true, + value_template: `{{ value_json.${state.property} }}`, + }, + }; + + if (state.property === 'keypad_lockout') { + // deprecated: keypad_lockout is messy, but changing is breaking + discoveryEntry.discovery_payload.payload_lock = state.value_on; + discoveryEntry.discovery_payload.payload_unlock = state.value_off; + discoveryEntry.discovery_payload.state_topic = true; + discoveryEntry.object_id = 'keypad_lock'; + } else if (state.property === 'child_lock') { + // deprecated: child_lock is messy, but changing is breaking + discoveryEntry.discovery_payload.payload_lock = state.value_on; + discoveryEntry.discovery_payload.payload_unlock = state.value_off; + discoveryEntry.discovery_payload.state_locked = 'LOCKED'; + discoveryEntry.discovery_payload.state_unlocked = 'UNLOCKED'; + discoveryEntry.discovery_payload.state_topic = true; + discoveryEntry.object_id = 'child_lock'; + } else { + discoveryEntry.discovery_payload.state_locked = state.value_on; + discoveryEntry.discovery_payload.state_unlocked = state.value_off; + } + + if (state.property !== 'state') { + discoveryEntry.discovery_payload.command_topic_postfix = state.property; + } + } else if (expose.type === 'cover') { + assert(!expose.endpoint, `Endpoint not supported for cover type`); + const hasPosition = expose.features.find((e) => e.name === 'position'); + const hasTilt = expose.features.find((e) => e.name === 'tilt'); + + discoveryEntry = { + type: 'cover', + object_id: 'cover', + discovery_payload: { + command_topic: true, + state_topic: !hasPosition, + }, + }; + + if (!hasPosition && !hasTilt) { + discoveryEntry.discovery_payload.optimistic = true; + } + + if (hasPosition) { + discoveryEntry.discovery_payload = {...discoveryEntry.discovery_payload, + value_template: '{{ value_json.position }}', + set_position_template: '{ "position": {{ position }} }', + set_position_topic: true, + position_topic: true, }; + } - const effect = def.exposes.find((e) => e.type === 'enum' && e.name === 'effect'); - if (effect) { - discoveryEntry.discovery_payload.effect = true; - discoveryEntry.discovery_payload.effect_list = effect.values; - } - } else if (expose.type === 'switch') { - const state = expose.features.find((f) => f.name === 'state'); - discoveryEntry = { - type: 'switch', - object_id: expose.endpoint ? `switch_${expose.endpoint}` : 'switch', - discovery_payload: { - payload_off: state.value_off, - payload_on: state.value_on, - value_template: `{{ value_json.${state.property} }}`, - command_topic: true, - command_topic_prefix: expose.endpoint ? expose.endpoint : undefined, - }, + if (hasTilt) { + discoveryEntry.discovery_payload = {...discoveryEntry.discovery_payload, + tilt_command_topic: true, + tilt_status_topic: true, + tilt_status_template: '{{ value_json.tilt }}', }; + } + } else if (expose.type === 'fan') { + assert(!expose.endpoint, `Endpoint not supported for fan type`); + discoveryEntry = { + type: 'fan', + object_id: 'fan', + discovery_payload: { + state_topic: true, + state_value_template: '{{ value_json.fan_state }}', + command_topic: true, + command_topic_postfix: 'fan_state', + }, + }; - const different = ['valve_detection', 'window_detection', 'auto_lock', 'away_mode']; - if (different.includes(state.property)) { - discoveryEntry.discovery_payload.command_topic_postfix = state.property; - discoveryEntry.discovery_payload.state_off = state.value_off; - discoveryEntry.discovery_payload.state_on = state.value_on; - discoveryEntry.discovery_payload.state_topic = true; - discoveryEntry.object_id = state.property; + const speed = expose.features.find((e) => e.name === 'mode'); + if (speed) { + discoveryEntry.discovery_payload.speed_state_topic = true; + discoveryEntry.discovery_payload.speed_command_topic = true; + discoveryEntry.discovery_payload.speed_value_template = '{{ value_json.fan_mode }}'; + discoveryEntry.discovery_payload.speeds = speed.values; + } + } else if (expose.type === 'binary') { + const lookup = { + occupancy: {device_class: 'motion'}, + battery_low: {device_class: 'battery'}, + water_leak: {device_class: 'moisture'}, + vibration: {device_class: 'vibration'}, + contact: {device_class: 'door'}, + smoke: {device_class: 'smoke'}, + gas: {device_class: 'gas'}, + carbon_monoxide: {device_class: 'safety'}, + presence: {device_class: 'presence'}, + }; - if (state.property === 'window_detection') { - discoveryEntry.discovery_payload.icon = 'mdi:window-open-variant'; - } - } - } else if (expose.type === 'climate') { - const setpointProperties = ['occupied_heating_setpoint', 'current_heating_setpoint']; - const setpoint = expose.features.find((f) => setpointProperties.includes(f.name)); - assert(setpoint, 'No setpoint found'); - const temperature = expose.features.find((f) => f.name === 'local_temperature'); - assert(temperature, 'No temperature found'); + discoveryEntry = { + type: 'binary_sensor', + object_id: expose.endpoint ? `${expose.name}_${expose.endpoint}` : `${expose.name}`, + discovery_payload: { + value_template: `{{ value_json.${expose.property} }}`, + payload_on: expose.value_on, + payload_off: expose.value_off, + ...(lookup[expose.name] || {}), + }, + }; + } else if (expose.type === 'numeric') { + const lookup = { + battery: {device_class: 'battery'}, + temperature: {device_class: 'temperature'}, + humidity: {device_class: 'humidity'}, + illuminance_lux: {device_class: 'illuminance'}, + illuminance: {device_class: 'illuminance'}, + soil_moisture: {icon: 'mdi:water-percent'}, + position: {icon: 'mdi:valve'}, + pressure: {device_class: 'pressure'}, + power: {device_class: 'power'}, + linkquality: {icon: 'mdi:signal'}, + current: {device_class: 'current'}, + voltage: {device_class: 'voltage'}, + current_phase_b: {device_class: 'current'}, + voltage_phase_b: {device_class: 'voltage'}, + current_phase_c: {device_class: 'current'}, + voltage_phase_c: {device_class: 'voltage'}, + energy: {device_class: 'energy'}, + smoke_density: {icon: 'mdi:google-circles-communities'}, + gas_density: {icon: 'mdi:google-circles-communities'}, + pm25: {icon: 'mdi:air-filter'}, + pm10: {icon: 'mdi:air-filter'}, + voc: {icon: 'mdi:air-filter'}, + aqi: {icon: 'mdi:air-filter'}, + hcho: {icon: 'mdi:air-filter'}, + requested_brightness_level: {icon: 'mdi:brightness-5'}, + requested_brightness_percent: {icon: 'mdi:brightness-5'}, + eco2: {icon: 'mdi:molecule-co2'}, + co2: {icon: 'mdi:molecule-co2'}, + local_temperature: {device_class: 'temperature'}, + }; - discoveryEntry = { - type: 'climate', - object_id: expose.endpoint ? `climate_${expose.endpoint}` : 'climate', - discovery_payload: { - // Static - state_topic: false, - temperature_unit: 'C', - // Setpoint - temp_step: setpoint.value_step, - min_temp: setpoint.value_min.toString(), - max_temp: setpoint.value_max.toString(), - // Temperature - current_temperature_topic: true, - current_temperature_template: `{{ value_json.${temperature.property} }}`, - }, - }; - - const mode = expose.features.find((f) => f.name === 'system_mode'); - if (mode) { - discoveryEntry.discovery_payload.mode_state_topic = true; - discoveryEntry.discovery_payload.mode_state_template = `{{ value_json.${mode.property} }}`; - discoveryEntry.discovery_payload.modes = mode.values; - discoveryEntry.discovery_payload.mode_command_topic = true; - } - - const state = expose.features.find((f) => f.name === 'running_state'); - if (state) { - discoveryEntry.discovery_payload.action_topic = true; - discoveryEntry.discovery_payload.action_template = `{% set values = ` + - `{'idle':'off','heat':'heating','cool':'cooling','fan only':'fan'}` + - ` %}{{ values[value_json.${state.property}] }}`; - } - - const coolingSetpoint = expose.features.find((f) => f.name === 'occupied_cooling_setpoint'); - if (coolingSetpoint) { - discoveryEntry.discovery_payload.temperature_low_command_topic = setpoint.name; - discoveryEntry.discovery_payload.temperature_low_state_template = - `{{ value_json.${setpoint.property} }}`; - discoveryEntry.discovery_payload.temperature_low_state_topic = true; - discoveryEntry.discovery_payload.temperature_high_command_topic = coolingSetpoint.name; - discoveryEntry.discovery_payload.temperature_high_state_template = - `{{ value_json.${coolingSetpoint.property} }}`; - discoveryEntry.discovery_payload.temperature_high_state_topic = true; - } else { - discoveryEntry.discovery_payload.temperature_command_topic = setpoint.name; - discoveryEntry.discovery_payload.temperature_state_template = - `{{ value_json.${setpoint.property} }}`; - discoveryEntry.discovery_payload.temperature_state_topic = true; - } - - const fanMode = expose.features.find((f) => f.name === 'fan_mode'); - if (fanMode) { - discoveryEntry.discovery_payload.fan_modes = fanMode.values; - discoveryEntry.discovery_payload.fan_mode_command_topic = true; - discoveryEntry.discovery_payload.fan_mode_state_template = - `{{ value_json.${fanMode.property} }}`; - discoveryEntry.discovery_payload.fan_mode_state_topic = true; - } - - const preset = expose.features.find((f) => f.name === 'preset'); - if (preset) { - discoveryEntry.discovery_payload.hold_modes = preset.values; - discoveryEntry.discovery_payload.hold_command_topic = true; - discoveryEntry.discovery_payload.hold_state_template = - `{{ value_json.${preset.property} }}`; - discoveryEntry.discovery_payload.hold_state_topic = true; - } - - const awayMode = expose.features.find((f) => f.name === 'away_mode'); - if (awayMode) { - discoveryEntry.discovery_payload.away_mode_command_topic = true; - discoveryEntry.discovery_payload.away_mode_state_topic = true; - discoveryEntry.discovery_payload.away_mode_state_template = - `{{ value_json.${awayMode.property} }}`; - } - - if (expose.endpoint) { - discoveryEntry.discovery_payload.state_topic_postfix = expose.endpoint; - } - } else if (expose.type === 'lock') { - assert(!expose.endpoint, `Endpoint not supported for lock type`); - const state = expose.features.find((f) => f.name === 'state'); - assert(state, 'No state found'); - discoveryEntry = { - type: 'lock', - object_id: 'lock', - discovery_payload: { - command_topic: true, - value_template: `{{ value_json.${state.property} }}`, - }, - }; - - if (state.property === 'keypad_lockout') { - // deprecated: keypad_lockout is messy, but changing is breaking - discoveryEntry.discovery_payload.payload_lock = state.value_on; - discoveryEntry.discovery_payload.payload_unlock = state.value_off; - discoveryEntry.discovery_payload.state_topic = true; - discoveryEntry.object_id = 'keypad_lock'; - } else if (state.property === 'child_lock') { - // deprecated: child_lock is messy, but changing is breaking - discoveryEntry.discovery_payload.payload_lock = state.value_on; - discoveryEntry.discovery_payload.payload_unlock = state.value_off; - discoveryEntry.discovery_payload.state_locked = 'LOCKED'; - discoveryEntry.discovery_payload.state_unlocked = 'UNLOCKED'; - discoveryEntry.discovery_payload.state_topic = true; - discoveryEntry.object_id = 'child_lock'; - } else { - discoveryEntry.discovery_payload.state_locked = state.value_on; - discoveryEntry.discovery_payload.state_unlocked = state.value_off; - } - - if (state.property !== 'state') { - discoveryEntry.discovery_payload.command_topic_postfix = state.property; - } - } else if (expose.type === 'cover') { - assert(!expose.endpoint, `Endpoint not supported for cover type`); - const hasPosition = expose.features.find((e) => e.name === 'position'); - const hasTilt = expose.features.find((e) => e.name === 'tilt'); - - discoveryEntry = { - type: 'cover', - object_id: 'cover', - discovery_payload: { - command_topic: true, - state_topic: !hasPosition, - }, - }; - - if (!hasPosition && !hasTilt) { - discoveryEntry.discovery_payload.optimistic = true; - } - - if (hasPosition) { - discoveryEntry.discovery_payload = {...discoveryEntry.discovery_payload, - value_template: '{{ value_json.position }}', - set_position_template: '{ "position": {{ position }} }', - set_position_topic: true, - position_topic: true, - }; - } - - if (hasTilt) { - discoveryEntry.discovery_payload = {...discoveryEntry.discovery_payload, - tilt_command_topic: true, - tilt_status_topic: true, - tilt_status_template: '{{ value_json.tilt }}', - }; - } - } else if (expose.type === 'fan') { - assert(!expose.endpoint, `Endpoint not supported for fan type`); - discoveryEntry = { - type: 'fan', - object_id: 'fan', - discovery_payload: { - state_topic: true, - state_value_template: '{{ value_json.fan_state }}', - command_topic: true, - command_topic_postfix: 'fan_state', - }, - }; - - const speed = expose.features.find((e) => e.name === 'mode'); - if (speed) { - discoveryEntry.discovery_payload.speed_state_topic = true; - discoveryEntry.discovery_payload.speed_command_topic = true; - discoveryEntry.discovery_payload.speed_value_template = '{{ value_json.fan_mode }}'; - discoveryEntry.discovery_payload.speeds = speed.values; - } - } else if (expose.type === 'binary') { + discoveryEntry = { + type: 'sensor', + object_id: expose.endpoint ? `${expose.name}_${expose.endpoint}` : `${expose.name}`, + discovery_payload: { + unit_of_measurement: expose.unit ? expose.unit : '-', + value_template: `{{ value_json.${expose.property} }}`, + ...lookup[expose.name], + }, + }; + } else if (expose.type === 'enum' || expose.type === 'text' || expose.type === 'composite') { + const ACCESS_STATE = 1; + if (expose.access & ACCESS_STATE) { const lookup = { - occupancy: {device_class: 'motion'}, - battery_low: {device_class: 'battery'}, - water_leak: {device_class: 'moisture'}, - vibration: {device_class: 'vibration'}, - contact: {device_class: 'door'}, - smoke: {device_class: 'smoke'}, - gas: {device_class: 'gas'}, - carbon_monoxide: {device_class: 'safety'}, - presence: {device_class: 'presence'}, - }; - - discoveryEntry = { - type: 'binary_sensor', - object_id: expose.endpoint ? `${expose.name}_${expose.endpoint}` : `${expose.name}`, - discovery_payload: { - value_template: `{{ value_json.${expose.property} }}`, - payload_on: expose.value_on, - payload_off: expose.value_off, - ...(lookup[expose.name] || {}), - }, - }; - } else if (expose.type === 'numeric') { - const lookup = { - battery: {device_class: 'battery'}, - temperature: {device_class: 'temperature'}, - humidity: {device_class: 'humidity'}, - illuminance_lux: {device_class: 'illuminance'}, - illuminance: {device_class: 'illuminance'}, - soil_moisture: {icon: 'mdi:water-percent'}, - position: {icon: 'mdi:valve'}, - pressure: {device_class: 'pressure'}, - power: {device_class: 'power'}, - linkquality: {icon: 'mdi:signal'}, - current: {device_class: 'current'}, - voltage: {device_class: 'voltage'}, - current_phase_b: {device_class: 'current'}, - voltage_phase_b: {device_class: 'voltage'}, - current_phase_c: {device_class: 'current'}, - voltage_phase_c: {device_class: 'voltage'}, - energy: {device_class: 'energy'}, - smoke_density: {icon: 'mdi:google-circles-communities'}, - gas_density: {icon: 'mdi:google-circles-communities'}, - pm25: {icon: 'mdi:air-filter'}, - pm10: {icon: 'mdi:air-filter'}, - voc: {icon: 'mdi:air-filter'}, - aqi: {icon: 'mdi:air-filter'}, - hcho: {icon: 'mdi:air-filter'}, - requested_brightness_level: {icon: 'mdi:brightness-5'}, - requested_brightness_percent: {icon: 'mdi:brightness-5'}, - eco2: {icon: 'mdi:molecule-co2'}, - co2: {icon: 'mdi:molecule-co2'}, - local_temperature: {device_class: 'temperature'}, + action: {icon: 'mdi:gesture-double-tap'}, }; discoveryEntry = { type: 'sensor', - object_id: expose.endpoint ? `${expose.name}_${expose.endpoint}` : `${expose.name}`, + object_id: expose.property, discovery_payload: { - unit_of_measurement: expose.unit ? expose.unit : '-', value_template: `{{ value_json.${expose.property} }}`, ...lookup[expose.name], }, }; - } else if (expose.type === 'enum' || expose.type === 'text' || expose.type === 'composite') { - const ACCESS_STATE = 1; - if (expose.access & ACCESS_STATE) { - const lookup = { - action: {icon: 'mdi:gesture-double-tap'}, - }; - - discoveryEntry = { - type: 'sensor', - object_id: expose.property, - discovery_payload: { - value_template: `{{ value_json.${expose.property} }}`, - ...lookup[expose.name], - }, - }; - } - } else { - throw new Error(`Unsupported exposes type: '${expose.type}'`); - } - - if (discoveryEntry) { - this.mapping[def.model].push(discoveryEntry); } + } else { + throw new Error(`Unsupported exposes type: '${expose.type}'`); + } + + if (discoveryEntry) { + this.mapping[def.model].push(discoveryEntry); } - } else { - logger.warn(`Supported device '${def.model}' has no Home Assistant mapping`); } } diff --git a/lib/extension/networkMap.js b/lib/extension/networkMap.js index 5adf8adf7..83b8e7bfa 100644 --- a/lib/extension/networkMap.js +++ b/lib/extension/networkMap.js @@ -245,7 +245,7 @@ class NetworkMap extends Extension { model: resolvedEntity.definition.model, vendor: resolvedEntity.definition.vendor, description: resolvedEntity.definition.description, - supports: Array.from(new Set((resolvedEntity.definition.exposes || []).map((e) => { + supports: Array.from(new Set((resolvedEntity.definition.exposes).map((e) => { return e.hasOwnProperty('name') ? e.name : `${e.type} (${e.features.map((f) => f.name).join(', ')})`; diff --git a/test/assets/mock-external-converter-multiple.js b/test/assets/mock-external-converter-multiple.js index 338a065f0..4bf7568f0 100644 --- a/test/assets/mock-external-converter-multiple.js +++ b/test/assets/mock-external-converter-multiple.js @@ -11,10 +11,23 @@ const homeassistantSwitch = { const mockDevices = [{ mock: 1, - model: 'external_converters_device', + model: 'external_converters_device_1', homeassistant: [homeassistantSwitch], + zigbeeModel: ['external_converter_device_1'], + vendor: 'external_1', + description: 'external_1', + fromZigbee: [], + toZigbee: [], + exposes: [], }, { - mock: 2 + mock: 2, + model: 'external_converters_device_2', + zigbeeModel: ['external_converter_device_2'], + vendor: 'external_2', + description: 'external_2', + fromZigbee: [], + toZigbee: [], + exposes: [], }]; module.exports = mockDevices; \ No newline at end of file diff --git a/test/assets/mock-external-converter.js b/test/assets/mock-external-converter.js index 01a31b0ef..493c1b972 100644 --- a/test/assets/mock-external-converter.js +++ b/test/assets/mock-external-converter.js @@ -2,10 +2,11 @@ const mockDevice = { mock: true, zigbeeModel: ['external_converter_device'], vendor: 'external', - model: ['external_converter_device'], - definition: { - foo: 'bar' - } + model: 'external_converter_device', + description: 'external', + fromZigbee: [], + toZigbee: [], + exposes: [], }; module.exports = mockDevice; \ No newline at end of file diff --git a/test/externalConverters.test.js b/test/externalConverters.test.js index 705e1ecc6..b6bef0780 100644 --- a/test/externalConverters.test.js +++ b/test/externalConverters.test.js @@ -70,10 +70,11 @@ describe('Loads external converters', () => { mock: true, zigbeeModel: ['external_converter_device'], vendor: 'external', - model: ['external_converter_device'], - definition: { - foo: 'bar' - } + model: 'external_converter_device', + description: 'external', + fromZigbee: [], + toZigbee: [], + exposes: [] }); }); @@ -87,10 +88,23 @@ describe('Loads external converters', () => { expect(zigbeeHerdsmanConverters.addDeviceDefinition).toHaveBeenCalledTimes(2); expect(zigbeeHerdsmanConverters.addDeviceDefinition).toHaveBeenNthCalledWith(1, { mock: 1, - model: 'external_converters_device', + model: 'external_converters_device_1', + zigbeeModel: ['external_converter_device_1'], + vendor: 'external_1', + description: 'external_1', + fromZigbee: [], + toZigbee: [], + exposes: [] }); expect(zigbeeHerdsmanConverters.addDeviceDefinition).toHaveBeenNthCalledWith(2, { - mock: 2 + mock: 2, + model: 'external_converters_device_2', + zigbeeModel: ['external_converter_device_2'], + vendor: 'external_2', + description: 'external_2', + fromZigbee: [], + toZigbee: [], + exposes: [] }); }); diff --git a/test/homeassistant.test.js b/test/homeassistant.test.js index b4da59211..55e8d79f4 100644 --- a/test/homeassistant.test.js +++ b/test/homeassistant.test.js @@ -1404,14 +1404,11 @@ describe('HomeAssistant extension', () => { it('Load Home Assistant mapping from external converters', async () => { fs.copyFileSync(path.join(__dirname, 'assets', 'mock-external-converter-multiple.js'), path.join(data.mockDir, 'mock-external-converter-multiple.js')); - const beforeCount = Object.entries((new HomeAssistant(null, null, null, null, {on: () => {}}))._getMapping()).length; settings.set(['external_converters'], ['mock-external-converter-multiple.js']); controller = new Controller(); const ha = controller.extensions.find((e) => e.constructor.name === 'HomeAssistant'); await controller.start(); await flushPromises(); - const afterCount = Object.entries(ha._getMapping()).length; - expect(beforeCount + 1).toStrictEqual(afterCount); const homeassistantSwitch = { type: 'switch', @@ -1423,7 +1420,7 @@ describe('HomeAssistant extension', () => { command_topic: true, }, }; - expect(ha._getMapping()['external_converters_device']).toEqual([homeassistantSwitch]); + expect(ha._getMapping()['external_converters_device_1']).toEqual([homeassistantSwitch]); }); it('Should clear outdated configs', async () => { diff --git a/test/networkMap.test.js b/test/networkMap.test.js index f40908967..a336aaaba 100644 --- a/test/networkMap.test.js +++ b/test/networkMap.test.js @@ -97,7 +97,7 @@ describe('Networkmap', () => { let call = MQTT.publish.mock.calls[0]; expect(call[0]).toStrictEqual('zigbee2mqtt/bridge/networkmap/raw'); - const expected = {"links":[{"depth":1,"linkquality":120,"lqi":120,"relationship":2,"routes":[],"source":{"ieeeAddr":"0x000b57fffec6a5b3","networkAddress":40399},"sourceIeeeAddr":"0x000b57fffec6a5b3","sourceNwkAddr":40399,"target":{"ieeeAddr":"0x00124b00120144ae","networkAddress":0},"targetIeeeAddr":"0x00124b00120144ae"},{"depth":1,"linkquality":92,"lqi":92,"relationship":2,"routes":[{"destinationAddress":6540,"nextHop":40369,"status":"ACTIVE"}],"source":{"ieeeAddr":"0x000b57fffec6a5b2","networkAddress":40369},"sourceIeeeAddr":"0x000b57fffec6a5b2","sourceNwkAddr":40369,"target":{"ieeeAddr":"0x00124b00120144ae","networkAddress":0},"targetIeeeAddr":"0x00124b00120144ae"},{"depth":1,"linkquality":92,"lqi":92,"relationship":2,"routes":[],"source":{"ieeeAddr":"0x0017880104e45511","networkAddress":1114},"sourceIeeeAddr":"0x0017880104e45511","sourceNwkAddr":1114,"target":{"ieeeAddr":"0x00124b00120144ae","networkAddress":0},"targetIeeeAddr":"0x00124b00120144ae"},{"depth":2,"linkquality":110,"lqi":110,"relationship":1,"routes":[],"source":{"ieeeAddr":"0x000b57fffec6a5b3","networkAddress":40399},"sourceIeeeAddr":"0x000b57fffec6a5b3","sourceNwkAddr":40399,"target":{"ieeeAddr":"0x000b57fffec6a5b2","networkAddress":40369},"targetIeeeAddr":"0x000b57fffec6a5b2"},{"depth":2,"linkquality":100,"lqi":100,"relationship":1,"routes":[],"source":{"ieeeAddr":"0x0017880104e45559","networkAddress":6540},"sourceIeeeAddr":"0x0017880104e45559","sourceNwkAddr":6540,"target":{"ieeeAddr":"0x000b57fffec6a5b2","networkAddress":40369},"targetIeeeAddr":"0x000b57fffec6a5b2"},{"depth":2,"linkquality":130,"lqi":130,"relationship":1,"routes":[],"source":{"ieeeAddr":"0x0017880104e45521","networkAddress":6538},"sourceIeeeAddr":"0x0017880104e45521","sourceNwkAddr":6538,"target":{"ieeeAddr":"0x0017880104e45559","networkAddress":6540},"targetIeeeAddr":"0x0017880104e45559"}],"nodes":[{"definition":null,"failed":[],"friendlyName":"Coordinator","ieeeAddr":"0x00124b00120144ae","lastSeen":1000,"modelID":null,"networkAddress":0,"type":"Coordinator"},{"definition":{"description":"TRADFRI LED bulb E26/E27 980 lumen, dimmable, white spectrum, opal white","model":"LED1545G12","supports":"light (state, brightness, color_temp, color_temp_startup), effect, linkquality","vendor":"IKEA"},"failed":[],"friendlyName":"bulb","ieeeAddr":"0x000b57fffec6a5b2","lastSeen":1000,"modelID":"TRADFRI bulb E27 WS opal 980lm","networkAddress":40369,"type":"Router"},{"definition":{"description":"Hue Go","model":"7146060PH","supports":"light (state, brightness, color_temp, color_temp_startup, color_xy, color_hs), effect, linkquality","vendor":"Philips"},"failed":[],"friendlyName":"bulb_color","ieeeAddr":"0x000b57fffec6a5b3","lastSeen":1000,"modelID":"LLC020","networkAddress":40399,"type":"Router"},{"definition":{"description":"Aqara double key wireless wall switch (2016 model)","model":"WXKG02LM_rev1","supports":"battery, action, linkquality","vendor":"Xiaomi"},"friendlyName":"button_double_key","ieeeAddr":"0x0017880104e45521","lastSeen":1000,"modelID":"lumi.sensor_86sw2.es1","networkAddress":6538,"type":"EndDevice"},{"definition":null,"failed":["lqi","routingTable"],"friendlyName":"0x0017880104e45525","ieeeAddr":"0x0017880104e45525","lastSeen":1000,"manufacturerName":"Boef","modelID":"notSupportedModelID","networkAddress":6536,"type":"Router"},{"definition":{"description":"[CC2530 router](http://ptvo.info/cc2530-based-zigbee-coordinator-and-router-112/)","model":"CC2530.ROUTER","supports":"led, linkquality","vendor":"Custom devices (DiY)"},"failed":[],"friendlyName":"cc2530_router","ieeeAddr":"0x0017880104e45559","lastSeen":1000,"modelID":"lumi.router","networkAddress":6540,"type":"Router"},{"definition":{"model":["external_converter_device"],"supports":"","vendor":"external"},"friendlyName":"0x0017880104e45511","ieeeAddr":"0x0017880104e45511","lastSeen":1000,"modelID":"external_converter_device","networkAddress":1114,"type":"EndDevice"}]}; + const expected = {"links":[{"depth":1,"linkquality":120,"lqi":120,"relationship":2,"routes":[],"source":{"ieeeAddr":"0x000b57fffec6a5b3","networkAddress":40399},"sourceIeeeAddr":"0x000b57fffec6a5b3","sourceNwkAddr":40399,"target":{"ieeeAddr":"0x00124b00120144ae","networkAddress":0},"targetIeeeAddr":"0x00124b00120144ae"},{"depth":1,"linkquality":92,"lqi":92,"relationship":2,"routes":[{"destinationAddress":6540,"nextHop":40369,"status":"ACTIVE"}],"source":{"ieeeAddr":"0x000b57fffec6a5b2","networkAddress":40369},"sourceIeeeAddr":"0x000b57fffec6a5b2","sourceNwkAddr":40369,"target":{"ieeeAddr":"0x00124b00120144ae","networkAddress":0},"targetIeeeAddr":"0x00124b00120144ae"},{"depth":1,"linkquality":92,"lqi":92,"relationship":2,"routes":[],"source":{"ieeeAddr":"0x0017880104e45511","networkAddress":1114},"sourceIeeeAddr":"0x0017880104e45511","sourceNwkAddr":1114,"target":{"ieeeAddr":"0x00124b00120144ae","networkAddress":0},"targetIeeeAddr":"0x00124b00120144ae"},{"depth":2,"linkquality":110,"lqi":110,"relationship":1,"routes":[],"source":{"ieeeAddr":"0x000b57fffec6a5b3","networkAddress":40399},"sourceIeeeAddr":"0x000b57fffec6a5b3","sourceNwkAddr":40399,"target":{"ieeeAddr":"0x000b57fffec6a5b2","networkAddress":40369},"targetIeeeAddr":"0x000b57fffec6a5b2"},{"depth":2,"linkquality":100,"lqi":100,"relationship":1,"routes":[],"source":{"ieeeAddr":"0x0017880104e45559","networkAddress":6540},"sourceIeeeAddr":"0x0017880104e45559","sourceNwkAddr":6540,"target":{"ieeeAddr":"0x000b57fffec6a5b2","networkAddress":40369},"targetIeeeAddr":"0x000b57fffec6a5b2"},{"depth":2,"linkquality":130,"lqi":130,"relationship":1,"routes":[],"source":{"ieeeAddr":"0x0017880104e45521","networkAddress":6538},"sourceIeeeAddr":"0x0017880104e45521","sourceNwkAddr":6538,"target":{"ieeeAddr":"0x0017880104e45559","networkAddress":6540},"targetIeeeAddr":"0x0017880104e45559"}],"nodes":[{"definition":null,"failed":[],"friendlyName":"Coordinator","ieeeAddr":"0x00124b00120144ae","lastSeen":1000,"modelID":null,"networkAddress":0,"type":"Coordinator"},{"definition":{"description":"TRADFRI LED bulb E26/E27 980 lumen, dimmable, white spectrum, opal white","model":"LED1545G12","supports":"light (state, brightness, color_temp, color_temp_startup), effect, linkquality","vendor":"IKEA"},"failed":[],"friendlyName":"bulb","ieeeAddr":"0x000b57fffec6a5b2","lastSeen":1000,"modelID":"TRADFRI bulb E27 WS opal 980lm","networkAddress":40369,"type":"Router"},{"definition":{"description":"Hue Go","model":"7146060PH","supports":"light (state, brightness, color_temp, color_temp_startup, color_xy, color_hs), effect, linkquality","vendor":"Philips"},"failed":[],"friendlyName":"bulb_color","ieeeAddr":"0x000b57fffec6a5b3","lastSeen":1000,"modelID":"LLC020","networkAddress":40399,"type":"Router"},{"definition":{"description":"Aqara double key wireless wall switch (2016 model)","model":"WXKG02LM_rev1","supports":"battery, action, linkquality","vendor":"Xiaomi"},"friendlyName":"button_double_key","ieeeAddr":"0x0017880104e45521","lastSeen":1000,"modelID":"lumi.sensor_86sw2.es1","networkAddress":6538,"type":"EndDevice"},{"definition":null,"failed":["lqi","routingTable"],"friendlyName":"0x0017880104e45525","ieeeAddr":"0x0017880104e45525","lastSeen":1000,"manufacturerName":"Boef","modelID":"notSupportedModelID","networkAddress":6536,"type":"Router"},{"definition":{"description":"[CC2530 router](http://ptvo.info/cc2530-based-zigbee-coordinator-and-router-112/)","model":"CC2530.ROUTER","supports":"led, linkquality","vendor":"Custom devices (DiY)"},"failed":[],"friendlyName":"cc2530_router","ieeeAddr":"0x0017880104e45559","lastSeen":1000,"modelID":"lumi.router","networkAddress":6540,"type":"Router"},{"definition":{"description": "external","model":"external_converter_device","supports":"","vendor":"external"},"friendlyName":"0x0017880104e45511","ieeeAddr":"0x0017880104e45511","lastSeen":1000,"modelID":"external_converter_device","networkAddress":1114,"type":"EndDevice"}]}; expect(JSON.parse(call[1])).toStrictEqual(expected); /** @@ -148,7 +148,7 @@ describe('Networkmap', () => { "0x0017880104e45525" [style="rounded, filled", fillcolor="#4ea3e0", fontcolor="#ffffff", label="{0x0017880104e45525|0x0017880104e45525 (6536)failed: lqi,routingTable|Boef notSupportedModelID|9 seconds ago}"]; "0x0017880104e45559" [style="rounded, filled", fillcolor="#4ea3e0", fontcolor="#ffffff", label="{cc2530_router|0x0017880104e45559 (6540)|Custom devices (DiY) [CC2530 router](http://ptvo.info/cc2530-based-zigbee-coordinator-and-router-112/) (CC2530.ROUTER)|9 seconds ago}"]; "0x0017880104e45559" -> "0x000b57fffec6a5b2" [penwidth=0.5, weight=0, color="#994444", label="100"] - "0x0017880104e45511" [style="rounded, dashed, filled", fillcolor="#fff8ce", fontcolor="#000000", label="{0x0017880104e45511|0x0017880104e45511 (1114)|external undefined (external_converter_device)|9 seconds ago}"]; + "0x0017880104e45511" [style="rounded, dashed, filled", fillcolor="#fff8ce", fontcolor="#000000", label="{0x0017880104e45511|0x0017880104e45511 (1114)|external external (external_converter_device)|9 seconds ago}"]; "0x0017880104e45511" -> "0x00124b00120144ae" [penwidth=1, weight=0, color="#994444", label="92"] }`; @@ -182,7 +182,7 @@ describe('Networkmap', () => { --- 0x0017880104e45511 (1114) --- - external undefined (external_converter_device) + external external (external_converter_device) --- 9 seconds ago ] @@ -271,7 +271,7 @@ describe('Networkmap', () => { let call = MQTT.publish.mock.calls[0]; expect(call[0]).toStrictEqual('zigbee2mqtt/bridge/response/networkmap'); - const expected = {"data":{"routes":true,"type":"raw","value":{"links":[{"depth":1,"linkquality":120,"lqi":120,"relationship":2,"routes":[],"source":{"ieeeAddr":"0x000b57fffec6a5b3","networkAddress":40399},"sourceIeeeAddr":"0x000b57fffec6a5b3","sourceNwkAddr":40399,"target":{"ieeeAddr":"0x00124b00120144ae","networkAddress":0},"targetIeeeAddr":"0x00124b00120144ae"},{"depth":1,"linkquality":92,"lqi":92,"relationship":2,"routes":[{"destinationAddress":6540,"nextHop":40369,"status":"ACTIVE"}],"source":{"ieeeAddr":"0x000b57fffec6a5b2","networkAddress":40369},"sourceIeeeAddr":"0x000b57fffec6a5b2","sourceNwkAddr":40369,"target":{"ieeeAddr":"0x00124b00120144ae","networkAddress":0},"targetIeeeAddr":"0x00124b00120144ae"},{"depth":1,"linkquality":92,"lqi":92,"relationship":2,"routes":[],"source":{"ieeeAddr":"0x0017880104e45511","networkAddress":1114},"sourceIeeeAddr":"0x0017880104e45511","sourceNwkAddr":1114,"target":{"ieeeAddr":"0x00124b00120144ae","networkAddress":0},"targetIeeeAddr":"0x00124b00120144ae"},{"depth":2,"linkquality":110,"lqi":110,"relationship":1,"routes":[],"source":{"ieeeAddr":"0x000b57fffec6a5b3","networkAddress":40399},"sourceIeeeAddr":"0x000b57fffec6a5b3","sourceNwkAddr":40399,"target":{"ieeeAddr":"0x000b57fffec6a5b2","networkAddress":40369},"targetIeeeAddr":"0x000b57fffec6a5b2"},{"depth":2,"linkquality":100,"lqi":100,"relationship":1,"routes":[],"source":{"ieeeAddr":"0x0017880104e45559","networkAddress":6540},"sourceIeeeAddr":"0x0017880104e45559","sourceNwkAddr":6540,"target":{"ieeeAddr":"0x000b57fffec6a5b2","networkAddress":40369},"targetIeeeAddr":"0x000b57fffec6a5b2"},{"depth":2,"linkquality":130,"lqi":130,"relationship":1,"routes":[],"source":{"ieeeAddr":"0x0017880104e45521","networkAddress":6538},"sourceIeeeAddr":"0x0017880104e45521","sourceNwkAddr":6538,"target":{"ieeeAddr":"0x0017880104e45559","networkAddress":6540},"targetIeeeAddr":"0x0017880104e45559"}],"nodes":[{"definition":null,"failed":[],"friendlyName":"Coordinator","ieeeAddr":"0x00124b00120144ae","lastSeen":1000,"modelID":null,"networkAddress":0,"type":"Coordinator"},{"definition":{"description":"TRADFRI LED bulb E26/E27 980 lumen, dimmable, white spectrum, opal white","model":"LED1545G12","supports":"light (state, brightness, color_temp, color_temp_startup), effect, linkquality","vendor":"IKEA"},"failed":[],"friendlyName":"bulb","ieeeAddr":"0x000b57fffec6a5b2","lastSeen":1000,"modelID":"TRADFRI bulb E27 WS opal 980lm","networkAddress":40369,"type":"Router"},{"definition":{"description":"Hue Go","model":"7146060PH","supports":"light (state, brightness, color_temp, color_temp_startup, color_xy, color_hs), effect, linkquality","vendor":"Philips"},"failed":[],"friendlyName":"bulb_color","ieeeAddr":"0x000b57fffec6a5b3","lastSeen":1000,"modelID":"LLC020","networkAddress":40399,"type":"Router"},{"definition":{"description":"Aqara double key wireless wall switch (2016 model)","model":"WXKG02LM_rev1","supports":"battery, action, linkquality","vendor":"Xiaomi"},"friendlyName":"button_double_key","ieeeAddr":"0x0017880104e45521","lastSeen":1000,"modelID":"lumi.sensor_86sw2.es1","networkAddress":6538,"type":"EndDevice"},{"definition":null,"failed":["lqi","routingTable"],"friendlyName":"0x0017880104e45525","ieeeAddr":"0x0017880104e45525","lastSeen":1000,"manufacturerName":"Boef","modelID":"notSupportedModelID","networkAddress":6536,"type":"Router"},{"definition":{"description":"[CC2530 router](http://ptvo.info/cc2530-based-zigbee-coordinator-and-router-112/)","model":"CC2530.ROUTER","supports":"led, linkquality","vendor":"Custom devices (DiY)"},"failed":[],"friendlyName":"cc2530_router","ieeeAddr":"0x0017880104e45559","lastSeen":1000,"modelID":"lumi.router","networkAddress":6540,"type":"Router"},{"definition":{"model":["external_converter_device"],"supports":"","vendor":"external"},"friendlyName":"0x0017880104e45511","ieeeAddr":"0x0017880104e45511","lastSeen":1000,"modelID":"external_converter_device","networkAddress":1114,"type":"EndDevice"}]}},"status":"ok"}; + const expected = {"data":{"routes":true,"type":"raw","value":{"links":[{"depth":1,"linkquality":120,"lqi":120,"relationship":2,"routes":[],"source":{"ieeeAddr":"0x000b57fffec6a5b3","networkAddress":40399},"sourceIeeeAddr":"0x000b57fffec6a5b3","sourceNwkAddr":40399,"target":{"ieeeAddr":"0x00124b00120144ae","networkAddress":0},"targetIeeeAddr":"0x00124b00120144ae"},{"depth":1,"linkquality":92,"lqi":92,"relationship":2,"routes":[{"destinationAddress":6540,"nextHop":40369,"status":"ACTIVE"}],"source":{"ieeeAddr":"0x000b57fffec6a5b2","networkAddress":40369},"sourceIeeeAddr":"0x000b57fffec6a5b2","sourceNwkAddr":40369,"target":{"ieeeAddr":"0x00124b00120144ae","networkAddress":0},"targetIeeeAddr":"0x00124b00120144ae"},{"depth":1,"linkquality":92,"lqi":92,"relationship":2,"routes":[],"source":{"ieeeAddr":"0x0017880104e45511","networkAddress":1114},"sourceIeeeAddr":"0x0017880104e45511","sourceNwkAddr":1114,"target":{"ieeeAddr":"0x00124b00120144ae","networkAddress":0},"targetIeeeAddr":"0x00124b00120144ae"},{"depth":2,"linkquality":110,"lqi":110,"relationship":1,"routes":[],"source":{"ieeeAddr":"0x000b57fffec6a5b3","networkAddress":40399},"sourceIeeeAddr":"0x000b57fffec6a5b3","sourceNwkAddr":40399,"target":{"ieeeAddr":"0x000b57fffec6a5b2","networkAddress":40369},"targetIeeeAddr":"0x000b57fffec6a5b2"},{"depth":2,"linkquality":100,"lqi":100,"relationship":1,"routes":[],"source":{"ieeeAddr":"0x0017880104e45559","networkAddress":6540},"sourceIeeeAddr":"0x0017880104e45559","sourceNwkAddr":6540,"target":{"ieeeAddr":"0x000b57fffec6a5b2","networkAddress":40369},"targetIeeeAddr":"0x000b57fffec6a5b2"},{"depth":2,"linkquality":130,"lqi":130,"relationship":1,"routes":[],"source":{"ieeeAddr":"0x0017880104e45521","networkAddress":6538},"sourceIeeeAddr":"0x0017880104e45521","sourceNwkAddr":6538,"target":{"ieeeAddr":"0x0017880104e45559","networkAddress":6540},"targetIeeeAddr":"0x0017880104e45559"}],"nodes":[{"definition":null,"failed":[],"friendlyName":"Coordinator","ieeeAddr":"0x00124b00120144ae","lastSeen":1000,"modelID":null,"networkAddress":0,"type":"Coordinator"},{"definition":{"description":"TRADFRI LED bulb E26/E27 980 lumen, dimmable, white spectrum, opal white","model":"LED1545G12","supports":"light (state, brightness, color_temp, color_temp_startup), effect, linkquality","vendor":"IKEA"},"failed":[],"friendlyName":"bulb","ieeeAddr":"0x000b57fffec6a5b2","lastSeen":1000,"modelID":"TRADFRI bulb E27 WS opal 980lm","networkAddress":40369,"type":"Router"},{"definition":{"description":"Hue Go","model":"7146060PH","supports":"light (state, brightness, color_temp, color_temp_startup, color_xy, color_hs), effect, linkquality","vendor":"Philips"},"failed":[],"friendlyName":"bulb_color","ieeeAddr":"0x000b57fffec6a5b3","lastSeen":1000,"modelID":"LLC020","networkAddress":40399,"type":"Router"},{"definition":{"description":"Aqara double key wireless wall switch (2016 model)","model":"WXKG02LM_rev1","supports":"battery, action, linkquality","vendor":"Xiaomi"},"friendlyName":"button_double_key","ieeeAddr":"0x0017880104e45521","lastSeen":1000,"modelID":"lumi.sensor_86sw2.es1","networkAddress":6538,"type":"EndDevice"},{"definition":null,"failed":["lqi","routingTable"],"friendlyName":"0x0017880104e45525","ieeeAddr":"0x0017880104e45525","lastSeen":1000,"manufacturerName":"Boef","modelID":"notSupportedModelID","networkAddress":6536,"type":"Router"},{"definition":{"description":"[CC2530 router](http://ptvo.info/cc2530-based-zigbee-coordinator-and-router-112/)","model":"CC2530.ROUTER","supports":"led, linkquality","vendor":"Custom devices (DiY)"},"failed":[],"friendlyName":"cc2530_router","ieeeAddr":"0x0017880104e45559","lastSeen":1000,"modelID":"lumi.router","networkAddress":6540,"type":"Router"},{"definition":{"description":"external","model":"external_converter_device","supports":"","vendor":"external"},"friendlyName":"0x0017880104e45511","ieeeAddr":"0x0017880104e45511","lastSeen":1000,"modelID":"external_converter_device","networkAddress":1114,"type":"EndDevice"}]}},"status":"ok"}; const actual = JSON.parse(call[1]); expect(actual).toStrictEqual(expected); });