homebridge-dyson-solarcycle-morph 1.4.1 → 1.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,66 @@ All notable changes to this project are documented here.
5
5
  The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.4.3] — 2026-09-22
9
+
10
+ ### Fixed
11
+
12
+ - **A setting the lamp refuses is no longer reported as applied.** The lamp
13
+ answers every write to its location with a success status and then, on some
14
+ lamps, keeps its own value — so coordinates put in the configuration could sit
15
+ there for months doing nothing while the log claimed on every connection that
16
+ they had been applied. Measured on a CF06: roughly fifteen writes across a
17
+ day, every one acknowledged, the factory location still in place afterwards,
18
+ and the same lamp accepting the coordinates from the MyDyson app minutes
19
+ later. What the app does differently is not yet known.
20
+
21
+ Writes to the location and the time zone are now read back and checked, and a
22
+ refusal is logged as a refusal. If your lamp is one of these, set the location
23
+ once in the MyDyson app: daylight tracking needs the lamp to *have* a
24
+ location, not to have been given one by this plugin, and the Daylight switch
25
+ works normally once it does.
26
+
27
+ ### Added
28
+
29
+ - **The log says what the lamp makes of its location.** Once a connection is up:
30
+
31
+ ```
32
+ F0:B1:A7:75:B1:D1 puts today's daylight between 07:09 and 19:22
33
+ ```
34
+
35
+ This is the lamp's own sunrise and sunset, and the only honest answer to
36
+ whether daylight tracking is really running. The Daylight switch is not: the
37
+ lamp accepts the mode, acknowledges it, never reports it back off, and then
38
+ does nothing at all if it has no location to work from — saying so only
39
+ through a small LED on its own base. A lamp in that state now says so in the
40
+ log instead.
41
+
42
+ - **The lamp is told what time zone it is in.** Daylight tracking turns a place
43
+ into an hour, so the lamp needs the time zone as well as the coordinates, and
44
+ the MyDyson app writes both on every connection. This plugin only ever wrote
45
+ the coordinates. The offset and the daylight-saving rules now go in beside
46
+ them, and only when the lamp disagrees, so a lamp already holding the right
47
+ values is never written to.
48
+
49
+ Both come from the time zone of the machine Homebridge runs on — no setting to
50
+ fill in, and nothing asked over the network, which matters because these are
51
+ written while reconnecting, exactly when the network may still be down. A time
52
+ zone whose clock changes on a pattern this cannot state is left alone rather
53
+ than guessed at.
54
+
55
+ ## [1.4.2] — 2026-09-22
56
+
57
+ ### Fixed
58
+
59
+ - **The Daylight, Auto Brightness and Movement switches no longer spring back
60
+ when the lamp is off.** The lamp takes none of the three while it is not lit:
61
+ the write goes nowhere, the lamp goes on reporting the mode it was already in,
62
+ and a moment later the Home app put the switch back where it had been. A
63
+ switch flipped on an off lamp now stays where it was put, and the mode is
64
+ written when the lamp comes on — by the Home app, the MyDyson app or the
65
+ button on its base. Nothing is held over a restart: a plugin starting up takes
66
+ all three from the lamp. With the lamp on, everything is as it was.
67
+
8
68
  ## [1.4.1] — 2026-09-21
9
69
 
10
70
  ### Changed
package/README.md CHANGED
@@ -134,6 +134,15 @@ job — turn Auto Brightness off if you would rather it stayed put.
134
134
  turns itself off when you do. That is the lamp's own behaviour, not something
135
135
  the plugin decides. Flip the switch back on to resume.
136
136
 
137
+ **The three mode switches work while the lamp is off**, though the lamp itself
138
+ will not have them until it is lit — it takes none of the three while it is off.
139
+ So a switch flipped on a dark lamp stays where you put it and is written the
140
+ moment the lamp comes on, whether you switch it on from the Home app, from the
141
+ MyDyson app or at the lamp. Turning Movement on last thing at night therefore
142
+ does what you meant by it. Nothing is kept over a restart: a plugin starting up
143
+ takes all three from the lamp, which is the only thing that knows what it is
144
+ really doing.
145
+
137
146
  **Changes made at the lamp appear in the Home app**, usually straight away.
138
147
 
139
148
  **Out of range, the lamp shows as *No Response*** rather than the last thing it
@@ -239,6 +248,23 @@ lamp was set up on, and never asks again — so a lamp added through the app
239
248
  already knows, and one that has moved house still believes it is at the old
240
249
  address.
241
250
 
