UNPKG

@dotwee/homebridge-z2m

Version:

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

388 lines 20.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.LightCreator = void 0; const z2mModels_1 = require("../z2mModels"); const hap_1 = require("../hap"); const helpers_1 = require("../helpers"); const monitor_1 = require("./monitor"); const colorhelper_1 = require("../colorhelper"); const experimental_1 = require("../experimental"); class LightCreator { createServicesFromExposes(accessory, exposes) { exposes .filter((e) => e.type === z2mModels_1.ExposesKnownTypes.LIGHT && (0, z2mModels_1.exposesHasFeatures)(e) && (0, z2mModels_1.exposesHasAllRequiredFeatures)(e, [LightHandler.PREDICATE_STATE]) && !accessory.isServiceHandlerIdKnown(LightHandler.generateIdentifier(e.endpoint))) .forEach((e) => this.createService(e, accessory)); } createService(expose, accessory) { try { const handler = new LightHandler(expose, accessory); accessory.registerServiceHandler(handler); } catch (error) { accessory.log.warn(`Failed to setup light for accessory ${accessory.displayName} from expose "${JSON.stringify(expose)}": ${error}`); } } } exports.LightCreator = LightCreator; class LightHandler { constructor(expose, accessory) { this.accessory = accessory; this.monitors = []; // Internal cache for hue and saturation. Needed in case X/Y is used this.cached_hue = 0.0; this.received_hue = false; this.cached_saturation = 0.0; this.received_saturation = false; const endpoint = expose.endpoint; this.identifier = LightHandler.generateIdentifier(endpoint); const features = expose.features.filter((e) => (0, z2mModels_1.exposesHasProperty)(e)).map((e) => e); // On/off characteristic (required by HomeKit) const potentialStateExpose = features.find((e) => LightHandler.PREDICATE_STATE(e)); if (potentialStateExpose === undefined) { throw new Error('Required "state" property not found for Light.'); } this.stateExpose = potentialStateExpose; const serviceName = accessory.getDefaultServiceDisplayName(endpoint); accessory.log.debug(`Configuring Light for ${serviceName}`); const service = accessory.getOrAddService(new hap_1.hap.Service.Lightbulb(serviceName, endpoint)); (0, helpers_1.getOrAddCharacteristic)(service, hap_1.hap.Characteristic.On).on('set', this.handleSetOn.bind(this)); const onOffValues = new Map(); onOffValues.set(this.stateExpose.value_on, true); onOffValues.set(this.stateExpose.value_off, false); this.monitors.push(new monitor_1.MappingCharacteristicMonitor(this.stateExpose.property, service, hap_1.hap.Characteristic.On, onOffValues)); // Brightness characteristic this.tryCreateBrightness(features, service); // Color: Hue/Saturation or X/Y this.tryCreateColor(expose, service); // Color temperature this.tryCreateColorTemperature(features, service); // Adaptive lighting this.tryCreateAdaptiveLighting(service); } get getableKeys() { const keys = []; if ((0, z2mModels_1.exposesCanBeGet)(this.stateExpose)) { keys.push(this.stateExpose.property); } if (this.brightnessExpose !== undefined && (0, z2mModels_1.exposesCanBeGet)(this.brightnessExpose)) { keys.push(this.brightnessExpose.property); } if (this.colorTempExpose !== undefined && (0, z2mModels_1.exposesCanBeGet)(this.colorTempExpose)) { keys.push(this.colorTempExpose.property); } if (this.colorExpose !== undefined && this.colorExpose.property !== undefined && ((this.colorComponentAExpose !== undefined && (0, z2mModels_1.exposesCanBeGet)(this.colorComponentAExpose)) || (this.colorComponentBExpose !== undefined && (0, z2mModels_1.exposesCanBeGet)(this.colorComponentBExpose)))) { keys.push(this.colorExpose.property); } return keys; } updateState(state) { // Use color_mode to filter out the non-active color information // to prevent "incorrect" updates (leading to "glitches" in the Home.app) if (this.accessory.isExperimentalFeatureEnabled(experimental_1.EXP_COLOR_MODE) && LightHandler.KEY_COLOR_MODE in state) { if (this.colorTempExpose !== undefined && this.colorTempExpose.property in state && state[LightHandler.KEY_COLOR_MODE] !== LightHandler.COLOR_MODE_TEMPERATURE) { // Color mode is NOT Color Temperature. Remove color temperature information. delete state[this.colorTempExpose.property]; } if (this.colorExpose !== undefined && this.colorExpose.property !== undefined && this.colorExpose.property in state && state[LightHandler.KEY_COLOR_MODE] === LightHandler.COLOR_MODE_TEMPERATURE) { // Color mode is Color Temperature. Remove HS/XY color information. delete state[this.colorExpose.property]; } } this.monitors.forEach((m) => m.callback(state)); } tryCreateAdaptiveLighting(service) { if (this.brightnessExpose === undefined || this.colorTempExpose === undefined || !this.accessory.isAdaptiveLightingEnabled()) { // Need at least brightness and color temperature to add Adaptive Lighting return; } this.adaptiveLighting = new hap_1.hap.AdaptiveLightingController(service).on('disable', this.resetAdaptiveLightingTemperature.bind(this)); this.accessory.configureController(this.adaptiveLighting); } resetAdaptiveLightingTemperature() { this.lastAdaptiveLightingTemperature = undefined; } tryCreateColor(expose, service) { // First see if color_hs is present this.colorExpose = expose.features.find((e) => (0, z2mModels_1.exposesHasFeatures)(e) && e.type === z2mModels_1.ExposesKnownTypes.COMPOSITE && e.name === 'color_hs' && e.property !== undefined); // Otherwise check for color_xy if (this.colorExpose === undefined) { this.colorExpose = expose.features.find((e) => (0, z2mModels_1.exposesHasFeatures)(e) && e.type === z2mModels_1.ExposesKnownTypes.COMPOSITE && e.name === 'color_xy' && e.property !== undefined); } if (this.colorExpose !== undefined && this.colorExpose.property !== undefined) { // Note: Components of color_xy and color_hs do not specify a range in zigbee-herdsman-converters const components = this.colorExpose.features .filter((e) => (0, z2mModels_1.exposesHasProperty)(e) && e.type === z2mModels_1.ExposesKnownTypes.NUMERIC) .map((e) => e); this.colorComponentAExpose = undefined; this.colorComponentBExpose = undefined; if (this.colorExpose.name === 'color_hs') { this.colorComponentAExpose = components.find((e) => e.name === 'hue'); this.colorComponentBExpose = components.find((e) => e.name === 'saturation'); } else if (this.colorExpose.name === 'color_xy') { this.colorComponentAExpose = components.find((e) => e.name === 'x'); this.colorComponentBExpose = components.find((e) => e.name === 'y'); } if (this.colorComponentAExpose === undefined || this.colorComponentBExpose === undefined) { // Can't create service if not all components are present. this.colorExpose = undefined; return; } this.colorHueCharacteristic = (0, helpers_1.getOrAddCharacteristic)(service, hap_1.hap.Characteristic.Hue).on('set', this.handleSetHue.bind(this)); this.colorSaturationCharacteristic = (0, helpers_1.getOrAddCharacteristic)(service, hap_1.hap.Characteristic.Saturation) .on('set', this.handleSetSaturation.bind(this)); if (this.colorExpose.name === 'color_hs') { this.monitors.push(new monitor_1.NestedCharacteristicMonitor(this.colorExpose.property, [ new monitor_1.PassthroughCharacteristicMonitor(this.colorComponentAExpose.property, service, hap_1.hap.Characteristic.Hue), new monitor_1.PassthroughCharacteristicMonitor(this.colorComponentBExpose.property, service, hap_1.hap.Characteristic.Saturation), ])); } else if (this.colorExpose.name === 'color_xy') { this.monitors.push(new ColorXyCharacteristicMonitor(service, this.colorExpose.property, this.colorComponentAExpose.property, this.colorComponentBExpose.property)); } } } tryCreateColorTemperature(features, service) { this.colorTempExpose = features.find((e) => e.name === 'color_temp' && (0, z2mModels_1.exposesHasNumericRangeProperty)(e) && (0, z2mModels_1.exposesCanBeSet)(e) && (0, z2mModels_1.exposesIsPublished)(e)); if (this.colorTempExpose !== undefined) { const characteristic = (0, helpers_1.getOrAddCharacteristic)(service, hap_1.hap.Characteristic.ColorTemperature); characteristic.setProps({ minValue: this.colorTempExpose.value_min, maxValue: this.colorTempExpose.value_max, minStep: 1, }); // Set default value characteristic.value = this.colorTempExpose.value_min; characteristic.on('set', this.handleSetColorTemperature.bind(this)); this.monitors.push(new monitor_1.NumericPassthroughCharacteristicMonitor(this.colorTempExpose.property, service, hap_1.hap.Characteristic.ColorTemperature, this.colorTempExpose.value_min, this.colorTempExpose.value_max)); // Also supports colors? if (this.accessory.isExperimentalFeatureEnabled(experimental_1.EXP_COLOR_MODE) && this.colorTempExpose !== undefined && this.colorExpose !== undefined) { // Add monitor to convert Color Temperature to Hue / Saturation // based on the 'color_mode' this.monitors.push(new ColorTemperatureToHueSatMonitor(service, this.colorTempExpose.property)); } } } tryCreateBrightness(features, service) { this.brightnessExpose = features.find((e) => e.name === 'brightness' && (0, z2mModels_1.exposesHasNumericRangeProperty)(e) && (0, z2mModels_1.exposesCanBeSet)(e) && (0, z2mModels_1.exposesIsPublished)(e)); if (this.brightnessExpose !== undefined) { (0, helpers_1.getOrAddCharacteristic)(service, hap_1.hap.Characteristic.Brightness).on('set', this.handleSetBrightness.bind(this)); this.monitors.push(new monitor_1.NumericCharacteristicMonitor(this.brightnessExpose.property, service, hap_1.hap.Characteristic.Brightness, this.brightnessExpose.value_min, this.brightnessExpose.value_max)); } } handleSetOn(value, callback) { const data = {}; data[this.stateExpose.property] = value ? this.stateExpose.value_on : this.stateExpose.value_off; this.accessory.queueDataForSetAction(data); callback(null); } handleSetBrightness(value, callback) { if (this.brightnessExpose !== undefined) { const data = {}; if (value <= 0) { data[this.brightnessExpose.property] = this.brightnessExpose.value_min; } else if (value >= 100) { data[this.brightnessExpose.property] = this.brightnessExpose.value_max; } else { data[this.brightnessExpose.property] = Math.round(this.brightnessExpose.value_min + (value / 100) * (this.brightnessExpose.value_max - this.brightnessExpose.value_min)); } this.accessory.queueDataForSetAction(data); callback(null); } else { callback(new Error('brightness not supported')); } } handleAdaptiveLighting(value, data) { // Adaptive Lighting active? if (this.adaptiveLighting !== undefined && this.adaptiveLighting.isAdaptiveLightingActive()) { if (this.lastAdaptiveLightingTemperature === undefined) { this.lastAdaptiveLightingTemperature = value; } else { const minChange = this.accessory.getAdaptiveLightingMinimumColorTemperatureChange(); const change = Math.abs(this.lastAdaptiveLightingTemperature - value); if (change < minChange) { this.accessory.log.debug(`Adaptive Lighting: Color temperature ${value} skipped for ${this.accessory.displayName}. ` + `Previous: ${this.lastAdaptiveLightingTemperature}`); return false; } const transition = this.accessory.getAdaptiveLightingTransitionTime(); if (transition > 0) { data[LightHandler.MQTT_PROPERTY_TRANSITION] = transition; } } } else { this.resetAdaptiveLightingTemperature(); } return true; } updateHueAndSaturationBasedOnColorTemperature(value) { if (this.colorHueCharacteristic !== undefined && this.colorSaturationCharacteristic !== undefined) { const color = hap_1.hap.ColorUtils.colorTemperatureToHueAndSaturation(value, true); this.colorHueCharacteristic.updateValue(color.hue); this.colorSaturationCharacteristic.updateValue(color.saturation); } } handleSetColorTemperature(value, callback) { if (this.colorTempExpose !== undefined && typeof value === 'number') { const data = {}; if (value < this.colorTempExpose.value_min) { value = this.colorTempExpose.value_min; } if (value > this.colorTempExpose.value_max) { value = this.colorTempExpose.value_max; } data[this.colorTempExpose.property] = value; if (this.handleAdaptiveLighting(value, data)) { this.accessory.queueDataForSetAction(data); } callback(null); } else { callback(new Error('color temperature not supported')); } } handleSetHue(value, callback) { var _a, _b; this.cached_hue = value; this.received_hue = true; if (((_a = this.colorExpose) === null || _a === void 0 ? void 0 : _a.name) === 'color_hs' && this.colorComponentAExpose !== undefined) { this.publishHueAndSaturation(); callback(null); } else if (((_b = this.colorExpose) === null || _b === void 0 ? void 0 : _b.name) === 'color_xy') { this.convertAndPublishHueAndSaturationAsXY(); callback(null); } else { callback(new Error('color not supported')); } } handleSetSaturation(value, callback) { var _a, _b; this.cached_saturation = value; this.received_saturation = true; if (((_a = this.colorExpose) === null || _a === void 0 ? void 0 : _a.name) === 'color_hs' && this.colorComponentBExpose !== undefined) { this.publishHueAndSaturation(); callback(null); } else if (((_b = this.colorExpose) === null || _b === void 0 ? void 0 : _b.name) === 'color_xy') { this.convertAndPublishHueAndSaturationAsXY(); callback(null); } else { callback(new Error('color not supported')); } } publishHueAndSaturation() { var _a, _b; try { if (this.received_hue && this.received_saturation) { this.received_hue = false; this.received_saturation = false; if (((_a = this.colorExpose) === null || _a === void 0 ? void 0 : _a.name) === 'color_hs' && ((_b = this.colorExpose) === null || _b === void 0 ? void 0 : _b.property) !== undefined && this.colorComponentAExpose !== undefined && this.colorComponentBExpose !== undefined) { const data = {}; data[this.colorExpose.property] = {}; data[this.colorExpose.property][this.colorComponentAExpose.property] = this.cached_hue; data[this.colorExpose.property][this.colorComponentBExpose.property] = this.cached_saturation; this.accessory.queueDataForSetAction(data); } } } catch (error) { this.accessory.log.error(`Failed to handle hue/saturation update for ${this.accessory.displayName}: ${error}`); } } convertAndPublishHueAndSaturationAsXY() { var _a, _b; try { if (this.received_hue && this.received_saturation) { this.received_hue = false; this.received_saturation = false; if (((_a = this.colorExpose) === null || _a === void 0 ? void 0 : _a.name) === 'color_xy' && ((_b = this.colorExpose) === null || _b === void 0 ? void 0 : _b.property) !== undefined && this.colorComponentAExpose !== undefined && this.colorComponentBExpose !== undefined) { const data = {}; const xy = (0, colorhelper_1.convertHueSatToXy)(this.cached_hue, this.cached_saturation); data[this.colorExpose.property] = {}; data[this.colorExpose.property][this.colorComponentAExpose.property] = xy[0]; data[this.colorExpose.property][this.colorComponentBExpose.property] = xy[1]; this.accessory.queueDataForSetAction(data); } } } catch (error) { this.accessory.log.error(`Failed to handle hue/saturation update for ${this.accessory.displayName}: ${error}`); } } static generateIdentifier(endpoint) { let identifier = hap_1.hap.Service.Lightbulb.UUID; if (endpoint !== undefined) { identifier += '_' + endpoint.trim(); } return identifier; } } LightHandler.PREDICATE_STATE = (e) => e.name === 'state' && (0, z2mModels_1.exposesIsPublished)(e) && (0, z2mModels_1.exposesCanBeSet)(e) && (0, z2mModels_1.exposesHasBinaryProperty)(e); LightHandler.KEY_COLOR_MODE = 'color_mode'; LightHandler.COLOR_MODE_TEMPERATURE = 'color_temp'; LightHandler.MINIMUM_HB_VERSION_ADAPTIVE_LIGHTING = '1.3.0-beta.46'; LightHandler.MQTT_PROPERTY_TRANSITION = 'transition'; class ColorTemperatureToHueSatMonitor { constructor(service, key_temp) { this.service = service; this.key_temp = key_temp; } callback(state) { if (this.key_temp in state && LightHandler.KEY_COLOR_MODE in state && state[LightHandler.KEY_COLOR_MODE] === LightHandler.COLOR_MODE_TEMPERATURE) { const temperature = state[this.key_temp]; const hueSat = (0, colorhelper_1.convertMiredColorTemperatureToHueSat)(temperature); this.service.updateCharacteristic(hap_1.hap.Characteristic.Hue, hueSat[0]); this.service.updateCharacteristic(hap_1.hap.Characteristic.Saturation, hueSat[1]); } } } class ColorXyCharacteristicMonitor { constructor(service, key, key_x, key_y) { this.service = service; this.key = key; this.key_x = key_x; this.key_y = key_y; } callback(state) { if (this.key in state) { const nested_state = state[this.key]; if (this.key_x in nested_state && this.key_y in nested_state) { const value_x = nested_state[this.key_x]; const value_y = nested_state[this.key_y]; const hueSat = (0, colorhelper_1.convertXyToHueSat)(value_x, value_y); this.service.updateCharacteristic(hap_1.hap.Characteristic.Hue, hueSat[0]); this.service.updateCharacteristic(hap_1.hap.Characteristic.Saturation, hueSat[1]); } } } } //# sourceMappingURL=light.js.map