mirror of
https://github.com/Koenkk/zigbee2mqtt.git
synced 2026-08-28 05:24:29 +00:00
Changes for exposes.
This commit is contained in:
@@ -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
@@ -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);
|
||||
|
||||
@@ -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: () => {}});
|
||||
|
||||
Reference in New Issue
Block a user