const settings = require('../util/settings'); const logger = require('../util/logger'); const utils = require('../util/utils'); const zigbee2mqttVersion = require('../../package.json').version; const Extension = require('./extension'); const stringify = require('json-stable-stringify-without-jsonify'); const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters'); const assert = require('assert'); const cfg = { // Binary sensor 'binary_sensor_occupancy': { type: 'binary_sensor', object_id: 'occupancy', discovery_payload: { payload_on: true, payload_off: false, value_template: '{{ value_json.occupancy }}', device_class: 'motion', }, }, 'binary_sensor_alarm': { type: 'binary_sensor', object_id: 'alarm', discovery_payload: { payload_on: true, payload_off: false, value_template: '{{ value_json.alarm }}', }, }, 'binary_sensor_temperature_alarm': { type: 'binary_sensor', object_id: 'temperature_alarm', discovery_payload: { payload_on: true, payload_off: false, value_template: '{{ value_json.temperature_alarm }}', }, }, 'binary_sensor_humidity_alarm': { type: 'binary_sensor', object_id: 'humidity_alarm', discovery_payload: { payload_on: true, payload_off: false, value_template: '{{ value_json.humidity_alarm }}', }, }, 'binary_sensor_presence': { type: 'binary_sensor', object_id: 'presence', discovery_payload: { payload_on: true, payload_off: false, value_template: '{{ value_json.presence }}', device_class: 'presence', }, }, 'binary_sensor_tamper': { type: 'binary_sensor', object_id: 'tamper', discovery_payload: { payload_on: false, payload_off: true, value_template: '{{ value_json.tamper }}', }, }, 'binary_sensor_contact': { type: 'binary_sensor', object_id: 'contact', discovery_payload: { payload_on: false, payload_off: true, value_template: '{{ value_json.contact }}', device_class: 'door', }, }, '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_vibration': { type: 'binary_sensor', object_id: 'vibration', discovery_payload: { payload_on: true, payload_off: false, value_template: '{{ value_json.vibration }}', device_class: 'vibration', }, }, 'binary_sensor_led': { type: 'binary_sensor', object_id: 'led', discovery_payload: { payload_on: true, payload_off: false, value_template: '{{ value_json.led }}', device_class: 'light', }, }, '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', discovery_payload: { payload_on: true, payload_off: false, value_template: '{{ value_json.update_available}}', }, }, 'binary_sensor_lock': { type: 'binary_sensor', object_id: 'lock', discovery_payload: { payload_on: 'UNLOCK', payload_off: 'LOCK', value_template: '{{ value_json.state}}', device_class: 'lock', }, }, 'binary_sensor_lock_reverse': { type: 'binary_sensor', object_id: 'lock_reverse', discovery_payload: { payload_on: 'UNLOCK', payload_off: 'LOCK', value_template: '{{ value_json.reverse}}', device_class: 'lock', }, }, 'binary_sensor_power_alarm_active': { type: 'binary_sensor', object_id: 'power_alarm_active', discovery_payload: { payload_on: true, payload_off: false, value_template: '{{ value_json.power_alarm_active}}', device_class: 'power', }, }, // Sensor 'sensor_update_state': { type: 'sensor', object_id: 'update_state', discovery_payload: { icon: 'mdi:update', value_template: `{{ value_json['update']['state'] }}`, }, }, 'sensor_illuminance': { type: 'sensor', object_id: 'illuminance', discovery_payload: { unit_of_measurement: '-', device_class: 'illuminance', value_template: '{{ value_json.illuminance }}', }, }, 'sensor_illuminance_lux_unit': { type: 'sensor', object_id: 'illuminance', discovery_payload: { unit_of_measurement: 'lx', device_class: 'illuminance', value_template: '{{ value_json.illuminance }}', }, }, 'sensor_illuminance_lux': { type: 'sensor', object_id: 'illuminance_lux', discovery_payload: { unit_of_measurement: 'lx', device_class: 'illuminance', value_template: '{{ value_json.illuminance_lux }}', }, }, 'sensor_humidity': { type: 'sensor', object_id: 'humidity', discovery_payload: { unit_of_measurement: '%', device_class: 'humidity', value_template: '{{ value_json.humidity }}', }, }, 'sensor_eco2': { type: 'sensor', object_id: 'eco2', discovery_payload: { unit_of_measurement: 'ppm', icon: 'mdi:air-filter', value_template: '{{ value_json.eco2 }}', }, }, 'sensor_voc': { type: 'sensor', object_id: 'voc', discovery_payload: { unit_of_measurement: 'ppb', icon: 'mdi:air-filter', value_template: '{{ value_json.voc }}', }, }, 'sensor_pm25': { type: 'sensor', object_id: 'pm25', discovery_payload: { unit_of_measurement: 'µg/m³', icon: 'mdi:air-filter', value_template: '{{ value_json.pm25 }}', }, }, 'sensor_pm10': { type: 'sensor', object_id: 'pm10', discovery_payload: { unit_of_measurement: 'µg/m³', icon: 'mdi:air-filter', value_template: '{{ value_json.pm10}}', }, }, 'sensor_hcho': { type: 'sensor', object_id: 'hcho', discovery_payload: { unit_of_measurement: 'µg/m³', icon: 'mdi:air-filter', value_template: '{{ value_json.hcho }}', }, }, 'sensor_aqi': { type: 'sensor', object_id: 'aqi', discovery_payload: { // unit_of_measurement: 'ppb', icon: 'mdi:air-filter', value_template: '{{ value_json.aqi }}', }, }, 'sensor_temperature': { type: 'sensor', object_id: 'temperature', discovery_payload: { unit_of_measurement: '°C', device_class: 'temperature', value_template: '{{ value_json.temperature }}', }, }, 'sensor_device_temperature': { type: 'sensor', object_id: 'device_temperature', discovery_payload: { unit_of_measurement: '°C', device_class: 'temperature', value_template: '{{ value_json.device_temperature }}', }, }, '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_pressure': { type: 'sensor', object_id: 'pressure', discovery_payload: { unit_of_measurement: 'hPa', device_class: 'pressure', value_template: '{{ value_json.pressure }}', }, }, 'sensor_click': { type: 'sensor', object_id: 'click', discovery_payload: { icon: 'mdi:toggle-switch', value_template: '{{ value_json.click }}', }, }, 'sensor_power': { type: 'sensor', object_id: 'power', discovery_payload: { unit_of_measurement: 'W', icon: 'mdi:flash', value_template: '{{ value_json.power }}', }, }, 'sensor_current': { type: 'sensor', object_id: 'current', discovery_payload: { unit_of_measurement: 'A', icon: 'mdi:current-ac', value_template: '{{ value_json.current }}', }, }, 'sensor_voltage': { type: 'sensor', object_id: 'voltage', discovery_payload: { unit_of_measurement: 'V', icon: 'mdi:alpha-v', value_template: '{{ value_json.voltage }}', }, }, 'sensor_energy': { type: 'sensor', object_id: 'energy', discovery_payload: { unit_of_measurement: 'kWh', icon: 'mdi:power-plug', value_template: '{{ value_json.energy }}', }, }, 'sensor_action': { type: 'sensor', object_id: 'action', discovery_payload: { icon: 'mdi:gesture-double-tap', value_template: '{{ value_json.action }}', }, }, 'sensor_action_color': { type: 'sensor', object_id: 'action_color', discovery_payload: { value_template: '{{ value_json.action_color }}', icon: 'mdi:palette', }, }, 'sensor_action_color_temperature': { type: 'sensor', object_id: 'action_color_temperature', discovery_payload: { value_template: '{{ value_json.action_color_temperature }}', icon: 'hass:thermometer', }, }, 'sensor_brightness': { type: 'sensor', object_id: 'brightness', discovery_payload: { unit_of_measurement: 'brightness', icon: 'mdi:brightness-5', value_template: '{{ value_json.brightness }}', }, }, 'sensor_lock': { type: 'sensor', object_id: 'lock', discovery_payload: { icon: 'mdi:lock', value_template: '{{ value_json.inserted }}', }, }, 'sensor_battery': { type: 'sensor', object_id: 'battery', discovery_payload: { unit_of_measurement: '%', device_class: 'battery', value_template: '{{ value_json.battery }}', }, }, 'sensor_battery_state': { type: 'sensor', object_id: 'battery_state', discovery_payload: { icon: 'mdi:battery-charging', value_template: '{{ value_json.battery_state }}', }, }, 'sensor_linkquality': { type: 'sensor', object_id: 'linkquality', discovery_payload: { icon: 'mdi:signal', unit_of_measurement: 'lqi', value_template: '{{ value_json.linkquality }}', }, }, 'sensor_gas_density': { type: 'sensor', object_id: 'gas_density', discovery_payload: { value_template: '{{ value_json.gas_density }}', icon: 'mdi:google-circles-communities', }, }, 'sensor_smoke_density': { type: 'sensor', object_id: 'smoke_density', discovery_payload: { value_template: '{{ value_json.smoke_density }}', icon: 'mdi:google-circles-communities', }, }, 'sensor_cover': { type: 'sensor', object_id: 'cover', discovery_payload: { value_template: '{{ value_json.position }}', icon: 'mdi:view-array', }, }, 'sensor_consumption': { type: 'sensor', object_id: 'consumption', discovery_payload: { unit_of_measurement: 'kWh', value_template: '{{ value_json.consumption }}', icon: 'mdi:flash', }, }, 'sensor_sensitivity': { type: 'sensor', object_id: 'sensitivity', discovery_payload: { value_template: '{{ value_json.sensitivity }}', icon: 'mdi:filter-variant', }, }, 'sensor_strength': { type: 'sensor', object_id: 'strength', discovery_payload: { value_template: '{{ value_json.strength }}', icon: 'mdi:weight', }, }, 'sensor_requested_brightness_level': { type: 'sensor', object_id: 'requested_brightness_level', discovery_payload: { value_template: '{{ value_json.requested_brightness_level }}', icon: 'mdi:brightness-5', }, }, 'sensor_requested_brightness_percent': { type: 'sensor', object_id: 'requested_brightness_percent', discovery_payload: { value_template: '{{ value_json.requested_brightness_percent }}', icon: 'mdi:brightness-5', }, }, 'sensor_radioactive_events_per_minute': { type: 'sensor', object_id: 'radioactive_events_per_minute', discovery_payload: { unit_of_measurement: 'rpm', value_template: '{{ value_json.radioactive_events_per_minute }}', }, }, 'sensor_radiation_dose_per_hour': { type: 'sensor', object_id: 'radiation_dose_per_hour', discovery_payload: { unit_of_measurement: 'rph', value_template: '{{ value_json.radiation_dose_per_hour }}', }, }, 'sensor_direction': { type: 'sensor', object_id: 'direction', discovery_payload: { value_template: '{{ value_json.direction }}', icon: 'mdi:rotate-3d-variant', }, }, 'sensor_co2': { type: 'sensor', object_id: 'co2', discovery_payload: { unit_of_measurement: 'ppm', value_template: '{{ value_json.co2 }}', icon: 'mdi:molecule-co2', }, }, // Light 'light_brightness': { type: 'light', object_id: 'light', discovery_payload: { brightness: true, schema: 'json', command_topic: true, brightness_scale: 254, }, }, // Switch 'switch': { type: 'switch', object_id: 'switch', discovery_payload: { payload_off: 'OFF', payload_on: 'ON', value_template: '{{ value_json.state }}', command_topic: true, }, }, '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 }}', }, }, // Cover 'cover': { type: 'cover', object_id: 'cover', discovery_payload: { command_topic: true, optimistic: true, }, }, // 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 }}', }, }, // Fan 'fan': { 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', speed_state_topic: true, speed_command_topic: true, speed_value_template: '{{ value_json.fan_mode }}', speeds: ['off', 'low', 'medium', 'high', 'on', 'auto', 'smart'], }, }, // 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', }, }, }; const switchEndpoint = (endpointName) => { return { type: 'switch', object_id: `switch_${endpointName}`, discovery_payload: { payload_off: 'OFF', payload_on: 'ON', value_template: `{{ value_json.state_${endpointName} }}`, command_topic: true, command_topic_prefix: endpointName, }, }; }; const sensorEndpoint = (endpointName) => { return { type: 'sensor', object_id: `sensor_${endpointName}`, discovery_payload: { value_template: `{{ value_json.${endpointName} }}`, }, }; }; 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 = { 'WXKG01LM': [cfg.sensor_click, cfg.sensor_action, cfg.sensor_battery], 'WXKG11LM': [cfg.sensor_click, cfg.sensor_action, cfg.sensor_battery], 'WXKG12LM': [cfg.sensor_click, cfg.sensor_action, cfg.sensor_battery], 'WXKG03LM': [cfg.sensor_click, cfg.sensor_action, cfg.sensor_battery], 'WXKG06LM': [cfg.sensor_battery, cfg.sensor_action], 'WXKG02LM': [cfg.sensor_click, cfg.sensor_action, cfg.sensor_battery], 'QBKG04LM': [cfg.switch, cfg.sensor_click, cfg.sensor_action], 'QBKG03LM': [ switchEndpoint('left'), switchEndpoint('right'), cfg.sensor_click, cfg.sensor_action, cfg.sensor_temperature, ], 'MFKZQ01LM': [cfg.sensor_action, cfg.sensor_battery], 'CC2530.ROUTER': [cfg.binary_sensor_led], 'ICTC-G-1': [cfg.sensor_brightness, cfg.sensor_battery, cfg.sensor_action], 'QBKG11LM': [cfg.switch, cfg.sensor_power, cfg.sensor_click, cfg.sensor_action, cfg.sensor_temperature], 'QBKG21LM': [cfg.switch, cfg.sensor_click, cfg.sensor_action], 'QBKG22LM': [switchEndpoint('left'), switchEndpoint('right'), cfg.sensor_action, cfg.sensor_click], 'QBKG12LM': [ switchEndpoint('left'), switchEndpoint('right'), cfg.sensor_power, cfg.sensor_click, cfg.sensor_temperature, cfg.sensor_action, ], 'Z809A': [cfg.switch, cfg.sensor_power], '324131092621': [cfg.sensor_action, cfg.sensor_battery], '9290012607': [ cfg.binary_sensor_occupancy, cfg.sensor_temperature, cfg.sensor_illuminance, cfg.sensor_illuminance_lux, cfg.sensor_battery, ], 'STSS-MULT-001': [cfg.binary_sensor_contact, cfg.sensor_battery], 'A6121': [cfg.sensor_lock], 'DJT11LM': [cfg.sensor_action, cfg.sensor_battery, cfg.sensor_sensitivity, cfg.sensor_strength], '4256251-RZHAC': [cfg.switch, cfg.sensor_power], '4257050-ZHAC': [cfg.light_brightness, cfg.sensor_power, cfg.sensor_current, cfg.sensor_voltage], 'STS-PRS-251': [cfg.binary_sensor_presence, cfg.sensor_battery], 'STSS-PRES-001': [cfg.binary_sensor_presence, cfg.sensor_battery], 'TS0003_curtain_switch': [cfg.cover], 'AV2010/25': [cfg.switch, cfg.sensor_power], '1TST-EU': [climate(), cfg.sensor_battery], 'AIRAM-CTR.U': [], 'AV2010/32': [climate(7, 30, 'occupied_heating_setpoint', 0.5), cfg.sensor_battery], 'E1524/E1810': [cfg.sensor_action, cfg.sensor_battery], 'AC0251100NJ/AC0251700NJ': [cfg.sensor_action, cfg.sensor_battery], 'MLI-404011': [cfg.sensor_action], 'SP600': [cfg.switch, cfg.sensor_power, cfg.sensor_energy], 'SPZB0001': [climate(5, 30, 'current_heating_setpoint', 0.5), cfg.sensor_battery], '81825': [cfg.sensor_action], 'E1743': [cfg.sensor_click, cfg.sensor_action, cfg.sensor_battery], '9290019758': [ cfg.binary_sensor_occupancy, cfg.sensor_temperature, cfg.sensor_illuminance, cfg.sensor_illuminance_lux, cfg.sensor_battery, ], 'ST218': [ climate(5, 30, 'occupied_heating_setpoint', 0.5), cfg.sensor_local_temperature, cfg.lock_keypad_lockout, ], 'E1525/E1745': [ cfg.binary_sensor_occupancy, cfg.sensor_battery, cfg.sensor_requested_brightness_level, cfg.sensor_requested_brightness_percent, ], 'ZYCT-202': [cfg.sensor_action], 'IM6001-BTP01': [cfg.sensor_click, cfg.sensor_action, cfg.sensor_temperature, cfg.sensor_battery], 'AV2010/34': [cfg.sensor_click, cfg.sensor_action], 'PP-WHT-US': [ cfg.switch, cfg.sensor_power, cfg.sensor_current, cfg.sensor_voltage, ], 'HGZB-1S': [cfg.switch, cfg.sensor_click, cfg.sensor_action], 'HGZB-045': [cfg.switch, cfg.sensor_click, cfg.sensor_action], '3310-S': [cfg.sensor_temperature, cfg.sensor_humidity, cfg.sensor_battery], 'SWO-KEF1PA': [cfg.sensor_action], 'HGZB-02S': [cfg.sensor_click, cfg.sensor_action, cfg.switch], 'HS1RC-N': [cfg.sensor_action, cfg.sensor_battery], 'HS2SS': [cfg.sensor_action, cfg.sensor_battery], 'N2G-SP': [cfg.sensor_power, cfg.switch, cfg.sensor_energy], '99432': [cfg.fan, cfg.light_brightness], 'InstaRemote': [cfg.sensor_action], 'ZGRC-KEY-013': [cfg.sensor_click, cfg.sensor_action], 'SZ-ESW01-AU': [cfg.sensor_power, cfg.switch], 'MEAZON_BIZY_PLUG': [cfg.sensor_power, cfg.switch, cfg.sensor_temperature], 'MEAZON_DINRAIL': [cfg.sensor_power, cfg.switch, cfg.sensor_temperature], 'ZNMS12LM': [ cfg.sensor_action, cfg.binary_sensor_lock, cfg.binary_sensor_lock_reverse, cfg.sensor_battery, ], 'ZNMS13LM': [ cfg.sensor_action, cfg.binary_sensor_lock, cfg.binary_sensor_lock_reverse, ], 'ZNMS11LM': [ cfg.sensor_action, cfg.binary_sensor_lock, cfg.binary_sensor_lock_reverse, ], '12050': [cfg.switch, cfg.sensor_power], 'RH3040': [cfg.sensor_battery, cfg.binary_sensor_occupancy], 'LZL4BWHL01': [cfg.sensor_action], '2AJZ4KPKEY': [cfg.sensor_click, cfg.sensor_action, cfg.sensor_battery], 'TH1123ZB': [ climate(7, 30, 'occupied_heating_setpoint', 0.5), cfg.sensor_local_temperature, cfg.lock_keypad_lockout, cfg.sensor_power, cfg.sensor_energy, ], 'TH1124ZB': [ climate(7, 30, 'occupied_heating_setpoint', 0.5), cfg.sensor_local_temperature, cfg.lock_keypad_lockout, cfg.sensor_power, cfg.sensor_energy, ], 'TH1300ZB': [ climate(7, 30, 'occupied_heating_setpoint', 0.5), cfg.sensor_local_temperature, cfg.lock_keypad_lockout, ], 'TH1400ZB': [climate()], 'TH1500ZB': [climate()], 'Zen-01-W': [climate(10, 30, 'occupied_heating_setpoint', 0.5)], 'ptvo.switch': [ switchEndpoint('l1'), switchEndpoint('l2'), switchEndpoint('l3'), switchEndpoint('l4'), switchEndpoint('l5'), switchEndpoint('l6'), switchEndpoint('l7'), switchEndpoint('l8'), sensorEndpoint('l1'), sensorEndpoint('l2'), sensorEndpoint('l3'), sensorEndpoint('l4'), sensorEndpoint('l5'), sensorEndpoint('l6'), sensorEndpoint('l7'), sensorEndpoint('l8'), cfg.sensor_click, cfg.sensor_temperature, cfg.sensor_voltage, cfg.sensor_pressure, cfg.sensor_humidity, cfg.sensor_action, ], 'DIYRuZ_KEYPAD20': [], 'DTB190502A1': [], 'ZWallRemote0': [cfg.sensor_click, cfg.sensor_action], 'D1': [cfg.light_brightness, cfg.sensor_power], 'Z3-1BRL': [cfg.sensor_action, cfg.sensor_brightness], 'ROB_200-008-0': [cfg.sensor_battery, cfg.sensor_click, cfg.sensor_action], 'SZ-ESW01': [cfg.switch, cfg.sensor_power], 'ICZB-KPD18S': [cfg.sensor_battery, cfg.sensor_click, cfg.sensor_action], 'E1766': [cfg.sensor_click, cfg.sensor_battery, cfg.sensor_action], 'DIYRuZ_magnet': [cfg.binary_sensor_contact, cfg.sensor_battery], 'E1744': [cfg.sensor_action, cfg.sensor_battery], 'TERNCY-PP01': [ cfg.sensor_temperature, cfg.binary_sensor_occupancy, cfg.sensor_illuminance, cfg.sensor_illuminance_lux, cfg.sensor_click, cfg.sensor_action, ], 'CR11S8UZ': [cfg.sensor_action], 'UK7004240': [climate(), cfg.sensor_battery], 'TS0218': [cfg.sensor_action, cfg.sensor_battery], 'DIYRuZ_rspm': [cfg.switch, cfg.sensor_action, cfg.sensor_power, cfg.sensor_current], 'WXCJKG11LM': [cfg.sensor_action, cfg.sensor_battery], 'WXCJKG12LM': [cfg.sensor_action, cfg.sensor_battery], 'WXCJKG13LM': [cfg.sensor_action, cfg.sensor_battery], '8718699693985': [cfg.sensor_action, cfg.sensor_battery], 'TS0043': [cfg.sensor_action, cfg.sensor_battery], 'TS0041': [cfg.sensor_action, cfg.sensor_battery], '6735/6736/6737': [cfg.switch, cfg.sensor_action], 'SKHMP30-I1': [cfg.switch, cfg.sensor_power], '6ARCZABZH': [cfg.sensor_battery, cfg.sensor_action], 'LVS-SC7': [cfg.sensor_action], 'WV704R0A0902': [climate()], '067773': [cfg.sensor_action, cfg.sensor_battery], '064873': [cfg.sensor_action], 'K4003C/L4003C/N4003C/NT4003C': [cfg.switch, cfg.sensor_action], 'STZB402': [ climate(5, 30, 'occupied_heating_setpoint', 0.5), cfg.sensor_local_temperature, cfg.lock_keypad_lockout, ], 'SMT402': [ climate(5, 30, 'occupied_heating_setpoint', 0.5), cfg.sensor_local_temperature, cfg.lock_keypad_lockout, ], 'SMT402AD': [ climate(5, 30, 'occupied_heating_setpoint', 0.5), cfg.sensor_local_temperature, cfg.lock_keypad_lockout, ], '067775': [cfg.switch, cfg.sensor_power], '412015': [cfg.sensor_power, cfg.binary_sensor_power_alarm_active], 'U86KCJ-ZP': [cfg.sensor_action], 'HS2IRC': [cfg.sensor_battery], 'B01M7Y8BP9': [cfg.sensor_action], 'ZG2835RAC': [cfg.light_brightness, cfg.sensor_power, cfg.sensor_energy], 'BW-IS3': [cfg.binary_sensor_occupancy], 'SLR1b': [climate()], 'TS0042': [cfg.sensor_action, cfg.sensor_battery], 'SNZB-01': [cfg.sensor_action, cfg.sensor_battery], '07046L': [cfg.sensor_action], 'TERNCY-SD01': [cfg.sensor_click, cfg.sensor_battery, cfg.sensor_action, cfg.sensor_direction], 'ICZB-KPD14S': [cfg.sensor_battery, cfg.sensor_click, cfg.sensor_action], '73743': [cfg.sensor_action, cfg.sensor_battery], 'C4': [cfg.sensor_action], '4512703': [cfg.sensor_action, cfg.sensor_battery], '4512721': [cfg.sensor_action, cfg.sensor_battery], '4512702': [cfg.sensor_action, cfg.sensor_battery], 'HS1EB/HS1EB-E': [cfg.sensor_click, cfg.sensor_action, cfg.sensor_battery], 'HS2SW1A/HS2SW1A-N': [cfg.switch, cfg.sensor_device_temperature], 'HS2SW2A/HS2SW2A-N': [switchEndpoint('left'), switchEndpoint('right'), cfg.sensor_device_temperature], 'HS2SW3A/HS2SW3A-N': [ switchEndpoint('left'), switchEndpoint('right'), switchEndpoint('center'), cfg.sensor_device_temperature, ], 'ROB_200-007-0': [cfg.sensor_action, cfg.sensor_battery], 'GS361A-H04': [ cfg.lock_child_lock, cfg.switch_window_detection, cfg.switch_valve_detection, climate(5, 30, 'current_heating_setpoint', 0.5), cfg.sensor_battery, ], 'S9ZGBRC01': [cfg.sensor_action, cfg.sensor_battery], '511.557': [cfg.sensor_action], 'U02I007C.01': [ cfg.sensor_action, cfg.binary_sensor_contact, cfg.binary_sensor_water_leak, cfg.sensor_temperature, cfg.sensor_humidity, cfg.sensor_battery, ], 'SSS401ZB': [cfg.switch, cfg.sensor_action], '3460-L': [cfg.sensor_action, cfg.sensor_temperature, cfg.sensor_battery], '3157100': [climate(10, 30, 'occupied_heating_setpoint', 1, ['off', 'heat', 'cool'], ['auto', 'on'], [], true, true), cfg.sensor_battery], '4512706': [cfg.sensor_battery, cfg.sensor_action], 'RC-2000WH': [climate(10, 30, 'occupied_heating_setpoint', 1, ['off', 'auto', 'heat', 'cool'], ['auto', 'on', 'smart'], [], true, true)], '511.344': [cfg.sensor_battery, cfg.sensor_action, cfg.sensor_action_color, cfg.sensor_action_color_temperature], 'AU-A1ZBPIA': [cfg.switch, cfg.sensor_power, cfg.sensor_voltage, cfg.sensor_current], 'GreenPower_On_Off_Switch': [cfg.sensor_action], 'GreenPower_7': [cfg.sensor_action], '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']), ], 'WXKG07LM': [cfg.sensor_action, cfg.sensor_battery], '752189': [cfg.sensor_action, cfg.sensor_battery], 'AU-A1ZBRC': [cfg.sensor_action, cfg.sensor_battery], 'ZK03840': [climate()], 'MCLH-08': [cfg.sensor_temperature, cfg.sensor_humidity, cfg.sensor_eco2, cfg.sensor_voc], '8840100H': [cfg.binary_sensor_water_leak, cfg.sensor_temperature, cfg.sensor_battery], 'TERNCY-DC01': [cfg.sensor_temperature, cfg.binary_sensor_contact], 'DIYRuZ_FreePad': [cfg.sensor_action, cfg.sensor_battery], 'SE21': [cfg.sensor_action], '500.67': [cfg.sensor_action], 'E1E-G7F': [cfg.sensor_action], 'QBKG25LM': [switchEndpoint('left'), switchEndpoint('center'), switchEndpoint('right'), cfg.sensor_action], 'QBKG24LM': [switchEndpoint('left'), switchEndpoint('right'), cfg.sensor_power, cfg.sensor_action], '100.462.31': [cfg.sensor_action], 'AU-A1ZBPIAB': [cfg.switch, cfg.sensor_voltage, cfg.sensor_current, cfg.sensor_power], '4058075816459': [cfg.sensor_action, cfg.sensor_battery], 'SAGE206612': [cfg.sensor_action, cfg.sensor_battery], 'ZS232000178': [cfg.sensor_action], 'DIYRuZ_Geiger': [cfg.sensor_radioactive_events_per_minute, cfg.sensor_radiation_dose_per_hour, cfg.sensor_action], 'DJT12LM': [cfg.sensor_action], 'KMPCIL_RES005': [ cfg.sensor_battery, cfg.sensor_temperature, cfg.sensor_humidity, cfg.sensor_pressure, cfg.sensor_illuminance, cfg.sensor_illuminance_lux, cfg.binary_sensor_occupancy, cfg.switch, ], '067774': [cfg.sensor_action, cfg.sensor_battery], '067694': [cfg.sensor_action, cfg.sensor_battery], 'SNTZ009': [cfg.binary_sensor_water_leak], 'NAS-AB02B0': [ cfg.sensor_temperature, cfg.sensor_humidity, cfg.binary_sensor_humidity_alarm, cfg.binary_sensor_temperature_alarm, cfg.binary_sensor_alarm, ], 'U86KWF-ZPSJ': [climate(5, 30, 'current_heating_setpoint', 0.5)], 'D3-DPWK-TY': [climate(5, 30, 'current_heating_setpoint', 0.5)], 'TS0215A': [cfg.sensor_battery, cfg.sensor_action], 'TPZRCO2HT-Z3': [cfg.sensor_temperature, cfg.sensor_humidity, cfg.sensor_battery, cfg.sensor_co2], '3400-D': [cfg.sensor_action, cfg.sensor_battery], 'ED-10011': [cfg.sensor_action], 'ED-10012': [cfg.sensor_action], 'SR-ZG9001T4-DIM-EU': [cfg.sensor_action], 'BHT-002-GCLZB': [ cfg.lock_child_lock, climate(5, 30, 'current_heating_setpoint', 1, ['off', 'heat'], [], ['hold', 'program']), ], 'QZR-ZIG2400': [], 'LeTV.8KEY': [cfg.sensor_action], 'TS0044': [cfg.sensor_action, cfg.sensor_battery], 'ZG2835': [cfg.sensor_action], 'CTR.UBX': [cfg.sensor_action], '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': [ climate(5, 30, 'current_heating_setpoint', 0.5, ['off', 'heat'], [], ['manual', 'program']), ], 'JS-SLK2-ZB': [cfg.sensor_action], 'TS0216': [cfg.sensor_battery], 'ICZB-RM11S': [cfg.sensor_battery, cfg.sensor_action], }; const defaultStatusTopic = 'homeassistant/status'; /** * This extensions handles integration with HomeAssistant */ class HomeAssistant extends Extension { constructor(zigbee, mqtt, state, publishEntityState, eventBus) { super(zigbee, mqtt, state, publishEntityState, eventBus); // A map of all discoverd devices this.discovered = {}; this.mapping = {}; this.discoveredTriggers = {}; this.legacyApi = settings.get().advanced.legacy_api; this.newApi = settings.get().experimental.new_api; if (!settings.get().advanced.cache_state) { logger.warn('In order for HomeAssistant integration to work properly set `cache_state: true'); } if (settings.get().experimental.output === 'attribute') { throw new Error('Home Assitant integration is not possible with attribute output!'); } this.discoveryTopic = settings.get().advanced.homeassistant_discovery_topic; this.statusTopic = settings.get().advanced.homeassistant_status_topic; this.eventBus.on('deviceRemoved', (data) => this.onDeviceRemoved(data.resolvedEntity), this.constructor.name); this.eventBus.on('publishEntityState', (data) => this.onPublishEntityState(data), this.constructor.name); this.eventBus.on('deviceRenamed', (data) => this.onDeviceRenamed(data.device, data.homeAssisantRename), this.constructor.name, ); this.populateMapping(); } 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] = []; 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: { brightness: !!expose.features.find((e) => e.name === 'brightness'), color_temp: !!expose.features.find((e) => e.name === 'color_temp'), xy: !!expose.features.find((e) => e.name === 'color' && e.schema.properties.x), hs: !!expose.features.find((e) => e.name === 'color' && e.schema.properties.hue), 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') { 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}` : ''} }}`, command_topic: true, command_topic_prefix: expose.endpoint ? expose.endpoint : undefined, }, }; } else if (expose.type === 'lock') { assert(!expose.endpoint, `Endpoint not supported for lock type`); discoveryEntry = { type: 'lock', object_id: 'lock', discovery_payload: { command_topic: true, value_template: '{{ value_json.state }}', state_locked: 'LOCK', state_unlocked: 'UNLOCK', }, }; } 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 === 'binary') { const lookup = { occupancy: {device_class: 'motion'}, battery_low: {device_class: 'battery'}, tamper: {}, moving: {}, sos: {}, 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'}, }; assert(!expose.endpoint, `Endpoint not supported for binary type`); assert(lookup[expose.name], `${expose.name} not in lookup`); discoveryEntry = { type: 'binary_sensor', object_id: expose.name, discovery_payload: { value_template: `{{ value_json.${expose.name} }}`, 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'}, pressure: {device_class: 'pressure'}, power: {icon: 'mdi:flash'}, linkquality: {icon: 'mdi:signal'}, 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'}, 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'}, }; assert(!expose.endpoint, `Endpoint not supported for numeric type`); assert(lookup[expose.name], `${expose.name} not in lookup`); discoveryEntry = { type: 'sensor', object_id: expose.name, discovery_payload: { unit_of_measurement: expose.unit ? expose.unit : '-', value_template: `{{ value_json.${expose.name} }}`, ...lookup[expose.name], }, }; } else if (expose.type === 'enum') { if (expose.access === 'r' || expose.access === 'rw') { assert(!expose.endpoint, `Endpoint not supported for enum type`); discoveryEntry = { type: 'sensor', object_id: expose.name, discovery_payload: { value_template: `{{ value_json.${expose.name} }}`, }, }; } } else { throw new Error(`Unsupported exposes type: '${expose.type}'`); } if (discoveryEntry) { 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`); } } for (const definition of utils.getExternalConvertersDefinitions(settings)) { if (definition.hasOwnProperty('homeassistant')) { this.mapping[definition.model] = definition.homeassistant; } } } onDeviceRemoved(resolvedEntity) { logger.debug(`Clearing Home Assistant discovery topic for '${resolvedEntity.name}'`); delete this.discovered[resolvedEntity.device.ieeeAddr]; for (const config of this.getConfigs(resolvedEntity)) { const topic = this.getDiscoveryTopic(config, resolvedEntity.device); this.mqtt.publish(topic, null, {retain: true, qos: 0}, this.discoveryTopic); } } async onPublishEntityState(data) { /** * In case we deal with a lightEndpoint configuration Zigbee2MQTT publishes * 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. */ if (data.entity.definition && this.mapping[data.entity.definition.model]) { for (const config of this.mapping[data.entity.definition.model]) { const match = /light_(.*)/.exec(config['object_id']); if (match) { const endpoint = match[1]; const endpointRegExp = new RegExp(`(.*)_${endpoint}`); const payload = {}; for (const key of Object.keys(data.payload)) { const keyMatch = endpointRegExp.exec(key); if (keyMatch) { payload[keyMatch[1]] = data.payload[key]; } } await this.mqtt.publish( `${data.entity.name}/${endpoint}`, stringify(payload), {}, ); } } } /** * 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) { const keys = ['action', 'click'].filter((k) => data.payload.hasOwnProperty(k) && data.payload[k] !== ''); for (const key of keys) { this.publishEntityState(data.entity.device.ieeeAddr, {[key]: ''}); } } /** * 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 */ if (data.entity.definition) { const keys = ['action', 'click'].filter((k) => data.payload[k] && data.payload[k] !== ''); for (const key of keys) { const value = data.payload[key].toString(); await this.publishDeviceTriggerDiscover(data.entity, key, value); await this.mqtt.publish(`${data.entity.name}/${key}`, value, {}); } } /** * Publish a value for update_available (if not there yet) to prevent Home Assistant generating warnings of * this value not being available. */ const supportsOTA = data.entity.definition && data.entity.definition.hasOwnProperty('ota'); const mockedValues = [ { property: 'update_available', condition: supportsOTA && this.legacyApi, value: false, }, { property: 'update', condition: supportsOTA && this.newApi, value: {state: 'idle'}, }, { property: 'water_leak', condition: data.entity.device && data.entity.definition && this.mapping[data.entity.definition.model] && this.mapping[data.entity.definition.model].includes(cfg.binary_sensor_water_leak), 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}); } } } onDeviceRenamed(device, homeAssisantRename) { logger.debug(`Refreshing Home Assistant discovery topic for '${device.ieeeAddr}'`); const resolvedEntity = this.zigbee.resolveEntity(device); // Clear before rename so Home Assistant uses new friendly_name // https://github.com/Koenkk/zigbee2mqtt/issues/4096#issuecomment-674044916 if (homeAssisantRename) { for (const config of this.getConfigs(resolvedEntity)) { const topic = this.getDiscoveryTopic(config, device); this.mqtt.publish(topic, null, {retain: true, qos: 0}, this.discoveryTopic); } } this.discover(resolvedEntity, true); 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); } } } async onMQTTConnected() { this.mqtt.subscribe(this.statusTopic); this.mqtt.subscribe(defaultStatusTopic); this.mqtt.subscribe(`${this.discoveryTopic}/#`); // MQTT discovery of all paired devices on startup. for (const device of this.zigbee.getClients()) { const resolvedEntity = this.zigbee.resolveEntity(device); this.discover(resolvedEntity, true); } } getConfigs(resolvedEntity) { 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); } if (this.newApi) { configs.push(cfg.sensor_update_state); } } if (resolvedEntity.settings.hasOwnProperty('legacy') && !resolvedEntity.settings.legacy) { configs = configs.filter((c) => c !== cfg.sensor_click); } if (!settings.get().advanced.homeassistant_legacy_triggers) { configs = configs.filter((c) => c !== cfg.sensor_action && c !== cfg.sensor_click); } // deep clone of the config objects configs = JSON.parse(JSON.stringify(configs)); if (resolvedEntity.settings.hasOwnProperty('homeassistant')) { 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; } }); } return configs; } discover(resolvedEntity, force=false) { // Check if already discoverd and check if there are configs. const {device, definition} = resolvedEntity; const discover = force || !this.discovered[device.ieeeAddr]; if (!discover || !device || !definition || !this.mapping[definition.model] || device.interviewing || (resolvedEntity.settings.hasOwnProperty('homeassistant') && !resolvedEntity.settings.homeassistant)) { return; } const friendlyName = resolvedEntity.settings.friendlyName; this.getConfigs(resolvedEntity).forEach((config) => { const payload = {...config.discovery_payload}; let stateTopic = `${settings.get().mqtt.base_topic}/${friendlyName}`; if (payload.state_topic_postfix) { stateTopic += `/${payload.state_topic_postfix}`; delete payload.state_topic_postfix; } if (!payload.hasOwnProperty('state_topic') || payload.state_topic) { payload.state_topic = stateTopic; } else { /* istanbul ignore else */ if (payload.hasOwnProperty('state_topic')) { delete payload.state_topic; } } if (payload.position_topic) { payload.position_topic = stateTopic; } if (payload.tilt_status_topic) { payload.tilt_status_topic = stateTopic; } payload.json_attributes_topic = stateTopic; // Set (unique) name, separate by space if friendlyName contains space. const nameSeparator = friendlyName.includes(' ') ? ' ' : '_'; payload.name = `${friendlyName}${nameSeparator}${config.object_id}`; // Set unique_id payload.unique_id = `${resolvedEntity.settings.ID}_${config.object_id}_${settings.get().mqtt.base_topic}`; // Attributes for device registry payload.device = this.getDevicePayload(resolvedEntity); // Set availability payload // When using availability_timeout each device has it's own availability topic. // If not, use the availability topic of Zigbee2MQTT. if (settings.get().advanced.availability_timeout) { payload.availability_topic = `${settings.get().mqtt.base_topic}/${friendlyName}/availability`; } else { payload.availability_topic = `${settings.get().mqtt.base_topic}/bridge/state`; } if (payload.command_topic) { payload.command_topic = `${settings.get().mqtt.base_topic}/${friendlyName}/`; if (payload.command_topic_prefix) { payload.command_topic += `${payload.command_topic_prefix}/`; delete payload.command_topic_prefix; } payload.command_topic += 'set'; if (payload.command_topic_postfix) { payload.command_topic += `/${payload.command_topic_postfix}`; delete payload.command_topic_postfix; } } if (payload.set_position_topic && payload.command_topic) { payload.set_position_topic = payload.command_topic; } 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'; } if (payload.mode_state_topic) { payload.mode_state_topic = stateTopic; } if (payload.mode_command_topic) { payload.mode_command_topic = `${stateTopic}/set/system_mode`; } if (payload.hold_command_topic) { payload.hold_command_topic = `${stateTopic}/set/preset`; } if (payload.hold_state_topic) { payload.hold_state_topic = stateTopic; } if (payload.away_mode_state_topic) { payload.away_mode_state_topic = stateTopic; } if (payload.away_mode_command_topic) { payload.away_mode_command_topic = `${stateTopic}/set/away_mode`; } if (payload.current_temperature_topic) { payload.current_temperature_topic = stateTopic; } if (payload.temperature_state_topic) { payload.temperature_state_topic = stateTopic; } if (payload.temperature_low_state_topic) { payload.temperature_low_state_topic = stateTopic; } if (payload.temperature_high_state_topic) { payload.temperature_high_state_topic = stateTopic; } if (payload.speed_state_topic) { payload.speed_state_topic = stateTopic; } if (payload.temperature_command_topic) { payload.temperature_command_topic = `${stateTopic}/set/${payload.temperature_command_topic}`; } 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`; } if (payload.speed_command_topic) { payload.speed_command_topic = `${stateTopic}/set/fan_mode`; } if (payload.action_topic) { payload.action_topic = stateTopic; } // Override configuration with user settings. if (resolvedEntity.settings.hasOwnProperty('homeassistant')) { const add = (obj) => { Object.keys(obj).forEach((key) => { if (['type', 'object_id'].includes(key)) { return; } else if (['number', 'string', 'boolean'].includes(typeof obj[key])) { payload[key] = obj[key]; } else if (obj[key] === null) { delete payload[key]; } else if (key === 'device' && typeof obj[key] === 'object') { Object.keys(obj['device']).forEach((key) => { payload['device'][key] = obj['device'][key]; }); } }); }; add(resolvedEntity.settings.homeassistant); if (resolvedEntity.settings.homeassistant.hasOwnProperty(config.object_id)) { add(resolvedEntity.settings.homeassistant[config.object_id]); } } const topic = this.getDiscoveryTopic(config, device); this.mqtt.publish(topic, stringify(payload), {retain: true, qos: 0}, this.discoveryTopic); }); this.discovered[device.ieeeAddr] = true; } onMQTTMessage(topic, message) { const discoveryRegex = new RegExp(`${this.discoveryTopic}/(.*)/(.*)/(.*)/config`); const discoveryMatch = topic.match(discoveryRegex); const isDeviceAutomation = discoveryMatch && discoveryMatch[1] === 'device_automation'; if (discoveryMatch) { // Clear outdated discovery configs and remember already discoverd device_automations try { message = JSON.parse(message); const property = isDeviceAutomation ? 'topic' : 'availability_topic'; if (!message || !message[property] || !message[property].startsWith(settings.get().mqtt.base_topic + '/')) { // Base topic is different, probably different Zigbee2MQTT instance. return; } } catch (e) { return; } const ieeeAddr = discoveryMatch[2]; const resolvedEntity = this.zigbee.resolveEntity(ieeeAddr); let clear = !resolvedEntity || !resolvedEntity.definition; // 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]); } } if (!clear && !isDeviceAutomation) { const type = discoveryMatch[1]; const objectID = discoveryMatch[3]; clear = !this.getConfigs(resolvedEntity).find((c) => c.type === type && c.object_id === objectID); } if (clear) { logger.debug(`Clearing Home Assistant config '${topic}'`); topic = topic.substring(this.discoveryTopic.length + 1); this.mqtt.publish(topic, null, {retain: true, qos: 0}, this.discoveryTopic); } } else if ((topic === this.statusTopic || topic === defaultStatusTopic) && message.toLowerCase() === 'online') { const timer = setTimeout(async () => { // Publish all device states. for (const device of this.zigbee.getClients()) { if (this.state.exists(device.ieeeAddr)) { this.publishEntityState(device.ieeeAddr, this.state.get(device.ieeeAddr)); } } clearTimeout(timer); }, 30000); } } onZigbeeEvent(type, data, resolvedEntity) { if (resolvedEntity && type !== 'deviceLeave' && this.mqtt.isConnected()) { this.discover(resolvedEntity); } } getDevicePayload(resolvedEntity) { return { identifiers: [`zigbee2mqtt_${resolvedEntity.settings.ID}`], name: resolvedEntity.settings.friendlyName, sw_version: `Zigbee2MQTT ${zigbee2mqttVersion}`, model: `${resolvedEntity.definition.description} (${resolvedEntity.definition.model})`, manufacturer: resolvedEntity.definition.vendor, }; } getDiscoveryTopic(config, device) { return `${config.type}/${device.ieeeAddr}/${config.object_id}/config`; } 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), }; await this.mqtt.publish(topic, stringify(payload), {retain: true, qos: 0}, this.discoveryTopic); this.discoveredTriggers[device.ieeeAddr].add(discoveredKey); } // Only for homeassistant.test.js _getMapping() { return this.mapping; } _clearDiscoveredTrigger() { this.discoveredTriggers = new Set(); } } module.exports = HomeAssistant;