homebridge-dyson-solarcycle-morph 0.2.2 → 0.3.0
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 +23 -0
- package/README.md +5 -3
- package/config.schema.json +8 -1
- package/dist/accessory.d.ts +1 -0
- package/dist/accessory.js +22 -0
- package/dist/accessory.js.map +1 -1
- package/dist/config.d.ts +6 -0
- package/dist/config.js.map +1 -1
- package/dist/dyson/lamp.d.ts +45 -6
- package/dist/dyson/lamp.js +105 -26
- package/dist/dyson/lamp.js.map +1 -1
- package/dist/dyson/pace.d.ts +40 -0
- package/dist/dyson/pace.js +53 -0
- package/dist/dyson/pace.js.map +1 -0
- package/dist/dyson/protocol.d.ts +38 -2
- package/dist/dyson/protocol.js +79 -2
- package/dist/dyson/protocol.js.map +1 -1
- package/docs/PROTOCOL.md +200 -10
- package/package.json +1 -1
- package/tools/daylight-test.js +531 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,29 @@ 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
|
+
## [0.3.0] — 2026-09-18
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Daylight tracking is a switch. It reports what the lamp is actually doing,
|
|
13
|
+
following the button on the lamp's base and the MyDyson app as well as
|
|
14
|
+
HomeKit, and it can be turned on and off. Setting a colour temperature ends
|
|
15
|
+
the tracking, which is the lamp's own behaviour, and the switch follows that
|
|
16
|
+
too. Turn it off per light with `daylightSwitch`.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- A write arriving immediately behind another was discarded, silently. HomeKit
|
|
21
|
+
sets brightness and colour temperature together whenever a scene is applied,
|
|
22
|
+
and the second of the two was being lost: on a CF06, colour temperature
|
|
23
|
+
reached its target in 1 of 8 attempts written back to back, and 8 of 8 when
|
|
24
|
+
spaced 100 ms apart. Control writes are now paced.
|
|
25
|
+
- The lamp is no longer taken out of daylight mode before every brightness and
|
|
26
|
+
colour-temperature write. It never needed to be — values land while the lamp
|
|
27
|
+
is tracking — and the write that was supposed to do it was never a valid
|
|
28
|
+
command in the first place. Brightness changes now leave daylight tracking
|
|
29
|
+
running; colour-temperature changes still end it, which is the lamp's doing.
|
|
30
|
+
|
|
8
31
|
## [0.2.2] — 2026-09-17
|
|
9
32
|
|
|
10
33
|
### Added
|
package/README.md
CHANGED
|
@@ -180,9 +180,11 @@ succeeds roughly two times in three, whatever you do, so the plugin simply tries
|
|
|
180
180
|
several times in quick succession. Only a cycle that never succeeds is worth
|
|
181
181
|
looking at.
|
|
182
182
|
|
|
183
|
-
**Daylight
|
|
184
|
-
|
|
185
|
-
|
|
183
|
+
**Daylight tracking appears as its own switch.** It shows what the lamp is
|
|
184
|
+
actually doing, following the button on the lamp's base and the MyDyson app as
|
|
185
|
+
well as HomeKit. Setting a colour temperature ends the tracking — that is the
|
|
186
|
+
lamp's own behaviour, not the plugin's — so the switch turns itself off when you
|
|
187
|
+
do. Turn it off per light with `daylightSwitch` if you would rather not have it.
|
|
186
188
|
|
|
187
189
|
**Setup without the Homebridge UI.** `dyson-morph-pair` does the same pairing
|
|
188
190
|
from a terminal. See [CONTRIBUTING.md](CONTRIBUTING.md).
|
package/config.schema.json
CHANGED
|
@@ -50,6 +50,12 @@
|
|
|
50
50
|
"type": "boolean",
|
|
51
51
|
"default": false,
|
|
52
52
|
"description": "Adds the lamp's built-in motion detector as a HomeKit motion sensor."
|
|
53
|
+
},
|
|
54
|
+
"daylightSwitch": {
|
|
55
|
+
"title": "Expose daylight tracking",
|
|
56
|
+
"type": "boolean",
|
|
57
|
+
"default": true,
|
|
58
|
+
"description": "Adds a switch for the lamp's daylight tracking, which adjusts colour temperature to the time of day. It reports the true state, following the button on the lamp base and the MyDyson app, and setting a colour temperature by hand ends the tracking as the lamp itself decides."
|
|
53
59
|
}
|
|
54
60
|
}
|
|
55
61
|
}
|
|
@@ -99,7 +105,8 @@
|
|
|
99
105
|
"lights[].name",
|
|
100
106
|
"lights[].mac",
|
|
101
107
|
"lights[].serial",
|
|
102
|
-
"lights[].motionSensor"
|
|
108
|
+
"lights[].motionSensor",
|
|
109
|
+
"lights[].daylightSwitch"
|
|
103
110
|
]
|
|
104
111
|
}
|
|
105
112
|
]
|
package/dist/accessory.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export declare class MorphAccessory {
|
|
|
15
15
|
private readonly config;
|
|
16
16
|
private readonly lightbulb;
|
|
17
17
|
private readonly motion?;
|
|
18
|
+
private readonly daylight?;
|
|
18
19
|
private readonly lamp;
|
|
19
20
|
constructor(platform: MorphPlatform, accessory: PlatformAccessory, config: ResolvedLightConfig);
|
|
20
21
|
start(): Promise<void>;
|
package/dist/accessory.js
CHANGED
|
@@ -15,6 +15,7 @@ export class MorphAccessory {
|
|
|
15
15
|
config;
|
|
16
16
|
lightbulb;
|
|
17
17
|
motion;
|
|
18
|
+
daylight;
|
|
18
19
|
lamp;
|
|
19
20
|
constructor(platform, accessory, config) {
|
|
20
21
|
this.platform = platform;
|
|
@@ -57,7 +58,24 @@ export class MorphAccessory {
|
|
|
57
58
|
this.lightbulb.updateCharacteristic(Characteristic.On, state.on);
|
|
58
59
|
this.lightbulb.updateCharacteristic(Characteristic.Brightness, state.brightness);
|
|
59
60
|
this.lightbulb.updateCharacteristic(Characteristic.ColorTemperature, kelvinToMired(state.kelvin));
|
|
61
|
+
this.daylight?.updateCharacteristic(Characteristic.On, state.daylight);
|
|
60
62
|
});
|
|
63
|
+
if (config.daylightSwitch !== false) {
|
|
64
|
+
// A switch rather than a characteristic on the lightbulb: HomeKit renders
|
|
65
|
+
// only the characteristics it knows, so a custom one would be invisible in
|
|
66
|
+
// the Home app and reachable only from third-party clients.
|
|
67
|
+
this.daylight =
|
|
68
|
+
this.accessory.getService(Service.Switch) ??
|
|
69
|
+
this.accessory.addService(Service.Switch, `${config.name} Daylight`, 'daylight');
|
|
70
|
+
this.daylight.setCharacteristic(Characteristic.Name, `${config.name} Daylight`);
|
|
71
|
+
this.daylight
|
|
72
|
+
.getCharacteristic(Characteristic.On)
|
|
73
|
+
.onGet(() => this.live(() => this.lamp.getState().daylight))
|
|
74
|
+
// Reports the mode faithfully but cannot change it; see setDaylight.
|
|
75
|
+
// The rejection is what makes the Home app put the switch back rather
|
|
76
|
+
// than leave it showing a state the lamp is not in.
|
|
77
|
+
.onSet((value) => this.handleSet('daylight mode', () => this.lamp.setDaylight(value)));
|
|
78
|
+
}
|
|
61
79
|
if (config.motionSensor) {
|
|
62
80
|
this.motion =
|
|
63
81
|
this.accessory.getService(Service.MotionSensor) ??
|
|
@@ -73,6 +91,9 @@ export class MorphAccessory {
|
|
|
73
91
|
this.lightbulb.updateCharacteristic(Characteristic.On, state.on);
|
|
74
92
|
this.lightbulb.updateCharacteristic(Characteristic.Brightness, state.brightness);
|
|
75
93
|
this.lightbulb.updateCharacteristic(Characteristic.ColorTemperature, kelvinToMired(state.kelvin));
|
|
94
|
+
// Follows the lamp leaving daylight mode on its own, which it does as soon
|
|
95
|
+
// as colour temperature is set by hand.
|
|
96
|
+
this.daylight?.updateCharacteristic(Characteristic.On, state.daylight);
|
|
76
97
|
});
|
|
77
98
|
}
|
|
78
99
|
async start() {
|
|
@@ -108,6 +129,7 @@ export class MorphAccessory {
|
|
|
108
129
|
this.lightbulb.updateCharacteristic(characteristic, error);
|
|
109
130
|
}
|
|
110
131
|
this.motion?.updateCharacteristic(Characteristic.MotionDetected, error);
|
|
132
|
+
this.daylight?.updateCharacteristic(Characteristic.On, error);
|
|
111
133
|
}
|
|
112
134
|
/**
|
|
113
135
|
* Apply a write, reporting failure to HomeKit rather than swallowing it.
|
package/dist/accessory.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"accessory.js","sourceRoot":"","sources":["../src/accessory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAI5C,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAG3F;;;;;;;GAOG;AACH,MAAM,OAAO,cAAc;
|
|
1
|
+
{"version":3,"file":"accessory.js","sourceRoot":"","sources":["../src/accessory.ts"],"names":[],"mappings":"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"}
|
package/dist/config.d.ts
CHANGED
|
@@ -17,6 +17,12 @@ export interface LightConfig {
|
|
|
17
17
|
accountId?: string;
|
|
18
18
|
/** Expose the built-in motion sensor as a separate HomeKit service. */
|
|
19
19
|
motionSensor?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Expose daylight tracking as a switch. On by default: it is the lamp's
|
|
22
|
+
* defining feature and there is no other way to turn it back on from HomeKit
|
|
23
|
+
* once setting a colour temperature has ended it.
|
|
24
|
+
*/
|
|
25
|
+
daylightSwitch?: boolean;
|
|
20
26
|
}
|
|
21
27
|
/** A lamp whose credentials are known, from the config or the credential store. */
|
|
22
28
|
export type ResolvedLightConfig = LightConfig & {
|
package/dist/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAqDA;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CAAC,KAA2B,EAAE,KAAa;IAC5E,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,MAAM,KAAK,GAAG,UAAU,KAAK,GAAG,CAAC;IAEjC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,CAAC;QACxB,QAAQ,CAAC,IAAI,CAAC,GAAG,KAAK,mBAAmB,CAAC,CAAC;IAC7C,CAAC;IACD,IAAI,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,sCAAsC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QAC1E,QAAQ,CAAC,IAAI,CAAC,GAAG,KAAK,8CAA8C,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACpG,CAAC;IACD,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,IAAI,EAAE,EAAE,CAAC;QAC1B,QAAQ,CAAC,IAAI,CAAC,GAAG,KAAK,qBAAqB,CAAC,CAAC;IAC/C,CAAC;IACD,yEAAyE;IACzE,8EAA8E;IAC9E,4CAA4C;IAC5C,IAAI,KAAK,CAAC,GAAG,KAAK,SAAS,IAAI,CAAC,CAAC,gBAAgB,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;QACjG,QAAQ,CAAC,IAAI,CAAC,GAAG,KAAK,wCAAwC,CAAC,CAAC;IAClE,CAAC;IACD,IAAI,KAAK,CAAC,SAAS,KAAK,SAAS,IAAI,CAAC,oBAAoB,CAAC,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE,CAAC;QACjF,QAAQ,CAAC,IAAI,CAAC,GAAG,KAAK,yCAAyC,CAAC,CAAC;IACnE,CAAC;IACD,IAAI,CAAC,KAAK,CAAC,GAAG,KAAK,SAAS,CAAC,KAAK,CAAC,KAAK,CAAC,SAAS,KAAK,SAAS,CAAC,EAAE,CAAC;QAClE,QAAQ,CAAC,IAAI,CAAC,GAAG,KAAK,8FAA8F,CAAC,CAAC;IACxH,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC"}
|
package/dist/dyson/lamp.d.ts
CHANGED
|
@@ -5,6 +5,14 @@ export interface LampState {
|
|
|
5
5
|
brightness: number;
|
|
6
6
|
/** Colour temperature in Kelvin, 2700-6500. */
|
|
7
7
|
kelvin: number;
|
|
8
|
+
/**
|
|
9
|
+
* Whether the lamp is tracking daylight.
|
|
10
|
+
*
|
|
11
|
+
* Asked for on connecting and reported by the lamp on every change. Carried
|
|
12
|
+
* across reconnects rather than reset, so that a lamp which does not answer
|
|
13
|
+
* starts from the last value seen rather than from a guess.
|
|
14
|
+
*/
|
|
15
|
+
daylight: boolean;
|
|
8
16
|
}
|
|
9
17
|
export interface LampOptions {
|
|
10
18
|
/** BLE MAC, e.g. `AA:BB:CC:DD:EE:FF`. */
|
|
@@ -46,6 +54,9 @@ export declare class DysonMorphLamp extends EventEmitter {
|
|
|
46
54
|
/** Serialises all GATT access. */
|
|
47
55
|
private readonly operations;
|
|
48
56
|
private readonly writes;
|
|
57
|
+
private readonly pacer;
|
|
58
|
+
/** Whether the lamp has stated the daylight mode, rather than it being inferred. */
|
|
59
|
+
private daylightReported;
|
|
49
60
|
private readonly reconciles;
|
|
50
61
|
private readonly settling;
|
|
51
62
|
/** Whether the running scan is one we started and must clean up. */
|
|
@@ -67,7 +78,6 @@ export declare class DysonMorphLamp extends EventEmitter {
|
|
|
67
78
|
private rssiMin;
|
|
68
79
|
private rssiMax;
|
|
69
80
|
private rssiReportedAt;
|
|
70
|
-
private manualModeSetAt;
|
|
71
81
|
private state;
|
|
72
82
|
/** What the user last asked for. Reconciliation aims at this, not at guesses. */
|
|
73
83
|
private desired;
|
|
@@ -82,6 +92,16 @@ export declare class DysonMorphLamp extends EventEmitter {
|
|
|
82
92
|
setPower(on: boolean): Promise<void>;
|
|
83
93
|
/** @param percent HomeKit brightness, 0-100 %. */
|
|
84
94
|
setBrightness(percent: number): Promise<void>;
|
|
95
|
+
/**
|
|
96
|
+
* Turn daylight tracking on or off.
|
|
97
|
+
*
|
|
98
|
+
* Goes out on the attribute channel, which is the one part of this lamp that
|
|
99
|
+
* acknowledges a write. Not reconciled all the same: the lamp announces the
|
|
100
|
+
* mode itself whenever it changes, from here, from the app, from the button
|
|
101
|
+
* on its base, or because setting a value by hand ended the tracking, so the
|
|
102
|
+
* report is a better answer than anything read back would be.
|
|
103
|
+
*/
|
|
104
|
+
setDaylight(on: boolean): Promise<void>;
|
|
85
105
|
setColorTemperature(kelvin: number): Promise<void>;
|
|
86
106
|
private writeUint16;
|
|
87
107
|
/**
|
|
@@ -143,7 +163,10 @@ export declare class DysonMorphLamp extends EventEmitter {
|
|
|
143
163
|
* match on characteristic UUID, which is unique regardless of its parent.
|
|
144
164
|
*/
|
|
145
165
|
private discoverCharacteristics;
|
|
146
|
-
/**
|
|
166
|
+
/**
|
|
167
|
+
* Write using whichever acknowledgement mode the characteristic supports,
|
|
168
|
+
* never closer than {@link MIN_WRITE_GAP_MS} behind the previous one.
|
|
169
|
+
*/
|
|
147
170
|
private write;
|
|
148
171
|
/**
|
|
149
172
|
* LTK re-authentication: prove we hold the key without contacting the cloud.
|
|
@@ -157,12 +180,16 @@ export declare class DysonMorphLamp extends EventEmitter {
|
|
|
157
180
|
private sendMessage;
|
|
158
181
|
private waitForMessage;
|
|
159
182
|
/**
|
|
160
|
-
*
|
|
183
|
+
* Ask the lamp whether it is tracking daylight.
|
|
184
|
+
*
|
|
185
|
+
* The characteristic has no `read` flag, but the attribute channel carries a
|
|
186
|
+
* request of its own, so the mode does not have to be guessed at. The answer
|
|
187
|
+
* arrives as a notification and is handled with the change reports.
|
|
161
188
|
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
189
|
+
* Best-effort: a lamp that does not answer leaves the inference in
|
|
190
|
+
* {@link publishFromLamp} to work it out from the tracking instead.
|
|
164
191
|
*/
|
|
165
|
-
private
|
|
192
|
+
private readDaylight;
|
|
166
193
|
private refreshState;
|
|
167
194
|
/** Read what the lamp currently reports. Values it will not give up are absent. */
|
|
168
195
|
private readState;
|
|
@@ -221,6 +248,18 @@ export declare class DysonMorphLamp extends EventEmitter {
|
|
|
221
248
|
* A value the user just set must stay where they put it; the steps the lamp
|
|
222
249
|
* takes on its way there are not news, they are the command happening.
|
|
223
250
|
*/
|
|
251
|
+
/**
|
|
252
|
+
* Take what the lamp reports, and read daylight mode out of it where it can.
|
|
253
|
+
*
|
|
254
|
+
* A fallback for a lamp that does not answer the question asked on connecting,
|
|
255
|
+
* which would otherwise leave the mode unknown until something changed it.
|
|
256
|
+
* Colour temperature moving when nobody asked for it is the tell: with
|
|
257
|
+
* daylight mode off, nothing but a write moves it.
|
|
258
|
+
*
|
|
259
|
+
* Only ever concludes the mode is on. Stillness proves nothing — the lamp
|
|
260
|
+
* drifts a few Kelvin a minute and less than that on a plateau — and a report
|
|
261
|
+
* from the lamp always wins over an inference once one arrives.
|
|
262
|
+
*/
|
|
224
263
|
private publishFromLamp;
|
|
225
264
|
private patchState;
|
|
226
265
|
}
|
package/dist/dyson/lamp.js
CHANGED
|
@@ -13,10 +13,11 @@ import { Variant } from 'dbus-next';
|
|
|
13
13
|
import { createBluetooth } from 'node-ble';
|
|
14
14
|
import { buildReauthPayloadA, buildReauthPayloadC, deriveAesKey, parseReauthPayloadB } from './crypto.js';
|
|
15
15
|
import { Debouncer } from './debounce.js';
|
|
16
|
+
import { Pacer } from './pace.js';
|
|
16
17
|
import { OperationQueue } from './queue.js';
|
|
17
18
|
import { planReconciliation } from './reconcile.js';
|
|
18
19
|
import { SettleWindow } from './settle.js';
|
|
19
|
-
import { CHAR_AUTH, CHAR_BRIGHTNESS_LM, CHAR_COLOR_TEMP, CHAR_MOTION, CHAR_POWER, CHAR_RSSI, CHAR_WRITE_ATTR,
|
|
20
|
+
import { CHAR_AUTH, CHAR_BRIGHTNESS_LM, CHAR_COLOR_TEMP, CHAR_MOTION, CHAR_POWER, CHAR_RSSI, CHAR_WRITE_ATTR, buildDaylightRead, buildDaylightWrite, decodeAttributeReport, decodeAttributeValue, MAX_KELVIN, MessageAssembler, MIN_KELVIN, MsgType, fragmentMessage, lumensToPercent, percentToLumens, } from './protocol.js';
|
|
20
21
|
/** Characteristics we look for. Anything else the lamp exposes is ignored. */
|
|
21
22
|
const WANTED_CHARACTERISTICS = new Set([
|
|
22
23
|
CHAR_AUTH,
|
|
@@ -57,14 +58,6 @@ const CONNECT_ATTEMPTS = 4;
|
|
|
57
58
|
const CONNECT_RETRY_MS = 1_500;
|
|
58
59
|
/** The lamp can take a while to answer the first handshake message. */
|
|
59
60
|
const HANDSHAKE_TIMEOUT_MS = 30_000;
|
|
60
|
-
/**
|
|
61
|
-
* Re-assert manual mode if the last write was longer ago than this. Doing it
|
|
62
|
-
* before every command wastes a write; never doing it means the lamp ignores us
|
|
63
|
-
* after someone used the physical daylight button.
|
|
64
|
-
*/
|
|
65
|
-
const MANUAL_MODE_TTL_MS = 60_000;
|
|
66
|
-
/** The lamp needs a moment to apply a mode change before the next write. */
|
|
67
|
-
const MODE_SETTLE_MS = 200;
|
|
68
61
|
/** Signal strength worth warning about, so a weak link is visible in the log. */
|
|
69
62
|
const WEAK_RSSI_DBM = -80;
|
|
70
63
|
/**
|
|
@@ -88,6 +81,15 @@ const NOISY_SPREAD_DB = 20;
|
|
|
88
81
|
* values a second while dragging; only the one it stops on matters.
|
|
89
82
|
*/
|
|
90
83
|
const WRITE_DEBOUNCE_MS = 400;
|
|
84
|
+
/**
|
|
85
|
+
* Minimum spacing between control writes; see {@link Pacer} for the measurement
|
|
86
|
+
* behind it. 150 ms keeps a margin over the 100 ms that tested clean, and is
|
|
87
|
+
* still far below what anyone notices in a lamp.
|
|
88
|
+
*
|
|
89
|
+
* Not applied to the auth channel, whose fragments are written back to back by
|
|
90
|
+
* design and have never shown the problem.
|
|
91
|
+
*/
|
|
92
|
+
const MIN_WRITE_GAP_MS = 150;
|
|
91
93
|
/**
|
|
92
94
|
* How long after the last command to check the lamp agrees. Verification cannot
|
|
93
95
|
* sit in the command's own path: the write costs a millisecond, reading it back
|
|
@@ -136,6 +138,9 @@ export class DysonMorphLamp extends EventEmitter {
|
|
|
136
138
|
/** Serialises all GATT access. */
|
|
137
139
|
operations = new OperationQueue();
|
|
138
140
|
writes = new Debouncer(WRITE_DEBOUNCE_MS, (task, key) => this.enqueue(task, key));
|
|
141
|
+
pacer = new Pacer(MIN_WRITE_GAP_MS);
|
|
142
|
+
/** Whether the lamp has stated the daylight mode, rather than it being inferred. */
|
|
143
|
+
daylightReported = false;
|
|
139
144
|
reconciles = new Debouncer(RECONCILE_DELAY_MS, (task, key) => this.enqueue(task, key));
|
|
140
145
|
settling = new SettleWindow(SETTLE_MS);
|
|
141
146
|
/** Whether the running scan is one we started and must clean up. */
|
|
@@ -157,8 +162,7 @@ export class DysonMorphLamp extends EventEmitter {
|
|
|
157
162
|
rssiMin = 0;
|
|
158
163
|
rssiMax = 0;
|
|
159
164
|
rssiReportedAt = 0;
|
|
160
|
-
|
|
161
|
-
state = { on: false, brightness: 100, kelvin: 2700 };
|
|
165
|
+
state = { on: false, brightness: 100, kelvin: 2700, daylight: false };
|
|
162
166
|
/** What the user last asked for. Reconciliation aims at this, not at guesses. */
|
|
163
167
|
desired = {};
|
|
164
168
|
constructor(options) {
|
|
@@ -247,6 +251,28 @@ export class DysonMorphLamp extends EventEmitter {
|
|
|
247
251
|
// that went missing.
|
|
248
252
|
await this.writes.schedule(CHAR_BRIGHTNESS_LM, () => this.writeUint16(CHAR_BRIGHTNESS_LM, lumens));
|
|
249
253
|
}
|
|
254
|
+
/**
|
|
255
|
+
* Turn daylight tracking on or off.
|
|
256
|
+
*
|
|
257
|
+
* Goes out on the attribute channel, which is the one part of this lamp that
|
|
258
|
+
* acknowledges a write. Not reconciled all the same: the lamp announces the
|
|
259
|
+
* mode itself whenever it changes, from here, from the app, from the button
|
|
260
|
+
* on its base, or because setting a value by hand ended the tracking, so the
|
|
261
|
+
* report is a better answer than anything read back would be.
|
|
262
|
+
*/
|
|
263
|
+
async setDaylight(on) {
|
|
264
|
+
this.requireConnection();
|
|
265
|
+
if (!this.chars[CHAR_WRITE_ATTR]) {
|
|
266
|
+
throw new Error('this lamp does not expose the attribute channel');
|
|
267
|
+
}
|
|
268
|
+
this.log.debug(`Daylight mode ${on ? 'on' : 'off'} requested`);
|
|
269
|
+
this.patchState({ daylight: on });
|
|
270
|
+
await this.writes.schedule(CHAR_WRITE_ATTR, async () => {
|
|
271
|
+
for (const fragment of buildDaylightWrite(on)) {
|
|
272
|
+
await this.write(CHAR_WRITE_ATTR, fragment);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
}
|
|
250
276
|
async setColorTemperature(kelvin) {
|
|
251
277
|
const clamped = Math.min(MAX_KELVIN, Math.max(MIN_KELVIN, Math.round(kelvin)));
|
|
252
278
|
this.requireConnection();
|
|
@@ -258,7 +284,6 @@ export class DysonMorphLamp extends EventEmitter {
|
|
|
258
284
|
await this.writes.schedule(CHAR_COLOR_TEMP, () => this.writeUint16(CHAR_COLOR_TEMP, clamped));
|
|
259
285
|
}
|
|
260
286
|
async writeUint16(uuid, value) {
|
|
261
|
-
await this.ensureManualMode();
|
|
262
287
|
const buffer = Buffer.alloc(2);
|
|
263
288
|
buffer.writeUInt16LE(value);
|
|
264
289
|
await this.write(uuid, buffer);
|
|
@@ -361,7 +386,8 @@ export class DysonMorphLamp extends EventEmitter {
|
|
|
361
386
|
}
|
|
362
387
|
await this.stopOurDiscovery();
|
|
363
388
|
this.connected = true;
|
|
364
|
-
this.
|
|
389
|
+
this.pacer.reset();
|
|
390
|
+
this.daylightReported = false;
|
|
365
391
|
this.settling.clear();
|
|
366
392
|
await this.refreshState();
|
|
367
393
|
// The lamp is the authority on where it is. Anything asked for before the
|
|
@@ -369,6 +395,7 @@ export class DysonMorphLamp extends EventEmitter {
|
|
|
369
395
|
// "correct" the lamp to a state nobody is asking for any more.
|
|
370
396
|
this.desired = { ...this.state };
|
|
371
397
|
await this.subscribeToState();
|
|
398
|
+
await this.readDaylight();
|
|
372
399
|
await this.subscribeToSignal();
|
|
373
400
|
this.startPolling();
|
|
374
401
|
const rssi = this.lastRssi;
|
|
@@ -491,8 +518,14 @@ export class DysonMorphLamp extends EventEmitter {
|
|
|
491
518
|
throw new Error(`${this.mac} is missing required characteristics: ${missing.join(', ')} — is this a Dyson Morph?`);
|
|
492
519
|
}
|
|
493
520
|
}
|
|
494
|
-
/**
|
|
521
|
+
/**
|
|
522
|
+
* Write using whichever acknowledgement mode the characteristic supports,
|
|
523
|
+
* never closer than {@link MIN_WRITE_GAP_MS} behind the previous one.
|
|
524
|
+
*/
|
|
495
525
|
async write(uuid, value) {
|
|
526
|
+
if (uuid !== CHAR_AUTH) {
|
|
527
|
+
await this.pacer.pace();
|
|
528
|
+
}
|
|
496
529
|
await this.characteristic(uuid).writeValue(value, { type: this.writeTypes[uuid] ?? 'command' });
|
|
497
530
|
}
|
|
498
531
|
/**
|
|
@@ -548,21 +581,27 @@ export class DysonMorphLamp extends EventEmitter {
|
|
|
548
581
|
});
|
|
549
582
|
}
|
|
550
583
|
/**
|
|
551
|
-
*
|
|
584
|
+
* Ask the lamp whether it is tracking daylight.
|
|
585
|
+
*
|
|
586
|
+
* The characteristic has no `read` flag, but the attribute channel carries a
|
|
587
|
+
* request of its own, so the mode does not have to be guessed at. The answer
|
|
588
|
+
* arrives as a notification and is handled with the change reports.
|
|
552
589
|
*
|
|
553
|
-
*
|
|
554
|
-
*
|
|
590
|
+
* Best-effort: a lamp that does not answer leaves the inference in
|
|
591
|
+
* {@link publishFromLamp} to work it out from the tracking instead.
|
|
555
592
|
*/
|
|
556
|
-
async
|
|
557
|
-
if (Date.now() - this.manualModeSetAt < MANUAL_MODE_TTL_MS) {
|
|
558
|
-
return;
|
|
559
|
-
}
|
|
593
|
+
async readDaylight() {
|
|
560
594
|
if (!this.chars[CHAR_WRITE_ATTR]) {
|
|
561
595
|
return;
|
|
562
596
|
}
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
597
|
+
try {
|
|
598
|
+
for (const fragment of buildDaylightRead()) {
|
|
599
|
+
await this.write(CHAR_WRITE_ATTR, fragment);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
catch (error) {
|
|
603
|
+
this.log.debug(`Could not ask for the daylight mode: ${describeError(error)}`);
|
|
604
|
+
}
|
|
566
605
|
}
|
|
567
606
|
async refreshState() {
|
|
568
607
|
this.patchState(await this.readState());
|
|
@@ -705,6 +744,23 @@ export class DysonMorphLamp extends EventEmitter {
|
|
|
705
744
|
},
|
|
706
745
|
],
|
|
707
746
|
[CHAR_COLOR_TEMP, (value) => (value.length >= 2 ? { kelvin: value.readUInt16LE(0) } : undefined)],
|
|
747
|
+
// Daylight mode arrives here twice over: the reply to the question asked
|
|
748
|
+
// on connecting, and a report whenever it changes.
|
|
749
|
+
[
|
|
750
|
+
CHAR_WRITE_ATTR,
|
|
751
|
+
(value) => {
|
|
752
|
+
// Anything the lamp says about the mode outranks what we inferred.
|
|
753
|
+
// Logged raw: this channel carries attributes that are not decoded,
|
|
754
|
+
// and a report that arrives but says nothing looks exactly like one
|
|
755
|
+
// that never arrived.
|
|
756
|
+
this.log.debug(`Attribute report ${value.toString('hex')}`);
|
|
757
|
+
const report = decodeAttributeReport(value) ?? decodeAttributeValue(value);
|
|
758
|
+
if (report) {
|
|
759
|
+
this.daylightReported = true;
|
|
760
|
+
}
|
|
761
|
+
return report;
|
|
762
|
+
},
|
|
763
|
+
],
|
|
708
764
|
];
|
|
709
765
|
for (const [uuid, decode] of sources) {
|
|
710
766
|
const characteristic = this.chars[uuid];
|
|
@@ -812,8 +868,28 @@ export class DysonMorphLamp extends EventEmitter {
|
|
|
812
868
|
* A value the user just set must stay where they put it; the steps the lamp
|
|
813
869
|
* takes on its way there are not news, they are the command happening.
|
|
814
870
|
*/
|
|
871
|
+
/**
|
|
872
|
+
* Take what the lamp reports, and read daylight mode out of it where it can.
|
|
873
|
+
*
|
|
874
|
+
* A fallback for a lamp that does not answer the question asked on connecting,
|
|
875
|
+
* which would otherwise leave the mode unknown until something changed it.
|
|
876
|
+
* Colour temperature moving when nobody asked for it is the tell: with
|
|
877
|
+
* daylight mode off, nothing but a write moves it.
|
|
878
|
+
*
|
|
879
|
+
* Only ever concludes the mode is on. Stillness proves nothing — the lamp
|
|
880
|
+
* drifts a few Kelvin a minute and less than that on a plateau — and a report
|
|
881
|
+
* from the lamp always wins over an inference once one arrives.
|
|
882
|
+
*/
|
|
815
883
|
publishFromLamp(patch) {
|
|
816
|
-
this.
|
|
884
|
+
const filtered = this.settling.filter(patch);
|
|
885
|
+
if (!this.daylightReported &&
|
|
886
|
+
!this.state.daylight &&
|
|
887
|
+
filtered.kelvin !== undefined &&
|
|
888
|
+
filtered.kelvin !== this.state.kelvin) {
|
|
889
|
+
this.log.debug(`Colour temperature moved to ${filtered.kelvin}K unasked — the lamp is tracking daylight`);
|
|
890
|
+
filtered.daylight = true;
|
|
891
|
+
}
|
|
892
|
+
this.patchState(filtered);
|
|
817
893
|
}
|
|
818
894
|
patchState(patch) {
|
|
819
895
|
if (Object.keys(patch).length === 0) {
|
|
@@ -829,7 +905,10 @@ export class DysonMorphLamp extends EventEmitter {
|
|
|
829
905
|
}
|
|
830
906
|
}
|
|
831
907
|
function describeState(state) {
|
|
832
|
-
|
|
908
|
+
// Daylight only when it is on: naming it every time would put "no daylight"
|
|
909
|
+
// on most lines of the log for a mode most lamps spend most of their time out of.
|
|
910
|
+
const daylight = state.daylight ? ', daylight' : '';
|
|
911
|
+
return `${state.on ? 'on' : 'off'}, ${state.brightness}%, ${state.kelvin}K${daylight}`;
|
|
833
912
|
}
|
|
834
913
|
function sleep(ms) {
|
|
835
914
|
return new Promise((resolve) => setTimeout(resolve, ms));
|