homebridge-http-advanced-platform 2.0.0-alpha.6
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/LICENSE +176 -0
- package/README.md +175 -0
- package/config.schema.json +449 -0
- package/dist/accessory.d.ts +27 -0
- package/dist/accessory.js +164 -0
- package/dist/accessory.js.map +1 -0
- package/dist/actions.d.ts +8 -0
- package/dist/actions.js +47 -0
- package/dist/actions.js.map +1 -0
- package/dist/compatibility.d.ts +3 -0
- package/dist/compatibility.js +26 -0
- package/dist/compatibility.js.map +1 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +81 -0
- package/dist/config.js.map +1 -0
- package/dist/coordinator.d.ts +51 -0
- package/dist/coordinator.js +145 -0
- package/dist/coordinator.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/mappers.d.ts +3 -0
- package/dist/mappers.js +116 -0
- package/dist/mappers.js.map +1 -0
- package/dist/metadata.d.ts +5 -0
- package/dist/metadata.js +8 -0
- package/dist/metadata.js.map +1 -0
- package/dist/platform.d.ts +18 -0
- package/dist/platform.js +129 -0
- package/dist/platform.js.map +1 -0
- package/dist/runtime.d.ts +54 -0
- package/dist/runtime.js +376 -0
- package/dist/runtime.js.map +1 -0
- package/dist/settings.d.ts +21 -0
- package/dist/settings.js +51 -0
- package/dist/settings.js.map +1 -0
- package/dist/transport.d.ts +25 -0
- package/dist/transport.js +139 -0
- package/dist/transport.js.map +1 -0
- package/dist/types.d.ts +106 -0
- package/dist/types.js +10 -0
- package/dist/types.js.map +1 -0
- package/dist/ui-config.d.ts +27 -0
- package/dist/ui-config.js +208 -0
- package/dist/ui-config.js.map +1 -0
- package/docs/alpha-release-notes.md +19 -0
- package/docs/legacy-reference.md +678 -0
- package/docs/migration.md +43 -0
- package/docs/modernization.md +211 -0
- package/docs/performance.md +37 -0
- package/docs/service-support.md +47 -0
- package/homebridge-ui/public/index.html +74 -0
- package/homebridge-ui/public/settings.js +132 -0
- package/homebridge-ui/server.js +17 -0
- package/package.json +83 -0
- package/sample-config.json +39 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Upgrade and migration
|
|
2
|
+
|
|
3
|
+
## Replace the accessory package with the platform package
|
|
4
|
+
|
|
5
|
+
This is a separate package with the existing Alpha behavior. It does not automatically convert accessory definitions into platform devices. Back up configuration, cached accessories, pairing data, identifier storage and the exact old plugin version/package before replacement.
|
|
6
|
+
|
|
7
|
+
1. Keep the Homebridge storage, bridge username, accessory names, service definitions, platform names, device IDs and `_bridge` settings unchanged.
|
|
8
|
+
2. Stop Homebridge before the package swap, and remove only the installed `homebridge-http-advanced-accessory` package through the installation's existing package-management workflow. Preserve configuration and Homebridge storage. Do not load both packages: both register `HttpAdvancedAccessory` and `HttpAdvanced`.
|
|
9
|
+
3. Unqualified `accessory: "HttpAdvancedAccessory"` and `platform: "HttpAdvanced"` remain valid. In explicitly qualified entries, replace only the package prefix with `homebridge-http-advanced-platform`, keeping the alias after the dot unchanged. Likewise update the package name in any `plugins` allowlist or `disabledPlugins` list. Runtime does not rewrite these settings.
|
|
10
|
+
4. Install the reviewed platform Alpha package through the installation's existing package-management workflow, explicitly selecting `@alpha`, `@2.0.0-alpha.6` or the reviewed local archive. See the [installation instructions](../README.md#install-or-upgrade), then restart Homebridge and its UI.
|
|
11
|
+
5. Verify native JSON Config, Plugin Config, legacy devices, cached platform devices, child bridges, rooms, scenes and automations before leaving the candidate unattended.
|
|
12
|
+
|
|
13
|
+
Platform UUIDs retain the immutable Alpha.5 namespace `homebridge-http-advanced-accessory`, independent of the new registration name. Homebridge 1/2 can reassociate cached platform accessories through the unchanged `HttpAdvanced` alias when the old package is absent. Regression tests exercise that fallback with serialized cache and identifier assignments. Isolated installed-UI, managed-child-bridge and package replacement/rollback checks also passed with HAP disabled. Paired Apple Home identity, rooms, scenes, automations and actual device controls remain live acceptance checks.
|
|
14
|
+
|
|
15
|
+
To roll back a package replacement, remove the new package, reinstall the exact backed-up old version/archive, restore package-qualified settings and plugin lists, and retain the same storage and bridge identity. Restore the appropriate full backup if cached identities or pairing were changed. Do not install both packages as a rollback shortcut.
|
|
16
|
+
|
|
17
|
+
## Legacy configuration compatibility
|
|
18
|
+
|
|
19
|
+
1. Back up Homebridge configuration, cached accessories, pairing data and identifier cache using Homebridge's backup facility.
|
|
20
|
+
2. Verify the Node/Homebridge requirements in the README.
|
|
21
|
+
3. Install the explicit Alpha version or reviewed local package in your Homebridge environment and restart.
|
|
22
|
+
4. Keep accessory names, `accessory: "HttpAdvancedAccessory"`, service definitions and storage unchanged.
|
|
23
|
+
5. Verify state freshness, control writes and existing automations before leaving Alpha unattended.
|
|
24
|
+
|
|
25
|
+
Runtime reads existing configuration without rewriting it. Plugin Config summarizes legacy accessories and maintains optional platforms and shared defaults; the plugin menu's **JSON Config** provides individual legacy accessory editors with add and delete controls. In Plugin Config, only an explicit **Save all settings** changes configuration, preserving the legacy definitions. The legacy registration and service identity remain unchanged. HAP identifier-cache regression tests cover replacing plugin instances without changing AIDs/IIDs. Live Apple Home pairing and automation verification remain a live-soak gate.
|
|
26
|
+
|
|
27
|
+
The platform-only settings screen in the first Alpha hid legacy configuration. Hidden entries are not evidence of deleted configuration: inspect `accessories[]` in the current `config.json` before attempting any restoration. Recover missing definitions from a known backup, preserving names, aliases and `_bridge` settings; do not reconstruct command URLs or mappings from cached HAP values. Merge only the missing definitions into the current file rather than rolling unrelated plugins back to an old complete configuration.
|
|
28
|
+
|
|
29
|
+
## Optional platform migration
|
|
30
|
+
|
|
31
|
+
Platform mode is for new devices or an explicitly planned conversion. It is not required to obtain the cache/performance improvements.
|
|
32
|
+
|
|
33
|
+
Use **Also use as a platform** in the plugin settings screen to create a platform while leaving legacy entries in place. Its enable checkbox controls `enabled`; after restart, disabling it retains definitions and cached identities while reporting device reads and commands unavailable. Re-enable with the same name and device IDs, then restart to restore operation. Shared timings can be maintained in the same screen and apply to legacy-only configurations too. The full JSON editor uses top-level `httpAdvanced` for these defaults.
|
|
34
|
+
|
|
35
|
+
The legacy UUID seed is controlled by Homebridge (`HttpAdvancedAccessory:<name>`). Platform UUIDs use the retained Alpha.5 namespace, platform name and device ID. These namespaces differ intentionally. Merely moving a JSON block is therefore not a seamless identity-preserving migration.
|
|
36
|
+
|
|
37
|
+
Do not copy the same device into both arrays. A future migration tool must prove identity preservation, back up state and handle partial failure before it is offered. No such tool is included in this Alpha. For a manual conversion, expect new HomeKit identities and plan room, scene and automation reassignment. Keep a full backup and test rollback first.
|
|
38
|
+
|
|
39
|
+
Within platform mode, a fixed device `id` preserves its UUID across display-name changes. Changing the platform name or device ID changes identity. Removing an entry from a valid platform inventory unregisters that accessory.
|
|
40
|
+
|
|
41
|
+
## Rollback
|
|
42
|
+
|
|
43
|
+
For rollback to the historical stable `homebridge-http-advanced-accessory@1.3.0`, remove the new platform package first, reinstall the original package and restore any package-qualified settings and plugin lists before restarting Homebridge. Legacy-only users keep their configuration and storage. If you introduced platform entries, restore the backed-up legacy configuration and appropriate Homebridge backup rather than attempting to run platform definitions with 1.3.0. The optional Alpha state file can remain; stable does not read it.
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# Modernization details and developer reference
|
|
2
|
+
|
|
3
|
+
This reference describes the behavior of **2.0.0-alpha.6**. Start with the [README](../README.md) for the compatibility summary, installation and everyday configuration.
|
|
4
|
+
|
|
5
|
+
## What non-breaking means here
|
|
6
|
+
|
|
7
|
+
This platform conversion retains Alpha.5 runtime behavior. The registered package is now `homebridge-http-advanced-platform`; see the [migration guide](migration.md) for replacement requirements and the retained platform UUID namespace. The existing `accessories[]` configuration model remains supported: the `HttpAdvancedAccessory` alias, device names, service definitions, getter/setter actions, encoded URLs, bodies, mapper chains, optional characteristics and property overrides can stay in place. Plugin Config summarizes these accessories; the plugin menu's JSON Config supports maintaining each definition separately. Runtime startup does not rewrite configuration. The shared cache, recovery and write-confirmation improvements apply without enabling a platform or modifying the web server.
|
|
8
|
+
|
|
9
|
+
Compatibility does not mean every runtime behavior is identical to 1.3.0. In particular:
|
|
10
|
+
|
|
11
|
+
| Area | Compatibility and limits |
|
|
12
|
+
|---|---|
|
|
13
|
+
| HomeKit identity | The legacy adapter retains its registration, service ordering and characteristic identities. Preserve accessory names and Homebridge pairing/identifier storage. Regression tests check identity continuity; physical-device and automation verification remains part of Alpha testing. |
|
|
14
|
+
| Optional platform | Legacy and platform devices can coexist. Moving an existing device to the platform creates a different identity; plan room, scene and automation assignments. There is no automatic identity-preserving migration tool. |
|
|
15
|
+
| Runtime support | Node 22.13+ in the 22.x line or Node 24.x; Homebridge 1.11.4+ in the 1.x line or 2.4+ in the 2.x line. Historical HAP services removed by a newer Homebridge version cannot be restored by this plugin. |
|
|
16
|
+
| Read timing | Default on-demand acquisition becomes background refresh. HomeKit reads return memory state promptly, with finite staleness and bounded background traffic. Positive polling intervals remain, with a brief confirmation phase after writes. |
|
|
17
|
+
| Writes | HTTP acknowledgement is followed by a bounded requested-state window while the getter catches up. Failed writes are not replayed. A matching response, expiry or command failure ends that command's pending state. |
|
|
18
|
+
| Responses and errors | Existing non-2xx body mapping remains the default, except 429/503 with `Retry-After`. Timeouts, size limits and invalid values produce contained failures. Stricter HTTP/extraction checks are optional. |
|
|
19
|
+
| Mapper compatibility | Legacy pass-through and raw-state semantics remain. Maintained JSONPath dependencies use a safe evaluator; unusual executable legacy JSONPath expressions require individual verification. |
|
|
20
|
+
|
|
21
|
+
See [upgrade and migration](migration.md), [supported services](service-support.md) and the behavior below before opting into Alpha on an existing installation.
|
|
22
|
+
|
|
23
|
+
## Shared settings and precedence
|
|
24
|
+
|
|
25
|
+
Shared settings live at top-level `httpAdvanced`, apply to both adapters, and are read by legacy child bridges as well. Per-action `timeout`, per-device `uriCallsDelay`, `setterDelay` and `writeConfirmationTimeout`, and individual device `refresh` values override their shared defaults. Positive `forceRefreshDelay` overrides the normal adaptive cadence. Request timeout, debounce, request spacing and write confirmation are milliseconds; refresh and recovery intervals are seconds.
|
|
26
|
+
|
|
27
|
+
Concurrency and recovery are coordinated per Homebridge process; child bridges share settings, not a cross-process queue. The earlier Alpha platform-level `coordinator` setting remains supported and overrides shared coordinator values within that platform's process. Within one process, each enabled platform applies its supplied coordinator keys in discovery order; the last supplied value for a key wins. Disabled platforms apply no overrides. For uniform limits, keep coordinator settings only in `httpAdvanced`. Plugin Config identifies platform-level overrides when present.
|
|
28
|
+
|
|
29
|
+
| Setting | Resolution, highest priority first | Unit / default |
|
|
30
|
+
|---|---|---|
|
|
31
|
+
| HTTP timeout | action `timeout`, global `requestTimeout`, built-in | ms / 10000 |
|
|
32
|
+
| Request spacing | device `uriCallsDelay`, global `uriCallsDelay`, built-in | ms / 0 |
|
|
33
|
+
| Setter debounce | device `setterDelay`, global `setterDelay`, built-in | ms / 0 |
|
|
34
|
+
| Write confirmation | device `writeConfirmationTimeout`, global value, built-in | ms / 10000 |
|
|
35
|
+
| Normal refresh | positive device `forceRefreshDelay`; otherwise each device `refresh` field, corresponding global field, built-in | seconds / active 5, idle 60, idle-after 60 |
|
|
36
|
+
| Recovery | global `recovery` fields, built-in | seconds / retry 5, max retry 30, quiet 90, reminder 300 |
|
|
37
|
+
| Request limits | enabled platform `coordinator` keys in discovery order, global `coordinator`, built-in | counts / total 4, per-origin 2, queue 256 |
|
|
38
|
+
|
|
39
|
+
Explicit zero overrides are supported for spacing, debounce and write confirmation; zero does not mean inherit. Timeout, refresh intervals, recovery retry/reminder intervals and request limits must be positive. A zero recovery quiet period enables immediate outage warnings. Platform-level timing settings other than the historical `coordinator` block are not a separate defaults layer: use global fields or device/action overrides.
|
|
40
|
+
|
|
41
|
+
Separate-process tests use Homebridge's real `User` storage path and API to verify shared defaults and isolated coordinator overrides. They do not advertise test bridges or replace a live child-bridge lifecycle check.
|
|
42
|
+
|
|
43
|
+
Increasing the HTTP timeout does not extend HomeKit's own request budget. It is mainly useful for background acquisition from slow servers; control writes still surface failures and are never automatically replayed.
|
|
44
|
+
|
|
45
|
+
## Configuration editor and platform lifecycle
|
|
46
|
+
|
|
47
|
+
The custom editor saves optional `platforms[]` entries and shared settings while preserving the loaded legacy `accessories[]` definitions, other plugins, bridge configuration and unknown fields. Each changed save retains an exact private backup beside `config.json`, checks for conflicting plugin edits (including legacy definitions edited elsewhere), and replaces the file atomically. Avoid simultaneous saves from multiple editors. The default plugin JSON/schema view identifies the legacy accessory alias and provides individual accessory editing; the custom UI summarizes legacy accessories and maintains platforms and shared settings. Runtime startup never rewrites configuration.
|
|
48
|
+
|
|
49
|
+
Plugin Config disables its controls while a save is pending so later input cannot be overwritten by the save response. Validation identifies shared fields or indexed platform/device blocks without echoing private values. Invalid shared settings on load direct the user to the full configuration editor for correction.
|
|
50
|
+
|
|
51
|
+
The platform restores cached accessories and removes obsolete ones only after successful inventory validation. Invalid or duplicate inventories preserve cached accessories and report an error. Set a permanent device `id` before pairing if the display name may change later; without one, its initial name is its identity. Keep the platform name stable. An omitted `enabled` means enabled. After restart, disabling retains definitions and cached identities while reporting device reads and commands unavailable; see [platform lifecycle](#platform-lifecycle).
|
|
52
|
+
|
|
53
|
+
## How reads and freshness work
|
|
54
|
+
|
|
55
|
+
HomeKit GETs read memory immediately. They do not wait for HTTP, retries, a queue, or another device. Unknown state returns HomeKit's communication error until the first usable refresh; an existing last-known value is returned while refreshing or recovering from failure.
|
|
56
|
+
|
|
57
|
+
A single scheduler serves both adapters. Default bounds are four total requests, two per origin, and 256 waiting requests. GETs for the same action never overlap. Overdue actions are serviced before recently refreshed ones, and eligible origins rotate. SETs have queue priority. Connections are reused.
|
|
58
|
+
|
|
59
|
+
| Setting | Unit | Behavior |
|
|
60
|
+
|---|---|---|
|
|
61
|
+
| `forceRefreshDelay` | seconds | Positive values retain explicit polling intervals; default 0 selects adaptive refresh. |
|
|
62
|
+
| `refresh.activeInterval` | seconds | Default 5 after startup and while reads are active. |
|
|
63
|
+
| `refresh.idleAfter` | seconds | Default 60 without a HomeKit read before switching to idle cadence. |
|
|
64
|
+
| `refresh.idleInterval` | seconds | Default 60 for idle devices. |
|
|
65
|
+
| `setterDelay` | milliseconds | Default 0. Positive values acknowledge immediately and debounce each characteristic; last write wins. |
|
|
66
|
+
| `writeConfirmationTimeout` | milliseconds | Default 10000 after HTTP success. Hold the requested value until a getter confirms it or this window expires; 0 disables the window. |
|
|
67
|
+
| `uriCallsDelay` | milliseconds | Default 0. Minimum spacing between this device's request starts, including GETs, SETs and fallbacks. |
|
|
68
|
+
|
|
69
|
+
Intervals run after request completion, with up to 10% positive jitter. Startup acquisition is spread across the first second. Explicit polling is not shortened by HomeKit reads. With adaptive refresh, a stale read makes work eligible for a later scheduler tick (100 ms resolution); it still returns memory state. Errors back off exponentially up to five minutes plus jitter. Error fallback values also back off, so a working `resultOnError` cannot create a retry storm.
|
|
70
|
+
|
|
71
|
+
Temporary network failures, timeouts, inconclusive responses, HTTP 429/503 responses with `Retry-After`, and transient HTTP errors rejected by `strictHTTP` share recovery by configured URL origin within a runtime. Background requests pause together, then probe one at a time after 5, 10, 20, and at most 30 seconds between failed probes. Server retry guidance can extend the pause up to five minutes. Queued background reads for a recovering origin are returned to the cache scheduler, freeing queue capacity for healthy origins and writes. This internal deferral is not a failed request and does not apply `resultOnError`. A usable response releases the origin and makes affected cached getters eligible again under the normal concurrency and spacing limits. Other origins and control writes retain normal scheduling. Writes are never automatically replayed; a failed write still reports failure.
|
|
72
|
+
|
|
73
|
+
Short interruptions stay quiet in normal logs. After 90 seconds without a usable response, one endpoint warning appears, with reminders at most every five minutes and one recovery notice. Short recoveries use debug logging. Enabling device diagnostics does not multiply warnings. Unknown state remains unknown until acquired; known state remains available unless an explicit `resultOnError` supplies a fallback. Actual mapper/configuration failures remain action-specific warnings. An endpoint that stays unavailable continues probing; there is no 30-second outage cutoff.
|
|
74
|
+
|
|
75
|
+
This changes the acquisition timing of `forceRefreshDelay: 0`: old versions fetched on demand, while Alpha learns state ahead of reads. It introduces bounded background traffic and finite staleness. Measure both freshness and load for your devices; very slow fleets can exceed the nominal interval. A 500-second configured interval still allows approximately 500 seconds of staleness. No cache promises mathematically instantaneous remote state.
|
|
76
|
+
|
|
77
|
+
Successful reads update HAP using `updateValue`, never a setter. Last successful values and timestamps are stored under Homebridge's persistence directory and restored only for an identical configuration fingerprint. Cache files contain values and hashes, not action URLs or credentials. A missing/corrupt cache is ignored. Persistence is periodic and at graceful shutdown; a crash can lose recent cache updates.
|
|
78
|
+
|
|
79
|
+
## Actions and HTTP
|
|
80
|
+
|
|
81
|
+
Action keys combine `get` or `set` with a characteristic name, such as `getOn`, `setBrightness`, or `getSecuritySystemTargetState`. Canonical HAP names are resolved by UUID; historical compact display names remain accepted.
|
|
82
|
+
|
|
83
|
+
Each action supports:
|
|
84
|
+
|
|
85
|
+
- `url`: HTTP or HTTPS endpoint.
|
|
86
|
+
- `httpMethod`: defaults to `GET`; legacy POST bodies and GET bodies are supported.
|
|
87
|
+
- `body`: string, sent without implicit JSON/form serialization.
|
|
88
|
+
- `headers`: optional explicit headers, including Content-Type if your endpoint requires one.
|
|
89
|
+
- `responsePattern`: optional JavaScript regular expression, without enclosing `/` delimiters, that the raw response body must match. A mismatch fails acquisition before mapping, retaining cached state unless an explicit `resultOnError` supplies a fallback. It also fails writes without replaying them. Omit it to retain unrestricted response formats.
|
|
90
|
+
- `requireResponseMatch`: default false. Opt-in getter protection against missing regex/JSONPath/XPath extractions that no later mapper explicitly handles. The default preserves legacy missing-match pass-through. It does not change outgoing setter mapping.
|
|
91
|
+
- `mappers`: ordered transformation chain.
|
|
92
|
+
- `resultOnError`: getter value returned on transport failure, bypassing mappers. Zero, false and empty string are valid fallbacks.
|
|
93
|
+
- `inconclusive`: another getter action when the mapped result is the string `"inconclusive"`. Up to 32 actions are allowed; cycles are rejected.
|
|
94
|
+
- `timeout`: milliseconds for the response body and redirects; default 10000 unless shared `requestTimeout` overrides it. Background acquisition starts its budget when first admitted to the network, so waiting behind an unavailable endpoint does not consume it. Control writes retain a total budget including initial queueing. A queued write rejects when that budget expires even if no slot has opened; it is removed from the queue and cannot execute later.
|
|
95
|
+
- `strictHTTP`: default false. True treats non-2xx responses as errors. Statuses 408, 429, 500, 502, 503, and 504 use shared recovery even without retry headers; other statuses remain action-specific failures.
|
|
96
|
+
|
|
97
|
+
For compatibility, non-2xx response bodies are mapped by default, as in 1.3.0, except HTTP 429/503 responses carrying `Retry-After`: those explicitly signal temporary unavailability and are never mapped as successful state or accepted as successful writes. Retry guidance accepts seconds or an HTTP date, bounded to five minutes; invalid guidance uses normal backoff. Status errors are counted separately in diagnostics. Enable `strictHTTP` to make other non-2xx responses fail and use `resultOnError`. GET/HEAD redirects are followed (up to ten); each hop goes through the coordinator. Credentials and cookies are removed on cross-origin redirects. POST redirects are not automatically followed, matching legacy defaults. Responses are limited to 8 MiB to bound memory use.
|
|
98
|
+
|
|
99
|
+
Set `username` and `password` on a device for Basic Auth. Supplied credentials are sent immediately, including when legacy `immediately: false` is present: 1.3.0's explicit Authorization header already overrode that setting. Alpha preserves that behavior. Without credentials, Alpha omits the old empty `Basic Og==` header. Credentials embedded in a URL are also handled by Node's HTTP client. Use HTTPS for sensitive endpoints.
|
|
100
|
+
|
|
101
|
+
### Servers you cannot change
|
|
102
|
+
|
|
103
|
+
Recovery does not require a gateway upgrade, JSON error messages, accurate HTTP status codes, or `Retry-After`. Timeouts and disconnections already trigger shared backoff. Malformed JSON/XML that the mapper chain cannot handle and numeric responses that cannot be converted to their characteristics also retain cached state and retry quietly.
|
|
104
|
+
|
|
105
|
+
For stricter extraction, set `"requireResponseMatch": true` on a getter. Missing JSONPath selections and missing regex/XPath node matches then become inconclusive when no later mapper explicitly handles the miss. This prevents leftover HTML, timeout text, or a missing field from becoming an accidental false/off value. It is opt-in because existing installations can deliberately rely on missing-match pass-through.
|
|
106
|
+
|
|
107
|
+
Request URLs, HTTP methods (including POST-based reads), bodies, authentication, and outgoing mapper behavior are unchanged. Intermediate mapper values retain legacy behavior so a later explicit mapping or `eval` can handle them. For example, a JSONPath miss followed by a static mapping from `"[]"` to `"0"` deliberately reports zero. An `inconclusive` fallback action still works. Successful false, zero, and empty-string selections remain valid.
|
|
108
|
+
|
|
109
|
+
With stricter extraction enabled, if an absent field deliberately means off for your protocol, express that in a mapper instead of relying on the final boolean conversion. Static mapper pass-through, scalar XPath results, and explicit `eval` results remain authoritative; the plugin cannot infer that every unfamiliar string or empty value is an error. Use an existing mapper to return `"inconclusive"` for recognized busy responses, or add an optional raw-response guard for a fixed vocabulary:
|
|
110
|
+
|
|
111
|
+
```json
|
|
112
|
+
{
|
|
113
|
+
"url": "http://device.example.invalid/state",
|
|
114
|
+
"httpMethod": "POST",
|
|
115
|
+
"body": "command=read",
|
|
116
|
+
"responsePattern": "^(?:ON|OFF)\\s*$",
|
|
117
|
+
"mappers": [
|
|
118
|
+
{ "type": "regex", "parameters": { "regexp": "^(ON|OFF)\\s*$" } },
|
|
119
|
+
{ "type": "static", "parameters": { "mapping": { "ON": "1", "OFF": "0" } } }
|
|
120
|
+
]
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The guard accepts the two state words with trailing whitespace; the regex mapper captures the word so a server's line ending cannot change the static lookup. On a setter that acknowledges commands with `OK`, `"responsePattern": "^OK\\s*$"` accepts the acknowledgement with optional whitespace and rejects an HTTP 200 busy/error reply. The write is sent once and its failure is reported. A guard mismatch is a response failure, so an explicit getter `resultOnError` may supply a fallback; the `inconclusive` action applies to mapped inconclusive results. Anchor patterns when the entire reply must match. Use `strictHTTP` only when the server's status codes can be trusted; otherwise retain intentional error-body mappings.
|
|
125
|
+
|
|
126
|
+
## SETs and templates
|
|
127
|
+
|
|
128
|
+
SETs apply mappers to the outgoing HomeKit value, expand templates, and send the request. A normal SET resolves after the HTTP operation; failures surface as a HomeKit error. `setterDelay` retains legacy immediate acknowledgement, so a later failure can only be logged and the cached value restored. The requested characteristic value stays visible during debounce and the HTTP operation, then for up to `writeConfirmationTimeout` (default 10 seconds) after HTTP success. This prevents an older server state from flipping the HomeKit control back while the command takes effect. The matching getter becomes eligible immediately and, while awaiting confirmation, at intervals of at most one second after each successful response, subject to normal queue limits, request spacing and outage backoff. This brief confirmation phase also applies to devices with longer explicit polling intervals; their normal cadence resumes afterward.
|
|
129
|
+
|
|
130
|
+
A matching usable getter response ends the window early. At expiry, HomeKit returns to the latest observed state, or a communication error if none is known. An HTTP failure clears that command's pending value immediately. GETs that overlap a write cannot confirm it, and error fallback values cannot confirm it either. Pending values are never persisted or substituted into legacy mapper/template state. Rapid toggles keep the newest intent; commands are never automatically replayed. No gateway upgrade is required. Shared diagnostics use the `HTTP Advanced` log prefix; accessory-specific messages keep the accessory name.
|
|
131
|
+
|
|
132
|
+
`{value}` (case-insensitive) substitutes the **mapped** value. Legacy JavaScript template expressions see the original `value` and characteristic `state`:
|
|
133
|
+
|
|
134
|
+
```json
|
|
135
|
+
{
|
|
136
|
+
"url": "http://device.example/set/${value}?mapped={value}",
|
|
137
|
+
"httpMethod": "POST",
|
|
138
|
+
"body": "temperature=${state.getTargetTemperature * 9/5 + 32}"
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Expressions are available in setter URLs and bodies. Getter URLs/bodies remain literal, matching 1.3.0. `state.getOn`, `state.getTargetTemperature`, etc. retain the legacy mapper-output types; polling converts numeric characteristics as before.
|
|
143
|
+
|
|
144
|
+
## Mappers
|
|
145
|
+
|
|
146
|
+
A chain feeds each mapper's output into the next. Getter mappers consume response text; setter mappers consume the outgoing HomeKit value. The table describes intermediate and default pass-through behavior. With `requireResponseMatch`, an unresolved extraction miss at the end of a getter chain becomes `"inconclusive"`.
|
|
147
|
+
|
|
148
|
+
| Type | Parameters | Semantics |
|
|
149
|
+
|---|---|---|
|
|
150
|
+
| `static` | `mapping` object | Lookup by input value; unmatched values pass through. Legacy falsey mapped values (`0`, `false`, `""`) also pass through. Use strings `"0"`/`"1"` for numeric state or an eval expression for an intentional falsey result. |
|
|
151
|
+
| `regex` | `regexp`, `capture` (default `"1"`) | Return the selected capture, or original input when unmatched. |
|
|
152
|
+
| `xpath` | `xpath`, `index` (default 0) | XPath text-node selection or string expression. Select `/text()` or `string(...)`, not entire elements. |
|
|
153
|
+
| `jpath` | `jpath`, `index` (default 0) | JSONPath selection, indexed result, objects/arrays serialized as JSON. Malformed or non-object JSON returns `"inconclusive"`. |
|
|
154
|
+
| `eval` | `expression` | Execute the legacy JavaScript expression with `value`, `self.state`, and `this.state`. |
|
|
155
|
+
|
|
156
|
+
```json
|
|
157
|
+
[
|
|
158
|
+
{ "type": "jpath", "parameters": { "jpath": "$.u", "index": 0 } },
|
|
159
|
+
{ "type": "static", "parameters": { "mapping": { "0": "0", "1": "1", "unset": "0" } } }
|
|
160
|
+
]
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**Eval and `${...}` templates execute trusted configuration as JavaScript with the privileges of Homebridge. They are not sandboxed.** Do not paste untrusted expressions. Evaluation is isolated in the compatibility module and exceptions are contained; a deliberately nonterminating expression can still block Node. JSONPath uses the maintained library's safe filter evaluator; exotic executable legacy JSONPath scripts need individual compatibility verification.
|
|
164
|
+
|
|
165
|
+
Malformed XML, invalid values, expression failures and exhausted numeric/boolean `inconclusive` results produce contained action failures. They cannot leave a getter callback waiting indefinitely. Numeric HAP formats are converted deliberately; unsupported values are rejected rather than cached as valid state.
|
|
166
|
+
|
|
167
|
+
## Services, optional characteristics and props
|
|
168
|
+
|
|
169
|
+
`service` uses the Homebridge HAP service name. `BatteryService` aliases `Battery`. Removed historical HAP services produce an explicit unsupported-service error; consult [the service inventory](service-support.md). The old `HomeKitExtensionTypes.js` was never loaded by the plugin entry point and did not provide a working configuration feature.
|
|
170
|
+
|
|
171
|
+
`optionCharacteristic` selects optional characteristics in HAP's original service order. `props` overrides properties by canonical or legacy compact name:
|
|
172
|
+
|
|
173
|
+
```json
|
|
174
|
+
{
|
|
175
|
+
"service": "Lightbulb",
|
|
176
|
+
"optionCharacteristic": ["Brightness", "Hue", "Saturation"],
|
|
177
|
+
"props": { "Brightness": { "minValue": 0, "maxValue": 100, "minStep": 1 } }
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
The legacy adapter retains the fixed Manufacturer, Model and SerialNumber values exposed by 1.3.0; its previously ignored `manufacturer`/`model` keys remain accepted. The platform honors these metadata settings. All historical extended examples (security system, contact sensor, Daikin, Yamaha and lightbulb) remain in [the legacy reference](legacy-reference.md).
|
|
182
|
+
|
|
183
|
+
## Platform lifecycle
|
|
184
|
+
|
|
185
|
+
After saving and restarting, a disabled platform retains cached identities but reports its device reads and commands unavailable. Re-enabling with the same platform name and device IDs restores those identities. Invalid enable flags or inventories retain cached accessories and report them unavailable instead of accepting commands without a working device handler. A valid empty inventory explicitly unregisters its devices.
|
|
186
|
+
|
|
187
|
+
Local integration tests serialize and deserialize Homebridge platform accessories between fresh API instances, covering disable, rename/re-enable, real getter/setter handlers, invalid inventory retention and explicit removal. This verifies the persistence boundary without pairing a household HomeKit controller.
|
|
188
|
+
|
|
189
|
+
## Measurements and validation
|
|
190
|
+
|
|
191
|
+
The recorded synthetic fixture compares about 2.15 seconds for a blocking 41-getter read with about 3 ms for a warmed 44-device cached snapshot, issuing no new HTTP getter requests for that snapshot. Refreshing all 44 devices separately took about 2.25 seconds. These measure different stages: the cache speeds up HomeKit reads; it does not make the web server or physical device instantaneous. They are fixture measurements, not a promised speedup on every installation.
|
|
192
|
+
|
|
193
|
+
Use the [measurement guide](performance.md) to compare read latency, cache freshness and sustained backend load together. The [Alpha release notes](alpha-release-notes.md) summarize the Alpha, completed isolated acceptance and remaining public-installation and live-soak checks.
|
|
194
|
+
|
|
195
|
+
## Development and release policy
|
|
196
|
+
|
|
197
|
+
The core is TypeScript compiled to ESM. Homebridge supplies HAP at runtime; the plugin does not bundle a second HAP implementation. Legacy and platform adapters share the HTTP coordinator, cache and mapping core.
|
|
198
|
+
|
|
199
|
+
```sh
|
|
200
|
+
npm ci
|
|
201
|
+
npm run check
|
|
202
|
+
HB_TEST_VERSION=1 npm test
|
|
203
|
+
npm run benchmark -- --save
|
|
204
|
+
npm pack --dry-run
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
CI exercises Node 22/24 and real Homebridge v1/v2 HAP implementations. Unit/integration tests use only loopback fake servers. `legacy-plugin` is a test-only alias of published 1.3.0; its obsolete dependencies are excluded from production installation and the tarball. `npm audit --omit=dev` audits the maintained runtime separately.
|
|
208
|
+
|
|
209
|
+
Any future public prerelease must use an explicit tag matching its version channel (`alpha` or `beta`) and be marked as a GitHub prerelease. The guard rejects stable versions, channel mismatches and `latest`. `publishConfig.tag` remains `alpha` for this Alpha candidate; update it deliberately when preparing Beta. No automatic publishing workflow is enabled. Stable requires broader device, restart and real-installation evidence, not merely one working household fixture.
|
|
210
|
+
|
|
211
|
+
The existing Apache-2.0 LICENSE remains unchanged. Package metadata is reconciled to that file, which has existed since the initial commit; historical authorship is retained and the current maintainer is credited.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Measuring latency, freshness and backend load
|
|
2
|
+
|
|
3
|
+
## Reproducible fixture
|
|
4
|
+
|
|
5
|
+
Run `npm run benchmark -- --save` from the source checkout. It uses real HAP serialization and a loopback backend that serializes 50 ms responses, modeling a single-command backend. It never contacts household devices.
|
|
6
|
+
|
|
7
|
+
The baseline uses the 41 on-demand entries from the 44-device fixture; the three 500-second pollers are separately covered by interval tests. The Alpha warms all 44 entries, then serializes their HAP state. Acquisition is separated from measurement so every network request caused by the bulk read can be attributed exactly. Timings are diagnostic rather than portable absolute performance guarantees.
|
|
8
|
+
|
|
9
|
+
`benchmark-results.json` records baseline bulk time, Alpha bulk time, getter request counts, full refresh sweep time, median/p95/max cache age and request latency, queue high-water mark, concurrency and failures. Cache age is measured at the end of a sweep and is not a long-running household freshness result.
|
|
10
|
+
|
|
11
|
+
## Actual Homebridge measurements still required
|
|
12
|
+
|
|
13
|
+
On a backed-up Alpha test installation, use the existing Homebridge UI/API session to time repeated `/accessories` requests. Keep authorization in the existing client; never paste headers into commands or logs. Capture baseline and Alpha on the same host with comparable load.
|
|
14
|
+
|
|
15
|
+
Record:
|
|
16
|
+
|
|
17
|
+
- `/accessories` median/p95/max wall time, including warm and restart cases;
|
|
18
|
+
- external reader end-to-end time and successful parsing;
|
|
19
|
+
- backend request counts and maximum concurrency during bulk reads;
|
|
20
|
+
- cache-age median/p95/max from periodic plugin debug snapshots;
|
|
21
|
+
- getter request-duration median/p95/max and error/timeout counts;
|
|
22
|
+
- sustained request rate from the difference in cumulative `requests.started` divided by elapsed seconds;
|
|
23
|
+
- elapsed time until every getter has completed one successful refresh after startup;
|
|
24
|
+
- whether devices with explicit polling intervals retain the expected freshness;
|
|
25
|
+
- normal writes, delayed writes and recovery after a backend outage.
|
|
26
|
+
|
|
27
|
+
Debug snapshots expose per-action hashes for correlation without exposing endpoint names or payloads. `age` is time since last successful HTTP acquisition. Fallback values do not reset that timestamp. An unknown entry has `age: null`. The bounded duration sample holds the most recent 2048 requests.
|
|
28
|
+
|
|
29
|
+
If an external reader adds orchestration or polling delay, measure that separately. Do not attribute an end-to-end improvement to the plugin without isolating its contribution.
|
|
30
|
+
|
|
31
|
+
Approve default cadence based on actual freshness and sustained load. If a full sweep is slower than the desired interval, reduce workload or deliberately tune the shared concurrency for the backend; do not simply remove the bounds. Avoid changing the three existing 500-second intervals unintentionally.
|
|
32
|
+
|
|
33
|
+
## Measurement caveat
|
|
34
|
+
|
|
35
|
+
Homebridge/HAP may reuse a recent `/accessories` snapshot. Space repeated measurements beyond the installed implementation's reuse window and record versions so snapshot reuse is not mistaken for a plugin improvement.
|
|
36
|
+
|
|
37
|
+
A controlled field performance comparison for this packaging candidate remains pending. Synthetic timings are not live-device measurements.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Historical service inventory
|
|
2
|
+
|
|
3
|
+
Generated against installed Homebridge 1.11.4 and 2.4.0. Unavailable historical services are not silently substituted. They also cannot be constructed by the old plugin on the same runtime. Service availability alone does not prove full support for complex services such as camera streaming.
|
|
4
|
+
|
|
5
|
+
| Legacy service | Homebridge 1.11.4 | Homebridge 2.4.0 |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| AccessoryInformation | Available | Available |
|
|
8
|
+
| AirQualitySensor | Available | Available |
|
|
9
|
+
| BatteryService | Alias: Battery | Alias: Battery |
|
|
10
|
+
| BridgeConfiguration | Available | Unavailable in this HAP runtime |
|
|
11
|
+
| BridgingState | Available | Unavailable in this HAP runtime |
|
|
12
|
+
| CameraControl | Available | Unavailable in this HAP runtime |
|
|
13
|
+
| CameraRTPStreamManagement | Available | Available |
|
|
14
|
+
| CarbonDioxideSensor | Available | Available |
|
|
15
|
+
| CarbonMonoxideSensor | Available | Available |
|
|
16
|
+
| ContactSensor | Available | Available |
|
|
17
|
+
| Door | Available | Available |
|
|
18
|
+
| Doorbell | Available | Available |
|
|
19
|
+
| Fan | Available | Available |
|
|
20
|
+
| GarageDoorOpener | Available | Available |
|
|
21
|
+
| HumiditySensor | Available | Available |
|
|
22
|
+
| LeakSensor | Available | Available |
|
|
23
|
+
| LightSensor | Available | Available |
|
|
24
|
+
| Lightbulb | Available | Available |
|
|
25
|
+
| LockManagement | Available | Available |
|
|
26
|
+
| LockMechanism | Available | Available |
|
|
27
|
+
| Microphone | Available | Available |
|
|
28
|
+
| MotionSensor | Available | Available |
|
|
29
|
+
| OccupancySensor | Available | Available |
|
|
30
|
+
| Outlet | Available | Available |
|
|
31
|
+
| Pairing | Available | Available |
|
|
32
|
+
| ProtocolInformation | Available | Available |
|
|
33
|
+
| Relay | Available | Unavailable in this HAP runtime |
|
|
34
|
+
| SecuritySystem | Available | Available |
|
|
35
|
+
| SmokeSensor | Available | Available |
|
|
36
|
+
| Speaker | Available | Available |
|
|
37
|
+
| StatefulProgrammableSwitch | Available | Available |
|
|
38
|
+
| StatelessProgrammableSwitch | Available | Available |
|
|
39
|
+
| Switch | Available | Available |
|
|
40
|
+
| TemperatureSensor | Available | Available |
|
|
41
|
+
| Thermostat | Available | Available |
|
|
42
|
+
| TimeInformation | Available | Unavailable in this HAP runtime |
|
|
43
|
+
| TunneledBTLEAccessoryService | Available | Unavailable in this HAP runtime |
|
|
44
|
+
| Window | Available | Available |
|
|
45
|
+
| WindowCovering | Available | Available |
|
|
46
|
+
|
|
47
|
+
BatteryService is explicitly mapped to Battery with the same HAP UUID. The dormant legacy extension file was not imported by the original entry point; FanIR/TVIR were not an exposed working plugin feature.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<style>
|
|
2
|
+
.http-settings { max-width: 960px; margin: auto; padding: 24px; font-size: 1rem; line-height: 1.5; }
|
|
3
|
+
.http-settings *, .http-settings *::before, .http-settings *::after { box-sizing: border-box; }
|
|
4
|
+
.http-settings h1, .http-settings h2, .http-settings p, .http-settings label { margin: 0; }
|
|
5
|
+
.http-settings h1 { font-size: 1.5em; font-weight: 600; line-height: 1.3; }
|
|
6
|
+
.http-settings h2 { font-size: 1.125em; font-weight: 600; line-height: 1.4; }
|
|
7
|
+
.http-settings p, .http-settings label, .http-settings summary, .http-settings .btn, .http-settings .form-control { font-size: 1em; line-height: 1.5; }
|
|
8
|
+
.http-settings .intro { display: grid; gap: 12px; margin-bottom: 24px; }
|
|
9
|
+
.http-settings form, .http-settings fieldset { display: grid; gap: 24px; margin: 0; padding: 0; border: 0; min-width: 0; }
|
|
10
|
+
.http-settings section { display: grid; gap: 16px; min-width: 0; border: 1px solid #8885; border-radius: 8px; padding: 24px; margin: 0; }
|
|
11
|
+
.http-settings textarea { font-family: monospace; tab-size: 2; min-height: 260px; resize: vertical; }
|
|
12
|
+
.http-settings .form-control { background-color: transparent; color: inherit; padding: 8px 12px; }
|
|
13
|
+
.http-settings .form-control::placeholder { color: inherit; opacity: .65; }
|
|
14
|
+
.http-settings .btn { justify-self: start; padding: 8px 16px; margin: 0; }
|
|
15
|
+
.http-settings .legacy-navigation { display: flex; align-items: baseline; flex-wrap: wrap; gap: 16px 24px; }
|
|
16
|
+
.http-settings .legacy-list { max-height: 220px; overflow-y: auto; margin: 12px 0 0; padding-left: 24px; }
|
|
17
|
+
.http-settings .legacy-list li + li { margin-top: 4px; }
|
|
18
|
+
.http-settings summary { cursor: pointer; padding: 8px 0; }
|
|
19
|
+
.http-settings .timings { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 20px 16px; }
|
|
20
|
+
.http-settings .timings > div { display: grid; grid-template-rows: 1fr auto; gap: 8px; min-width: 0; }
|
|
21
|
+
.http-settings #platforms:empty { display: none; }
|
|
22
|
+
.http-settings .platform { display: grid; gap: 12px; border-top: 1px solid #8885; padding-top: 16px; }
|
|
23
|
+
.http-settings .platform + .platform { margin-top: 24px; }
|
|
24
|
+
.http-settings label { display: block; }
|
|
25
|
+
.http-settings input[type="checkbox"] { margin: 0 8px 0 0; }
|
|
26
|
+
.http-settings .actions { display: flex; gap: 12px 24px; align-items: center; flex-wrap: wrap; }
|
|
27
|
+
.http-settings [hidden] { display: none !important; }
|
|
28
|
+
.http-settings #status { margin: 0; padding: 0; }
|
|
29
|
+
@media (max-width: 600px) {
|
|
30
|
+
.http-settings { padding: 16px; }
|
|
31
|
+
.http-settings section { padding: 16px; }
|
|
32
|
+
.http-settings .timings { grid-template-columns: 1fr; }
|
|
33
|
+
}
|
|
34
|
+
</style>
|
|
35
|
+
<div class="http-settings">
|
|
36
|
+
<header class="intro">
|
|
37
|
+
<h1>Homebridge HTTP Advanced Platform</h1>
|
|
38
|
+
<p>Keep existing accessories and add an optional platform alongside them. Both use the shared cache and recovery behavior.</p>
|
|
39
|
+
<p id="status" role="status" aria-live="polite">Loading configuration…</p>
|
|
40
|
+
</header>
|
|
41
|
+
<form id="settings" hidden>
|
|
42
|
+
<fieldset id="settings-fields">
|
|
43
|
+
<section aria-labelledby="legacy-title">
|
|
44
|
+
<h2 id="legacy-title">Legacy accessories <span id="legacy-count"></span></h2>
|
|
45
|
+
<p id="legacy-summary"></p>
|
|
46
|
+
<p>To edit, add or delete an accessory, close this screen and choose <strong>JSON Config</strong> from this plugin's menu. Each accessory has its own editor for URLs, mappings and other settings.</p>
|
|
47
|
+
<p>Save any changes below before leaving this screen. Keep an existing accessory's name and alias unchanged to retain its HomeKit identity.</p>
|
|
48
|
+
<div class="legacy-navigation">
|
|
49
|
+
<button type="button" id="legacy-config" class="btn btn-outline-primary">Back to plugin menu</button>
|
|
50
|
+
<details id="legacy-details">
|
|
51
|
+
<summary>View configured accessories</summary>
|
|
52
|
+
<ul id="legacy-list" class="legacy-list"></ul>
|
|
53
|
+
</details>
|
|
54
|
+
</div>
|
|
55
|
+
</section>
|
|
56
|
+
<section aria-labelledby="global-title">
|
|
57
|
+
<h2 id="global-title">Shared settings</h2>
|
|
58
|
+
<p>Defaults for legacy accessories and platforms, including child bridges. Existing per-device and per-action values take precedence. Blank fields use the built-in default shown.</p>
|
|
59
|
+
<div id="timings" class="timings"></div>
|
|
60
|
+
</section>
|
|
61
|
+
<section aria-labelledby="platform-title">
|
|
62
|
+
<h2 id="platform-title">Optional platforms</h2>
|
|
63
|
+
<p>Add new devices here while maintaining legacy accessories through JSON Config. Moving an existing accessory here creates a new HomeKit identity; plan room, scene and automation reassignment. Do not define the same device in both sections.</p>
|
|
64
|
+
<div id="platforms"></div>
|
|
65
|
+
<button type="button" id="add-platform" class="btn btn-outline-primary">Also use as a platform</button>
|
|
66
|
+
</section>
|
|
67
|
+
<div class="actions">
|
|
68
|
+
<button type="submit" id="save" class="btn btn-primary">Save all settings</button>
|
|
69
|
+
<span>Restart Homebridge after saving to apply changes.</span>
|
|
70
|
+
</div>
|
|
71
|
+
</fieldset>
|
|
72
|
+
</form>
|
|
73
|
+
</div>
|
|
74
|
+
<script src="settings.js"></script>
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/* global homebridge, document */
|
|
2
|
+
(() => {
|
|
3
|
+
const $ = id => document.getElementById(id);
|
|
4
|
+
const fields = [
|
|
5
|
+
['requestTimeout', 'Request timeout (ms)', 10000],
|
|
6
|
+
['uriCallsDelay', 'Request spacing per device (ms)', 0],
|
|
7
|
+
['setterDelay', 'Setter debounce (ms)', 0],
|
|
8
|
+
['writeConfirmationTimeout', 'Write confirmation window (ms)', 10000],
|
|
9
|
+
['refresh.activeInterval', 'Active refresh interval (seconds)', 5],
|
|
10
|
+
['refresh.idleInterval', 'Idle refresh interval (seconds)', 60],
|
|
11
|
+
['refresh.idleAfter', 'Idle after (seconds)', 60],
|
|
12
|
+
['recovery.retryInterval', 'First recovery retry (seconds)', 5],
|
|
13
|
+
['recovery.maxRetryInterval', 'Maximum recovery retry (seconds)', 30],
|
|
14
|
+
['recovery.quietPeriod', 'Wait before outage warning (seconds)', 90],
|
|
15
|
+
['recovery.reminderInterval', 'Outage reminder interval (seconds)', 300],
|
|
16
|
+
['coordinator.concurrency', 'Concurrent requests per process', 4],
|
|
17
|
+
['coordinator.perOrigin', 'Concurrent requests per endpoint', 2],
|
|
18
|
+
['coordinator.maxQueue', 'Maximum queued requests', 256],
|
|
19
|
+
];
|
|
20
|
+
let loaded;
|
|
21
|
+
let hasChanges = false;
|
|
22
|
+
let saving = false;
|
|
23
|
+
let platformEditors = [];
|
|
24
|
+
const message = (text, error = false) => {
|
|
25
|
+
$('status').textContent = text; $('status').className = error ? 'text-danger' : '';
|
|
26
|
+
if (error) { $('status').tabIndex = -1; $('status').focus(); }
|
|
27
|
+
};
|
|
28
|
+
const parse = (editor, label) => {
|
|
29
|
+
try { return JSON.parse(editor.value); }
|
|
30
|
+
catch { throw new Error(`${label} contains invalid JSON; no changes saved`); }
|
|
31
|
+
};
|
|
32
|
+
const addPlatform = block => {
|
|
33
|
+
const index = platformEditors.length + 1;
|
|
34
|
+
const container = document.createElement('div'); container.className = 'platform';
|
|
35
|
+
const enabledLabel = document.createElement('label');
|
|
36
|
+
const enabled = document.createElement('input'); enabled.type = 'checkbox'; enabled.checked = block.enabled !== false;
|
|
37
|
+
enabledLabel.append(enabled, ` Enable platform ${index}`);
|
|
38
|
+
const label = document.createElement('label'); label.htmlFor = `platform-${index}`; label.textContent = `Platform ${index} configuration (JSON object)`;
|
|
39
|
+
const editor = document.createElement('textarea'); editor.id = label.htmlFor; editor.className = 'form-control'; editor.spellcheck = false; editor.autocomplete = 'off'; editor.value = JSON.stringify(block, null, 2);
|
|
40
|
+
const note = document.createElement('p'); note.textContent = 'After restart, disabling retains definitions and cached identities but reports devices unavailable for reads and commands. Its name and device IDs determine HomeKit identity.';
|
|
41
|
+
const limitsNote = document.createElement('p');
|
|
42
|
+
limitsNote.textContent = 'When enabled, this platform’s coordinator overrides affect all HTTP Advanced devices in the same Homebridge process. For uniform limits, keep them in Shared settings and remove coordinator from this platform.';
|
|
43
|
+
const showOverrides = value => { limitsNote.hidden = !value?.coordinator || !Object.keys(value.coordinator).length; };
|
|
44
|
+
showOverrides(block);
|
|
45
|
+
enabled.addEventListener('change', () => {
|
|
46
|
+
try {
|
|
47
|
+
const value = parse(editor, `Platform ${index}`);
|
|
48
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Platform configuration must be an object');
|
|
49
|
+
value.enabled = enabled.checked; editor.value = JSON.stringify(value, null, 2);
|
|
50
|
+
} catch (error) { enabled.checked = !enabled.checked; message(error.message, true); }
|
|
51
|
+
});
|
|
52
|
+
editor.addEventListener('input', () => { try { const value = JSON.parse(editor.value); enabled.checked = value.enabled !== false; showOverrides(value); } catch { /* allow incomplete JSON while editing */ } });
|
|
53
|
+
container.append(enabledLabel, note, limitsNote, label, editor); $('platforms').append(container);
|
|
54
|
+
platformEditors.push(editor);
|
|
55
|
+
};
|
|
56
|
+
const render = result => {
|
|
57
|
+
loaded = result;
|
|
58
|
+
const accessories = result.draft.accessories;
|
|
59
|
+
$('legacy-count').textContent = `(${accessories.length})`;
|
|
60
|
+
$('legacy-summary').textContent = accessories.length
|
|
61
|
+
? `${accessories.length} ${accessories.length === 1 ? 'accessory uses' : 'accessories use'} the shared settings below. Manage their individual definitions in JSON Config.`
|
|
62
|
+
: 'No legacy accessories are configured. Add one through JSON Config, or use an optional platform below.';
|
|
63
|
+
$('legacy-list').replaceChildren();
|
|
64
|
+
for (const accessory of accessories) {
|
|
65
|
+
const item = document.createElement('li');
|
|
66
|
+
item.textContent = `${accessory.name} — ${accessory.service}`;
|
|
67
|
+
$('legacy-list').append(item);
|
|
68
|
+
}
|
|
69
|
+
$('legacy-details').hidden = accessories.length === 0;
|
|
70
|
+
$('timings').replaceChildren();
|
|
71
|
+
for (const [key, title, fallback] of fields) {
|
|
72
|
+
const box = document.createElement('div'); const label = document.createElement('label');
|
|
73
|
+
label.htmlFor = key; label.textContent = title;
|
|
74
|
+
const input = document.createElement('input'); input.id = key; input.className = 'form-control'; input.type = 'number'; input.step = key.startsWith('coordinator.') ? '1' : 'any'; input.min = key.startsWith('coordinator.') ? '1' : '0'; input.placeholder = String(fallback);
|
|
75
|
+
const value = key.split('.').reduce((v, k) => v?.[k], result.draft.settings);
|
|
76
|
+
input.value = value === undefined ? '' : String(value);
|
|
77
|
+
const validate = () => {
|
|
78
|
+
const zeroAllowed = ['uriCallsDelay', 'setterDelay', 'writeConfirmationTimeout', 'recovery.quietPeriod'].includes(key);
|
|
79
|
+
input.setCustomValidity(input.value !== '' && !zeroAllowed && Number(input.value) <= 0 ? `${title} must be greater than zero` : '');
|
|
80
|
+
};
|
|
81
|
+
input.addEventListener('input', validate); validate();
|
|
82
|
+
box.append(label, input); $('timings').append(box);
|
|
83
|
+
}
|
|
84
|
+
platformEditors = []; $('platforms').replaceChildren(); result.draft.platforms.forEach(addPlatform);
|
|
85
|
+
$('settings').hidden = false;
|
|
86
|
+
hasChanges = false;
|
|
87
|
+
};
|
|
88
|
+
$('settings').addEventListener('input', () => { hasChanges = true; });
|
|
89
|
+
$('legacy-config').addEventListener('click', () => {
|
|
90
|
+
if (hasChanges) {
|
|
91
|
+
message('Save your changes below before returning to the plugin menu. To discard them, use the dialog’s Close button.', true);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
homebridge.closeSettings();
|
|
95
|
+
});
|
|
96
|
+
$('add-platform').addEventListener('click', () => {
|
|
97
|
+
const names = platformEditors.map(editor => { try { return JSON.parse(editor.value).name; } catch { return undefined; } });
|
|
98
|
+
let number = 1; let name = 'HTTP Advanced';
|
|
99
|
+
while (names.includes(name)) name = `HTTP Advanced ${++number}`;
|
|
100
|
+
addPlatform({ platform: 'HttpAdvanced', name, enabled: true, devices: [] });
|
|
101
|
+
hasChanges = true;
|
|
102
|
+
});
|
|
103
|
+
$('settings').addEventListener('submit', async event => {
|
|
104
|
+
event.preventDefault();
|
|
105
|
+
if (saving || !$('settings').reportValidity()) return;
|
|
106
|
+
saving = true; $('settings-fields').disabled = true;
|
|
107
|
+
message('Saving settings…');
|
|
108
|
+
try {
|
|
109
|
+
const draft = structuredClone(loaded.draft);
|
|
110
|
+
// legacy definitions stay unchanged here; JSON Config owns their editing workflow
|
|
111
|
+
draft.platforms = platformEditors.map((editor, index) => parse(editor, `Platform ${index + 1}`));
|
|
112
|
+
for (const [key] of fields) {
|
|
113
|
+
const input = $(key).value;
|
|
114
|
+
const keys = key.split('.'); const property = keys.pop();
|
|
115
|
+
let target = draft.settings;
|
|
116
|
+
for (const section of keys) { target[section] ??= {}; target = target[section]; }
|
|
117
|
+
if (input === '') delete target[property]; else target[property] = Number(input);
|
|
118
|
+
// blank settings should not materialize empty sections in an unchanged config
|
|
119
|
+
if (keys.length && !Object.keys(target).length && loaded.draft.settings[keys[0]] === undefined) delete draft.settings[keys[0]];
|
|
120
|
+
}
|
|
121
|
+
const result = await homebridge.request('/settings/save', { revision: loaded.revision, draft });
|
|
122
|
+
render(result); message(result.changed ? 'Saved. Restart Homebridge to apply these settings.' : 'No changes to save.');
|
|
123
|
+
} catch (error) { message(error.message || 'Could not save settings; no changes saved', true); }
|
|
124
|
+
finally { saving = false; $('settings-fields').disabled = false; }
|
|
125
|
+
});
|
|
126
|
+
homebridge.addEventListener('ready', async () => {
|
|
127
|
+
// the standard save path handles only one plugin alias/type, so both sections use our bounded editor
|
|
128
|
+
homebridge.disableSaveButton();
|
|
129
|
+
try { render(await homebridge.request('/settings/load')); message('Configuration loaded. Changes stay here until you save.'); }
|
|
130
|
+
catch (error) { message(error.message || 'Could not load settings', true); }
|
|
131
|
+
}, { once: true });
|
|
132
|
+
})();
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { HomebridgePluginUiServer, RequestError } from '@homebridge/plugin-ui-utils';
|
|
2
|
+
import { ConfigEditor, SettingsError } from '../dist/ui-config.js';
|
|
3
|
+
|
|
4
|
+
class SettingsServer extends HomebridgePluginUiServer {
|
|
5
|
+
constructor() {
|
|
6
|
+
super();
|
|
7
|
+
const editor = new ConfigEditor(this.homebridgeConfigPath);
|
|
8
|
+
const handle = work => {
|
|
9
|
+
try { return work(); }
|
|
10
|
+
catch (error) { throw new RequestError(error instanceof SettingsError ? error.message : 'Could not process settings', {}); }
|
|
11
|
+
};
|
|
12
|
+
this.onRequest('/settings/load', () => handle(() => editor.load()));
|
|
13
|
+
this.onRequest('/settings/save', body => handle(() => editor.save(body)));
|
|
14
|
+
this.ready();
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
new SettingsServer();
|