@dotwee/homebridge-z2m
Version:
Expose your Zigbee devices to HomeKit with ease, by integrating Zigbee2MQTT with Homebridge.
229 lines • 12.6 kB
JavaScript
"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