homebridge-zwave-usb 2.2.0 → 2.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/homebridge-ui/public/index.html +508 -347
- package/package.json +1 -1
|
@@ -1,388 +1,549 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
<
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
</div>
|
|
19
|
-
<div class="form-group">
|
|
20
|
-
<label for="serialPort">Serial Port Path</label>
|
|
21
|
-
<input type="text" class="form-control" id="serialPort" placeholder="/dev/ttyACM0" required />
|
|
22
|
-
<small class="form-text text-muted">
|
|
23
|
-
WARNING: Do not use /dev/ttyUSB0. Use /dev/serial/by-id/...
|
|
24
|
-
</small>
|
|
25
|
-
</div>
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>Z-Wave USB Plugin</title>
|
|
6
|
+
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
|
|
7
|
+
<style>
|
|
8
|
+
/* Homebridge Theme Compatibility */
|
|
9
|
+
:root {
|
|
10
|
+
--primary-color: #007bff;
|
|
11
|
+
--secondary-color: #6c757d;
|
|
12
|
+
--success-color: #28a745;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
body {
|
|
16
|
+
background-color: transparent !important;
|
|
17
|
+
}
|
|
26
18
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
19
|
+
.card {
|
|
20
|
+
margin-bottom: 20px;
|
|
21
|
+
background-color: var(--hb-background-color, #fff);
|
|
22
|
+
color: var(--hb-text-color, #212529);
|
|
23
|
+
border: 1px solid rgba(0,0,0,.125);
|
|
24
|
+
}
|
|
33
25
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
26
|
+
.table {
|
|
27
|
+
color: inherit;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.table thead th {
|
|
31
|
+
border-bottom: 2px solid var(--hb-border-color, #dee2e6);
|
|
32
|
+
color: var(--hb-text-color, #212529);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.table td {
|
|
36
|
+
border-top: 1px solid var(--hb-border-color, #dee2e6);
|
|
37
|
+
vertical-align: middle;
|
|
38
|
+
color: var(--hb-text-color, #212529);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.badge {
|
|
42
|
+
padding: 0.4em 0.6em;
|
|
43
|
+
border-radius: 0.25rem;
|
|
44
|
+
font-weight: 700;
|
|
45
|
+
color: #fff !important; /* Force white text for badges */
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.badge-success { background-color: var(--success-color); }
|
|
49
|
+
.badge-secondary { background-color: var(--secondary-color); }
|
|
50
|
+
.badge-light { background-color: #f8f9fa; color: #212529 !important; }
|
|
51
|
+
|
|
52
|
+
.nav-tabs { margin-bottom: 20px; border-bottom: 1px solid var(--hb-border-color, #dee2e6); }
|
|
53
|
+
.nav-link { color: var(--hb-text-color, #007bff); }
|
|
54
|
+
.nav-link.active {
|
|
55
|
+
background-color: var(--hb-background-color, #fff) !important;
|
|
56
|
+
color: var(--hb-text-color, #495057) !important;
|
|
57
|
+
border-color: var(--hb-border-color, #dee2e6) var(--hb-border-color, #dee2e6) transparent !important;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
fieldset { border: 1px solid var(--hb-border-color, #ddd); padding: 15px; border-radius: 4px; margin-bottom: 15px; }
|
|
61
|
+
legend { width: auto; padding: 0 10px; font-size: 1.1rem; font-weight: bold; color: var(--hb-text-color, inherit); }
|
|
62
|
+
|
|
63
|
+
.text-muted { color: #6c757d !important; }
|
|
64
|
+
.form-control {
|
|
65
|
+
background-color: var(--hb-background-color, #fff);
|
|
66
|
+
color: var(--hb-text-color, #495057);
|
|
67
|
+
border: 1px solid var(--hb-border-color, #ced4da);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.btn-group .btn { margin-right: 2px; }
|
|
71
|
+
|
|
72
|
+
/* Custom Modal styles to replace native prompt/confirm */
|
|
73
|
+
.custom-modal-backdrop {
|
|
74
|
+
display: none;
|
|
75
|
+
position: fixed;
|
|
76
|
+
top: 0; left: 0; width: 100%; height: 100%;
|
|
77
|
+
background: rgba(0,0,0,0.5);
|
|
78
|
+
z-index: 1050;
|
|
79
|
+
}
|
|
80
|
+
.custom-modal {
|
|
81
|
+
display: none;
|
|
82
|
+
position: fixed;
|
|
83
|
+
top: 50%; left: 50%;
|
|
84
|
+
transform: translate(-50%, -50%);
|
|
85
|
+
background: var(--hb-background-color, #fff);
|
|
86
|
+
color: var(--hb-text-color, #212529);
|
|
87
|
+
padding: 20px;
|
|
88
|
+
border-radius: 8px;
|
|
89
|
+
box-shadow: 0 5px 15px rgba(0,0,0,0.3);
|
|
90
|
+
z-index: 1051;
|
|
91
|
+
width: 90%;
|
|
92
|
+
max-width: 400px;
|
|
93
|
+
}
|
|
94
|
+
</style>
|
|
95
|
+
</head>
|
|
96
|
+
<body>
|
|
97
|
+
<div class="container-fluid p-3">
|
|
98
|
+
<ul class="nav nav-tabs mb-3" id="mainTabs" role="tablist">
|
|
99
|
+
<li class="nav-item">
|
|
100
|
+
<a class="nav-link active" id="config-tab" href="#config" role="tab">Configuration</a>
|
|
101
|
+
</li>
|
|
102
|
+
<li class="nav-item">
|
|
103
|
+
<a class="nav-link" id="maintenance-tab" href="#maintenance" role="tab">Maintenance</a>
|
|
104
|
+
</li>
|
|
105
|
+
</ul>
|
|
106
|
+
|
|
107
|
+
<div class="tab-content" id="mainTabsContent">
|
|
108
|
+
<!-- Configuration Tab -->
|
|
109
|
+
<div class="tab-pane fade show active" id="config" role="tabpanel">
|
|
110
|
+
<div class="card card-body">
|
|
111
|
+
<form id="configForm">
|
|
112
|
+
<div class="form-group">
|
|
113
|
+
<label for="name">Name</label>
|
|
114
|
+
<input type="text" class="form-control" id="name" placeholder="Homebridge Z-Wave USB" />
|
|
115
|
+
</div>
|
|
116
|
+
<div class="form-group">
|
|
117
|
+
<label for="serialPort">Serial Port Path</label>
|
|
118
|
+
<input type="text" class="form-control" id="serialPort" placeholder="/dev/ttyACM0" required />
|
|
119
|
+
<small class="form-text text-muted">
|
|
120
|
+
WARNING: Do not use /dev/ttyUSB0. Use /dev/serial/by-id/...
|
|
121
|
+
</small>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<fieldset>
|
|
125
|
+
<legend>Z-Wave Security Keys</legend>
|
|
126
|
+
<p class="text-muted small">Required for secure devices (Locks, Sensors).</p>
|
|
127
|
+
<button type="button" class="btn btn-outline-primary btn-sm mb-3" id="generateKeys">
|
|
128
|
+
Auto-Generate Keys
|
|
129
|
+
</button>
|
|
130
|
+
|
|
131
|
+
<div class="form-group">
|
|
132
|
+
<label for="S0_Legacy">S0 Legacy</label>
|
|
133
|
+
<input type="text" class="form-control" id="S0_Legacy" maxlength="32" />
|
|
134
|
+
</div>
|
|
135
|
+
<div class="form-group">
|
|
136
|
+
<label for="S2_Unauthenticated">S2 Unauthenticated</label>
|
|
137
|
+
<input type="text" class="form-control" id="S2_Unauthenticated" maxlength="32" />
|
|
138
|
+
</div>
|
|
139
|
+
<div class="form-group">
|
|
140
|
+
<label for="S2_Authenticated">S2 Authenticated</label>
|
|
141
|
+
<input type="text" class="form-control" id="S2_Authenticated" maxlength="32" />
|
|
142
|
+
</div>
|
|
143
|
+
<div class="form-group">
|
|
144
|
+
<label for="S2_AccessControl">S2 Access Control</label>
|
|
145
|
+
<input type="text" class="form-control" id="S2_AccessControl" maxlength="32" />
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
<p class="text-muted small mt-3">
|
|
149
|
+
Optional: Long Range specific keys (defaults to Classic S2 if empty)
|
|
150
|
+
</p>
|
|
151
|
+
<div class="form-group">
|
|
152
|
+
<label for="S2_Authenticated_LR">S2 Authenticated (LR)</label>
|
|
153
|
+
<input type="text" class="form-control" id="S2_Authenticated_LR" maxlength="32" />
|
|
154
|
+
</div>
|
|
155
|
+
<div class="form-group">
|
|
156
|
+
<label for="S2_AccessControl_LR">S2 Access Control (LR)</label>
|
|
157
|
+
<input type="text" class="form-control" id="S2_AccessControl_LR" maxlength="32" />
|
|
158
|
+
</div>
|
|
159
|
+
</fieldset>
|
|
160
|
+
|
|
161
|
+
<fieldset>
|
|
162
|
+
<legend>Advanced</legend>
|
|
163
|
+
<div class="form-group">
|
|
164
|
+
<label for="inclusionTimeoutSeconds">Inclusion Timeout (Seconds)</label>
|
|
165
|
+
<input type="number" class="form-control" id="inclusionTimeoutSeconds" placeholder="60" />
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<div class="form-check mt-3">
|
|
169
|
+
<input type="checkbox" class="form-check-input" id="debug" />
|
|
170
|
+
<label class="form-check-label" for="debug">Enable Verbose Driver Logging</label>
|
|
171
|
+
</div>
|
|
172
|
+
</fieldset>
|
|
173
|
+
</form>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
50
176
|
|
|
51
|
-
|
|
52
|
-
|
|
177
|
+
<!-- Maintenance Tab -->
|
|
178
|
+
<div class="tab-pane fade" id="maintenance" role="tabpanel">
|
|
179
|
+
<div class="card card-body">
|
|
180
|
+
<h5>Z-Wave Node Maintenance</h5>
|
|
181
|
+
<p class="text-muted small">
|
|
182
|
+
View and manage your Z-Wave nodes. Check for official firmware updates via Z-Wave JS Service.
|
|
53
183
|
</p>
|
|
54
|
-
<div class="
|
|
55
|
-
<
|
|
56
|
-
<input type="text" class="form-control" id="S2_Authenticated_LR" maxlength="32" />
|
|
184
|
+
<div class="alert alert-warning py-2 small" role="alert">
|
|
185
|
+
<strong>Beta Feature:</strong> Z-Wave Firmware Updates are provided as-is. Use at your own risk. Feedback is welcome!
|
|
57
186
|
</div>
|
|
58
|
-
<div class="
|
|
59
|
-
<
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
187
|
+
<div class="table-responsive">
|
|
188
|
+
<table class="table table-sm" id="nodeTable">
|
|
189
|
+
<thead>
|
|
190
|
+
<tr>
|
|
191
|
+
<th>ID</th>
|
|
192
|
+
<th>Name</th>
|
|
193
|
+
<th>Status</th>
|
|
194
|
+
<th>Firmware</th>
|
|
195
|
+
<th>Action</th>
|
|
196
|
+
</tr>
|
|
197
|
+
</thead>
|
|
198
|
+
<tbody id="nodeTableBody">
|
|
199
|
+
<tr>
|
|
200
|
+
<td colspan="5" class="text-center">Loading nodes...</td>
|
|
201
|
+
</tr>
|
|
202
|
+
</tbody>
|
|
203
|
+
</table>
|
|
69
204
|
</div>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
70
209
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
210
|
+
<!-- Custom Modals -->
|
|
211
|
+
<div id="modalBackdrop" class="custom-modal-backdrop"></div>
|
|
212
|
+
|
|
213
|
+
<div id="renameModal" class="custom-modal">
|
|
214
|
+
<h6>Rename Node</h6>
|
|
215
|
+
<div class="form-group">
|
|
216
|
+
<input type="text" class="form-control" id="newLabelInput" />
|
|
217
|
+
</div>
|
|
218
|
+
<div class="text-right">
|
|
219
|
+
<button class="btn btn-sm btn-secondary" id="cancelRename">Cancel</button>
|
|
220
|
+
<button class="btn btn-sm btn-primary" id="confirmRename">Save</button>
|
|
77
221
|
</div>
|
|
78
222
|
</div>
|
|
79
223
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
<
|
|
83
|
-
|
|
84
|
-
<
|
|
85
|
-
|
|
86
|
-
</p>
|
|
87
|
-
<div class="alert alert-warning py-2 small" role="alert">
|
|
88
|
-
<strong>Beta Feature:</strong> Z-Wave Firmware Updates are provided as-is. Use at your own risk. Feedback is welcome!
|
|
89
|
-
</div>
|
|
90
|
-
<div class="table-responsive">
|
|
91
|
-
<table class="table table-sm" id="nodeTable">
|
|
92
|
-
<thead>
|
|
93
|
-
<tr>
|
|
94
|
-
<th>ID</th>
|
|
95
|
-
<th>Name</th>
|
|
96
|
-
<th>Status</th>
|
|
97
|
-
<th>Firmware</th>
|
|
98
|
-
<th>Action</th>
|
|
99
|
-
</tr>
|
|
100
|
-
</thead>
|
|
101
|
-
<tbody id="nodeTableBody">
|
|
102
|
-
<tr>
|
|
103
|
-
<td colspan="5" class="text-center">Loading nodes...</td>
|
|
104
|
-
</tr>
|
|
105
|
-
</tbody>
|
|
106
|
-
</table>
|
|
107
|
-
</div>
|
|
224
|
+
<div id="confirmModal" class="custom-modal">
|
|
225
|
+
<h6>Confirm Action</h6>
|
|
226
|
+
<p id="confirmMessage" class="small"></p>
|
|
227
|
+
<div class="text-right">
|
|
228
|
+
<button class="btn btn-sm btn-secondary" id="cancelConfirm">Cancel</button>
|
|
229
|
+
<button class="btn btn-sm btn-primary" id="executeConfirm">OK</button>
|
|
108
230
|
</div>
|
|
109
231
|
</div>
|
|
110
|
-
</div>
|
|
111
|
-
|
|
112
|
-
<script>
|
|
113
|
-
(async () => {
|
|
114
|
-
let currentConfig = {};
|
|
115
|
-
|
|
116
|
-
const getInt = (id, def) => {
|
|
117
|
-
const el = document.getElementById(id);
|
|
118
|
-
if (!el) return def;
|
|
119
|
-
const val = parseInt(el.value, 10);
|
|
120
|
-
return isNaN(val) ? def : val;
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
const getConfig = () => {
|
|
124
|
-
const securityKeys = {
|
|
125
|
-
S0_Legacy: document.getElementById('S0_Legacy').value,
|
|
126
|
-
S2_Unauthenticated: document.getElementById('S2_Unauthenticated').value,
|
|
127
|
-
S2_Authenticated: document.getElementById('S2_Authenticated').value,
|
|
128
|
-
S2_AccessControl: document.getElementById('S2_AccessControl').value,
|
|
129
|
-
S2_Authenticated_LR: document.getElementById('S2_Authenticated_LR').value,
|
|
130
|
-
S2_AccessControl_LR: document.getElementById('S2_AccessControl_LR').value,
|
|
131
|
-
};
|
|
132
232
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
233
|
+
<script>
|
|
234
|
+
(async () => {
|
|
235
|
+
let currentConfig = {};
|
|
236
|
+
let isPromptOpen = false;
|
|
237
|
+
let activeNodeId = null;
|
|
238
|
+
let activeUpdateData = null;
|
|
239
|
+
|
|
240
|
+
const getInt = (id, def) => {
|
|
241
|
+
const el = document.getElementById(id);
|
|
242
|
+
if (!el) return def;
|
|
243
|
+
const val = parseInt(el.value, 10);
|
|
244
|
+
return isNaN(val) ? def : val;
|
|
140
245
|
};
|
|
141
246
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
247
|
+
const getConfig = () => {
|
|
248
|
+
const securityKeys = {
|
|
249
|
+
S0_Legacy: document.getElementById('S0_Legacy').value,
|
|
250
|
+
S2_Unauthenticated: document.getElementById('S2_Unauthenticated').value,
|
|
251
|
+
S2_Authenticated: document.getElementById('S2_Authenticated').value,
|
|
252
|
+
S2_AccessControl: document.getElementById('S2_AccessControl').value,
|
|
253
|
+
S2_Authenticated_LR: document.getElementById('S2_Authenticated_LR').value,
|
|
254
|
+
S2_AccessControl_LR: document.getElementById('S2_AccessControl_LR').value,
|
|
255
|
+
};
|
|
150
256
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
257
|
+
const newConfig = {
|
|
258
|
+
...currentConfig,
|
|
259
|
+
name: document.getElementById('name').value,
|
|
260
|
+
platform: 'ZWaveUSB',
|
|
261
|
+
serialPort: document.getElementById('serialPort').value,
|
|
262
|
+
inclusionTimeoutSeconds: getInt('inclusionTimeoutSeconds', 60),
|
|
263
|
+
debug: document.getElementById('debug').checked,
|
|
264
|
+
};
|
|
157
265
|
|
|
158
|
-
|
|
266
|
+
if (Object.values(securityKeys).some((k) => k && k.length > 0)) {
|
|
267
|
+
newConfig.securityKeys = securityKeys;
|
|
268
|
+
} else {
|
|
269
|
+
delete newConfig.securityKeys;
|
|
270
|
+
}
|
|
159
271
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
const nodes = await window.homebridge.request('get-nodes');
|
|
163
|
-
const tbody = document.getElementById('nodeTableBody');
|
|
164
|
-
tbody.innerHTML = '';
|
|
272
|
+
return newConfig;
|
|
273
|
+
};
|
|
165
274
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
275
|
+
const updateConfig = async () => {
|
|
276
|
+
if (window.homebridge) {
|
|
277
|
+
const newConfig = getConfig();
|
|
278
|
+
await window.homebridge.updatePluginConfig([newConfig]);
|
|
169
279
|
}
|
|
280
|
+
};
|
|
170
281
|
|
|
171
|
-
|
|
172
|
-
const tr = document.createElement('tr');
|
|
173
|
-
const status = statusMap[node.status] || 'Unknown';
|
|
174
|
-
const progress = node.firmwareProgress;
|
|
175
|
-
const nodeName = node.name || node.label || 'Node ' + node.nodeId;
|
|
176
|
-
|
|
177
|
-
let actionHtml = '';
|
|
178
|
-
if (progress) {
|
|
179
|
-
const pct = Math.round((progress.sent / progress.total) * 100);
|
|
180
|
-
actionHtml = `
|
|
181
|
-
<div class="progress" style="height: 20px;">
|
|
182
|
-
<div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: ${pct}%">${pct}%</div>
|
|
183
|
-
</div>
|
|
184
|
-
`;
|
|
185
|
-
} else if (node.nodeId === 1) {
|
|
186
|
-
actionHtml = `<span class="badge badge-light">Controller</span>`;
|
|
187
|
-
} else {
|
|
188
|
-
actionHtml = `
|
|
189
|
-
<div class="btn-group">
|
|
190
|
-
<button class="btn btn-sm btn-outline-secondary rename-node" data-nodeid="${node.nodeId}" data-name="${nodeName}">Rename</button>
|
|
191
|
-
<button class="btn btn-sm btn-outline-primary check-update" data-nodeid="${node.nodeId}">Check Update</button>
|
|
192
|
-
</div>
|
|
193
|
-
`;
|
|
194
|
-
}
|
|
282
|
+
const statusMap = ['Unknown', 'Asleep', 'Awake', 'Dead', 'Alive'];
|
|
195
283
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
tbody.
|
|
204
|
-
});
|
|
284
|
+
const loadNodes = async () => {
|
|
285
|
+
if (isPromptOpen) return;
|
|
286
|
+
try {
|
|
287
|
+
const nodes = await window.homebridge.request('get-nodes');
|
|
288
|
+
const tbody = document.getElementById('nodeTableBody');
|
|
289
|
+
if (!tbody) return;
|
|
290
|
+
|
|
291
|
+
tbody.innerHTML = '';
|
|
205
292
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const currentName = e.target.getAttribute('data-name');
|
|
211
|
-
const newName = prompt(`Enter new name for Node ${nodeId}:`, currentName);
|
|
212
|
-
|
|
213
|
-
if (newName !== null && newName !== currentName && newName.trim() !== '') {
|
|
214
|
-
try {
|
|
215
|
-
await window.homebridge.request('rename-node', { nodeId, name: newName.trim() });
|
|
216
|
-
window.homebridge.toast.success(`Node ${nodeId} renamed to ${newName}`);
|
|
217
|
-
loadNodes();
|
|
218
|
-
} catch (err) {
|
|
219
|
-
window.homebridge.toast.error(err.message);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
});
|
|
223
|
-
});
|
|
293
|
+
if (!Array.isArray(nodes)) {
|
|
294
|
+
tbody.innerHTML = '<tr><td colspan="5" class="text-center text-danger">Plugin not responding. Is it running?</td></tr>';
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
224
297
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
const
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
const warning = isBattery
|
|
249
|
-
? 'WARNING: This is a battery-powered device. You MUST manually wake it up (usually by pressing a button on the device) immediately after clicking OK to receive the update. Proceed?'
|
|
250
|
-
: 'WARNING: Firmware updates carry risk. Do not unplug the bridge or device during the process. Proceed?';
|
|
251
|
-
|
|
252
|
-
if (!confirm(warning)) return;
|
|
253
|
-
|
|
254
|
-
const target = e.currentTarget;
|
|
255
|
-
const u = JSON.parse(target.getAttribute('data-update'));
|
|
256
|
-
target.disabled = true;
|
|
257
|
-
target.innerHTML = 'Starting...';
|
|
258
|
-
|
|
259
|
-
try {
|
|
260
|
-
await window.homebridge.request('start-update', { nodeId, update: u });
|
|
261
|
-
window.homebridge.toast.success('Firmware update started!');
|
|
262
|
-
loadNodes();
|
|
263
|
-
} catch (err) {
|
|
264
|
-
window.homebridge.toast.error(err.message);
|
|
265
|
-
loadNodes();
|
|
266
|
-
}
|
|
267
|
-
});
|
|
268
|
-
} else {
|
|
269
|
-
window.homebridge.toast.info('No updates available for this device.');
|
|
270
|
-
e.target.disabled = false;
|
|
271
|
-
e.target.innerHTML = 'Check Update';
|
|
272
|
-
}
|
|
273
|
-
} catch (err) {
|
|
274
|
-
window.homebridge.toast.error(err.message);
|
|
275
|
-
e.target.disabled = false;
|
|
276
|
-
e.target.innerHTML = 'Check Update';
|
|
298
|
+
nodes.forEach((node) => {
|
|
299
|
+
const tr = document.createElement('tr');
|
|
300
|
+
const status = statusMap[node.status] || 'Unknown';
|
|
301
|
+
const progress = node.firmwareProgress;
|
|
302
|
+
const nodeName = node.name || node.label || 'Node ' + node.nodeId;
|
|
303
|
+
|
|
304
|
+
let actionHtml = '';
|
|
305
|
+
if (progress) {
|
|
306
|
+
const pct = Math.round((progress.sent / progress.total) * 100);
|
|
307
|
+
actionHtml = `
|
|
308
|
+
<div class="progress" style="height: 20px;">
|
|
309
|
+
<div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: ${pct}%">${pct}%</div>
|
|
310
|
+
</div>
|
|
311
|
+
`;
|
|
312
|
+
} else if (node.nodeId === 1) {
|
|
313
|
+
actionHtml = `<span class="badge badge-light">Controller</span>`;
|
|
314
|
+
} else {
|
|
315
|
+
actionHtml = `
|
|
316
|
+
<div class="btn-group">
|
|
317
|
+
<button class="btn btn-sm btn-outline-secondary rename-node" data-nodeid="${node.nodeId}">Rename</button>
|
|
318
|
+
<button class="btn btn-sm btn-outline-primary check-update" data-nodeid="${node.nodeId}">Check Update</button>
|
|
319
|
+
</div>
|
|
320
|
+
`;
|
|
277
321
|
}
|
|
322
|
+
|
|
323
|
+
tr.innerHTML = `
|
|
324
|
+
<td>${node.nodeId}</td>
|
|
325
|
+
<td id="node-name-${node.nodeId}">${nodeName}</td>
|
|
326
|
+
<td><span class="badge badge-${status === 'Alive' || status === 'Awake' ? 'success' : 'secondary'}">${status}</span></td>
|
|
327
|
+
<td>${node.firmwareVersion || 'Unknown'}</td>
|
|
328
|
+
<td id="node-action-${node.nodeId}">${actionHtml}</td>
|
|
329
|
+
`;
|
|
330
|
+
tbody.appendChild(tr);
|
|
278
331
|
});
|
|
279
|
-
})
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
const init = async () => {
|
|
287
|
-
if (!window.homebridge) return;
|
|
288
|
-
|
|
289
|
-
// Load Config
|
|
290
|
-
const pluginConfig = await window.homebridge.getPluginConfig();
|
|
291
|
-
currentConfig = pluginConfig[0] || { platform: 'ZWaveUSB' };
|
|
292
|
-
const config = currentConfig;
|
|
293
|
-
|
|
294
|
-
// Populate Form
|
|
295
|
-
document.getElementById('name').value = config.name || 'Homebridge Z-Wave USB';
|
|
296
|
-
document.getElementById('serialPort').value = config.serialPort || '';
|
|
297
|
-
|
|
298
|
-
// Keys
|
|
299
|
-
const keys = config.securityKeys || {};
|
|
300
|
-
document.getElementById('S0_Legacy').value = keys.S0_Legacy || '';
|
|
301
|
-
document.getElementById('S2_Unauthenticated').value = keys.S2_Unauthenticated || '';
|
|
302
|
-
document.getElementById('S2_Authenticated').value = keys.S2_Authenticated || '';
|
|
303
|
-
document.getElementById('S2_AccessControl').value = keys.S2_AccessControl || '';
|
|
304
|
-
document.getElementById('S2_Authenticated_LR').value = keys.S2_Authenticated_LR || '';
|
|
305
|
-
document.getElementById('S2_AccessControl_LR').value = keys.S2_AccessControl_LR || '';
|
|
306
|
-
|
|
307
|
-
// Advanced
|
|
308
|
-
document.getElementById('inclusionTimeoutSeconds').value =
|
|
309
|
-
config.inclusionTimeoutSeconds || 60;
|
|
310
|
-
document.getElementById('debug').checked = config.debug || false;
|
|
311
|
-
|
|
312
|
-
// Auto-save on change
|
|
313
|
-
document.getElementById('configForm').addEventListener('input', updateConfig);
|
|
314
|
-
document.getElementById('configForm').addEventListener('change', updateConfig);
|
|
315
|
-
|
|
316
|
-
// Generate Keys
|
|
317
|
-
document.getElementById('generateKeys').addEventListener('click', () => {
|
|
318
|
-
if (document.getElementById('S0_Legacy').value && !confirm('Overwrite existing keys?'))
|
|
319
|
-
return;
|
|
332
|
+
} catch (err) {
|
|
333
|
+
const tbody = document.getElementById('nodeTableBody');
|
|
334
|
+
if (tbody) {
|
|
335
|
+
tbody.innerHTML = `<tr><td colspan="5" class="text-center text-danger">${err.message}</td></tr>`;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
};
|
|
320
339
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
};
|
|
340
|
+
// Modal helpers
|
|
341
|
+
const showModal = (modalId) => {
|
|
342
|
+
document.getElementById('modalBackdrop').style.display = 'block';
|
|
343
|
+
document.getElementById(modalId).style.display = 'block';
|
|
344
|
+
isPromptOpen = true;
|
|
345
|
+
};
|
|
328
346
|
|
|
329
|
-
|
|
330
|
-
document.getElementById('
|
|
331
|
-
document.getElementById('
|
|
332
|
-
document.getElementById('
|
|
333
|
-
|
|
334
|
-
|
|
347
|
+
const hideModals = () => {
|
|
348
|
+
document.getElementById('modalBackdrop').style.display = 'none';
|
|
349
|
+
document.getElementById('renameModal').style.display = 'none';
|
|
350
|
+
document.getElementById('confirmModal').style.display = 'none';
|
|
351
|
+
isPromptOpen = false;
|
|
352
|
+
activeNodeId = null;
|
|
353
|
+
activeUpdateData = null;
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
// Event Delegation for Table Actions (Rename & Firmware)
|
|
357
|
+
document.getElementById('nodeTableBody').addEventListener('click', async (e) => {
|
|
358
|
+
const target = e.target.closest('button');
|
|
359
|
+
if (!target) return;
|
|
335
360
|
|
|
336
|
-
|
|
337
|
-
|
|
361
|
+
const nodeId = target.getAttribute('data-nodeid');
|
|
362
|
+
if (!nodeId) return;
|
|
363
|
+
|
|
364
|
+
// 1. Rename Logic
|
|
365
|
+
if (target.classList.contains('rename-node')) {
|
|
366
|
+
const nameCell = document.getElementById(`node-name-${nodeId}`);
|
|
367
|
+
activeNodeId = nodeId;
|
|
368
|
+
document.getElementById('newLabelInput').value = nameCell ? nameCell.textContent : '';
|
|
369
|
+
showModal('renameModal');
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// 2. Check Update Logic
|
|
374
|
+
if (target.classList.contains('check-update')) {
|
|
375
|
+
target.disabled = true;
|
|
376
|
+
target.innerHTML = '<span class="spinner-border spinner-border-sm" role="status"></span>';
|
|
377
|
+
|
|
378
|
+
try {
|
|
379
|
+
const updates = await window.homebridge.request('check-firmware', nodeId);
|
|
380
|
+
if (updates && updates.length > 0) {
|
|
381
|
+
const update = updates[0];
|
|
382
|
+
const actionCell = document.getElementById(`node-action-${nodeId}`);
|
|
383
|
+
actionCell.innerHTML = `
|
|
384
|
+
<button class="btn btn-sm btn-success start-update" data-nodeid="${nodeId}" data-update='${JSON.stringify(update)}'>
|
|
385
|
+
Update to v${update.version}
|
|
386
|
+
</button>
|
|
387
|
+
<div class="small text-muted mt-1">Found update!</div>
|
|
388
|
+
`;
|
|
389
|
+
} else {
|
|
390
|
+
window.homebridge.toast.info('No updates available for this device.');
|
|
391
|
+
target.disabled = false;
|
|
392
|
+
target.innerHTML = 'Check Update';
|
|
393
|
+
}
|
|
394
|
+
} catch (err) {
|
|
395
|
+
window.homebridge.toast.error(err.message);
|
|
396
|
+
target.disabled = false;
|
|
397
|
+
target.innerHTML = 'Check Update';
|
|
398
|
+
}
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// 3. Start Update Logic
|
|
403
|
+
if (target.classList.contains('start-update')) {
|
|
404
|
+
activeUpdateData = JSON.parse(target.getAttribute('data-update'));
|
|
405
|
+
activeNodeId = nodeId;
|
|
406
|
+
document.getElementById('confirmMessage').textContent = 'WARNING: Firmware updates carry risk. Do not unplug the bridge or device during the process. Proceed?';
|
|
407
|
+
showModal('confirmModal');
|
|
408
|
+
}
|
|
338
409
|
});
|
|
339
410
|
|
|
340
|
-
//
|
|
341
|
-
|
|
342
|
-
|
|
411
|
+
// Modal Button Handlers
|
|
412
|
+
document.getElementById('cancelRename').addEventListener('click', hideModals);
|
|
413
|
+
document.getElementById('confirmRename').addEventListener('click', async () => {
|
|
414
|
+
const newName = document.getElementById('newLabelInput').value.trim();
|
|
415
|
+
const nodeId = activeNodeId;
|
|
416
|
+
hideModals();
|
|
417
|
+
|
|
418
|
+
if (newName !== '') {
|
|
419
|
+
try {
|
|
420
|
+
await window.homebridge.request('rename-node', { nodeId, name: newName });
|
|
421
|
+
window.homebridge.toast.success(`Node ${nodeId} renamed to ${newName}`);
|
|
422
|
+
loadNodes();
|
|
423
|
+
} catch (err) {
|
|
424
|
+
window.homebridge.toast.error(err.message);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
343
427
|
});
|
|
344
428
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
429
|
+
document.getElementById('cancelConfirm').addEventListener('click', hideModals);
|
|
430
|
+
document.getElementById('executeConfirm').addEventListener('click', async () => {
|
|
431
|
+
const nodeId = activeNodeId;
|
|
432
|
+
const updateData = activeUpdateData;
|
|
433
|
+
hideModals();
|
|
434
|
+
|
|
435
|
+
try {
|
|
436
|
+
await window.homebridge.request('start-update', { nodeId, update: updateData });
|
|
437
|
+
window.homebridge.toast.success('Firmware update started!');
|
|
438
|
+
loadNodes();
|
|
439
|
+
} catch (err) {
|
|
440
|
+
window.homebridge.toast.error(err.message);
|
|
349
441
|
loadNodes();
|
|
350
442
|
}
|
|
351
|
-
}
|
|
443
|
+
});
|
|
352
444
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
445
|
+
const init = async () => {
|
|
446
|
+
if (!window.homebridge) return;
|
|
447
|
+
|
|
448
|
+
// Load Config
|
|
449
|
+
const pluginConfig = await window.homebridge.getPluginConfig();
|
|
450
|
+
currentConfig = pluginConfig[0] || { platform: 'ZWaveUSB' };
|
|
451
|
+
const config = currentConfig;
|
|
452
|
+
|
|
453
|
+
// Populate Form
|
|
454
|
+
document.getElementById('name').value = config.name || 'Homebridge Z-Wave USB';
|
|
455
|
+
document.getElementById('serialPort').value = config.serialPort || '';
|
|
456
|
+
|
|
457
|
+
// Keys
|
|
458
|
+
const keys = config.securityKeys || {};
|
|
459
|
+
document.getElementById('S0_Legacy').value = keys.S0_Legacy || '';
|
|
460
|
+
document.getElementById('S2_Unauthenticated').value = keys.S2_Unauthenticated || '';
|
|
461
|
+
document.getElementById('S2_Authenticated').value = keys.S2_Authenticated || '';
|
|
462
|
+
document.getElementById('S2_AccessControl').value = keys.S2_AccessControl || '';
|
|
463
|
+
document.getElementById('S2_Authenticated_LR').value = keys.S2_Authenticated_LR || '';
|
|
464
|
+
document.getElementById('S2_AccessControl_LR').value = keys.S2_AccessControl_LR || '';
|
|
465
|
+
|
|
466
|
+
// Advanced
|
|
467
|
+
document.getElementById('inclusionTimeoutSeconds').value =
|
|
468
|
+
config.inclusionTimeoutSeconds || 60;
|
|
469
|
+
document.getElementById('debug').checked = config.debug || false;
|
|
470
|
+
|
|
471
|
+
// Auto-save on change
|
|
472
|
+
document.getElementById('configForm').addEventListener('input', updateConfig);
|
|
473
|
+
document.getElementById('configForm').addEventListener('change', updateConfig);
|
|
474
|
+
|
|
475
|
+
// Generate Keys
|
|
476
|
+
document.getElementById('generateKeys').addEventListener('click', () => {
|
|
477
|
+
if (document.getElementById('S0_Legacy').value && !confirm('Overwrite existing keys?'))
|
|
478
|
+
return;
|
|
479
|
+
|
|
480
|
+
const gen = () => {
|
|
481
|
+
const arr = new Uint8Array(16);
|
|
482
|
+
window.crypto.getRandomValues(arr);
|
|
483
|
+
return Array.from(arr, (b) => b.toString(16).padStart(2, '0'))
|
|
484
|
+
.join('')
|
|
485
|
+
.toUpperCase();
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
document.getElementById('S0_Legacy').value = gen();
|
|
489
|
+
document.getElementById('S2_Unauthenticated').value = gen();
|
|
490
|
+
document.getElementById('S2_Authenticated').value = gen();
|
|
491
|
+
document.getElementById('S2_AccessControl').value = gen();
|
|
492
|
+
document.getElementById('S2_Authenticated_LR').value = gen();
|
|
493
|
+
document.getElementById('S2_AccessControl_LR').value = gen();
|
|
494
|
+
|
|
495
|
+
updateConfig();
|
|
496
|
+
window.homebridge.toast.success('Keys generated and configuration updated!');
|
|
497
|
+
});
|
|
362
498
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
const targetPane = document.getElementById(targetId);
|
|
368
|
-
if (targetPane) {
|
|
369
|
-
targetPane.classList.add('show', 'active');
|
|
370
|
-
}
|
|
499
|
+
// Save event listener as a backup
|
|
500
|
+
window.homebridge.addEventListener('submitting', async () => {
|
|
501
|
+
await updateConfig();
|
|
502
|
+
});
|
|
371
503
|
|
|
372
|
-
|
|
504
|
+
// Initialize nodes
|
|
505
|
+
loadNodes();
|
|
506
|
+
setInterval(() => {
|
|
507
|
+
if (document.getElementById('maintenance').classList.contains('active')) {
|
|
373
508
|
loadNodes();
|
|
374
509
|
}
|
|
510
|
+
}, 5000);
|
|
511
|
+
|
|
512
|
+
// Manual tab switching logic for environments without full Bootstrap JS
|
|
513
|
+
document.querySelectorAll('#mainTabs .nav-link').forEach((tab) => {
|
|
514
|
+
tab.addEventListener('click', (e) => {
|
|
515
|
+
e.preventDefault();
|
|
516
|
+
const targetId = tab.getAttribute('href').replace('#', '');
|
|
517
|
+
|
|
518
|
+
// Update Tab Active State
|
|
519
|
+
document.querySelectorAll('#mainTabs .nav-link').forEach((t) => t.classList.remove('active'));
|
|
520
|
+
tab.classList.add('active');
|
|
521
|
+
|
|
522
|
+
// Update Content Active State
|
|
523
|
+
document.querySelectorAll('.tab-pane').forEach((pane) => {
|
|
524
|
+
pane.classList.remove('show', 'active');
|
|
525
|
+
});
|
|
526
|
+
const targetPane = document.getElementById(targetId);
|
|
527
|
+
if (targetPane) {
|
|
528
|
+
targetPane.classList.add('show', 'active');
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (targetId === 'maintenance') {
|
|
532
|
+
loadNodes();
|
|
533
|
+
}
|
|
534
|
+
});
|
|
375
535
|
});
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
</
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
// Try to initialize
|
|
539
|
+
if (window.homebridge && window.homebridge.getPluginConfig) {
|
|
540
|
+
init();
|
|
541
|
+
} else if (window.homebridge) {
|
|
542
|
+
window.homebridge.addEventListener('ready', init);
|
|
543
|
+
} else {
|
|
544
|
+
window.addEventListener('homebridgeReady', init);
|
|
545
|
+
}
|
|
546
|
+
})();
|
|
547
|
+
</script>
|
|
548
|
+
</body>
|
|
549
|
+
</html>
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "homebridge-zwave-usb",
|
|
3
3
|
"displayName": "Homebridge Z-Wave USB",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.3.1",
|
|
5
5
|
"description": "A Homebridge dynamic platform plugin for Z-Wave USB controllers using zwave-js.",
|
|
6
6
|
"license": "Polyform-Noncommercial-1.0.0",
|
|
7
7
|
"funding": {
|