251
+ > **Not every lamp accepts them.** One CF06 measured on 2026-09-22 acknowledged
252
+ > both coordinates and stored neither — roughly fifteen attempts across a day,
253
+ > every one reported as successful by the lamp itself, and the factory location
254
+ > still in place afterwards. The same lamp took them from the MyDyson app
255
+ > minutes later. Nobody has yet worked out what the app does differently. The
256
+ > plugin now reads the values back after writing and says plainly in the log
257
+ > when they did not stick:
258
+ >
259
+ > ```
260
+ > F0:B1:A7:75:B1:D1 would not take the latitude: it acknowledges the write and
261
+ > keeps its own value. Set it in the MyDyson app instead — see the README.
262
+ > ```
263
+ >
264
+ > If you see that, set the location once in the MyDyson app. It sticks, and
265
+ > daylight tracking works from then on — including through the plugin, which
266
+ > needs the lamp to *have* a location but not to have been the one to give it.
267
+
242
268
  The two fields exist to set the coordinates without the app, and to correct them
243
269
  afterwards. Leave them empty and the plugin does not touch what the lamp holds —
244
270
  it still reads them on connecting and puts them in the debug log, which is the
@@ -264,6 +290,31 @@ connection goes out through a VPN. Nothing is sent to either service: they read
264
290
  the public address the request arrives from, which is what every site this
265
291
  machine contacts already sees. Check what it filled in before saving.
266
292
 
293
+ ### And what time it is there
294
+
295
+ Coordinates alone are not enough: the lamp turns them into a sunrise using a
296
+ clock, and it keeps neither reliably. **Take the lamp off the mains and it comes
297
+ back holding Dyson's own coordinates in Wiltshire**, and its offset from UTC has
298
+ been seen an hour out with nobody having written it. A lamp in that state blinks
299
+ a few times when daylight tracking is switched on and then drops out of it —
300
+ from the Home app and from the button on its base alike, because nothing is
301
+ wrong with the command.
302
+
303
+ So the plugin puts the time zone in beside the coordinates, on every connection,
304
+ and only when the lamp disagrees:
305
+
306
+ ```
307
+ UTC offset of F0:B1:A7:75:B1:D1 set to 2 (was 1)
308
+ ```
309
+
310
+ **There is nothing to configure.** Both the offset and the daylight-saving rules
311
+ come from the time zone of the machine Homebridge runs on, which already knows
312
+ them exactly and knows them without asking the network — worth having, since
313
+ these go in while reconnecting, which after a power cut is the moment the
314
+ network is least likely to be up. A time zone whose clock changes on a pattern
315
+ the lamp has no way of being told is left alone rather than guessed at; the
316
+ common European and British rules are exact.
317
+
267
318
  ### Age adjustment
268
319
 
269
320
  The lamp can trim the brightness of its **Study and Relax modes** to suit the
