@homebridge-plugins/homebridge-govee 11.32.2-beta.15 → 11.32.2-beta.16
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 +3 -0
- package/lib/device/appliance-basic.js +10 -0
- package/lib/device/index.js +2 -0
- package/lib/platform.js +36 -475
- package/lib/utils/config-parser.js +280 -0
- package/lib/utils/dispatch.js +159 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -32,6 +32,9 @@ All notable changes to `@homebridge-plugins/homebridge-govee` will be documented
|
|
|
32
32
|
- refactor: build the switch and outlet tiles of a multi-socket device from one handler instead of two copies of it
|
|
33
33
|
- fix: move the H7152 dehumidifier's speed slider when it reports a new speed, which until now only the H7150 and H7151 did
|
|
34
34
|
- refactor: point the H7101 and H7111 fans at the H7100 handler they matched command for command
|
|
35
|
+
- feat: give a diffuser model that has not been worked out yet the same starting handler the other appliances have, so it works for on and off
|
|
36
|
+
- refactor: choose a device handler from a table, so adding a device type no longer means editing the platform
|
|
37
|
+
- refactor: move the settings parsing into its own file, away from the code that talks to devices
|
|
35
38
|
|
|
36
39
|
## v11.32.1 (2026-08-02)
|
|
37
40
|
|
|
@@ -356,6 +356,16 @@ export const deviceDehumidifierBasic = makeBasicAppliance({
|
|
|
356
356
|
currentStateOff: 0, // inactive
|
|
357
357
|
})
|
|
358
358
|
|
|
359
|
+
// Diffusers are power only, so there is no variable control to offer. They use
|
|
360
|
+
// the same tile as the models already handled, so a new one looks the same
|
|
361
|
+
export const deviceDiffuserBasic = makeBasicAppliance({
|
|
362
|
+
family: 'diffuser',
|
|
363
|
+
service: 'AirPurifier',
|
|
364
|
+
currentStateChar: 'CurrentAirPurifierState',
|
|
365
|
+
currentStateOn: 2, // purifying air
|
|
366
|
+
currentStateOff: 0, // inactive
|
|
367
|
+
})
|
|
368
|
+
|
|
359
369
|
export const devicePurifierBasic = makeBasicAppliance({
|
|
360
370
|
family: 'purifier',
|
|
361
371
|
service: 'AirPurifier',
|
package/lib/device/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
deviceDehumidifierBasic,
|
|
3
|
+
deviceDiffuserBasic,
|
|
3
4
|
deviceFanBasic,
|
|
4
5
|
deviceHumidifierBasic,
|
|
5
6
|
deviceIceMakerBasic,
|
|
@@ -55,6 +56,7 @@ export default {
|
|
|
55
56
|
deviceDiffuserH7161,
|
|
56
57
|
deviceDiffuserH7162: deviceDiffuserH7161, // identical to the H7161
|
|
57
58
|
deviceDehumidifierBasic,
|
|
59
|
+
deviceDiffuserBasic,
|
|
58
60
|
deviceFanBasic,
|
|
59
61
|
deviceFanH1310: deviceFanH7100, // same fan, minus a confirmed swing status message
|
|
60
62
|
deviceFanH1370,
|
package/lib/platform.js
CHANGED
|
@@ -11,18 +11,18 @@ import awsClient from './connection/aws.js'
|
|
|
11
11
|
import httpClient from './connection/http.js'
|
|
12
12
|
import lanClient from './connection/lan.js'
|
|
13
13
|
import openApiClient from './connection/openapi.js'
|
|
14
|
-
import deviceTypes from './device/index.js'
|
|
15
14
|
import eveService from './fakegato/fakegato-history.js'
|
|
16
15
|
import { BLE_UPDATE_TIMEOUT } from './utils/ble-protocol.js'
|
|
17
16
|
import { buildCommand } from './utils/command-builder.js'
|
|
17
|
+
import { applyUserConfig } from './utils/config-parser.js'
|
|
18
18
|
import platformConsts from './utils/constants.js'
|
|
19
19
|
import platformChars from './utils/custom-chars.js'
|
|
20
20
|
import { getDeviceCapabilities } from './utils/device-capabilities.js'
|
|
21
|
+
import { resolveDeviceType } from './utils/dispatch.js'
|
|
21
22
|
import eveChars from './utils/eve-chars.js'
|
|
22
23
|
import {
|
|
23
24
|
base64ToHex,
|
|
24
25
|
hasProperty,
|
|
25
|
-
parseDeviceId,
|
|
26
26
|
parseError,
|
|
27
27
|
pfxToCertAndKey,
|
|
28
28
|
updateFailureError,
|
|
@@ -86,7 +86,7 @@ export default class {
|
|
|
86
86
|
|
|
87
87
|
// Apply the user's configuration
|
|
88
88
|
this.config = platformConsts.defaultConfig
|
|
89
|
-
|
|
89
|
+
applyUserConfig(this, config)
|
|
90
90
|
|
|
91
91
|
// Set up empty clients
|
|
92
92
|
this.bleClient = false
|
|
@@ -104,269 +104,6 @@ export default class {
|
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
applyUserConfig(config) {
|
|
108
|
-
// These shorthand functions save line space during config parsing
|
|
109
|
-
const logDefault = (k, def) => {
|
|
110
|
-
this.log.warn('%s [%s] %s %s.', platformLang.cfgItem, k, platformLang.cfgDef, def)
|
|
111
|
-
}
|
|
112
|
-
const logDuplicate = (k) => {
|
|
113
|
-
this.log.warn('%s [%s] %s.', platformLang.cfgItem, k, platformLang.cfgDup)
|
|
114
|
-
}
|
|
115
|
-
const logIgnore = (k) => {
|
|
116
|
-
this.log.warn('%s [%s] %s.', platformLang.cfgItem, k, platformLang.cfgIgn)
|
|
117
|
-
}
|
|
118
|
-
const logIgnoreItem = (k) => {
|
|
119
|
-
this.log.warn('%s [%s] %s.', platformLang.cfgItem, k, platformLang.cfgIgnItem)
|
|
120
|
-
}
|
|
121
|
-
const logIncrease = (k, min) => {
|
|
122
|
-
this.log.warn('%s [%s] %s %s.', platformLang.cfgItem, k, platformLang.cfgLow, min)
|
|
123
|
-
}
|
|
124
|
-
const logQuotes = (k) => {
|
|
125
|
-
this.log.warn('%s [%s] %s.', platformLang.cfgItem, k, platformLang.cfgQts)
|
|
126
|
-
}
|
|
127
|
-
const logRemove = (k) => {
|
|
128
|
-
this.log.warn('%s [%s] %s.', platformLang.cfgItem, k, platformLang.cfgRmv)
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Begin applying the user's config
|
|
132
|
-
Object.entries(config).forEach((entry) => {
|
|
133
|
-
const [key, val] = entry
|
|
134
|
-
switch (key) {
|
|
135
|
-
case 'apiKey':
|
|
136
|
-
case 'code':
|
|
137
|
-
case 'password':
|
|
138
|
-
case 'username':
|
|
139
|
-
if (typeof val !== 'string' || val === '') {
|
|
140
|
-
logIgnore(key)
|
|
141
|
-
} else {
|
|
142
|
-
this.config[key] = val
|
|
143
|
-
}
|
|
144
|
-
break
|
|
145
|
-
case 'lanScanSubnets': {
|
|
146
|
-
// Comma-separated list of CIDR subnets to scan directly (for devices
|
|
147
|
-
// on a different subnet from Homebridge, which multicast can't reach)
|
|
148
|
-
if (typeof val !== 'string' || val.trim() === '') {
|
|
149
|
-
break
|
|
150
|
-
}
|
|
151
|
-
const subnets = val
|
|
152
|
-
.split(',')
|
|
153
|
-
.map(s => s.trim())
|
|
154
|
-
.filter(s => /^\d{1,3}(?:\.\d{1,3}){3}\/\d{1,2}$/.test(s))
|
|
155
|
-
if (subnets.length === 0) {
|
|
156
|
-
logIgnore(key)
|
|
157
|
-
} else {
|
|
158
|
-
this.config[key] = subnets
|
|
159
|
-
}
|
|
160
|
-
break
|
|
161
|
-
}
|
|
162
|
-
case 'awsDisable':
|
|
163
|
-
case 'bleDisable':
|
|
164
|
-
case 'colourSafeMode':
|
|
165
|
-
case 'disableDeviceLogging':
|
|
166
|
-
case 'ignoreMatter':
|
|
167
|
-
case 'lanDisable':
|
|
168
|
-
if (typeof val === 'string') {
|
|
169
|
-
logQuotes(key)
|
|
170
|
-
}
|
|
171
|
-
this.config[key] = val === 'false' ? false : !!val
|
|
172
|
-
break
|
|
173
|
-
case 'bleControlInterval':
|
|
174
|
-
case 'bleRefreshTime':
|
|
175
|
-
case 'httpRefreshTime':
|
|
176
|
-
case 'lanRefreshTime':
|
|
177
|
-
case 'lanScanInterval': {
|
|
178
|
-
if (typeof val === 'string') {
|
|
179
|
-
logQuotes(key)
|
|
180
|
-
}
|
|
181
|
-
const intVal = Number.parseInt(val, 10)
|
|
182
|
-
if (Number.isNaN(intVal)) {
|
|
183
|
-
logDefault(key, platformConsts.defaultValues[key])
|
|
184
|
-
this.config[key] = platformConsts.defaultValues[key]
|
|
185
|
-
} else if (intVal < platformConsts.minValues[key]) {
|
|
186
|
-
logIncrease(key, platformConsts.minValues[key])
|
|
187
|
-
this.config[key] = platformConsts.minValues[key]
|
|
188
|
-
} else {
|
|
189
|
-
this.config[key] = intVal
|
|
190
|
-
}
|
|
191
|
-
break
|
|
192
|
-
}
|
|
193
|
-
case 'dehumidifierDevices':
|
|
194
|
-
case 'fanDevices':
|
|
195
|
-
case 'heaterDevices':
|
|
196
|
-
case 'humidifierDevices':
|
|
197
|
-
case 'iceMakerDevices':
|
|
198
|
-
case 'kettleDevices':
|
|
199
|
-
case 'leakDevices':
|
|
200
|
-
case 'lightDevices':
|
|
201
|
-
case 'purifierDevices':
|
|
202
|
-
case 'diffuserDevices':
|
|
203
|
-
case 'switchDevices':
|
|
204
|
-
case 'thermoDevices':
|
|
205
|
-
if (Array.isArray(val) && val.length > 0) {
|
|
206
|
-
val.forEach((x) => {
|
|
207
|
-
if (!x.deviceId) {
|
|
208
|
-
logIgnoreItem(key)
|
|
209
|
-
return
|
|
210
|
-
}
|
|
211
|
-
const id = parseDeviceId(x.deviceId)
|
|
212
|
-
if (Object.keys(this.deviceConf).includes(id)) {
|
|
213
|
-
logDuplicate(`${key}.${id}`)
|
|
214
|
-
return
|
|
215
|
-
}
|
|
216
|
-
const entries = Object.entries(x)
|
|
217
|
-
if (entries.length === 1) {
|
|
218
|
-
logRemove(`${key}.${id}`)
|
|
219
|
-
return
|
|
220
|
-
}
|
|
221
|
-
this.deviceConf[id] = {}
|
|
222
|
-
entries.forEach((subEntry) => {
|
|
223
|
-
const [k, v] = subEntry
|
|
224
|
-
switch (k) {
|
|
225
|
-
case 'adaptiveLightingShift':
|
|
226
|
-
case 'brightnessStep':
|
|
227
|
-
case 'lowBattThreshold': {
|
|
228
|
-
if (typeof v === 'string') {
|
|
229
|
-
logQuotes(`${key}.${k}`)
|
|
230
|
-
}
|
|
231
|
-
const intVal = Number.parseInt(v, 10)
|
|
232
|
-
if (Number.isNaN(intVal)) {
|
|
233
|
-
logDefault(`${key}.${id}.${k}`, platformConsts.defaultValues[k])
|
|
234
|
-
this.deviceConf[id][k] = platformConsts.defaultValues[k]
|
|
235
|
-
} else if (intVal < platformConsts.minValues[k]) {
|
|
236
|
-
logIncrease(`${key}.${id}.${k}`, platformConsts.minValues[k])
|
|
237
|
-
this.deviceConf[id][k] = platformConsts.minValues[k]
|
|
238
|
-
} else {
|
|
239
|
-
this.deviceConf[id][k] = intVal
|
|
240
|
-
}
|
|
241
|
-
break
|
|
242
|
-
}
|
|
243
|
-
case 'awsBrightnessNoScale':
|
|
244
|
-
case 'hideLight':
|
|
245
|
-
case 'hideModeGreenTea':
|
|
246
|
-
case 'hideModeOolongTea':
|
|
247
|
-
case 'hideModeCoffee':
|
|
248
|
-
case 'hideModeBlackTea':
|
|
249
|
-
case 'showCustomMode1':
|
|
250
|
-
case 'showCustomMode2':
|
|
251
|
-
case 'showExtraSwitch':
|
|
252
|
-
case 'tempReporting':
|
|
253
|
-
if (typeof v === 'string') {
|
|
254
|
-
logQuotes(`${key}.${id}.${k}`)
|
|
255
|
-
}
|
|
256
|
-
this.deviceConf[id][k] = v === 'false' ? false : !!v
|
|
257
|
-
break
|
|
258
|
-
case 'awsColourMode':
|
|
259
|
-
case 'openApiTempUnit':
|
|
260
|
-
case 'showAs': {
|
|
261
|
-
if (typeof v !== 'string' || !platformConsts.allowed[k].includes(v)) {
|
|
262
|
-
logIgnore(`${key}.${id}.${k}`)
|
|
263
|
-
} else {
|
|
264
|
-
this.deviceConf[id][k] = v
|
|
265
|
-
}
|
|
266
|
-
break
|
|
267
|
-
}
|
|
268
|
-
case 'customAddress':
|
|
269
|
-
case 'customIPAddress':
|
|
270
|
-
if (typeof v !== 'string' || v === '') {
|
|
271
|
-
logIgnore(`${key}.${id}.${k}`)
|
|
272
|
-
} else {
|
|
273
|
-
this.deviceConf[id][k] = v.replace(WHITESPACE_REGEX, '')
|
|
274
|
-
}
|
|
275
|
-
break
|
|
276
|
-
case 'deviceId':
|
|
277
|
-
break
|
|
278
|
-
case 'diyMode':
|
|
279
|
-
case 'diyModeTwo':
|
|
280
|
-
case 'diyModeThree':
|
|
281
|
-
case 'diyModeFour':
|
|
282
|
-
case 'musicMode':
|
|
283
|
-
case 'musicModeTwo':
|
|
284
|
-
case 'scene':
|
|
285
|
-
case 'sceneTwo':
|
|
286
|
-
case 'sceneThree':
|
|
287
|
-
case 'sceneFour':
|
|
288
|
-
case 'segmented':
|
|
289
|
-
case 'segmentedTwo':
|
|
290
|
-
case 'segmentedThree':
|
|
291
|
-
case 'segmentedFour':
|
|
292
|
-
case 'temperatureSource':
|
|
293
|
-
case 'videoMode':
|
|
294
|
-
case 'videoModeTwo': {
|
|
295
|
-
if (typeof v === 'string') {
|
|
296
|
-
this.log.warn(`${key}.${id}.${k} incorrectly configured - please use the config screen to reconfigure this item:`)
|
|
297
|
-
this.log.warn(`${key}.${id}.${k}: ${v}`)
|
|
298
|
-
}
|
|
299
|
-
if (typeof v === 'object') {
|
|
300
|
-
// object - only allowed keys are 'sceneCode', 'bleCode' and 'showAs'
|
|
301
|
-
const subEntries = Object.entries(v)
|
|
302
|
-
if (subEntries.length > 0) {
|
|
303
|
-
this.deviceConf[id][k] = {}
|
|
304
|
-
subEntries.forEach((subSubEntry) => {
|
|
305
|
-
const [k1, v1] = subSubEntry
|
|
306
|
-
switch (k1) {
|
|
307
|
-
case 'bleCode':
|
|
308
|
-
case 'sceneCode':
|
|
309
|
-
if (typeof v1 !== 'string' || v1 === '') {
|
|
310
|
-
logIgnore(`${key}.${id}.${k}.${k1}`)
|
|
311
|
-
} else {
|
|
312
|
-
this.deviceConf[id][k][k1] = v1
|
|
313
|
-
}
|
|
314
|
-
break
|
|
315
|
-
case 'showAs': {
|
|
316
|
-
if (typeof v1 !== 'string' || !['default', 'switch'].includes(v1)) {
|
|
317
|
-
logIgnore(`${key}.${id}.${k}.${k1}`)
|
|
318
|
-
} else {
|
|
319
|
-
this.deviceConf[id][k][k1] = v1
|
|
320
|
-
}
|
|
321
|
-
break
|
|
322
|
-
}
|
|
323
|
-
default:
|
|
324
|
-
logIgnore(`${key}.${id}.${k}.${k1}`)
|
|
325
|
-
break
|
|
326
|
-
}
|
|
327
|
-
})
|
|
328
|
-
} else {
|
|
329
|
-
logIgnore(`${key}.${id}.${k}`)
|
|
330
|
-
}
|
|
331
|
-
} else {
|
|
332
|
-
logIgnore(`${key}.${id}.${k}`)
|
|
333
|
-
}
|
|
334
|
-
break
|
|
335
|
-
}
|
|
336
|
-
case 'ignoreDevice':
|
|
337
|
-
if (typeof v === 'string') {
|
|
338
|
-
logQuotes(`${key}.${id}.${k}`)
|
|
339
|
-
}
|
|
340
|
-
if (!!v && v !== 'false') {
|
|
341
|
-
this.ignoredDevices.push(id)
|
|
342
|
-
}
|
|
343
|
-
break
|
|
344
|
-
case 'label':
|
|
345
|
-
if (typeof v !== 'string' || v === '') {
|
|
346
|
-
logIgnore(`${key}.${id}.${k}`)
|
|
347
|
-
} else {
|
|
348
|
-
this.deviceConf[id][k] = v
|
|
349
|
-
}
|
|
350
|
-
break
|
|
351
|
-
default:
|
|
352
|
-
logRemove(`${key}.${id}.${k}`)
|
|
353
|
-
}
|
|
354
|
-
})
|
|
355
|
-
})
|
|
356
|
-
} else {
|
|
357
|
-
logIgnore(key)
|
|
358
|
-
}
|
|
359
|
-
break
|
|
360
|
-
case 'name':
|
|
361
|
-
case 'platform':
|
|
362
|
-
break
|
|
363
|
-
default:
|
|
364
|
-
logRemove(key)
|
|
365
|
-
break
|
|
366
|
-
}
|
|
367
|
-
})
|
|
368
|
-
}
|
|
369
|
-
|
|
370
107
|
async pluginSetup() {
|
|
371
108
|
// Plugin has finished initialising so now onto setup
|
|
372
109
|
try {
|
|
@@ -1095,215 +832,14 @@ export default class {
|
|
|
1095
832
|
try {
|
|
1096
833
|
const deviceConf = this.deviceConf[device.device.toUpperCase()] || {}
|
|
1097
834
|
const uuid = this.api.hap.uuid.generate(device.device)
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
accessory = devicesInHB.get(uuid) || this.addAccessory(device)
|
|
1107
|
-
} else if (platformConsts.models.switchSingle.includes(device.model)) {
|
|
1108
|
-
// Device is a cloud enabled Wi-Fi switch
|
|
1109
|
-
switch (deviceConf.showAs || platformConsts.defaultValues.showAs) {
|
|
1110
|
-
case 'audio': {
|
|
1111
|
-
if (devicesInHB.get(uuid)) {
|
|
1112
|
-
this.removeAccessory(devicesInHB.get(uuid))
|
|
1113
|
-
}
|
|
1114
|
-
devInstance = deviceTypes.deviceTVSingle
|
|
1115
|
-
accessory = this.addExternalAccessory(device, 34)
|
|
1116
|
-
break
|
|
1117
|
-
}
|
|
1118
|
-
case 'box': {
|
|
1119
|
-
if (devicesInHB.get(uuid)) {
|
|
1120
|
-
this.removeAccessory(devicesInHB.get(uuid))
|
|
1121
|
-
}
|
|
1122
|
-
devInstance = deviceTypes.deviceTVSingle
|
|
1123
|
-
accessory = this.addExternalAccessory(device, 35)
|
|
1124
|
-
break
|
|
1125
|
-
}
|
|
1126
|
-
case 'stick': {
|
|
1127
|
-
if (devicesInHB.get(uuid)) {
|
|
1128
|
-
this.removeAccessory(devicesInHB.get(uuid))
|
|
1129
|
-
}
|
|
1130
|
-
devInstance = deviceTypes.deviceTVSingle
|
|
1131
|
-
accessory = this.addExternalAccessory(device, 36)
|
|
1132
|
-
break
|
|
1133
|
-
}
|
|
1134
|
-
case 'cooler': {
|
|
1135
|
-
if (!deviceConf.temperatureSource) {
|
|
1136
|
-
this.log.warn('[%s] %s.', device.deviceName, platformLang.heaterSimNoSensor)
|
|
1137
|
-
if (devicesInHB.has(uuid)) {
|
|
1138
|
-
this.removeAccessory(devicesInHB.get(uuid))
|
|
1139
|
-
}
|
|
1140
|
-
return
|
|
1141
|
-
}
|
|
1142
|
-
devInstance = deviceTypes.deviceCoolerSingle
|
|
1143
|
-
accessory = devicesInHB.get(uuid) || this.addAccessory(device)
|
|
1144
|
-
break
|
|
1145
|
-
}
|
|
1146
|
-
case 'heater': {
|
|
1147
|
-
if (!deviceConf.temperatureSource) {
|
|
1148
|
-
this.log.warn('[%s] %s.', device.deviceName, platformLang.heaterSimNoSensor)
|
|
1149
|
-
if (devicesInHB.has(uuid)) {
|
|
1150
|
-
this.removeAccessory(devicesInHB.get(uuid))
|
|
1151
|
-
}
|
|
1152
|
-
return
|
|
1153
|
-
}
|
|
1154
|
-
devInstance = deviceTypes.deviceHeater2Single
|
|
1155
|
-
accessory = devicesInHB.get(uuid) || this.addAccessory(device)
|
|
1156
|
-
break
|
|
1157
|
-
}
|
|
1158
|
-
case 'purifier': {
|
|
1159
|
-
devInstance = deviceTypes.devicePurifierSingle
|
|
1160
|
-
accessory = devicesInHB.get(uuid) || this.addAccessory(device)
|
|
1161
|
-
break
|
|
1162
|
-
}
|
|
1163
|
-
case 'switch': {
|
|
1164
|
-
devInstance = deviceTypes.deviceSwitchSingle
|
|
1165
|
-
accessory = devicesInHB.get(uuid) || this.addAccessory(device)
|
|
1166
|
-
break
|
|
1167
|
-
}
|
|
1168
|
-
case 'tap': {
|
|
1169
|
-
devInstance = deviceTypes.deviceTapSingle
|
|
1170
|
-
accessory = devicesInHB.get(uuid) || this.addAccessory(device)
|
|
1171
|
-
break
|
|
1172
|
-
}
|
|
1173
|
-
case 'valve': {
|
|
1174
|
-
devInstance = deviceTypes.deviceValveSingle
|
|
1175
|
-
accessory = devicesInHB.get(uuid) || this.addAccessory(device)
|
|
1176
|
-
break
|
|
1177
|
-
}
|
|
1178
|
-
default:
|
|
1179
|
-
devInstance = deviceTypes.deviceOutletSingle
|
|
1180
|
-
accessory = devicesInHB.get(uuid) || this.addAccessory(device)
|
|
1181
|
-
break
|
|
1182
|
-
}
|
|
1183
|
-
} else if (platformConsts.models.switchDouble.includes(device.model)) {
|
|
1184
|
-
// Device is an AWS enabled Wi-Fi double switch
|
|
1185
|
-
switch (deviceConf.showAs || platformConsts.defaultValues.showAs) {
|
|
1186
|
-
case 'switch': {
|
|
1187
|
-
devInstance = deviceTypes.deviceSwitchDouble
|
|
1188
|
-
accessory = devicesInHB.get(uuid) || this.addAccessory(device)
|
|
1189
|
-
break
|
|
1190
|
-
}
|
|
1191
|
-
default: {
|
|
1192
|
-
devInstance = deviceTypes.deviceOutletDouble
|
|
1193
|
-
accessory = devicesInHB.get(uuid) || this.addAccessory(device)
|
|
1194
|
-
break
|
|
1195
|
-
}
|
|
1196
|
-
}
|
|
1197
|
-
} else if (platformConsts.models.switchTriple.includes(device.model)) {
|
|
1198
|
-
// Device is an AWS enabled Wi-Fi double switch
|
|
1199
|
-
switch (deviceConf.showAs || platformConsts.defaultValues.showAs) {
|
|
1200
|
-
case 'switch': {
|
|
1201
|
-
devInstance = deviceTypes.deviceSwitchTriple
|
|
1202
|
-
accessory = devicesInHB.get(uuid) || this.addAccessory(device)
|
|
1203
|
-
break
|
|
1204
|
-
}
|
|
1205
|
-
default: {
|
|
1206
|
-
devInstance = deviceTypes.deviceOutletTriple
|
|
1207
|
-
accessory = devicesInHB.get(uuid) || this.addAccessory(device)
|
|
1208
|
-
break
|
|
1209
|
-
}
|
|
1210
|
-
}
|
|
1211
|
-
} else if (platformConsts.models.sensorLeak.includes(device.model)) {
|
|
1212
|
-
// Device is a leak sensor
|
|
1213
|
-
devInstance = deviceTypes.deviceSensorLeak
|
|
1214
|
-
accessory = devicesInHB.get(uuid) || this.addAccessory(device)
|
|
1215
|
-
} else if (platformConsts.models.sensorPresence.includes(device.model)) {
|
|
1216
|
-
// Device is a presence sensor
|
|
1217
|
-
devInstance = deviceTypes.deviceSensorPresence
|
|
1218
|
-
accessory = devicesInHB.get(uuid) || this.addAccessory(device)
|
|
1219
|
-
} else if (platformConsts.models.sensorThermo.includes(device.model)) {
|
|
1220
|
-
// Device is a thermo-hygrometer sensor
|
|
1221
|
-
devInstance = deviceConf.showExtraSwitch
|
|
1222
|
-
? deviceTypes.deviceSensorThermoSwitch
|
|
1223
|
-
: deviceTypes.deviceSensorThermo
|
|
1224
|
-
accessory = devicesInHB.get(uuid) || this.addAccessory(device)
|
|
1225
|
-
} else if (platformConsts.models.sensorThermo4.includes(device.model)) {
|
|
1226
|
-
// Device is a thermo-hygrometer sensor with 4 prongs and AWS support
|
|
1227
|
-
devInstance = deviceTypes.deviceSensorThermo4
|
|
1228
|
-
accessory = devicesInHB.get(uuid) || this.addAccessory(device)
|
|
1229
|
-
} else if (platformConsts.models.sensorMonitor.includes(device.model)) {
|
|
1230
|
-
devInstance = deviceTypes.deviceSensorMonitor
|
|
1231
|
-
doAWSPolling = true
|
|
1232
|
-
accessory = devicesInHB.get(uuid) || this.addAccessory(device)
|
|
1233
|
-
} else if (platformConsts.models.sensorCO2.includes(device.model)) {
|
|
1234
|
-
// Device is a CO2 + temperature + humidity monitor (H5140)
|
|
1235
|
-
devInstance = deviceTypes.deviceSensorCO2
|
|
1236
|
-
doAWSPolling = true
|
|
1237
|
-
accessory = devicesInHB.get(uuid) || this.addAccessory(device)
|
|
1238
|
-
} else if (platformConsts.models.fan.includes(device.model)) {
|
|
1239
|
-
// Device is a fan. Models whose speeds have not been worked out yet fall
|
|
1240
|
-
// back to the basic handler, which offers on and off and asks for what
|
|
1241
|
-
// it needs rather than leaving the device unsupported altogether
|
|
1242
|
-
devInstance = deviceTypes[`deviceFan${device.model}`] || deviceTypes.deviceFanBasic
|
|
1243
|
-
accessory = devicesInHB.get(uuid) || this.addAccessory(device)
|
|
1244
|
-
} else if (platformConsts.models.heater1.includes(device.model)) {
|
|
1245
|
-
// Some of these heaters report the room temperature and some do not,
|
|
1246
|
-
// and the two need different tiles in HomeKit - a plain fan against a
|
|
1247
|
-
// heater with a temperature readout. Rather than leave the owner to
|
|
1248
|
-
// work that out and tick a box, take Govee's own word for it where the
|
|
1249
|
-
// device says so, and otherwise use what a previous run learnt from the
|
|
1250
|
-
// readings themselves. The setting still wins if it has been set, so
|
|
1251
|
-
// nobody who has already configured this sees their tile change.
|
|
1252
|
-
const reportsTemperature = device.openApiInfo?.byInstance?.sensorTemperature
|
|
1253
|
-
? true
|
|
1254
|
-
: devicesInHB.get(uuid)?.context?.supportsAmbientTemp
|
|
1255
|
-
devInstance = (deviceConf.tempReporting ?? reportsTemperature)
|
|
1256
|
-
? deviceTypes.deviceHeater1B
|
|
1257
|
-
: deviceTypes.deviceHeater1A
|
|
1258
|
-
doAWSPolling = true
|
|
1259
|
-
accessory = devicesInHB.get(uuid) || this.addAccessory(device)
|
|
1260
|
-
} else if (platformConsts.models.heater2.includes(device.model)) {
|
|
1261
|
-
// Device is a H7131/H7132
|
|
1262
|
-
devInstance = deviceTypes.deviceHeater2
|
|
1263
|
-
doAWSPolling = true
|
|
1264
|
-
accessory = devicesInHB.get(uuid) || this.addAccessory(device)
|
|
1265
|
-
} else if (platformConsts.models.humidifier.includes(device.model)) {
|
|
1266
|
-
// Device is a humidifier
|
|
1267
|
-
doAWSPolling = true
|
|
1268
|
-
devInstance = deviceTypes[`deviceHumidifier${device.model}`] || deviceTypes.deviceHumidifierBasic
|
|
1269
|
-
accessory = devicesInHB.get(uuid) || this.addAccessory(device)
|
|
1270
|
-
} else if (platformConsts.models.dehumidifier.includes(device.model)) {
|
|
1271
|
-
// Device is a dehumidifier
|
|
1272
|
-
devInstance = deviceTypes[`deviceDehumidifier${device.model}`] || deviceTypes.deviceDehumidifierBasic
|
|
1273
|
-
doAWSPolling = true
|
|
1274
|
-
accessory = devicesInHB.get(uuid) || this.addAccessory(device)
|
|
1275
|
-
} else if (platformConsts.models.purifier.includes(device.model)) {
|
|
1276
|
-
// Device is a purifier
|
|
1277
|
-
devInstance = deviceTypes[`devicePurifier${device.model}`] || deviceTypes.devicePurifierBasic
|
|
1278
|
-
doAWSPolling = true
|
|
1279
|
-
accessory = devicesInHB.get(uuid) || this.addAccessory(device)
|
|
1280
|
-
} else if (platformConsts.models.diffuser.includes(device.model)) {
|
|
1281
|
-
// Device is a diffuser
|
|
1282
|
-
devInstance = deviceTypes[`deviceDiffuser${device.model}`]
|
|
1283
|
-
doAWSPolling = true
|
|
1284
|
-
accessory = devicesInHB.get(uuid) || this.addAccessory(device)
|
|
1285
|
-
} else if (platformConsts.models.sensorButton.includes(device.model)) {
|
|
1286
|
-
// Device is a button
|
|
1287
|
-
devInstance = deviceTypes.deviceSensorButton
|
|
1288
|
-
accessory = devicesInHB.get(uuid) || this.addAccessory(device)
|
|
1289
|
-
} else if (platformConsts.models.sensorContact.includes(device.model)) {
|
|
1290
|
-
// Device is a contact sensor
|
|
1291
|
-
devInstance = deviceTypes.deviceSensorContact
|
|
1292
|
-
accessory = devicesInHB.get(uuid) || this.addAccessory(device)
|
|
1293
|
-
} else if (platformConsts.models.kettle.includes(device.model)) {
|
|
1294
|
-
// Device is a kettle
|
|
1295
|
-
devInstance = deviceTypes.deviceKettle
|
|
1296
|
-
accessory = devicesInHB.get(uuid) || this.addAccessory(device)
|
|
1297
|
-
} else if (platformConsts.models.iceMaker.includes(device.model)) {
|
|
1298
|
-
// Device is an ice maker
|
|
1299
|
-
devInstance = deviceTypes[`deviceIceMaker${device.model}`] || deviceTypes.deviceIceMakerBasic
|
|
1300
|
-
accessory = devicesInHB.get(uuid) || this.addAccessory(device)
|
|
1301
|
-
} else if (platformConsts.models.template.includes(device.model)) {
|
|
1302
|
-
// Device is a work-in-progress
|
|
1303
|
-
devInstance = deviceTypes.deviceTemplate
|
|
1304
|
-
accessory = devicesInHB.get(uuid) || this.addAccessory(device)
|
|
1305
|
-
} else {
|
|
1306
|
-
// Device is not in any supported model list but could be implemented into the plugin
|
|
835
|
+
const cached = devicesInHB.get(uuid)
|
|
836
|
+
|
|
837
|
+
// Which handler this device gets is decided by the dispatch table, so
|
|
838
|
+
// adding a device type does not mean editing this file
|
|
839
|
+
const route = resolveDeviceType(device, deviceConf, cached?.context)
|
|
840
|
+
|
|
841
|
+
if (!route) {
|
|
842
|
+
// Not in any supported model list, but it could be added to the plugin
|
|
1307
843
|
this.log.warn(
|
|
1308
844
|
'[%s] %s:\n%s',
|
|
1309
845
|
device.deviceName,
|
|
@@ -1313,6 +849,31 @@ export default class {
|
|
|
1313
849
|
return
|
|
1314
850
|
}
|
|
1315
851
|
|
|
852
|
+
if (route.skip === 'needsTemperatureSource') {
|
|
853
|
+
// Standing in for a heater or cooler needs a temperature from somewhere
|
|
854
|
+
// else, and without one there is nothing to show
|
|
855
|
+
this.log.warn('[%s] %s.', device.deviceName, platformLang.heaterSimNoSensor)
|
|
856
|
+
if (cached) {
|
|
857
|
+
this.removeAccessory(cached)
|
|
858
|
+
}
|
|
859
|
+
return
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
const devInstance = route.handler
|
|
863
|
+
const doAWSPolling = route.awsPolling
|
|
864
|
+
|
|
865
|
+
let accessory
|
|
866
|
+
if (route.externalCategory) {
|
|
867
|
+
// A television has to be published as its own accessory rather than as
|
|
868
|
+
// a tile on the bridge, so any tile from a previous setting goes first
|
|
869
|
+
if (cached) {
|
|
870
|
+
this.removeAccessory(cached)
|
|
871
|
+
}
|
|
872
|
+
accessory = this.addExternalAccessory(device, route.externalCategory)
|
|
873
|
+
} else {
|
|
874
|
+
accessory = cached || this.addAccessory(device)
|
|
875
|
+
}
|
|
876
|
+
|
|
1316
877
|
// Final check the accessory now exists in Homebridge
|
|
1317
878
|
if (!accessory) {
|
|
1318
879
|
throw new Error(platformLang.accNotFound)
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import platformConsts from './constants.js'
|
|
2
|
+
import { parseDeviceId } from './functions.js'
|
|
3
|
+
import platformLang from './lang-en.js'
|
|
4
|
+
|
|
5
|
+
const WHITESPACE_REGEX = /\s+/g
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Reads the user's settings, warns about anything that will not be used, and
|
|
9
|
+
* fills in the defaults for the rest.
|
|
10
|
+
*
|
|
11
|
+
* Lifted out of the platform unchanged. It was the largest single thing in
|
|
12
|
+
* there and had nothing to do with talking to devices, so a change to how a
|
|
13
|
+
* device works no longer means opening the same file as a change to how a
|
|
14
|
+
* setting is read.
|
|
15
|
+
*
|
|
16
|
+
* It writes `config`, `deviceConf` and `ignoredDevices` onto the platform
|
|
17
|
+
* rather than returning them, which is how it always worked.
|
|
18
|
+
*/
|
|
19
|
+
export function applyUserConfig(platform, config) {
|
|
20
|
+
// These shorthand functions save line space during config parsing
|
|
21
|
+
const logDefault = (k, def) => {
|
|
22
|
+
platform.log.warn('%s [%s] %s %s.', platformLang.cfgItem, k, platformLang.cfgDef, def)
|
|
23
|
+
}
|
|
24
|
+
const logDuplicate = (k) => {
|
|
25
|
+
platform.log.warn('%s [%s] %s.', platformLang.cfgItem, k, platformLang.cfgDup)
|
|
26
|
+
}
|
|
27
|
+
const logIgnore = (k) => {
|
|
28
|
+
platform.log.warn('%s [%s] %s.', platformLang.cfgItem, k, platformLang.cfgIgn)
|
|
29
|
+
}
|
|
30
|
+
const logIgnoreItem = (k) => {
|
|
31
|
+
platform.log.warn('%s [%s] %s.', platformLang.cfgItem, k, platformLang.cfgIgnItem)
|
|
32
|
+
}
|
|
33
|
+
const logIncrease = (k, min) => {
|
|
34
|
+
platform.log.warn('%s [%s] %s %s.', platformLang.cfgItem, k, platformLang.cfgLow, min)
|
|
35
|
+
}
|
|
36
|
+
const logQuotes = (k) => {
|
|
37
|
+
platform.log.warn('%s [%s] %s.', platformLang.cfgItem, k, platformLang.cfgQts)
|
|
38
|
+
}
|
|
39
|
+
const logRemove = (k) => {
|
|
40
|
+
platform.log.warn('%s [%s] %s.', platformLang.cfgItem, k, platformLang.cfgRmv)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Begin applying the user's config
|
|
44
|
+
Object.entries(config).forEach((entry) => {
|
|
45
|
+
const [key, val] = entry
|
|
46
|
+
switch (key) {
|
|
47
|
+
case 'apiKey':
|
|
48
|
+
case 'code':
|
|
49
|
+
case 'password':
|
|
50
|
+
case 'username':
|
|
51
|
+
if (typeof val !== 'string' || val === '') {
|
|
52
|
+
logIgnore(key)
|
|
53
|
+
} else {
|
|
54
|
+
platform.config[key] = val
|
|
55
|
+
}
|
|
56
|
+
break
|
|
57
|
+
case 'lanScanSubnets': {
|
|
58
|
+
// Comma-separated list of CIDR subnets to scan directly (for devices
|
|
59
|
+
// on a different subnet from Homebridge, which multicast can't reach)
|
|
60
|
+
if (typeof val !== 'string' || val.trim() === '') {
|
|
61
|
+
break
|
|
62
|
+
}
|
|
63
|
+
const subnets = val
|
|
64
|
+
.split(',')
|
|
65
|
+
.map(s => s.trim())
|
|
66
|
+
.filter(s => /^\d{1,3}(?:\.\d{1,3}){3}\/\d{1,2}$/.test(s))
|
|
67
|
+
if (subnets.length === 0) {
|
|
68
|
+
logIgnore(key)
|
|
69
|
+
} else {
|
|
70
|
+
platform.config[key] = subnets
|
|
71
|
+
}
|
|
72
|
+
break
|
|
73
|
+
}
|
|
74
|
+
case 'awsDisable':
|
|
75
|
+
case 'bleDisable':
|
|
76
|
+
case 'colourSafeMode':
|
|
77
|
+
case 'disableDeviceLogging':
|
|
78
|
+
case 'ignoreMatter':
|
|
79
|
+
case 'lanDisable':
|
|
80
|
+
if (typeof val === 'string') {
|
|
81
|
+
logQuotes(key)
|
|
82
|
+
}
|
|
83
|
+
platform.config[key] = val === 'false' ? false : !!val
|
|
84
|
+
break
|
|
85
|
+
case 'bleControlInterval':
|
|
86
|
+
case 'bleRefreshTime':
|
|
87
|
+
case 'httpRefreshTime':
|
|
88
|
+
case 'lanRefreshTime':
|
|
89
|
+
case 'lanScanInterval': {
|
|
90
|
+
if (typeof val === 'string') {
|
|
91
|
+
logQuotes(key)
|
|
92
|
+
}
|
|
93
|
+
const intVal = Number.parseInt(val, 10)
|
|
94
|
+
if (Number.isNaN(intVal)) {
|
|
95
|
+
logDefault(key, platformConsts.defaultValues[key])
|
|
96
|
+
platform.config[key] = platformConsts.defaultValues[key]
|
|
97
|
+
} else if (intVal < platformConsts.minValues[key]) {
|
|
98
|
+
logIncrease(key, platformConsts.minValues[key])
|
|
99
|
+
platform.config[key] = platformConsts.minValues[key]
|
|
100
|
+
} else {
|
|
101
|
+
platform.config[key] = intVal
|
|
102
|
+
}
|
|
103
|
+
break
|
|
104
|
+
}
|
|
105
|
+
case 'dehumidifierDevices':
|
|
106
|
+
case 'fanDevices':
|
|
107
|
+
case 'heaterDevices':
|
|
108
|
+
case 'humidifierDevices':
|
|
109
|
+
case 'iceMakerDevices':
|
|
110
|
+
case 'kettleDevices':
|
|
111
|
+
case 'leakDevices':
|
|
112
|
+
case 'lightDevices':
|
|
113
|
+
case 'purifierDevices':
|
|
114
|
+
case 'diffuserDevices':
|
|
115
|
+
case 'switchDevices':
|
|
116
|
+
case 'thermoDevices':
|
|
117
|
+
if (Array.isArray(val) && val.length > 0) {
|
|
118
|
+
val.forEach((x) => {
|
|
119
|
+
if (!x.deviceId) {
|
|
120
|
+
logIgnoreItem(key)
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
const id = parseDeviceId(x.deviceId)
|
|
124
|
+
if (Object.keys(platform.deviceConf).includes(id)) {
|
|
125
|
+
logDuplicate(`${key}.${id}`)
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
const entries = Object.entries(x)
|
|
129
|
+
if (entries.length === 1) {
|
|
130
|
+
logRemove(`${key}.${id}`)
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
platform.deviceConf[id] = {}
|
|
134
|
+
entries.forEach((subEntry) => {
|
|
135
|
+
const [k, v] = subEntry
|
|
136
|
+
switch (k) {
|
|
137
|
+
case 'adaptiveLightingShift':
|
|
138
|
+
case 'brightnessStep':
|
|
139
|
+
case 'lowBattThreshold': {
|
|
140
|
+
if (typeof v === 'string') {
|
|
141
|
+
logQuotes(`${key}.${k}`)
|
|
142
|
+
}
|
|
143
|
+
const intVal = Number.parseInt(v, 10)
|
|
144
|
+
if (Number.isNaN(intVal)) {
|
|
145
|
+
logDefault(`${key}.${id}.${k}`, platformConsts.defaultValues[k])
|
|
146
|
+
platform.deviceConf[id][k] = platformConsts.defaultValues[k]
|
|
147
|
+
} else if (intVal < platformConsts.minValues[k]) {
|
|
148
|
+
logIncrease(`${key}.${id}.${k}`, platformConsts.minValues[k])
|
|
149
|
+
platform.deviceConf[id][k] = platformConsts.minValues[k]
|
|
150
|
+
} else {
|
|
151
|
+
platform.deviceConf[id][k] = intVal
|
|
152
|
+
}
|
|
153
|
+
break
|
|
154
|
+
}
|
|
155
|
+
case 'awsBrightnessNoScale':
|
|
156
|
+
case 'hideLight':
|
|
157
|
+
case 'hideModeGreenTea':
|
|
158
|
+
case 'hideModeOolongTea':
|
|
159
|
+
case 'hideModeCoffee':
|
|
160
|
+
case 'hideModeBlackTea':
|
|
161
|
+
case 'showCustomMode1':
|
|
162
|
+
case 'showCustomMode2':
|
|
163
|
+
case 'showExtraSwitch':
|
|
164
|
+
case 'tempReporting':
|
|
165
|
+
if (typeof v === 'string') {
|
|
166
|
+
logQuotes(`${key}.${id}.${k}`)
|
|
167
|
+
}
|
|
168
|
+
platform.deviceConf[id][k] = v === 'false' ? false : !!v
|
|
169
|
+
break
|
|
170
|
+
case 'awsColourMode':
|
|
171
|
+
case 'openApiTempUnit':
|
|
172
|
+
case 'showAs': {
|
|
173
|
+
if (typeof v !== 'string' || !platformConsts.allowed[k].includes(v)) {
|
|
174
|
+
logIgnore(`${key}.${id}.${k}`)
|
|
175
|
+
} else {
|
|
176
|
+
platform.deviceConf[id][k] = v
|
|
177
|
+
}
|
|
178
|
+
break
|
|
179
|
+
}
|
|
180
|
+
case 'customAddress':
|
|
181
|
+
case 'customIPAddress':
|
|
182
|
+
if (typeof v !== 'string' || v === '') {
|
|
183
|
+
logIgnore(`${key}.${id}.${k}`)
|
|
184
|
+
} else {
|
|
185
|
+
platform.deviceConf[id][k] = v.replace(WHITESPACE_REGEX, '')
|
|
186
|
+
}
|
|
187
|
+
break
|
|
188
|
+
case 'deviceId':
|
|
189
|
+
break
|
|
190
|
+
case 'diyMode':
|
|
191
|
+
case 'diyModeTwo':
|
|
192
|
+
case 'diyModeThree':
|
|
193
|
+
case 'diyModeFour':
|
|
194
|
+
case 'musicMode':
|
|
195
|
+
case 'musicModeTwo':
|
|
196
|
+
case 'scene':
|
|
197
|
+
case 'sceneTwo':
|
|
198
|
+
case 'sceneThree':
|
|
199
|
+
case 'sceneFour':
|
|
200
|
+
case 'segmented':
|
|
201
|
+
case 'segmentedTwo':
|
|
202
|
+
case 'segmentedThree':
|
|
203
|
+
case 'segmentedFour':
|
|
204
|
+
case 'temperatureSource':
|
|
205
|
+
case 'videoMode':
|
|
206
|
+
case 'videoModeTwo': {
|
|
207
|
+
if (typeof v === 'string') {
|
|
208
|
+
platform.log.warn(`${key}.${id}.${k} incorrectly configured - please use the config screen to reconfigure this item:`)
|
|
209
|
+
platform.log.warn(`${key}.${id}.${k}: ${v}`)
|
|
210
|
+
}
|
|
211
|
+
if (typeof v === 'object') {
|
|
212
|
+
// object - only allowed keys are 'sceneCode', 'bleCode' and 'showAs'
|
|
213
|
+
const subEntries = Object.entries(v)
|
|
214
|
+
if (subEntries.length > 0) {
|
|
215
|
+
platform.deviceConf[id][k] = {}
|
|
216
|
+
subEntries.forEach((subSubEntry) => {
|
|
217
|
+
const [k1, v1] = subSubEntry
|
|
218
|
+
switch (k1) {
|
|
219
|
+
case 'bleCode':
|
|
220
|
+
case 'sceneCode':
|
|
221
|
+
if (typeof v1 !== 'string' || v1 === '') {
|
|
222
|
+
logIgnore(`${key}.${id}.${k}.${k1}`)
|
|
223
|
+
} else {
|
|
224
|
+
platform.deviceConf[id][k][k1] = v1
|
|
225
|
+
}
|
|
226
|
+
break
|
|
227
|
+
case 'showAs': {
|
|
228
|
+
if (typeof v1 !== 'string' || !['default', 'switch'].includes(v1)) {
|
|
229
|
+
logIgnore(`${key}.${id}.${k}.${k1}`)
|
|
230
|
+
} else {
|
|
231
|
+
platform.deviceConf[id][k][k1] = v1
|
|
232
|
+
}
|
|
233
|
+
break
|
|
234
|
+
}
|
|
235
|
+
default:
|
|
236
|
+
logIgnore(`${key}.${id}.${k}.${k1}`)
|
|
237
|
+
break
|
|
238
|
+
}
|
|
239
|
+
})
|
|
240
|
+
} else {
|
|
241
|
+
logIgnore(`${key}.${id}.${k}`)
|
|
242
|
+
}
|
|
243
|
+
} else {
|
|
244
|
+
logIgnore(`${key}.${id}.${k}`)
|
|
245
|
+
}
|
|
246
|
+
break
|
|
247
|
+
}
|
|
248
|
+
case 'ignoreDevice':
|
|
249
|
+
if (typeof v === 'string') {
|
|
250
|
+
logQuotes(`${key}.${id}.${k}`)
|
|
251
|
+
}
|
|
252
|
+
if (!!v && v !== 'false') {
|
|
253
|
+
platform.ignoredDevices.push(id)
|
|
254
|
+
}
|
|
255
|
+
break
|
|
256
|
+
case 'label':
|
|
257
|
+
if (typeof v !== 'string' || v === '') {
|
|
258
|
+
logIgnore(`${key}.${id}.${k}`)
|
|
259
|
+
} else {
|
|
260
|
+
platform.deviceConf[id][k] = v
|
|
261
|
+
}
|
|
262
|
+
break
|
|
263
|
+
default:
|
|
264
|
+
logRemove(`${key}.${id}.${k}`)
|
|
265
|
+
}
|
|
266
|
+
})
|
|
267
|
+
})
|
|
268
|
+
} else {
|
|
269
|
+
logIgnore(key)
|
|
270
|
+
}
|
|
271
|
+
break
|
|
272
|
+
case 'name':
|
|
273
|
+
case 'platform':
|
|
274
|
+
break
|
|
275
|
+
default:
|
|
276
|
+
logRemove(key)
|
|
277
|
+
break
|
|
278
|
+
}
|
|
279
|
+
})
|
|
280
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import deviceTypes from '../device/index.js'
|
|
2
|
+
import platformConsts from './constants.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Works out which handler a device should get.
|
|
6
|
+
*
|
|
7
|
+
* This used to be a long chain of `else if` inside the platform, which meant
|
|
8
|
+
* adding a device type involved editing the file that also does config parsing,
|
|
9
|
+
* discovery and connection setup. It is a table now, read top to bottom, so a
|
|
10
|
+
* new type is a row.
|
|
11
|
+
*
|
|
12
|
+
* Only the choice lives here. Creating the accessory stays with the platform,
|
|
13
|
+
* because that talks to Homebridge - so this returns what to build rather than
|
|
14
|
+
* building it, and can be tested on its own.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* How a model list picks its handler.
|
|
19
|
+
*
|
|
20
|
+
* - `handler` one handler for every model in the list
|
|
21
|
+
* - `prefix` look for a handler named after the model, eg `deviceFanH7100`
|
|
22
|
+
* - `fallback` used when `prefix` finds nothing, so a model nobody has worked
|
|
23
|
+
* out still gets power rather than being left unsupported
|
|
24
|
+
* - `choose` for the few lists where the answer depends on the device or the
|
|
25
|
+
* owner's settings
|
|
26
|
+
* - `awsPolling` these devices need their status asking for over AWS
|
|
27
|
+
*/
|
|
28
|
+
const routes = [
|
|
29
|
+
{
|
|
30
|
+
list: 'rgb',
|
|
31
|
+
choose: ({ deviceConf }) => (deviceConf.showAs === 'switch' ? 'deviceLightSwitch' : 'deviceLight'),
|
|
32
|
+
},
|
|
33
|
+
{ list: 'switchSingle', choose: chooseSingleSwitch },
|
|
34
|
+
{
|
|
35
|
+
list: 'switchDouble',
|
|
36
|
+
choose: ({ deviceConf }) => (showAs(deviceConf) === 'switch' ? 'deviceSwitchDouble' : 'deviceOutletDouble'),
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
list: 'switchTriple',
|
|
40
|
+
choose: ({ deviceConf }) => (showAs(deviceConf) === 'switch' ? 'deviceSwitchTriple' : 'deviceOutletTriple'),
|
|
41
|
+
},
|
|
42
|
+
{ list: 'sensorLeak', handler: 'deviceSensorLeak' },
|
|
43
|
+
{ list: 'sensorPresence', handler: 'deviceSensorPresence' },
|
|
44
|
+
{
|
|
45
|
+
list: 'sensorThermo',
|
|
46
|
+
choose: ({ deviceConf }) => (deviceConf.showExtraSwitch ? 'deviceSensorThermoSwitch' : 'deviceSensorThermo'),
|
|
47
|
+
},
|
|
48
|
+
{ list: 'sensorThermo4', handler: 'deviceSensorThermo4' },
|
|
49
|
+
{ list: 'sensorMonitor', handler: 'deviceSensorMonitor', awsPolling: true },
|
|
50
|
+
{ list: 'sensorCO2', handler: 'deviceSensorCO2', awsPolling: true },
|
|
51
|
+
{ list: 'fan', prefix: 'deviceFan', fallback: 'deviceFanBasic' },
|
|
52
|
+
{ list: 'heater1', choose: chooseHeater, awsPolling: true },
|
|
53
|
+
{ list: 'heater2', handler: 'deviceHeater2', awsPolling: true },
|
|
54
|
+
{ list: 'humidifier', prefix: 'deviceHumidifier', fallback: 'deviceHumidifierBasic', awsPolling: true },
|
|
55
|
+
{ list: 'dehumidifier', prefix: 'deviceDehumidifier', fallback: 'deviceDehumidifierBasic', awsPolling: true },
|
|
56
|
+
{ list: 'purifier', prefix: 'devicePurifier', fallback: 'devicePurifierBasic', awsPolling: true },
|
|
57
|
+
{ list: 'diffuser', prefix: 'deviceDiffuser', fallback: 'deviceDiffuserBasic', awsPolling: true },
|
|
58
|
+
{ list: 'sensorButton', handler: 'deviceSensorButton' },
|
|
59
|
+
{ list: 'sensorContact', handler: 'deviceSensorContact' },
|
|
60
|
+
{ list: 'kettle', handler: 'deviceKettle' },
|
|
61
|
+
{ list: 'iceMaker', prefix: 'deviceIceMaker', fallback: 'deviceIceMakerBasic' },
|
|
62
|
+
{ list: 'template', handler: 'deviceTemplate' },
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
function showAs(deviceConf) {
|
|
66
|
+
return deviceConf.showAs || platformConsts.defaultValues.showAs
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* A single socket can be shown as all sorts of things, because people use them
|
|
71
|
+
* for all sorts of things. Three of those are televisions, which Homebridge has
|
|
72
|
+
* to publish as their own accessory rather than as a tile on the bridge.
|
|
73
|
+
*/
|
|
74
|
+
function chooseSingleSwitch({ deviceConf }) {
|
|
75
|
+
switch (showAs(deviceConf)) {
|
|
76
|
+
case 'audio':
|
|
77
|
+
return { handler: 'deviceTVSingle', externalCategory: 34 }
|
|
78
|
+
case 'box':
|
|
79
|
+
return { handler: 'deviceTVSingle', externalCategory: 35 }
|
|
80
|
+
case 'stick':
|
|
81
|
+
return { handler: 'deviceTVSingle', externalCategory: 36 }
|
|
82
|
+
case 'cooler':
|
|
83
|
+
// Standing in for a cooler means reading a temperature from somewhere
|
|
84
|
+
// else, and without that there is nothing to show
|
|
85
|
+
return deviceConf.temperatureSource
|
|
86
|
+
? { handler: 'deviceCoolerSingle' }
|
|
87
|
+
: { skip: 'needsTemperatureSource' }
|
|
88
|
+
case 'heater':
|
|
89
|
+
return deviceConf.temperatureSource
|
|
90
|
+
? { handler: 'deviceHeater2Single' }
|
|
91
|
+
: { skip: 'needsTemperatureSource' }
|
|
92
|
+
case 'purifier':
|
|
93
|
+
return 'devicePurifierSingle'
|
|
94
|
+
case 'switch':
|
|
95
|
+
return 'deviceSwitchSingle'
|
|
96
|
+
case 'tap':
|
|
97
|
+
return 'deviceTapSingle'
|
|
98
|
+
case 'valve':
|
|
99
|
+
return 'deviceValveSingle'
|
|
100
|
+
default:
|
|
101
|
+
return 'deviceOutletSingle'
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Some of these heaters report the room temperature and some do not, and the
|
|
107
|
+
* two need different tiles - a plain fan against a heater with a temperature
|
|
108
|
+
* readout. Rather than leave the owner to work that out and tick a box, take
|
|
109
|
+
* Govee's own word for it where the device says so, and otherwise use what a
|
|
110
|
+
* previous run learnt from the readings themselves. An owner who has already
|
|
111
|
+
* set the option still wins, so nobody's tile changes under them.
|
|
112
|
+
*/
|
|
113
|
+
function chooseHeater({ device, deviceConf, cachedContext }) {
|
|
114
|
+
const reportsTemperature = device.openApiInfo?.byInstance?.sensorTemperature
|
|
115
|
+
? true
|
|
116
|
+
: cachedContext?.supportsAmbientTemp
|
|
117
|
+
return (deviceConf.tempReporting ?? reportsTemperature)
|
|
118
|
+
? 'deviceHeater1B'
|
|
119
|
+
: 'deviceHeater1A'
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function normalise(answer) {
|
|
123
|
+
return typeof answer === 'string' ? { handler: answer } : answer
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Decide what to build for a device.
|
|
128
|
+
*
|
|
129
|
+
* @param {object} device the device as Govee describes it
|
|
130
|
+
* @param {object} [deviceConf] this device's settings
|
|
131
|
+
* @param {object} [cachedContext] the context of the accessory from a previous
|
|
132
|
+
* run, where there is one - some choices depend on what was learnt before
|
|
133
|
+
* @returns {object|null} `{ handler, handlerKey, awsPolling, externalCategory,
|
|
134
|
+
* skip }`, or null when the model is not one we handle
|
|
135
|
+
*/
|
|
136
|
+
export function resolveDeviceType(device, deviceConf = {}, cachedContext = undefined) {
|
|
137
|
+
const route = routes.find(entry => platformConsts.models[entry.list]?.includes(device.model))
|
|
138
|
+
if (!route) {
|
|
139
|
+
return null
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const chosen = route.choose
|
|
143
|
+
? normalise(route.choose({ device, deviceConf, cachedContext }))
|
|
144
|
+
: { handler: route.handler ?? (deviceTypes[`${route.prefix}${device.model}`] ? `${route.prefix}${device.model}` : route.fallback) }
|
|
145
|
+
|
|
146
|
+
if (chosen.skip) {
|
|
147
|
+
return { skip: chosen.skip, list: route.list }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
list: route.list,
|
|
152
|
+
handlerKey: chosen.handler,
|
|
153
|
+
handler: deviceTypes[chosen.handler],
|
|
154
|
+
awsPolling: !!route.awsPolling,
|
|
155
|
+
externalCategory: chosen.externalCategory,
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export { routes }
|
package/package.json
CHANGED