UNPKG

54.5 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3exports.AdaptiveLightingController = exports.AdaptiveLightingControllerEvents = exports.AdaptiveLightingControllerMode = void 0;
4const tslib_1 = require("tslib");
5const assert_1 = tslib_1.__importDefault(require("assert"));
6const color_utils_1 = require("../util/color-utils");
7const hapStatusError_1 = require("../util/hapStatusError");
8const time_1 = require("../util/time");
9const uuid = tslib_1.__importStar(require("../util/uuid"));
10const debug_1 = tslib_1.__importDefault(require("debug"));
11const events_1 = require("events");
12const Characteristic_1 = require("../Characteristic");
13const tlv = tslib_1.__importStar(require("../util/tlv"));
14const debug = (0, debug_1.default)("HAP-NodeJS:Controller:TransitionControl");
15var SupportedCharacteristicValueTransitionConfigurationsTypes;
16(function (SupportedCharacteristicValueTransitionConfigurationsTypes) {
17 SupportedCharacteristicValueTransitionConfigurationsTypes[SupportedCharacteristicValueTransitionConfigurationsTypes["SUPPORTED_TRANSITION_CONFIGURATION"] = 1] = "SUPPORTED_TRANSITION_CONFIGURATION";
18})(SupportedCharacteristicValueTransitionConfigurationsTypes || (SupportedCharacteristicValueTransitionConfigurationsTypes = {}));
19var SupportedValueTransitionConfigurationTypes;
20(function (SupportedValueTransitionConfigurationTypes) {
21 SupportedValueTransitionConfigurationTypes[SupportedValueTransitionConfigurationTypes["CHARACTERISTIC_IID"] = 1] = "CHARACTERISTIC_IID";
22 SupportedValueTransitionConfigurationTypes[SupportedValueTransitionConfigurationTypes["TRANSITION_TYPE"] = 2] = "TRANSITION_TYPE";
23})(SupportedValueTransitionConfigurationTypes || (SupportedValueTransitionConfigurationTypes = {}));
24var TransitionType;
25(function (TransitionType) {
26 TransitionType[TransitionType["BRIGHTNESS"] = 1] = "BRIGHTNESS";
27 TransitionType[TransitionType["COLOR_TEMPERATURE"] = 2] = "COLOR_TEMPERATURE";
28})(TransitionType || (TransitionType = {}));
29var TransitionControlTypes;
30(function (TransitionControlTypes) {
31 TransitionControlTypes[TransitionControlTypes["READ_CURRENT_VALUE_TRANSITION_CONFIGURATION"] = 1] = "READ_CURRENT_VALUE_TRANSITION_CONFIGURATION";
32 TransitionControlTypes[TransitionControlTypes["UPDATE_VALUE_TRANSITION_CONFIGURATION"] = 2] = "UPDATE_VALUE_TRANSITION_CONFIGURATION";
33})(TransitionControlTypes || (TransitionControlTypes = {}));
34var ReadValueTransitionConfiguration;
35(function (ReadValueTransitionConfiguration) {
36 ReadValueTransitionConfiguration[ReadValueTransitionConfiguration["CHARACTERISTIC_IID"] = 1] = "CHARACTERISTIC_IID";
37})(ReadValueTransitionConfiguration || (ReadValueTransitionConfiguration = {}));
38var UpdateValueTransitionConfigurationsTypes;
39(function (UpdateValueTransitionConfigurationsTypes) {
40 UpdateValueTransitionConfigurationsTypes[UpdateValueTransitionConfigurationsTypes["VALUE_TRANSITION_CONFIGURATION"] = 1] = "VALUE_TRANSITION_CONFIGURATION";
41})(UpdateValueTransitionConfigurationsTypes || (UpdateValueTransitionConfigurationsTypes = {}));
42var ValueTransitionConfigurationTypes;
43(function (ValueTransitionConfigurationTypes) {
44 // noinspection JSUnusedGlobalSymbols
45 ValueTransitionConfigurationTypes[ValueTransitionConfigurationTypes["CHARACTERISTIC_IID"] = 1] = "CHARACTERISTIC_IID";
46 ValueTransitionConfigurationTypes[ValueTransitionConfigurationTypes["TRANSITION_PARAMETERS"] = 2] = "TRANSITION_PARAMETERS";
47 ValueTransitionConfigurationTypes[ValueTransitionConfigurationTypes["UNKNOWN_3"] = 3] = "UNKNOWN_3";
48 ValueTransitionConfigurationTypes[ValueTransitionConfigurationTypes["UNKNOWN_4"] = 4] = "UNKNOWN_4";
49 ValueTransitionConfigurationTypes[ValueTransitionConfigurationTypes["TRANSITION_CURVE_CONFIGURATION"] = 5] = "TRANSITION_CURVE_CONFIGURATION";
50 ValueTransitionConfigurationTypes[ValueTransitionConfigurationTypes["UPDATE_INTERVAL"] = 6] = "UPDATE_INTERVAL";
51 ValueTransitionConfigurationTypes[ValueTransitionConfigurationTypes["UNKNOWN_7"] = 7] = "UNKNOWN_7";
52 ValueTransitionConfigurationTypes[ValueTransitionConfigurationTypes["NOTIFY_INTERVAL_THRESHOLD"] = 8] = "NOTIFY_INTERVAL_THRESHOLD";
53})(ValueTransitionConfigurationTypes || (ValueTransitionConfigurationTypes = {}));
54var ValueTransitionParametersTypes;
55(function (ValueTransitionParametersTypes) {
56 ValueTransitionParametersTypes[ValueTransitionParametersTypes["TRANSITION_ID"] = 1] = "TRANSITION_ID";
57 ValueTransitionParametersTypes[ValueTransitionParametersTypes["START_TIME"] = 2] = "START_TIME";
58 ValueTransitionParametersTypes[ValueTransitionParametersTypes["UNKNOWN_3"] = 3] = "UNKNOWN_3";
59})(ValueTransitionParametersTypes || (ValueTransitionParametersTypes = {}));
60var TransitionCurveConfigurationTypes;
61(function (TransitionCurveConfigurationTypes) {
62 TransitionCurveConfigurationTypes[TransitionCurveConfigurationTypes["TRANSITION_ENTRY"] = 1] = "TRANSITION_ENTRY";
63 TransitionCurveConfigurationTypes[TransitionCurveConfigurationTypes["ADJUSTMENT_CHARACTERISTIC_IID"] = 2] = "ADJUSTMENT_CHARACTERISTIC_IID";
64 TransitionCurveConfigurationTypes[TransitionCurveConfigurationTypes["ADJUSTMENT_MULTIPLIER_RANGE"] = 3] = "ADJUSTMENT_MULTIPLIER_RANGE";
65})(TransitionCurveConfigurationTypes || (TransitionCurveConfigurationTypes = {}));
66var TransitionEntryTypes;
67(function (TransitionEntryTypes) {
68 TransitionEntryTypes[TransitionEntryTypes["ADJUSTMENT_FACTOR"] = 1] = "ADJUSTMENT_FACTOR";
69 TransitionEntryTypes[TransitionEntryTypes["VALUE"] = 2] = "VALUE";
70 TransitionEntryTypes[TransitionEntryTypes["TRANSITION_OFFSET"] = 3] = "TRANSITION_OFFSET";
71 TransitionEntryTypes[TransitionEntryTypes["DURATION"] = 4] = "DURATION";
72})(TransitionEntryTypes || (TransitionEntryTypes = {}));
73var TransitionAdjustmentMultiplierRange;
74(function (TransitionAdjustmentMultiplierRange) {
75 TransitionAdjustmentMultiplierRange[TransitionAdjustmentMultiplierRange["MINIMUM_ADJUSTMENT_MULTIPLIER"] = 1] = "MINIMUM_ADJUSTMENT_MULTIPLIER";
76 TransitionAdjustmentMultiplierRange[TransitionAdjustmentMultiplierRange["MAXIMUM_ADJUSTMENT_MULTIPLIER"] = 2] = "MAXIMUM_ADJUSTMENT_MULTIPLIER";
77})(TransitionAdjustmentMultiplierRange || (TransitionAdjustmentMultiplierRange = {}));
78var ValueTransitionConfigurationResponseTypes;
79(function (ValueTransitionConfigurationResponseTypes) {
80 ValueTransitionConfigurationResponseTypes[ValueTransitionConfigurationResponseTypes["VALUE_CONFIGURATION_STATUS"] = 1] = "VALUE_CONFIGURATION_STATUS";
81})(ValueTransitionConfigurationResponseTypes || (ValueTransitionConfigurationResponseTypes = {}));
82var ValueTransitionConfigurationStatusTypes;
83(function (ValueTransitionConfigurationStatusTypes) {
84 ValueTransitionConfigurationStatusTypes[ValueTransitionConfigurationStatusTypes["CHARACTERISTIC_IID"] = 1] = "CHARACTERISTIC_IID";
85 ValueTransitionConfigurationStatusTypes[ValueTransitionConfigurationStatusTypes["TRANSITION_PARAMETERS"] = 2] = "TRANSITION_PARAMETERS";
86 ValueTransitionConfigurationStatusTypes[ValueTransitionConfigurationStatusTypes["TIME_SINCE_START"] = 3] = "TIME_SINCE_START";
87})(ValueTransitionConfigurationStatusTypes || (ValueTransitionConfigurationStatusTypes = {}));
88// eslint-disable-next-line @typescript-eslint/no-explicit-any
89function isAdaptiveLightingContext(context) {
90 return context && "controller" in context;
91}
92/**
93 * Defines in which mode the {@link AdaptiveLightingController} will operate in.
94 * @group Adaptive Lighting
95 */
96var AdaptiveLightingControllerMode;
97(function (AdaptiveLightingControllerMode) {
98 /**
99 * In automatic mode pretty much everything from setup to transition scheduling is done by the controller.
100 */
101 AdaptiveLightingControllerMode[AdaptiveLightingControllerMode["AUTOMATIC"] = 1] = "AUTOMATIC";
102 /**
103 * In manual mode setup is done by the controller but the actual transition must be done by the user.
104 * This is useful for lights which natively support transitions.
105 */
106 AdaptiveLightingControllerMode[AdaptiveLightingControllerMode["MANUAL"] = 2] = "MANUAL";
107})(AdaptiveLightingControllerMode || (exports.AdaptiveLightingControllerMode = AdaptiveLightingControllerMode = {}));
108/**
109 * @group Adaptive Lighting
110 */
111var AdaptiveLightingControllerEvents;
112(function (AdaptiveLightingControllerEvents) {
113 /**
114 * This event is called once a HomeKit controller enables Adaptive Lighting
115 * or a HomeHub sends an updated transition schedule for the next 24 hours.
116 * This is also called on startup when AdaptiveLighting was previously enabled.
117 */
118 AdaptiveLightingControllerEvents["UPDATE"] = "update";
119 /**
120 * In yet unknown circumstances HomeKit may also send a dedicated disable command
121 * via the control point characteristic. You may want to handle that in manual mode as well.
122 * The current transition will still be associated with the controller object when this event is called.
123 */
124 AdaptiveLightingControllerEvents["DISABLED"] = "disable";
125})(AdaptiveLightingControllerEvents || (exports.AdaptiveLightingControllerEvents = AdaptiveLightingControllerEvents = {}));
126/**
127 * This class allows adding Adaptive Lighting support to Lightbulb services.
128 * The Lightbulb service MUST have the {@link Characteristic.ColorTemperature} characteristic AND
129 * the {@link Characteristic.Brightness} characteristic added.
130 * The light may also expose {@link Characteristic.Hue} and {@link Characteristic.Saturation} characteristics
131 * (though additional work is required to keep them in sync with the color temperature characteristic. see below)
132 *
133 * How Adaptive Lighting works:
134 * When enabling AdaptiveLighting the iDevice will send a transition schedule for the next 24 hours.
135 * This schedule will be renewed all 24 hours by a HomeHub in your home
136 * (updating the schedule according to your current day/night situation).
137 * Once enabled the lightbulb will execute the provided transitions. The color temperature value set is always
138 * dependent on the current brightness value. Meaning brighter light will be colder and darker light will be warmer.
139 * HomeKit considers Adaptive Lighting to be disabled as soon a write happens to either the
140 * Hue/Saturation or the ColorTemperature characteristics.
141 * The AdaptiveLighting state must persist across reboots.
142 *
143 * The AdaptiveLightingController can be operated in two modes: {@link AdaptiveLightingControllerMode.AUTOMATIC} and
144 * {@link AdaptiveLightingControllerMode.MANUAL} with AUTOMATIC being the default.
145 * The goal would be that the color transition is done DIRECTLY on the light itself, thus not creating any
146 * additional/heavy traffic on the network.
147 * So if your light hardware/API supports transitions please go the extra mile and use MANUAL mode.
148 *
149 *
150 *
151 * Below is an overview what you need to or consider when enabling AdaptiveLighting (categorized by mode).
152 * The {@link AdaptiveLightingControllerMode} can be defined with the second constructor argument.
153 *
154 * <b>AUTOMATIC (Default mode):</b>
155 *
156 * This is the easiest mode to setup and needs less to no work form your side for AdaptiveLighting to work.
157 * The AdaptiveLightingController will go through setup procedure with HomeKit and automatically update
158 * the color temperature characteristic base on the current transition schedule.
159 * It is also adjusting the color temperature when a write to the brightness characteristic happens.
160 * Additionally, it will also handle turning off AdaptiveLighting, when it detects a write happening to the
161 * ColorTemperature, Hue or Saturation characteristic (though it can only detect writes coming from HomeKit and
162 * can't detect changes done to the physical devices directly! See below).
163 *
164 * So what do you need to consider in automatic mode:
165 * - Brightness and ColorTemperature characteristics MUST be set up. Hue and Saturation may be added for color support.
166 * - Color temperature will be updated all 60 seconds by calling the SET handler of the ColorTemperature characteristic.
167 * So every transition behaves like a regular write to the ColorTemperature characteristic.
168 * - Every transition step is dependent on the current brightness value. Try to keep the internal cache updated
169 * as the controller won't call the GET handler every 60 seconds.
170 * (The cached brightness value is updated on SET/GET operations or by manually calling {@link Characteristic.updateValue}
171 * on the brightness characteristic).
172 * - Detecting changes on the lightbulb side:
173 * Any manual change to ColorTemperature or Hue/Saturation is considered as a signal to turn AdaptiveLighting off.
174 * In order to notify the AdaptiveLightingController of such an event happening OUTSIDE of HomeKit
175 * you must call {@link disableAdaptiveLighting} manually!
176 * - Be aware that even when the light is turned off the transition will continue to call the SET handler
177 * of the ColorTemperature characteristic.
178 * - When using Hue/Saturation:
179 * When using Hue/Saturation in combination with the ColorTemperature characteristic you need to update the
180 * respective other in a particular way depending on if being in "color mode" or "color temperature mode".
181 * When a write happens to Hue/Saturation characteristic in is advised to set the internal value of the
182 * ColorTemperature to the minimal (NOT RAISING an event).
183 * When a write happens to the ColorTemperature characteristic just MUST convert to a proper representation
184 * in hue and saturation values, with RAISING an event.
185 * As noted above you MUST NOT call the {@link Characteristic.setValue} method for this, as this will be considered
186 * a write to the characteristic and will turn off AdaptiveLighting. Instead, you should use
187 * {@link Characteristic.updateValue} for this.
188 * You can and SHOULD use the supplied utility method {@link ColorUtils.colorTemperatureToHueAndSaturation}
189 * for converting mired to hue and saturation values.
190 *
191 *
192 * <b>MANUAL mode:</b>
193 *
194 * Manual mode is recommended for any accessories which support transitions natively on the devices end.
195 * Like for example ZigBee lights which support sending transitions directly to the lightbulb which
196 * then get executed ON the lightbulb itself reducing unnecessary network traffic.
197 * Here is a quick overview what you have to consider to successfully implement AdaptiveLighting support.
198 * The AdaptiveLightingController will also in manual mode do all the setup procedure.
199 * It will also save the transition schedule to disk to keep AdaptiveLighting enabled across reboots.
200 * The "only" thing you have to do yourself is handling the actual transitions, check that event notifications
201 * are only sent in the defined interval threshold, adjust the color temperature when brightness is changed
202 * and signal that Adaptive Lighting should be disabled if ColorTemperature, Hue or Saturation is changed manually.
203 *
204 * First step is to setup up an event handler for the {@link AdaptiveLightingControllerEvents.UPDATE}, which is called
205 * when AdaptiveLighting is enabled, the HomeHub updates the schedule for the next 24 hours or AdaptiveLighting
206 * is restored from disk on startup.
207 * In the event handler you can get the current schedule via {@link AdaptiveLightingController.getAdaptiveLightingTransitionCurve},
208 * retrieve current intervals like {@link AdaptiveLightingController.getAdaptiveLightingUpdateInterval} or
209 * {@link AdaptiveLightingController.getAdaptiveLightingNotifyIntervalThreshold} and get the date in epoch millis
210 * when the current transition curve started using {@link AdaptiveLightingController.getAdaptiveLightingStartTimeOfTransition}.
211 * Additionally {@link AdaptiveLightingController.getAdaptiveLightingBrightnessMultiplierRange} can be used
212 * to get the valid range for the brightness value to calculate the brightness adjustment factor.
213 * The method {@link AdaptiveLightingController.isAdaptiveLightingActive} can be used to check if AdaptiveLighting is enabled.
214 * Besides, actually running the transition (see {@link AdaptiveLightingTransitionCurveEntry}) you must correctly update
215 * the color temperature when the brightness of the lightbulb changes (see {@link AdaptiveLightingTransitionCurveEntry.brightnessAdjustmentFactor}),
216 * and signal when AdaptiveLighting got disabled by calling {@link AdaptiveLightingController.disableAdaptiveLighting}
217 * when ColorTemperature, Hue or Saturation where changed manually.
218 * Lastly you should set up a event handler for the {@link AdaptiveLightingControllerEvents.DISABLED} event.
219 * In yet unknown circumstances HomeKit may also send a dedicated disable command via the control point characteristic.
220 * Be prepared to handle that.
221 *
222 * @group Adaptive Lighting
223 */
224// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
225class AdaptiveLightingController extends events_1.EventEmitter {
226 stateChangeDelegate;
227 lightbulb;
228 mode;
229 customTemperatureAdjustment;
230 adjustmentFactorChangedListener;
231 characteristicManualWrittenChangeListener;
232 supportedTransitionConfiguration;
233 transitionControl;
234 activeTransitionCount;
235 colorTemperatureCharacteristic;
236 brightnessCharacteristic;
237 saturationCharacteristic;
238 hueCharacteristic;
239 activeTransition;
240 didRunFirstInitializationStep = false;
241 updateTimeout;
242 lastTransitionPointInfo;
243 lastEventNotificationSent = 0;
244 lastNotifiedTemperatureValue = 0;
245 lastNotifiedSaturationValue = 0;
246 lastNotifiedHueValue = 0;
247 /**
248 * Creates a new instance of the AdaptiveLightingController.
249 * Refer to the {@link AdaptiveLightingController} documentation on how to use it.
250 *
251 * @param service - The lightbulb to which Adaptive Lighting support should be added.
252 * @param options - Optional options to define the operating mode (automatic vs manual).
253 */
254 constructor(service, options) {
255 super();
256 this.lightbulb = service;
257 this.mode = options?.controllerMode ?? 1 /* AdaptiveLightingControllerMode.AUTOMATIC */;
258 this.customTemperatureAdjustment = options?.customTemperatureAdjustment ?? 0;
259 (0, assert_1.default)(this.lightbulb.testCharacteristic(Characteristic_1.Characteristic.ColorTemperature), "Lightbulb must have the ColorTemperature characteristic added!");
260 (0, assert_1.default)(this.lightbulb.testCharacteristic(Characteristic_1.Characteristic.Brightness), "Lightbulb must have the Brightness characteristic added!");
261 this.adjustmentFactorChangedListener = this.handleAdjustmentFactorChanged.bind(this);
262 this.characteristicManualWrittenChangeListener = this.handleCharacteristicManualWritten.bind(this);
263 }
264 /**
265 * @private
266 */
267 controllerId() {
268 return "characteristic-transition" /* DefaultControllerType.CHARACTERISTIC_TRANSITION */ + "-" + this.lightbulb.getServiceId();
269 }
270 // ----------- PUBLIC API START -----------
271 /**
272 * Returns if a Adaptive Lighting transition is currently active.
273 */
274 isAdaptiveLightingActive() {
275 return !!this.activeTransition;
276 }
277 /**
278 * This method can be called to manually disable the current active Adaptive Lighting transition.
279 * When using {@link AdaptiveLightingControllerMode.AUTOMATIC} you won't need to call this method.
280 * In {@link AdaptiveLightingControllerMode.MANUAL} you must call this method when Adaptive Lighting should be disabled.
281 * This is the case when the user manually changes the value of Hue, Saturation or ColorTemperature characteristics
282 * (or if any of those values is changed by physical interaction with the lightbulb).
283 */
284 disableAdaptiveLighting() {
285 if (this.updateTimeout) {
286 clearTimeout(this.updateTimeout);
287 this.updateTimeout = undefined;
288 }
289 if (this.activeTransition) {
290 this.colorTemperatureCharacteristic?.removeListener("change" /* CharacteristicEventTypes.CHANGE */, this.characteristicManualWrittenChangeListener);
291 this.brightnessCharacteristic?.removeListener("change" /* CharacteristicEventTypes.CHANGE */, this.adjustmentFactorChangedListener);
292 this.hueCharacteristic?.removeListener("change" /* CharacteristicEventTypes.CHANGE */, this.characteristicManualWrittenChangeListener);
293 this.saturationCharacteristic?.removeListener("change" /* CharacteristicEventTypes.CHANGE */, this.characteristicManualWrittenChangeListener);
294 this.activeTransition = undefined;
295 this.stateChangeDelegate?.();
296 }
297 this.colorTemperatureCharacteristic = undefined;
298 this.brightnessCharacteristic = undefined;
299 this.hueCharacteristic = undefined;
300 this.saturationCharacteristic = undefined;
301 this.lastTransitionPointInfo = undefined;
302 this.lastEventNotificationSent = 0;
303 this.lastNotifiedTemperatureValue = 0;
304 this.lastNotifiedSaturationValue = 0;
305 this.lastNotifiedHueValue = 0;
306 this.didRunFirstInitializationStep = false;
307 this.activeTransitionCount?.sendEventNotification(0);
308 debug("[%s] Disabling adaptive lighting", this.lightbulb.displayName);
309 }
310 /**
311 * Returns the time where the current transition curve was started in epoch time millis.
312 * A transition curves is active for 24 hours typically and is renewed every 24 hours by a HomeHub.
313 * Additionally see {@link getAdaptiveLightingTimeOffset}.
314 */
315 getAdaptiveLightingStartTimeOfTransition() {
316 if (!this.activeTransition) {
317 throw new Error("There is no active transition!");
318 }
319 return this.activeTransition.transitionStartMillis;
320 }
321 /**
322 * It is not necessarily given, that we have the same time (or rather the correct time) as the HomeKit controller
323 * who set up the transition schedule.
324 * Thus we record the delta between our current time and the the time send with the setup request.
325 * <code>timeOffset</code> is defined as <code>Date.now() - getAdaptiveLightingStartTimeOfTransition();</code>.
326 * So in the case were we actually have a correct local time, it most likely will be positive (due to network latency).
327 * But of course it can also be negative.
328 */
329 getAdaptiveLightingTimeOffset() {
330 if (!this.activeTransition) {
331 throw new Error("There is no active transition!");
332 }
333 return this.activeTransition.timeMillisOffset;
334 }
335 getAdaptiveLightingTransitionCurve() {
336 if (!this.activeTransition) {
337 throw new Error("There is no active transition!");
338 }
339 return this.activeTransition.transitionCurve;
340 }
341 getAdaptiveLightingBrightnessMultiplierRange() {
342 if (!this.activeTransition) {
343 throw new Error("There is no active transition!");
344 }
345 return this.activeTransition.brightnessAdjustmentRange;
346 }
347 /**
348 * This method returns the interval (in milliseconds) in which the light should update its internal color temperature
349 * (aka changes it physical color).
350 * A lightbulb should ideally change this also when turned of in oder to have a smooth transition when turning the light on.
351 *
352 * Typically this evaluates to 60000 milliseconds (60 seconds).
353 */
354 getAdaptiveLightingUpdateInterval() {
355 if (!this.activeTransition) {
356 throw new Error("There is no active transition!");
357 }
358 return this.activeTransition.updateInterval;
359 }
360 /**
361 * Returns the minimum interval threshold (in milliseconds) a accessory may notify HomeKit controllers about a new
362 * color temperature value via event notifications (what happens when you call {@link Characteristic.updateValue}).
363 * Meaning the accessory should only send event notifications to subscribed HomeKit controllers at the specified interval.
364 *
365 * Typically this evaluates to 600000 milliseconds (10 minutes).
366 */
367 getAdaptiveLightingNotifyIntervalThreshold() {
368 if (!this.activeTransition) {
369 throw new Error("There is no active transition!");
370 }
371 return this.activeTransition.notifyIntervalThreshold;
372 }
373 // ----------- PUBLIC API END -----------
374 handleActiveTransitionUpdated(calledFromDeserializer = false) {
375 if (this.activeTransitionCount) {
376 if (!calledFromDeserializer) {
377 this.activeTransitionCount.sendEventNotification(1);
378 }
379 else {
380 this.activeTransitionCount.value = 1;
381 }
382 }
383 if (this.mode === 1 /* AdaptiveLightingControllerMode.AUTOMATIC */) {
384 this.scheduleNextUpdate();
385 }
386 else if (this.mode === 2 /* AdaptiveLightingControllerMode.MANUAL */) {
387 if (!this.activeTransition) {
388 throw new Error("There is no active transition!");
389 }
390 const update = {
391 transitionStartMillis: this.activeTransition.transitionStartMillis,
392 timeMillisOffset: this.activeTransition.timeMillisOffset,
393 transitionCurve: this.activeTransition.transitionCurve,
394 brightnessAdjustmentRange: this.activeTransition.brightnessAdjustmentRange,
395 updateInterval: this.activeTransition.updateInterval,
396 notifyIntervalThreshold: this.activeTransition.notifyIntervalThreshold,
397 };
398 this.emit("update" /* AdaptiveLightingControllerEvents.UPDATE */, update);
399 }
400 else {
401 throw new Error("Unsupported adaptive lighting controller mode: " + this.mode);
402 }
403 if (!calledFromDeserializer) {
404 this.stateChangeDelegate?.();
405 }
406 }
407 handleAdaptiveLightingEnabled() {
408 if (!this.activeTransition) {
409 throw new Error("There is no active transition!");
410 }
411 this.colorTemperatureCharacteristic = this.lightbulb.getCharacteristic(Characteristic_1.Characteristic.ColorTemperature);
412 this.brightnessCharacteristic = this.lightbulb.getCharacteristic(Characteristic_1.Characteristic.Brightness);
413 this.colorTemperatureCharacteristic.on("change" /* CharacteristicEventTypes.CHANGE */, this.characteristicManualWrittenChangeListener);
414 this.brightnessCharacteristic.on("change" /* CharacteristicEventTypes.CHANGE */, this.adjustmentFactorChangedListener);
415 if (this.lightbulb.testCharacteristic(Characteristic_1.Characteristic.Hue)) {
416 this.hueCharacteristic = this.lightbulb.getCharacteristic(Characteristic_1.Characteristic.Hue)
417 .on("change" /* CharacteristicEventTypes.CHANGE */, this.characteristicManualWrittenChangeListener);
418 }
419 if (this.lightbulb.testCharacteristic(Characteristic_1.Characteristic.Saturation)) {
420 this.saturationCharacteristic = this.lightbulb.getCharacteristic(Characteristic_1.Characteristic.Saturation)
421 .on("change" /* CharacteristicEventTypes.CHANGE */, this.characteristicManualWrittenChangeListener);
422 }
423 }
424 handleAdaptiveLightingDisabled() {
425 if (this.mode === 2 /* AdaptiveLightingControllerMode.MANUAL */ && this.activeTransition) { // only emit the event if a transition is actually enabled
426 this.emit("disable" /* AdaptiveLightingControllerEvents.DISABLED */);
427 }
428 this.disableAdaptiveLighting();
429 }
430 handleAdjustmentFactorChanged(change) {
431 if (change.newValue === change.oldValue) {
432 return;
433 }
434 // consider the following scenario:
435 // a HomeKit controller queries the light (meaning e.g. Brightness, Hue and Saturation characteristics).
436 // As of the implementation of the light the brightness characteristic get handler returns first
437 // (and returns a value different than the cached value).
438 // This change handler gets called and we will update the color temperature accordingly
439 // (which also adjusts the internal cached values for Hue and Saturation).
440 // After some short time the Hue or Saturation get handler return with the last known value to the plugin.
441 // As those values now differ from the cached values (we already updated) we get a call to handleCharacteristicManualWritten
442 // which again disables adaptive lighting.
443 if (change.reason === "read" /* ChangeReason.READ */) {
444 // if the reason is a read request, we expect that Hue/Saturation are also read
445 // thus we postpone our update to ColorTemperature a bit.
446 // It doesn't ensure that those race conditions do not happen anymore, but with a 1s delay it reduces the possibility by a bit
447 setTimeout(() => {
448 if (!this.activeTransition) {
449 return; // was disabled in the mean time
450 }
451 this.scheduleNextUpdate(true);
452 }, 1000).unref();
453 }
454 else {
455 this.scheduleNextUpdate(true); // run a dry scheduleNextUpdate to adjust the colorTemperature using the new brightness value
456 }
457 }
458 /**
459 * This method is called when a change happens to the Hue/Saturation or ColorTemperature characteristic.
460 * When such a write happens (caused by the user changing the color/temperature) Adaptive Lighting must be disabled.
461 *
462 * @param change
463 */
464 handleCharacteristicManualWritten(change) {
465 if (change.reason === "write" /* ChangeReason.WRITE */ && !(isAdaptiveLightingContext(change.context) && change.context.controller === this)) {
466 // we ignore write request which are the result of calls made to updateValue or sendEventNotification
467 // or the result of a changed value returned by a read handler
468 // or the change was done by the controller itself
469 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);
470 this.disableAdaptiveLighting();
471 }
472 }
473 /**
474 * Retrieve the {@link AdaptiveLightingTransitionPoint} for the current timestamp.
475 * Returns undefined if the current transition schedule reached its end.
476 */
477 getCurrentAdaptiveLightingTransitionPoint() {
478 if (!this.activeTransition) {
479 throw new Error("Cannot calculate current transition point if no transition is active!");
480 }
481 // adjustedNow is the now() date corrected to the time of the initiating controller
482 const adjustedNow = Date.now() - this.activeTransition.timeMillisOffset;
483 // "offset" since the start of the transition schedule
484 const offset = adjustedNow - this.activeTransition.transitionStartMillis;
485 let i = this.lastTransitionPointInfo?.curveIndex ?? 0;
486 let lowerBoundTimeOffset = this.lastTransitionPointInfo?.lowerBoundTimeOffset ?? 0; // time offset to the lowerBound transition entry
487 let lowerBound = undefined;
488 let upperBound = undefined;
489 for (; i + 1 < this.activeTransition.transitionCurve.length; i++) {
490 const lowerBound0 = this.activeTransition.transitionCurve[i];
491 const upperBound0 = this.activeTransition.transitionCurve[i + 1];
492 const lowerBoundDuration = lowerBound0.duration ?? 0;
493 lowerBoundTimeOffset += lowerBound0.transitionTime;
494 if (offset >= lowerBoundTimeOffset) {
495 if (offset <= lowerBoundTimeOffset + lowerBoundDuration + upperBound0.transitionTime) {
496 lowerBound = lowerBound0;
497 upperBound = upperBound0;
498 break;
499 }
500 }
501 else if (this.lastTransitionPointInfo) {
502 // if we reached here the entry in the transitionCurve we are searching for is somewhere before current i.
503 // This can only happen when we have a faulty lastTransitionPointInfo (otherwise we would start from i=0).
504 // Thus we try again by searching from i=0
505 this.lastTransitionPointInfo = undefined;
506 return this.getCurrentAdaptiveLightingTransitionPoint();
507 }
508 lowerBoundTimeOffset += lowerBoundDuration;
509 }
510 if (!lowerBound || !upperBound) {
511 this.lastTransitionPointInfo = undefined;
512 return undefined;
513 }
514 this.lastTransitionPointInfo = {
515 curveIndex: i,
516 // we need to subtract lowerBound.transitionTime. When we start the loop above
517 // with a saved transition point, we will always add lowerBound.transitionTime as first step.
518 // Otherwise our calculations are simply wrong.
519 lowerBoundTimeOffset: lowerBoundTimeOffset - lowerBound.transitionTime,
520 };
521 return {
522 lowerBoundTimeOffset: lowerBoundTimeOffset,
523 transitionOffset: offset - lowerBoundTimeOffset,
524 lowerBound: lowerBound,
525 upperBound: upperBound,
526 };
527 }
528 scheduleNextUpdate(dryRun = false) {
529 if (!this.activeTransition) {
530 throw new Error("tried scheduling transition when no transition was active!");
531 }
532 if (!dryRun) {
533 this.updateTimeout = undefined;
534 }
535 if (!this.didRunFirstInitializationStep) {
536 this.didRunFirstInitializationStep = true;
537 this.handleAdaptiveLightingEnabled();
538 }
539 const transitionPoint = this.getCurrentAdaptiveLightingTransitionPoint();
540 if (!transitionPoint) {
541 debug("[%s] Reached end of transition curve!", this.lightbulb.displayName);
542 if (!dryRun) {
543 // the transition schedule is only for 24 hours, we reached the end?
544 this.disableAdaptiveLighting();
545 }
546 return;
547 }
548 const lowerBound = transitionPoint.lowerBound;
549 const upperBound = transitionPoint.upperBound;
550 let interpolatedTemperature;
551 let interpolatedAdjustmentFactor;
552 if (lowerBound.duration && transitionPoint.transitionOffset <= lowerBound.duration) {
553 interpolatedTemperature = lowerBound.temperature;
554 interpolatedAdjustmentFactor = lowerBound.brightnessAdjustmentFactor;
555 }
556 else {
557 const timePercentage = (transitionPoint.transitionOffset - (lowerBound.duration ?? 0)) / upperBound.transitionTime;
558 interpolatedTemperature = lowerBound.temperature + (upperBound.temperature - lowerBound.temperature) * timePercentage;
559 interpolatedAdjustmentFactor = lowerBound.brightnessAdjustmentFactor
560 + (upperBound.brightnessAdjustmentFactor - lowerBound.brightnessAdjustmentFactor) * timePercentage;
561 }
562 const adjustmentMultiplier = Math.max(this.activeTransition.brightnessAdjustmentRange.minBrightnessValue, Math.min(this.activeTransition.brightnessAdjustmentRange.maxBrightnessValue, this.brightnessCharacteristic?.value));
563 let temperature = Math.round(interpolatedTemperature + interpolatedAdjustmentFactor * adjustmentMultiplier);
564 // apply any manually applied temperature adjustments
565 temperature += this.customTemperatureAdjustment;
566 const min = this.colorTemperatureCharacteristic?.props.minValue ?? 140;
567 const max = this.colorTemperatureCharacteristic?.props.maxValue ?? 500;
568 temperature = Math.max(min, Math.min(max, temperature));
569 const color = color_utils_1.ColorUtils.colorTemperatureToHueAndSaturation(temperature);
570 debug("[%s] Next temperature value is %d (for brightness %d adj: %s)", this.lightbulb.displayName, temperature, adjustmentMultiplier, this.customTemperatureAdjustment);
571 const context = {
572 controller: this,
573 omitEventUpdate: true,
574 };
575 /*
576 * We set saturation and hue values BEFORE we call the ColorTemperature SET handler (via setValue).
577 * First thought was so the API user could get the values in the SET handler of the color temperature characteristic.
578 * Do this is probably not really elegant cause this would only work when Adaptive Lighting is turned on
579 * an the accessory MUST in any case update the Hue/Saturation values on a ColorTemperature write
580 * (obviously only if Hue/Saturation characteristics are added to the service).
581 *
582 * The clever thing about this though is that, that it prevents notifications from being sent for Hue and Saturation
583 * outside the specified notifyIntervalThreshold (see below where notifications are manually sent).
584 * As the dev will or must call something like updateValue to propagate the updated hue and saturation values
585 * to all HomeKit clients (so that the color is reflected in the UI), HAP-NodeJS won't send notifications
586 * as the values are the same.
587 * This of course only works if the plugin uses the exact same algorithm of "converting" the color temperature
588 * value to the hue and saturation representation.
589 */
590 if (this.saturationCharacteristic) {
591 this.saturationCharacteristic.value = color.saturation;
592 }
593 if (this.hueCharacteristic) {
594 this.hueCharacteristic.value = color.hue;
595 }
596 this.colorTemperatureCharacteristic?.handleSetRequest(temperature, undefined, context).catch(reason => {
597 debug("[%s] Failed to next adaptive lighting transition point: %d", this.lightbulb.displayName, reason);
598 });
599 if (!this.activeTransition) {
600 console.warn("[" + this.lightbulb.displayName + "] Adaptive Lighting was probably disable my mistake by some call in " +
601 "the SET handler of the ColorTemperature characteristic! " +
602 "Please check that you don't call setValue/setCharacteristic on the Hue, Saturation or ColorTemperature characteristic!");
603 return;
604 }
605 const now = Date.now();
606 if (!dryRun && now - this.lastEventNotificationSent >= this.activeTransition.notifyIntervalThreshold) {
607 debug("[%s] Sending event notifications for current transition!", this.lightbulb.displayName);
608 this.lastEventNotificationSent = now;
609 const eventContext = {
610 controller: this,
611 };
612 if (this.lastNotifiedTemperatureValue !== temperature) {
613 this.colorTemperatureCharacteristic?.sendEventNotification(temperature, eventContext);
614 this.lastNotifiedTemperatureValue = temperature;
615 }
616 if (this.saturationCharacteristic && this.lastNotifiedSaturationValue !== color.saturation) {
617 this.saturationCharacteristic.sendEventNotification(color.saturation, eventContext);
618 this.lastNotifiedSaturationValue = color.saturation;
619 }
620 if (this.hueCharacteristic && this.lastNotifiedHueValue !== color.hue) {
621 this.hueCharacteristic.sendEventNotification(color.hue, eventContext);
622 this.lastNotifiedHueValue = color.hue;
623 }
624 }
625 if (!dryRun) {
626 this.updateTimeout = setTimeout(this.scheduleNextUpdate.bind(this), this.activeTransition.updateInterval);
627 }
628 }
629 /**
630 * @private
631 */
632 constructServices() {
633 return {};
634 }
635 /**
636 * @private
637 */
638 // eslint-disable-next-line @typescript-eslint/no-unused-vars
639 initWithServices(serviceMap) {
640 // do nothing
641 }
642 /**
643 * @private
644 */
645 configureServices() {
646 this.supportedTransitionConfiguration = this.lightbulb.getCharacteristic(Characteristic_1.Characteristic.SupportedCharacteristicValueTransitionConfiguration);
647 this.transitionControl = this.lightbulb.getCharacteristic(Characteristic_1.Characteristic.CharacteristicValueTransitionControl)
648 .updateValue("");
649 this.activeTransitionCount = this.lightbulb.getCharacteristic(Characteristic_1.Characteristic.CharacteristicValueActiveTransitionCount)
650 .updateValue(0);
651 this.supportedTransitionConfiguration
652 .onGet(this.handleSupportedTransitionConfigurationRead.bind(this));
653 this.transitionControl
654 .onGet(() => {
655 return this.buildTransitionControlResponseBuffer().toString("base64");
656 })
657 .onSet(value => {
658 try {
659 return this.handleTransitionControlWrite(value);
660 }
661 catch (error) {
662 console.warn(`[%s] DEBUG: '${value}'`);
663 console.warn("[%s] Encountered error on CharacteristicValueTransitionControl characteristic: " + error.stack);
664 this.disableAdaptiveLighting();
665 throw new hapStatusError_1.HapStatusError(-70402 /* HAPStatus.SERVICE_COMMUNICATION_FAILURE */);
666 }
667 });
668 }
669 /**
670 * @private
671 */
672 handleControllerRemoved() {
673 this.lightbulb.removeCharacteristic(this.supportedTransitionConfiguration);
674 this.lightbulb.removeCharacteristic(this.transitionControl);
675 this.lightbulb.removeCharacteristic(this.activeTransitionCount);
676 this.supportedTransitionConfiguration = undefined;
677 this.transitionControl = undefined;
678 this.activeTransitionCount = undefined;
679 this.removeAllListeners();
680 }
681 /**
682 * @private
683 */
684 handleFactoryReset() {
685 this.handleAdaptiveLightingDisabled();
686 }
687 /**
688 * @private
689 */
690 serialize() {
691 if (!this.activeTransition) {
692 return undefined;
693 }
694 return {
695 activeTransition: this.activeTransition,
696 };
697 }
698 /**
699 * @private
700 */
701 deserialize(serialized) {
702 this.activeTransition = serialized.activeTransition;
703 // Data migrations from beta builds
704 if (!this.activeTransition.transitionId) {
705 // @ts-expect-error: data migration from beta builds
706 this.activeTransition.transitionId = this.activeTransition.id1;
707 // @ts-expect-error: data migration from beta builds
708 delete this.activeTransition.id1;
709 }
710 if (!this.activeTransition.timeMillisOffset) { // compatibility to data produced by early betas
711 this.activeTransition.timeMillisOffset = 0;
712 }
713 this.handleActiveTransitionUpdated(true);
714 }
715 /**
716 * @private
717 */
718 setupStateChangeDelegate(delegate) {
719 this.stateChangeDelegate = delegate;
720 }
721 handleSupportedTransitionConfigurationRead() {
722 const brightnessIID = this.lightbulb?.getCharacteristic(Characteristic_1.Characteristic.Brightness).iid;
723 const temperatureIID = this.lightbulb?.getCharacteristic(Characteristic_1.Characteristic.ColorTemperature).iid;
724 (0, assert_1.default)(brightnessIID, "iid for brightness characteristic is undefined");
725 (0, assert_1.default)(temperatureIID, "iid for temperature characteristic is undefined");
726 return tlv.encode(1 /* SupportedCharacteristicValueTransitionConfigurationsTypes.SUPPORTED_TRANSITION_CONFIGURATION */, [
727 tlv.encode(1 /* SupportedValueTransitionConfigurationTypes.CHARACTERISTIC_IID */, tlv.writeVariableUIntLE(brightnessIID), 2 /* SupportedValueTransitionConfigurationTypes.TRANSITION_TYPE */, 1 /* TransitionType.BRIGHTNESS */),
728 tlv.encode(1 /* SupportedValueTransitionConfigurationTypes.CHARACTERISTIC_IID */, tlv.writeVariableUIntLE(temperatureIID), 2 /* SupportedValueTransitionConfigurationTypes.TRANSITION_TYPE */, 2 /* TransitionType.COLOR_TEMPERATURE */),
729 ]).toString("base64");
730 }
731 buildTransitionControlResponseBuffer(time) {
732 if (!this.activeTransition) {
733 return Buffer.alloc(0);
734 }
735 const active = this.activeTransition;
736 const timeSinceStart = time ?? (Date.now() - active.timeMillisOffset - active.transitionStartMillis);
737 const timeSinceStartBuffer = tlv.writeVariableUIntLE(timeSinceStart);
738 let parameters = tlv.encode(1 /* ValueTransitionParametersTypes.TRANSITION_ID */, uuid.write(active.transitionId), 2 /* ValueTransitionParametersTypes.START_TIME */, Buffer.from(active.transitionStartBuffer, "hex"));
739 if (active.id3) {
740 parameters = Buffer.concat([
741 parameters,
742 tlv.encode(3 /* ValueTransitionParametersTypes.UNKNOWN_3 */, Buffer.from(active.id3, "hex")),
743 ]);
744 }
745 const status = tlv.encode(1 /* ValueTransitionConfigurationStatusTypes.CHARACTERISTIC_IID */, tlv.writeVariableUIntLE(active.iid), 2 /* ValueTransitionConfigurationStatusTypes.TRANSITION_PARAMETERS */, parameters, 3 /* ValueTransitionConfigurationStatusTypes.TIME_SINCE_START */, timeSinceStartBuffer);
746 return tlv.encode(1 /* ValueTransitionConfigurationResponseTypes.VALUE_CONFIGURATION_STATUS */, status);
747 }
748 handleTransitionControlWrite(value) {
749 if (typeof value !== "string") {
750 throw new hapStatusError_1.HapStatusError(-70410 /* HAPStatus.INVALID_VALUE_IN_REQUEST */);
751 }
752 const tlvData = tlv.decode(Buffer.from(value, "base64"));
753 const responseBuffers = [];
754 const readTransition = tlvData[1 /* TransitionControlTypes.READ_CURRENT_VALUE_TRANSITION_CONFIGURATION */];
755 if (readTransition) {
756 const readTransitionResponse = this.handleTransitionControlReadTransition(readTransition);
757 if (readTransitionResponse) {
758 responseBuffers.push(readTransitionResponse);
759 }
760 }
761 const updateTransition = tlvData[2 /* TransitionControlTypes.UPDATE_VALUE_TRANSITION_CONFIGURATION */];
762 if (updateTransition) {
763 const updateTransitionResponse = this.handleTransitionControlUpdateTransition(updateTransition);
764 if (updateTransitionResponse) {
765 responseBuffers.push(updateTransitionResponse);
766 }
767 }
768 return Buffer.concat(responseBuffers).toString("base64");
769 }
770 handleTransitionControlReadTransition(buffer) {
771 const readTransition = tlv.decode(buffer);
772 const iid = tlv.readVariableUIntLE(readTransition[1 /* ReadValueTransitionConfiguration.CHARACTERISTIC_IID */]);
773 if (this.activeTransition) {
774 if (this.activeTransition.iid !== iid) {
775 console.warn("[" + this.lightbulb.displayName + "] iid of current adaptive lighting transition (" + this.activeTransition.iid
776 + ") doesn't match the requested one " + iid);
777 throw new hapStatusError_1.HapStatusError(-70410 /* HAPStatus.INVALID_VALUE_IN_REQUEST */);
778 }
779 let parameters = tlv.encode(1 /* ValueTransitionParametersTypes.TRANSITION_ID */, uuid.write(this.activeTransition.transitionId), 2 /* ValueTransitionParametersTypes.START_TIME */, Buffer.from(this.activeTransition.transitionStartBuffer, "hex"));
780 if (this.activeTransition.id3) {
781 parameters = Buffer.concat([
782 parameters,
783 tlv.encode(3 /* ValueTransitionParametersTypes.UNKNOWN_3 */, Buffer.from(this.activeTransition.id3, "hex")),
784 ]);
785 }
786 return tlv.encode(1 /* TransitionControlTypes.READ_CURRENT_VALUE_TRANSITION_CONFIGURATION */, tlv.encode(1 /* ValueTransitionConfigurationTypes.CHARACTERISTIC_IID */, tlv.writeVariableUIntLE(this.activeTransition.iid), 2 /* ValueTransitionConfigurationTypes.TRANSITION_PARAMETERS */, parameters, 3 /* ValueTransitionConfigurationTypes.UNKNOWN_3 */, 1, 5 /* ValueTransitionConfigurationTypes.TRANSITION_CURVE_CONFIGURATION */, tlv.encode(1 /* TransitionCurveConfigurationTypes.TRANSITION_ENTRY */, this.activeTransition.transitionCurve.map((entry, index, array) => {
787 const duration = array[index - 1]?.duration ?? 0; // we store stuff differently :sweat_smile:
788 return tlv.encode(1 /* TransitionEntryTypes.ADJUSTMENT_FACTOR */, tlv.writeFloat32LE(entry.brightnessAdjustmentFactor), 2 /* TransitionEntryTypes.VALUE */, tlv.writeFloat32LE(entry.temperature), 3 /* TransitionEntryTypes.TRANSITION_OFFSET */, tlv.writeVariableUIntLE(entry.transitionTime), 4 /* TransitionEntryTypes.DURATION */, tlv.writeVariableUIntLE(duration));
789 }), 2 /* TransitionCurveConfigurationTypes.ADJUSTMENT_CHARACTERISTIC_IID */, tlv.writeVariableUIntLE(this.activeTransition.brightnessCharacteristicIID), 3 /* TransitionCurveConfigurationTypes.ADJUSTMENT_MULTIPLIER_RANGE */, tlv.encode(1 /* TransitionAdjustmentMultiplierRange.MINIMUM_ADJUSTMENT_MULTIPLIER */, tlv.writeUInt32(this.activeTransition.brightnessAdjustmentRange.minBrightnessValue), 2 /* TransitionAdjustmentMultiplierRange.MAXIMUM_ADJUSTMENT_MULTIPLIER */, tlv.writeUInt32(this.activeTransition.brightnessAdjustmentRange.maxBrightnessValue))), 6 /* ValueTransitionConfigurationTypes.UPDATE_INTERVAL */, tlv.writeVariableUIntLE(this.activeTransition.updateInterval), 8 /* ValueTransitionConfigurationTypes.NOTIFY_INTERVAL_THRESHOLD */, tlv.writeVariableUIntLE(this.activeTransition.notifyIntervalThreshold)));
790 }
791 else {
792 return undefined; // returns empty string
793 }
794 }
795 handleTransitionControlUpdateTransition(buffer) {
796 const updateTransition = tlv.decode(buffer);
797 const transitionConfiguration = tlv.decode(updateTransition[1 /* UpdateValueTransitionConfigurationsTypes.VALUE_TRANSITION_CONFIGURATION */]);
798 const iid = tlv.readVariableUIntLE(transitionConfiguration[1 /* ValueTransitionConfigurationTypes.CHARACTERISTIC_IID */]);
799 if (!this.lightbulb.getCharacteristicByIID(iid)) {
800 throw new hapStatusError_1.HapStatusError(-70410 /* HAPStatus.INVALID_VALUE_IN_REQUEST */);
801 }
802 const param3 = transitionConfiguration[3 /* ValueTransitionConfigurationTypes.UNKNOWN_3 */]?.readUInt8(0); // when present it is always 1
803 if (!param3) { // if HomeKit just sends the iid, we consider that as "disable adaptive lighting" (assumption)
804 this.handleAdaptiveLightingDisabled();
805 return tlv.encode(2 /* TransitionControlTypes.UPDATE_VALUE_TRANSITION_CONFIGURATION */, Buffer.alloc(0));
806 }
807 const parametersTLV = tlv.decode(transitionConfiguration[2 /* ValueTransitionConfigurationTypes.TRANSITION_PARAMETERS */]);
808 const curveConfiguration = tlv.decodeWithLists(transitionConfiguration[5 /* ValueTransitionConfigurationTypes.TRANSITION_CURVE_CONFIGURATION */]);
809 const updateInterval = transitionConfiguration[6 /* ValueTransitionConfigurationTypes.UPDATE_INTERVAL */]?.readUInt16LE(0);
810 const notifyIntervalThreshold = transitionConfiguration[8 /* ValueTransitionConfigurationTypes.NOTIFY_INTERVAL_THRESHOLD */].readUInt32LE(0);
811 const transitionId = parametersTLV[1 /* ValueTransitionParametersTypes.TRANSITION_ID */];
812 const startTime = parametersTLV[2 /* ValueTransitionParametersTypes.START_TIME */];
813 const id3 = parametersTLV[3 /* ValueTransitionParametersTypes.UNKNOWN_3 */]; // this may be undefined
814 const startTimeMillis = (0, time_1.epochMillisFromMillisSince2001_01_01Buffer)(startTime);
815 const timeMillisOffset = Date.now() - startTimeMillis;
816 const transitionCurve = [];
817 let previous = undefined;
818 const transitions = curveConfiguration[1 /* TransitionCurveConfigurationTypes.TRANSITION_ENTRY */];
819 for (const entry of transitions) {
820 const tlvEntry = tlv.decode(entry);
821 const adjustmentFactor = tlvEntry[1 /* TransitionEntryTypes.ADJUSTMENT_FACTOR */].readFloatLE(0);
822 const value = tlvEntry[2 /* TransitionEntryTypes.VALUE */].readFloatLE(0);
823 const transitionOffset = tlv.readVariableUIntLE(tlvEntry[3 /* TransitionEntryTypes.TRANSITION_OFFSET */]);
824 const duration = tlvEntry[4 /* TransitionEntryTypes.DURATION */] ? tlv.readVariableUIntLE(tlvEntry[4 /* TransitionEntryTypes.DURATION */]) : undefined;
825 if (previous) {
826 previous.duration = duration;
827 }
828 previous = {
829 temperature: value,
830 brightnessAdjustmentFactor: adjustmentFactor,
831 transitionTime: transitionOffset,
832 };
833 transitionCurve.push(previous);
834 }
835 const adjustmentIID = tlv.readVariableUIntLE(curveConfiguration[2 /* TransitionCurveConfigurationTypes.ADJUSTMENT_CHARACTERISTIC_IID */]);
836 const adjustmentMultiplierRange = tlv.decode(curveConfiguration[3 /* TransitionCurveConfigurationTypes.ADJUSTMENT_MULTIPLIER_RANGE */]);
837 const minAdjustmentMultiplier = adjustmentMultiplierRange[1 /* TransitionAdjustmentMultiplierRange.MINIMUM_ADJUSTMENT_MULTIPLIER */].readUInt32LE(0);
838 const maxAdjustmentMultiplier = adjustmentMultiplierRange[2 /* TransitionAdjustmentMultiplierRange.MAXIMUM_ADJUSTMENT_MULTIPLIER */].readUInt32LE(0);
839 this.activeTransition = {
840 iid: iid,
841 transitionStartMillis: startTimeMillis,
842 timeMillisOffset: timeMillisOffset,
843 transitionId: uuid.unparse(transitionId),
844 transitionStartBuffer: startTime.toString("hex"),
845 id3: id3?.toString("hex"),
846 brightnessCharacteristicIID: adjustmentIID,
847 brightnessAdjustmentRange: {
848 minBrightnessValue: minAdjustmentMultiplier,
849 maxBrightnessValue: maxAdjustmentMultiplier,
850 },
851 transitionCurve: transitionCurve,
852 updateInterval: updateInterval ?? 60000,
853 notifyIntervalThreshold: notifyIntervalThreshold,
854 };
855 if (this.updateTimeout) {
856 clearTimeout(this.updateTimeout);
857 this.updateTimeout = undefined;
858 debug("[%s] Adaptive lighting was renewed.", this.lightbulb.displayName);
859 }
860 else {
861 debug("[%s] Adaptive lighting was enabled.", this.lightbulb.displayName);
862 }
863 this.handleActiveTransitionUpdated();
864 return tlv.encode(2 /* TransitionControlTypes.UPDATE_VALUE_TRANSITION_CONFIGURATION */, this.buildTransitionControlResponseBuffer(0));
865 }
866}
867exports.AdaptiveLightingController = AdaptiveLightingController;
868//# sourceMappingURL=AdaptiveLightingController.js.map
\No newline at end of file