UNPKG

@dotwee/homebridge-z2m

Version:

Expose your Zigbee devices to HomeKit with ease, by integrating Zigbee2MQTT with Homebridge.

229 lines 12.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ThermostatCreator = void 0; const z2mModels_1 = require("../z2mModels"); const hap_1 = require("../hap"); const monitor_1 = require("./monitor"); const helpers_1 = require("../helpers"); class ThermostatCreator { createServicesFromExposes(accessory, exposes) { exposes .filter((e) => e.type === z2mModels_1.ExposesKnownTypes.CLIMATE && (0, z2mModels_1.exposesHasFeatures)(e) && ThermostatHandler.hasRequiredFeatures(accessory, e) && !accessory.isServiceHandlerIdKnown(ThermostatHandler.generateIdentifier(e.endpoint))) .forEach((e) => this.createService(e, accessory)); } createService(expose, accessory) { try { const handler = new ThermostatHandler(expose, accessory); accessory.registerServiceHandler(handler); } catch (error) { accessory.log.warn(`Failed to setup thermostat for accessory ${accessory.displayName} from expose "${JSON.stringify(expose)}":` + error); } } } exports.ThermostatCreator = ThermostatCreator; class ThermostatHandler { constructor(expose, accessory) { this.accessory = accessory; this.monitors = []; const endpoint = expose.endpoint; this.identifier = ThermostatHandler.generateIdentifier(endpoint); // Store all required features const possibleLocalTemp = expose.features.find(ThermostatHandler.PREDICATE_LOCAL_TEMPERATURE); if (possibleLocalTemp === undefined) { throw new Error('Local temperature feature not found.'); } this.localTemperatureExpose = possibleLocalTemp; const possibleSetpoint = expose.features.find(ThermostatHandler.PREDICATE_SETPOINT); if (possibleSetpoint === undefined) { throw new Error('Setpoint feature not found.'); } this.setpointExpose = possibleSetpoint; this.targetModeExpose = expose.features.find(ThermostatHandler.PREDICATE_TARGET_MODE); this.currentStateExpose = expose.features.find(ThermostatHandler.PREDICATE_CURRENT_STATE); if (this.targetModeExpose === undefined || this.currentStateExpose === undefined) { if (this.targetModeExpose !== undefined) { this.accessory.log.debug(`${accessory.displayName}: ignore ${this.targetModeExpose.property}; no current state exposed.`); } if (this.currentStateExpose !== undefined) { this.accessory.log.debug(`${accessory.displayName}: ignore ${this.currentStateExpose.property}; no current state exposed.`); } // If one of them is undefined, ignore the other one this.targetModeExpose = undefined; this.currentStateExpose = undefined; } // Setup service const serviceName = accessory.getDefaultServiceDisplayName(endpoint); accessory.log.debug(`Configuring Thermostat for ${serviceName}`); const service = accessory.getOrAddService(new hap_1.hap.Service.Thermostat(serviceName, endpoint)); // Monitor local temperature const currentTemperature = (0, helpers_1.getOrAddCharacteristic)(service, hap_1.hap.Characteristic.CurrentTemperature); (0, helpers_1.copyExposesRangeToCharacteristic)(this.localTemperatureExpose, currentTemperature); this.monitors.push(new monitor_1.PassthroughCharacteristicMonitor(this.localTemperatureExpose.property, service, hap_1.hap.Characteristic.CurrentTemperature)); // Setpoint const setpoint = (0, helpers_1.getOrAddCharacteristic)(service, hap_1.hap.Characteristic.TargetTemperature).on('set', this.handleSetSetpoint.bind(this)); (0, helpers_1.copyExposesRangeToCharacteristic)(this.setpointExpose, setpoint); this.monitors.push(new monitor_1.PassthroughCharacteristicMonitor(this.setpointExpose.property, service, hap_1.hap.Characteristic.TargetTemperature)); // Map mode/state if (this.targetModeExpose !== undefined && this.currentStateExpose !== undefined) { // Current state const stateMapping = ThermostatHandler.getCurrentStateFromMqttMapping(this.currentStateExpose.values); if (stateMapping.size === 0) { throw new Error('Cannot map current state'); } const stateValues = [...stateMapping.values()].map((x) => x); (0, helpers_1.getOrAddCharacteristic)(service, hap_1.hap.Characteristic.CurrentHeatingCoolingState).setProps({ minValue: Math.min(...stateValues), maxValue: Math.max(...stateValues), validValues: stateValues, }); this.monitors.push(new monitor_1.MappingCharacteristicMonitor(this.currentStateExpose.property, service, hap_1.hap.Characteristic.CurrentHeatingCoolingState, stateMapping)); // Target state/mode const targetMapping = ThermostatHandler.getTargetModeFromMqttMapping(this.targetModeExpose.values); if (targetMapping.size === 0) { throw new Error('Cannot map target state/mode'); } // Store reverse mapping for changing the state from HomeKit this.targetModeFromHomeKitMapping = new Map(); for (const [mqtt, hk] of targetMapping) { this.targetModeFromHomeKitMapping.set(hk, mqtt); } const targetValues = [...targetMapping.values()].map((x) => x); (0, helpers_1.getOrAddCharacteristic)(service, hap_1.hap.Characteristic.TargetHeatingCoolingState) .setProps({ minValue: Math.min(...targetValues), maxValue: Math.max(...targetValues), validValues: targetValues, }) .on('set', this.handleSetTargetState.bind(this)); this.monitors.push(new monitor_1.MappingCharacteristicMonitor(this.targetModeExpose.property, service, hap_1.hap.Characteristic.TargetHeatingCoolingState, targetMapping)); } else { // Assume heat only device (0, helpers_1.getOrAddCharacteristic)(service, hap_1.hap.Characteristic.CurrentHeatingCoolingState) .setProps({ minValue: hap_1.hap.Characteristic.CurrentHeatingCoolingState.HEAT, maxValue: hap_1.hap.Characteristic.CurrentHeatingCoolingState.HEAT, validValues: [hap_1.hap.Characteristic.CurrentHeatingCoolingState.HEAT], }) .updateValue(hap_1.hap.Characteristic.CurrentHeatingCoolingState.HEAT); (0, helpers_1.getOrAddCharacteristic)(service, hap_1.hap.Characteristic.TargetHeatingCoolingState) .setProps({ minValue: hap_1.hap.Characteristic.TargetHeatingCoolingState.HEAT, maxValue: hap_1.hap.Characteristic.TargetHeatingCoolingState.HEAT, validValues: [hap_1.hap.Characteristic.TargetHeatingCoolingState.HEAT], }) .updateValue(hap_1.hap.Characteristic.TargetHeatingCoolingState.HEAT); } // Only support degrees Celsius (0, helpers_1.getOrAddCharacteristic)(service, hap_1.hap.Characteristic.TemperatureDisplayUnits) .setProps({ minValue: hap_1.hap.Characteristic.TemperatureDisplayUnits.CELSIUS, maxValue: hap_1.hap.Characteristic.TemperatureDisplayUnits.CELSIUS, validValues: [hap_1.hap.Characteristic.TemperatureDisplayUnits.CELSIUS], }) .updateValue(hap_1.hap.Characteristic.TemperatureDisplayUnits.CELSIUS); } static getCurrentStateFromMqttMapping(values) { const mapping = new Map(); if (values.includes('idle')) { mapping.set('idle', hap_1.hap.Characteristic.CurrentHeatingCoolingState.OFF); } if (values.includes('heat')) { mapping.set('heat', hap_1.hap.Characteristic.CurrentHeatingCoolingState.HEAT); } if (values.includes('cool')) { mapping.set('cool', hap_1.hap.Characteristic.CurrentHeatingCoolingState.COOL); } return mapping; } static getTargetModeFromMqttMapping(values) { const mapping = new Map(); // 'off', 'heat', 'cool', 'auto', 'dry', 'fan_only' if (values.includes('off')) { mapping.set('off', hap_1.hap.Characteristic.TargetHeatingCoolingState.OFF); } if (values.includes('heat')) { mapping.set('heat', hap_1.hap.Characteristic.TargetHeatingCoolingState.HEAT); } if (values.includes('cool')) { mapping.set('cool', hap_1.hap.Characteristic.TargetHeatingCoolingState.COOL); } if (values.includes('auto')) { mapping.set('auto', hap_1.hap.Characteristic.TargetHeatingCoolingState.AUTO); } // NOTE: MQTT values 'dry' and 'fan_only' cannot be mapped to/from HomeKit. return mapping; } static hasRequiredFeatures(accessory, e) { if (e.features.findIndex((f) => f.name === 'occupied_cooling_setpoint') >= 0) { // For now ignore devices that have a cooling setpoint as I haven't figured our how to handle this correctly in HomeKit. return false; } return (0, z2mModels_1.exposesHasAllRequiredFeatures)(e, [ThermostatHandler.PREDICATE_SETPOINT, ThermostatHandler.PREDICATE_LOCAL_TEMPERATURE]); } get getableKeys() { const keys = []; if ((0, z2mModels_1.exposesCanBeGet)(this.localTemperatureExpose)) { keys.push(this.localTemperatureExpose.property); } if ((0, z2mModels_1.exposesCanBeGet)(this.setpointExpose)) { keys.push(this.setpointExpose.property); } if (this.targetModeExpose !== undefined && (0, z2mModels_1.exposesCanBeGet)(this.targetModeExpose)) { keys.push(this.targetModeExpose.property); } if (this.currentStateExpose !== undefined && (0, z2mModels_1.exposesCanBeGet)(this.currentStateExpose)) { keys.push(this.currentStateExpose.property); } return keys; } updateState(state) { this.monitors.forEach((m) => m.callback(state)); } static generateIdentifier(endpoint) { let identifier = hap_1.hap.Service.Thermostat.UUID; if (endpoint !== undefined) { identifier += '_' + endpoint.trim(); } return identifier; } handleSetTargetState(value, callback) { if (this.targetModeExpose !== undefined && this.targetModeFromHomeKitMapping !== undefined && this.targetModeFromHomeKitMapping.size > 0) { const mqttValue = this.targetModeFromHomeKitMapping.get(value); if (mqttValue !== undefined) { const data = {}; data[this.targetModeExpose.property] = mqttValue; this.accessory.queueDataForSetAction(data); } callback(null); } else { callback(new Error('Changing the target state is not supported for this device')); } } handleSetSetpoint(value, callback) { const data = {}; data[this.setpointExpose.property] = value; this.accessory.queueDataForSetAction(data); callback(null); } } ThermostatHandler.NAMES_SETPOINT = new Set(['current_heating_setpoint', 'occupied_heating_setpoint']); ThermostatHandler.NAME_TARGET_MODE = 'system_mode'; ThermostatHandler.NAME_CURRENT_STATE = 'running_state'; ThermostatHandler.NAME_LOCAL_TEMPERATURE = 'local_temperature'; ThermostatHandler.PREDICATE_TARGET_MODE = (f) => f.name === ThermostatHandler.NAME_TARGET_MODE && (0, z2mModels_1.exposesHasEnumProperty)(f) && (0, z2mModels_1.exposesCanBeSet)(f) && (0, z2mModels_1.exposesIsPublished)(f); ThermostatHandler.PREDICATE_CURRENT_STATE = (f) => f.name === ThermostatHandler.NAME_CURRENT_STATE && (0, z2mModels_1.exposesHasEnumProperty)(f) && (0, z2mModels_1.exposesIsPublished)(f); ThermostatHandler.PREDICATE_LOCAL_TEMPERATURE = (f) => f.name === ThermostatHandler.NAME_LOCAL_TEMPERATURE && (0, z2mModels_1.exposesHasProperty)(f) && (0, z2mModels_1.exposesIsPublished)(f); ThermostatHandler.PREDICATE_SETPOINT = (f) => f.name !== undefined && ThermostatHandler.NAMES_SETPOINT.has(f.name) && (0, z2mModels_1.exposesHasProperty)(f) && (0, z2mModels_1.exposesCanBeSet)(f) && (0, z2mModels_1.exposesIsPublished)(f); //# sourceMappingURL=climate.js.map