@johanwistbacka/node-red-contrib-sw-light 0.1.4 → 0.1.8
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 +32 -0
- package/README.md +129 -58
- package/examples/basic.json +2 -1
- package/examples/manual-adaptive.json +3 -2
- package/lib/adaptive.js +33 -23
- package/lib/light.js +14 -10
- package/nodes/sw-light.html +26 -13
- package/nodes/sw-light.js +32 -9
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.8 — 2026-09-30
|
|
4
|
+
|
|
5
|
+
- Simplify Adaptive Lighting to an On/Off checkbox with collapsed advanced profile selection.
|
|
6
|
+
- Use the supported color-only `adaptive_lighting.apply` action with a fresh provider calculation, only after automatic OFF → ON on CT-capable lights. SW Light retains brightness, maximum, override protection and transitions.
|
|
7
|
+
- Explicit message colors and manual commands suppress adaptation; already-on brightness changes do not reapply it. Add `msg.adaptive_lighting` opt-in/out.
|
|
8
|
+
- Discover only unique exact `configuration.lights` matches. Missing, ambiguous, unavailable, running and RGB sleep profiles fall back with structured warnings. Non-CT lights retain normal ON.
|
|
9
|
+
- Require the HA profile main switch off for one-shot ownership. Preserve inactive legacy nodes; migrate former optional-apply opt-in.
|
|
10
|
+
- Document inspected upstream API/source, two-action timing limitations and the exact live verification sequence.
|
|
11
|
+
- Full syntax/editor/example check and 42 tests passed, including all 3 isolated published-provider integration tests. No live HA/device verification or publication.
|
|
12
|
+
|
|
13
|
+
## 0.1.7 — local default threshold adjustment
|
|
14
|
+
|
|
15
|
+
- Sets the runtime/editor default manual override threshold to 98% (HA brightness 250 or above), retaining the 94% automatic maximum.
|
|
16
|
+
- Updates example flows and documentation. Existing saved thresholds remain unchanged.
|
|
17
|
+
- Verifies the new default boundary and release below threshold; preserves explicit 100% threshold coverage.
|
|
18
|
+
- No npm publication.
|
|
19
|
+
|
|
20
|
+
## 0.1.6 — local HA state normalization fix
|
|
21
|
+
|
|
22
|
+
- Omits null/non-array observed HA color attributes instead of spreading them, preventing `a[k] is not iterable` during observation and commands.
|
|
23
|
+
- Keeps valid color arrays as independent copies; retains brightness and manual override behavior.
|
|
24
|
+
- Adds null/non-array normalization coverage and exercises all three override regressions with null HA color attributes.
|
|
25
|
+
- Full checks and isolated Node-RED/HA contrib integration: 33 tests passed. No npm publication; physical-device verification pending.
|
|
26
|
+
|
|
27
|
+
## 0.1.5 — local override fix candidate
|
|
28
|
+
|
|
29
|
+
- Starts per-light HA observation after flow startup, before the first input.
|
|
30
|
+
- Retries temporary provider initialization failures without freezing the node; preserves the shared-client/readiness adapter.
|
|
31
|
+
- Fixes idle OFF nodes missing external brightness changes: observed 255 activates override, observed 204 releases the default threshold, and explicit manual OFF bypasses the guard.
|
|
32
|
+
- Adds real-adapter regressions for automatic 94% → external 100% → suppressed OFF, external 80% release, manual OFF, and entity isolation.
|
|
33
|
+
- Full suite including isolated Node-RED/HA contrib 0.80.3 integration: 32 tests passed. Physical-device verification of this candidate is pending. No npm publication.
|
|
34
|
+
|
|
3
35
|
## 0.1.4 — scoped distribution candidate
|
|
4
36
|
|
|
5
37
|
- Changes npm identity to `@johanwistbacka/node-red-contrib-sw-light`.
|
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# SW Light — v0.1.
|
|
1
|
+
# SW Light — v0.1.8
|
|
2
2
|
|
|
3
3
|
A reusable Node-RED node for one Home Assistant `light.*` entity. Select an
|
|
4
4
|
existing HA server, select a light, deploy in a **development** environment,
|
|
@@ -14,8 +14,9 @@ and integration only. With a fixed Action, a normal timestamp Inject is enough.
|
|
|
14
14
|
|
|
15
15
|
The permanent npm package name is `@johanwistbacka/node-red-contrib-sw-light`.
|
|
16
16
|
The Node-RED type stays `sw-light` and its display name stays **SW Light**.
|
|
17
|
-
The scoped package
|
|
18
|
-
for each npm package name.
|
|
17
|
+
The scoped package started at version 0.1.4; version numbers are independent
|
|
18
|
+
for each npm package name. Version 0.1.5 fixes observation before the first command; 0.1.6 also tolerates
|
|
19
|
+
null/non-array observed HA color attributes.
|
|
19
20
|
|
|
20
21
|
If upgrading from the old unscoped `node-red-contrib-sw-light`, export your
|
|
21
22
|
flows first. Remove the old package from the Node-RED user directory before
|
|
@@ -25,7 +26,7 @@ or, with Node-RED stopped, run these commands in its user directory:
|
|
|
25
26
|
|
|
26
27
|
```sh
|
|
27
28
|
npm uninstall node-red-contrib-sw-light
|
|
28
|
-
npm install @johanwistbacka/node-red-contrib-sw-light@0.1.
|
|
29
|
+
npm install @johanwistbacka/node-red-contrib-sw-light@0.1.8
|
|
29
30
|
```
|
|
30
31
|
|
|
31
32
|
Restart Node-RED and reload the editor. Existing flow nodes retain type
|
|
@@ -51,7 +52,7 @@ are enabled. Test with a development light before relying on this node.
|
|
|
51
52
|
After this version is published, install it in your Node-RED user directory:
|
|
52
53
|
|
|
53
54
|
```sh
|
|
54
|
-
npm install @johanwistbacka/node-red-contrib-sw-light@0.1.
|
|
55
|
+
npm install @johanwistbacka/node-red-contrib-sw-light@0.1.8
|
|
55
56
|
```
|
|
56
57
|
|
|
57
58
|
After publication, the current development release is available under npm latest:
|
|
@@ -74,7 +75,7 @@ For edits, use npm's local directory installation/link or reinstall an archive
|
|
|
74
75
|
created with `npm pack`. Restart the isolated Node-RED runtime after changes.
|
|
75
76
|
There is no build step or additional runtime dependency.
|
|
76
77
|
|
|
77
|
-
The current development release uses the normal SemVer version `0.1.
|
|
78
|
+
The current development release uses the normal SemVer version `0.1.8` and
|
|
78
79
|
npm tag `latest` for Node-RED compatibility. This version remains experimental;
|
|
79
80
|
it is not a SemVer prerelease. Every installation/publication candidate needs a
|
|
80
81
|
unique version. Use `npm version patch --no-git-tag-version` for the next normal
|
|
@@ -86,7 +87,7 @@ After publication, submit the package through the
|
|
|
86
87
|
[Node-RED Flow Library](https://flows.nodered.org/add/node), or request a refresh
|
|
87
88
|
if it is already listed.
|
|
88
89
|
|
|
89
|
-
## Temporary runtime diagnostics in 0.1.
|
|
90
|
+
## Temporary runtime diagnostics in 0.1.8
|
|
90
91
|
|
|
91
92
|
This build logs `[SW Light debug]` at construction and on the first input,
|
|
92
93
|
then when the relevant structure/readiness changes. It reports version,
|
|
@@ -96,7 +97,7 @@ It never logs server IDs, entity IDs, credentials, hosts, full objects or state
|
|
|
96
97
|
snapshots. Provider initialization errors are reduced to a generic message.
|
|
97
98
|
|
|
98
99
|
After installation, restart Node-RED and verify a log line containing
|
|
99
|
-
`[SW Light debug] version=0.1.
|
|
100
|
+
`[SW Light debug] version=0.1.8`. Inject once and retain the prefixed lines,
|
|
100
101
|
along with any error/stack. Those lines distinguish missing configuration,
|
|
101
102
|
a missing provider client and a connected client with unexpected methods.
|
|
102
103
|
The real installation problem remains unverified until this build is tested
|
|
@@ -119,11 +120,10 @@ there is no `require.cache` scan or search for alternate package copies.
|
|
|
119
120
|
| On/off transition | 1s each | Sent only with light transition capability |
|
|
120
121
|
| Manual override | Enabled | Pause automatic commands while override is active |
|
|
121
122
|
| Detection | Brightness threshold + flag | Alternative: explicit flag only |
|
|
122
|
-
| Threshold |
|
|
123
|
-
| Color mode |
|
|
124
|
-
| Adaptive
|
|
125
|
-
|
|
|
126
|
-
| Respect adaptive manual control | Enabled | Skip apply when light is manually controlled |
|
|
123
|
+
| Threshold | 98% | Lamp brightness at/above this value activates override |
|
|
124
|
+
| Color mode | Keep current | Normal behavior and fallback |
|
|
125
|
+
| Adaptive Lighting | Off | Current temperature once on automatic OFF → ON |
|
|
126
|
+
| Advanced profile | Automatic | Unique exact light membership; explicit selection if ambiguous |
|
|
127
127
|
|
|
128
128
|
Require `minimum <= on brightness <= automatic maximum`. When threshold
|
|
129
129
|
detection is enabled, automatic maximum must be below the threshold.
|
|
@@ -180,6 +180,7 @@ validated**, even those that lose precedence.
|
|
|
180
180
|
| `rgbww_color`, `rgbw_color`, `rgb_color` | Arrays of 5, 4, 3 integer bytes 0–255 |
|
|
181
181
|
| `hs_color`, `xy_color`, `color_name` | [0–360,0–100], [0–1,0–1], HA color name |
|
|
182
182
|
| `transition` | 0–3600 seconds |
|
|
183
|
+
| `adaptive_lighting` | Boolean; overrides Adaptive Lighting for one command |
|
|
183
184
|
|
|
184
185
|
The complete color precedence follows the table top to bottom. `color_name`
|
|
185
186
|
is passed to HA for name resolution/validation, only on a color-capable lamp.
|
|
@@ -210,15 +211,21 @@ This hard guard requires brightness exactly **255/255**, so 94%, 95%, 99% and
|
|
|
210
211
|
is disabled or set to explicit-only. Lowering the light below 255 allows
|
|
211
212
|
automatic control again unless an additional configured override is active.
|
|
212
213
|
|
|
213
|
-
Threshold detection observes HA state, not requested brightness.
|
|
214
|
-
|
|
215
|
-
|
|
214
|
+
Threshold detection observes HA state, not requested brightness. Each node starts
|
|
215
|
+
its state subscription after flow startup, before its first command, and retries
|
|
216
|
+
temporary provider initialization failures. This includes separate ON/OFF nodes
|
|
217
|
+
for the same physical light. The configured
|
|
218
|
+
threshold is converted to a byte; default 98% means brightness 250 or above.
|
|
219
|
+
With defaults, automation uses 1–94%, and observed brightness at/above this threshold activates
|
|
216
220
|
override. It pauses **all automatic commands**, including ON, OFF, toggle and
|
|
217
221
|
brightness zero, so an automatic ON cannot accidentally lower and release a
|
|
218
222
|
manual setting. A manually lowered observed brightness releases the threshold
|
|
219
223
|
flag. OFF releases all flags. Missing/unknown/unavailable state preserves flags
|
|
220
224
|
and rejects commands.
|
|
221
225
|
|
|
226
|
+
Existing nodes retain their saved threshold. Change a saved 100% threshold to
|
|
227
|
+
98% manually to use the new default behavior.
|
|
228
|
+
|
|
222
229
|
An explicit latch is independent of the threshold. It persists until explicit
|
|
223
230
|
release or observed OFF. A release flag clears both flags for its command;
|
|
224
231
|
subsequent threshold observations can reactivate override. Flags are in memory,
|
|
@@ -228,49 +235,112 @@ detection. `source` is the v0.1 command-origin contract.
|
|
|
228
235
|
|
|
229
236
|
## Adaptive Lighting
|
|
230
237
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
238
|
+
The primary editor control is **Adaptive Lighting: On / Off** (off for new nodes).
|
|
239
|
+
Color mode independently defines normal/fallback color behavior; the default is
|
|
240
|
+
Keep current. Advanced profile selection is collapsed. Discovery selects only
|
|
241
|
+
one exact light-membership match in a main switch's `configuration.lights`.
|
|
242
|
+
No entity names or undocumented `lights` attributes are guessed. Configuration
|
|
243
|
+
attributes must be exposed in HA (`include_config_in_attributes`) for discovery;
|
|
244
|
+
otherwise select the main switch explicitly. Multiple matches never choose the
|
|
245
|
+
first profile. Group membership expansion is not performed.
|
|
246
|
+
|
|
247
|
+
Keep the selected Adaptive Lighting **main switch OFF** to use its calculation
|
|
248
|
+
without continuous control or ON interception. The supported `adaptive_lighting.apply`
|
|
249
|
+
action works with that switch off and calculates fresh values for every call.
|
|
250
|
+
SW Light never toggles that switch, changes profile settings, clears manual
|
|
251
|
+
control or starts a continuous adaptation loop. Choose a profile using
|
|
252
|
+
color-temperature sleep color; RGB sleep profiles are rejected with a warning.
|
|
253
|
+
|
|
254
|
+
Precedence for each command:
|
|
255
|
+
|
|
256
|
+
1. Explicit message color/temperature wins, including unsupported color that
|
|
257
|
+
is omitted with an event. It always suppresses Adaptive Lighting.
|
|
258
|
+
2. Otherwise, Adaptive Lighting runs only after a successful automatic OFF → ON
|
|
259
|
+
service, on a color-temperature-capable light.
|
|
260
|
+
3. Otherwise, normal configured SW Light color behavior applies.
|
|
261
|
+
4. Keep current/lamp default omit color and leave normal HA/device behavior.
|
|
262
|
+
|
|
263
|
+
`msg.adaptive_lighting` (boolean) overrides the checkbox for that command.
|
|
264
|
+
Brightness, automatic maximum, override protection and fade stay with SW Light.
|
|
265
|
+
An already-ON brightness command, manual command, OFF or zero-brightness command
|
|
266
|
+
never invokes Adaptive Lighting. Configured fixed color still follows normal
|
|
267
|
+
SW Light behavior on these commands. Legacy adaptive flows with optional apply
|
|
268
|
+
already enabled retain their opt-in; former inactive adaptive flows stay off.
|
|
269
|
+
Legacy discover/respect-manual controls are retired.
|
|
270
|
+
|
|
271
|
+
After `light.turn_on`, SW Light calls `adaptive_lighting.apply` for exactly one
|
|
272
|
+
light with `adapt_brightness: false`, `adapt_color: true`, `prefer_rgb_color: false`,
|
|
273
|
+
`turn_on_lights: true`, and the SW Light command transition (0 if unsupported).
|
|
274
|
+
The true flag avoids silently skipping color when HA has not yet observed the
|
|
275
|
+
successful ON. Adaptive Lighting clamps temperature to the light's range.
|
|
276
|
+
The two actions are not atomic: a brief initial/default color may be visible,
|
|
277
|
+
and an external OFF between them can race with the second action. Service
|
|
278
|
+
acknowledgement reports an accepted action, not physical temperature confirmation.
|
|
279
|
+
|
|
280
|
+
Missing/unavailable/ambiguous/running profiles, a missing apply action or its
|
|
281
|
+
failure emit output 2 `warning`, with `phase: "adaptive_apply"`, structured
|
|
282
|
+
`reason`, and `light_service_completed: true`. Normal ON and output 1 survive,
|
|
283
|
+
retaining the configured color fallback. Non-CT lights emit `unsupported` and
|
|
284
|
+
still turn on without temperature parameters. A running HA profile or other
|
|
285
|
+
external automation can still alter lights independently; turn those off for
|
|
286
|
+
this ownership model. SW Light cannot cancel their behavior.
|
|
287
|
+
|
|
288
|
+
Investigation: [upstream apply documentation](https://adaptive-lighting.nijho.lt/services/)
|
|
289
|
+
and [source inspected on 2026-09-30](https://github.com/basnijholt/adaptive-lighting/blob/283fec08ae7d34901d13c32e91fb001f92395fe7/custom_components/adaptive_lighting/switch.py).
|
|
290
|
+
The main switch exposes last calculated `color_temp_kelvin` when on, but null
|
|
291
|
+
when off; reading that cache cannot deliver the requested fresh calculation.
|
|
292
|
+
`apply` recalculates in `prepare_adaptation_data` even while off. No dedicated
|
|
293
|
+
read-only temperature calculation service is exposed. Calculations remain in
|
|
294
|
+
Adaptive Lighting, including sun/time and sleep settings.
|
|
295
|
+
|
|
296
|
+
### Real-world verification
|
|
297
|
+
|
|
298
|
+
1. Install the local 0.1.8 archive, restart Node-RED, and verify its startup
|
|
299
|
+
version. Use a test light with `color_temp` support. Connect both outputs to Debug.
|
|
300
|
+
2. In HA, expose the profile's configuration attributes for automatic discovery
|
|
301
|
+
(or select it under Advanced). Keep its main switch off, use color-temperature
|
|
302
|
+
sleep color, and disable any other automatic controllers for this test light.
|
|
303
|
+
3. In SW Light select Action On, brightness/max 94%, Keep current, Adaptive Lighting
|
|
304
|
+
On. Turn the light off in HA and wait until Node-RED observes OFF. Trigger ON.
|
|
305
|
+
4. Verify `adaptive.applied: true` and brightness 239/255 (about 93.7%). After the
|
|
306
|
+
fade, inspect HA light state and physically check the light. To get a comparison
|
|
307
|
+
value while the profile is off, call HA's `adaptive_lighting.apply` yourself
|
|
308
|
+
with the same profile/light, brightness false, color true, RGB preference false,
|
|
309
|
+
turn-on true and transition 0; verify the light's temperature stays the same
|
|
310
|
+
(allow calculation changes around a time boundary and device rounding).
|
|
311
|
+
5. Turn the light off and wait for OFF. Repeat at another time, or toggle the
|
|
312
|
+
profile's sleep-mode helper with a distinct configured sleep temperature.
|
|
313
|
+
Trigger ON; verify the new value rather than the previous temperature is used.
|
|
314
|
+
Restore the sleep helper after testing.
|
|
315
|
+
6. While ON, send another automatic ON with `brightness_pct: 80`. Verify brightness
|
|
316
|
+
changes and `adaptive.applied: false`; with Keep current, temperature stays put.
|
|
317
|
+
7. From OFF, send ON with `color_temp_kelvin: 3000`, then a supported explicit
|
|
318
|
+
color command. Verify each wins and no adaptive action runs. Try a manual ON
|
|
319
|
+
and verify the same suppression.
|
|
320
|
+
8. Set brightness to 100% externally. Automatic OFF must remain blocked. Lower
|
|
321
|
+
below the configured threshold and verify OFF works again.
|
|
322
|
+
9. Test missing/ambiguous profiles, the main switch ON, and a non-CT light. Confirm
|
|
323
|
+
normal ON plus the corresponding warning/unsupported event. Redeploy/reconnect
|
|
324
|
+
and verify fresh discovery, safe observation and no duplicate adaptive calls.
|
|
325
|
+
|
|
326
|
+
Actual Adaptive Lighting version, exposed configuration, sleep behavior, lamp
|
|
327
|
+
latency/rounding and the two-action race still require verification on your HA
|
|
328
|
+
installation. Automated tests mock that service boundary, not its algorithm.
|
|
329
|
+
|
|
330
|
+
## Test the updated 0.1.8 in your HA Node-RED installation
|
|
261
331
|
|
|
262
332
|
1. Save/export your current test flow. Transfer the rebuilt local
|
|
263
|
-
`johanwistbacka-node-red-contrib-sw-light-0.1.
|
|
333
|
+
`johanwistbacka-node-red-contrib-sw-light-0.1.8.tgz` to the Node-RED host. Check that HA contrib
|
|
264
334
|
is 0.80.3; do not upgrade it for this task.
|
|
265
335
|
2. In a terminal for that Node-RED instance, find **User directory** in its
|
|
266
336
|
startup log. Change to that directory (not a global npm directory) and run
|
|
267
|
-
`npm install ./johanwistbacka-node-red-contrib-sw-light-0.1.
|
|
337
|
+
`npm install ./johanwistbacka-node-red-contrib-sw-light-0.1.8.tgz --ignore-scripts --no-audit --no-fund`.
|
|
268
338
|
Use the add-on's supported local package installation mechanism if its terminal
|
|
269
339
|
does not expose that user directory/npm. Restart Node-RED/the Node-RED add-on
|
|
270
340
|
and reload the editor so the updated runtime and HTML are both loaded.
|
|
271
341
|
3. Add a timestamp Inject directly into SW Light. Choose your **existing HA
|
|
272
342
|
server**, one `light.*` entity, Action **On**, and the desired On brightness
|
|
273
|
-
(94% by default). Keep
|
|
343
|
+
(94% by default). Keep Adaptive Lighting off for this basic test.
|
|
274
344
|
Remove downstream HA Action nodes; leave outputs unconnected or use Debug.
|
|
275
345
|
4. Deploy. SW Light constructs without resolving HA and initially shows
|
|
276
346
|
`HA NOT READY`. With the light OFF, click Inject: the light should turn on at the
|
|
@@ -279,7 +349,7 @@ SW Light never repeatedly corrects brightness or fights its adaptation.
|
|
|
279
349
|
5. Set the lamp to 100% manually in HA (verify attribute `brightness: 255`).
|
|
280
350
|
Try On, Off and Toggle: no automatic light service should run, and the node
|
|
281
351
|
should show `MANUAL · 100% · BLOCK ...`. Lower the light manually to 94%, 95%
|
|
282
|
-
|
|
352
|
+
then retry; automatic control should resume with default override settings.
|
|
283
353
|
6. Select **From msg** and use Inject properties `action` (string) with `on`,
|
|
284
354
|
`off` and `toggle`. Also test `auto_light.action` when top-level action is
|
|
285
355
|
absent. A missing/invalid action should show an error, emit output 2, and
|
|
@@ -310,13 +380,14 @@ Output 1 follows successful light service acknowledgement:
|
|
|
310
380
|
"service_data": { "brightness": 115, "transition": 1 },
|
|
311
381
|
"ignored_conflicts": [], "state_confirmation": "last_observed",
|
|
312
382
|
"observed_at": "2026-09-30T10:00:00Z",
|
|
313
|
-
"adaptive": { "entity_id": null, "active": false, "
|
|
383
|
+
"adaptive": { "entity_id": null, "active": false, "state": null, "candidates": [], "applied": false, "reason": "not_requested" }
|
|
314
384
|
}
|
|
315
385
|
}
|
|
316
386
|
```
|
|
317
387
|
|
|
318
|
-
`adaptive_lighting` means
|
|
319
|
-
|
|
388
|
+
`adaptive_lighting` means the command's effective enablement (message overrides node).
|
|
389
|
+
`adaptive.active` means a resolved profile is available with its main switch OFF.
|
|
390
|
+
`adaptive.applied` reports successful action acknowledgement, not measured Kelvin.
|
|
320
391
|
The state is the **last observed HA snapshot**, possibly preceding the action
|
|
321
392
|
or an unfinished transition. Service acknowledgement does not confirm a physical
|
|
322
393
|
lamp change. No requested value is fabricated as observed state. The first input
|
|
@@ -357,7 +428,7 @@ Output 2 has a stable versioned event envelope:
|
|
|
357
428
|
```
|
|
358
429
|
|
|
359
430
|
Types: `manual_override`, `override_released`, `unavailable`, `unsupported`,
|
|
360
|
-
`error`. Details include `reason`, `parameters`, `message`, `phase` as applicable.
|
|
431
|
+
`warning`, `error`. Details include `reason`, `parameters`, `message`, `phase` as applicable.
|
|
361
432
|
Unsupported parameters are omitted and the supported action still runs.
|
|
362
433
|
Blocked commands emit only output 2. Invalid input and transport failures also
|
|
363
434
|
call Node-RED `done(error)` for Catch-node handling. Connection/state events have
|
|
@@ -381,7 +452,7 @@ npm pack --dry-run
|
|
|
381
452
|
Run these checks from the source checkout; test tooling is not included in the npm package.
|
|
382
453
|
Tests use Node's built-in test runner without HA or npm dependencies. They cover
|
|
383
454
|
numeric ranges, precedence, max/min, manual override, color translation,
|
|
384
|
-
unsupported capabilities, adaptive discovery/
|
|
455
|
+
unsupported capabilities, one-shot adaptive discovery/fallback, runtime outputs,
|
|
385
456
|
async errors, reconnect readiness and listener cleanup. The source checkout retains a separate development integration report; it is
|
|
386
457
|
excluded from the public npm package. Importable examples are in `examples/`; select your
|
|
387
458
|
own HA server and light before deploying them in a dev instance.
|
|
@@ -395,14 +466,14 @@ own HA server and light before deploying them in a dev instance.
|
|
|
395
466
|
- A v0.1 raw state subscription receives all state changes and filters locally;
|
|
396
467
|
one is created per SW Light node. It is removed on close, and HA's websocket
|
|
397
468
|
resubscription handles reconnects. Sharing subscriptions is a v0.2 improvement.
|
|
398
|
-
- Adaptive
|
|
399
|
-
|
|
469
|
+
- Adaptive discovery requires exposed configuration and exact light membership.
|
|
470
|
+
Use Advanced selection when discovery is unavailable; verify the installed profile.
|
|
400
471
|
- Keep current depends on HA/device behavior. Color gamut correction and XY to
|
|
401
472
|
other color-space conversion are not implemented. ON/OFF lights remain usable
|
|
402
473
|
without brightness/color capabilities.
|
|
403
474
|
|
|
404
475
|
Before live use, test server selection/autocomplete, each lamp capability,
|
|
405
|
-
external brightness changes, threshold/explicit release, adaptive
|
|
476
|
+
external brightness changes, threshold/explicit release, adaptive one-shot behavior,
|
|
406
477
|
long transitions, deletion/unavailability, reconnect and repeated redeployment
|
|
407
478
|
in your real editor with a development light. No live deployment, global config
|
|
408
479
|
change, npm publication or GitHub release is part of this implementation.
|
package/examples/basic.json
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"type": "comment",
|
|
12
12
|
"z": "sw-example-manual-adaptive",
|
|
13
13
|
"name": "Select HA server + light in SW Light before deploy",
|
|
14
|
-
"info": "Adaptive
|
|
14
|
+
"info": "Adaptive Lighting is off by default. Enable the checkbox for automatic OFF to ON; keep the HA profile main switch off. Advanced selection is only needed when discovery is ambiguous or unavailable.",
|
|
15
15
|
"x": 350,
|
|
16
16
|
"y": 60,
|
|
17
17
|
"wires": []
|
|
@@ -193,7 +193,8 @@
|
|
|
193
193
|
"sw-example-manual-adaptive-events"
|
|
194
194
|
]
|
|
195
195
|
],
|
|
196
|
-
"action": "msg"
|
|
196
|
+
"action": "msg",
|
|
197
|
+
"overrideThreshold": 98
|
|
197
198
|
},
|
|
198
199
|
{
|
|
199
200
|
"id": "sw-example-manual-adaptive-result",
|
package/lib/adaptive.js
CHANGED
|
@@ -2,38 +2,48 @@
|
|
|
2
2
|
|
|
3
3
|
const { fault } = require('./light');
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
return
|
|
5
|
+
// Main Adaptive Lighting switches expose configuration.lights in upstream
|
|
6
|
+
// extra_state_attributes. Auxiliary switches do not. Never infer entity names.
|
|
7
|
+
function candidates(c, states) {
|
|
8
|
+
return Object.values(states).filter(s => s.entity_id?.startsWith('switch.') &&
|
|
9
|
+
Array.isArray(s.attributes?.configuration?.lights) &&
|
|
10
|
+
s.attributes.configuration.lights.includes(c.entityId)).map(s => s.entity_id);
|
|
9
11
|
}
|
|
10
12
|
function select(c, states) {
|
|
11
13
|
if (c.adaptiveEntity) return c.adaptiveEntity;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
lights(s).includes(c.entityId) &&
|
|
15
|
-
(s.attributes?.configuration || Array.isArray(s.attributes?.manual_control)));
|
|
16
|
-
return candidates.length === 1 ? candidates[0].entity_id : null;
|
|
14
|
+
const matches = candidates(c, states);
|
|
15
|
+
return matches.length === 1 ? matches[0] : null;
|
|
17
16
|
}
|
|
18
17
|
function inspect(c, states) {
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
return { entity_id: entityId, active: c.colorMode === 'adaptive' && state?.state === 'on',
|
|
24
|
-
manual_control: manual, state: state?.state ?? null };
|
|
18
|
+
const matches = candidates(c, states), entityId = select(c, states);
|
|
19
|
+
const state = entityId ? states[entityId] : null;
|
|
20
|
+
return { entity_id: entityId, active: c.adaptiveEnabled && state?.state === 'off',
|
|
21
|
+
state: state?.state ?? null, candidates: matches };
|
|
25
22
|
}
|
|
26
23
|
async function apply(adapter, c, command) {
|
|
27
|
-
if (
|
|
24
|
+
if (!command.adaptiveEnabled || !command.offToOn || command.source !== 'automatic' || command.explicitColor) {
|
|
25
|
+
return { applied: false, reason: 'not_requested' };
|
|
26
|
+
}
|
|
27
|
+
if (!command.supportsTemperature) return { applied: false, reason: 'unsupported_color_temperature' };
|
|
28
28
|
const info = inspect(c, adapter.states());
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (!
|
|
29
|
+
const fail = (reason, message) => {
|
|
30
|
+
const error = fault(message, 'warning'); error.reason = reason; throw error;
|
|
31
|
+
};
|
|
32
|
+
if (!info.entity_id) fail(info.candidates.length > 1 ? 'ambiguous_configuration' : 'configuration_missing',
|
|
33
|
+
'Adaptive Lighting: no unique profile for this light; select a profile under Advanced.');
|
|
34
|
+
if (!['on', 'off'].includes(info.state)) fail('profile_unavailable', 'Adaptive Lighting profile is unavailable.');
|
|
35
|
+
// A running profile can intercept ON and continuously adapt brightness/color.
|
|
36
|
+
// One-shot apply also works with the main switch off, without installing listeners.
|
|
37
|
+
if (info.state !== 'off') fail('profile_running', 'Turn off the Adaptive Lighting main switch to use it as a one-shot profile.');
|
|
38
|
+
const configuration = adapter.states()[info.entity_id]?.attributes?.configuration;
|
|
39
|
+
if (configuration?.sleep_rgb_or_color_temp === 'rgb_color') fail('profile_uses_rgb',
|
|
40
|
+
'Adaptive Lighting profile uses RGB sleep color; choose a color-temperature sleep profile.');
|
|
41
|
+
if (!adapter.services()?.adaptive_lighting?.apply) fail('service_unavailable', 'Adaptive Lighting apply action is unavailable.');
|
|
33
42
|
await adapter.call('adaptive_lighting', 'apply', {
|
|
34
43
|
entity_id: info.entity_id, lights: [c.entityId], adapt_brightness: false,
|
|
35
|
-
adapt_color: true, turn_on_lights:
|
|
44
|
+
adapt_color: true, prefer_rgb_color: false, turn_on_lights: true,
|
|
45
|
+
transition: command.data.transition ?? 0
|
|
36
46
|
});
|
|
37
|
-
return { applied: true };
|
|
47
|
+
return { applied: true, entity_id: info.entity_id };
|
|
38
48
|
}
|
|
39
|
-
module.exports = { select, inspect, apply };
|
|
49
|
+
module.exports = { candidates, select, inspect, apply };
|
package/lib/light.js
CHANGED
|
@@ -3,10 +3,9 @@
|
|
|
3
3
|
const DEFAULTS = Object.freeze({
|
|
4
4
|
action: 'msg',
|
|
5
5
|
brightnessPct: 94, autoMaxPct: 94, minPct: 1, transitionOn: 1,
|
|
6
|
-
transitionOff: 1, manualOverride: true, overrideThreshold:
|
|
7
|
-
overrideStrategy: 'threshold', colorMode: '
|
|
8
|
-
mired: 370, fixedColor: '#ffffff', adaptiveEntity: ''
|
|
9
|
-
adaptiveDiscover: true, adaptiveApply: false, adaptiveRespectManual: true
|
|
6
|
+
transitionOff: 1, manualOverride: true, overrideThreshold: 98,
|
|
7
|
+
overrideStrategy: 'threshold', adaptiveEnabled: false, colorMode: 'keep', kelvin: 2700,
|
|
8
|
+
mired: 370, fixedColor: '#ffffff', adaptiveEntity: ''
|
|
10
9
|
});
|
|
11
10
|
const COLOR_KEYS = ['color_temp_kelvin', 'color_temp', 'rgbww_color', 'rgbw_color',
|
|
12
11
|
'rgb_color', 'hs_color', 'xy_color', 'color_name'];
|
|
@@ -34,6 +33,11 @@ function boolean(value, name) {
|
|
|
34
33
|
}
|
|
35
34
|
function config(raw) {
|
|
36
35
|
const c = { ...DEFAULTS, ...raw };
|
|
36
|
+
if (own(raw, 'adaptiveApply')) boolean(raw.adaptiveApply, 'adaptiveApply');
|
|
37
|
+
// Preserve opt-in from legacy flows; inactive legacy adaptive mode stays off.
|
|
38
|
+
c.adaptiveEnabled = own(raw, 'adaptiveEnabled') ? boolean(raw.adaptiveEnabled, 'adaptiveEnabled') :
|
|
39
|
+
raw.colorMode === 'adaptive' && boolean(raw.adaptiveApply ?? false, 'adaptiveApply');
|
|
40
|
+
if (c.colorMode === 'adaptive') c.colorMode = 'keep';
|
|
37
41
|
if (!['on', 'off', 'toggle', 'msg'].includes(c.action)) throw fault('Invalid configured action');
|
|
38
42
|
for (const key of ['brightnessPct', 'autoMaxPct', 'minPct', 'overrideThreshold']) {
|
|
39
43
|
c[key] = number(c[key], key, key === 'minPct' || key === 'overrideThreshold' ? 1 : 0, 100);
|
|
@@ -42,9 +46,7 @@ function config(raw) {
|
|
|
42
46
|
throw fault('Require minPct <= brightnessPct <= autoMaxPct');
|
|
43
47
|
}
|
|
44
48
|
for (const key of ['transitionOn', 'transitionOff']) c[key] = number(c[key], key, 0, 3600);
|
|
45
|
-
|
|
46
|
-
c[key] = boolean(c[key], key);
|
|
47
|
-
}
|
|
49
|
+
c.manualOverride = boolean(c.manualOverride, 'manualOverride');
|
|
48
50
|
if (c.manualOverride && c.overrideStrategy === 'threshold' && c.autoMaxPct >= c.overrideThreshold) {
|
|
49
51
|
throw fault('Automatic maximum must be below the manual override threshold');
|
|
50
52
|
}
|
|
@@ -138,6 +140,7 @@ function resolveAction(c, msg) {
|
|
|
138
140
|
function plan(c, msg, state, overrideActive) {
|
|
139
141
|
const action = resolveAction(c, msg);
|
|
140
142
|
const source = msg.source ?? 'automatic';
|
|
143
|
+
const adaptiveEnabled = own(msg, 'adaptive_lighting') ? boolean(msg.adaptive_lighting, 'adaptive_lighting') : c.adaptiveEnabled;
|
|
141
144
|
if (!['automatic', 'manual'].includes(source)) throw fault('msg.source must be automatic or manual');
|
|
142
145
|
const turningOn = action === 'on' || (action === 'toggle' && state.state === 'off');
|
|
143
146
|
const cap = capabilities(state), data = {}, unsupported = [];
|
|
@@ -180,8 +183,9 @@ function plan(c, msg, state, overrideActive) {
|
|
|
180
183
|
for (const key of Object.keys(data)) if (key !== 'transition') delete data[key];
|
|
181
184
|
if (!own(msg, 'transition') && cap.transition) data.transition = c.transitionOff;
|
|
182
185
|
}
|
|
183
|
-
return { action, source, service: turningOn && !zero ? 'turn_on' : 'turn_off',
|
|
184
|
-
turningOn: turningOn && !zero,
|
|
186
|
+
return { action, source, adaptiveEnabled, service: turningOn && !zero ? 'turn_on' : 'turn_off',
|
|
187
|
+
turningOn: turningOn && !zero, offToOn: turningOn && !zero && state.state === 'off',
|
|
188
|
+
supportsTemperature: cap.modes.includes('color_temp'), data, unsupported,
|
|
185
189
|
blocked: source === 'automatic' && overrideActive,
|
|
186
190
|
explicitColor: colors.length > 0,
|
|
187
191
|
ignoredConflicts: [...bKeys.slice(1), ...colors.slice(1).map(([key]) => key)] };
|
|
@@ -192,7 +196,7 @@ function normalized(state, override, adaptive, action) {
|
|
|
192
196
|
brightness: a.brightness ?? null, brightness_pct: Number.isFinite(a.brightness) ? Math.round(pct(a.brightness) * 10) / 10 : null,
|
|
193
197
|
color_mode: a.color_mode ?? null, color_temp_kelvin: a.color_temp_kelvin ?? (a.color_temp ? Math.round(1000000 / a.color_temp) : null),
|
|
194
198
|
color_temp: a.color_temp ?? null,
|
|
195
|
-
color: Object.fromEntries(['rgb_color','rgbw_color','rgbww_color','hs_color','xy_color'].filter(k => a[k]
|
|
199
|
+
color: Object.fromEntries(['rgb_color','rgbw_color','rgbww_color','hs_color','xy_color'].filter(k => Array.isArray(a[k])).map(k => [k, [...a[k]]])),
|
|
196
200
|
manual_override: override, adaptive_lighting: adaptive, action,
|
|
197
201
|
observed_at: state?.last_updated ?? null };
|
|
198
202
|
}
|
package/nodes/sw-light.html
CHANGED
|
@@ -26,20 +26,31 @@
|
|
|
26
26
|
minPct: { value: 1, validate: numeric('minPct') },
|
|
27
27
|
transitionOn: { value: 1, validate: numeric('transitionOn') },
|
|
28
28
|
transitionOff: { value: 1, validate: numeric('transitionOff') },
|
|
29
|
-
manualOverride: { value: true }, overrideThreshold: { value:
|
|
30
|
-
overrideStrategy: { value: 'threshold' }, colorMode: { value: '
|
|
29
|
+
manualOverride: { value: true }, overrideThreshold: { value: 98, validate: numeric('overrideThreshold') },
|
|
30
|
+
overrideStrategy: { value: 'threshold' }, colorMode: { value: 'keep' },
|
|
31
|
+
adaptiveEnabled: { value: false },
|
|
31
32
|
kelvin: { value: 2700, validate: numeric('kelvin') }, mired: { value: 370, validate: numeric('mired') },
|
|
32
33
|
fixedColor: { value: '#ffffff', validate: v => /^#[0-9a-f]{6}$/i.test(v) },
|
|
33
34
|
adaptiveEntity: { value: '', validate: v => !v || /^switch\.[a-z0-9_]+$/.test(v) },
|
|
34
|
-
adaptiveDiscover: { value: true }, adaptiveApply: { value: false }, adaptiveRespectManual: { value: true }
|
|
35
35
|
},
|
|
36
36
|
oneditprepare: function () {
|
|
37
|
+
if (this.colorMode === 'adaptive') {
|
|
38
|
+
$('#node-input-colorMode').val('keep');
|
|
39
|
+
$('#node-input-adaptiveEnabled').prop('checked', this.adaptiveApply === true);
|
|
40
|
+
}
|
|
37
41
|
let states = {}, request, generation = 0;
|
|
38
42
|
const entity = $('#node-input-entityId'), adaptive = $('#node-input-adaptiveEntity');
|
|
43
|
+
function modesForLight() { return states[entity.val()]?.attributes?.supported_color_modes || []; }
|
|
39
44
|
function update() {
|
|
40
45
|
const mode = $('#node-input-colorMode').val();
|
|
41
46
|
$('.sw-color-row').hide();
|
|
42
47
|
$('.sw-color-' + mode).show();
|
|
48
|
+
$('.sw-adaptive-row').toggle($('#node-input-adaptiveEnabled').prop('checked'));
|
|
49
|
+
const profiles = Object.values(states).filter(s => s.entity_id.startsWith('switch.') &&
|
|
50
|
+
Array.isArray(s.attributes?.configuration?.lights) && s.attributes.configuration.lights.includes(entity.val()));
|
|
51
|
+
$('#sw-adaptive-info').text(modesForLight().includes('color_temp') ?
|
|
52
|
+
(profiles.length === 1 ? 'Profile found. Keep its main switch off for one-shot use.' :
|
|
53
|
+
'No unique profile found. Select a profile under Advanced.') : 'This light has no known color-temperature support. Normal ON still works.');
|
|
43
54
|
$('.sw-override-row').toggle($('#node-input-manualOverride').prop('checked'));
|
|
44
55
|
$('.sw-threshold-row').toggle($('#node-input-manualOverride').prop('checked') && $('#node-input-overrideStrategy').val() === 'threshold');
|
|
45
56
|
const s = states[entity.val()], modes = s && s.attributes && s.attributes.supported_color_modes;
|
|
@@ -52,6 +63,7 @@
|
|
|
52
63
|
return function (req, respond) {
|
|
53
64
|
const term = req.term.toLowerCase();
|
|
54
65
|
respond(Object.values(states).filter(s => s.entity_id.startsWith(domain + '.') &&
|
|
66
|
+
(domain !== 'switch' || s.attributes?.configuration) &&
|
|
55
67
|
(s.entity_id.toLowerCase().includes(term) || String(s.attributes.friendly_name || '').toLowerCase().includes(term)))
|
|
56
68
|
.slice(0,100).map(s => ({ label: (s.attributes.friendly_name || s.entity_id) + ' (' + s.entity_id + ')', value: s.entity_id })));
|
|
57
69
|
};
|
|
@@ -71,12 +83,12 @@
|
|
|
71
83
|
.fail(function (_xhr, status) { if (status !== 'abort' && token === generation) $('#sw-capabilities').text('Server must be deployed and connected. Entity IDs can also be entered directly.'); });
|
|
72
84
|
}
|
|
73
85
|
$('#node-input-server').on('change.swLight', load);
|
|
74
|
-
$('#node-input-colorMode, #node-input-manualOverride, #node-input-overrideStrategy, #node-input-entityId').on('change.swLight', update);
|
|
86
|
+
$('#node-input-colorMode, #node-input-adaptiveEnabled, #node-input-manualOverride, #node-input-overrideStrategy, #node-input-entityId').on('change.swLight', update);
|
|
75
87
|
entity.on('focus.swLight', function () { entity.autocomplete('search', entity.val()); });
|
|
76
88
|
adaptive.on('focus.swLight', function () { adaptive.autocomplete('search', adaptive.val()); });
|
|
77
89
|
this._swCleanup = function () {
|
|
78
90
|
generation++; if (request) request.abort();
|
|
79
|
-
$('#node-input-server, #node-input-colorMode, #node-input-manualOverride, #node-input-overrideStrategy, #node-input-entityId, #node-input-adaptiveEntity').off('.swLight');
|
|
91
|
+
$('#node-input-server, #node-input-colorMode, #node-input-adaptiveEnabled, #node-input-manualOverride, #node-input-overrideStrategy, #node-input-entityId, #node-input-adaptiveEntity').off('.swLight');
|
|
80
92
|
entity.autocomplete('destroy'); adaptive.autocomplete('destroy');
|
|
81
93
|
delete this._swCleanup;
|
|
82
94
|
};
|
|
@@ -108,15 +120,16 @@
|
|
|
108
120
|
<p class="sw-override-row">Automatic commands pause during override. Manual commands use <code>msg.source = "manual"</code>.</p>
|
|
109
121
|
</fieldset>
|
|
110
122
|
<fieldset><legend>Color</legend>
|
|
111
|
-
<div class="form-row"><label for="node-input-colorMode">Mode</label><select id="node-input-colorMode"><option value="
|
|
123
|
+
<div class="form-row"><label for="node-input-colorMode">Mode</label><select id="node-input-colorMode"><option value="keep">Keep current</option><option value="kelvin">Fixed Kelvin</option><option value="mired">Fixed mired</option><option value="default">Lamp default</option><option value="color">Fixed color</option></select></div>
|
|
112
124
|
<div class="form-row sw-color-row sw-color-kelvin"><label for="node-input-kelvin">Kelvin</label><input id="node-input-kelvin" type="number" min="1000" max="40000"></div>
|
|
113
125
|
<div class="form-row sw-color-row sw-color-mired"><label for="node-input-mired">Mired</label><input id="node-input-mired" type="number" min="25" max="1000" step="0.1"></div>
|
|
114
126
|
<div class="form-row sw-color-row sw-color-color"><label for="node-input-fixedColor">Color</label><input id="node-input-fixedColor" type="color"></div>
|
|
115
|
-
<div class="form-row
|
|
116
|
-
<
|
|
117
|
-
<
|
|
118
|
-
|
|
119
|
-
|
|
127
|
+
<div class="form-row"><label for="node-input-adaptiveEnabled">Adaptive Lighting</label><input id="node-input-adaptiveEnabled" type="checkbox" style="width:auto"> On / Off</div>
|
|
128
|
+
<p class="sw-adaptive-row" id="sw-adaptive-info"></p>
|
|
129
|
+
<details class="sw-adaptive-row"><summary>Advanced: profile selection</summary>
|
|
130
|
+
<div class="form-row"><label for="node-input-adaptiveEntity">Profile</label><input id="node-input-adaptiveEntity" type="text" placeholder="Automatic (unique light membership)"></div>
|
|
131
|
+
<p>Select only when automatic discovery is unavailable or ambiguous. The profile's main switch must be off.</p>
|
|
132
|
+
</details>
|
|
120
133
|
</fieldset>
|
|
121
134
|
</script>
|
|
122
135
|
|
|
@@ -125,8 +138,8 @@
|
|
|
125
138
|
<h3>Inputs</h3>
|
|
126
139
|
<dl class="message-properties"><dt>action <span class="property-type">string</span></dt><dd>Required with From msg: <code>on</code>, <code>off</code>, or <code>toggle</code>. Falls back to <code>msg.auto_light.action</code> when msg.action is absent. Fixed Action settings ignore message actions.</dd><dt class="optional">source <span class="property-type">string</span></dt><dd><code>automatic</code> (default) or <code>manual</code>.</dd><dt class="optional">brightness_pct <span class="property-type">number</span></dt><dd>0–100. Automatic brightness is capped at the configured maximum.</dd><dt class="optional">transition <span class="property-type">number</span></dt><dd>Fade seconds, 0–3600, if supported.</dd><dt class="optional">manual_override <span class="property-type">boolean</span></dt><dd>Latch/release override with <code>source: "manual"</code>.</dd></dl>
|
|
127
140
|
<h3>Outputs</h3><ol><li>Original message plus <code>msg.sw_light</code>: last observed HA state, action, service data, and override/adaptive information.</li><li>Original message plus <code>msg.sw_light_event</code>: version, type, entity_id, timestamp, and details.</li></ol>
|
|
128
|
-
<h3>Details</h3><p>Default on brightness/max 94%, minimum 1%, fade 1s. ON at 100% (brightness 255) always blocks automatic ON, OFF and Toggle. 94%, 95% and 99% do not trigger this guard.
|
|
141
|
+
<h3>Details</h3><p>Default on brightness/max 94%, minimum 1%, fade 1s. ON at 100% (brightness 255) always blocks automatic ON, OFF and Toggle. 94%, 95% and 99% do not trigger this guard. Threshold detection defaults to 98% (brightness 250 or above) and pauses automatic commands; explicit overrides can also pause automation. Lowering brightness below the threshold releases its flag; turning off releases all override flags. Manual commands bypass the automatic maximum. Explicit flags require explicit release or off.</p>
|
|
129
142
|
<p>Top-level message properties override configuration. Brightness precedence: percent, byte, percent step, byte step. Color precedence: Kelvin, mired, RGBWW, RGBW, RGB, HS, XY, name. Invalid values produce errors; unsupported parameters emit an event and are omitted.</p>
|
|
130
|
-
<p>Adaptive
|
|
143
|
+
<p>Adaptive Lighting applies current color temperature once after automatic OFF → ON, with SW Light brightness and fade. Explicit message color wins; already-on brightness changes and manual commands never apply it. A unique profile is discovered automatically; Advanced selects a fallback. Keep its main switch off to prevent continuous control. Use a profile with color-temperature sleep color; RGB sleep profiles fall back with a warning. <code>msg.adaptive_lighting</code> overrides the checkbox for one command. Missing, ambiguous or running profiles emit a warning and retain normal configured color behavior. Keep current and lamp default both omit configured color and leave HA/device behavior in control.</p>
|
|
131
144
|
<p>Requires HA contrib 0.80.3. See package README for full contracts, examples and compatibility limitations.</p>
|
|
132
145
|
</script>
|
package/nodes/sw-light.js
CHANGED
|
@@ -11,7 +11,7 @@ module.exports = function register(RED, adapterFactory = createAdapter) {
|
|
|
11
11
|
const node = this;
|
|
12
12
|
node.log?.(`[SW Light debug] version=${require('../package.json').version} constructor=created serverIdPresent=${Boolean(raw.server)}`);
|
|
13
13
|
let c, detector, adapter, closed = false, lastAvailability;
|
|
14
|
-
let queue = Promise.resolve(), startup = Promise.resolve();
|
|
14
|
+
let queue = Promise.resolve(), startup = Promise.resolve(), observerTimer;
|
|
15
15
|
const protectedAtFullBrightness = state => state?.state === 'on' && state.attributes?.brightness === 255;
|
|
16
16
|
const overrideActive = state => detector.active || protectedAtFullBrightness(state);
|
|
17
17
|
function event(type, detail = {}, msg = {}, send = node.send.bind(node)) {
|
|
@@ -34,11 +34,11 @@ module.exports = function register(RED, adapterFactory = createAdapter) {
|
|
|
34
34
|
if (lastAvailability !== availability) event('unavailable', { reason: availability });
|
|
35
35
|
} else {
|
|
36
36
|
const active = overrideActive(state);
|
|
37
|
-
const info = light.normalized(state, active, c.
|
|
37
|
+
const info = light.normalized(state, active, c.adaptiveEnabled, 'state_changed');
|
|
38
38
|
let text = active ? 'MANUAL' : `AUTO · ${info.state.toUpperCase()}`;
|
|
39
39
|
if (info.state === 'on' && info.brightness_pct !== null) text += ` · ${info.brightness_pct}%`;
|
|
40
40
|
if (info.state === 'on' && info.color_temp_kelvin) text += ` · ${info.color_temp_kelvin}K`;
|
|
41
|
-
if (info.state === 'on' && c.
|
|
41
|
+
if (info.state === 'on' && c.adaptiveEnabled) text += ' · adaptive';
|
|
42
42
|
node.status({ fill: active ? 'yellow' : info.state === 'on' ? 'green' : 'grey', shape: 'dot', text });
|
|
43
43
|
}
|
|
44
44
|
lastAvailability = availability;
|
|
@@ -60,12 +60,26 @@ module.exports = function register(RED, adapterFactory = createAdapter) {
|
|
|
60
60
|
});
|
|
61
61
|
connection(adapter.connected());
|
|
62
62
|
}
|
|
63
|
+
function startObserving() {
|
|
64
|
+
if (closed || node.swLightError) return;
|
|
65
|
+
startup = Promise.resolve().then(async () => {
|
|
66
|
+
if (!adapter) startAdapter();
|
|
67
|
+
if (adapter.resolve) await adapter.resolve();
|
|
68
|
+
}).catch(error => {
|
|
69
|
+
if (closed) return;
|
|
70
|
+
event(error.eventType || 'error', { message: error.message, phase: 'state_subscription' });
|
|
71
|
+
observerTimer = setTimeout(startObserving, 1000);
|
|
72
|
+
observerTimer.unref?.();
|
|
73
|
+
});
|
|
74
|
+
}
|
|
63
75
|
try {
|
|
64
76
|
c = light.config(raw);
|
|
65
77
|
detector = new light.Override(c);
|
|
66
|
-
//
|
|
67
|
-
//
|
|
78
|
+
// Defer provider resolution until after Flow.start. Observe even
|
|
79
|
+
// before this node's first command; retry temporary startup failures.
|
|
68
80
|
node.status({ fill: 'red', shape: 'ring', text: 'HA NOT READY' });
|
|
81
|
+
observerTimer = setTimeout(startObserving, 0);
|
|
82
|
+
observerTimer.unref?.();
|
|
69
83
|
} catch (error) {
|
|
70
84
|
node.status({ fill: 'red', shape: 'ring', text: 'CONFIG ERROR' });
|
|
71
85
|
node.error(error.message);
|
|
@@ -107,15 +121,23 @@ module.exports = function register(RED, adapterFactory = createAdapter) {
|
|
|
107
121
|
serviceCompleted = true;
|
|
108
122
|
if (closed) { done(); return; }
|
|
109
123
|
let adaptiveResult;
|
|
110
|
-
try {
|
|
111
|
-
|
|
124
|
+
try {
|
|
125
|
+
adaptiveResult = await adaptive.apply(adapter, c, command);
|
|
126
|
+
if (adaptiveResult.reason === 'unsupported_color_temperature') event('unsupported',
|
|
127
|
+
{ phase: 'adaptive_apply', reason: adaptiveResult.reason, light_service_completed: true }, msg, send);
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
adaptiveResult = { applied: false, reason: error.reason || 'apply_failed' };
|
|
131
|
+
event('warning', { message: error.message, reason: adaptiveResult.reason,
|
|
132
|
+
phase: 'adaptive_apply', light_service_completed: true }, msg, send);
|
|
133
|
+
}
|
|
112
134
|
if (closed) { done(); return; }
|
|
113
135
|
const current = adapter.state(c.entityId);
|
|
114
136
|
observe(current);
|
|
115
|
-
send([{ ...msg, sw_light: { ...light.normalized(current, overrideActive(current),
|
|
137
|
+
send([{ ...msg, sw_light: { ...light.normalized(current, overrideActive(current), command.adaptiveEnabled, command.service),
|
|
116
138
|
entity_id: c.entityId, source: command.source, requested_action: command.action,
|
|
117
139
|
service_data: command.data, ignored_conflicts: command.ignoredConflicts,
|
|
118
|
-
state_confirmation: 'last_observed', adaptive: { ...adaptive.inspect(c, adapter.states()), ...adaptiveResult } } }, null]);
|
|
140
|
+
state_confirmation: 'last_observed', adaptive: { ...adaptive.inspect({ ...c, adaptiveEnabled: command.adaptiveEnabled }, adapter.states()), ...adaptiveResult } } }, null]);
|
|
119
141
|
const level = command.data.brightness === undefined ? '' : ` · ${Math.round(light.pct(command.data.brightness))}%`;
|
|
120
142
|
node.status({ fill: command.source === 'manual' ? 'yellow' : command.turningOn ? 'green' : 'grey', shape: 'dot',
|
|
121
143
|
text: `${command.source === 'manual' ? 'MANUAL' : 'AUTO'} · ${command.turningOn ? 'ON' : 'OFF'}${level}` });
|
|
@@ -132,6 +154,7 @@ module.exports = function register(RED, adapterFactory = createAdapter) {
|
|
|
132
154
|
});
|
|
133
155
|
node.on('close', (_removed, done) => {
|
|
134
156
|
closed = true;
|
|
157
|
+
clearTimeout(observerTimer);
|
|
135
158
|
Promise.resolve().then(() => adapter?.close()).then(() => startup).catch(() => {}).finally(() => done());
|
|
136
159
|
});
|
|
137
160
|
}
|