@@ -118,6 +118,8 @@ export declare class DysonMorphLamp extends EventEmitter {
118
118
  private daylightReported;
119
119
  private readonly reconciles;
120
120
  private readonly settling;
121
+ /** Mode changes made while the lamp was off, waiting for it to light. */
122
+ private readonly pending;
121
123
  /** Whether the running scan is one we started and must clean up. */
122
124
  private discoveryIsOurs;
123
125
  /** So the "try power-cycling" hint is given once, not on every attempt. */
@@ -151,15 +153,6 @@ export declare class DysonMorphLamp extends EventEmitter {
151
153
  setPower(on: boolean): Promise<void>;
152
154
  /** @param percent HomeKit brightness, 0-100 %. */
153
155
  setBrightness(percent: number): Promise<void>;
154
- /**
155
- * Turn daylight tracking on or off.
156
- *
157
- * Goes out on the attribute channel, which is the one part of this lamp that
158
- * acknowledges a write. Not reconciled all the same: the lamp announces the
159
- * mode itself whenever it changes, from here, from the app, from the button
160
- * on its base, or because setting a value by hand ended the tracking, so the
161
- * report is a better answer than anything read back would be.
162
- */
163
156
  /** Turn the lamp's own brightness trimming on or off. */
164
157
  setAutoBrightness(on: boolean): Promise<void>;
165
158
  /** Turn movement-triggered lighting on or off. */
@@ -172,6 +165,7 @@ export declare class DysonMorphLamp extends EventEmitter {
172
165
  * within a moment if the write did not land.
173
166
  */
174
167
  private setFlag;
168
+ private writeFlag;
175
169
  /**
176
170
  * Put the lamp into a preset, or take it out of the one it is in.
177
171
  *
@@ -179,7 +173,38 @@ export declare class DysonMorphLamp extends EventEmitter {
179
173
  * itself and says so, and writing both would race that.
180
174
  */
181
175
  setPreset(preset: Preset | 'none'): Promise<void>;
176
+ /**
177
+ * Turn daylight tracking on or off.
178
+ *
179
+ * Goes out on the attribute channel, which is the one part of this lamp that
180
+ * acknowledges a write. Not reconciled all the same: the lamp announces the
181
+ * mode itself whenever it changes, from here, from the app, from the button
182
+ * on its base, or because setting a value by hand ended the tracking, so the
183
+ * report is a better answer than anything read back would be.
184
+ */
182
185
  setDaylight(on: boolean): Promise<void>;
186
+ private writeDaylight;
187
+ /**
188
+ * Keep a mode change back while the lamp is off, rather than losing it.
189
+ *
190
+ * An off lamp discards these three silently and goes on reporting the old
191
+ * value, so writing one now would achieve nothing except a switch that
192
+ * springs back in the Home app a moment after it was flipped. The switch is
193
+ * left showing what was asked for — {@link patchState} has already published
194
+ * it — and the value is written when the lamp lights.
195
+ *
196
+ * @returns whether the change was held, in which case nothing is written.
197
+ */
198
+ private holdUntilLit;
199
+ /**
200
+ * Write the modes that were held back while the lamp was off.
201
+ *
202
+ * Called whenever the lamp lights, whoever lit it — HomeKit, the MyDyson app
203
+ * or the button on its base — and once more on connecting, for the lamp that
204
+ * was switched on while the link was down.
205
+ */
206
+ private flushPending;
207
+ private applyMode;
183
208
  setColorTemperature(kelvin: number): Promise<void>;
184
209
  private writeUint16;
185
210
  /**
@@ -291,8 +316,39 @@ export declare class DysonMorphLamp extends EventEmitter {
291
316
  * Read first, write only on a difference: these are settings rather than
292
317
  * controls, and rewriting them every connection would be churn on a channel
293
318
  * shared with the mode switches.
319
+ *
320
+ * **Some lamps will not take them at all.** Measured on a CF06 on 2026-09-22:
321
+ * both coordinates acknowledged with status `00`, neither stored, not even
322
+ * for the rest of the session, across roughly fifteen attempts in a day. The
323
+ * same lamp took the coordinates from the MyDyson app minutes later. Until
324
+ * somebody works out what the app does differently, the write is attempted,
325
+ * checked, and reported honestly when it fails — see {@link writeAndVerify}.
294
326
  */
295
327
  private syncLocation;
328
+ /**
329
+ * Put the host's time zone in the lamp, so it can turn a place into an hour.
330
+ *
331
+ * Sunrise needs a position *and* a clock. The MyDyson app writes the offset
332
+ * and the daylight-saving rules on every connection (`nd0/e.java`), taking
333
+ * both from the phone; this takes them from the host, which knows them
334
+ * exactly and knows them without asking the network — worth having, because
335
+ * these go out while reconnecting.
336
+ *
337
+ * Written only when the lamp disagrees, so a lamp that already holds the
338
+ * right values is never touched and the log stays quiet. Some lamps refuse
339
+ * the offset outright and say otherwise; see {@link writeAndVerify}.
340
+ */
341
+ private syncClock;
342
+ /**
343
+ * Say what the lamp makes of its location, for the log.
344
+ *
345
+ * These two are the only honest answer to "is daylight tracking actually
346
+ * running?". The mode attribute is no answer at all — the lamp takes the
347
+ * write, acknowledges it, never reports it back off, and then does nothing if
348
+ * it has no location to work from, saying so only through a small LED on its
349
+ * base. Sunrise and sunset are what it has actually computed.
350
+ */
351
+ private reportDaylightHours;
296
352
  /**
297
353
  * Put the configured age adjustment in the lamp.
298
354
  *
@@ -334,6 +390,24 @@ export declare class DysonMorphLamp extends EventEmitter {
334
390
  private writeAttribute;
335
391
  /** Ask the lamp for one coordinate, in degrees. */
336
392
  private readCoordinate;
393
+ /**
394
+ * Write an attribute and check the lamp actually took it.
395
+ *
396
+ * The acknowledgement is not proof for the settings behind daylight tracking.
397
+ * `0x2003`, `0x2004` and `0x2005` are all answered with status `00` and then,
398
+ * on some lamps, simply not stored — measured on a CF06 on 2026-09-22 by
399
+ * reading them back in the same session, immediately after the write. A
400
+ * plugin that believed the reply reported success for months while the lamp
401
+ * sat on the factory location.
402
+ *
403
+ * Tried once, not repeated. Repetition is the answer to most of this lamp's
404
+ * refusals, but not this one: five attempts in a row were refused just as
405
+ * uniformly as one, and each costs a write and a read on a link that would
406
+ * rather be left alone.
407
+ *
408
+ * @returns whether the lamp ended up holding the wanted value.
409
+ */
410
+ private writeAndVerify;
337
411
  private refreshState;
338
412
  /** Read what the lamp currently reports. Values it will not give up are absent. */
339
413
  private readState;