@homebridge-plugins/homebridge-meross 10.16.5 → 10.17.0-beta.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/CHANGELOG.md +7 -0
- package/README.md +2 -2
- package/branding/Homebridge_x_Meross.png +0 -0
- package/config.schema.json +1 -1
- package/lib/homebridge-ui/public/index.html +356 -190
- package/lib/platform.js +4 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@homebridge-plugins/homebridge-meross` will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## v10.17.0 (Pending Release)
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- feat(ui): add, remove and hide devices from the config via the my devices tab
|
|
10
|
+
- style(ui): standardise the custom ui layout and sync the support tab with the readme
|
|
11
|
+
|
|
5
12
|
## v10.16.5 (2026-07-18)
|
|
6
13
|
|
|
7
14
|
### Changed
|
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<a href="https://github.com/homebridge-plugins/homebridge-meross"><img alt="Homebridge Verified" src="https://
|
|
2
|
+
<a href="https://github.com/homebridge-plugins/homebridge-meross"><img alt="Homebridge Verified" src="https://raw.githubusercontent.com/homebridge-plugins/homebridge-meross/latest/branding/Homebridge_x_Meross.png" width="600px"></a>
|
|
3
3
|
</p>
|
|
4
4
|
<span align="center">
|
|
5
5
|
|
|
@@ -35,7 +35,7 @@ Homebridge plugin to integrate Meross devices into HomeKit
|
|
|
35
35
|
|
|
36
36
|
- [Installation](https://github.com/homebridge-plugins/homebridge-meross/wiki/Installation)
|
|
37
37
|
- [Configuration](https://github.com/homebridge-plugins/homebridge-meross/wiki/Configuration)
|
|
38
|
-
- [Beta Version](https://github.com/homebridge/homebridge/wiki/
|
|
38
|
+
- [Beta Version](https://github.com/homebridge-plugins/homebridge-meross/wiki/Beta-Version)
|
|
39
39
|
- [Node Version](https://github.com/homebridge-plugins/homebridge-meross/wiki/Node-Version)
|
|
40
40
|
|
|
41
41
|
### Features
|
|
Binary file
|
package/config.schema.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"strictValidation": true,
|
|
6
6
|
"customUi": true,
|
|
7
7
|
"customUiPath": "./lib/homebridge-ui",
|
|
8
|
-
"headerDisplay": "<p align=\"center\"><img width=\"
|
|
8
|
+
"headerDisplay": "<p align=\"center\"><img width=\"250px\" src=\"https://raw.githubusercontent.com/homebridge-plugins/homebridge-meross/latest/branding/Homebridge_x_Meross.png\"></p><p align=\"center\">For help and support please visit our <a href=\"https://github.com/homebridge-plugins/homebridge-meross/wiki\">GitHub Wiki</a>. We hope you find this plugin useful!</p>",
|
|
9
9
|
"schema": {
|
|
10
10
|
"name": {
|
|
11
11
|
"type": "string",
|
|
@@ -17,6 +17,22 @@
|
|
|
17
17
|
color: #eeeeee !important;
|
|
18
18
|
font-weight: 500 !important;
|
|
19
19
|
}
|
|
20
|
+
|
|
21
|
+
.list-group-item {
|
|
22
|
+
background-color: #333333;
|
|
23
|
+
border-color: #444444;
|
|
24
|
+
color: #eeeeee;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.text-muted {
|
|
28
|
+
color: #aaaaaa !important;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
.min-w-50 {
|
|
32
|
+
min-width: 50px;
|
|
33
|
+
}
|
|
34
|
+
.action-desc {
|
|
35
|
+
font-size: 0.85rem;
|
|
20
36
|
}
|
|
21
37
|
select {
|
|
22
38
|
background-image:
|
|
@@ -70,11 +86,46 @@
|
|
|
70
86
|
<div id="pageDevices" class="mt-4" style="display: none;">
|
|
71
87
|
<div id="deviceInfo">
|
|
72
88
|
<form>
|
|
73
|
-
<div class="form-group">
|
|
89
|
+
<div class="form-group mb-0">
|
|
74
90
|
<select class="form-control" id="deviceSelect"></select>
|
|
75
91
|
</div>
|
|
76
92
|
</form>
|
|
77
|
-
<
|
|
93
|
+
<ul class="list-group mt-4 mb-4 mx-0" id="configActionList" style="display: none;">
|
|
94
|
+
<li class="list-group-item">
|
|
95
|
+
<div class="d-flex justify-content-between align-items-center">
|
|
96
|
+
<div class="pr-2">
|
|
97
|
+
<div>Add All Devices</div>
|
|
98
|
+
<div class="text-muted action-desc">Add all devices to the config that are not already in the config.</div>
|
|
99
|
+
</div>
|
|
100
|
+
<button type="button" class="btn btn-primary m-0 ml-3 py-1 min-w-50" id="addAllBtn" aria-label="Add all devices to the config">
|
|
101
|
+
<i aria-hidden="true" class="fas fa-arrow-right"></i>
|
|
102
|
+
</button>
|
|
103
|
+
</div>
|
|
104
|
+
</li>
|
|
105
|
+
<li class="list-group-item">
|
|
106
|
+
<div class="d-flex justify-content-between align-items-center">
|
|
107
|
+
<div class="pr-2">
|
|
108
|
+
<div id="deviceConfigTitle">Add This Device</div>
|
|
109
|
+
<div class="text-muted action-desc" id="deviceConfigText">Add this device to the plugin config.</div>
|
|
110
|
+
</div>
|
|
111
|
+
<button type="button" class="btn btn-primary m-0 ml-3 py-1 min-w-50" id="deviceConfigBtn" aria-label="Add this device to the config">
|
|
112
|
+
<i aria-hidden="true" class="fas fa-arrow-right"></i>
|
|
113
|
+
</button>
|
|
114
|
+
</div>
|
|
115
|
+
</li>
|
|
116
|
+
<li class="list-group-item">
|
|
117
|
+
<div class="d-flex justify-content-between align-items-center">
|
|
118
|
+
<div class="pr-2">
|
|
119
|
+
<div id="hideDeviceTitle">Hide This Device</div>
|
|
120
|
+
<div class="text-muted action-desc" id="hideDeviceText">Hide this device from HomeKit. This adds the device to the config if needed.</div>
|
|
121
|
+
</div>
|
|
122
|
+
<button type="button" class="btn btn-primary m-0 ml-3 py-1 min-w-50" id="hideDeviceBtn" aria-label="Hide this device from HomeKit">
|
|
123
|
+
<i aria-hidden="true" class="fas fa-arrow-right"></i>
|
|
124
|
+
</button>
|
|
125
|
+
</div>
|
|
126
|
+
</li>
|
|
127
|
+
</ul>
|
|
128
|
+
<table class="table w-100 mt-0" id="deviceTable" style="display: none;">
|
|
78
129
|
<thead>
|
|
79
130
|
<tr class="table-active">
|
|
80
131
|
<th scope="col" style="width: 40%;">Device Name</th>
|
|
@@ -114,134 +165,313 @@
|
|
|
114
165
|
<th scope="row">Firmware Version</th>
|
|
115
166
|
<td id="firmware"></td>
|
|
116
167
|
</tr>
|
|
117
|
-
<tr>
|
|
118
|
-
<th scope="row">Hide from HomeKit</th>
|
|
119
|
-
<td>
|
|
120
|
-
<div class="custom-control custom-checkbox">
|
|
121
|
-
<input type="checkbox" class="custom-control-input" id="hideDevice">
|
|
122
|
-
<label class="custom-control-label" for="hideDevice">
|
|
123
|
-
Hide this device from HomeKit
|
|
124
|
-
</label>
|
|
125
|
-
</div>
|
|
126
|
-
</td>
|
|
127
|
-
</tr>
|
|
128
168
|
</tbody>
|
|
129
169
|
</table>
|
|
130
170
|
</div>
|
|
131
171
|
</div>
|
|
132
|
-
<div id="pageSupport" class="mt-4" style="display: none
|
|
172
|
+
<div id="pageSupport" class="mt-4" style="display: none">
|
|
133
173
|
<p class="text-center lead">Thank you for using <strong>homebridge-meross</strong></p>
|
|
134
|
-
<
|
|
135
|
-
|
|
136
|
-
<
|
|
137
|
-
<li>
|
|
138
|
-
<a href="https://github.com/homebridge-plugins/homebridge-meross/wiki/Installation" target="_blank"
|
|
139
|
-
>Installation</a
|
|
140
|
-
>
|
|
141
|
-
</li>
|
|
142
|
-
<li>
|
|
143
|
-
<a href="https://github.com/homebridge-plugins/homebridge-meross/wiki/Configuration" target="_blank"
|
|
144
|
-
>Configuration</a
|
|
145
|
-
>
|
|
146
|
-
</li>
|
|
147
|
-
<li>
|
|
148
|
-
<a href="https://github.com/homebridge/homebridge/wiki/How-to-Install-Alternate-Plugin-Versions" target="_blank"
|
|
149
|
-
>Beta Version</a
|
|
150
|
-
>
|
|
151
|
-
</li>
|
|
152
|
-
<li>
|
|
153
|
-
<a href="https://github.com/homebridge-plugins/homebridge-meross/wiki/Node-Version" target="_blank"
|
|
154
|
-
>Node Version</a
|
|
155
|
-
>
|
|
156
|
-
</li>
|
|
157
|
-
</ul>
|
|
158
|
-
<h4>Features</h4>
|
|
174
|
+
<h5>Plugin Information</h5>
|
|
175
|
+
<ul>
|
|
176
|
+
<li>This plugin allows you to view and control your Meross devices within HomeKit. The plugin:</li>
|
|
159
177
|
<ul>
|
|
160
|
-
<li>
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
</li>
|
|
165
|
-
<li>
|
|
166
|
-
<a href="https://github.com/homebridge-plugins/homebridge-meross/wiki/Cloud-Control" target="_blank"
|
|
167
|
-
>Cloud Control</a
|
|
168
|
-
>
|
|
169
|
-
</li>
|
|
170
|
-
<li>
|
|
171
|
-
<a href="https://github.com/homebridge-plugins/homebridge-meross/wiki/Local-Control" target="_blank"
|
|
172
|
-
>Local Control</a
|
|
173
|
-
>
|
|
174
|
-
</li>
|
|
178
|
+
<li>downloads a device list if your Meross credentials are supplied</li>
|
|
179
|
+
<li>attempts to control your devices locally, reverting to cloud control if necessary</li>
|
|
180
|
+
<li>listens for real-time device updates when controlled externally</li>
|
|
181
|
+
<li>supports configuring devices for local-only control without your Meross credentials</li>
|
|
182
|
+
<li>can ignore any HomeKit-native devices you have using the configuration</li>
|
|
175
183
|
</ul>
|
|
176
|
-
|
|
184
|
+
</ul>
|
|
185
|
+
<h5>Prerequisites</h5>
|
|
186
|
+
<ul>
|
|
187
|
+
<li>To use this plugin, you will need to already have:</li>
|
|
177
188
|
<ul>
|
|
178
|
-
<li>
|
|
179
|
-
|
|
180
|
-
>Common Errors</a
|
|
181
|
-
>
|
|
182
|
-
</li>
|
|
183
|
-
<li>
|
|
184
|
-
<a href="https://github.com/homebridge-plugins/homebridge-meross/issues/new/choose" target="_blank"
|
|
185
|
-
>Support Request</a
|
|
186
|
-
>
|
|
187
|
-
</li>
|
|
188
|
-
<li>
|
|
189
|
-
<a href="https://github.com/homebridge-plugins/homebridge-meross/blob/latest/CHANGELOG.md" target="_blank"
|
|
190
|
-
>Changelog</a
|
|
191
|
-
>
|
|
192
|
-
</li>
|
|
193
|
-
<li>
|
|
194
|
-
<a href="https://github.com/sponsors/bwp91" target="_blank">About Me</a>
|
|
195
|
-
</li>
|
|
196
|
-
</ul>
|
|
197
|
-
<h4>Credits</h4>
|
|
198
|
-
<ul>
|
|
199
|
-
<li>
|
|
200
|
-
This is a fork of the work originally done by
|
|
201
|
-
<a href="https://github.com/Robdel12" target="_blank">@Robdel12</a> and
|
|
202
|
-
<a href="https://github.com/dylanfrankcom" target="_blank">@dylanfrankcom</a>.
|
|
203
|
-
</li>
|
|
204
|
-
<li>
|
|
205
|
-
To <a href="https://github.com/donavanbecker" target="_blank">@donavanbecker</a> the previous
|
|
206
|
-
maintainer of this plugin.
|
|
207
|
-
</li>
|
|
208
|
-
<li>
|
|
209
|
-
To <a href="https://github.com/Apollon77" target="_blank">@Apollon77</a> and
|
|
210
|
-
<a href="https://github.com/colthreepv" target="_blank">@colthreepv</a> for the
|
|
211
|
-
<a href="https://github.com/Apollon77/meross-cloud" target="_blank">meross-cloud</a> library
|
|
212
|
-
(contained in this plugin).
|
|
213
|
-
</li>
|
|
214
|
-
<li>
|
|
215
|
-
To the creator of the awesome plugin header logo:
|
|
216
|
-
<a href="https://www.instagram.com/keryan.me" target="_blank">Keryan Belahcene</a>.
|
|
217
|
-
</li>
|
|
218
|
-
<li>
|
|
219
|
-
To the creators/contributors of
|
|
220
|
-
<a href="https://github.com/simont77/fakegato-history" target="_blank">Fakegato</a>:
|
|
221
|
-
<a href="https://github.com/simont77" target="_blank">@simont77</a> and
|
|
222
|
-
<a href="https://github.com/NorthernMan54" target="_blank">@NorthernMan54</a>.
|
|
223
|
-
</li>
|
|
224
|
-
<li>
|
|
225
|
-
To the creators/contributors of
|
|
226
|
-
<a href="https://homebridge.io" target="_blank">Homebridge</a> who make this plugin possible.
|
|
227
|
-
</li>
|
|
228
|
-
</ul>
|
|
229
|
-
|
|
230
|
-
<h4>Disclaimer</h4>
|
|
231
|
-
<ul>
|
|
232
|
-
<li>
|
|
233
|
-
I am in no way affiliated with Meross and this plugin is a personal project that I maintain in
|
|
234
|
-
my free time.
|
|
235
|
-
</li>
|
|
236
|
-
<li>
|
|
237
|
-
Use this plugin entirely at your own risk - please see licence for more information.
|
|
238
|
-
</li>
|
|
189
|
+
<li><a href="https://nodejs.org" target="_blank">Node</a>: latest version of <code>v22</code> or <code>v24</code> - any other major version is not supported.</li>
|
|
190
|
+
<li><a href="https://homebridge.io" target="_blank">Homebridge</a>: <code>v1.6</code> or above - refer to link for more information and installation instructions.</li>
|
|
239
191
|
</ul>
|
|
192
|
+
</ul>
|
|
193
|
+
<h5>Setup</h5>
|
|
194
|
+
<ul>
|
|
195
|
+
<li><a href="https://github.com/homebridge-plugins/homebridge-meross/wiki/Installation" target="_blank">Installation</a></li>
|
|
196
|
+
<li><a href="https://github.com/homebridge-plugins/homebridge-meross/wiki/Configuration" target="_blank">Configuration</a></li>
|
|
197
|
+
<li><a href="https://github.com/homebridge-plugins/homebridge-meross/wiki/Beta-Version" target="_blank">Beta Version</a></li>
|
|
198
|
+
<li><a href="https://github.com/homebridge-plugins/homebridge-meross/wiki/Node-Version" target="_blank">Node Version</a></li>
|
|
199
|
+
</ul>
|
|
200
|
+
<h5>Features</h5>
|
|
201
|
+
<ul>
|
|
202
|
+
<li><a href="https://github.com/homebridge-plugins/homebridge-meross/wiki/Supported-Devices" target="_blank">Supported Devices</a></li>
|
|
203
|
+
<li><a href="https://github.com/homebridge-plugins/homebridge-meross/wiki/Cloud-Control" target="_blank">Cloud Control</a></li>
|
|
204
|
+
<li><a href="https://github.com/homebridge-plugins/homebridge-meross/wiki/Local-Control" target="_blank">Local Control</a></li>
|
|
205
|
+
</ul>
|
|
206
|
+
<h5>Help/About</h5>
|
|
207
|
+
<ul>
|
|
208
|
+
<li><a href="https://github.com/homebridge-plugins/homebridge-meross/wiki/Common-Errors" target="_blank">Common Errors</a></li>
|
|
209
|
+
<li><a href="https://github.com/homebridge-plugins/homebridge-meross/issues/new/choose" target="_blank">Support Request</a></li>
|
|
210
|
+
<li><a href="https://github.com/homebridge-plugins/homebridge-meross/blob/latest/CHANGELOG.md" target="_blank">Changelog</a></li>
|
|
211
|
+
<li><a href="https://github.com/sponsors/bwp91" target="_blank">About Me</a></li>
|
|
212
|
+
</ul>
|
|
213
|
+
<h5>Credits</h5>
|
|
214
|
+
<ul>
|
|
215
|
+
<li>This is a fork of the work originally done by <a href="https://github.com/Robdel12" target="_blank">@Robdel12</a> and <a href="https://github.com/dylanfrankcom" target="_blank">@dylanfrankcom</a>.</li>
|
|
216
|
+
<li>To <a href="https://github.com/donavanbecker" target="_blank">@donavanbecker</a> the previous maintainer of this plugin.</li>
|
|
217
|
+
<li>To <a href="https://github.com/Apollon77" target="_blank">@Apollon77</a> and <a href="https://github.com/colthreepv" target="_blank">@colthreepv</a> for the <a href="https://github.com/Apollon77/meross-cloud" target="_blank">meross-cloud</a> library (contained in this plugin).</li>
|
|
218
|
+
<li>To the creator of the awesome plugin header logo: <a href="https://www.instagram.com/keryan.me" target="_blank">Keryan Belahcene</a>.</li>
|
|
219
|
+
<li>To the creators/contributors of <a href="https://github.com/simont77/fakegato-history" target="_blank">Fakegato</a>: <a href="https://github.com/simont77" target="_blank">@simont77</a> and <a href="https://github.com/NorthernMan54" target="_blank">@NorthernMan54</a>.</li>
|
|
220
|
+
<li>To the creators/contributors of <a href="https://homebridge.io" target="_blank">Homebridge</a> who make this plugin possible.</li>
|
|
221
|
+
</ul>
|
|
222
|
+
<h5>Disclaimer</h5>
|
|
223
|
+
<ul>
|
|
224
|
+
<li>I am in no way affiliated with Meross and this plugin is a personal project that I maintain in my free time.</li>
|
|
225
|
+
<li>Use this plugin entirely at your own risk - please see licence for more information.</li>
|
|
226
|
+
</ul>
|
|
240
227
|
</div>
|
|
241
228
|
<script>
|
|
242
229
|
;(async () => {
|
|
243
230
|
try {
|
|
244
231
|
const currentConfig = await homebridge.getPluginConfig()
|
|
232
|
+
|
|
233
|
+
// ===== Device picker (add/remove/hide config entries from the My Devices tab) =====
|
|
234
|
+
// The control state is always computed from a fresh getPluginConfig() call,
|
|
235
|
+
// so staged-but-unsaved edits made in the Settings tab are respected
|
|
236
|
+
const deviceArrays = [
|
|
237
|
+
'singleDevices',
|
|
238
|
+
'multiDevices',
|
|
239
|
+
'lightDevices',
|
|
240
|
+
'fanDevices',
|
|
241
|
+
'diffuserDevices',
|
|
242
|
+
'purifierDevices',
|
|
243
|
+
'humidifierDevices',
|
|
244
|
+
'thermostatDevices',
|
|
245
|
+
'garageDevices',
|
|
246
|
+
'rollerDevices',
|
|
247
|
+
'babyDevices',
|
|
248
|
+
'sensorDevices',
|
|
249
|
+
]
|
|
250
|
+
let pickerAccessories = []
|
|
251
|
+
let removeConfirmTimer
|
|
252
|
+
|
|
253
|
+
// The action buttons always show the settings-page arrow; the row text
|
|
254
|
+
// (and the button's aria-label) says what the arrow will do
|
|
255
|
+
const actionArrow = '<i aria-hidden="true" class="fas fa-arrow-right"></i>'
|
|
256
|
+
|
|
257
|
+
// Compare serial numbers ignoring case and separators, so an entry typed
|
|
258
|
+
// by hand in a different format still counts as the same device
|
|
259
|
+
const looseId = id => (id || '').toString().toLowerCase().replace(/[^a-z0-9]+/g, '')
|
|
260
|
+
|
|
261
|
+
const getConfigBlocks = async () => {
|
|
262
|
+
const blocks = await homebridge.getPluginConfig()
|
|
263
|
+
if (!blocks.length) {
|
|
264
|
+
blocks.push({ name: 'Meross' })
|
|
265
|
+
}
|
|
266
|
+
return blocks
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const findInConfig = (config, serialNumber) => {
|
|
270
|
+
const target = looseId(serialNumber)
|
|
271
|
+
for (const key of deviceArrays) {
|
|
272
|
+
if (Array.isArray(config[key])) {
|
|
273
|
+
const index = config[key].findIndex(entry => entry && looseId(entry.serialNumber) === target)
|
|
274
|
+
if (index !== -1) {
|
|
275
|
+
return { key, index }
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return null
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const selectedAccessory = () => {
|
|
283
|
+
const uuid = document.getElementById('deviceSelect').value
|
|
284
|
+
return pickerAccessories.find(x => x.UUID === uuid)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const setHideControl = hidden => {
|
|
288
|
+
const btn = document.getElementById('hideDeviceBtn')
|
|
289
|
+
btn.dataset.hidden = hidden ? 'yes' : 'no'
|
|
290
|
+
btn.innerHTML = actionArrow
|
|
291
|
+
btn.setAttribute('aria-label', hidden ? 'Show this device in HomeKit' : 'Hide this device from HomeKit')
|
|
292
|
+
document.getElementById('hideDeviceTitle').innerHTML = hidden ? 'Show This Device' : 'Hide This Device'
|
|
293
|
+
document.getElementById('hideDeviceText').innerHTML = hidden
|
|
294
|
+
? 'Show this device in HomeKit again. The config entry is kept.'
|
|
295
|
+
: 'Hide this device from HomeKit. This adds the device to the config if needed.'
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const setConfigButton = inConfig => {
|
|
299
|
+
const btn = document.getElementById('deviceConfigBtn')
|
|
300
|
+
clearTimeout(removeConfirmTimer)
|
|
301
|
+
delete btn.dataset.confirm
|
|
302
|
+
btn.dataset.state = inConfig ? 'remove' : 'add'
|
|
303
|
+
btn.classList.remove('btn-danger')
|
|
304
|
+
btn.classList.add('btn-primary')
|
|
305
|
+
btn.innerHTML = actionArrow
|
|
306
|
+
btn.setAttribute('aria-label', inConfig ? 'Remove this device from the config' : 'Add this device to the config')
|
|
307
|
+
document.getElementById('deviceConfigTitle').innerHTML = inConfig ? 'Remove This Device' : 'Add This Device'
|
|
308
|
+
document.getElementById('deviceConfigText').innerHTML = inConfig
|
|
309
|
+
? 'Remove this device from the plugin config.'
|
|
310
|
+
: 'Add this device to the plugin config.'
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Which config array a model belongs in comes from the plugin's own
|
|
314
|
+
// model lists via the UI server; a model in none of them cannot be added
|
|
315
|
+
const addDeviceToConfig = async (config, acc) => {
|
|
316
|
+
const arrayKey = await homebridge.request('/getDeviceConfigKey', { model: acc.context.model })
|
|
317
|
+
if (!arrayKey) {
|
|
318
|
+
return false
|
|
319
|
+
}
|
|
320
|
+
if (!Array.isArray(config[arrayKey])) {
|
|
321
|
+
config[arrayKey] = []
|
|
322
|
+
}
|
|
323
|
+
config[arrayKey].push({ name: acc.displayName, serialNumber: looseId(acc.context.serialNumber) })
|
|
324
|
+
return true
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
document.getElementById('deviceConfigBtn').addEventListener('click', async () => {
|
|
328
|
+
try {
|
|
329
|
+
const btn = document.getElementById('deviceConfigBtn')
|
|
330
|
+
const acc = selectedAccessory()
|
|
331
|
+
if (!acc) {
|
|
332
|
+
return
|
|
333
|
+
}
|
|
334
|
+
const blocks = await getConfigBlocks()
|
|
335
|
+
const config = blocks[0]
|
|
336
|
+
const found = findInConfig(config, acc.context.serialNumber)
|
|
337
|
+
if (btn.dataset.state === 'add') {
|
|
338
|
+
// The config may have changed since the button was drawn, so
|
|
339
|
+
// re-check and never add a duplicate - just correct the button
|
|
340
|
+
if (found) {
|
|
341
|
+
setConfigButton(true)
|
|
342
|
+
setHideControl(!!config[found.key][found.index].ignoreDevice)
|
|
343
|
+
homebridge.toast.info(acc.displayName + ' is already in the config.', 'Not Added')
|
|
344
|
+
return
|
|
345
|
+
}
|
|
346
|
+
if (!(await addDeviceToConfig(config, acc))) {
|
|
347
|
+
homebridge.toast.error('Unsupported device model: ' + acc.context.model, 'Cannot Add Device')
|
|
348
|
+
return
|
|
349
|
+
}
|
|
350
|
+
await homebridge.updatePluginConfig(blocks)
|
|
351
|
+
setConfigButton(true)
|
|
352
|
+
setHideControl(false)
|
|
353
|
+
homebridge.toast.success(acc.displayName + ' added - press Save to apply.', 'Added To Config')
|
|
354
|
+
} else {
|
|
355
|
+
if (!found) {
|
|
356
|
+
setConfigButton(false)
|
|
357
|
+
setHideControl(false)
|
|
358
|
+
homebridge.toast.info(acc.displayName + ' is not in the config.', 'Not Removed')
|
|
359
|
+
return
|
|
360
|
+
}
|
|
361
|
+
const entry = config[found.key][found.index]
|
|
362
|
+
const extraKeys = Object.keys(entry).filter(k => !['serialNumber', 'name'].includes(k))
|
|
363
|
+
if (extraKeys.length > 0 && btn.dataset.confirm !== 'yes') {
|
|
364
|
+
// The entry has settings beyond serial and name which would be
|
|
365
|
+
// lost, so ask for a second click within five seconds
|
|
366
|
+
btn.dataset.confirm = 'yes'
|
|
367
|
+
btn.classList.remove('btn-primary')
|
|
368
|
+
btn.classList.add('btn-danger')
|
|
369
|
+
btn.innerHTML = actionArrow
|
|
370
|
+
btn.setAttribute('aria-label', 'Confirm removing this device from the config')
|
|
371
|
+
document.getElementById('deviceConfigText').innerHTML = 'This entry has extra settings which will be lost. Click again to remove it.'
|
|
372
|
+
removeConfirmTimer = setTimeout(() => setConfigButton(true), 5000)
|
|
373
|
+
return
|
|
374
|
+
}
|
|
375
|
+
config[found.key].splice(found.index, 1)
|
|
376
|
+
if (config[found.key].length === 0) {
|
|
377
|
+
delete config[found.key]
|
|
378
|
+
}
|
|
379
|
+
await homebridge.updatePluginConfig(blocks)
|
|
380
|
+
setConfigButton(false)
|
|
381
|
+
setHideControl(false)
|
|
382
|
+
homebridge.toast.success(acc.displayName + ' removed - press Save to apply.', 'Removed From Config')
|
|
383
|
+
}
|
|
384
|
+
} catch (err) {
|
|
385
|
+
homebridge.toast.error(err.message, 'Error')
|
|
386
|
+
}
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
document.getElementById('hideDeviceBtn').addEventListener('click', async () => {
|
|
390
|
+
try {
|
|
391
|
+
const acc = selectedAccessory()
|
|
392
|
+
if (!acc) {
|
|
393
|
+
return
|
|
394
|
+
}
|
|
395
|
+
const blocks = await getConfigBlocks()
|
|
396
|
+
const config = blocks[0]
|
|
397
|
+
let found = findInConfig(config, acc.context.serialNumber)
|
|
398
|
+
const currentlyHidden = document.getElementById('hideDeviceBtn').dataset.hidden === 'yes'
|
|
399
|
+
if (!currentlyHidden) {
|
|
400
|
+
// Hiding needs a config entry to carry the flag, so create one if
|
|
401
|
+
// the device is not in the config yet
|
|
402
|
+
if (!found) {
|
|
403
|
+
if (!(await addDeviceToConfig(config, acc))) {
|
|
404
|
+
homebridge.toast.error('Unsupported device model: ' + acc.context.model, 'Cannot Hide Device')
|
|
405
|
+
return
|
|
406
|
+
}
|
|
407
|
+
found = findInConfig(config, acc.context.serialNumber)
|
|
408
|
+
}
|
|
409
|
+
config[found.key][found.index].ignoreDevice = true
|
|
410
|
+
await homebridge.updatePluginConfig(blocks)
|
|
411
|
+
setConfigButton(true)
|
|
412
|
+
setHideControl(true)
|
|
413
|
+
homebridge.toast.success(acc.displayName + ' will be hidden from HomeKit - press Save to apply.', 'Device Hidden')
|
|
414
|
+
} else {
|
|
415
|
+
if (found) {
|
|
416
|
+
// Drop the flag rather than storing ignoreDevice false, to keep
|
|
417
|
+
// the entry clean
|
|
418
|
+
delete config[found.key][found.index].ignoreDevice
|
|
419
|
+
await homebridge.updatePluginConfig(blocks)
|
|
420
|
+
}
|
|
421
|
+
setConfigButton(!!found)
|
|
422
|
+
setHideControl(false)
|
|
423
|
+
homebridge.toast.success(acc.displayName + ' will be shown in HomeKit - press Save to apply.', 'Device Unhidden')
|
|
424
|
+
}
|
|
425
|
+
} catch (err) {
|
|
426
|
+
homebridge.toast.error(err.message, 'Error')
|
|
427
|
+
}
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
document.getElementById('addAllBtn').addEventListener('click', async () => {
|
|
431
|
+
try {
|
|
432
|
+
const blocks = await getConfigBlocks()
|
|
433
|
+
const config = blocks[0]
|
|
434
|
+
let added = 0
|
|
435
|
+
let skipped = 0
|
|
436
|
+
let unsupported = 0
|
|
437
|
+
for (const acc of pickerAccessories) {
|
|
438
|
+
if (!acc.context?.serialNumber) {
|
|
439
|
+
continue
|
|
440
|
+
}
|
|
441
|
+
if (findInConfig(config, acc.context.serialNumber)) {
|
|
442
|
+
skipped += 1
|
|
443
|
+
continue
|
|
444
|
+
}
|
|
445
|
+
if (await addDeviceToConfig(config, acc)) {
|
|
446
|
+
added += 1
|
|
447
|
+
} else {
|
|
448
|
+
unsupported += 1
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
if (added > 0) {
|
|
452
|
+
await homebridge.updatePluginConfig(blocks)
|
|
453
|
+
const parts = [added + ' device(s) added', skipped + ' already in the config']
|
|
454
|
+
if (unsupported > 0) {
|
|
455
|
+
parts.push(unsupported + ' unsupported')
|
|
456
|
+
}
|
|
457
|
+
homebridge.toast.success(parts.join(', ') + ' - press Save to apply.', 'Added To Config')
|
|
458
|
+
} else {
|
|
459
|
+
homebridge.toast.info('All devices are already in the config.', 'Nothing To Add')
|
|
460
|
+
}
|
|
461
|
+
const acc = selectedAccessory()
|
|
462
|
+
if (acc) {
|
|
463
|
+
const nowFound = findInConfig(config, acc.context.serialNumber)
|
|
464
|
+
setConfigButton(!!nowFound)
|
|
465
|
+
if (nowFound) {
|
|
466
|
+
setHideControl(!!config[nowFound.key][nowFound.index].ignoreDevice)
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
} catch (err) {
|
|
470
|
+
homebridge.toast.error(err.message, 'Error')
|
|
471
|
+
}
|
|
472
|
+
})
|
|
473
|
+
// ===== End device picker =====
|
|
474
|
+
|
|
245
475
|
showIntro = () => {
|
|
246
476
|
homebridge.disableSaveButton?.()
|
|
247
477
|
const introContinue = document.getElementById('introContinue')
|
|
@@ -256,7 +486,9 @@
|
|
|
256
486
|
}
|
|
257
487
|
showDevices = async () => {
|
|
258
488
|
homebridge.showSpinner()
|
|
259
|
-
|
|
489
|
+
// The save button stays enabled here so entries staged by the picker
|
|
490
|
+
// buttons can be saved without switching back to the Settings tab
|
|
491
|
+
homebridge.enableSaveButton?.()
|
|
260
492
|
homebridge.hideSchemaForm()
|
|
261
493
|
document.getElementById('menuHome').classList.remove('btn-elegant')
|
|
262
494
|
document.getElementById('menuHome').classList.add('btn-primary')
|
|
@@ -279,6 +511,7 @@
|
|
|
279
511
|
: 0
|
|
280
512
|
})
|
|
281
513
|
}
|
|
514
|
+
pickerAccessories = cachedAccessories
|
|
282
515
|
const deviceSelect = document.getElementById('deviceSelect')
|
|
283
516
|
deviceSelect.innerHTML = ''
|
|
284
517
|
cachedAccessories.forEach(a => {
|
|
@@ -306,80 +539,12 @@
|
|
|
306
539
|
document.getElementById('hardware').innerHTML = context.hardware || 'N/A'
|
|
307
540
|
document.getElementById('firmware').innerHTML = context.firmware || 'N/A'
|
|
308
541
|
|
|
309
|
-
// Handle hide device checkbox
|
|
310
|
-
const hideCheckbox = document.getElementById('hideDevice')
|
|
311
|
-
const deviceUUID = context.serialNumber.toLowerCase().replace(/[^a-z0-9]/g, '')
|
|
312
|
-
const deviceArrays = [
|
|
313
|
-
'singleDevices', 'multiDevices', 'lightDevices', 'fanDevices',
|
|
314
|
-
'diffuserDevices', 'purifierDevices', 'humidifierDevices',
|
|
315
|
-
'thermostatDevices', 'garageDevices', 'rollerDevices',
|
|
316
|
-
'babyDevices', 'sensorDevices',
|
|
317
|
-
]
|
|
318
|
-
const findEntry = (cfg) => {
|
|
319
|
-
for (const key of deviceArrays) {
|
|
320
|
-
const arr = cfg[0]?.[key]
|
|
321
|
-
if (!Array.isArray(arr)) continue
|
|
322
|
-
const idx = arr.findIndex(e => e.serialNumber?.toLowerCase().replace(/[^a-z0-9]/g, '') === deviceUUID)
|
|
323
|
-
if (idx !== -1) return { key, idx, entry: arr[idx] }
|
|
324
|
-
}
|
|
325
|
-
return null
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// Check if device is currently hidden
|
|
329
|
-
const config = await homebridge.getPluginConfig()
|
|
330
|
-
const existing = findEntry(config)
|
|
331
|
-
hideCheckbox.checked = !!existing?.entry?.ignoreDevice
|
|
332
|
-
|
|
333
|
-
// Remove previous event listeners by cloning the element
|
|
334
|
-
const newCheckbox = hideCheckbox.cloneNode(true)
|
|
335
|
-
hideCheckbox.parentNode.replaceChild(newCheckbox, hideCheckbox)
|
|
336
|
-
|
|
337
|
-
// Add event listener for checkbox changes
|
|
338
|
-
newCheckbox.addEventListener('change', async (e) => {
|
|
339
|
-
try {
|
|
340
|
-
homebridge.showSpinner()
|
|
341
|
-
const cfg = await homebridge.getPluginConfig()
|
|
342
|
-
const found = findEntry(cfg)
|
|
343
|
-
|
|
344
|
-
if (e.target.checked) {
|
|
345
|
-
if (found) {
|
|
346
|
-
cfg[0][found.key][found.idx].ignoreDevice = true
|
|
347
|
-
} else {
|
|
348
|
-
const arrayKey = await homebridge.request('/getDeviceConfigKey', { model: context.model })
|
|
349
|
-
if (!arrayKey) {
|
|
350
|
-
homebridge.toast.error(`Unsupported device model: ${context.model}`, 'Cannot hide device')
|
|
351
|
-
e.target.checked = false
|
|
352
|
-
homebridge.hideSpinner()
|
|
353
|
-
return
|
|
354
|
-
}
|
|
355
|
-
if (!Array.isArray(cfg[0][arrayKey])) {
|
|
356
|
-
cfg[0][arrayKey] = []
|
|
357
|
-
}
|
|
358
|
-
cfg[0][arrayKey].push({
|
|
359
|
-
name: thisAcc.displayName,
|
|
360
|
-
serialNumber: deviceUUID,
|
|
361
|
-
ignoreDevice: true,
|
|
362
|
-
})
|
|
363
|
-
}
|
|
364
|
-
homebridge.toast.success(`${thisAcc.displayName} will be hidden from HomeKit after restart`, 'Device Hidden')
|
|
365
|
-
} else if (found) {
|
|
366
|
-
cfg[0][found.key][found.idx].ignoreDevice = false
|
|
367
|
-
homebridge.toast.success(`${thisAcc.displayName} will be shown in HomeKit after restart`, 'Device Unhidden')
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
await homebridge.updatePluginConfig(cfg)
|
|
371
|
-
|
|
372
|
-
// Switch to Settings tab to sync the form with updated config
|
|
373
|
-
showSettings()
|
|
374
|
-
homebridge.toast.info('Please click Save to persist your changes', 'Settings Updated')
|
|
375
|
-
homebridge.hideSpinner()
|
|
376
|
-
} catch (err) {
|
|
377
|
-
homebridge.toast.error(err.message, 'Error')
|
|
378
|
-
homebridge.hideSpinner()
|
|
379
|
-
}
|
|
380
|
-
})
|
|
381
|
-
|
|
382
542
|
document.getElementById('deviceTable').style.display = 'inline-table'
|
|
543
|
+
const configBlocks = await getConfigBlocks()
|
|
544
|
+
const configEntry = findInConfig(configBlocks[0], context.serialNumber)
|
|
545
|
+
setConfigButton(!!configEntry)
|
|
546
|
+
setHideControl(!!(configEntry && configBlocks[0][configEntry.key][configEntry.index].ignoreDevice))
|
|
547
|
+
document.getElementById('configActionList').style.display = 'block'
|
|
383
548
|
homebridge.hideSpinner()
|
|
384
549
|
}
|
|
385
550
|
deviceSelect.addEventListener('change', event => showDeviceInfo(event.target.value))
|
|
@@ -390,6 +555,7 @@
|
|
|
390
555
|
option.text = 'No Devices'
|
|
391
556
|
deviceSelect.add(option)
|
|
392
557
|
deviceSelect.disabled = true
|
|
558
|
+
document.getElementById('configActionList').style.display = 'none'
|
|
393
559
|
}
|
|
394
560
|
homebridge.hideSpinner()
|
|
395
561
|
}
|
package/lib/platform.js
CHANGED
|
@@ -137,7 +137,10 @@ export default class {
|
|
|
137
137
|
return
|
|
138
138
|
}
|
|
139
139
|
const entries = Object.entries(x)
|
|
140
|
-
|
|
140
|
+
// An entry with just a serial number carries nothing; two keys
|
|
141
|
+
// (serial number plus name) is kept, as that is what the UI
|
|
142
|
+
// device picker creates for users to build on
|
|
143
|
+
if (entries.length < 2) {
|
|
141
144
|
logRemove(`${key}.${id}`)
|
|
142
145
|
return
|
|
143
146
|
}
|
package/package.json
CHANGED