2018-11-16 12:23:11 -07:00
|
|
|
const settings = require('../util/settings');
|
|
|
|
const logger = require('../util/logger');
|
2020-07-03 12:20:22 -07:00
|
|
|
const utils = require('../util/utils');
|
2018-12-07 15:17:29 -07:00
|
|
|
const zigbee2mqttVersion = require('../../package.json').version;
|
2020-04-11 09:10:56 -07:00
|
|
|
const Extension = require('./extension');
|
2020-09-24 09:06:43 -07:00
|
|
|
const stringify = require('json-stable-stringify-without-jsonify');
|
2020-10-01 09:33:59 -07:00
|
|
|
const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters');
|
|
|
|
const assert = require('assert');
|
2018-04-25 11:54:41 -07:00
|
|
|
|
2019-09-22 11:02:46 -07:00
|
|
|
const cfg = {
|
2018-04-23 09:29:35 -07:00
|
|
|
// Binary sensor
|
2020-02-16 08:00:15 -07:00
|
|
|
'binary_sensor_update_available': {
|
|
|
|
type: 'binary_sensor',
|
|
|
|
object_id: 'update_available',
|
|
|
|
discovery_payload: {
|
|
|
|
payload_on: true,
|
|
|
|
payload_off: false,
|
|
|
|
value_template: '{{ value_json.update_available}}',
|
|
|
|
},
|
|
|
|
},
|
2018-04-23 09:29:35 -07:00
|
|
|
|
|
|
|
// Sensor
|
2020-08-02 14:09:43 -07:00
|
|
|
'sensor_update_state': {
|
|
|
|
type: 'sensor',
|
2020-08-14 04:48:19 -07:00
|
|
|
object_id: 'update_state',
|
2020-08-02 14:09:43 -07:00
|
|
|
discovery_payload: {
|
|
|
|
icon: 'mdi:update',
|
|
|
|
value_template: `{{ value_json['update']['state'] }}`,
|
|
|
|
},
|
|
|
|
},
|
2018-05-08 11:13:01 -07:00
|
|
|
'sensor_click': {
|
2018-04-23 09:29:35 -07:00
|
|
|
type: 'sensor',
|
2018-05-08 11:13:01 -07:00
|
|
|
object_id: 'click',
|
2018-04-23 09:29:35 -07:00
|
|
|
discovery_payload: {
|
|
|
|
icon: 'mdi:toggle-switch',
|
|
|
|
value_template: '{{ value_json.click }}',
|
2018-05-17 08:20:46 -07:00
|
|
|
},
|
2018-04-23 09:29:35 -07:00
|
|
|
},
|
2018-06-08 11:34:34 -07:00
|
|
|
'sensor_brightness': {
|
|
|
|
type: 'sensor',
|
|
|
|
object_id: 'brightness',
|
|
|
|
discovery_payload: {
|
|
|
|
unit_of_measurement: 'brightness',
|
|
|
|
icon: 'mdi:brightness-5',
|
|
|
|
value_template: '{{ value_json.brightness }}',
|
|
|
|
},
|
|
|
|
},
|
2018-11-19 12:43:23 -07:00
|
|
|
'sensor_linkquality': {
|
|
|
|
type: 'sensor',
|
|
|
|
object_id: 'linkquality',
|
|
|
|
discovery_payload: {
|
2020-01-25 12:43:51 -07:00
|
|
|
icon: 'mdi:signal',
|
|
|
|
unit_of_measurement: 'lqi',
|
2018-11-19 12:43:23 -07:00
|
|
|
value_template: '{{ value_json.linkquality }}',
|
|
|
|
},
|
|
|
|
},
|
2018-04-23 13:36:30 -07:00
|
|
|
|
2020-02-29 10:07:15 -07:00
|
|
|
// Trigger
|
|
|
|
'trigger_action': {
|
|
|
|
type: 'device_automation',
|
|
|
|
discovery_payload: {
|
|
|
|
automation_type: 'trigger',
|
|
|
|
type: 'action',
|
|
|
|
},
|
|
|
|
},
|
|
|
|
'trigger_click': {
|
|
|
|
type: 'device_automation',
|
|
|
|
discovery_payload: {
|
|
|
|
automation_type: 'trigger',
|
|
|
|
type: 'click',
|
|
|
|
},
|
|
|
|
},
|
2018-08-04 11:05:34 -07:00
|
|
|
};
|
|
|
|
|
2020-07-16 14:16:59 -07:00
|
|
|
const defaultStatusTopic = 'homeassistant/status';
|
|
|
|
|
2018-11-16 12:23:11 -07:00
|
|
|
/**
|
|
|
|
* This extensions handles integration with HomeAssistant
|
|
|
|
*/
|
2020-04-11 09:10:56 -07:00
|
|
|
class HomeAssistant extends Extension {
|
2020-01-09 13:47:19 -07:00
|
|
|
constructor(zigbee, mqtt, state, publishEntityState, eventBus) {
|
|
|
|
super(zigbee, mqtt, state, publishEntityState, eventBus);
|
2018-04-25 11:54:41 -07:00
|
|
|
|
2018-11-16 12:23:11 -07:00
|
|
|
// A map of all discoverd devices
|
|
|
|
this.discovered = {};
|
2020-10-01 09:33:59 -07:00
|
|
|
this.mapping = {};
|
2020-02-29 10:07:15 -07:00
|
|
|
this.discoveredTriggers = {};
|
2020-08-02 14:09:43 -07:00
|
|
|
this.legacyApi = settings.get().advanced.legacy_api;
|
2018-04-25 11:54:41 -07:00
|
|
|
|
2018-11-16 12:23:11 -07:00
|
|
|
if (!settings.get().advanced.cache_state) {
|
|
|
|
logger.warn('In order for HomeAssistant integration to work properly set `cache_state: true');
|
|
|
|
}
|
2019-03-04 10:13:36 -07:00
|
|
|
|
|
|
|
if (settings.get().experimental.output === 'attribute') {
|
|
|
|
throw new Error('Home Assitant integration is not possible with attribute output!');
|
|
|
|
}
|
2019-03-15 14:41:39 -07:00
|
|
|
|
|
|
|
this.discoveryTopic = settings.get().advanced.homeassistant_discovery_topic;
|
2019-06-24 11:52:47 -07:00
|
|
|
this.statusTopic = settings.get().advanced.homeassistant_status_topic;
|
2020-01-09 13:47:19 -07:00
|
|
|
|
2020-09-25 07:50:12 -07:00
|
|
|
this.eventBus.on('deviceRemoved', (data) => this.onDeviceRemoved(data.resolvedEntity), this.constructor.name);
|
2020-07-29 14:10:03 -07:00
|
|
|
this.eventBus.on('publishEntityState', (data) => this.onPublishEntityState(data), this.constructor.name);
|
2020-09-04 12:08:20 -07:00
|
|
|
this.eventBus.on('deviceRenamed', (data) =>
|
|
|
|
this.onDeviceRenamed(data.device, data.homeAssisantRename), this.constructor.name,
|
|
|
|
);
|
2020-07-03 12:20:22 -07:00
|
|
|
|
2020-10-01 09:33:59 -07:00
|
|
|
this.populateMapping();
|
|
|
|
}
|
|
|
|
|
|
|
|
populateMapping() {
|
|
|
|
for (const def of zigbeeHerdsmanConverters.definitions) {
|
|
|
|
if (def.hasOwnProperty('exposes')) {
|
|
|
|
this.mapping[def.model] = [];
|
|
|
|
|
2020-10-30 15:39:26 -07:00
|
|
|
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', 'QBKG04LM', 'QBKG03LM',
|
|
|
|
'QBKG11LM', 'QBKG21LM', 'QBKG22LM', 'WXKG12LM', 'QBKG12LM', 'E1743'].includes(def.model)) {
|
|
|
|
// deprecated
|
|
|
|
this.mapping[def.model].push(cfg.sensor_click);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (['ICTC-G-1'].includes(def.model)) {
|
|
|
|
// deprecated
|
|
|
|
this.mapping[def.model].push(cfg.sensor_brightness);
|
|
|
|
}
|
|
|
|
|
2020-10-01 09:33:59 -07:00
|
|
|
for (const expose of def.exposes) {
|
|
|
|
let discoveryEntry = null;
|
|
|
|
/* istanbul ignore else */
|
|
|
|
if (expose.type === 'light') {
|
|
|
|
discoveryEntry = {
|
|
|
|
type: 'light',
|
|
|
|
object_id: expose.endpoint ? `light_${expose.endpoint}` : 'light',
|
|
|
|
discovery_payload: {
|
2020-10-18 12:51:32 -07:00
|
|
|
brightness: !!expose.features.find((e) => e.name === 'brightness'),
|
|
|
|
color_temp: !!expose.features.find((e) => e.name === 'color_temp'),
|
2020-10-24 05:35:21 -07:00
|
|
|
xy: !!expose.features.find((e) => e.name === 'color_xy'),
|
|
|
|
hs: !!expose.features.find((e) => e.name === 'color_hs'),
|
2020-10-01 09:33:59 -07:00
|
|
|
schema: 'json',
|
|
|
|
command_topic: true,
|
|
|
|
brightness_scale: 254,
|
|
|
|
command_topic_prefix: expose.endpoint ? expose.endpoint : undefined,
|
|
|
|
state_topic_postfix: expose.endpoint ? expose.endpoint : undefined,
|
|
|
|
},
|
|
|
|
};
|
2020-10-19 07:48:09 -07:00
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
2020-10-02 08:26:08 -07:00
|
|
|
} else if (expose.type === 'switch') {
|
2020-11-04 14:33:00 -07:00
|
|
|
const state = expose.features.find((f) => f.name === 'state');
|
2020-10-02 08:26:08 -07:00
|
|
|
discoveryEntry = {
|
|
|
|
type: 'switch',
|
|
|
|
object_id: expose.endpoint ? `switch_${expose.endpoint}` : 'switch',
|
|
|
|
discovery_payload: {
|
|
|
|
payload_off: 'OFF',
|
|
|
|
payload_on: 'ON',
|
2020-11-04 14:33:00 -07:00
|
|
|
value_template: `{{ value_json.${state.property} }}`,
|
2020-10-02 08:26:08 -07:00
|
|
|
command_topic: true,
|
|
|
|
command_topic_prefix: expose.endpoint ? expose.endpoint : undefined,
|
|
|
|
},
|
|
|
|
};
|
2020-11-04 14:33:00 -07:00
|
|
|
|
|
|
|
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';
|
|
|
|
}
|
|
|
|
}
|
2020-11-02 10:00:58 -07:00
|
|
|
} 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} }}`,
|
|
|
|
},
|
|
|
|
};
|
2020-11-04 14:33:00 -07:00
|
|
|
|
2020-11-26 12:12:09 -07:00
|
|
|
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}] }}`;
|
|
|
|
}
|
|
|
|
|
2020-11-04 14:33:00 -07:00
|
|
|
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;
|
|
|
|
}
|
2020-10-19 10:06:49 -07:00
|
|
|
} else if (expose.type === 'lock') {
|
|
|
|
assert(!expose.endpoint, `Endpoint not supported for lock type`);
|
2020-11-02 10:00:58 -07:00
|
|
|
const state = expose.features.find((f) => f.name === 'state');
|
|
|
|
assert(state, 'No state found');
|
2020-10-19 10:06:49 -07:00
|
|
|
discoveryEntry = {
|
|
|
|
type: 'lock',
|
|
|
|
object_id: 'lock',
|
|
|
|
discovery_payload: {
|
|
|
|
command_topic: true,
|
2020-11-02 10:00:58 -07:00
|
|
|
value_template: `{{ value_json.${state.property} }}`,
|
2020-10-19 10:06:49 -07:00
|
|
|
},
|
|
|
|
};
|
2020-11-02 10:00:58 -07:00
|
|
|
|
|
|
|
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';
|
2020-11-04 14:33:00 -07:00
|
|
|
} 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';
|
2020-11-02 10:00:58 -07:00
|
|
|
} 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;
|
|
|
|
}
|
2020-10-12 08:22:53 -07:00
|
|
|
} else if (expose.type === 'cover') {
|
|
|
|
assert(!expose.endpoint, `Endpoint not supported for cover type`);
|
2020-10-18 12:51:32 -07:00
|
|
|
const hasPosition = expose.features.find((e) => e.name === 'position');
|
|
|
|
const hasTilt = expose.features.find((e) => e.name === 'tilt');
|
2020-10-12 08:22:53 -07:00
|
|
|
|
|
|
|
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 }}',
|
|
|
|
};
|
|
|
|
}
|
2020-10-30 15:39:26 -07:00
|
|
|
} 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;
|
|
|
|
}
|
2020-10-18 12:51:32 -07:00
|
|
|
} else if (expose.type === 'binary') {
|
2020-10-07 08:37:03 -07:00
|
|
|
const lookup = {
|
2020-10-18 12:51:32 -07:00
|
|
|
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'},
|
2020-11-07 11:16:16 -07:00
|
|
|
presence: {device_class: 'presence'},
|
2020-10-07 08:37:03 -07:00
|
|
|
};
|
|
|
|
|
2020-10-18 12:51:32 -07:00
|
|
|
assert(!expose.endpoint, `Endpoint not supported for binary type`);
|
2020-10-07 08:37:03 -07:00
|
|
|
|
|
|
|
discoveryEntry = {
|
|
|
|
type: 'binary_sensor',
|
2020-10-18 12:51:32 -07:00
|
|
|
object_id: expose.name,
|
2020-10-07 08:37:03 -07:00
|
|
|
discovery_payload: {
|
2020-10-24 05:35:21 -07:00
|
|
|
value_template: `{{ value_json.${expose.property} }}`,
|
2020-10-18 12:51:32 -07:00
|
|
|
payload_on: expose.value_on,
|
|
|
|
payload_off: expose.value_off,
|
2020-10-26 09:25:41 -07:00
|
|
|
...(lookup[expose.name] || {}),
|
2020-10-07 08:37:03 -07:00
|
|
|
},
|
|
|
|
};
|
|
|
|
} 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'},
|
2020-11-11 10:22:37 -07:00
|
|
|
position: {icon: 'mdi:valve'},
|
2020-10-07 08:37:03 -07:00
|
|
|
pressure: {device_class: 'pressure'},
|
2020-10-08 13:08:18 -07:00
|
|
|
power: {icon: 'mdi:flash'},
|
2020-10-09 12:57:54 -07:00
|
|
|
linkquality: {icon: 'mdi:signal'},
|
2020-10-09 16:25:53 -07:00
|
|
|
current: {icon: 'mdi:current-ac'},
|
|
|
|
voltage: {icon: 'mdi:alpha-v'},
|
|
|
|
current_phase_b: {icon: 'mdi:current-ac'},
|
|
|
|
voltage_phase_b: {icon: 'mdi:alpha-v'},
|
|
|
|
current_phase_c: {icon: 'mdi:current-ac'},
|
|
|
|
voltage_phase_c: {icon: 'mdi:alpha-v'},
|
|
|
|
energy: {icon: 'mdi:power-plug'},
|
2020-10-18 12:51:32 -07:00
|
|
|
smoke_density: {icon: 'mdi:google-circles-communities'},
|
|
|
|
gas_density: {icon: 'mdi:google-circles-communities'},
|
2020-10-19 10:06:49 -07:00
|
|
|
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'},
|
2020-10-29 13:43:47 -07:00
|
|
|
requested_brightness_level: {icon: 'mdi:brightness-5'},
|
|
|
|
requested_brightness_percent: {icon: 'mdi:brightness-5'},
|
2020-10-30 15:39:26 -07:00
|
|
|
eco2: {icon: 'mdi:molecule-co2'},
|
|
|
|
co2: {icon: 'mdi:molecule-co2'},
|
2020-11-02 10:00:58 -07:00
|
|
|
local_temperature: {device_class: 'temperature'},
|
2020-10-07 08:37:03 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
assert(!expose.endpoint, `Endpoint not supported for numeric type`);
|
|
|
|
|
|
|
|
discoveryEntry = {
|
|
|
|
type: 'sensor',
|
2020-10-18 12:51:32 -07:00
|
|
|
object_id: expose.name,
|
2020-10-07 08:37:03 -07:00
|
|
|
discovery_payload: {
|
|
|
|
unit_of_measurement: expose.unit ? expose.unit : '-',
|
2020-10-24 05:35:21 -07:00
|
|
|
value_template: `{{ value_json.${expose.property} }}`,
|
2020-10-18 12:51:32 -07:00
|
|
|
...lookup[expose.name],
|
|
|
|
},
|
|
|
|
};
|
2020-10-30 15:39:26 -07:00
|
|
|
} else if (expose.type === 'enum' || expose.type === 'text') {
|
2020-11-08 06:45:56 -07:00
|
|
|
const ACCESS_STATE = 1;
|
|
|
|
if (expose.access & ACCESS_STATE) {
|
2020-10-30 15:39:26 -07:00
|
|
|
const lookup = {
|
|
|
|
action: {icon: 'mdi:gesture-double-tap'},
|
|
|
|
};
|
2020-10-18 12:51:32 -07:00
|
|
|
|
2020-10-19 07:48:09 -07:00
|
|
|
discoveryEntry = {
|
|
|
|
type: 'sensor',
|
2020-11-01 03:02:35 -07:00
|
|
|
object_id: expose.property,
|
2020-10-19 07:48:09 -07:00
|
|
|
discovery_payload: {
|
2020-10-24 05:35:21 -07:00
|
|
|
value_template: `{{ value_json.${expose.property} }}`,
|
2020-10-30 15:39:26 -07:00
|
|
|
...lookup[expose.name],
|
2020-10-19 07:48:09 -07:00
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
2020-10-07 08:37:03 -07:00
|
|
|
} else {
|
|
|
|
throw new Error(`Unsupported exposes type: '${expose.type}'`);
|
2020-10-01 09:33:59 -07:00
|
|
|
}
|
|
|
|
|
2020-10-19 07:48:09 -07:00
|
|
|
if (discoveryEntry) {
|
|
|
|
this.mapping[def.model].push(discoveryEntry);
|
|
|
|
}
|
2020-10-01 09:33:59 -07:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
logger.warn(`Supported device '${def.model}' has no Home Assistant mapping`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-03 12:20:22 -07:00
|
|
|
for (const definition of utils.getExternalConvertersDefinitions(settings)) {
|
|
|
|
if (definition.hasOwnProperty('homeassistant')) {
|
2020-10-01 09:33:59 -07:00
|
|
|
this.mapping[definition.model] = definition.homeassistant;
|
2020-07-03 12:20:22 -07:00
|
|
|
}
|
|
|
|
}
|
2020-01-09 13:47:19 -07:00
|
|
|
}
|
|
|
|
|
2020-09-25 07:50:12 -07:00
|
|
|
onDeviceRemoved(resolvedEntity) {
|
|
|
|
logger.debug(`Clearing Home Assistant discovery topic for '${resolvedEntity.name}'`);
|
|
|
|
delete this.discovered[resolvedEntity.device.ieeeAddr];
|
2020-08-17 11:57:12 -07:00
|
|
|
for (const config of this.getConfigs(resolvedEntity)) {
|
2020-09-25 07:50:12 -07:00
|
|
|
const topic = this.getDiscoveryTopic(config, resolvedEntity.device);
|
2020-11-16 09:03:22 -07:00
|
|
|
this.mqtt.publish(topic, null, {retain: true, qos: 0}, this.discoveryTopic, false, false);
|
2020-01-09 13:47:19 -07:00
|
|
|
}
|
2018-11-16 12:23:11 -07:00
|
|
|
}
|
2018-04-25 11:54:41 -07:00
|
|
|
|
2020-02-29 10:07:15 -07:00
|
|
|
async onPublishEntityState(data) {
|
2020-03-15 01:38:39 -07:00
|
|
|
/**
|
2020-08-01 01:36:20 -07:00
|
|
|
* In case we deal with a lightEndpoint configuration Zigbee2MQTT publishes
|
2020-03-15 01:38:39 -07:00
|
|
|
* e.g. {state_l1: ON, brightness_l1: 250} to zigbee2mqtt/mydevice.
|
|
|
|
* As the Home Assistant MQTT JSON light cannot be configured to use state_l1/brightness_l1
|
|
|
|
* as the state variables, the state topic is set to zigbee2mqtt/mydevice/l1.
|
|
|
|
* Here we retrieve all the attributes with the _l1 values and republish them on
|
|
|
|
* zigbee2mqtt/mydevice/l1.
|
|
|
|
*/
|
2020-10-01 09:33:59 -07:00
|
|
|
if (data.entity.definition && this.mapping[data.entity.definition.model]) {
|
|
|
|
for (const config of this.mapping[data.entity.definition.model]) {
|
2020-03-15 01:38:39 -07:00
|
|
|
const match = /light_(.*)/.exec(config['object_id']);
|
|
|
|
if (match) {
|
2020-04-11 11:34:50 -07:00
|
|
|
const endpoint = match[1];
|
|
|
|
const endpointRegExp = new RegExp(`(.*)_${endpoint}`);
|
2020-03-15 01:38:39 -07:00
|
|
|
const payload = {};
|
|
|
|
for (const key of Object.keys(data.payload)) {
|
2020-04-11 11:34:50 -07:00
|
|
|
const keyMatch = endpointRegExp.exec(key);
|
2020-03-15 01:38:39 -07:00
|
|
|
if (keyMatch) {
|
|
|
|
payload[keyMatch[1]] = data.payload[key];
|
|
|
|
}
|
|
|
|
}
|
2020-02-29 10:07:15 -07:00
|
|
|
|
2020-03-15 01:38:39 -07:00
|
|
|
await this.mqtt.publish(
|
2020-08-13 11:00:35 -07:00
|
|
|
`${data.entity.name}/${endpoint}`, stringify(payload), {},
|
2020-03-15 01:38:39 -07:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-15 13:34:59 -07:00
|
|
|
/**
|
|
|
|
* Publish an empty value for click and action payload, in this way Home Assistant
|
|
|
|
* can use Home Assistant entities in automations.
|
|
|
|
* https://github.com/Koenkk/zigbee2mqtt/issues/959#issuecomment-480341347
|
|
|
|
*/
|
|
|
|
if (settings.get().advanced.homeassistant_legacy_triggers) {
|
2020-08-23 13:58:07 -07:00
|
|
|
const keys = ['action', 'click'].filter((k) => data.payload.hasOwnProperty(k) && data.payload[k] !== '');
|
|
|
|
for (const key of keys) {
|
2020-04-15 13:34:59 -07:00
|
|
|
this.publishEntityState(data.entity.device.ieeeAddr, {[key]: ''});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-15 01:38:39 -07:00
|
|
|
/**
|
|
|
|
* Implements the MQTT device trigger (https://www.home-assistant.io/integrations/device_trigger.mqtt/)
|
|
|
|
* The MQTT device trigger does not support JSON parsing, so it cannot listen to zigbee2mqtt/my_device
|
|
|
|
* Whenever a device publish an {action: *} we discover an MQTT device trigger sensor
|
|
|
|
* and republish it to zigbee2mqtt/my_devic/action
|
|
|
|
*/
|
2020-08-23 14:17:15 -07:00
|
|
|
if (data.entity.definition) {
|
|
|
|
const keys = ['action', 'click'].filter((k) => data.payload[k] && data.payload[k] !== '');
|
|
|
|
for (const key of keys) {
|
2020-08-23 13:58:07 -07:00
|
|
|
const value = data.payload[key].toString();
|
2020-09-25 10:48:30 -07:00
|
|
|
await this.publishDeviceTriggerDiscover(data.entity, key, value);
|
2020-08-23 13:58:07 -07:00
|
|
|
await this.mqtt.publish(`${data.entity.name}/${key}`, value, {});
|
|
|
|
}
|
2020-02-29 10:07:15 -07:00
|
|
|
}
|
2020-03-20 09:50:28 -07:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Publish a value for update_available (if not there yet) to prevent Home Assistant generating warnings of
|
|
|
|
* this value not being available.
|
|
|
|
*/
|
2020-08-02 14:09:43 -07:00
|
|
|
const supportsOTA = data.entity.definition && data.entity.definition.hasOwnProperty('ota');
|
2020-03-26 14:01:05 -07:00
|
|
|
const mockedValues = [
|
|
|
|
{
|
|
|
|
property: 'update_available',
|
2020-08-02 14:09:43 -07:00
|
|
|
condition: supportsOTA && this.legacyApi,
|
2020-03-26 14:01:05 -07:00
|
|
|
value: false,
|
|
|
|
},
|
2020-08-02 14:09:43 -07:00
|
|
|
{
|
|
|
|
property: 'update',
|
2020-12-02 12:38:48 -07:00
|
|
|
condition: supportsOTA,
|
2020-08-02 14:09:43 -07:00
|
|
|
value: {state: 'idle'},
|
|
|
|
},
|
2020-03-26 14:01:05 -07:00
|
|
|
{
|
|
|
|
property: 'water_leak',
|
2020-10-01 09:33:59 -07:00
|
|
|
condition: data.entity.device && data.entity.definition && this.mapping[data.entity.definition.model] &&
|
2020-10-30 15:39:26 -07:00
|
|
|
this.mapping[data.entity.definition.model].filter((c) => c.object_id === 'water_leak').length === 1,
|
2020-03-26 14:01:05 -07:00
|
|
|
value: false,
|
|
|
|
},
|
|
|
|
];
|
|
|
|
|
|
|
|
for (const entry of mockedValues) {
|
|
|
|
if (entry.condition && !data.payload.hasOwnProperty(entry.property)) {
|
|
|
|
logger.debug(`Mocking '${entry.property}' value for Home Assistant`);
|
|
|
|
this.publishEntityState(data.entity.device.ieeeAddr, {[entry.property]: entry.value});
|
|
|
|
}
|
2020-03-20 09:50:28 -07:00
|
|
|
}
|
2020-02-29 10:07:15 -07:00
|
|
|
}
|
|
|
|
|
2020-09-04 12:08:20 -07:00
|
|
|
onDeviceRenamed(device, homeAssisantRename) {
|
2020-08-17 11:57:12 -07:00
|
|
|
logger.debug(`Refreshing Home Assistant discovery topic for '${device.ieeeAddr}'`);
|
2020-09-25 08:44:24 -07:00
|
|
|
const resolvedEntity = this.zigbee.resolveEntity(device);
|
2020-08-17 11:19:44 -07:00
|
|
|
|
2020-08-17 13:12:41 -07:00
|
|
|
// Clear before rename so Home Assistant uses new friendly_name
|
2020-08-17 11:19:44 -07:00
|
|
|
// https://github.com/Koenkk/zigbee2mqtt/issues/4096#issuecomment-674044916
|
2020-09-25 08:32:40 -07:00
|
|
|
if (homeAssisantRename) {
|
|
|
|
for (const config of this.getConfigs(resolvedEntity)) {
|
|
|
|
const topic = this.getDiscoveryTopic(config, device);
|
2020-11-16 09:03:22 -07:00
|
|
|
this.mqtt.publish(topic, null, {retain: true, qos: 0}, this.discoveryTopic, false, false);
|
2020-09-25 08:32:40 -07:00
|
|
|
}
|
2020-08-17 11:19:44 -07:00
|
|
|
}
|
|
|
|
|
2020-04-11 11:34:50 -07:00
|
|
|
this.discover(resolvedEntity, true);
|
2020-09-25 10:48:30 -07:00
|
|
|
|
|
|
|
if (this.discoveredTriggers[device.ieeeAddr]) {
|
|
|
|
for (const config of this.discoveredTriggers[device.ieeeAddr]) {
|
|
|
|
const key = config.substring(0, config.indexOf('_'));
|
|
|
|
const value = config.substring(config.indexOf('_') + 1);
|
|
|
|
this.publishDeviceTriggerDiscover(resolvedEntity, key, value, true);
|
|
|
|
}
|
|
|
|
}
|
2020-03-04 04:55:08 -07:00
|
|
|
}
|
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
async onMQTTConnected() {
|
2019-06-24 11:52:47 -07:00
|
|
|
this.mqtt.subscribe(this.statusTopic);
|
2020-07-16 14:16:59 -07:00
|
|
|
this.mqtt.subscribe(defaultStatusTopic);
|
2020-08-17 08:24:57 -07:00
|
|
|
this.mqtt.subscribe(`${this.discoveryTopic}/#`);
|
2018-04-25 11:54:41 -07:00
|
|
|
|
2018-11-16 12:23:11 -07:00
|
|
|
// MQTT discovery of all paired devices on startup.
|
2019-09-23 13:21:27 -07:00
|
|
|
for (const device of this.zigbee.getClients()) {
|
2020-04-11 11:34:50 -07:00
|
|
|
const resolvedEntity = this.zigbee.resolveEntity(device);
|
|
|
|
this.discover(resolvedEntity, true);
|
2019-09-09 10:48:09 -07:00
|
|
|
}
|
2018-11-16 12:23:11 -07:00
|
|
|
}
|
2018-06-24 05:21:56 -07:00
|
|
|
|
2020-08-17 11:57:12 -07:00
|
|
|
getConfigs(resolvedEntity) {
|
2020-10-01 09:33:59 -07:00
|
|
|
if (!resolvedEntity || !resolvedEntity.definition || !this.mapping[resolvedEntity.definition.model]) return [];
|
2020-08-17 11:57:12 -07:00
|
|
|
|
2020-10-01 09:33:59 -07:00
|
|
|
let configs = this.mapping[resolvedEntity.definition.model].slice();
|
2020-08-17 11:57:12 -07:00
|
|
|
if (resolvedEntity.definition.hasOwnProperty('ota')) {
|
2020-12-02 12:38:48 -07:00
|
|
|
configs.push(cfg.sensor_update_state);
|
2020-08-02 14:09:43 -07:00
|
|
|
if (this.legacyApi) {
|
|
|
|
configs.push(cfg.binary_sensor_update_available);
|
|
|
|
}
|
2020-02-21 16:32:15 -07:00
|
|
|
}
|
2020-03-01 07:55:20 -07:00
|
|
|
|
2020-08-17 11:57:12 -07:00
|
|
|
if (resolvedEntity.settings.hasOwnProperty('legacy') && !resolvedEntity.settings.legacy) {
|
2020-07-24 13:53:10 -07:00
|
|
|
configs = configs.filter((c) => c !== cfg.sensor_click);
|
|
|
|
}
|
|
|
|
|
2020-03-01 07:55:20 -07:00
|
|
|
if (!settings.get().advanced.homeassistant_legacy_triggers) {
|
2020-10-30 15:39:26 -07:00
|
|
|
configs = configs.filter((c) => c.object_id !== 'action' && c.object_id !== 'click');
|
2020-03-01 07:55:20 -07:00
|
|
|
}
|
|
|
|
|
2020-10-13 11:33:48 -07:00
|
|
|
// deep clone of the config objects
|
|
|
|
configs = JSON.parse(JSON.stringify(configs));
|
|
|
|
|
2020-12-08 08:45:31 -07:00
|
|
|
if (resolvedEntity.settings.homeassistant) {
|
2020-10-13 11:33:48 -07:00
|
|
|
configs.forEach((config) => {
|
|
|
|
const configOverride = resolvedEntity.settings.homeassistant[config.object_id];
|
|
|
|
if (configOverride) {
|
|
|
|
config.object_id = configOverride.object_id || config.object_id;
|
|
|
|
config.type = configOverride.type || config.type;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-02-21 16:32:15 -07:00
|
|
|
return configs;
|
|
|
|
}
|
|
|
|
|
2020-04-11 11:34:50 -07:00
|
|
|
discover(resolvedEntity, force=false) {
|
2018-11-16 12:23:11 -07:00
|
|
|
// Check if already discoverd and check if there are configs.
|
2020-04-11 11:34:50 -07:00
|
|
|
const {device, definition} = resolvedEntity;
|
2020-01-09 13:47:19 -07:00
|
|
|
const discover = force || !this.discovered[device.ieeeAddr];
|
2020-10-01 09:33:59 -07:00
|
|
|
if (!discover || !device || !definition || !this.mapping[definition.model] || device.interviewing ||
|
2020-08-17 11:57:12 -07:00
|
|
|
(resolvedEntity.settings.hasOwnProperty('homeassistant') && !resolvedEntity.settings.homeassistant)) {
|
2019-02-19 11:43:50 -07:00
|
|
|
return;
|
|
|
|
}
|
2018-11-16 12:23:11 -07:00
|
|
|
|
2020-08-17 11:57:12 -07:00
|
|
|
const friendlyName = resolvedEntity.settings.friendlyName;
|
|
|
|
this.getConfigs(resolvedEntity).forEach((config) => {
|
2018-11-16 12:23:11 -07:00
|
|
|
const payload = {...config.discovery_payload};
|
2020-08-17 11:57:12 -07:00
|
|
|
let stateTopic = `${settings.get().mqtt.base_topic}/${friendlyName}`;
|
2020-03-15 01:38:39 -07:00
|
|
|
if (payload.state_topic_postfix) {
|
|
|
|
stateTopic += `/${payload.state_topic_postfix}`;
|
|
|
|
delete payload.state_topic_postfix;
|
|
|
|
}
|
2019-02-23 08:02:45 -07:00
|
|
|
|
|
|
|
if (!payload.hasOwnProperty('state_topic') || payload.state_topic) {
|
|
|
|
payload.state_topic = stateTopic;
|
2019-09-09 10:48:09 -07:00
|
|
|
} else {
|
|
|
|
/* istanbul ignore else */
|
|
|
|
if (payload.hasOwnProperty('state_topic')) {
|
|
|
|
delete payload.state_topic;
|
|
|
|
}
|
2019-02-23 08:02:45 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
if (payload.position_topic) {
|
|
|
|
payload.position_topic = stateTopic;
|
|
|
|
}
|
2018-11-16 12:23:11 -07:00
|
|
|
|
2019-08-08 11:26:50 -07:00
|
|
|
if (payload.tilt_status_topic) {
|
|
|
|
payload.tilt_status_topic = stateTopic;
|
|
|
|
}
|
|
|
|
|
2019-11-19 12:50:55 -07:00
|
|
|
payload.json_attributes_topic = stateTopic;
|
2019-01-14 10:04:33 -07:00
|
|
|
|
2020-08-08 04:54:39 -07:00
|
|
|
// Set (unique) name, separate by space if friendlyName contains space.
|
|
|
|
const nameSeparator = friendlyName.includes(' ') ? ' ' : '_';
|
2020-11-23 11:01:16 -07:00
|
|
|
payload.name = friendlyName;
|
|
|
|
if (config.object_id.startsWith(config.type) && config.object_id.includes('_')) {
|
|
|
|
payload.name += `${nameSeparator}${config.object_id.split(/_(.+)/)[1]}`;
|
|
|
|
} else if (!config.object_id.startsWith(config.type)) {
|
|
|
|
payload.name += `${nameSeparator}${config.object_id}`;
|
|
|
|
}
|
2018-11-16 12:23:11 -07:00
|
|
|
|
2018-12-07 15:09:15 -07:00
|
|
|
// Set unique_id
|
2020-08-17 11:57:12 -07:00
|
|
|
payload.unique_id = `${resolvedEntity.settings.ID}_${config.object_id}_${settings.get().mqtt.base_topic}`;
|
2018-11-16 12:23:11 -07:00
|
|
|
|
2018-12-07 15:17:29 -07:00
|
|
|
// Attributes for device registry
|
2020-04-11 11:34:50 -07:00
|
|
|
payload.device = this.getDevicePayload(resolvedEntity);
|
2018-12-07 15:17:29 -07:00
|
|
|
|
2020-10-27 13:49:44 -07:00
|
|
|
// Availability payload
|
2020-11-13 11:45:27 -07:00
|
|
|
payload.availability = [{topic: `${settings.get().mqtt.base_topic}/bridge/state`}];
|
|
|
|
if (settings.get().advanced.availability_timeout) {
|
|
|
|
payload.availability.push({topic: `${settings.get().mqtt.base_topic}/${friendlyName}/availability`});
|
|
|
|
}
|
2018-12-29 11:55:59 -07:00
|
|
|
|
2018-11-16 12:23:11 -07:00
|
|
|
if (payload.command_topic) {
|
2020-04-11 11:34:50 -07:00
|
|
|
payload.command_topic = `${settings.get().mqtt.base_topic}/${friendlyName}/`;
|
2018-04-29 05:09:49 -07:00
|
|
|
|
2018-11-16 12:23:11 -07:00
|
|
|
if (payload.command_topic_prefix) {
|
|
|
|
payload.command_topic += `${payload.command_topic_prefix}/`;
|
2019-02-07 11:33:00 -07:00
|
|
|
delete payload.command_topic_prefix;
|
2018-11-16 12:23:11 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
payload.command_topic += 'set';
|
2019-04-24 11:59:06 -07:00
|
|
|
|
|
|
|
if (payload.command_topic_postfix) {
|
|
|
|
payload.command_topic += `/${payload.command_topic_postfix}`;
|
|
|
|
delete payload.command_topic_postfix;
|
|
|
|
}
|
2018-04-29 05:09:49 -07:00
|
|
|
}
|
|
|
|
|
2019-02-23 08:02:45 -07:00
|
|
|
if (payload.set_position_topic && payload.command_topic) {
|
|
|
|
payload.set_position_topic = payload.command_topic;
|
|
|
|
}
|
|
|
|
|
2019-08-08 11:26:50 -07:00
|
|
|
if (payload.tilt_command_topic && payload.command_topic) {
|
|
|
|
// Home Assistant does not support templates to set tilt (as of 2019-08-17),
|
|
|
|
// so we (have to) use a subtopic.
|
|
|
|
payload.tilt_command_topic = payload.command_topic + '/tilt';
|
|
|
|
}
|
|
|
|
|
2019-04-16 07:31:05 -07:00
|
|
|
if (payload.mode_state_topic) {
|
|
|
|
payload.mode_state_topic = stateTopic;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (payload.mode_command_topic) {
|
|
|
|
payload.mode_command_topic = `${stateTopic}/set/system_mode`;
|
|
|
|
}
|
|
|
|
|
2020-08-23 13:36:30 -07:00
|
|
|
if (payload.hold_command_topic) {
|
|
|
|
payload.hold_command_topic = `${stateTopic}/set/preset`;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (payload.hold_state_topic) {
|
|
|
|
payload.hold_state_topic = stateTopic;
|
|
|
|
}
|
|
|
|
|
2020-10-06 12:37:15 -07:00
|
|
|
if (payload.away_mode_state_topic) {
|
|
|
|
payload.away_mode_state_topic = stateTopic;
|
|
|
|
}
|
2020-09-30 10:52:11 -07:00
|
|
|
|
2020-10-06 12:37:15 -07:00
|
|
|
if (payload.away_mode_command_topic) {
|
|
|
|
payload.away_mode_command_topic = `${stateTopic}/set/away_mode`;
|
|
|
|
}
|
2020-09-30 10:52:11 -07:00
|
|
|
|
2019-04-16 07:31:05 -07:00
|
|
|
if (payload.current_temperature_topic) {
|
|
|
|
payload.current_temperature_topic = stateTopic;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (payload.temperature_state_topic) {
|
|
|
|
payload.temperature_state_topic = stateTopic;
|
|
|
|
}
|
|
|
|
|
2020-03-29 12:22:48 -07:00
|
|
|
if (payload.temperature_low_state_topic) {
|
|
|
|
payload.temperature_low_state_topic = stateTopic;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (payload.temperature_high_state_topic) {
|
|
|
|
payload.temperature_high_state_topic = stateTopic;
|
|
|
|
}
|
|
|
|
|
2019-04-24 11:59:06 -07:00
|
|
|
if (payload.speed_state_topic) {
|
|
|
|
payload.speed_state_topic = stateTopic;
|
|
|
|
}
|
|
|
|
|
2019-04-16 07:31:05 -07:00
|
|
|
if (payload.temperature_command_topic) {
|
2019-10-17 09:31:42 -07:00
|
|
|
payload.temperature_command_topic = `${stateTopic}/set/${payload.temperature_command_topic}`;
|
2019-04-16 07:31:05 -07:00
|
|
|
}
|
|
|
|
|
2020-03-29 12:22:48 -07:00
|
|
|
if (payload.temperature_low_command_topic) {
|
|
|
|
payload.temperature_low_command_topic = `${stateTopic}/set/${payload.temperature_low_command_topic}`;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (payload.temperature_high_command_topic) {
|
|
|
|
payload.temperature_high_command_topic = `${stateTopic}/set/${payload.temperature_high_command_topic}`;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (payload.fan_mode_state_topic) {
|
|
|
|
payload.fan_mode_state_topic = stateTopic;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (payload.fan_mode_command_topic) {
|
|
|
|
payload.fan_mode_command_topic = `${stateTopic}/set/fan_mode`;
|
|
|
|
}
|
|
|
|
|
2019-04-24 11:59:06 -07:00
|
|
|
if (payload.speed_command_topic) {
|
|
|
|
payload.speed_command_topic = `${stateTopic}/set/fan_mode`;
|
|
|
|
}
|
|
|
|
|
2020-01-06 13:36:45 -07:00
|
|
|
if (payload.action_topic) {
|
|
|
|
payload.action_topic = stateTopic;
|
|
|
|
}
|
|
|
|
|
2019-02-09 11:42:31 -07:00
|
|
|
// Override configuration with user settings.
|
2020-08-17 11:57:12 -07:00
|
|
|
if (resolvedEntity.settings.hasOwnProperty('homeassistant')) {
|
2019-02-09 11:42:31 -07:00
|
|
|
const add = (obj) => {
|
|
|
|
Object.keys(obj).forEach((key) => {
|
2020-10-13 11:33:48 -07:00
|
|
|
if (['type', 'object_id'].includes(key)) {
|
|
|
|
return;
|
|
|
|
} else if (['number', 'string', 'boolean'].includes(typeof obj[key])) {
|
2019-02-09 11:42:31 -07:00
|
|
|
payload[key] = obj[key];
|
2019-09-09 10:48:09 -07:00
|
|
|
} else if (obj[key] === null) {
|
2019-08-16 08:44:18 -07:00
|
|
|
delete payload[key];
|
2019-03-10 13:48:40 -07:00
|
|
|
} else if (key === 'device' && typeof obj[key] === 'object') {
|
|
|
|
Object.keys(obj['device']).forEach((key) => {
|
|
|
|
payload['device'][key] = obj['device'][key];
|
|
|
|
});
|
2019-02-09 11:42:31 -07:00
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2020-08-17 11:57:12 -07:00
|
|
|
add(resolvedEntity.settings.homeassistant);
|
2019-02-09 11:42:31 -07:00
|
|
|
|
2020-08-17 11:57:12 -07:00
|
|
|
if (resolvedEntity.settings.homeassistant.hasOwnProperty(config.object_id)) {
|
|
|
|
add(resolvedEntity.settings.homeassistant[config.object_id]);
|
2019-02-09 11:42:31 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-08 10:38:25 -07:00
|
|
|
const topic = this.getDiscoveryTopic(config, device);
|
2020-11-16 09:03:22 -07:00
|
|
|
this.mqtt.publish(topic, stringify(payload), {retain: true, qos: 0}, this.discoveryTopic, false, false);
|
2018-11-16 12:23:11 -07:00
|
|
|
});
|
|
|
|
|
2020-01-09 13:47:19 -07:00
|
|
|
this.discovered[device.ieeeAddr] = true;
|
2018-11-16 12:23:11 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
onMQTTMessage(topic, message) {
|
2020-09-28 10:43:04 -07:00
|
|
|
const discoveryRegex = new RegExp(`${this.discoveryTopic}/(.*)/(.*)/(.*)/config`);
|
2020-08-17 08:24:57 -07:00
|
|
|
const discoveryMatch = topic.match(discoveryRegex);
|
2020-09-25 09:20:27 -07:00
|
|
|
const isDeviceAutomation = discoveryMatch && discoveryMatch[1] === 'device_automation';
|
2020-08-17 08:24:57 -07:00
|
|
|
if (discoveryMatch) {
|
2020-09-25 09:47:28 -07:00
|
|
|
// Clear outdated discovery configs and remember already discoverd device_automations
|
2020-08-17 08:24:57 -07:00
|
|
|
try {
|
|
|
|
message = JSON.parse(message);
|
2020-10-27 13:49:44 -07:00
|
|
|
const baseTopic = settings.get().mqtt.base_topic + '/';
|
|
|
|
if (isDeviceAutomation && (!message.topic || !message.topic.startsWith(baseTopic))) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!isDeviceAutomation &&
|
|
|
|
(!message.availability || !message.availability[0].topic.startsWith(baseTopic))) {
|
2020-08-17 08:24:57 -07:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const ieeeAddr = discoveryMatch[2];
|
|
|
|
const resolvedEntity = this.zigbee.resolveEntity(ieeeAddr);
|
|
|
|
let clear = !resolvedEntity || !resolvedEntity.definition;
|
2018-04-25 11:54:41 -07:00
|
|
|
|
2020-09-25 10:48:30 -07:00
|
|
|
// Only save when topic matches otherwise config is not updated when renamed by editing configuration.yaml
|
|
|
|
if (resolvedEntity) {
|
|
|
|
const key = `${discoveryMatch[3].substring(0, discoveryMatch[3].indexOf('_'))}`;
|
|
|
|
const triggerTopic = `${settings.get().mqtt.base_topic}/${resolvedEntity.name}/${key}`;
|
|
|
|
if (isDeviceAutomation && message.topic === triggerTopic) {
|
|
|
|
if (!this.discoveredTriggers[ieeeAddr]) {
|
|
|
|
this.discoveredTriggers[ieeeAddr] = new Set();
|
|
|
|
}
|
|
|
|
this.discoveredTriggers[ieeeAddr].add(discoveryMatch[3]);
|
2020-09-25 09:47:28 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-25 09:20:27 -07:00
|
|
|
if (!clear && !isDeviceAutomation) {
|
2020-08-17 08:24:57 -07:00
|
|
|
const type = discoveryMatch[1];
|
|
|
|
const objectID = discoveryMatch[3];
|
2020-08-17 11:57:12 -07:00
|
|
|
clear = !this.getConfigs(resolvedEntity).find((c) => c.type === type && c.object_id === objectID);
|
2020-08-17 08:24:57 -07:00
|
|
|
}
|
2020-12-08 08:45:31 -07:00
|
|
|
// Device was flagged to be excluded from homeassistant discovery
|
|
|
|
clear = clear || (resolvedEntity.settings.hasOwnProperty('homeassistant') &&
|
|
|
|
!resolvedEntity.settings.homeassistant);
|
2020-08-17 08:24:57 -07:00
|
|
|
|
|
|
|
if (clear) {
|
|
|
|
logger.debug(`Clearing Home Assistant config '${topic}'`);
|
|
|
|
topic = topic.substring(this.discoveryTopic.length + 1);
|
2020-11-16 09:03:22 -07:00
|
|
|
this.mqtt.publish(topic, null, {retain: true, qos: 0}, this.discoveryTopic, false, false);
|
2020-08-17 08:24:57 -07:00
|
|
|
}
|
|
|
|
} else if ((topic === this.statusTopic || topic === defaultStatusTopic) && message.toLowerCase() === 'online') {
|
2019-09-09 10:48:09 -07:00
|
|
|
const timer = setTimeout(async () => {
|
2018-11-16 12:23:11 -07:00
|
|
|
// Publish all device states.
|
2019-09-23 13:21:27 -07:00
|
|
|
for (const device of this.zigbee.getClients()) {
|
2019-05-02 11:14:44 -07:00
|
|
|
if (this.state.exists(device.ieeeAddr)) {
|
2019-03-15 15:18:19 -07:00
|
|
|
this.publishEntityState(device.ieeeAddr, this.state.get(device.ieeeAddr));
|
2018-11-16 12:23:11 -07:00
|
|
|
}
|
2019-09-09 10:48:09 -07:00
|
|
|
}
|
2018-04-25 11:54:41 -07:00
|
|
|
|
2018-11-16 12:23:11 -07:00
|
|
|
clearTimeout(timer);
|
2020-03-10 11:32:44 -07:00
|
|
|
}, 30000);
|
2018-11-16 12:23:11 -07:00
|
|
|
}
|
2018-06-09 03:27:04 -07:00
|
|
|
}
|
|
|
|
|
2020-04-05 06:41:24 -07:00
|
|
|
onZigbeeEvent(type, data, resolvedEntity) {
|
2020-07-22 04:44:47 -07:00
|
|
|
if (resolvedEntity && type !== 'deviceLeave' && this.mqtt.isConnected()) {
|
2020-04-11 11:34:50 -07:00
|
|
|
this.discover(resolvedEntity);
|
2018-11-16 12:23:11 -07:00
|
|
|
}
|
|
|
|
}
|
2018-06-09 03:27:04 -07:00
|
|
|
|
2020-04-11 11:34:50 -07:00
|
|
|
getDevicePayload(resolvedEntity) {
|
2020-02-29 10:07:15 -07:00
|
|
|
return {
|
2020-04-11 11:34:50 -07:00
|
|
|
identifiers: [`zigbee2mqtt_${resolvedEntity.settings.ID}`],
|
|
|
|
name: resolvedEntity.settings.friendlyName,
|
2020-08-01 01:36:20 -07:00
|
|
|
sw_version: `Zigbee2MQTT ${zigbee2mqttVersion}`,
|
2020-04-11 11:34:50 -07:00
|
|
|
model: `${resolvedEntity.definition.description} (${resolvedEntity.definition.model})`,
|
|
|
|
manufacturer: resolvedEntity.definition.vendor,
|
2020-02-29 10:07:15 -07:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2020-01-09 13:47:19 -07:00
|
|
|
getDiscoveryTopic(config, device) {
|
|
|
|
return `${config.type}/${device.ieeeAddr}/${config.object_id}/config`;
|
|
|
|
}
|
|
|
|
|
2020-09-25 10:48:30 -07:00
|
|
|
async publishDeviceTriggerDiscover(entity, key, value, force=false) {
|
|
|
|
const device = entity.device;
|
|
|
|
if (!this.discoveredTriggers[device.ieeeAddr]) {
|
|
|
|
this.discoveredTriggers[device.ieeeAddr] = new Set();
|
|
|
|
}
|
|
|
|
|
|
|
|
const discoveredKey = `${key}_${value}`;
|
|
|
|
if (this.discoveredTriggers[device.ieeeAddr].has(discoveredKey) && !force) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const config = cfg[`trigger_${key}`];
|
|
|
|
config.object_id = `${key}_${value}`;
|
|
|
|
const topic = this.getDiscoveryTopic(config, device);
|
|
|
|
const payload = {
|
|
|
|
...config.discovery_payload,
|
|
|
|
subtype: value,
|
|
|
|
payload: value,
|
|
|
|
topic: `${settings.get().mqtt.base_topic}/${entity.name}/${key}`,
|
|
|
|
device: this.getDevicePayload(entity),
|
|
|
|
};
|
|
|
|
|
2020-11-16 09:03:22 -07:00
|
|
|
await this.mqtt.publish(topic, stringify(payload), {retain: true, qos: 0}, this.discoveryTopic, false, false);
|
2020-09-25 10:48:30 -07:00
|
|
|
this.discoveredTriggers[device.ieeeAddr].add(discoveredKey);
|
|
|
|
}
|
|
|
|
|
2018-11-16 12:23:11 -07:00
|
|
|
// Only for homeassistant.test.js
|
|
|
|
_getMapping() {
|
2020-10-01 09:33:59 -07:00
|
|
|
return this.mapping;
|
2018-11-16 12:23:11 -07:00
|
|
|
}
|
2020-09-25 10:48:30 -07:00
|
|
|
|
|
|
|
_clearDiscoveredTrigger() {
|
|
|
|
this.discoveredTriggers = new Set();
|
|
|
|
}
|
2018-06-09 03:27:04 -07:00
|
|
|
}
|
|
|
|
|
2018-11-16 12:23:11 -07:00
|
|
|
module.exports = HomeAssistant;
|