Changes for exposes.

This commit is contained in:
Koen Kanters
2020-11-04 22:33:00 +01:00
parent 3c2eab00d0
commit c27e66b618
3 changed files with 70 additions and 265 deletions
+1 -2
View File
@@ -527,8 +527,7 @@ class Bridge extends Extension {
model: definition.model,
vendor: definition.vendor,
description: definition.description,
[definition.hasOwnProperty('exposes') ? 'exposes' : 'supports']:
definition.hasOwnProperty('exposes') ? definition.exposes : definition.supports,
exposes: definition.exposes,
};
} else {
return null;
+69 -250
View File
@@ -9,26 +9,6 @@ const assert = require('assert');
const cfg = {
// Binary sensor
'binary_sensor_water_leak': {
type: 'binary_sensor',
object_id: 'water_leak',
discovery_payload: {
payload_on: true,
payload_off: false,
value_template: '{{ value_json.water_leak }}',
device_class: 'moisture',
},
},
'binary_sensor_battery_low': {
type: 'binary_sensor',
object_id: 'battery_low',
discovery_payload: {
payload_on: true,
payload_off: false,
value_template: '{{ value_json.battery_low}}',
device_class: 'battery',
},
},
'binary_sensor_update_available': {
type: 'binary_sensor',
object_id: 'update_available',
@@ -48,15 +28,6 @@ const cfg = {
value_template: `{{ value_json['update']['state'] }}`,
},
},
'sensor_local_temperature': {
type: 'sensor',
object_id: 'local_temperature',
discovery_payload: {
unit_of_measurement: '°C',
device_class: 'temperature',
value_template: '{{ value_json.local_temperature }}',
},
},
'sensor_click': {
type: 'sensor',
object_id: 'click',
@@ -74,15 +45,6 @@ const cfg = {
value_template: '{{ value_json.brightness }}',
},
},
'sensor_battery': {
type: 'sensor',
object_id: 'battery',
discovery_payload: {
unit_of_measurement: '%',
device_class: 'battery',
value_template: '{{ value_json.battery }}',
},
},
'sensor_linkquality': {
type: 'sensor',
object_id: 'linkquality',
@@ -93,65 +55,6 @@ const cfg = {
},
},
// Switch
'switch_window_detection': {
type: 'switch',
object_id: 'window_detection',
discovery_payload: {
state_topic: true,
command_topic: true,
command_topic_postfix: 'window_detection',
payload_off: 'OFF',
payload_on: 'ON',
state_off: 'OFF',
state_on: 'ON',
value_template: '{{ value_json.window_detection }}',
icon: 'mdi:window-open-variant',
},
},
'switch_valve_detection': {
type: 'switch',
object_id: 'valve_detection',
discovery_payload: {
state_topic: true,
command_topic: true,
command_topic_postfix: 'valve_detection',
payload_off: 'OFF',
payload_on: 'ON',
state_off: 'OFF',
state_on: 'ON',
value_template: '{{ value_json.valve_detection }}',
},
},
// Lock
'lock_keypad_lockout': {
type: 'lock',
object_id: 'keypad_lock',
discovery_payload: {
state_topic: true,
command_topic: true,
command_topic_postfix: 'keypad_lockout',
payload_unlock: '0',
payload_lock: '1',
value_template: '{{ value_json.keypad_lockout }}',
},
},
'lock_child_lock': {
type: 'lock',
object_id: 'child_lock',
discovery_payload: {
state_topic: true,
command_topic: true,
command_topic_postfix: 'child_lock',
payload_lock: 'LOCK',
payload_unlock: 'UNLOCK',
state_locked: 'LOCKED',
state_unlocked: 'UNLOCKED',
value_template: '{{ value_json.child_lock }}',
},
},
// Trigger
'trigger_action': {
type: 'device_automation',
@@ -169,146 +72,6 @@ const cfg = {
},
};
/* istanbul ignore next */
const climate = (minTemp=7, maxTemp=30, temperatureStateProperty='occupied_heating_setpoint',
tempStep=1, systemModes=['off', 'auto', 'heat'], fanModes=[], holdModes=[],
temperatureLowStateTopic=false, temperatureHighStateTopic=false, endpoint=null ) => {
const jsonProperty = (key) => `value_json.${key}${endpoint ? `_${endpoint}` : ''}`;
const retVal = {
type: 'climate',
object_id: endpoint ? `climate_${endpoint}` : 'climate',
discovery_payload: {
state_topic: false,
temperature_unit: 'C',
min_temp: `${minTemp}`,
max_temp: `${maxTemp}`,
mode_state_topic: true,
mode_state_template: `{{ ${jsonProperty('system_mode')} }}`,
mode_command_topic: true,
current_temperature_topic: true,
current_temperature_template: `{{ ${jsonProperty('local_temperature')} }}`,
temp_step: tempStep,
action_topic: true,
action_template:
'{% set values = {\'idle\':\'off\',\'heat\':\'heating\',\'cool\':\'cooling\',\'fan only\':\'fan\'}'+
` %}{{ values[${jsonProperty('running_state')}] }}`,
},
};
if (endpoint) {
retVal.discovery_payload.state_topic_postfix = endpoint;
}
// system_modes empty <=> use auto (in other case ha ui is showing all modes)
if (systemModes.length > 0) {
retVal.discovery_payload.modes = systemModes;
} else {
retVal.discovery_payload.modes = ['auto'];
}
// hold_modes empty <=> don't use presets
if (holdModes.length > 0) {
// NOTE: Preset 'none' will be added as first item on HA side `mqtt/climate.py preset_modes()`
const indexOfNone = holdModes.indexOf('none');
if (indexOfNone > -1) holdModes.splice(indexOfNone, 1);
// HA has special behaviour for the away mode
// https://github.com/Koenkk/zigbee2mqtt/pull/4491#issuecomment-701550476
const indexOfAway = holdModes.indexOf('away');
/* istanbul ignore else */
if (indexOfAway > -1) {
holdModes.splice(indexOfAway, 1); // HA will add "Away" to modes by itself
retVal.discovery_payload.away_mode_command_topic = true;
retVal.discovery_payload.away_mode_state_topic = true;
retVal.discovery_payload.away_mode_state_template =
`{{ ${jsonProperty('away_mode')} }}`;
}
if (holdModes.length > 0) { // || indexOfAway > -1) {
retVal.discovery_payload.hold_modes = holdModes;
retVal.discovery_payload.hold_command_topic = true;
retVal.discovery_payload.hold_state_template = `{{ ${jsonProperty('preset')} }}`;
retVal.discovery_payload.hold_state_topic = true;
}
}
// fan_modes empty <=> don't use fan modes
if (fanModes.length > 0) {
retVal.discovery_payload.fan_modes = fanModes;
retVal.discovery_payload.fan_mode_command_topic = true;
retVal.discovery_payload.fan_mode_state_template = `{{ ${jsonProperty('fan_mode')} }}`;
retVal.discovery_payload.fan_mode_state_topic = true;
}
// if no high and low temp used then use temperature_state_topic
if (!temperatureHighStateTopic && !temperatureLowStateTopic) {
retVal.discovery_payload.temperature_state_topic = true;
retVal.discovery_payload.temperature_state_template = `{{ ${jsonProperty(temperatureStateProperty)} }}`;
retVal.discovery_payload.temperature_command_topic = temperatureStateProperty;
}
// use low target temperature
if (temperatureLowStateTopic) {
retVal.discovery_payload.temperature_low_state_topic = temperatureLowStateTopic;
retVal.discovery_payload.temperature_low_state_template = `{{ ${jsonProperty('occupied_heating_setpoint')} }}`;
retVal.discovery_payload.temperature_low_command_topic = 'occupied_heating_setpoint';
}
// use high target temperature
if (temperatureHighStateTopic) {
retVal.discovery_payload.temperature_high_state_topic = temperatureHighStateTopic;
retVal.discovery_payload.temperature_high_state_template = `{{ ${jsonProperty('occupied_cooling_setpoint')} }}`;
retVal.discovery_payload.temperature_high_command_topic = 'occupied_cooling_setpoint';
}
return retVal;
};
// Map Home Assistant configurations to devices.
const manualMaping = {
'GS361A-H04': [
cfg.lock_child_lock,
cfg.switch_window_detection,
cfg.switch_valve_detection,
climate(5, 30, 'current_heating_setpoint', 0.5, ['off', 'auto', 'heat', 'manual']),
cfg.sensor_battery,
],
'3157100': [climate(10, 30, 'occupied_heating_setpoint', 1, ['off', 'heat', 'cool'],
['auto', 'on'], [], true, true), cfg.sensor_battery],
'RC-2000WH': [climate(10, 30, 'occupied_heating_setpoint', 1, ['off', 'auto', 'heat', 'cool'],
['auto', 'on', 'smart'], [], true, true)],
'TS0601_thermostat': [
cfg.lock_child_lock, cfg.switch_window_detection, cfg.switch_valve_detection, cfg.sensor_battery,
climate(5, 30, 'current_heating_setpoint', 0.5, [], [],
['schedule', 'manual', 'away', 'boost', 'complex', 'comfort', 'eco']),
],
'HT-08': [
cfg.lock_child_lock,
climate(5, 35, 'current_heating_setpoint', 0.5,
['off', 'heat', 'auto'], [], ['none', 'away']),
],
'HT-10': [
cfg.lock_child_lock, cfg.binary_sensor_battery_low,
climate(5, 35, 'current_heating_setpoint', 0.5,
['off', 'heat', 'auto'], [], ['none', 'away']),
],
'07703L': [
cfg.lock_child_lock, cfg.binary_sensor_battery_low,
climate(5, 35, 'current_heating_setpoint', 0.5,
['off', 'heat', 'auto'], [], ['none', 'away']),
],
'BHT-002-GCLZB': [
cfg.lock_child_lock, climate(5, 30, 'current_heating_setpoint', 1, ['off', 'heat'], [], ['hold', 'program']),
],
'SLR2': [
climate(7, 30, 'occupied_heating_setpoint', 1, ['off', 'auto', 'heat'], [], [], false, false, 'heat'),
climate(7, 30, 'occupied_heating_setpoint', 1, ['off', 'auto', 'heat'], [], [], false, false, 'cool'),
],
'SEA801-Zigbee': [
cfg.binary_sensor_battery_low,
climate(5, 30, 'current_heating_setpoint', 0.5, ['off', 'heat'], [], ['manual', 'auto']),
],
'SEA802-Zigbee': [
cfg.binary_sensor_battery_low,
climate(5, 30, 'current_heating_setpoint', 0.5, ['off', 'heat'], [], ['manual', 'auto']),
],
};
const defaultStatusTopic = 'homeassistant/status';
/**
@@ -348,7 +111,6 @@ class HomeAssistant extends Extension {
populateMapping() {
for (const def of zigbeeHerdsmanConverters.definitions) {
if (def.hasOwnProperty('exposes')) {
assert(!manualMaping.hasOwnProperty(def.model), `'${def.model}' has manual mapping and exposes`);
this.mapping[def.model] = [];
if (['WXKG01LM', 'HS1EB/HS1EB-E', 'ICZB-KPD14S', 'TERNCY-SD01', 'TERNCY-PP01', 'ICZB-KPD18S',
@@ -390,19 +152,31 @@ class HomeAssistant extends Extension {
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: 'OFF',
payload_on: 'ON',
value_template: `{{ value_json.state${expose.endpoint ? `_${expose.endpoint}` : ''} }}`,
value_template: `{{ value_json.${state.property} }}`,
command_topic: true,
command_topic_prefix: expose.endpoint ? expose.endpoint : undefined,
},
};
if (state.property === 'valve_detection' || state.property === 'window_detection') {
discoveryEntry.discovery_payload.command_topic_postfix = state.property;
discoveryEntry.discovery_payload.state_off = 'OFF';
discoveryEntry.discovery_payload.state_on = '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') {
assert(!expose.endpoint, `Endpoint not supported for climate type`);
const setpointProperties = ['occupied_heating_setpoint', 'current_heating_setpoint'];
const setpoint = expose.features.find((f) => setpointProperties.includes(f.name));
assert(setpoint, 'No setpoint found');
@@ -424,9 +198,6 @@ class HomeAssistant extends Extension {
temp_step: setpoint.value_step,
min_temp: setpoint.value_min.toString(),
max_temp: setpoint.value_max.toString(),
temperature_command_topic: setpoint.property,
temperature_state_template: `{{ value_json.${setpoint.property} }}`,
temperature_state_topic: true,
// Temperature
current_temperature_topic: true,
current_temperature_template: `{{ value_json.${temperature.property} }}`,
@@ -442,6 +213,53 @@ class HomeAssistant extends Extension {
` %}{{ 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');
@@ -461,6 +279,14 @@ class HomeAssistant extends Extension {
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;
@@ -613,8 +439,6 @@ class HomeAssistant extends Extension {
this.mapping[def.model].push(discoveryEntry);
}
}
} else if (manualMaping.hasOwnProperty(def.model)) {
this.mapping[def.model] = manualMaping[def.model];
} else {
logger.warn(`Supported device '${def.model}' has no Home Assistant mapping`);
}
@@ -765,11 +589,6 @@ class HomeAssistant extends Extension {
if (!resolvedEntity || !resolvedEntity.definition || !this.mapping[resolvedEntity.definition.model]) return [];
let configs = this.mapping[resolvedEntity.definition.model].slice();
if (!resolvedEntity.definition.hasOwnProperty('exposes')) {
// Exposes already has linkquality.
configs.push(cfg.sensor_linkquality);
}
if (resolvedEntity.definition.hasOwnProperty('ota')) {
if (this.legacyApi) {
configs.push(cfg.binary_sensor_update_available);
-13
View File
@@ -22,19 +22,6 @@ describe('HomeAssistant extension', () => {
settings.set(['homeassistant'], true);
});
it('Should have mapping for all devices supported by zigbee-herdsman-converters', () => {
const missing = [];
const ha = new HomeAssistant(null, null, null, null, {on: () => {}});
require('zigbee-herdsman-converters').definitions.forEach((d) => {
if (!d.hasOwnProperty('exposes') && !ha._getMapping()[d.model]) {
missing.push(d.model);
}
});
expect(missing).toHaveLength(0);
});
it('Should not have duplicate type/object_ids in a mapping', () => {
const duplicated = [];
const ha = new HomeAssistant(null, null, null, null, {on: () => {}});