homebridge-powerview-universal 4.3.2

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 ADDED
@@ -0,0 +1,338 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ ## [4.3.2] - 2026-09-04
6
+
7
+ ### Fixed
8
+
9
+ - Lint error in the test suite that failed the release build (`no-unused-vars`). No runtime change.
10
+
11
+ ## [4.3.1] - 2026-09-04
12
+
13
+ ### Fixed
14
+
15
+ - **Scene activation no longer reports a shade count it does not have.** `activateScene` defaulted
16
+ a missing `shadeIds` to an empty list, so a gen1 hub — which answers an activation without that
17
+ field — produced `Scene 4102 activated (0 shade(s))` while five shades were visibly moving. The
18
+ count is now logged only when the hub supplies one.
19
+
20
+ ### Notes
21
+
22
+ - Scene activation is no longer unverified: it was confirmed end to end against a live scene on a
23
+ gen1 hub (build 827), activated from the HomeKit switch, moving five shades to the scene's
24
+ position.
25
+
26
+ ## [4.3.0] - 2026-09-04
27
+
28
+ ### Added
29
+
30
+ - **Hub scenes are exposed as HomeKit switches** (`exposeScenes`, default on). Activating one is a
31
+ single hub call that the hub expands itself, so every motor gets its RF command at once. Driving
32
+ the same shades individually costs one serialised write each and they visibly stagger — this is
33
+ why a Pebble remote closes a group in unison and the plugin could not. Nothing appears unless
34
+ scenes are defined in the PowerView app. Switches are stateless: they fire and reset.
35
+ - Scene accessories survive restarts, and are never removed on an empty scene list — the same
36
+ rule that stopped a partial response destroying shade accessories in 4.1.2.
37
+
38
+ ### Changed
39
+
40
+ - **Shades are reported as moving rather than arrived.** `setPosition` fed the hub's PUT reply
41
+ into `CurrentPosition`, and that reply echoes the target, so a shade with seconds of travel left
42
+ read as already there; `PositionState` was hardcoded to `STOPPED`, the only value it ever took.
43
+ `TargetPosition` is now the commanded value, `CurrentPosition` stays where the shade is, and the
44
+ state reports `INCREASING`/`DECREASING` until the estimated travel elapses. A re-target
45
+ mid-travel cancels the previous arrival, which would otherwise land later and park the shade at
46
+ a position it had been steered away from.
47
+ - **Startup no longer waits on the position sync.** It does an RF read per shade the hub has no
48
+ position for — five motors and 82 seconds from init to ready in one observed startup. It now
49
+ runs behind launch and abandons itself on shutdown.
50
+
51
+ ### Notes
52
+
53
+ - Arrival is estimated from `SHADE_FULL_TRAVEL_MS` (20s for the full range) rather than polled.
54
+ Confirming it would mean an RF round-trip that wakes the motor again the instant it stopped. Any
55
+ later refresh corrects the value.
56
+ - Scene **activation** is implemented against the documented PowerView v2 API but has not been
57
+ verified against a live scene, because the hub it was developed on has none defined.
58
+
59
+ ## [4.2.0] - 2026-09-04
60
+
61
+ ### Changed
62
+
63
+ - **HomeKit position reads no longer wait on the hub.** The non-blocking work in 1061351 removed
64
+ only the RF refresh from the read path; every read still awaited a cached hub round-trip. Those
65
+ are serialised, so opening the Home app — which reads every characteristic of every shade at
66
+ once — queued them and the later ones exceeded HomeKit's budget, logging "read handler was slow
67
+ to respond" (15 in one startup on a five-shade system). A read with a cached position now
68
+ answers immediately and refreshes behind the answer. `refreshShades` and `strictErrors` both opt
69
+ into hitting the hub and keep the blocking path.
70
+ - **Background refreshes no longer wake the motor.** `scheduleBackgroundRefresh` always passed
71
+ `refresh=true`, an RF round-trip that spins the shade and takes seconds. That is now reserved
72
+ for the case it was meant for — the hub having no position at all. Servicing an ordinary read
73
+ uses the cheap cached read, rate limited to one per shade per
74
+ `BACKGROUND_REFRESH_INTERVAL_MS` (10s) so a burst of reads cannot stack up.
75
+
76
+ ## [4.1.2] - 2026-09-04
77
+
78
+ ### Fixed
79
+
80
+ - **A partial hub response no longer destroys HomeKit accessories.** `updateShades()`
81
+ unregistered any accessory missing from a *single* `/api/shades` response. Upstream guarded
82
+ against the list request outright failing, but not against it coming back short — and a gen1
83
+ hub under load does answer short. Observed live: three of five shades were unregistered in one
84
+ poll, taking their rooms and automations with them, and "close all" then moved only the two
85
+ that survived. A shade must now be absent from `SHADE_REMOVAL_THRESHOLD` (3) consecutive lists
86
+ before its accessory is removed, each miss is logged, and an empty list never prunes anything.
87
+
88
+ ## [4.1.1] - 2026-09-04
89
+
90
+ ### Fixed
91
+
92
+ - **A dropped connection no longer loses the command outright.** Only HTTP 423 was retried, so a
93
+ bare `fetch failed` — which is what a gen1 hub gives you when it drops a TCP connection while
94
+ its radio is transmitting — threw immediately and the shade never moved. Transient network
95
+ failures now retry three times, 500ms apart.
96
+
97
+ ### Changed
98
+
99
+ - **`requestIntervalMs` returns to a 100ms default**, reverting the 25ms introduced in 4.1.0. The
100
+ probe behind that number ran cached reads, which never engage the hub's radio; writes do. In
101
+ practice a five-shade group move at 25ms lost two shades to dropped connections. The option
102
+ stays, so the spacing can still be retuned — against writes, not just reads.
103
+
104
+ ## [4.1.0] - 2026-09-04
105
+
106
+ ### Added
107
+
108
+ - `requestIntervalMs` (default `25`), exposed in the Homebridge UI schema. Spacing between
109
+ serialised hub requests, so a hub that struggles under load can be given more room without a
110
+ code change.
111
+
112
+ ### Changed
113
+
114
+ - **HomeKit writes now overtake background reads.** The request queue was strictly FIFO, so a
115
+ shade command issued while a refresh was in flight waited for that refresh and for every read
116
+ queued ahead of it. Writes now run first, FIFO within a priority, still one request at a time
117
+ because the hub cannot do better.
118
+ - **Request spacing drops from 100ms to 25ms.** Measured, not guessed: 30 serialised reads
119
+ against a gen1 hub on build 827 at 100/50/25/10/0ms spacing returned 30/30 valid responses in
120
+ every condition — no bad status, no malformed JSON, no timeouts — with response times flat at
121
+ ~70ms. 25ms keeps headroom rather than removing the guard.
122
+ - The spacing delay no longer runs after the final request, only between requests.
123
+
124
+ ### Notes
125
+
126
+ - This takes roughly 375ms off a five-shade group move, but does not make shades move in unison.
127
+ Five sequential round-trips are ~350ms of irreducible spread. Only a hub scene, which is one
128
+ call the hub expands itself, can move a group together — scene support is not implemented yet.
129
+
130
+ ## [4.0.0] - 2026-09-04
131
+
132
+ First release under the name **homebridge-powerview-universal**. Consolidates the
133
+ `3.1.4-local.1` … `3.1.4-local.8` working versions, none of which were published.
134
+ All hub behaviour below was verified against a PowerView hub on firmware build 827.
135
+
136
+ Major rather than minor: the npm package is renamed, so this does not upgrade in
137
+ place — the old package must be uninstalled and this one installed. `PLUGIN_NAME`
138
+ changes with it, which migrates the cached accessories. Battery polling also
139
+ changes from on-by-default to off-by-default.
140
+
141
+ ### Added
142
+
143
+ - `batteryPolling` (boolean, default `false`) and `batteryPollAt` (`"HH:MM"`, default `14:00`)
144
+ config options, both exposed in the Homebridge UI schema.
145
+ - `syncPositionsOnStart` (boolean, default `true`) and `quietHours` (default 21:00–08:00).
146
+ Homebridge can restart unattended, so the startup position sync is skipped inside the quiet
147
+ window rather than being free to wake every motor at 03:00.
148
+ - `PowerViewHub.stopShade()`; `jogShade` and `stopShade` now share one `motionRequest` path.
149
+ - An ISC `LICENSE` file, naming the full copyright chain from 2018 onward.
150
+
151
+ ### Changed
152
+
153
+ - **Battery polling is now opt-in and off by default.** It previously ran every 6 hours on a
154
+ timer anchored to plugin start, with no config option. `updateBatteryLevel=true` is an RF
155
+ round-trip that wakes the shade motor — 3.73s against 0.08s for a cached read, audible, and it
156
+ nudges the shade. On a 6-hour period at least one poll always landed overnight regardless of
157
+ restart time; observed waking a household at 05:54 (5 shades). The hub already refreshes
158
+ battery weekly on its own, which is what makes off-by-default correct rather than merely quiet.
159
+ - Polling, when enabled, runs **once per day at a configurable local time** instead of on an
160
+ interval. The next deadline is recomputed from the current time after every run, so the poll
161
+ stays pinned to the same wall-clock time across DST changes. Runs are logged at `info`; the old
162
+ poll logged nothing on success, which is why the shade movement could not be attributed.
163
+ - **Shade behaviour is resolved from ShadeCapabilities, not from a hand-picked type list.** The
164
+ old `SHADE_TYPE_IDS` knew 6 shade types; the PowerView Hub REST API v2 appendix
165
+ (jlaur/hdpowerview-doc v1.0.4) documents **26**, mapped onto 10 capability classes. That left
166
+ 20 types falling through to "unknown type, assuming roller", and for 17 of them roller is
167
+ wrong. Capability now comes from the hub's own `capabilities` field when it reports one, and
168
+ from the documented type table otherwise — older hubs, including build 827, omit the field.
169
+ - **Unsupported capabilities are stated rather than silently mistreated.** Capability 6 runs its
170
+ primary rail *reversed*, 2 and 5 tilt through 180 degrees rather than the 90 the tilt maths
171
+ assumes, and 8/9 have overlapped panels. These are still driven as rollers, but now warn once
172
+ per shade that position or tilt may be wrong instead of reporting a confident wrong number.
173
+ Implementing their maths needs hardware to verify against, so it is not guessed at here.
174
+ - **Every hub request is serialised through one promise chain** with `REQUEST_INTERVAL_MS`
175
+ spacing. Legacy hubs answer one request at a time, but only shade requests went through the
176
+ existing queue — capability probes and `/api/shades` called `fetchJson` directly and raced it,
177
+ which made the hub time out and return truncated JSON mid-response.
178
+ - **HomeKit position reads answer from cache immediately** and refresh in the background, deduped
179
+ per shade. The old blocking refresh inside the read handler routinely exceeded HomeKit's read
180
+ budget and logged "read handler didn't respond at all". `strictErrors` keeps the old blocking
181
+ behaviour, since that option exists to surface hub failures.
182
+ - Positions are logged only when they change, and the message says what it is. `Set for <id>`
183
+ fired at `info` on every read — four identical lines in twelve seconds during one restart — and
184
+ read like a write when it was a read.
185
+ - Errors preserve the underlying failure as `cause`, and timeouts are distinguished from
186
+ unreachable hosts instead of every failure reporting as Unreachable.
187
+ - `PositionMap` is defined once in `shadeUtils.ts` instead of being redeclared in `platform.ts`.
188
+ - **A poll costs half the hub requests it did.** `/api/shades` already returns each shade, but the
189
+ loop then called `getShade(id)` for every one, so N shades meant 2N serialised requests per
190
+ cycle. The per-shade fetch now runs only when the list entry carried no positions.
191
+ - Package renamed to `homebridge-powerview-universal`; repository, bugs, and homepage links now
192
+ point at this fork rather than upstream's tracker.
193
+
194
+ ### Fixed
195
+
196
+ - **Hub requests still overlapped after being serialised.** `serialize()` wrapped only the
197
+ `fetch()` call, and `fetch()` settles when response headers arrive, not when the body is
198
+ consumed — so `response.json()` ran after the chain had already advanced, streaming one
199
+ request's body while the next request was already in flight. That is the overlap that makes a
200
+ legacy hub time out and return truncated JSON. The body is now read inside the serialised
201
+ section.
202
+ - **A tilt report no longer forces the shade to read as fully closed.** Both vanes branches
203
+ called `applyCoveringPosition(service, 0)`, so `posKind1` set the real position and `posKind2`
204
+ immediately overwrote it with 0 in the same loop. Every tilt-capable shade reported itself
205
+ closed while the cache held the true value.
206
+ - **Restarts no longer report a shade as fully closed.** `configure()` seeded `CurrentPosition`
207
+ to 0 unconditionally, running on every restart immediately after the cache was restored — so
208
+ the persisted position was overwritten before HomeKit could read it. It now seeds from the
209
+ cache for both rails.
210
+ - **The shade poll no longer survives shutdown.** Its `setTimeout` handle was never stored, so
211
+ the shutdown listener could not clear it; with `pollShadesForUpdate` enabled, shutdown left a
212
+ 30s loop hitting the hub and holding the event loop open.
213
+ - **A position set for a removed shade reports an error instead of crashing.** `setPosition` read
214
+ `this.accessories[shadeId].context` unguarded, before its `try`, so the throw escaped the
215
+ HomeKit handler as an unhandled rejection.
216
+ - **Every reported position kind is read.** The loop terminated at the first absent `posKind` and
217
+ silently dropped every kind after a gap.
218
+ - **An unusable position value is skipped rather than cached as `NaN`.** HAP rejected the
219
+ characteristic write, but the `NaN` still reached the position map, where it defeats
220
+ `positionMapsEqual` — `NaN !== NaN` — so every subsequent read rewrote the accessory cache file.
221
+ - **Requests could hang forever.** Node's `fetch` has no default timeout and the hub serialises
222
+ every call through one queue, so a half-open socket stalled all later requests indefinitely.
223
+ Every hub request now carries a 15s `AbortController` timeout.
224
+ - **A throw during shade URL construction wedged the queue**, leaving pending HomeKit requests
225
+ unresolved. The URL is built inside the `try` and the queue advances in a `finally`.
226
+ - **Battery percentage was wrong above 100.** `batteryStrength` is tenths of a volt against an
227
+ 18.0V nominal pack (matching aiopvapi), not a 0–100 percentage. Any Gen 1/2 reading above 100
228
+ fell through to a four-value `batteryStatus` lookup, so a pack at 67% and one at 98% both
229
+ reported 90%.
230
+ - **`batteryStrength` 0 is treated as unknown rather than empty.** The hub reports 0 for shades it
231
+ has not polled, which raised a false low-battery warning.
232
+ - **`StatusLowBattery` is no longer set on `AccessoryInformation`**, where it is not a valid
233
+ characteristic and logged a warning per shade per refresh.
234
+ - **Hub generation is derived from the firmware revision.** Both Gen 1 and Gen 2 report
235
+ `mainProcessor.name` as "PowerView Hub", so the name never matched.
236
+ - **`Invalid position value received` no longer fires on the happy path.** This hub returns no
237
+ `positions` object at all on a cached read — positions exist only after a `refresh=true` read or
238
+ a set — so the key was absent on *every* HomeKit position read and the warning fired 77 times in
239
+ one day. Absent is now `debug` plus a once-per-shade `info`; only a key present with unusable
240
+ data still warns, and the message includes the value.
241
+ - **Last known positions persist across restarts**, written to accessory context via
242
+ `api.updatePlatformAccessories()` and restored (validated) in `configureAccessory`. A cold cache
243
+ no longer falls through to `resolvePositionValue()`'s `0`, which reads as "fully closed" in the
244
+ Home app until the background refresh lands, and sticks if that refresh times out. Writes are
245
+ guarded by `positionMapsEqual` so an ordinary read that changes nothing does not rewrite the
246
+ cache file.
247
+ - **Positions are re-synced once at startup** for shades the hub has no position for. Persisting
248
+ the cache meant `value == null` was never true, so `scheduleBackgroundRefresh` never fired and a
249
+ stale position could be served indefinitely.
250
+ - **HoldPosition jogged the shade instead of stopping it.** HomeKit's `HoldPosition` was wired to
251
+ `jogShade`, so asking a shade to stop made it wiggle. It now sends `motion: "stop"`, and is no
252
+ longer gated on `jogSupported`, which was the wrong capability.
253
+ - **`PLUGIN_NAME` now matches the package name.** It still read `homebridge-powerview-3` after the
254
+ rename, so Homebridge could not resolve the plugin when registering accessories and logged a
255
+ warning for every shade.
256
+ - `probeEndpoint` is given an absolute URL so it cannot throw if it is ever used.
257
+
258
+ ### Removed
259
+
260
+ - `SHADE_TYPE_IDS`. It listed type 16, which appears nowhere in the published table, and omitted
261
+ the actual vertical shades (54, 55, 56).
262
+
263
+ ### Notes
264
+
265
+ - Disabling the battery poll does not remove battery reporting: `batteryStrength` /
266
+ `batteryStatus` ride along on ordinary hub reads and still update the HomeKit Battery service.
267
+ The poll only forces a fresh measurement from the shade.
268
+ - On first start under the new package name, Homebridge re-associates cached accessories from
269
+ `homebridge-powerview-3` to `homebridge-powerview-universal` automatically and logs that it did
270
+ so. Uninstall the old package first — if both are installed, both claim the `PowerView` platform
271
+ name and cached accessories are dropped instead.
272
+
273
+ ## [3.1.2] - 2026-05-18
274
+
275
+ ### Added
276
+
277
+ - [RELEASE_NOTES.md](RELEASE_NOTES.md) for publish/GitHub release copy (see Latest section when tagging)
278
+
279
+ ## [3.1.1] - 2026-05-18
280
+
281
+ ### Added
282
+
283
+ - Linked HomeKit **Battery** service per shade with `BatteryLevel` (0–100%) so battery appears in the Home app, not only the low-battery flag on Accessory Information
284
+
285
+ ## [3.1.0] - 2026-05-18
286
+
287
+ ### Added
288
+
289
+ - Structured hub HTTP handling with retry on HTTP 423 (hub busy / maintenance)
290
+ - Startup capability probe for `/api/fwversion`, `/api/scenes`, and `/api/scenecollections` (logged; scenes not exposed in HomeKit yet)
291
+ - Position cache: refresh timeouts and transient read failures return last known values (unless `strictErrors` is enabled)
292
+ - `strictErrors` platform option for fail-fast HomeKit reads (debugging)
293
+ - Per-shade `FirmwareRevision` and `StatusLowBattery` in Accessory Information when the hub reports them
294
+ - Periodic battery refresh via `updateBatteryLevel` (every 6 hours)
295
+ - Top/bottom shade top-rail display name from hub `secondaryName`
296
+ - **Hold** (Hold Position) and **Identify** wired to hub jog motion where supported
297
+ - Vitest unit tests for HTTP classification, position parsing, and battery helpers
298
+
299
+ ### Changed
300
+
301
+ - `putShade` queue merge skips invalid position kind 4 (hub position error) from PUT bodies
302
+ - Shade list failures during poll no longer remove existing accessories
303
+
304
+ ## [3.0.0] - 2026-05-17
305
+
306
+ ### Added
307
+
308
+ - Homebridge **2.x** support (ESM plugin, Node.js 22/24)
309
+ - TypeScript source, `config.schema.json` for Homebridge UI settings
310
+ - `AccessoryInformation` service on each shade (manufacturer, hub firmware model)
311
+
312
+ ### Changed
313
+
314
+ - **Breaking:** npm package renamed to `homebridge-powerview-3`
315
+ - **Breaking:** plugin identifier for accessory cache is now `homebridge-powerview-3` (was `homebridge-powerview`)
316
+ - Replaced deprecated `request` with native `fetch`
317
+ - Replaced `getServiceByUUIDAndSubType` with `getServiceById` (Homebridge 2 API)
318
+ - Removed use of removed `accessory.reachable` property
319
+ - Modern `api.registerPlatform(PLATFORM_NAME, PowerViewPlatform)` registration
320
+ - Hub HTTP client uses Promises/async throughout
321
+
322
+ ### Fixed
323
+
324
+ - `putShade` queue merge incorrectly deleted bottom position (`delete position[...]` → `delete positions[...]`)
325
+ - `jogShade` callback referenced undefined `position` variable
326
+ - `removeService` called when top Window Covering service did not exist
327
+
328
+ ### Migration from homebridge-powerview-2
329
+
330
+ 1. Uninstall `homebridge-powerview-2`, install `homebridge-powerview-3`.
331
+ 2. Keep `"platform": "PowerView"` in `config.json`; add `"name": "PowerView"` if using the config UI schema.
332
+ 3. Restart Homebridge. Remove duplicate shade accessories from the Home app if the cache identifier change left orphans.
333
+
334
+ Requires Homebridge **^1.8.0 || ^2.0.0** and Node **^22.12.0 || ^24.0.0**.
335
+
336
+ ## [1.0.9] and earlier
337
+
338
+ See [homebridge-powerview-2](https://github.com/owenselles/homebridge-powerview-2) history.
package/LICENSE ADDED
@@ -0,0 +1,18 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2018 Scott James Remnant
4
+ Copyright (c) 2023 Owen Selles
5
+ Copyright (c) 2026 squircle12
6
+ Copyright (c) 2026 sunnierseattle
7
+
8
+ Permission to use, copy, modify, and/or distribute this software for any
9
+ purpose with or without fee is hereby granted, provided that the above
10
+ copyright notice and this permission notice appear in all copies.
11
+
12
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
13
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
14
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
15
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
16
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
17
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
18
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,129 @@
1
+ # homebridge-powerview-universal
2
+
3
+ [![npm](https://img.shields.io/npm/v/homebridge-powerview-universal.svg)](https://www.npmjs.com/package/homebridge-powerview-universal)
4
+ [![npm](https://img.shields.io/npm/dt/homebridge-powerview-universal.svg)](https://www.npmjs.com/package/homebridge-powerview-universal)
5
+
6
+ Homebridge plugin for [Hunter Douglas PowerView](https://www.hunterdouglas.com/operating-systems/motorized/powerview-motorization) window shades. Compatible with **Homebridge 1.8+** and **Homebridge 2.x** (Node.js 22 or 24).
7
+
8
+ Supports Generation 1 and 2 PowerView hubs.
9
+
10
+ > **Credits.** This is a fork of [squircle12/homebridge-powerview-3](https://github.com/squircle12/homebridge-powerview-3), itself a TypeScript rewrite of [owenselles/homebridge-powerview-2](https://github.com/owenselles/homebridge-powerview-2), originally by [Scott James Remnant](https://github.com/keybuk). The full upstream commit history is preserved in this repository. This fork adds fixes to hub request serialisation, position reporting, battery scaling, and shade capability detection.
11
+
12
+ ## Requirements
13
+
14
+ - [Homebridge](https://github.com/homebridge/homebridge) **1.8.0** or **2.0.0**
15
+ - Node.js **22** or **24**
16
+
17
+ ## Supported shades
18
+
19
+ - Roller shades
20
+ - Horizontal vane shades (e.g. Silhouette, Pirouette) — position plus tilt in Details
21
+ - Vertical vane shades (e.g. Luminette) — position plus tilt in Details
22
+ - Top-down/bottom-up shades (e.g. Duette) — two Window Covering services per shade
23
+
24
+ Shades work in HomeKit scenes and automations.
25
+
26
+ ## Installation
27
+
28
+ 1. Install and set up [Homebridge](https://github.com/homebridge/homebridge).
29
+ 2. Install the plugin (Homebridge UI **Plugins** tab, or CLI):
30
+
31
+ ```bash
32
+ npm install -g homebridge-powerview-universal
33
+ ```
34
+
35
+ 3. Add the **PowerView** platform via the plugin **Settings** button in the Homebridge UI, or add a platform block to `config.json`:
36
+
37
+ ```json
38
+ "platforms": [
39
+ {
40
+ "platform": "PowerView",
41
+ "name": "PowerView"
42
+ }
43
+ ]
44
+ ```
45
+
46
+ The hub is contacted at `powerview-hub.local` by default.
47
+
48
+ ## Configuration
49
+
50
+ | Option | Description | Default |
51
+ |--------|-------------|---------|
52
+ | `name` | Platform name in Homebridge | `PowerView` |
53
+ | `host` | Hub hostname or IP | `powerview-hub.local` |
54
+ | `refreshShades` | Request fresh positions from the hub on every HomeKit read | `false` |
55
+ | `pollShadesForUpdate` | Poll the hub every 30 seconds for position updates | `false` |
56
+ | `strictErrors` | Fail HomeKit reads on hub errors instead of returning the last known position | `false` |
57
+ | `forceRollerShades` | Shade IDs to treat as roller | `[]` |
58
+ | `forceTopBottomShades` | Shade IDs to treat as top/bottom | `[]` |
59
+ | `forceHorizontalShades` | Shade IDs to treat as horizontal vane | `[]` |
60
+ | `forceVerticalShades` | Shade IDs to treat as vertical vane | `[]` |
61
+
62
+ Example with host and polling:
63
+
64
+ ```json
65
+ {
66
+ "platform": "PowerView",
67
+ "name": "PowerView",
68
+ "host": "192.168.1.50",
69
+ "pollShadesForUpdate": true
70
+ }
71
+ ```
72
+
73
+ ### Unknown shade types
74
+
75
+ If the hub reports an unknown shade type, the log may show:
76
+
77
+ ```
78
+ Shade 12345 has unknown type 66, assuming roller
79
+ ```
80
+
81
+ Please [open an issue](https://github.com/sunnierseattle/homebridge-powerview-universal/issues) with the shade model. You can override detection with the `force*` arrays above.
82
+
83
+ ### Hub resilience and optional features
84
+
85
+ - If the hub is busy (HTTP 423, maintenance), shade requests are retried automatically.
86
+ - When a position refresh times out or the hub is unreachable, HomeKit reads return the **last known position** unless `strictErrors` is enabled.
87
+ - **Battery** percentage appears in the Home app (via a linked HomeKit Battery service on each shade) when the hub reports `batteryStrength` or `batteryStatus`; levels are refreshed about every 6 hours. Window Covering does not expose battery directly—only the standard Battery service does.
88
+ - **Jog** (short nudge) is triggered from **Hold** on the shade control where supported, and when you **Identify** the accessory in the Home app while pairing.
89
+ - PowerView **scenes** and **multi-room scene collections** are probed at startup for future use but are **not** exposed as HomeKit accessories yet.
90
+
91
+ ## Migrating from homebridge-powerview-2
92
+
93
+ 1. Stop Homebridge.
94
+ 2. Uninstall the old plugin: `npm uninstall -g homebridge-powerview-2`
95
+ 3. Install `homebridge-powerview-universal`.
96
+ 4. Update your config: keep `"platform": "PowerView"` and add `"name": "PowerView"` if missing.
97
+ 5. Remove any old **PowerView** platform entry that pointed at the v2 package, then add the platform again for v3.
98
+ 6. Restart Homebridge.
99
+
100
+ The plugin identifier changed from `homebridge-powerview` to `homebridge-powerview-universal`. Cached accessories from the old package may appear as duplicates in the Home app. Remove ghost accessories from Home if needed.
101
+
102
+ ## Shade examples
103
+
104
+ Tap an accessory to open/close; long-press for a custom position.
105
+
106
+ ### Horizontal and vertical vanes
107
+
108
+ Use **Details** after a long-press to adjust tilt. For scenes: use **Closed** when setting vane tilt; use **0°** tilt when setting position.
109
+
110
+ ### Top-down/bottom-up
111
+
112
+ Two controls are created per shade (bottom and top), which can be used independently or in scenes.
113
+
114
+ ## Releases
115
+
116
+ Version history for installers is in [RELEASE_NOTES.md](RELEASE_NOTES.md). Detailed developer notes are in [CHANGELOG.md](CHANGELOG.md).
117
+
118
+ ## Development
119
+
120
+ ```bash
121
+ npm install
122
+ npm run build
123
+ npm run lint
124
+ npm test
125
+ ```
126
+
127
+ ## License
128
+
129
+ ISC. See [LICENSE](LICENSE) for the full text and the copyright chain.