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