iobroker.hassemu 1.2.0 → 1.3.1
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/README.md +49 -43
- package/admin/jsonConfig.json +159 -159
- package/build/lib/client-registry.js +40 -23
- package/build/lib/client-registry.js.map +2 -2
- package/build/lib/coerce.js +18 -7
- package/build/lib/coerce.js.map +2 -2
- package/build/lib/constants.js +10 -1
- package/build/lib/constants.js.map +2 -2
- package/build/lib/global-config.js +20 -22
- package/build/lib/global-config.js.map +2 -2
- package/build/lib/network.js +2 -7
- package/build/lib/network.js.map +2 -2
- package/build/lib/webserver.js +104 -20
- package/build/lib/webserver.js.map +2 -2
- package/build/main.js +1 -9
- package/build/main.js.map +2 -2
- package/io-package.json +39 -39
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,16 +36,16 @@ ioBroker adapter that emulates a [Home Assistant](https://www.home-assistant.io)
|
|
|
36
36
|
- **Node.js >= 20**
|
|
37
37
|
- **ioBroker js-controller >= 7.0.7**
|
|
38
38
|
- **ioBroker Admin >= 7.7.22**
|
|
39
|
-
- **ioBroker web >= 8.0.0**
|
|
39
|
+
- **ioBroker web >= 8.0.0**
|
|
40
40
|
|
|
41
41
|
---
|
|
42
42
|
|
|
43
43
|
## Ports
|
|
44
44
|
|
|
45
|
-
| Port | Protocol | Purpose
|
|
46
|
-
| ---- | -------- |
|
|
47
|
-
| 8123 | TCP/HTTP | Home Assistant emulation (HA standard port)
|
|
48
|
-
| 5353 | UDP | mDNS service broadcast (only if mDNS enabled)| No |
|
|
45
|
+
| Port | Protocol | Purpose | Configurable |
|
|
46
|
+
| ---- | -------- | --------------------------------------------- | ------------ |
|
|
47
|
+
| 8123 | TCP/HTTP | Home Assistant emulation (HA standard port) | No — fixed |
|
|
48
|
+
| 5353 | UDP | mDNS service broadcast (only if mDNS enabled) | No |
|
|
49
49
|
|
|
50
50
|
---
|
|
51
51
|
|
|
@@ -53,15 +53,13 @@ ioBroker adapter that emulates a [Home Assistant](https://www.home-assistant.io)
|
|
|
53
53
|
|
|
54
54
|
The Admin UI configures the server. Redirect URLs are set via the state tree (see below).
|
|
55
55
|
|
|
56
|
-
| Option
|
|
57
|
-
|
|
58
|
-
| **Bind to Interface**
|
|
59
|
-
| **Service Name**
|
|
60
|
-
| **mDNS Enabled**
|
|
61
|
-
| **Auth Required**
|
|
62
|
-
| **Username / Password** | Used when
|
|
63
|
-
|
|
64
|
-
> URLs you set must be reachable from the display — use the LAN IP of the ioBroker host, not `localhost`.
|
|
56
|
+
| Option | Description | Default |
|
|
57
|
+
| ----------------------- | ---------------------------------------------------------------- | ------------- |
|
|
58
|
+
| **Bind to Interface** | Network interface to listen on | 0.0.0.0 (all) |
|
|
59
|
+
| **Service Name** | Name broadcast via mDNS, shown as the server name on the display | `ioBroker` |
|
|
60
|
+
| **mDNS Enabled** | Broadcast `_home-assistant._tcp` on the LAN | `true` |
|
|
61
|
+
| **Auth Required** | Check credentials the display sends during login | `false` |
|
|
62
|
+
| **Username / Password** | Used when _Auth Required_ is on (password encrypted at rest) | `admin` / — |
|
|
65
63
|
|
|
66
64
|
---
|
|
67
65
|
|
|
@@ -86,11 +84,11 @@ hassemu.0.
|
|
|
86
84
|
|
|
87
85
|
The adapter reads `clients.<id>.mode` on every visit:
|
|
88
86
|
|
|
89
|
-
| `mode` value
|
|
90
|
-
|
|
91
|
-
| `global`
|
|
92
|
-
| `manual`
|
|
93
|
-
| a URL
|
|
87
|
+
| `mode` value | redirect target |
|
|
88
|
+
| --------------- | -------------------------------------------------------------- |
|
|
89
|
+
| `global` | `global.mode` / `global.manualUrl` (same rules, one level up) |
|
|
90
|
+
| `manual` | `clients.<id>.manualUrl` |
|
|
91
|
+
| a URL | that URL |
|
|
94
92
|
| empty / unknown | landing page (small HTML with device ID, refreshes every 15 s) |
|
|
95
93
|
|
|
96
94
|
### Master switch
|
|
@@ -107,12 +105,12 @@ The adapter broadcasts `_home-assistant._tcp` via mDNS. If the display does not
|
|
|
107
105
|
|
|
108
106
|
1. Check the adapter log for `mDNS: Broadcasting`.
|
|
109
107
|
2. Verify the service is visible from another host:
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
108
|
+
```bash
|
|
109
|
+
# macOS
|
|
110
|
+
dns-sd -B _home-assistant._tcp
|
|
111
|
+
# Linux (with avahi-utils)
|
|
112
|
+
avahi-browse _home-assistant._tcp -r -t
|
|
113
|
+
```
|
|
116
114
|
3. Make sure UDP 5353 is not blocked by a firewall.
|
|
117
115
|
4. If mDNS is not usable on your LAN, set the URL manually on the display: `http://<ioBroker-IP>:8123`.
|
|
118
116
|
|
|
@@ -149,37 +147,45 @@ Reverse DNS on a home LAN depends on your router/DHCP server and often fails. Th
|
|
|
149
147
|
---
|
|
150
148
|
|
|
151
149
|
## Changelog
|
|
150
|
+
### 1.3.1 (2026-04-30)
|
|
151
|
+
|
|
152
|
+
- Hotfix for legacy v1.1.x clients: their `visUrl` channel did not have `mode` / `manualUrl` objects. The v1.2.0 migration wrote states without the matching objects, which the broker logged as `State has no existing object` and rendered the `mode` datapoint without a name or dropdown in the object browser. `ClientRegistry.restore()` now calls an idempotent `ensureObjects()` for every client, so the v1.2.0+ object shapes exist before any migration writes happen.
|
|
153
|
+
- Mode dropdown gains a numeric `0 = "---"` no-choice fallback (analogous to govee-smart's pattern). Existing displays keep their setting; new displays start at `0` and the resolver falls back to the landing page until a real choice is made.
|
|
154
|
+
|
|
155
|
+
### 1.3.0 (2026-04-30)
|
|
156
|
+
|
|
157
|
+
- Security: brute-force lockout on `/auth/login_flow/:flowId` — after 5 failed credential attempts an IP is rejected with HTTP 429 for 15 min. Successful login resets the counter.
|
|
158
|
+
- DRY refactor: shared `parseManualUrlWrite` helper between client + global config; FIFO-cap helper in WebServer; OAuth access-token TTL + lockout window/threshold are now named constants instead of magic numbers.
|
|
159
|
+
- Dead-code cleanup: `resolveBindToReachable`, `coerceUuid` strict-V4 parameter, `DEFAULT_REFRESH_DEBOUNCE_MS` export, internal `getMode`/`getManualUrl` test affordances — all removed; tests rewritten to assert observable behaviour.
|
|
160
|
+
- New `landing-page` test suite with XSS-escape coverage and 11-language fallback verification.
|
|
161
|
+
- Emulated Home Assistant version bumped from 2026.3.1 to 2026.4.0.
|
|
162
|
+
|
|
152
163
|
### 1.2.0 (2026-04-29)
|
|
153
164
|
|
|
154
|
-
-
|
|
155
|
-
-
|
|
156
|
-
-
|
|
157
|
-
-
|
|
158
|
-
-
|
|
165
|
+
- Redirect target now configured via `mode` (dropdown) + `manualUrl` (free text) instead of the old `visUrl`. Migration runs automatically.
|
|
166
|
+
- Master switch `global.enabled` syncs every display: on → all follow the global URL, off → each display picks up its own again.
|
|
167
|
+
- Idle displays without auth token are auto-removed after 30 days.
|
|
168
|
+
- Security hardening of the auth flow.
|
|
169
|
+
- `web` adapter declared as dependency.
|
|
159
170
|
|
|
160
171
|
### 1.1.6 (2026-04-28)
|
|
172
|
+
|
|
161
173
|
- Audit cleanup against the upstream `ioBroker.example/TypeScript` full standard:
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
174
|
+
- Test setup migrated: tests now live next to source as `src/lib/*.test.ts` and run directly via `ts-node/register`. Removed `tsconfig.test.json` + `build-test/`, added `test/mocharc.custom.json` + `test/mocha.setup.js` + `test/tsconfig.json` + `test/.eslintrc.json`
|
|
175
|
+
- `@types/node` rolled back from `^25.6.0` to `^20.19.24` so type defs match `engines.node: ">=20"`
|
|
176
|
+
- Dependabot now ignores major bumps for `@types/node`, `typescript`, `eslint`, `actions/checkout`, `actions/setup-node`
|
|
177
|
+
- `nyc` config + `coverage` script added
|
|
178
|
+
- Orphan `.github/auto-merge.yml` removed (active workflow is `automerge-dependabot.yml` using `gh pr merge`)
|
|
167
179
|
|
|
168
180
|
### 1.1.5 (2026-04-26)
|
|
181
|
+
|
|
169
182
|
- Process-level `unhandledRejection` / `uncaughtException` handlers added as last-line-of-defence against fire-and-forget rejections.
|
|
170
183
|
- Stop shipping the `manual-review` release-script plugin — adapter-only consequence.
|
|
171
184
|
- Audit-driven boilerplate sync with the other krobi adapters (`.vscode` json5 schemas, `tsconfig.test` looser test rules).
|
|
172
185
|
- Min js-controller correction: was `>=7.0.0`, restored to repochecker-recommended `>=6.0.11` (Source: `ioBroker.repochecker/lib/M1000_IOPackageJson.js`).
|
|
173
186
|
- `@types/iobroker` bumped to `^7.1.1`.
|
|
174
187
|
|
|
175
|
-
|
|
176
|
-
- Separate test-build output (`build-test/`) from production `build/` — `npm test` no longer risks leaving duplicated `build/src` + `build/test` trees in the published package. No runtime change.
|
|
177
|
-
|
|
178
|
-
### 1.1.3 (2026-04-19)
|
|
179
|
-
|
|
180
|
-
- **Fix duplicate client registration on first connect** — HA displays fire several parallel cookieless requests (`GET /`, `GET /api/`, `POST /auth/login_flow`) within milliseconds of each other. Each used to create a separate client record, leaving orphans behind. The registry now locks per IP while the first client is being created, so parallel burst requests from the same display attach to the same client and cookie.
|
|
181
|
-
- **Setup page redesigned** — big green OK banner so "everything's connected" is visible at a glance, responsive layout with dark-mode support, IP shown alongside the device ID, clearer step-by-step instructions.
|
|
182
|
-
- **Setup page localized into all 11 adapter languages** — automatically picks the ioBroker system language (set in Admin → Main Settings), falls back to English for unknown languages.
|
|
188
|
+
Older entries are in [CHANGELOG_OLD.md](CHANGELOG_OLD.md).
|
|
183
189
|
|
|
184
190
|
## Support
|
|
185
191
|
|
package/admin/jsonConfig.json
CHANGED
|
@@ -1,161 +1,161 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
2
|
+
"i18n": true,
|
|
3
|
+
"type": "panel",
|
|
4
|
+
"items": {
|
|
5
|
+
"_headerServer": {
|
|
6
|
+
"type": "header",
|
|
7
|
+
"text": "header_server",
|
|
8
|
+
"size": 4
|
|
9
|
+
},
|
|
10
|
+
"bindAddress": {
|
|
11
|
+
"type": "ip",
|
|
12
|
+
"label": "bindAddress",
|
|
13
|
+
"tooltip": "bindAddressTooltip",
|
|
14
|
+
"listenOnAllPorts": true,
|
|
15
|
+
"onlyIp4": true,
|
|
16
|
+
"noInternal": false,
|
|
17
|
+
"xs": 12,
|
|
18
|
+
"sm": 6,
|
|
19
|
+
"md": 4,
|
|
20
|
+
"lg": 4,
|
|
21
|
+
"xl": 4
|
|
22
|
+
},
|
|
23
|
+
"serviceName": {
|
|
24
|
+
"type": "text",
|
|
25
|
+
"label": "serviceName",
|
|
26
|
+
"tooltip": "serviceNameTooltip",
|
|
27
|
+
"default": "ioBroker",
|
|
28
|
+
"xs": 12,
|
|
29
|
+
"sm": 6,
|
|
30
|
+
"md": 4,
|
|
31
|
+
"lg": 4,
|
|
32
|
+
"xl": 4
|
|
33
|
+
},
|
|
34
|
+
"_redirectInfo": {
|
|
35
|
+
"type": "staticText",
|
|
36
|
+
"text": "redirectInfo",
|
|
37
|
+
"xs": 12,
|
|
38
|
+
"sm": 12,
|
|
39
|
+
"md": 12,
|
|
40
|
+
"lg": 12,
|
|
41
|
+
"xl": 12
|
|
42
|
+
},
|
|
43
|
+
"_headerMdns": {
|
|
44
|
+
"type": "header",
|
|
45
|
+
"text": "header_mdns",
|
|
46
|
+
"size": 4
|
|
47
|
+
},
|
|
48
|
+
"mdnsEnabled": {
|
|
49
|
+
"type": "checkbox",
|
|
50
|
+
"label": "mdnsEnabled",
|
|
51
|
+
"tooltip": "mdnsEnabledTooltip",
|
|
52
|
+
"default": true,
|
|
53
|
+
"xs": 12,
|
|
54
|
+
"sm": 12,
|
|
55
|
+
"md": 12,
|
|
56
|
+
"lg": 12,
|
|
57
|
+
"xl": 12
|
|
58
|
+
},
|
|
59
|
+
"_mdnsInfo": {
|
|
60
|
+
"type": "staticText",
|
|
61
|
+
"text": "mdnsInfo",
|
|
62
|
+
"xs": 12,
|
|
63
|
+
"sm": 12,
|
|
64
|
+
"md": 12,
|
|
65
|
+
"lg": 12,
|
|
66
|
+
"xl": 12
|
|
67
|
+
},
|
|
68
|
+
"_headerAuth": {
|
|
69
|
+
"type": "header",
|
|
70
|
+
"text": "header_auth",
|
|
71
|
+
"size": 4
|
|
72
|
+
},
|
|
73
|
+
"authRequired": {
|
|
74
|
+
"type": "checkbox",
|
|
75
|
+
"label": "authRequired",
|
|
76
|
+
"tooltip": "authRequiredTooltip",
|
|
77
|
+
"default": false,
|
|
78
|
+
"xs": 12,
|
|
79
|
+
"sm": 12,
|
|
80
|
+
"md": 12,
|
|
81
|
+
"lg": 12,
|
|
82
|
+
"xl": 12
|
|
83
|
+
},
|
|
84
|
+
"username": {
|
|
85
|
+
"type": "text",
|
|
86
|
+
"label": "username",
|
|
87
|
+
"default": "admin",
|
|
88
|
+
"hidden": "!data.authRequired",
|
|
89
|
+
"xs": 12,
|
|
90
|
+
"sm": 6,
|
|
91
|
+
"md": 6,
|
|
92
|
+
"lg": 6,
|
|
93
|
+
"xl": 6
|
|
94
|
+
},
|
|
95
|
+
"password": {
|
|
96
|
+
"type": "password",
|
|
97
|
+
"label": "password",
|
|
98
|
+
"visible": true,
|
|
99
|
+
"hidden": "!data.authRequired",
|
|
100
|
+
"xs": 12,
|
|
101
|
+
"sm": 6,
|
|
102
|
+
"md": 6,
|
|
103
|
+
"lg": 6,
|
|
104
|
+
"xl": 6
|
|
105
|
+
},
|
|
106
|
+
"_authInfo": {
|
|
107
|
+
"type": "staticText",
|
|
108
|
+
"text": "authInfo",
|
|
109
|
+
"hidden": "data.authRequired",
|
|
110
|
+
"xs": 12,
|
|
111
|
+
"sm": 12,
|
|
112
|
+
"md": 12,
|
|
113
|
+
"lg": 12,
|
|
114
|
+
"xl": 12
|
|
115
|
+
},
|
|
116
|
+
"_supportHeader": {
|
|
117
|
+
"type": "header",
|
|
118
|
+
"text": "supportHeader",
|
|
119
|
+
"size": 5
|
|
120
|
+
},
|
|
121
|
+
"_aboutInfo": {
|
|
122
|
+
"type": "staticText",
|
|
123
|
+
"text": "aboutInfo",
|
|
124
|
+
"xs": 12,
|
|
125
|
+
"sm": 12,
|
|
126
|
+
"md": 12,
|
|
127
|
+
"lg": 12,
|
|
128
|
+
"xl": 12,
|
|
129
|
+
"style": {
|
|
130
|
+
"fontSize": 14,
|
|
131
|
+
"marginBottom": 16
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
"_kofiLink": {
|
|
135
|
+
"type": "staticLink",
|
|
136
|
+
"href": "https://ko-fi.com/krobipd",
|
|
137
|
+
"label": "donateKofi",
|
|
138
|
+
"button": true,
|
|
139
|
+
"variant": "outlined",
|
|
140
|
+
"color": "primary",
|
|
141
|
+
"xs": 12,
|
|
142
|
+
"sm": 6,
|
|
143
|
+
"md": 4,
|
|
144
|
+
"lg": 4,
|
|
145
|
+
"xl": 4
|
|
146
|
+
},
|
|
147
|
+
"_paypalLink": {
|
|
148
|
+
"type": "staticLink",
|
|
149
|
+
"href": "https://paypal.me/krobipd",
|
|
150
|
+
"label": "donatePaypal",
|
|
151
|
+
"button": true,
|
|
152
|
+
"variant": "outlined",
|
|
153
|
+
"color": "primary",
|
|
154
|
+
"xs": 12,
|
|
155
|
+
"sm": 6,
|
|
156
|
+
"md": 4,
|
|
157
|
+
"lg": 4,
|
|
158
|
+
"xl": 4
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
161
|
}
|
|
@@ -112,6 +112,7 @@ class ClientRegistry {
|
|
|
112
112
|
const hostname = channelName && channelName !== ip && channelName !== id ? channelName : null;
|
|
113
113
|
const record = { id, cookie, token, mode, manualUrl, ip, hostname };
|
|
114
114
|
this.trackInMemory(record);
|
|
115
|
+
await this.ensureObjects(record);
|
|
115
116
|
}
|
|
116
117
|
this.adapter.log.debug(`client-registry: restored ${this.byId.size} client(s)`);
|
|
117
118
|
}
|
|
@@ -214,14 +215,14 @@ class ClientRegistry {
|
|
|
214
215
|
if (!record) {
|
|
215
216
|
return;
|
|
216
217
|
}
|
|
217
|
-
if (
|
|
218
|
-
|
|
219
|
-
await this.adapter.setStateAsync(`clients.${id}.mode`, { val:
|
|
218
|
+
if (rawValue === 0 || rawValue === "0" || rawValue === "") {
|
|
219
|
+
record.mode = "";
|
|
220
|
+
await this.adapter.setStateAsync(`clients.${id}.mode`, { val: 0, ack: true });
|
|
220
221
|
return;
|
|
221
222
|
}
|
|
222
|
-
if (rawValue
|
|
223
|
-
|
|
224
|
-
await this.adapter.setStateAsync(`clients.${id}.mode`, { val:
|
|
223
|
+
if (typeof rawValue !== "string") {
|
|
224
|
+
this.adapter.log.warn(`client-registry: rejected non-string mode for ${id}`);
|
|
225
|
+
await this.adapter.setStateAsync(`clients.${id}.mode`, { val: record.mode || 0, ack: true });
|
|
225
226
|
return;
|
|
226
227
|
}
|
|
227
228
|
if (rawValue === import_global_config.MODE_GLOBAL || rawValue === import_global_config.MODE_MANUAL) {
|
|
@@ -251,21 +252,20 @@ class ClientRegistry {
|
|
|
251
252
|
* @param rawValue Value written to the state.
|
|
252
253
|
*/
|
|
253
254
|
async handleManualUrlWrite(id, rawValue) {
|
|
254
|
-
var _a;
|
|
255
|
+
var _a, _b;
|
|
255
256
|
const record = this.byId.get(id);
|
|
256
257
|
if (!record) {
|
|
257
258
|
return;
|
|
258
259
|
}
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
if (!empty && !safe) {
|
|
260
|
+
const result = (0, import_coerce.parseManualUrlWrite)(rawValue);
|
|
261
|
+
if (!result.ok) {
|
|
262
262
|
this.adapter.log.warn(`client-registry: rejected unsafe manualUrl for ${id}`);
|
|
263
263
|
await this.adapter.setStateAsync(`clients.${id}.manualUrl`, { val: (_a = record.manualUrl) != null ? _a : "", ack: true });
|
|
264
264
|
return;
|
|
265
265
|
}
|
|
266
|
-
record.manualUrl = safe;
|
|
267
|
-
await this.adapter.setStateAsync(`clients.${id}.manualUrl`, { val: safe != null ?
|
|
268
|
-
if (record.mode === import_global_config.MODE_MANUAL && !safe) {
|
|
266
|
+
record.manualUrl = result.safe;
|
|
267
|
+
await this.adapter.setStateAsync(`clients.${id}.manualUrl`, { val: (_b = result.safe) != null ? _b : "", ack: true });
|
|
268
|
+
if (record.mode === import_global_config.MODE_MANUAL && !result.safe) {
|
|
269
269
|
this.adapter.log.warn(
|
|
270
270
|
`client-registry: ${id} manualUrl cleared while mode='manual' \u2014 display will hit the setup page`
|
|
271
271
|
);
|
|
@@ -324,11 +324,7 @@ class ClientRegistry {
|
|
|
324
324
|
*/
|
|
325
325
|
async syncUrlDropdown(states) {
|
|
326
326
|
this.currentUrlStates = states;
|
|
327
|
-
const merged =
|
|
328
|
-
[import_global_config.MODE_GLOBAL]: "Global URL",
|
|
329
|
-
[import_global_config.MODE_MANUAL]: "Manual URL",
|
|
330
|
-
...states
|
|
331
|
-
};
|
|
327
|
+
const merged = this.buildModeStates();
|
|
332
328
|
for (const id of this.byId.keys()) {
|
|
333
329
|
await this.adapter.extendObjectAsync(`clients.${id}.mode`, {
|
|
334
330
|
common: { states: merged }
|
|
@@ -376,14 +372,31 @@ class ClientRegistry {
|
|
|
376
372
|
this.lastSeenFlushedAt.set(record.id, now);
|
|
377
373
|
this.adapter.extendObjectAsync(`clients.${record.id}`, { native: { lastSeen: now } }).catch((err) => this.adapter.log.debug(`touchLastSeen failed for ${record.id}: ${String(err)}`));
|
|
378
374
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
375
|
+
/**
|
|
376
|
+
* Builds the dropdown-states map for `clients.<id>.mode`. Includes the
|
|
377
|
+
* `0='---'` no-choice fallback (analogous to the govee-smart pattern), the
|
|
378
|
+
* `'global'` + `'manual'` sentinels, and all currently discovered URLs.
|
|
379
|
+
*/
|
|
380
|
+
buildModeStates() {
|
|
381
|
+
return {
|
|
382
|
+
0: "---",
|
|
383
383
|
[import_global_config.MODE_GLOBAL]: "Global URL",
|
|
384
384
|
[import_global_config.MODE_MANUAL]: "Manual URL",
|
|
385
385
|
...this.currentUrlStates
|
|
386
386
|
};
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Idempotently creates all per-client objects (channel + states). Safe to
|
|
390
|
+
* call repeatedly — uses `setObjectNotExistsAsync` everywhere. Called from
|
|
391
|
+
* both `restore()` (so legacy v1.1.x clients gain the new mode/manualUrl
|
|
392
|
+
* objects before migration writes states) and `createClient()`.
|
|
393
|
+
*
|
|
394
|
+
* @param record Client to create or ensure objects for.
|
|
395
|
+
*/
|
|
396
|
+
async ensureObjects(record) {
|
|
397
|
+
var _a;
|
|
398
|
+
const { id, cookie, ip, hostname } = record;
|
|
399
|
+
const mergedStates = this.buildModeStates();
|
|
387
400
|
await Promise.all([
|
|
388
401
|
this.adapter.setObjectNotExistsAsync(`clients.${id}`, {
|
|
389
402
|
type: "channel",
|
|
@@ -402,7 +415,7 @@ class ClientRegistry {
|
|
|
402
415
|
role: "value",
|
|
403
416
|
read: true,
|
|
404
417
|
write: true,
|
|
405
|
-
def:
|
|
418
|
+
def: 0,
|
|
406
419
|
states: mergedStates
|
|
407
420
|
},
|
|
408
421
|
native: {}
|
|
@@ -437,6 +450,10 @@ class ClientRegistry {
|
|
|
437
450
|
native: {}
|
|
438
451
|
})
|
|
439
452
|
]);
|
|
453
|
+
}
|
|
454
|
+
async createObjects(record) {
|
|
455
|
+
await this.ensureObjects(record);
|
|
456
|
+
const { id, mode, ip } = record;
|
|
440
457
|
await Promise.all([
|
|
441
458
|
this.adapter.setStateAsync(`clients.${id}.ip`, { val: ip != null ? ip : "", ack: true }),
|
|
442
459
|
this.adapter.setStateAsync(`clients.${id}.mode`, { val: mode, ack: true }),
|