homebridge-plugin-wrapper
Version:
Wrapper for Homebridge and NodeJS-HAP with reduced dependencies that allows to intercept plugin values and also send to them
786 lines • 54.3 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.AdaptiveLightingController = exports.AdaptiveLightingControllerEvents = exports.AdaptiveLightingControllerMode = void 0;
var tslib_1 = require("tslib");
var assert_1 = (0, tslib_1.__importDefault)(require("assert"));
var uuid = (0, tslib_1.__importStar)(require("../util/uuid"));
var debug_1 = (0, tslib_1.__importDefault)(require("debug"));
var events_1 = require("events");
var __1 = require("../..");
var Characteristic_1 = require("../Characteristic");
var tlv = (0, tslib_1.__importStar)(require("../util/tlv"));
var debug = (0, debug_1.default)("HAP-NodeJS:Controller:TransitionControl");
var SupportedCharacteristicValueTransitionConfigurationsTypes;
(function (SupportedCharacteristicValueTransitionConfigurationsTypes) {
SupportedCharacteristicValueTransitionConfigurationsTypes[SupportedCharacteristicValueTransitionConfigurationsTypes["SUPPORTED_TRANSITION_CONFIGURATION"] = 1] = "SUPPORTED_TRANSITION_CONFIGURATION";
})(SupportedCharacteristicValueTransitionConfigurationsTypes || (SupportedCharacteristicValueTransitionConfigurationsTypes = {}));
var SupportedValueTransitionConfigurationTypes;
(function (SupportedValueTransitionConfigurationTypes) {
SupportedValueTransitionConfigurationTypes[SupportedValueTransitionConfigurationTypes["CHARACTERISTIC_IID"] = 1] = "CHARACTERISTIC_IID";
SupportedValueTransitionConfigurationTypes[SupportedValueTransitionConfigurationTypes["TRANSITION_TYPE"] = 2] = "TRANSITION_TYPE";
})(SupportedValueTransitionConfigurationTypes || (SupportedValueTransitionConfigurationTypes = {}));
var TransitionType;
(function (TransitionType) {
TransitionType[TransitionType["BRIGHTNESS"] = 1] = "BRIGHTNESS";
TransitionType[TransitionType["COLOR_TEMPERATURE"] = 2] = "COLOR_TEMPERATURE";
})(TransitionType || (TransitionType = {}));
var TransitionControlTypes;
(function (TransitionControlTypes) {
TransitionControlTypes[TransitionControlTypes["READ_CURRENT_VALUE_TRANSITION_CONFIGURATION"] = 1] = "READ_CURRENT_VALUE_TRANSITION_CONFIGURATION";
TransitionControlTypes[TransitionControlTypes["UPDATE_VALUE_TRANSITION_CONFIGURATION"] = 2] = "UPDATE_VALUE_TRANSITION_CONFIGURATION";
})(TransitionControlTypes || (TransitionControlTypes = {}));
var ReadValueTransitionConfiguration;
(function (ReadValueTransitionConfiguration) {
ReadValueTransitionConfiguration[ReadValueTransitionConfiguration["CHARACTERISTIC_IID"] = 1] = "CHARACTERISTIC_IID";
})(ReadValueTransitionConfiguration || (ReadValueTransitionConfiguration = {}));
var UpdateValueTransitionConfigurationsTypes;
(function (UpdateValueTransitionConfigurationsTypes) {
UpdateValueTransitionConfigurationsTypes[UpdateValueTransitionConfigurationsTypes["VALUE_TRANSITION_CONFIGURATION"] = 1] = "VALUE_TRANSITION_CONFIGURATION";
})(UpdateValueTransitionConfigurationsTypes || (UpdateValueTransitionConfigurationsTypes = {}));
var ValueTransitionConfigurationTypes;
(function (ValueTransitionConfigurationTypes) {
// noinspection JSUnusedGlobalSymbols
ValueTransitionConfigurationTypes[ValueTransitionConfigurationTypes["CHARACTERISTIC_IID"] = 1] = "CHARACTERISTIC_IID";
ValueTransitionConfigurationTypes[ValueTransitionConfigurationTypes["TRANSITION_PARAMETERS"] = 2] = "TRANSITION_PARAMETERS";
ValueTransitionConfigurationTypes[ValueTransitionConfigurationTypes["UNKNOWN_3"] = 3] = "UNKNOWN_3";
ValueTransitionConfigurationTypes[ValueTransitionConfigurationTypes["UNKNOWN_4"] = 4] = "UNKNOWN_4";
ValueTransitionConfigurationTypes[ValueTransitionConfigurationTypes["TRANSITION_CURVE_CONFIGURATION"] = 5] = "TRANSITION_CURVE_CONFIGURATION";
ValueTransitionConfigurationTypes[ValueTransitionConfigurationTypes["UPDATE_INTERVAL"] = 6] = "UPDATE_INTERVAL";
ValueTransitionConfigurationTypes[ValueTransitionConfigurationTypes["UNKNOWN_7"] = 7] = "UNKNOWN_7";
ValueTransitionConfigurationTypes[ValueTransitionConfigurationTypes["NOTIFY_INTERVAL_THRESHOLD"] = 8] = "NOTIFY_INTERVAL_THRESHOLD";
})(ValueTransitionConfigurationTypes || (ValueTransitionConfigurationTypes = {}));
var ValueTransitionParametersTypes;
(function (ValueTransitionParametersTypes) {
ValueTransitionParametersTypes[ValueTransitionParametersTypes["TRANSITION_ID"] = 1] = "TRANSITION_ID";
ValueTransitionParametersTypes[ValueTransitionParametersTypes["START_TIME"] = 2] = "START_TIME";
ValueTransitionParametersTypes[ValueTransitionParametersTypes["UNKNOWN_3"] = 3] = "UNKNOWN_3";
})(ValueTransitionParametersTypes || (ValueTransitionParametersTypes = {}));
var TransitionCurveConfigurationTypes;
(function (TransitionCurveConfigurationTypes) {
TransitionCurveConfigurationTypes[TransitionCurveConfigurationTypes["TRANSITION_ENTRY"] = 1] = "TRANSITION_ENTRY";
TransitionCurveConfigurationTypes[TransitionCurveConfigurationTypes["ADJUSTMENT_CHARACTERISTIC_IID"] = 2] = "ADJUSTMENT_CHARACTERISTIC_IID";
TransitionCurveConfigurationTypes[TransitionCurveConfigurationTypes["ADJUSTMENT_MULTIPLIER_RANGE"] = 3] = "ADJUSTMENT_MULTIPLIER_RANGE";
})(TransitionCurveConfigurationTypes || (TransitionCurveConfigurationTypes = {}));
var TransitionEntryTypes;
(function (TransitionEntryTypes) {
TransitionEntryTypes[TransitionEntryTypes["ADJUSTMENT_FACTOR"] = 1] = "ADJUSTMENT_FACTOR";
TransitionEntryTypes[TransitionEntryTypes["VALUE"] = 2] = "VALUE";
TransitionEntryTypes[TransitionEntryTypes["TRANSITION_OFFSET"] = 3] = "TRANSITION_OFFSET";
TransitionEntryTypes[TransitionEntryTypes["DURATION"] = 4] = "DURATION";
})(TransitionEntryTypes || (TransitionEntryTypes = {}));
var TransitionAdjustmentMultiplierRange;
(function (TransitionAdjustmentMultiplierRange) {
TransitionAdjustmentMultiplierRange[TransitionAdjustmentMultiplierRange["MINIMUM_ADJUSTMENT_MULTIPLIER"] = 1] = "MINIMUM_ADJUSTMENT_MULTIPLIER";
TransitionAdjustmentMultiplierRange[TransitionAdjustmentMultiplierRange["MAXIMUM_ADJUSTMENT_MULTIPLIER"] = 2] = "MAXIMUM_ADJUSTMENT_MULTIPLIER";
})(TransitionAdjustmentMultiplierRange || (TransitionAdjustmentMultiplierRange = {}));
var ValueTransitionConfigurationResponseTypes;
(function (ValueTransitionConfigurationResponseTypes) {
ValueTransitionConfigurationResponseTypes[ValueTransitionConfigurationResponseTypes["VALUE_CONFIGURATION_STATUS"] = 1] = "VALUE_CONFIGURATION_STATUS";
})(ValueTransitionConfigurationResponseTypes || (ValueTransitionConfigurationResponseTypes = {}));
var ValueTransitionConfigurationStatusTypes;
(function (ValueTransitionConfigurationStatusTypes) {
ValueTransitionConfigurationStatusTypes[ValueTransitionConfigurationStatusTypes["CHARACTERISTIC_IID"] = 1] = "CHARACTERISTIC_IID";
ValueTransitionConfigurationStatusTypes[ValueTransitionConfigurationStatusTypes["TRANSITION_PARAMETERS"] = 2] = "TRANSITION_PARAMETERS";
ValueTransitionConfigurationStatusTypes[ValueTransitionConfigurationStatusTypes["TIME_SINCE_START"] = 3] = "TIME_SINCE_START";
})(ValueTransitionConfigurationStatusTypes || (ValueTransitionConfigurationStatusTypes = {}));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function isAdaptiveLightingContext(context) {
return context && "controller" in context;
}
/**
* Defines in which mode the {@link AdaptiveLightingController} will operate in.
*/
var AdaptiveLightingControllerMode;
(function (AdaptiveLightingControllerMode) {
/**
* In automatic mode pretty much everything from setup to transition scheduling is done by the controller.
*/
AdaptiveLightingControllerMode[AdaptiveLightingControllerMode["AUTOMATIC"] = 1] = "AUTOMATIC";
/**
* In manual mode setup is done by the controller but the actual transition must be done by the user.
* This is useful for lights which natively support transitions.
*/
AdaptiveLightingControllerMode[AdaptiveLightingControllerMode["MANUAL"] = 2] = "MANUAL";
})(AdaptiveLightingControllerMode = exports.AdaptiveLightingControllerMode || (exports.AdaptiveLightingControllerMode = {}));
var AdaptiveLightingControllerEvents;
(function (AdaptiveLightingControllerEvents) {
/**
* This event is called once a HomeKit controller enables Adaptive Lighting
* or a HomeHub sends a updated transition schedule for the next 24 hours.
* This is also called on startup when AdaptiveLighting was previously enabled.
*/
AdaptiveLightingControllerEvents["UPDATE"] = "update";
/**
* In yet unknown circumstances HomeKit may also send a dedicated disable command
* via the control point characteristic. You may want to handle that in manual mode as well.
* The current transition will still be associated with the controller object when this event is called.
*/
AdaptiveLightingControllerEvents["DISABLED"] = "disable";
})(AdaptiveLightingControllerEvents = exports.AdaptiveLightingControllerEvents || (exports.AdaptiveLightingControllerEvents = {}));
/**
* This class allows adding Adaptive Lighting support to Lightbulb services.
* The Lightbulb service MUST have the {@link Characteristic.ColorTemperature} characteristic AND
* the {@link Characteristic.Brightness} characteristic added.
* The light may also expose {@link Characteristic.Hue} and {@link Characteristic.Saturation} characteristics
* (though additional work is required to keep them in sync with the color temperature characteristic. see below)
*
* How Adaptive Lighting works:
* When enabling AdaptiveLighting the iDevice will send a transition schedule for the next 24 hours.
* This schedule will be renewed all 24 hours by a HomeHub in your home
* (updating the schedule according to your current day/night situation).
* Once enabled the lightbulb will execute the provided transitions. The color temperature value set is always
* dependent on the current brightness value. Meaning brighter light will be colder and darker light will be warmer.
* HomeKit considers Adaptive Lighting to be disabled as soon a write happens to either the
* Hue/Saturation or the ColorTemperature characteristics.
* The AdaptiveLighting state must persist across reboots.
*
* The AdaptiveLightingController can be operated in two modes: {@link AdaptiveLightingControllerMode.AUTOMATIC} and
* {@link AdaptiveLightingControllerMode.MANUAL} with AUTOMATIC being the default.
* The goal would be that the color transition is done DIRECTLY on the light itself, thus not creating any
* additional/heavy traffic on the network.
* So if your light hardware/API supports transitions please go the extra mile and use MANUAL mode.
*
*
*
* Below is an overview what you need to or consider when enabling AdaptiveLighting (categorized by mode).
* The {@link AdaptiveLightingControllerMode} can be defined with the second constructor argument.
*
* <b>AUTOMATIC (Default mode):</b>
*
* This is the easiest mode to setup and needs less to no work form your side for AdaptiveLighting to work.
* The AdaptiveLightingController will go through setup procedure with HomeKit and automatically update
* the color temperature characteristic base on the current transition schedule.
* It is also adjusting the color temperature when a write to the brightness characteristic happens.
* Additionally, it will also handle turning off AdaptiveLighting, when it detects a write happening to the
* ColorTemperature, Hue or Saturation characteristic (though it can only detect writes coming from HomeKit and
* can't detect changes done to the physical devices directly! See below).
*
* So what do you need to consider in automatic mode:
* - Brightness and ColorTemperature characteristics MUST be set up. Hue and Saturation may be added for color support.
* - Color temperature will be updated all 60 seconds by calling the SET handler of the ColorTemperature characteristic.
* So every transition behaves like a regular write to the ColorTemperature characteristic.
* - Every transition step is dependent on the current brightness value. Try to keep the internal cache updated
* as the controller won't call the GET handler every 60 seconds.
* (The cached brightness value is updated on SET/GET operations or by manually calling {@link Characteristic.updateValue}
* on the brightness characteristic).
* - Detecting changes on the lightbulb side:
* Any manual change to ColorTemperature or Hue/Saturation is considered as a signal to turn AdaptiveLighting off.
* In order to notify the AdaptiveLightingController of such an event happening OUTSIDE of HomeKit
* you must call {@link disableAdaptiveLighting} manually!
* - Be aware that even when the light is turned off the transition will continue to call the SET handler
* of the ColorTemperature characteristic.
* - When using Hue/Saturation:
* When using Hue/Saturation in combination with the ColorTemperature characteristic you need to update the
* respective other in a particular way depending on if being in "color mode" or "color temperature mode".
* When a write happens to Hue/Saturation characteristic in is advised to set the internal value of the
* ColorTemperature to the minimal (NOT RAISING an event).
* When a write happens to the ColorTemperature characteristic just MUST convert to a proper representation
* in hue and saturation values, with RAISING an event.
* As noted above you MUST NOT call the {@link Characteristic.setValue} method for this, as this will be considered
* a write to the characteristic and will turn off AdaptiveLighting. Instead, you should use
* {@link Characteristic.updateValue} for this.
* You can and SHOULD use the supplied utility method {@link ColorUtils.colorTemperatureToHueAndSaturation}
* for converting mired to hue and saturation values.
*
*
* <b>MANUAL mode:</b>
*
* Manual mode is recommended for any accessories which support transitions natively on the devices end.
* Like for example ZigBee lights which support sending transitions directly to the lightbulb which
* then get executed ON the lightbulb itself reducing unnecessary network traffic.
* Here is a quick overview what you have to consider to successfully implement AdaptiveLighting support.
* The AdaptiveLightingController will also in manual mode do all the setup procedure.
* It will also save the transition schedule to disk to keep AdaptiveLighting enabled across reboots.
* The "only" thing you have to do yourself is handling the actual transitions, check that event notifications
* are only sent in the defined interval threshold, adjust the color temperature when brightness is changed
* and signal that Adaptive Lighting should be disabled if ColorTemperature, Hue or Saturation is changed manually.
*
* First step is to setup up an event handler for the {@link AdaptiveLightingControllerEvents.UPDATE}, which is called
* when AdaptiveLighting is enabled, the HomeHub updates the schedule for the next 24 hours or AdaptiveLighting
* is restored from disk on startup.
* In the event handler you can get the current schedule via {@link AdaptiveLightingController.getAdaptiveLightingTransitionCurve},
* retrieve current intervals like {@link AdaptiveLightingController.getAdaptiveLightingUpdateInterval} or
* {@link AdaptiveLightingController.getAdaptiveLightingNotifyIntervalThreshold} and get the date in epoch millis
* when the current transition curve started using {@link AdaptiveLightingController.getAdaptiveLightingStartTimeOfTransition}.
* Additionally {@link AdaptiveLightingController.getAdaptiveLightingBrightnessMultiplierRange} can be used
* to get the valid range for the brightness value to calculate the brightness adjustment factor.
* The method {@link AdaptiveLightingController.isAdaptiveLightingActive} can be used to check if AdaptiveLighting is enabled.
* Besides, actually running the transition (see {@link AdaptiveLightingTransitionCurveEntry}) you must correctly update
* the color temperature when the brightness of the lightbulb changes (see {@link AdaptiveLightingTransitionCurveEntry.brightnessAdjustmentFactor}),
* and signal when AdaptiveLighting got disabled by calling {@link AdaptiveLightingController.disableAdaptiveLighting}
* when ColorTemperature, Hue or Saturation where changed manually.
* Lastly you should set up a event handler for the {@link AdaptiveLightingControllerEvents.DISABLED} event.
* In yet unknown circumstances HomeKit may also send a dedicated disable command via the control point characteristic.
* Be prepared to handle that.
*/
var AdaptiveLightingController = /** @class */ (function (_super) {
(0, tslib_1.__extends)(AdaptiveLightingController, _super);
/**
* Creates a new instance of the AdaptiveLightingController.
* Refer to the {@link AdaptiveLightingController} documentation on how to use it.
*
* @param service - The lightbulb to which Adaptive Lighting support should be added.
* @param options - Optional options to define the operating mode (automatic vs manual).
*/
function AdaptiveLightingController(service, options) {
var _a, _b;
var _this = _super.call(this) || this;
_this.didRunFirstInitializationStep = false;
_this.lastEventNotificationSent = 0;
_this.lastNotifiedTemperatureValue = 0;
_this.lastNotifiedSaturationValue = 0;
_this.lastNotifiedHueValue = 0;
_this.lightbulb = service;
_this.mode = (_a = options === null || options === void 0 ? void 0 : options.controllerMode) !== null && _a !== void 0 ? _a : 1 /* AUTOMATIC */;
_this.customTemperatureAdjustment = (_b = options === null || options === void 0 ? void 0 : options.customTemperatureAdjustment) !== null && _b !== void 0 ? _b : 0;
(0, assert_1.default)(_this.lightbulb.testCharacteristic(Characteristic_1.Characteristic.ColorTemperature), "Lightbulb must have the ColorTemperature characteristic added!");
(0, assert_1.default)(_this.lightbulb.testCharacteristic(Characteristic_1.Characteristic.Brightness), "Lightbulb must have the Brightness characteristic added!");
_this.adjustmentFactorChangedListener = _this.handleAdjustmentFactorChanged.bind(_this);
_this.characteristicManualWrittenChangeListener = _this.handleCharacteristicManualWritten.bind(_this);
return _this;
}
/**
* @private
*/
AdaptiveLightingController.prototype.controllerId = function () {
return "characteristic-transition" /* CHARACTERISTIC_TRANSITION */ + "-" + this.lightbulb.getServiceId();
};
// ----------- PUBLIC API START -----------
/**
* Returns if a Adaptive Lighting transition is currently active.
*/
AdaptiveLightingController.prototype.isAdaptiveLightingActive = function () {
return !!this.activeTransition;
};
/**
* This method can be called to manually disable the current active Adaptive Lighting transition.
* When using {@link AdaptiveLightingControllerMode.AUTOMATIC} you won't need to call this method.
* In {@link AdaptiveLightingControllerMode.MANUAL} you must call this method when Adaptive Lighting should be disabled.
* This is the case when the user manually changes the value of Hue, Saturation or ColorTemperature characteristics
* (or if any of those values is changed by physical interaction with the lightbulb).
*/
AdaptiveLightingController.prototype.disableAdaptiveLighting = function () {
var _a;
if (this.updateTimeout) {
clearTimeout(this.updateTimeout);
this.updateTimeout = undefined;
}
if (this.activeTransition) {
this.colorTemperatureCharacteristic.removeListener("change" /* CHANGE */, this.characteristicManualWrittenChangeListener);
this.brightnessCharacteristic.removeListener("change" /* CHANGE */, this.adjustmentFactorChangedListener);
if (this.hueCharacteristic) {
this.hueCharacteristic.removeListener("change" /* CHANGE */, this.characteristicManualWrittenChangeListener);
}
if (this.saturationCharacteristic) {
this.saturationCharacteristic.removeListener("change" /* CHANGE */, this.characteristicManualWrittenChangeListener);
}
this.activeTransition = undefined;
(_a = this.stateChangeDelegate) === null || _a === void 0 ? void 0 : _a.call(this);
}
this.colorTemperatureCharacteristic = undefined;
this.brightnessCharacteristic = undefined;
this.hueCharacteristic = undefined;
this.saturationCharacteristic = undefined;
this.lastTransitionPointInfo = undefined;
this.lastEventNotificationSent = 0;
this.lastNotifiedTemperatureValue = 0;
this.lastNotifiedSaturationValue = 0;
this.lastNotifiedHueValue = 0;
this.didRunFirstInitializationStep = false;
this.activeTransitionCount.sendEventNotification(0);
debug("[%s] Disabling adaptive lighting", this.lightbulb.displayName);
};
/**
* Returns the time where the current transition curve was started in epoch time millis.
* A transition curves is active for 24 hours typically and is renewed every 24 hours by a HomeHub.
* Additionally see {@link getAdaptiveLightingTimeOffset}.
*/
AdaptiveLightingController.prototype.getAdaptiveLightingStartTimeOfTransition = function () {
if (!this.activeTransition) {
throw new Error("There is no active transition!");
}
return this.activeTransition.transitionStartMillis;
};
/**
* It is not necessarily given, that we have the same time (or rather the correct time) as the HomeKit controller
* who set up the transition schedule.
* Thus we record the delta between our current time and the the time send with the setup request.
* <code>timeOffset</code> is defined as <code>Date.now() - getAdaptiveLightingStartTimeOfTransition();</code>.
* So in the case were we actually have a correct local time, it most likely will be positive (due to network latency).
* But of course it can also be negative.
*/
AdaptiveLightingController.prototype.getAdaptiveLightingTimeOffset = function () {
if (!this.activeTransition) {
throw new Error("There is no active transition!");
}
return this.activeTransition.timeMillisOffset;
};
AdaptiveLightingController.prototype.getAdaptiveLightingTransitionCurve = function () {
if (!this.activeTransition) {
throw new Error("There is no active transition!");
}
return this.activeTransition.transitionCurve;
};
AdaptiveLightingController.prototype.getAdaptiveLightingBrightnessMultiplierRange = function () {
if (!this.activeTransition) {
throw new Error("There is no active transition!");
}
return this.activeTransition.brightnessAdjustmentRange;
};
/**
* This method returns the interval (in milliseconds) in which the light should update its internal color temperature
* (aka changes it physical color).
* A lightbulb should ideally change this also when turned of in oder to have a smooth transition when turning the light on.
*
* Typically this evaluates to 60000 milliseconds (60 seconds).
*/
AdaptiveLightingController.prototype.getAdaptiveLightingUpdateInterval = function () {
if (!this.activeTransition) {
throw new Error("There is no active transition!");
}
return this.activeTransition.updateInterval;
};
/**
* Returns the minimum interval threshold (in milliseconds) a accessory may notify HomeKit controllers about a new
* color temperature value via event notifications (what happens when you call {@link Characteristic.updateValue}).
* Meaning the accessory should only send event notifications to subscribed HomeKit controllers at the specified interval.
*
* Typically this evaluates to 600000 milliseconds (10 minutes).
*/
AdaptiveLightingController.prototype.getAdaptiveLightingNotifyIntervalThreshold = function () {
if (!this.activeTransition) {
throw new Error("There is no active transition!");
}
return this.activeTransition.notifyIntervalThreshold;
};
// ----------- PUBLIC API END -----------
AdaptiveLightingController.prototype.handleActiveTransitionUpdated = function (calledFromDeserializer) {
var _a;
if (calledFromDeserializer === void 0) { calledFromDeserializer = false; }
if (!calledFromDeserializer) {
this.activeTransitionCount.sendEventNotification(1);
}
else {
this.activeTransitionCount.value = 1;
}
if (this.mode === 1 /* AUTOMATIC */) {
this.scheduleNextUpdate();
}
else if (this.mode === 2 /* MANUAL */) {
this.emit("update" /* UPDATE */);
}
else {
throw new Error("Unsupported adaptive lighting controller mode: " + this.mode);
}
if (!calledFromDeserializer) {
(_a = this.stateChangeDelegate) === null || _a === void 0 ? void 0 : _a.call(this);
}
};
AdaptiveLightingController.prototype.handleAdaptiveLightingEnabled = function () {
if (!this.activeTransition) {
throw new Error("There is no active transition!");
}
this.colorTemperatureCharacteristic = this.lightbulb.getCharacteristic(Characteristic_1.Characteristic.ColorTemperature);
this.brightnessCharacteristic = this.lightbulb.getCharacteristic(Characteristic_1.Characteristic.Brightness);
this.colorTemperatureCharacteristic.on("change" /* CHANGE */, this.characteristicManualWrittenChangeListener);
this.brightnessCharacteristic.on("change" /* CHANGE */, this.adjustmentFactorChangedListener);
if (this.lightbulb.testCharacteristic(Characteristic_1.Characteristic.Hue)) {
this.hueCharacteristic = this.lightbulb.getCharacteristic(Characteristic_1.Characteristic.Hue)
.on("change" /* CHANGE */, this.characteristicManualWrittenChangeListener);
}
if (this.lightbulb.testCharacteristic(Characteristic_1.Characteristic.Saturation)) {
this.saturationCharacteristic = this.lightbulb.getCharacteristic(Characteristic_1.Characteristic.Saturation)
.on("change" /* CHANGE */, this.characteristicManualWrittenChangeListener);
}
};
AdaptiveLightingController.prototype.handleAdaptiveLightingDisabled = function () {
if (this.mode === 2 /* MANUAL */ && this.activeTransition) { // only emit the event if a transition is actually enabled
this.emit("disable" /* DISABLED */);
}
this.disableAdaptiveLighting();
};
AdaptiveLightingController.prototype.handleAdjustmentFactorChanged = function (change) {
var _this = this;
if (change.newValue === change.oldValue) {
return;
}
// consider the following scenario:
// a HomeKit controller queries the light (meaning e.g. Brightness, Hue and Saturation characteristics).
// As of the implementation of the light the brightness characteristic get handler returns first
// (and returns a value different than the cached value).
// This change handler gets called and we will update the color temperature accordingly
// (which also adjusts the internal cached values for Hue and Saturation).
// After some short time the Hue or Saturation get handler return with the last known value to the plugin.
// As those values now differ from the cached values (we already updated) we get a call to handleCharacteristicManualWritten
// which again disables adaptive lighting.
if (change.reason === "read" /* READ */) {
// if the reason is a read request, we expect that Hue/Saturation are also read
// thus we postpone our update to ColorTemperature a bit.
// It doesn't ensure that those race conditions do not happen anymore, but with a 1s delay it reduces the possibility by a bit
setTimeout(function () {
if (!_this.activeTransition) {
return; // was disabled in the mean time
}
_this.scheduleNextUpdate(true);
}, 1000).unref();
}
else {
this.scheduleNextUpdate(true); // run a dry scheduleNextUpdate to adjust the colorTemperature using the new brightness value
}
};
/**
* This method is called when a change happens to the Hue/Saturation or ColorTemperature characteristic.
* When such a write happens (caused by the user changing the color/temperature) Adaptive Lighting must be disabled.
*
* @param change
*/
AdaptiveLightingController.prototype.handleCharacteristicManualWritten = function (change) {
if (change.reason === "write" /* WRITE */ && !(isAdaptiveLightingContext(change.context) && change.context.controller === this)) {
// we ignore write request which are the result of calls made to updateValue or sendEventNotification
// or the result of a changed value returned by a read handler
// or the change was done by the controller itself
debug("[%s] Received a manual write to an characteristic (newValue: %d, oldValue: %d, reason: %s). Thus disabling adaptive lighting!", this.lightbulb.displayName, change.newValue, change.oldValue, change.reason);
this.disableAdaptiveLighting();
}
};
/**
* Retrieve the {@link AdaptiveLightingTransitionPoint} for the current timestamp.
* Returns undefined if the current transition schedule reached its end.
*/
AdaptiveLightingController.prototype.getCurrentAdaptiveLightingTransitionPoint = function () {
var _a, _b, _c, _d, _e;
if (!this.activeTransition) {
throw new Error("Cannot calculate current transition point if no transition is active!");
}
// adjustedNow is the now() date corrected to the time of the initiating controller
var adjustedNow = Date.now() - this.activeTransition.timeMillisOffset;
// "offset" since the start of the transition schedule
var offset = adjustedNow - this.activeTransition.transitionStartMillis;
var i = (_b = (_a = this.lastTransitionPointInfo) === null || _a === void 0 ? void 0 : _a.curveIndex) !== null && _b !== void 0 ? _b : 0;
var lowerBoundTimeOffset = (_d = (_c = this.lastTransitionPointInfo) === null || _c === void 0 ? void 0 : _c.lowerBoundTimeOffset) !== null && _d !== void 0 ? _d : 0; // time offset to the lowerBound transition entry
var lowerBound = undefined;
var upperBound = undefined;
for (; i + 1 < this.activeTransition.transitionCurve.length; i++) {
var lowerBound0 = this.activeTransition.transitionCurve[i];
var upperBound0 = this.activeTransition.transitionCurve[i + 1];
var lowerBoundDuration = (_e = lowerBound0.duration) !== null && _e !== void 0 ? _e : 0;
lowerBoundTimeOffset += lowerBound0.transitionTime;
if (offset >= lowerBoundTimeOffset) {
if (offset <= lowerBoundTimeOffset + lowerBoundDuration + upperBound0.transitionTime) {
lowerBound = lowerBound0;
upperBound = upperBound0;
break;
}
}
else if (this.lastTransitionPointInfo) {
// if we reached here the entry in the transitionCurve we are searching for is somewhere before current i.
// This can only happen when we have a faulty lastTransitionPointInfo (otherwise we would start from i=0).
// Thus we try again by searching from i=0
this.lastTransitionPointInfo = undefined;
return this.getCurrentAdaptiveLightingTransitionPoint();
}
lowerBoundTimeOffset += lowerBoundDuration;
}
if (!lowerBound || !upperBound) {
this.lastTransitionPointInfo = undefined;
return undefined;
}
this.lastTransitionPointInfo = {
curveIndex: i,
// we need to subtract lowerBound.transitionTime. When we start the loop above
// with a saved transition point, we will always add lowerBound.transitionTime as first step.
// Otherwise our calculations are simply wrong.
lowerBoundTimeOffset: lowerBoundTimeOffset - lowerBound.transitionTime,
};
return {
lowerBoundTimeOffset: lowerBoundTimeOffset,
transitionOffset: offset - lowerBoundTimeOffset,
lowerBound: lowerBound,
upperBound: upperBound,
};
};
AdaptiveLightingController.prototype.scheduleNextUpdate = function (dryRun) {
var _this = this;
var _a, _b, _c, _d, _e;
if (dryRun === void 0) { dryRun = false; }
if (!this.activeTransition) {
throw new Error("tried scheduling transition when no transition was active!");
}
if (!dryRun) {
this.updateTimeout = undefined;
}
if (!this.didRunFirstInitializationStep) {
this.didRunFirstInitializationStep = true;
this.handleAdaptiveLightingEnabled();
}
var transitionPoint = this.getCurrentAdaptiveLightingTransitionPoint();
if (!transitionPoint) {
debug("[%s] Reached end of transition curve!", this.lightbulb.displayName);
if (!dryRun) {
// the transition schedule is only for 24 hours, we reached the end?
this.disableAdaptiveLighting();
}
return;
}
var lowerBound = transitionPoint.lowerBound;
var upperBound = transitionPoint.upperBound;
var interpolatedTemperature;
var interpolatedAdjustmentFactor;
if (lowerBound.duration && transitionPoint.transitionOffset <= lowerBound.duration) {
interpolatedTemperature = lowerBound.temperature;
interpolatedAdjustmentFactor = lowerBound.brightnessAdjustmentFactor;
}
else {
var timePercentage = (transitionPoint.transitionOffset - ((_a = lowerBound.duration) !== null && _a !== void 0 ? _a : 0)) / upperBound.transitionTime;
interpolatedTemperature = lowerBound.temperature + (upperBound.temperature - lowerBound.temperature) * timePercentage;
interpolatedAdjustmentFactor = lowerBound.brightnessAdjustmentFactor
+ (upperBound.brightnessAdjustmentFactor - lowerBound.brightnessAdjustmentFactor) * timePercentage;
}
var adjustmentMultiplier = Math.max(this.activeTransition.brightnessAdjustmentRange.minBrightnessValue, Math.min(this.activeTransition.brightnessAdjustmentRange.maxBrightnessValue, this.brightnessCharacteristic.value));
var temperature = Math.round(interpolatedTemperature + interpolatedAdjustmentFactor * adjustmentMultiplier);
// apply any manually applied temperature adjustments
temperature += this.customTemperatureAdjustment;
var min = (_c = (_b = this.colorTemperatureCharacteristic) === null || _b === void 0 ? void 0 : _b.props.minValue) !== null && _c !== void 0 ? _c : 140;
var max = (_e = (_d = this.colorTemperatureCharacteristic) === null || _d === void 0 ? void 0 : _d.props.maxValue) !== null && _e !== void 0 ? _e : 500;
temperature = Math.max(min, Math.min(max, temperature));
var color = __1.ColorUtils.colorTemperatureToHueAndSaturation(temperature);
debug("[%s] Next temperature value is %d (for brightness %d adj: %s)", this.lightbulb.displayName, temperature, adjustmentMultiplier, this.customTemperatureAdjustment);
var context = {
controller: this,
omitEventUpdate: true,
};
/*
* We set saturation and hue values BEFORE we call the ColorTemperature SET handler (via setValue).
* First thought was so the API user could get the values in the SET handler of the color temperature characteristic.
* Do this is probably not really elegant cause this would only work when Adaptive Lighting is turned on
* an the accessory MUST in any case update the Hue/Saturation values on a ColorTemperature write
* (obviously only if Hue/Saturation characteristics are added to the service).
*
* The clever thing about this though is that, that it prevents notifications from being sent for Hue and Saturation
* outside the specified notifyIntervalThreshold (see below where notifications are manually sent).
* As the dev will or must call something like updateValue to propagate the updated hue and saturation values
* to all HomeKit clients (so that the color is reflected in the UI), HAP-NodeJS won't send notifications
* as the values are the same.
* This of course only works if the plugin uses the exact same algorithm of "converting" the color temperature
* value to the hue and saturation representation.
*/
if (this.saturationCharacteristic) {
this.saturationCharacteristic.value = color.saturation;
}
if (this.hueCharacteristic) {
this.hueCharacteristic.value = color.hue;
}
this.colorTemperatureCharacteristic.handleSetRequest(temperature, undefined, context).catch(function (reason) {
debug("[%s] Failed to next adaptive lighting transition point: %d", _this.lightbulb.displayName, reason);
});
if (!this.activeTransition) {
console.warn("[" + this.lightbulb.displayName + "] Adaptive Lighting was probably disable my mistake by some call in " +
"the SET handler of the ColorTemperature characteristic! " +
"Please check that you don't call setValue/setCharacteristic on the Hue, Saturation or ColorTemperature characteristic!");
return;
}
var now = Date.now();
if (!dryRun && now - this.lastEventNotificationSent >= this.activeTransition.notifyIntervalThreshold) {
debug("[%s] Sending event notifications for current transition!", this.lightbulb.displayName);
this.lastEventNotificationSent = now;
var eventContext = {
controller: this,
};
if (this.lastNotifiedTemperatureValue !== temperature) {
this.colorTemperatureCharacteristic.sendEventNotification(temperature, eventContext);
this.lastNotifiedTemperatureValue = temperature;
}
if (this.saturationCharacteristic && this.lastNotifiedSaturationValue !== color.saturation) {
this.saturationCharacteristic.sendEventNotification(color.saturation, eventContext);
this.lastNotifiedSaturationValue = color.saturation;
}
if (this.hueCharacteristic && this.lastNotifiedHueValue !== color.hue) {
this.hueCharacteristic.sendEventNotification(color.hue, eventContext);
this.lastNotifiedHueValue = color.hue;
}
}
if (!dryRun) {
this.updateTimeout = setTimeout(this.scheduleNextUpdate.bind(this), this.activeTransition.updateInterval);
}
};
/**
* @private
*/
AdaptiveLightingController.prototype.constructServices = function () {
return {};
};
/**
* @private
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
AdaptiveLightingController.prototype.initWithServices = function (serviceMap) {
// do nothing
};
/**
* @private
*/
AdaptiveLightingController.prototype.configureServices = function () {
var _this = this;
this.supportedTransitionConfiguration = this.lightbulb.getCharacteristic(Characteristic_1.Characteristic.SupportedCharacteristicValueTransitionConfiguration);
this.transitionControl = this.lightbulb.getCharacteristic(Characteristic_1.Characteristic.CharacteristicValueTransitionControl)
.updateValue("");
this.activeTransitionCount = this.lightbulb.getCharacteristic(Characteristic_1.Characteristic.CharacteristicValueActiveTransitionCount)
.updateValue(0);
this.supportedTransitionConfiguration
.onGet(this.handleSupportedTransitionConfigurationRead.bind(this));
this.transitionControl
.onGet(function () {
return _this.buildTransitionControlResponseBuffer().toString("base64");
})
.onSet(function (value) {
try {
return _this.handleTransitionControlWrite(value);
}
catch (error) {
console.warn("[%s] DEBUG: '".concat(value, "'"));
console.warn("[%s] Encountered error on CharacteristicValueTransitionControl characteristic: " + error.stack);
_this.disableAdaptiveLighting();
throw new __1.HapStatusError(-70402 /* SERVICE_COMMUNICATION_FAILURE */);
}
});
};
/**
* @private
*/
AdaptiveLightingController.prototype.handleControllerRemoved = function () {
this.lightbulb.removeCharacteristic(this.supportedTransitionConfiguration);
this.lightbulb.removeCharacteristic(this.transitionControl);
this.lightbulb.removeCharacteristic(this.activeTransitionCount);
this.supportedTransitionConfiguration = undefined;
this.transitionControl = undefined;
this.activeTransitionCount = undefined;
this.removeAllListeners();
};
/**
* @private
*/
AdaptiveLightingController.prototype.handleFactoryReset = function () {
this.handleAdaptiveLightingDisabled();
};
/**
* @private
*/
AdaptiveLightingController.prototype.serialize = function () {
if (!this.activeTransition) {
return undefined;
}
return {
activeTransition: this.activeTransition,
};
};
/**
* @private
*/
AdaptiveLightingController.prototype.deserialize = function (serialized) {
this.activeTransition = serialized.activeTransition;
// Data migrations from beta builds
if (!this.activeTransition.transitionId) {
// @ts-expect-error: data migration from beta builds
this.activeTransition.transitionId = this.activeTransition.id1;
// @ts-expect-error: data migration from beta builds
delete this.activeTransition.id1;
}
if (!this.activeTransition.timeMillisOffset) { // compatibility to data produced by early betas
this.activeTransition.timeMillisOffset = 0;
}
this.handleActiveTransitionUpdated(true);
};
/**
* @private
*/
AdaptiveLightingController.prototype.setupStateChangeDelegate = function (delegate) {
this.stateChangeDelegate = delegate;
};
AdaptiveLightingController.prototype.handleSupportedTransitionConfigurationRead = function () {
var brightnessIID = this.lightbulb.getCharacteristic(Characteristic_1.Characteristic.Brightness).iid;
var temperatureIID = this.lightbulb.getCharacteristic(Characteristic_1.Characteristic.ColorTemperature).iid;
(0, assert_1.default)(brightnessIID, "iid for brightness characteristic is undefined");
(0, assert_1.default)(temperatureIID, "iid for temperature characteristic is undefined");
return tlv.encode(1 /* SUPPORTED_TRANSITION_CONFIGURATION */, [
tlv.encode(1 /* CHARACTERISTIC_IID */, tlv.writeVariableUIntLE(brightnessIID), 2 /* TRANSITION_TYPE */, 1 /* BRIGHTNESS */),
tlv.encode(1 /* CHARACTERISTIC_IID */, tlv.writeVariableUIntLE(temperatureIID), 2 /* TRANSITION_TYPE */, 2 /* COLOR_TEMPERATURE */),
]).toString("base64");
};
AdaptiveLightingController.prototype.buildTransitionControlResponseBuffer = function (time) {
if (!this.activeTransition) {
return Buffer.alloc(0);
}
var active = this.activeTransition;
var timeSinceStart = time !== null && time !== void 0 ? time : (Date.now() - active.timeMillisOffset - active.transitionStartMillis);
var timeSinceStartBuffer = tlv.writeVariableUIntLE(timeSinceStart);
var parameters = tlv.encode(1 /* TRANSITION_ID */, uuid.write(active.transitionId), 2 /* START_TIME */, Buffer.from(active.transitionStartBuffer, "hex"));
if (active.id3) {
parameters = Buffer.concat([
parameters,
tlv.encode(3 /* UNKNOWN_3 */, Buffer.from(active.id3, "hex")),
]);
}
var status = tlv.encode(1 /* CHARACTERISTIC_IID */, tlv.writeVariableUIntLE(active.iid), 2 /* TRANSITION_PARAMETERS */, parameters, 3 /* TIME_SINCE_START */, timeSinceStartBuffer);
return tlv.encode(1 /* VALUE_CONFIGURATION_STATUS */, status);
};
AdaptiveLightingController.prototype.handleTransitionControlWrite = function (value) {
if (typeof value !== "string") {
throw new __1.HapStatusError(-70410 /* INVALID_VALUE_IN_REQUEST */);
}
var tlvData = tlv.decode(Buffer.from(value, "base64"));
var responseBuffers = [];
var readTransition = tlvData[1 /* READ_CURRENT_VALUE_TRANSITION_CONFIGURATION */];
if (readTransition) {
var readTransitionResponse = this.handleTransitionControlReadTransition(readTransition);
if (readTransitionResponse) {
responseBuffers.push(readTransitionResponse);
}
}
var updateTransition = tlvData[2 /* UPDATE_VALUE_TRANSITION_CONFIGURATION */];
if (updateTransition) {
var updateTransitionResponse = this.handleTransitionControlUpdateTransition(updateTransition);
if (updateTransitionResponse) {
responseBuffers.push(updateTransitionResponse);
}
}
return Buffer.concat(responseBuffers).toString("base64");
};
AdaptiveLightingController.prototype.handleTransitionControlReadTransition = function (buffer) {
var readTransition = tlv.decode(buffer);
var iid = tlv.readVariableUIntLE(readTransition[1 /* CHARACTERISTIC_IID */]);
if (this.activeTransition) {
if (this.activeTransition.iid !== iid) {
console.warn("[" + this.lightbulb.displayName + "] iid of current adaptive lighting transition (" + this.activeTransition.iid
+ ") doesn't match the requested one " + iid);
throw new __1.HapStatusError(-70410 /* INVALID_VALUE_IN_REQUEST */);
}
var parameters = tlv.encode(1 /* TRANSITION_ID */, uuid.write(this.activeTransition.transitionId), 2 /* START_TIME */, Buffer.from(this.activeTransition.transitionStartBuffer, "hex"));
if (this.activeTransition.id3) {
parameters = Buffer.concat([
parameters,
tlv.encode(3 /* UNKNOWN_3 */, Buffer.from(this.activeTransition.id3, "hex")),
]);
}
return tlv.encode(1 /* READ_CURRENT_VALUE_TRANSITION_CONFIGURATION */, tlv.encode(1 /* CHARACTERISTIC_IID */, tlv.writeVariableUIntLE(this.activeTransition.iid), 2 /* TRANSITION_PARAMETERS */, parameters, 3 /* UNKNOWN_3 */, 1, 5 /* TRANSITION_CURVE_CONFIGURATION */, tlv.encode(1 /* TRANSITION_ENTRY */, this.activeTransition.transitionCurve.map(function (entry, index, array) {
var _a, _b;
var duration = (_b = (_a = array[index - 1]) === null || _a === void 0 ? void 0 : _a.duration) !== null && _b !== void 0 ? _b : 0; // we store stuff differently :sweat_smile:
return tlv.encode(1 /* ADJUSTMENT_FACTOR */, tlv.writeFloat32LE(entry.brightnessAdjustmentFactor), 2 /* VALUE */, tlv.writeFloat32LE(entry.temperature), 3 /* TRANSITION_OFFSET */, tlv.writeVariableUIntLE(entry.transitionTime), 4 /* DURATION */, tlv.writeVariableUIntLE(duration));
}), 2 /* ADJUSTMENT_CHARACTERISTIC_IID */, tlv.writeVariableUIntLE(this.activeTransition.brightnessCharacteristicIID), 3 /* ADJUSTMENT_MULTIPLIER_RANGE */, tlv.encode(1 /* MINIMUM_ADJUSTMENT_MULTIPLIER */, tlv.writeUInt32(this.activeTransition.brightnessAdjustmentRange.minBrightnessValue), 2 /* MAXIMUM_ADJUSTMENT_MULTIPLIER */, tlv.writeUInt32(this.activeTransition.brightnessAdjustmentRange.maxBrightnessValue))), 6 /* UPDATE_INTERVAL */, tlv.writeVariableUIntLE(this.activeTransition.updateInterval), 8 /* NOTIFY_INTERVAL_THRESHOLD */, tlv.writeVariableUIntLE(this.activeTransition.notifyIntervalThreshold)));
}
else {
return undefined; // returns empty string
}
};
AdaptiveLightingController.prototype.handleTransitionControlUpdateTransition = function (buffer) {
var e_1, _a;
var _b, _c;
var updateTransition = tlv.decode(buffer);
var transitionConfiguration = tlv.decode(updateTransition[1 /* VALUE_TRANSITION_CONFIGURATION */]);
var iid = tlv.readVariableUIntLE(transitionConfiguration[1 /* CHARACTERISTIC_IID */]);
if (!this.lightbulb.getCharacteristicByIID(iid)) {
throw new __1.HapStatusError(-70410 /* INVALID_VALUE_IN_REQUEST */);
}
var param3 = (_b = transitionConfiguration[3 /* UNKNOWN_3 */]) === null || _b === void 0 ? void 0 : _b.readUInt8(0); // when present it is always 1
if (!param3) { // if HomeKit just sends the iid, we consider that as "disable adaptive lighting" (assumption)
this.handleAdaptiveLightingDisabled();
return