node-red-contrib-alarm-ultimate 0.1.0 → 0.1.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 +76 -13
- package/docs/images/alarm-panel-mock.svg +114 -0
- package/docs/images/banner.svg +63 -0
- package/docs/images/flow-overview.svg +85 -0
- package/examples/README.md +19 -11
- package/examples/alarm-ultimate-dashboard-controls.json +574 -0
- package/nodes/AlarmSystemUltimate.html +3 -3
- package/nodes/AlarmUltimateSiren.html +3 -3
- package/nodes/AlarmUltimateSiren.js +6 -2
- package/nodes/AlarmUltimateState.html +3 -3
- package/nodes/AlarmUltimateState.js +6 -2
- package/nodes/AlarmUltimateZone.html +9 -4
- package/nodes/AlarmUltimateZone.js +21 -3
- package/nodes/icons/alarm-ultimate-siren.svg +6 -0
- package/nodes/icons/alarm-ultimate-state.svg +5 -0
- package/nodes/icons/alarm-ultimate-zone.svg +5 -0
- package/nodes/icons/alarm-ultimate.svg +6 -0
- package/package.json +2 -2
- package/tools/alarm-json-mapper.html +1118 -465
|
@@ -1,241 +1,309 @@
|
|
|
1
1
|
<!doctype html>
|
|
2
2
|
<html lang="en">
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="utf-8" />
|
|
6
|
+
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
7
|
+
<title>Alarm JSON Mapper (Node-RED)</title>
|
|
8
|
+
<style>
|
|
9
|
+
:root {
|
|
10
|
+
color-scheme: light dark;
|
|
11
|
+
--bg: #0b1020;
|
|
12
|
+
--panel: #111936;
|
|
13
|
+
--text: #e7eaf6;
|
|
14
|
+
--muted: #a9b0d2;
|
|
15
|
+
--border: #2a355f;
|
|
16
|
+
--accent: #6ea8fe;
|
|
17
|
+
--ok: #2fbf71;
|
|
18
|
+
--warn: #ffcc66;
|
|
19
|
+
--danger: #ff6b6b;
|
|
20
|
+
--mono:
|
|
21
|
+
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
|
22
|
+
"Liberation Mono", "Courier New", monospace;
|
|
23
|
+
--sans:
|
|
24
|
+
system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial,
|
|
25
|
+
"Apple Color Emoji", "Segoe UI Emoji";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@media (prefers-color-scheme: light) {
|
|
8
29
|
:root {
|
|
9
|
-
|
|
10
|
-
--
|
|
11
|
-
--
|
|
12
|
-
--
|
|
13
|
-
--
|
|
14
|
-
--
|
|
15
|
-
--accent: #6ea8fe;
|
|
16
|
-
--ok: #2fbf71;
|
|
17
|
-
--warn: #ffcc66;
|
|
18
|
-
--danger: #ff6b6b;
|
|
19
|
-
--mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New",
|
|
20
|
-
monospace;
|
|
21
|
-
--sans: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji",
|
|
22
|
-
"Segoe UI Emoji";
|
|
30
|
+
--bg: #f6f8ff;
|
|
31
|
+
--panel: #ffffff;
|
|
32
|
+
--text: #111827;
|
|
33
|
+
--muted: #5b647a;
|
|
34
|
+
--border: #d8deef;
|
|
35
|
+
--accent: #2563eb;
|
|
23
36
|
}
|
|
37
|
+
}
|
|
24
38
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
--accent: #2563eb;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
39
|
+
body {
|
|
40
|
+
margin: 0;
|
|
41
|
+
background: var(--bg);
|
|
42
|
+
color: var(--text);
|
|
43
|
+
font-family: var(--sans);
|
|
44
|
+
line-height: 1.4;
|
|
45
|
+
}
|
|
35
46
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
47
|
+
header {
|
|
48
|
+
padding: 18px 16px;
|
|
49
|
+
border-bottom: 1px solid var(--border);
|
|
50
|
+
background: linear-gradient(180deg,
|
|
51
|
+
rgba(110, 168, 254, 0.12),
|
|
52
|
+
rgba(0, 0, 0, 0));
|
|
53
|
+
}
|
|
43
54
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
55
|
+
header h1 {
|
|
56
|
+
font-size: 18px;
|
|
57
|
+
margin: 0 0 6px 0;
|
|
58
|
+
}
|
|
49
59
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
60
|
+
header p {
|
|
61
|
+
margin: 0;
|
|
62
|
+
color: var(--muted);
|
|
63
|
+
font-size: 13px;
|
|
64
|
+
}
|
|
54
65
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
66
|
+
main {
|
|
67
|
+
max-width: 1100px;
|
|
68
|
+
margin: 0 auto;
|
|
69
|
+
padding: 16px;
|
|
70
|
+
display: grid;
|
|
71
|
+
gap: 12px;
|
|
72
|
+
}
|
|
60
73
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
gap: 12px;
|
|
67
|
-
}
|
|
74
|
+
.grid {
|
|
75
|
+
display: grid;
|
|
76
|
+
grid-template-columns: 1.2fr 0.8fr;
|
|
77
|
+
gap: 12px;
|
|
78
|
+
}
|
|
68
79
|
|
|
80
|
+
@media (max-width: 980px) {
|
|
69
81
|
.grid {
|
|
70
|
-
|
|
71
|
-
grid-template-columns: 1.2fr 0.8fr;
|
|
72
|
-
gap: 12px;
|
|
82
|
+
grid-template-columns: 1fr;
|
|
73
83
|
}
|
|
84
|
+
}
|
|
74
85
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
86
|
+
.card {
|
|
87
|
+
border: 1px solid var(--border);
|
|
88
|
+
background: var(--panel);
|
|
89
|
+
border-radius: 10px;
|
|
90
|
+
padding: 12px;
|
|
91
|
+
}
|
|
80
92
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
padding: 12px;
|
|
86
|
-
}
|
|
93
|
+
.card h2 {
|
|
94
|
+
font-size: 14px;
|
|
95
|
+
margin: 0 0 10px 0;
|
|
96
|
+
}
|
|
87
97
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
98
|
+
.row {
|
|
99
|
+
display: grid;
|
|
100
|
+
grid-template-columns: 160px 1fr;
|
|
101
|
+
gap: 10px;
|
|
102
|
+
align-items: center;
|
|
103
|
+
margin: 8px 0;
|
|
104
|
+
}
|
|
92
105
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
align-items: center;
|
|
98
|
-
margin: 8px 0;
|
|
99
|
-
}
|
|
106
|
+
.row label {
|
|
107
|
+
font-size: 12px;
|
|
108
|
+
color: var(--muted);
|
|
109
|
+
}
|
|
100
110
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
111
|
+
textarea,
|
|
112
|
+
select,
|
|
113
|
+
input[type="text"] {
|
|
114
|
+
width: 100%;
|
|
115
|
+
border: 1px solid var(--border);
|
|
116
|
+
background: transparent;
|
|
117
|
+
color: var(--text);
|
|
118
|
+
border-radius: 8px;
|
|
119
|
+
padding: 8px 10px;
|
|
120
|
+
box-sizing: border-box;
|
|
121
|
+
font-family: var(--mono);
|
|
122
|
+
font-size: 12px;
|
|
123
|
+
}
|
|
105
124
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
border: 1px solid var(--border);
|
|
111
|
-
background: transparent;
|
|
112
|
-
color: var(--text);
|
|
113
|
-
border-radius: 8px;
|
|
114
|
-
padding: 8px 10px;
|
|
115
|
-
box-sizing: border-box;
|
|
116
|
-
font-family: var(--mono);
|
|
117
|
-
font-size: 12px;
|
|
118
|
-
}
|
|
125
|
+
textarea {
|
|
126
|
+
min-height: 240px;
|
|
127
|
+
resize: vertical;
|
|
128
|
+
}
|
|
119
129
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
130
|
+
.buttons {
|
|
131
|
+
display: flex;
|
|
132
|
+
flex-wrap: wrap;
|
|
133
|
+
gap: 8px;
|
|
134
|
+
margin-top: 10px;
|
|
135
|
+
}
|
|
124
136
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
137
|
+
button {
|
|
138
|
+
border: 1px solid var(--border);
|
|
139
|
+
background: rgba(110, 168, 254, 0.12);
|
|
140
|
+
color: var(--text);
|
|
141
|
+
border-radius: 8px;
|
|
142
|
+
padding: 8px 10px;
|
|
143
|
+
cursor: pointer;
|
|
144
|
+
font-size: 12px;
|
|
145
|
+
}
|
|
131
146
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
border-radius: 8px;
|
|
137
|
-
padding: 8px 10px;
|
|
138
|
-
cursor: pointer;
|
|
139
|
-
font-size: 12px;
|
|
140
|
-
}
|
|
147
|
+
button.primary {
|
|
148
|
+
border-color: rgba(110, 168, 254, 0.5);
|
|
149
|
+
background: rgba(110, 168, 254, 0.22);
|
|
150
|
+
}
|
|
141
151
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
152
|
+
button:disabled {
|
|
153
|
+
opacity: 0.6;
|
|
154
|
+
cursor: not-allowed;
|
|
155
|
+
}
|
|
146
156
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
157
|
+
.hint {
|
|
158
|
+
font-size: 12px;
|
|
159
|
+
color: var(--muted);
|
|
160
|
+
margin: 6px 0 0 0;
|
|
161
|
+
}
|
|
151
162
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
163
|
+
.status {
|
|
164
|
+
font-family: var(--mono);
|
|
165
|
+
font-size: 12px;
|
|
166
|
+
padding: 8px 10px;
|
|
167
|
+
border-radius: 8px;
|
|
168
|
+
border: 1px solid var(--border);
|
|
169
|
+
margin-top: 10px;
|
|
170
|
+
}
|
|
157
171
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
border-radius: 8px;
|
|
163
|
-
border: 1px solid var(--border);
|
|
164
|
-
margin-top: 10px;
|
|
165
|
-
}
|
|
172
|
+
.status.ok {
|
|
173
|
+
border-color: rgba(47, 191, 113, 0.55);
|
|
174
|
+
color: var(--ok);
|
|
175
|
+
}
|
|
166
176
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
177
|
+
.status.warn {
|
|
178
|
+
border-color: rgba(255, 204, 102, 0.55);
|
|
179
|
+
color: var(--warn);
|
|
180
|
+
}
|
|
171
181
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
182
|
+
.status.err {
|
|
183
|
+
border-color: rgba(255, 107, 107, 0.55);
|
|
184
|
+
color: var(--danger);
|
|
185
|
+
}
|
|
176
186
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
187
|
+
pre {
|
|
188
|
+
margin: 0;
|
|
189
|
+
white-space: pre-wrap;
|
|
190
|
+
word-break: break-word;
|
|
191
|
+
font-family: var(--mono);
|
|
192
|
+
font-size: 12px;
|
|
193
|
+
}
|
|
181
194
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
word-break: break-word;
|
|
186
|
-
font-family: var(--mono);
|
|
187
|
-
font-size: 12px;
|
|
188
|
-
}
|
|
195
|
+
.mono {
|
|
196
|
+
font-family: var(--mono);
|
|
197
|
+
}
|
|
189
198
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
199
|
+
.two {
|
|
200
|
+
display: grid;
|
|
201
|
+
grid-template-columns: 1fr 1fr;
|
|
202
|
+
gap: 12px;
|
|
203
|
+
}
|
|
193
204
|
|
|
205
|
+
@media (max-width: 980px) {
|
|
194
206
|
.two {
|
|
195
|
-
|
|
196
|
-
grid-template-columns: 1fr 1fr;
|
|
197
|
-
gap: 12px;
|
|
207
|
+
grid-template-columns: 1fr;
|
|
198
208
|
}
|
|
209
|
+
}
|
|
210
|
+
</style>
|
|
211
|
+
</head>
|
|
212
|
+
|
|
213
|
+
<body>
|
|
214
|
+
<header>
|
|
215
|
+
<h1>Alarm JSON Mapper</h1>
|
|
216
|
+
<p>
|
|
217
|
+
Paste a sample incoming message (e.g. KNX Ultimate) and map its fields
|
|
218
|
+
to what the Alarm node needs (topic/value), or paste an ETS Group
|
|
219
|
+
Addresses export (TSV) to generate zones in batch.
|
|
220
|
+
</p>
|
|
221
|
+
</header>
|
|
222
|
+
|
|
223
|
+
<main>
|
|
224
|
+
<div class="grid">
|
|
225
|
+
<section class="card">
|
|
226
|
+
<h2>1) Input message</h2>
|
|
227
|
+
<textarea id="input" spellcheck="false"
|
|
228
|
+
placeholder="Paste JSON here (strict JSON). Tip: this tool also accepts JS-style objects with unquoted keys and // comments."></textarea>
|
|
229
|
+
<div class="buttons">
|
|
230
|
+
<button id="btn-load-knx">Load JSON sample</button>
|
|
231
|
+
<button id="btn-load-ets">Load ETS sample</button>
|
|
232
|
+
<button id="btn-load-ha">Load HA sample</button>
|
|
233
|
+
<button id="btn-load-ha-registry">Load HA registry sample</button>
|
|
234
|
+
<button id="btn-clear">Clear</button>
|
|
235
|
+
<button class="primary" id="btn-parse">Parse</button>
|
|
236
|
+
</div>
|
|
237
|
+
<p class="hint">
|
|
238
|
+
If your input is not strict JSON (comments, unquoted keys), click
|
|
239
|
+
Parse anyway: the tool will try to normalize it.
|
|
240
|
+
</p>
|
|
241
|
+
<div id="parse-status" class="status warn" style="display: none"></div>
|
|
242
|
+
</section>
|
|
199
243
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
<textarea
|
|
221
|
-
id="input"
|
|
222
|
-
spellcheck="false"
|
|
223
|
-
placeholder='Paste JSON here (strict JSON). Tip: this tool also accepts JS-style objects with unquoted keys and // comments.'
|
|
224
|
-
></textarea>
|
|
225
|
-
<div class="buttons">
|
|
226
|
-
<button class="primary" id="btn-parse">Parse</button>
|
|
227
|
-
<button id="btn-load-knx">Load KNX sample</button>
|
|
228
|
-
<button id="btn-clear">Clear</button>
|
|
244
|
+
<section class="card">
|
|
245
|
+
<h2>2) Field mapping</h2>
|
|
246
|
+
<div id="ets-hint" class="status ok" style="display: none"></div>
|
|
247
|
+
<div id="ha-hint" class="status ok" style="display: none"></div>
|
|
248
|
+
<div id="ets-mapping" style="display: none">
|
|
249
|
+
<div class="row">
|
|
250
|
+
<label for="etsAddressFilter">ETS address filter</label>
|
|
251
|
+
<input
|
|
252
|
+
type="text"
|
|
253
|
+
id="etsAddressFilter"
|
|
254
|
+
placeholder="Examples: 0/0/* • 0/1/?? • */7/* (comma or newline separated)"
|
|
255
|
+
/>
|
|
256
|
+
</div>
|
|
257
|
+
<div class="row">
|
|
258
|
+
<label for="etsNameFilter">ETS name filter</label>
|
|
259
|
+
<input
|
|
260
|
+
type="text"
|
|
261
|
+
id="etsNameFilter"
|
|
262
|
+
placeholder='Optional. Examples: Front door • *garage* (supports * and ?)'
|
|
263
|
+
/>
|
|
229
264
|
</div>
|
|
230
265
|
<p class="hint">
|
|
231
|
-
|
|
232
|
-
|
|
266
|
+
Address patterns are matched against the ETS <span class="mono">Address</span> column (e.g.
|
|
267
|
+
<span class="mono">0/0/1</span>). Name filter is matched against <span class="mono">Description</span>,
|
|
268
|
+
fallback to <span class="mono">Group name</span>.
|
|
233
269
|
</p>
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
270
|
+
</div>
|
|
271
|
+
<div id="ha-mapping" style="display: none">
|
|
272
|
+
<div class="row">
|
|
273
|
+
<label for="haEntityFilter">HA entity filter</label>
|
|
274
|
+
<input
|
|
275
|
+
type="text"
|
|
276
|
+
id="haEntityFilter"
|
|
277
|
+
placeholder="Examples: binary_sensor.*door* • *_pir • switch.* (comma or newline separated)"
|
|
278
|
+
/>
|
|
279
|
+
</div>
|
|
280
|
+
<div class="row">
|
|
281
|
+
<label for="haNameFilter">HA name filter</label>
|
|
282
|
+
<input
|
|
283
|
+
type="text"
|
|
284
|
+
id="haNameFilter"
|
|
285
|
+
placeholder='Optional. Examples: Front door • *garage* (supports * and ?)'
|
|
286
|
+
/>
|
|
287
|
+
</div>
|
|
288
|
+
<div class="row">
|
|
289
|
+
<label for="haDomains">HA domains</label>
|
|
290
|
+
<input
|
|
291
|
+
type="text"
|
|
292
|
+
id="haDomains"
|
|
293
|
+
placeholder="Optional. Example: binary_sensor,input_boolean,switch (default: boolean-like domains)"
|
|
294
|
+
/>
|
|
295
|
+
</div>
|
|
296
|
+
<div class="row">
|
|
297
|
+
<label for="haIncludeDisabled">Include disabled</label>
|
|
298
|
+
<input type="checkbox" id="haIncludeDisabled" style="width:auto; margin-top:7px;" />
|
|
299
|
+
</div>
|
|
300
|
+
<p class="hint">
|
|
301
|
+
Paste Home Assistant JSON (e.g. the array returned by <span class="mono">/api/states</span>). The zone topic
|
|
302
|
+
will be the <span class="mono">entity_id</span>. Names use <span class="mono">attributes.friendly_name</span>
|
|
303
|
+
if present.
|
|
304
|
+
</p>
|
|
305
|
+
</div>
|
|
306
|
+
<div id="json-mapping">
|
|
239
307
|
<div class="row">
|
|
240
308
|
<label for="topicPath">Topic path</label>
|
|
241
309
|
<select id="topicPath"></select>
|
|
@@ -248,51 +316,66 @@
|
|
|
248
316
|
<label for="namePath">Zone name path</label>
|
|
249
317
|
<select id="namePath"></select>
|
|
250
318
|
</div>
|
|
251
|
-
<p class="hint">
|
|
252
|
-
Alarm expects zone messages with <span class="mono">msg.topic</span> and a boolean value in the configured
|
|
253
|
-
“With Input” property (default <span class="mono">payload</span>).
|
|
254
|
-
</p>
|
|
255
|
-
<div class="buttons">
|
|
256
|
-
<button class="primary" id="btn-generate">Generate output</button>
|
|
257
|
-
</div>
|
|
258
|
-
<div id="gen-status" class="status warn" style="display: none"></div>
|
|
259
|
-
</section>
|
|
260
|
-
</div>
|
|
261
|
-
|
|
262
|
-
<section class="card">
|
|
263
|
-
<h2>3) Zone JSON template (for Alarm node)</h2>
|
|
264
|
-
<div class="buttons">
|
|
265
|
-
<button id="btn-copy-zone" disabled>Copy</button>
|
|
266
319
|
</div>
|
|
267
|
-
<textarea
|
|
268
|
-
id="zone-output"
|
|
269
|
-
spellcheck="false"
|
|
270
|
-
placeholder="Click “Generate output” to create a zone template, then edit it here if needed."
|
|
271
|
-
></textarea>
|
|
272
320
|
<p class="hint">
|
|
273
|
-
|
|
274
|
-
|
|
321
|
+
Alarm expects zone messages with
|
|
322
|
+
<span class="mono">msg.topic</span> and a boolean value in the
|
|
323
|
+
configured “With Input” property (default
|
|
324
|
+
<span class="mono">payload</span>).
|
|
275
325
|
</p>
|
|
326
|
+
<div class="buttons">
|
|
327
|
+
<button class="primary" id="btn-generate">Generate output</button>
|
|
328
|
+
</div>
|
|
329
|
+
<div id="gen-status" class="status warn" style="display: none"></div>
|
|
276
330
|
</section>
|
|
277
|
-
</
|
|
278
|
-
|
|
279
|
-
<script>
|
|
280
|
-
const els = {
|
|
281
|
-
input: document.getElementById("input"),
|
|
282
|
-
btnParse: document.getElementById("btn-parse"),
|
|
283
|
-
btnLoadKnx: document.getElementById("btn-load-knx"),
|
|
284
|
-
btnClear: document.getElementById("btn-clear"),
|
|
285
|
-
parseStatus: document.getElementById("parse-status"),
|
|
286
|
-
genStatus: document.getElementById("gen-status"),
|
|
287
|
-
topicPath: document.getElementById("topicPath"),
|
|
288
|
-
valuePath: document.getElementById("valuePath"),
|
|
289
|
-
namePath: document.getElementById("namePath"),
|
|
290
|
-
btnGenerate: document.getElementById("btn-generate"),
|
|
291
|
-
zoneOutput: document.getElementById("zone-output"),
|
|
292
|
-
btnCopyZone: document.getElementById("btn-copy-zone"),
|
|
293
|
-
};
|
|
331
|
+
</div>
|
|
294
332
|
|
|
295
|
-
|
|
333
|
+
<section class="card">
|
|
334
|
+
<h2>3) Zone JSON template (for Alarm node)</h2>
|
|
335
|
+
<div class="buttons">
|
|
336
|
+
<button id="btn-copy-zone" disabled>Copy</button>
|
|
337
|
+
</div>
|
|
338
|
+
<textarea id="zone-output" spellcheck="false"
|
|
339
|
+
placeholder="Click “Generate output” to create a zone template, then edit it here if needed."></textarea>
|
|
340
|
+
<p class="hint">
|
|
341
|
+
This creates a single zone object using the mapped topic and optional
|
|
342
|
+
name. Paste it into the Zones field (legacy: one JSON object per line,
|
|
343
|
+
or formatted: JSON array).
|
|
344
|
+
</p>
|
|
345
|
+
</section>
|
|
346
|
+
</main>
|
|
347
|
+
|
|
348
|
+
<script>
|
|
349
|
+
const els = {
|
|
350
|
+
input: document.getElementById("input"),
|
|
351
|
+
btnParse: document.getElementById("btn-parse"),
|
|
352
|
+
btnLoadKnx: document.getElementById("btn-load-knx"),
|
|
353
|
+
btnLoadEts: document.getElementById("btn-load-ets"),
|
|
354
|
+
btnLoadHa: document.getElementById("btn-load-ha"),
|
|
355
|
+
btnLoadHaRegistry: document.getElementById("btn-load-ha-registry"),
|
|
356
|
+
btnClear: document.getElementById("btn-clear"),
|
|
357
|
+
parseStatus: document.getElementById("parse-status"),
|
|
358
|
+
genStatus: document.getElementById("gen-status"),
|
|
359
|
+
topicPath: document.getElementById("topicPath"),
|
|
360
|
+
valuePath: document.getElementById("valuePath"),
|
|
361
|
+
namePath: document.getElementById("namePath"),
|
|
362
|
+
etsHint: document.getElementById("ets-hint"),
|
|
363
|
+
haHint: document.getElementById("ha-hint"),
|
|
364
|
+
etsMapping: document.getElementById("ets-mapping"),
|
|
365
|
+
etsAddressFilter: document.getElementById("etsAddressFilter"),
|
|
366
|
+
etsNameFilter: document.getElementById("etsNameFilter"),
|
|
367
|
+
haMapping: document.getElementById("ha-mapping"),
|
|
368
|
+
haEntityFilter: document.getElementById("haEntityFilter"),
|
|
369
|
+
haNameFilter: document.getElementById("haNameFilter"),
|
|
370
|
+
haDomains: document.getElementById("haDomains"),
|
|
371
|
+
haIncludeDisabled: document.getElementById("haIncludeDisabled"),
|
|
372
|
+
jsonMapping: document.getElementById("json-mapping"),
|
|
373
|
+
btnGenerate: document.getElementById("btn-generate"),
|
|
374
|
+
zoneOutput: document.getElementById("zone-output"),
|
|
375
|
+
btnCopyZone: document.getElementById("btn-copy-zone"),
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
const KNX_SAMPLE = `{
|
|
296
379
|
topic: "0/1/2",
|
|
297
380
|
payload: false,
|
|
298
381
|
previouspayload: true,
|
|
@@ -312,285 +395,855 @@
|
|
|
312
395
|
dpt: "1.001",
|
|
313
396
|
dptdesc: "Humidity",
|
|
314
397
|
source: "15.15.22",
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
398
|
+
destination: "0/1/2",
|
|
399
|
+
rawValue: {
|
|
400
|
+
0: "0x0"
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}`;
|
|
404
|
+
|
|
405
|
+
const ETS_SAMPLE = `"Group name"\t"Address"\t"Central"\t"Unfiltered"\t"Description"\t"DatapointType"\t"Security"
|
|
406
|
+
"Lights"\t"0/-/-"\t""\t""\t""\t""\t"Auto"
|
|
407
|
+
"Living room"\t"0/0/-"\t""\t""\t""\t""\t"Auto"
|
|
408
|
+
"Front door contact"\t"0/0/1"\t""\t""\t""\t"DPST-1-1"\t"Auto"
|
|
409
|
+
"Motion sensor"\t"0/0/2"\t""\t""\t""\t"DPT-1"\t"Auto"
|
|
410
|
+
"Dimming value"\t"0/0/3"\t""\t""\t""\t"DPST-5-1"\t"Auto"`;
|
|
411
|
+
|
|
412
|
+
const HA_SAMPLE = `[
|
|
413
|
+
{
|
|
414
|
+
"entity_id": "binary_sensor.front_door",
|
|
415
|
+
"state": "off",
|
|
416
|
+
"attributes": {
|
|
417
|
+
"friendly_name": "Front door"
|
|
418
|
+
}
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
"entity_id": "binary_sensor.living_pir",
|
|
422
|
+
"state": "on",
|
|
423
|
+
"attributes": {
|
|
424
|
+
"friendly_name": "Living PIR"
|
|
425
|
+
}
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
"entity_id": "switch.garden_lights",
|
|
429
|
+
"state": "off",
|
|
430
|
+
"attributes": {
|
|
431
|
+
"friendly_name": "Garden lights"
|
|
318
432
|
}
|
|
319
433
|
}
|
|
434
|
+
]`;
|
|
435
|
+
|
|
436
|
+
const HA_REGISTRY_SAMPLE = `{
|
|
437
|
+
"data": {
|
|
438
|
+
"entities": [
|
|
439
|
+
{
|
|
440
|
+
"entity_id": "binary_sensor.front_door",
|
|
441
|
+
"name": "Front door",
|
|
442
|
+
"original_name": "Front door contact",
|
|
443
|
+
"disabled_by": null
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
"entity_id": "binary_sensor.living_pir",
|
|
447
|
+
"name": "Living PIR",
|
|
448
|
+
"original_name": "Living PIR",
|
|
449
|
+
"disabled_by": null
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
"entity_id": "switch.garden_lights",
|
|
453
|
+
"name": "Garden lights",
|
|
454
|
+
"original_name": "Garden lights",
|
|
455
|
+
"disabled_by": "user"
|
|
456
|
+
}
|
|
457
|
+
]
|
|
458
|
+
},
|
|
459
|
+
"key": "core.entity_registry",
|
|
460
|
+
"version": 1
|
|
320
461
|
}`;
|
|
321
462
|
|
|
322
|
-
|
|
323
|
-
|
|
463
|
+
let parsedObject = null;
|
|
464
|
+
let lastGenerated = null;
|
|
324
465
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
466
|
+
function showStatus(el, kind, message) {
|
|
467
|
+
el.style.display = "";
|
|
468
|
+
el.className = `status ${kind}`;
|
|
469
|
+
el.textContent = message;
|
|
470
|
+
}
|
|
330
471
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
472
|
+
function hideStatus(el) {
|
|
473
|
+
el.style.display = "none";
|
|
474
|
+
el.textContent = "";
|
|
475
|
+
}
|
|
335
476
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
477
|
+
function normalizeLenientJson(input) {
|
|
478
|
+
let text = String(input || "");
|
|
479
|
+
text = text.replace(/\r\n/g, "\n");
|
|
480
|
+
text = text.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
481
|
+
text = text.replace(/(^|[^:])\/\/.*$/gm, "$1");
|
|
341
482
|
|
|
342
|
-
|
|
343
|
-
|
|
483
|
+
text = text.replace(/([{,]\s*)([A-Za-z_$][\w$]*)(\s*:)/g, '$1"$2"$3');
|
|
484
|
+
text = text.replace(/([{,]\s*)(\d+)(\s*:)/g, '$1"$2"$3');
|
|
344
485
|
|
|
345
|
-
|
|
486
|
+
text = text.replace(/:\s*0x([0-9a-fA-F]+)\b/g, ': "0x$1"');
|
|
346
487
|
|
|
347
|
-
|
|
348
|
-
|
|
488
|
+
return text;
|
|
489
|
+
}
|
|
349
490
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
491
|
+
function parseInput(text) {
|
|
492
|
+
const raw = String(text || "").trim();
|
|
493
|
+
if (!raw) return { ok: false, error: "Empty input." };
|
|
353
494
|
|
|
495
|
+
try {
|
|
496
|
+
return {
|
|
497
|
+
ok: true,
|
|
498
|
+
value: JSON.parse(raw),
|
|
499
|
+
note: "Parsed as strict JSON.",
|
|
500
|
+
};
|
|
501
|
+
} catch (err1) {
|
|
354
502
|
try {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
details: String(err2 && err2.message ? err2.message : err2),
|
|
367
|
-
};
|
|
503
|
+
const normalized = normalizeLenientJson(raw);
|
|
504
|
+
const value = JSON.parse(normalized);
|
|
505
|
+
return {
|
|
506
|
+
ok: true,
|
|
507
|
+
value,
|
|
508
|
+
note: "Parsed after normalizing (removed comments, quoted keys).",
|
|
509
|
+
};
|
|
510
|
+
} catch (err2) {
|
|
511
|
+
const ets = parseEtsExport(raw);
|
|
512
|
+
if (ets.ok) {
|
|
513
|
+
return { ok: true, value: { __ets: true, ets }, note: ets.note };
|
|
368
514
|
}
|
|
515
|
+
return {
|
|
516
|
+
ok: false,
|
|
517
|
+
error:
|
|
518
|
+
"Unable to parse. Please paste strict JSON, or a JS-style object with only // comments and unquoted keys.",
|
|
519
|
+
details: String(err2 && err2.message ? err2.message : err2),
|
|
520
|
+
};
|
|
369
521
|
}
|
|
370
522
|
}
|
|
523
|
+
}
|
|
371
524
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
return out;
|
|
380
|
-
}
|
|
381
|
-
if (typeof value !== "object") {
|
|
382
|
-
out.push(basePath || "(root)");
|
|
383
|
-
return out;
|
|
384
|
-
}
|
|
525
|
+
function normalizeHeaderKey(value) {
|
|
526
|
+
return String(value || "")
|
|
527
|
+
.trim()
|
|
528
|
+
.toLowerCase()
|
|
529
|
+
.replace(/^\ufeff/, "")
|
|
530
|
+
.replace(/[^a-z0-9]/g, "");
|
|
531
|
+
}
|
|
385
532
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
533
|
+
function parseDelimitedLine(line, delimiter) {
|
|
534
|
+
const out = [];
|
|
535
|
+
let cur = "";
|
|
536
|
+
let inQuotes = false;
|
|
537
|
+
for (let i = 0; i < line.length; i += 1) {
|
|
538
|
+
const ch = line[i];
|
|
539
|
+
if (inQuotes) {
|
|
540
|
+
if (ch === '"') {
|
|
541
|
+
const next = line[i + 1];
|
|
542
|
+
if (next === '"') {
|
|
543
|
+
cur += '"';
|
|
544
|
+
i += 1;
|
|
545
|
+
} else {
|
|
546
|
+
inQuotes = false;
|
|
547
|
+
}
|
|
548
|
+
} else {
|
|
549
|
+
cur += ch;
|
|
550
|
+
}
|
|
551
|
+
} else {
|
|
552
|
+
if (ch === '"') {
|
|
553
|
+
inQuotes = true;
|
|
554
|
+
} else if (ch === delimiter) {
|
|
555
|
+
out.push(cur.trim());
|
|
556
|
+
cur = "";
|
|
557
|
+
} else {
|
|
558
|
+
cur += ch;
|
|
391
559
|
}
|
|
392
|
-
return out;
|
|
393
560
|
}
|
|
561
|
+
}
|
|
562
|
+
out.push(cur.trim());
|
|
563
|
+
return out;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function detectDelimiter(headerLine) {
|
|
567
|
+
const line = String(headerLine || "");
|
|
568
|
+
if (line.includes("\t")) return "\t";
|
|
569
|
+
if (line.includes(";")) return ";";
|
|
570
|
+
if (line.includes(",")) return ",";
|
|
571
|
+
return "\t";
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function isBooleanDatapointType(value) {
|
|
575
|
+
const t = String(value || "")
|
|
576
|
+
.trim()
|
|
577
|
+
.toLowerCase();
|
|
578
|
+
if (!t) return false;
|
|
579
|
+
return (
|
|
580
|
+
t === "dpt-1" ||
|
|
581
|
+
t.startsWith("dpt-1-") ||
|
|
582
|
+
t.startsWith("dpst-1-") ||
|
|
583
|
+
/^1\.\d+/.test(t)
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function parseEtsExport(text) {
|
|
588
|
+
const raw = String(text || "")
|
|
589
|
+
.replace(/\r\n/g, "\n")
|
|
590
|
+
.replace(/\r/g, "\n")
|
|
591
|
+
.trim();
|
|
592
|
+
|
|
593
|
+
const lines = raw
|
|
594
|
+
.split("\n")
|
|
595
|
+
.map((l) => l.trim())
|
|
596
|
+
.filter((l) => l.length > 0);
|
|
597
|
+
|
|
598
|
+
if (lines.length < 2) return { ok: false };
|
|
599
|
+
|
|
600
|
+
const headerLine = lines[0];
|
|
601
|
+
const delimiter = detectDelimiter(headerLine);
|
|
602
|
+
const headers = parseDelimitedLine(headerLine, delimiter);
|
|
603
|
+
const keyByIndex = headers.map(normalizeHeaderKey);
|
|
604
|
+
|
|
605
|
+
const idxGroupName = keyByIndex.indexOf("groupname");
|
|
606
|
+
const idxAddress = keyByIndex.indexOf("address");
|
|
607
|
+
const idxDescription = keyByIndex.indexOf("description");
|
|
608
|
+
const idxDatapointType = keyByIndex.indexOf("datapointtype");
|
|
609
|
+
|
|
610
|
+
if (idxGroupName < 0 || idxAddress < 0) return { ok: false };
|
|
611
|
+
|
|
612
|
+
const rows = [];
|
|
613
|
+
for (let i = 1; i < lines.length; i += 1) {
|
|
614
|
+
const cols = parseDelimitedLine(lines[i], delimiter);
|
|
615
|
+
const groupName = cols[idxGroupName] || "";
|
|
616
|
+
const address = cols[idxAddress] || "";
|
|
617
|
+
const description =
|
|
618
|
+
idxDescription >= 0 ? cols[idxDescription] || "" : "";
|
|
619
|
+
const datapointType =
|
|
620
|
+
idxDatapointType >= 0 ? cols[idxDatapointType] || "" : "";
|
|
621
|
+
|
|
622
|
+
rows.push({ groupName, address, description, datapointType });
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
return {
|
|
626
|
+
ok: true,
|
|
627
|
+
note: "Detected ETS Group Addresses export (TSV).",
|
|
628
|
+
rows,
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function escapeRegExp(value) {
|
|
633
|
+
return String(value || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function globToRegExp(glob) {
|
|
637
|
+
const raw = String(glob || "").trim();
|
|
638
|
+
if (!raw) return null;
|
|
639
|
+
let pattern = "";
|
|
640
|
+
for (let i = 0; i < raw.length; i += 1) {
|
|
641
|
+
const ch = raw[i];
|
|
642
|
+
if (ch === "*") pattern += ".*";
|
|
643
|
+
else if (ch === "?") pattern += ".";
|
|
644
|
+
else pattern += escapeRegExp(ch);
|
|
645
|
+
}
|
|
646
|
+
try {
|
|
647
|
+
return new RegExp(`^${pattern}$`, "i");
|
|
648
|
+
} catch (_err) {
|
|
649
|
+
return null;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function splitPatterns(text) {
|
|
654
|
+
return String(text || "")
|
|
655
|
+
.split(/[\n,]+/g)
|
|
656
|
+
.map((p) => p.trim())
|
|
657
|
+
.filter((p) => p.length > 0);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function matchesAnyGlob(value, patterns) {
|
|
661
|
+
if (!patterns || patterns.length === 0) return true;
|
|
662
|
+
const target = String(value || "").trim();
|
|
663
|
+
if (!target) return false;
|
|
664
|
+
for (const p of patterns) {
|
|
665
|
+
const re = globToRegExp(p);
|
|
666
|
+
if (re && re.test(target)) return true;
|
|
667
|
+
}
|
|
668
|
+
return false;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function matchesNameFilter(row, nameFilterRaw) {
|
|
672
|
+
const filter = String(nameFilterRaw || "").trim();
|
|
673
|
+
if (!filter) return true;
|
|
674
|
+
const name = String(row && (row.description || row.groupName) ? row.description || row.groupName : "")
|
|
675
|
+
.trim()
|
|
676
|
+
.toLowerCase();
|
|
677
|
+
if (!name) return false;
|
|
678
|
+
if (filter.includes("*") || filter.includes("?")) {
|
|
679
|
+
const re = globToRegExp(filter);
|
|
680
|
+
return Boolean(re && re.test(name));
|
|
681
|
+
}
|
|
682
|
+
return name.includes(filter.toLowerCase());
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function extractHomeAssistantStates(value) {
|
|
686
|
+
if (Array.isArray(value)) return value;
|
|
687
|
+
if (value && typeof value === "object") {
|
|
688
|
+
if (Array.isArray(value.result)) return value.result;
|
|
689
|
+
if (Array.isArray(value.states)) return value.states;
|
|
690
|
+
if (Array.isArray(value.entities)) return value.entities;
|
|
691
|
+
}
|
|
692
|
+
return null;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function isHomeAssistantStatesExport(value) {
|
|
696
|
+
const states = extractHomeAssistantStates(value);
|
|
697
|
+
if (!states || states.length === 0) return false;
|
|
698
|
+
const sample = states.slice(0, 10);
|
|
699
|
+
return sample.some((s) => s && typeof s === "object" && typeof s.entity_id === "string");
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function extractHomeAssistantEntityRegistry(value) {
|
|
703
|
+
if (!value || typeof value !== "object") return null;
|
|
704
|
+
if (Array.isArray(value.entities)) return value.entities;
|
|
705
|
+
if (value.data && Array.isArray(value.data.entities)) return value.data.entities;
|
|
706
|
+
if (value.result && value.result.data && Array.isArray(value.result.data.entities)) return value.result.data.entities;
|
|
707
|
+
return null;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function isHomeAssistantEntityRegistryExport(value) {
|
|
711
|
+
const entities = extractHomeAssistantEntityRegistry(value);
|
|
712
|
+
if (!entities || entities.length === 0) return false;
|
|
713
|
+
const sample = entities.slice(0, 10);
|
|
714
|
+
return sample.some((e) => e && typeof e === "object" && typeof e.entity_id === "string");
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function getHaDomain(entityId) {
|
|
718
|
+
const id = String(entityId || "");
|
|
719
|
+
const idx = id.indexOf(".");
|
|
720
|
+
if (idx <= 0) return "";
|
|
721
|
+
return id.slice(0, idx).toLowerCase();
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function parseDomainList(input) {
|
|
725
|
+
return String(input || "")
|
|
726
|
+
.split(/[\s,]+/g)
|
|
727
|
+
.map((d) => d.trim().toLowerCase())
|
|
728
|
+
.filter((d) => d.length > 0);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function isPlainObject(value) {
|
|
732
|
+
return (
|
|
733
|
+
Boolean(value) && typeof value === "object" && !Array.isArray(value)
|
|
734
|
+
);
|
|
735
|
+
}
|
|
394
736
|
|
|
737
|
+
function enumeratePaths(value, basePath = "", out = []) {
|
|
738
|
+
if (value === null || value === undefined) {
|
|
739
|
+
out.push(basePath || "(root)");
|
|
740
|
+
return out;
|
|
741
|
+
}
|
|
742
|
+
if (typeof value !== "object") {
|
|
395
743
|
out.push(basePath || "(root)");
|
|
396
|
-
const keys = Object.keys(value).slice(0, 200);
|
|
397
|
-
for (const key of keys) {
|
|
398
|
-
const next = basePath ? `${basePath}.${key}` : key;
|
|
399
|
-
enumeratePaths(value[key], next, out);
|
|
400
|
-
}
|
|
401
744
|
return out;
|
|
402
745
|
}
|
|
403
746
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
const
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
.forEach((segment) => {
|
|
410
|
-
const m = segment.match(/^([^[\]]+)(\[(\d+)\])?$/);
|
|
411
|
-
if (!m) {
|
|
412
|
-
parts.push(segment);
|
|
413
|
-
return;
|
|
414
|
-
}
|
|
415
|
-
parts.push(m[1]);
|
|
416
|
-
if (m[2]) parts.push(Number(m[3]));
|
|
417
|
-
});
|
|
418
|
-
|
|
419
|
-
let cur = obj;
|
|
420
|
-
for (const part of parts) {
|
|
421
|
-
if (cur === null || cur === undefined) return undefined;
|
|
422
|
-
cur = cur[part];
|
|
747
|
+
if (Array.isArray(value)) {
|
|
748
|
+
out.push(basePath || "(root)");
|
|
749
|
+
const limit = Math.min(20, value.length);
|
|
750
|
+
for (let i = 0; i < limit; i += 1) {
|
|
751
|
+
enumeratePaths(value[i], `${basePath}[${i}]`, out);
|
|
423
752
|
}
|
|
424
|
-
return
|
|
753
|
+
return out;
|
|
425
754
|
}
|
|
426
755
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
} catch (err) {
|
|
433
|
-
return String(value);
|
|
434
|
-
}
|
|
756
|
+
out.push(basePath || "(root)");
|
|
757
|
+
const keys = Object.keys(value).slice(0, 200);
|
|
758
|
+
for (const key of keys) {
|
|
759
|
+
const next = basePath ? `${basePath}.${key}` : key;
|
|
760
|
+
enumeratePaths(value[key], next, out);
|
|
435
761
|
}
|
|
762
|
+
return out;
|
|
763
|
+
}
|
|
436
764
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
765
|
+
function getByPath(obj, path) {
|
|
766
|
+
if (!path || path === "(root)") return obj;
|
|
767
|
+
const parts = [];
|
|
768
|
+
String(path)
|
|
769
|
+
.split(".")
|
|
770
|
+
.forEach((segment) => {
|
|
771
|
+
const m = segment.match(/^([^[\]]+)(\[(\d+)\])?$/);
|
|
772
|
+
if (!m) {
|
|
773
|
+
parts.push(segment);
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
parts.push(m[1]);
|
|
777
|
+
if (m[2]) parts.push(Number(m[3]));
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
let cur = obj;
|
|
781
|
+
for (const part of parts) {
|
|
782
|
+
if (cur === null || cur === undefined) return undefined;
|
|
783
|
+
cur = cur[part];
|
|
442
784
|
}
|
|
785
|
+
return cur;
|
|
786
|
+
}
|
|
443
787
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
}
|
|
452
|
-
select.value = selected;
|
|
788
|
+
function toOneLine(value) {
|
|
789
|
+
if (value === undefined) return "undefined";
|
|
790
|
+
if (typeof value === "string") return value;
|
|
791
|
+
try {
|
|
792
|
+
return JSON.stringify(value);
|
|
793
|
+
} catch (err) {
|
|
794
|
+
return String(value);
|
|
453
795
|
}
|
|
796
|
+
}
|
|
454
797
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
const id = topic ? topic.replace(/[^\w]+/g, "_").replace(/^_+|_+$/g, "").toLowerCase() : "zone1";
|
|
798
|
+
function guessDefault(paths, candidates) {
|
|
799
|
+
for (const c of candidates) {
|
|
800
|
+
if (paths.includes(c)) return c;
|
|
801
|
+
}
|
|
802
|
+
return paths.includes("topic") ? "topic" : paths[0] || "(root)";
|
|
803
|
+
}
|
|
462
804
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
chime: false,
|
|
471
|
-
};
|
|
805
|
+
function setSelectOptions(select, paths, selected) {
|
|
806
|
+
select.innerHTML = "";
|
|
807
|
+
for (const p of paths) {
|
|
808
|
+
const opt = document.createElement("option");
|
|
809
|
+
opt.value = p;
|
|
810
|
+
opt.textContent = p;
|
|
811
|
+
select.appendChild(opt);
|
|
472
812
|
}
|
|
813
|
+
select.value = selected;
|
|
814
|
+
}
|
|
473
815
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
816
|
+
function buildZoneTemplate(normalizedMsg, zoneNameValue) {
|
|
817
|
+
const topic =
|
|
818
|
+
normalizedMsg && normalizedMsg.topic
|
|
819
|
+
? String(normalizedMsg.topic)
|
|
820
|
+
: "";
|
|
821
|
+
const name =
|
|
822
|
+
zoneNameValue !== undefined &&
|
|
823
|
+
zoneNameValue !== null &&
|
|
824
|
+
String(zoneNameValue).trim().length > 0
|
|
825
|
+
? String(zoneNameValue).trim()
|
|
826
|
+
: topic || "Zone";
|
|
827
|
+
const id = topic
|
|
828
|
+
? topic
|
|
829
|
+
.replace(/[^\w]+/g, "_")
|
|
830
|
+
.replace(/^_+|_+$/g, "")
|
|
831
|
+
.toLowerCase()
|
|
832
|
+
: "zone1";
|
|
833
|
+
|
|
834
|
+
return {
|
|
835
|
+
id,
|
|
836
|
+
name,
|
|
837
|
+
topic,
|
|
838
|
+
type: "perimeter",
|
|
839
|
+
entry: false,
|
|
840
|
+
bypassable: true,
|
|
841
|
+
chime: false,
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
async function copyToClipboard(text) {
|
|
846
|
+
try {
|
|
847
|
+
await navigator.clipboard.writeText(text);
|
|
848
|
+
return true;
|
|
849
|
+
} catch (err) {
|
|
850
|
+
return false;
|
|
481
851
|
}
|
|
852
|
+
}
|
|
482
853
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
854
|
+
function renderJson(el, obj) {
|
|
855
|
+
el.style.display = "";
|
|
856
|
+
el.className = "status ok";
|
|
857
|
+
el.innerHTML = "";
|
|
858
|
+
const pre = document.createElement("pre");
|
|
859
|
+
pre.textContent = JSON.stringify(obj, null, 2);
|
|
860
|
+
el.appendChild(pre);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
function renderText(el, text) {
|
|
864
|
+
el.style.display = "";
|
|
865
|
+
el.className = "status ok";
|
|
866
|
+
el.innerHTML = "";
|
|
867
|
+
const pre = document.createElement("pre");
|
|
868
|
+
pre.textContent = String(text || "");
|
|
869
|
+
el.appendChild(pre);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
function parseAndPopulate() {
|
|
873
|
+
hideStatus(els.parseStatus);
|
|
874
|
+
hideStatus(els.genStatus);
|
|
875
|
+
hideStatus(els.etsHint);
|
|
876
|
+
hideStatus(els.haHint);
|
|
877
|
+
els.jsonMapping.style.display = "";
|
|
878
|
+
els.etsMapping.style.display = "none";
|
|
879
|
+
els.haMapping.style.display = "none";
|
|
880
|
+
els.zoneOutput.value = "";
|
|
881
|
+
els.btnCopyZone.disabled = true;
|
|
882
|
+
lastGenerated = null;
|
|
883
|
+
|
|
884
|
+
const result = parseInput(els.input.value);
|
|
885
|
+
if (!result.ok) {
|
|
886
|
+
parsedObject = null;
|
|
887
|
+
showStatus(
|
|
888
|
+
els.parseStatus,
|
|
889
|
+
"err",
|
|
890
|
+
`${result.error}${result.details ? "\n" + result.details : ""}`,
|
|
891
|
+
);
|
|
892
|
+
return;
|
|
490
893
|
}
|
|
491
894
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
895
|
+
if (
|
|
896
|
+
result.value &&
|
|
897
|
+
result.value.__ets &&
|
|
898
|
+
result.value.ets &&
|
|
899
|
+
Array.isArray(result.value.ets.rows)
|
|
900
|
+
) {
|
|
901
|
+
parsedObject = result.value;
|
|
902
|
+
showStatus(els.parseStatus, "ok", result.note);
|
|
903
|
+
els.jsonMapping.style.display = "none";
|
|
904
|
+
els.etsMapping.style.display = "";
|
|
905
|
+
els.haMapping.style.display = "none";
|
|
906
|
+
showStatus(
|
|
907
|
+
els.etsHint,
|
|
908
|
+
"ok",
|
|
909
|
+
"ETS import mode: mapping is fixed.\n- Topic: Address (e.g. 0/0/1)\n- Name: Description, fallback to Group name\nClick “Generate output” to create a zones JSON array.",
|
|
910
|
+
);
|
|
911
|
+
return;
|
|
499
912
|
}
|
|
500
913
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
els.
|
|
505
|
-
els.
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
914
|
+
if (isHomeAssistantStatesExport(result.value)) {
|
|
915
|
+
parsedObject = { __ha: true, ha: { kind: "states", states: extractHomeAssistantStates(result.value) } };
|
|
916
|
+
showStatus(els.parseStatus, "ok", "Detected Home Assistant states export (JSON).");
|
|
917
|
+
els.jsonMapping.style.display = "none";
|
|
918
|
+
els.etsMapping.style.display = "none";
|
|
919
|
+
els.haMapping.style.display = "";
|
|
920
|
+
showStatus(
|
|
921
|
+
els.haHint,
|
|
922
|
+
"ok",
|
|
923
|
+
"Home Assistant import mode.\n- Topic: entity_id\n- Name: attributes.friendly_name (fallback entity_id)\nUse the filters below, then click “Generate output”.",
|
|
924
|
+
);
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
514
927
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
928
|
+
if (isHomeAssistantEntityRegistryExport(result.value)) {
|
|
929
|
+
parsedObject = { __ha: true, ha: { kind: "entity_registry", entities: extractHomeAssistantEntityRegistry(result.value) } };
|
|
930
|
+
showStatus(els.parseStatus, "ok", "Detected Home Assistant entity registry export (core.entity_registry).");
|
|
931
|
+
els.jsonMapping.style.display = "none";
|
|
932
|
+
els.etsMapping.style.display = "none";
|
|
933
|
+
els.haMapping.style.display = "";
|
|
934
|
+
showStatus(
|
|
935
|
+
els.haHint,
|
|
936
|
+
"ok",
|
|
937
|
+
"Home Assistant import mode (entity registry).\n- Topic: entity_id\n- Name: name/original_name (fallback entity_id)\nUse the filters below, then click “Generate output”.",
|
|
938
|
+
);
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
526
941
|
|
|
527
|
-
|
|
528
|
-
|
|
942
|
+
els.topicPath.disabled = false;
|
|
943
|
+
els.valuePath.disabled = false;
|
|
944
|
+
els.namePath.disabled = false;
|
|
945
|
+
|
|
946
|
+
if (!isPlainObject(result.value)) {
|
|
947
|
+
parsedObject = result.value;
|
|
948
|
+
showStatus(
|
|
949
|
+
els.parseStatus,
|
|
950
|
+
"warn",
|
|
951
|
+
`${result.note} Note: the root is not an object. Paths will be limited.`,
|
|
952
|
+
);
|
|
953
|
+
} else {
|
|
954
|
+
parsedObject = result.value;
|
|
955
|
+
showStatus(els.parseStatus, "ok", result.note);
|
|
956
|
+
}
|
|
529
957
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
958
|
+
const paths = Array.from(new Set(enumeratePaths(parsedObject))).filter(
|
|
959
|
+
Boolean,
|
|
960
|
+
);
|
|
961
|
+
paths.sort((a, b) => a.localeCompare(b));
|
|
962
|
+
|
|
963
|
+
const defaultTopic = guessDefault(paths, [
|
|
964
|
+
"topic",
|
|
965
|
+
"knx.destination",
|
|
966
|
+
"destination",
|
|
967
|
+
]);
|
|
968
|
+
const defaultValue = guessDefault(paths, [
|
|
969
|
+
"payload",
|
|
970
|
+
"payloadsubtypevalue",
|
|
971
|
+
"value",
|
|
972
|
+
]);
|
|
973
|
+
const defaultName = guessDefault(paths, [
|
|
974
|
+
"devicename",
|
|
975
|
+
"gainfo.ganame",
|
|
976
|
+
"name",
|
|
977
|
+
"topic",
|
|
978
|
+
]);
|
|
979
|
+
|
|
980
|
+
setSelectOptions(els.topicPath, paths, defaultTopic);
|
|
981
|
+
setSelectOptions(els.valuePath, paths, defaultValue);
|
|
982
|
+
setSelectOptions(els.namePath, paths, defaultName);
|
|
983
|
+
}
|
|
533
984
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
985
|
+
function generate() {
|
|
986
|
+
hideStatus(els.genStatus);
|
|
987
|
+
if (!parsedObject) {
|
|
988
|
+
showStatus(els.genStatus, "err", "Parse a valid message first.");
|
|
989
|
+
return;
|
|
537
990
|
}
|
|
538
991
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
992
|
+
if (
|
|
993
|
+
parsedObject &&
|
|
994
|
+
parsedObject.__ets &&
|
|
995
|
+
parsedObject.ets &&
|
|
996
|
+
Array.isArray(parsedObject.ets.rows)
|
|
997
|
+
) {
|
|
998
|
+
const allRows = parsedObject.ets.rows;
|
|
999
|
+
const addressPatterns = splitPatterns(
|
|
1000
|
+
els.etsAddressFilter ? els.etsAddressFilter.value : "",
|
|
1001
|
+
);
|
|
1002
|
+
const nameFilter = els.etsNameFilter ? els.etsNameFilter.value : "";
|
|
1003
|
+
|
|
1004
|
+
const leafRows = allRows.filter((r) => {
|
|
1005
|
+
const addr = String(r && r.address ? r.address : "").trim();
|
|
1006
|
+
if (!addr) return false;
|
|
1007
|
+
if (addr.includes("-")) return false;
|
|
1008
|
+
return /^\d+\/\d+\/\d+$/.test(addr);
|
|
1009
|
+
});
|
|
1010
|
+
|
|
1011
|
+
const booleanRows = leafRows.filter((r) =>
|
|
1012
|
+
isBooleanDatapointType(r.datapointType),
|
|
1013
|
+
);
|
|
1014
|
+
const filteredRows = booleanRows.filter((r) => {
|
|
1015
|
+
const addr = String(r && r.address ? r.address : "").trim();
|
|
1016
|
+
if (!matchesAnyGlob(addr, addressPatterns)) return false;
|
|
1017
|
+
if (!matchesNameFilter(r, nameFilter)) return false;
|
|
1018
|
+
return true;
|
|
1019
|
+
});
|
|
1020
|
+
|
|
1021
|
+
const zones = filteredRows.map((row) => {
|
|
1022
|
+
const address = String(row.address).trim();
|
|
1023
|
+
const topic = address;
|
|
1024
|
+
const nameCandidate =
|
|
1025
|
+
String(row.description || "").trim() ||
|
|
1026
|
+
String(row.groupName || "").trim();
|
|
1027
|
+
const name = nameCandidate || address;
|
|
1028
|
+
const idSafe = address
|
|
1029
|
+
.replace(/[^\w]+/g, "_")
|
|
1030
|
+
.replace(/^_+|_+$/g, "")
|
|
1031
|
+
.toLowerCase();
|
|
1032
|
+
const id = `ga_${idSafe || "unknown"}`;
|
|
1033
|
+
return {
|
|
1034
|
+
id,
|
|
1035
|
+
name,
|
|
1036
|
+
topic,
|
|
1037
|
+
type: "perimeter",
|
|
1038
|
+
entry: false,
|
|
1039
|
+
bypassable: true,
|
|
1040
|
+
chime: false,
|
|
1041
|
+
};
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
els.zoneOutput.value = JSON.stringify(zones, null, 2);
|
|
1045
|
+
els.btnCopyZone.disabled = zones.length === 0;
|
|
1046
|
+
const skippedGroups = allRows.length - leafRows.length;
|
|
1047
|
+
const skippedNonBoolean = leafRows.length - booleanRows.length;
|
|
1048
|
+
const skippedByFilters = booleanRows.length - filteredRows.length;
|
|
1049
|
+
showStatus(
|
|
1050
|
+
els.genStatus,
|
|
1051
|
+
zones.length ? "ok" : "warn",
|
|
1052
|
+
`Generated ${zones.length} zones from ETS (skipped ${skippedGroups} group rows, ${skippedNonBoolean} non-boolean datapoints, ${skippedByFilters} filtered out).`,
|
|
1053
|
+
);
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
545
1056
|
|
|
546
|
-
|
|
547
|
-
const
|
|
548
|
-
const
|
|
1057
|
+
if (parsedObject && parsedObject.__ha && parsedObject.ha && parsedObject.ha.kind === "states" && Array.isArray(parsedObject.ha.states)) {
|
|
1058
|
+
const states = parsedObject.ha.states;
|
|
1059
|
+
const entityPatterns = splitPatterns(els.haEntityFilter ? els.haEntityFilter.value : "");
|
|
1060
|
+
const nameFilter = els.haNameFilter ? els.haNameFilter.value : "";
|
|
1061
|
+
const domains = parseDomainList(els.haDomains ? els.haDomains.value : "");
|
|
1062
|
+
const defaultBooleanDomains = ["binary_sensor", "input_boolean", "switch"];
|
|
1063
|
+
const allowedDomains = domains.length ? domains : defaultBooleanDomains;
|
|
1064
|
+
|
|
1065
|
+
const eligible = states.filter((s) => s && typeof s === "object" && typeof s.entity_id === "string");
|
|
1066
|
+
const domainFiltered = eligible.filter((s) => allowedDomains.includes(getHaDomain(s.entity_id)));
|
|
1067
|
+
const filtered = domainFiltered.filter((s) => {
|
|
1068
|
+
const entityId = String(s.entity_id || "").trim();
|
|
1069
|
+
if (!matchesAnyGlob(entityId, entityPatterns)) return false;
|
|
1070
|
+
const row = { description: s.attributes && s.attributes.friendly_name ? s.attributes.friendly_name : "" , groupName: entityId };
|
|
1071
|
+
if (!matchesNameFilter(row, nameFilter)) return false;
|
|
1072
|
+
return true;
|
|
1073
|
+
});
|
|
1074
|
+
|
|
1075
|
+
const zones = filtered.map((s) => {
|
|
1076
|
+
const entityId = String(s.entity_id).trim();
|
|
1077
|
+
const friendly = s.attributes && s.attributes.friendly_name ? String(s.attributes.friendly_name).trim() : "";
|
|
1078
|
+
const name = friendly || entityId;
|
|
1079
|
+
const idSafe = entityId.replace(/[^\w]+/g, "_").replace(/^_+|_+$/g, "").toLowerCase();
|
|
1080
|
+
const id = `ha_${idSafe || "unknown"}`;
|
|
1081
|
+
return {
|
|
1082
|
+
id,
|
|
1083
|
+
name,
|
|
1084
|
+
topic: entityId,
|
|
1085
|
+
type: "perimeter",
|
|
1086
|
+
entry: false,
|
|
1087
|
+
bypassable: true,
|
|
1088
|
+
chime: false,
|
|
1089
|
+
};
|
|
1090
|
+
});
|
|
1091
|
+
|
|
1092
|
+
els.zoneOutput.value = JSON.stringify(zones, null, 2);
|
|
1093
|
+
els.btnCopyZone.disabled = zones.length === 0;
|
|
1094
|
+
const skippedNonEntity = states.length - eligible.length;
|
|
1095
|
+
const skippedDomain = eligible.length - domainFiltered.length;
|
|
1096
|
+
const skippedByFilters = domainFiltered.length - filtered.length;
|
|
1097
|
+
showStatus(
|
|
1098
|
+
els.genStatus,
|
|
1099
|
+
zones.length ? "ok" : "warn",
|
|
1100
|
+
`Generated ${zones.length} zones from HA (skipped ${skippedNonEntity} invalid rows, ${skippedDomain} domain filtered, ${skippedByFilters} filtered out).`,
|
|
1101
|
+
);
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
549
1104
|
|
|
550
|
-
|
|
551
|
-
const
|
|
552
|
-
const
|
|
1105
|
+
if (parsedObject && parsedObject.__ha && parsedObject.ha && parsedObject.ha.kind === "entity_registry" && Array.isArray(parsedObject.ha.entities)) {
|
|
1106
|
+
const entities = parsedObject.ha.entities;
|
|
1107
|
+
const includeDisabled = Boolean(els.haIncludeDisabled && els.haIncludeDisabled.checked);
|
|
1108
|
+
const entityPatterns = splitPatterns(els.haEntityFilter ? els.haEntityFilter.value : "");
|
|
1109
|
+
const nameFilter = els.haNameFilter ? els.haNameFilter.value : "";
|
|
1110
|
+
const domains = parseDomainList(els.haDomains ? els.haDomains.value : "");
|
|
1111
|
+
const defaultBooleanDomains = ["binary_sensor", "input_boolean", "switch"];
|
|
1112
|
+
const allowedDomains = domains.length ? domains : defaultBooleanDomains;
|
|
1113
|
+
|
|
1114
|
+
const eligible = entities.filter((e) => e && typeof e === "object" && typeof e.entity_id === "string");
|
|
1115
|
+
const enabledOnly = eligible.filter((e) => includeDisabled || !e.disabled_by);
|
|
1116
|
+
const domainFiltered = enabledOnly.filter((e) => allowedDomains.includes(getHaDomain(e.entity_id)));
|
|
1117
|
+
const filtered = domainFiltered.filter((e) => {
|
|
1118
|
+
const entityId = String(e.entity_id || "").trim();
|
|
1119
|
+
if (!matchesAnyGlob(entityId, entityPatterns)) return false;
|
|
1120
|
+
const displayName = String(e.name || "").trim() || String(e.original_name || "").trim() || entityId;
|
|
1121
|
+
const row = { description: displayName, groupName: entityId };
|
|
1122
|
+
if (!matchesNameFilter(row, nameFilter)) return false;
|
|
1123
|
+
return true;
|
|
1124
|
+
});
|
|
1125
|
+
|
|
1126
|
+
const zones = filtered.map((e) => {
|
|
1127
|
+
const entityId = String(e.entity_id).trim();
|
|
1128
|
+
const displayName = String(e.name || "").trim() || String(e.original_name || "").trim() || entityId;
|
|
1129
|
+
const idSafe = entityId.replace(/[^\w]+/g, "_").replace(/^_+|_+$/g, "").toLowerCase();
|
|
1130
|
+
const id = `ha_${idSafe || "unknown"}`;
|
|
1131
|
+
return {
|
|
1132
|
+
id,
|
|
1133
|
+
name: displayName,
|
|
1134
|
+
topic: entityId,
|
|
1135
|
+
type: "perimeter",
|
|
1136
|
+
entry: false,
|
|
1137
|
+
bypassable: true,
|
|
1138
|
+
chime: false,
|
|
1139
|
+
};
|
|
1140
|
+
});
|
|
1141
|
+
|
|
1142
|
+
els.zoneOutput.value = JSON.stringify(zones, null, 2);
|
|
1143
|
+
els.btnCopyZone.disabled = zones.length === 0;
|
|
1144
|
+
const skippedNonEntity = entities.length - eligible.length;
|
|
1145
|
+
const skippedDisabled = eligible.length - enabledOnly.length;
|
|
1146
|
+
const skippedDomain = enabledOnly.length - domainFiltered.length;
|
|
1147
|
+
const skippedByFilters = domainFiltered.length - filtered.length;
|
|
1148
|
+
showStatus(
|
|
1149
|
+
els.genStatus,
|
|
1150
|
+
zones.length ? "ok" : "warn",
|
|
1151
|
+
`Generated ${zones.length} zones from HA registry (skipped ${skippedNonEntity} invalid rows, ${skippedDisabled} disabled, ${skippedDomain} domain filtered, ${skippedByFilters} filtered out).`,
|
|
1152
|
+
);
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
553
1155
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
1156
|
+
const topicPath = els.topicPath.value;
|
|
1157
|
+
const valuePath = els.valuePath.value;
|
|
1158
|
+
const namePath = els.namePath.value;
|
|
1159
|
+
|
|
1160
|
+
const topicValue = getByPath(parsedObject, topicPath);
|
|
1161
|
+
const valueValue = getByPath(parsedObject, valuePath);
|
|
1162
|
+
const nameValue = getByPath(parsedObject, namePath);
|
|
1163
|
+
|
|
1164
|
+
if (topicValue === undefined) {
|
|
1165
|
+
showStatus(
|
|
1166
|
+
els.genStatus,
|
|
1167
|
+
"err",
|
|
1168
|
+
`Topic path "${topicPath}" is undefined in the input message.`,
|
|
1169
|
+
);
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
558
1172
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
1173
|
+
const normalized = {
|
|
1174
|
+
...parsedObject,
|
|
1175
|
+
topic: String(topicValue),
|
|
1176
|
+
payload: valueValue,
|
|
1177
|
+
};
|
|
1178
|
+
const zone = buildZoneTemplate(normalized, nameValue);
|
|
1179
|
+
els.zoneOutput.value = JSON.stringify(zone, null, 2);
|
|
562
1180
|
|
|
563
|
-
|
|
1181
|
+
els.btnCopyZone.disabled = false;
|
|
564
1182
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
1183
|
+
lastGenerated = { normalized, zone };
|
|
1184
|
+
showStatus(
|
|
1185
|
+
els.genStatus,
|
|
1186
|
+
"ok",
|
|
1187
|
+
`Generated using topic="${toOneLine(topicValue)}" and value="${valuePath}".`,
|
|
1188
|
+
);
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
els.btnParse.addEventListener("click", () => parseAndPopulate());
|
|
1192
|
+
els.btnGenerate.addEventListener("click", () => generate());
|
|
1193
|
+
els.btnClear.addEventListener("click", () => {
|
|
1194
|
+
els.input.value = "";
|
|
1195
|
+
parsedObject = null;
|
|
1196
|
+
lastGenerated = null;
|
|
1197
|
+
hideStatus(els.parseStatus);
|
|
1198
|
+
hideStatus(els.genStatus);
|
|
1199
|
+
hideStatus(els.etsHint);
|
|
1200
|
+
hideStatus(els.haHint);
|
|
1201
|
+
els.jsonMapping.style.display = "";
|
|
1202
|
+
els.etsMapping.style.display = "none";
|
|
1203
|
+
if (els.etsAddressFilter) els.etsAddressFilter.value = "";
|
|
1204
|
+
if (els.etsNameFilter) els.etsNameFilter.value = "";
|
|
1205
|
+
els.haMapping.style.display = "none";
|
|
1206
|
+
if (els.haEntityFilter) els.haEntityFilter.value = "";
|
|
1207
|
+
if (els.haNameFilter) els.haNameFilter.value = "";
|
|
1208
|
+
if (els.haDomains) els.haDomains.value = "";
|
|
1209
|
+
if (els.haIncludeDisabled) els.haIncludeDisabled.checked = false;
|
|
1210
|
+
els.zoneOutput.value = "";
|
|
1211
|
+
els.btnCopyZone.disabled = true;
|
|
1212
|
+
els.topicPath.innerHTML = "";
|
|
1213
|
+
els.valuePath.innerHTML = "";
|
|
1214
|
+
els.namePath.innerHTML = "";
|
|
1215
|
+
els.topicPath.disabled = false;
|
|
1216
|
+
els.valuePath.disabled = false;
|
|
1217
|
+
els.namePath.disabled = false;
|
|
1218
|
+
});
|
|
1219
|
+
els.btnLoadKnx.addEventListener("click", () => {
|
|
1220
|
+
els.input.value = KNX_SAMPLE;
|
|
1221
|
+
parseAndPopulate();
|
|
1222
|
+
});
|
|
1223
|
+
els.btnLoadEts.addEventListener("click", () => {
|
|
1224
|
+
els.input.value = ETS_SAMPLE;
|
|
1225
|
+
parseAndPopulate();
|
|
1226
|
+
});
|
|
1227
|
+
els.btnLoadHa.addEventListener("click", () => {
|
|
1228
|
+
els.input.value = HA_SAMPLE;
|
|
1229
|
+
parseAndPopulate();
|
|
1230
|
+
});
|
|
1231
|
+
els.btnLoadHaRegistry.addEventListener("click", () => {
|
|
1232
|
+
els.input.value = HA_REGISTRY_SAMPLE;
|
|
1233
|
+
parseAndPopulate();
|
|
1234
|
+
});
|
|
1235
|
+
|
|
1236
|
+
els.btnCopyZone.addEventListener("click", async () => {
|
|
1237
|
+
const text = String(els.zoneOutput.value || "").trim();
|
|
1238
|
+
if (!text) return;
|
|
1239
|
+
const ok = await copyToClipboard(text);
|
|
1240
|
+
showStatus(
|
|
1241
|
+
els.genStatus,
|
|
1242
|
+
ok ? "ok" : "warn",
|
|
1243
|
+
ok ? "Zone JSON copied." : "Copy failed.",
|
|
1244
|
+
);
|
|
1245
|
+
});
|
|
1246
|
+
</script>
|
|
1247
|
+
</body>
|
|
568
1248
|
|
|
569
|
-
els.btnParse.addEventListener("click", () => parseAndPopulate());
|
|
570
|
-
els.btnGenerate.addEventListener("click", () => generate());
|
|
571
|
-
els.btnClear.addEventListener("click", () => {
|
|
572
|
-
els.input.value = "";
|
|
573
|
-
parsedObject = null;
|
|
574
|
-
lastGenerated = null;
|
|
575
|
-
hideStatus(els.parseStatus);
|
|
576
|
-
hideStatus(els.genStatus);
|
|
577
|
-
els.zoneOutput.value = "";
|
|
578
|
-
els.btnCopyZone.disabled = true;
|
|
579
|
-
els.topicPath.innerHTML = "";
|
|
580
|
-
els.valuePath.innerHTML = "";
|
|
581
|
-
els.namePath.innerHTML = "";
|
|
582
|
-
});
|
|
583
|
-
els.btnLoadKnx.addEventListener("click", () => {
|
|
584
|
-
els.input.value = KNX_SAMPLE;
|
|
585
|
-
parseAndPopulate();
|
|
586
|
-
});
|
|
587
|
-
|
|
588
|
-
els.btnCopyZone.addEventListener("click", async () => {
|
|
589
|
-
const text = String(els.zoneOutput.value || "").trim();
|
|
590
|
-
if (!text) return;
|
|
591
|
-
const ok = await copyToClipboard(text);
|
|
592
|
-
showStatus(els.genStatus, ok ? "ok" : "warn", ok ? "Zone JSON copied." : "Copy failed.");
|
|
593
|
-
});
|
|
594
|
-
</script>
|
|
595
|
-
</body>
|
|
596
1249
|
</html>
|