@homebridge-plugins/homebridge-govee 10.12.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 +1937 -0
- package/LICENSE +21 -0
- package/README.md +72 -0
- package/config.schema.json +1727 -0
- package/eslint.config.js +49 -0
- package/lib/connection/aws.js +174 -0
- package/lib/connection/ble.js +208 -0
- package/lib/connection/cert/AmazonRootCA1.pem +20 -0
- package/lib/connection/http.js +240 -0
- package/lib/connection/lan.js +284 -0
- package/lib/device/cooler-single.js +300 -0
- package/lib/device/dehumidifier-H7150.js +182 -0
- package/lib/device/dehumidifier-H7151.js +157 -0
- package/lib/device/diffuser-H7161.js +117 -0
- package/lib/device/diffuser-H7162.js +117 -0
- package/lib/device/fan-H7100.js +274 -0
- package/lib/device/fan-H7101.js +330 -0
- package/lib/device/fan-H7102.js +274 -0
- package/lib/device/fan-H7105.js +503 -0
- package/lib/device/fan-H7106.js +274 -0
- package/lib/device/fan-H7111.js +335 -0
- package/lib/device/heater-single.js +300 -0
- package/lib/device/heater1a.js +353 -0
- package/lib/device/heater1b.js +616 -0
- package/lib/device/heater2.js +838 -0
- package/lib/device/humidifier-H7140.js +224 -0
- package/lib/device/humidifier-H7141.js +257 -0
- package/lib/device/humidifier-H7142.js +522 -0
- package/lib/device/humidifier-H7143.js +157 -0
- package/lib/device/humidifier-H7148.js +157 -0
- package/lib/device/humidifier-H7160.js +446 -0
- package/lib/device/ice-maker-H7162.js +46 -0
- package/lib/device/index.js +105 -0
- package/lib/device/kettle.js +269 -0
- package/lib/device/light-switch.js +86 -0
- package/lib/device/light.js +617 -0
- package/lib/device/outlet-double.js +121 -0
- package/lib/device/outlet-single.js +172 -0
- package/lib/device/outlet-triple.js +160 -0
- package/lib/device/purifier-H7120.js +336 -0
- package/lib/device/purifier-H7121.js +336 -0
- package/lib/device/purifier-H7122.js +449 -0
- package/lib/device/purifier-H7123.js +411 -0
- package/lib/device/purifier-H7124.js +411 -0
- package/lib/device/purifier-H7126.js +296 -0
- package/lib/device/purifier-H7127.js +296 -0
- package/lib/device/purifier-H712C.js +296 -0
- package/lib/device/purifier-single.js +119 -0
- package/lib/device/sensor-button.js +22 -0
- package/lib/device/sensor-contact.js +22 -0
- package/lib/device/sensor-leak.js +87 -0
- package/lib/device/sensor-monitor.js +190 -0
- package/lib/device/sensor-presence.js +53 -0
- package/lib/device/sensor-thermo.js +144 -0
- package/lib/device/sensor-thermo4.js +55 -0
- package/lib/device/switch-double.js +121 -0
- package/lib/device/switch-single.js +95 -0
- package/lib/device/switch-triple.js +160 -0
- package/lib/device/tap-single.js +108 -0
- package/lib/device/template.js +43 -0
- package/lib/device/tv-single.js +84 -0
- package/lib/device/valve-single.js +155 -0
- package/lib/fakegato/LICENSE +21 -0
- package/lib/fakegato/fakegato-history.js +814 -0
- package/lib/fakegato/fakegato-storage.js +108 -0
- package/lib/fakegato/fakegato-timer.js +125 -0
- package/lib/fakegato/uuid.js +27 -0
- package/lib/homebridge-ui/public/index.html +433 -0
- package/lib/homebridge-ui/server.js +10 -0
- package/lib/index.js +8 -0
- package/lib/platform.js +1967 -0
- package/lib/utils/colour.js +564 -0
- package/lib/utils/constants.js +579 -0
- package/lib/utils/custom-chars.js +225 -0
- package/lib/utils/eve-chars.js +68 -0
- package/lib/utils/functions.js +117 -0
- package/lib/utils/lang-en.js +131 -0
- package/package.json +75 -0
package/lib/platform.js
ADDED
|
@@ -0,0 +1,1967 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer'
|
|
2
|
+
import { existsSync, mkdirSync, promises } from 'node:fs'
|
|
3
|
+
import { createRequire } from 'node:module'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import process from 'node:process'
|
|
6
|
+
|
|
7
|
+
import storage from 'node-persist'
|
|
8
|
+
import PQueue from 'p-queue'
|
|
9
|
+
|
|
10
|
+
import awsClient from './connection/aws.js'
|
|
11
|
+
import httpClient from './connection/http.js'
|
|
12
|
+
import lanClient from './connection/lan.js'
|
|
13
|
+
import deviceTypes from './device/index.js'
|
|
14
|
+
import eveService from './fakegato/fakegato-history.js'
|
|
15
|
+
import { k2rgb } from './utils/colour.js'
|
|
16
|
+
import platformConsts from './utils/constants.js'
|
|
17
|
+
import platformChars from './utils/custom-chars.js'
|
|
18
|
+
import eveChars from './utils/eve-chars.js'
|
|
19
|
+
import {
|
|
20
|
+
base64ToHex,
|
|
21
|
+
hasProperty,
|
|
22
|
+
parseDeviceId,
|
|
23
|
+
parseError,
|
|
24
|
+
pfxToCertAndKey,
|
|
25
|
+
} from './utils/functions.js'
|
|
26
|
+
import platformLang from './utils/lang-en.js'
|
|
27
|
+
|
|
28
|
+
const require = createRequire(import.meta.url)
|
|
29
|
+
const plugin = require('../package.json')
|
|
30
|
+
|
|
31
|
+
const devicesInHB = new Map()
|
|
32
|
+
const awsDevices = []
|
|
33
|
+
const awsDevicesToPoll = []
|
|
34
|
+
const httpDevices = []
|
|
35
|
+
const lanDevices = []
|
|
36
|
+
|
|
37
|
+
export default class {
|
|
38
|
+
constructor(log, config, api) {
|
|
39
|
+
if (!log || !api) {
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Begin plugin initialisation
|
|
44
|
+
try {
|
|
45
|
+
this.api = api
|
|
46
|
+
this.log = log
|
|
47
|
+
this.isBeta = plugin.version.includes('beta')
|
|
48
|
+
|
|
49
|
+
// Configuration objects for accessories
|
|
50
|
+
this.deviceConf = {}
|
|
51
|
+
this.ignoredDevices = []
|
|
52
|
+
|
|
53
|
+
// Make sure user is running Homebridge v1.5 or above
|
|
54
|
+
if (!api.versionGreaterOrEqual?.('1.5.0')) {
|
|
55
|
+
throw new Error(platformLang.hbVersionFail)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Check the user has configured the plugin
|
|
59
|
+
if (!config) {
|
|
60
|
+
throw new Error(platformLang.pluginNotConf)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Log some environment info for debugging
|
|
64
|
+
this.log(
|
|
65
|
+
'%s v%s | System %s | Node %s | HB v%s | HAPNodeJS v%s...',
|
|
66
|
+
platformLang.initialising,
|
|
67
|
+
plugin.version,
|
|
68
|
+
process.platform,
|
|
69
|
+
process.version,
|
|
70
|
+
api.serverVersion,
|
|
71
|
+
api.hap.HAPLibraryVersion(),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
// Apply the user's configuration
|
|
75
|
+
this.config = platformConsts.defaultConfig
|
|
76
|
+
this.applyUserConfig(config)
|
|
77
|
+
|
|
78
|
+
// Set up empty clients
|
|
79
|
+
this.bleClient = false
|
|
80
|
+
this.httpClient = false
|
|
81
|
+
this.lanClient = false
|
|
82
|
+
|
|
83
|
+
// Set up the Homebridge events
|
|
84
|
+
this.api.on('didFinishLaunching', () => this.pluginSetup())
|
|
85
|
+
this.api.on('shutdown', () => this.pluginShutdown())
|
|
86
|
+
} catch (err) {
|
|
87
|
+
// Catch any errors during initialisation
|
|
88
|
+
log.warn('***** %s [v%s]. *****', platformLang.disabling, plugin.version)
|
|
89
|
+
log.warn('***** %s. *****', parseError(err, [platformLang.hbVersionFail, platformLang.pluginNotConf]))
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
applyUserConfig(config) {
|
|
94
|
+
// These shorthand functions save line space during config parsing
|
|
95
|
+
const logDefault = (k, def) => {
|
|
96
|
+
this.log.warn('%s [%s] %s %s.', platformLang.cfgItem, k, platformLang.cfgDef, def)
|
|
97
|
+
}
|
|
98
|
+
const logDuplicate = (k) => {
|
|
99
|
+
this.log.warn('%s [%s] %s.', platformLang.cfgItem, k, platformLang.cfgDup)
|
|
100
|
+
}
|
|
101
|
+
const logIgnore = (k) => {
|
|
102
|
+
this.log.warn('%s [%s] %s.', platformLang.cfgItem, k, platformLang.cfgIgn)
|
|
103
|
+
}
|
|
104
|
+
const logIgnoreItem = (k) => {
|
|
105
|
+
this.log.warn('%s [%s] %s.', platformLang.cfgItem, k, platformLang.cfgIgnItem)
|
|
106
|
+
}
|
|
107
|
+
const logIncrease = (k, min) => {
|
|
108
|
+
this.log.warn('%s [%s] %s %s.', platformLang.cfgItem, k, platformLang.cfgLow, min)
|
|
109
|
+
}
|
|
110
|
+
const logQuotes = (k) => {
|
|
111
|
+
this.log.warn('%s [%s] %s.', platformLang.cfgItem, k, platformLang.cfgQts)
|
|
112
|
+
}
|
|
113
|
+
const logRemove = (k) => {
|
|
114
|
+
this.log.warn('%s [%s] %s.', platformLang.cfgItem, k, platformLang.cfgRmv)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Begin applying the user's config
|
|
118
|
+
Object.entries(config).forEach((entry) => {
|
|
119
|
+
const [key, val] = entry
|
|
120
|
+
switch (key) {
|
|
121
|
+
case 'bleControlInterval':
|
|
122
|
+
case 'bleRefreshTime':
|
|
123
|
+
case 'httpRefreshTime':
|
|
124
|
+
case 'lanRefreshTime':
|
|
125
|
+
case 'lanScanInterval': {
|
|
126
|
+
if (typeof val === 'string') {
|
|
127
|
+
logQuotes(key)
|
|
128
|
+
}
|
|
129
|
+
const intVal = Number.parseInt(val, 10)
|
|
130
|
+
if (Number.isNaN(intVal)) {
|
|
131
|
+
logDefault(key, platformConsts.defaultValues[key])
|
|
132
|
+
this.config[key] = platformConsts.defaultValues[key]
|
|
133
|
+
} else if (intVal < platformConsts.minValues[key]) {
|
|
134
|
+
logIncrease(key, platformConsts.minValues[key])
|
|
135
|
+
this.config[key] = platformConsts.minValues[key]
|
|
136
|
+
} else {
|
|
137
|
+
this.config[key] = intVal
|
|
138
|
+
}
|
|
139
|
+
break
|
|
140
|
+
}
|
|
141
|
+
case 'awsDisable':
|
|
142
|
+
case 'bleDisable':
|
|
143
|
+
case 'colourSafeMode':
|
|
144
|
+
case 'disableDeviceLogging':
|
|
145
|
+
case 'lanDisable':
|
|
146
|
+
if (typeof val === 'string') {
|
|
147
|
+
logQuotes(key)
|
|
148
|
+
}
|
|
149
|
+
this.config[key] = val === 'false' ? false : !!val
|
|
150
|
+
break
|
|
151
|
+
case 'dehumidifierDevices':
|
|
152
|
+
case 'fanDevices':
|
|
153
|
+
case 'heaterDevices':
|
|
154
|
+
case 'humidifierDevices':
|
|
155
|
+
case 'iceMakerDevices':
|
|
156
|
+
case 'kettleDevices':
|
|
157
|
+
case 'leakDevices':
|
|
158
|
+
case 'lightDevices':
|
|
159
|
+
case 'purifierDevices':
|
|
160
|
+
case 'diffuserDevices':
|
|
161
|
+
case 'switchDevices':
|
|
162
|
+
case 'thermoDevices':
|
|
163
|
+
if (Array.isArray(val) && val.length > 0) {
|
|
164
|
+
val.forEach((x) => {
|
|
165
|
+
if (!x.deviceId) {
|
|
166
|
+
logIgnoreItem(key)
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
const id = parseDeviceId(x.deviceId)
|
|
170
|
+
if (Object.keys(this.deviceConf).includes(id)) {
|
|
171
|
+
logDuplicate(`${key}.${id}`)
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
const entries = Object.entries(x)
|
|
175
|
+
if (entries.length === 1) {
|
|
176
|
+
logRemove(`${key}.${id}`)
|
|
177
|
+
return
|
|
178
|
+
}
|
|
179
|
+
this.deviceConf[id] = {}
|
|
180
|
+
entries.forEach((subEntry) => {
|
|
181
|
+
const [k, v] = subEntry
|
|
182
|
+
switch (k) {
|
|
183
|
+
case 'adaptiveLightingShift':
|
|
184
|
+
case 'brightnessStep':
|
|
185
|
+
case 'lowBattThreshold': {
|
|
186
|
+
if (typeof v === 'string') {
|
|
187
|
+
logQuotes(`${key}.${k}`)
|
|
188
|
+
}
|
|
189
|
+
const intVal = Number.parseInt(v, 10)
|
|
190
|
+
if (Number.isNaN(intVal)) {
|
|
191
|
+
logDefault(`${key}.${id}.${k}`, platformConsts.defaultValues[k])
|
|
192
|
+
this.deviceConf[id][k] = platformConsts.defaultValues[k]
|
|
193
|
+
} else if (intVal < platformConsts.minValues[k]) {
|
|
194
|
+
logIncrease(`${key}.${id}.${k}`, platformConsts.minValues[k])
|
|
195
|
+
this.deviceConf[id][k] = platformConsts.minValues[k]
|
|
196
|
+
} else {
|
|
197
|
+
this.deviceConf[id][k] = intVal
|
|
198
|
+
}
|
|
199
|
+
break
|
|
200
|
+
}
|
|
201
|
+
case 'awsBrightnessNoScale':
|
|
202
|
+
case 'hideModeGreenTea':
|
|
203
|
+
case 'hideModeOolongTea':
|
|
204
|
+
case 'hideModeCoffee':
|
|
205
|
+
case 'hideModeBlackTea':
|
|
206
|
+
case 'showCustomMode1':
|
|
207
|
+
case 'showCustomMode2':
|
|
208
|
+
case 'tempReporting':
|
|
209
|
+
if (typeof v === 'string') {
|
|
210
|
+
logQuotes(`${key}.${id}.${k}`)
|
|
211
|
+
}
|
|
212
|
+
this.deviceConf[id][k] = v === 'false' ? false : !!v
|
|
213
|
+
break
|
|
214
|
+
case 'awsColourMode':
|
|
215
|
+
case 'showAs': {
|
|
216
|
+
if (typeof v !== 'string' || !platformConsts.allowed[k].includes(v)) {
|
|
217
|
+
logIgnore(`${key}.${id}.${k}`)
|
|
218
|
+
} else {
|
|
219
|
+
this.deviceConf[id][k] = v
|
|
220
|
+
}
|
|
221
|
+
break
|
|
222
|
+
}
|
|
223
|
+
case 'customAddress':
|
|
224
|
+
case 'customIPAddress':
|
|
225
|
+
if (typeof v !== 'string' || v === '') {
|
|
226
|
+
logIgnore(`${key}.${id}.${k}`)
|
|
227
|
+
} else {
|
|
228
|
+
this.deviceConf[id][k] = v.replace(/\s+/g, '')
|
|
229
|
+
}
|
|
230
|
+
break
|
|
231
|
+
case 'deviceId':
|
|
232
|
+
break
|
|
233
|
+
case 'diyMode':
|
|
234
|
+
case 'diyModeTwo':
|
|
235
|
+
case 'diyModeThree':
|
|
236
|
+
case 'diyModeFour':
|
|
237
|
+
case 'musicMode':
|
|
238
|
+
case 'musicModeTwo':
|
|
239
|
+
case 'scene':
|
|
240
|
+
case 'sceneTwo':
|
|
241
|
+
case 'sceneThree':
|
|
242
|
+
case 'sceneFour':
|
|
243
|
+
case 'segmented':
|
|
244
|
+
case 'segmentedTwo':
|
|
245
|
+
case 'segmentedThree':
|
|
246
|
+
case 'segmentedFour':
|
|
247
|
+
case 'temperatureSource':
|
|
248
|
+
case 'videoMode':
|
|
249
|
+
case 'videoModeTwo': {
|
|
250
|
+
if (typeof v === 'string') {
|
|
251
|
+
this.log.warn(`${key}.${id}.${k} incorrectly configured - please use the config screen to reconfigure this item:`)
|
|
252
|
+
this.log.warn(`${key}.${id}.${k}: ${v}`)
|
|
253
|
+
}
|
|
254
|
+
if (typeof v === 'object') {
|
|
255
|
+
// object - only allowed keys are 'sceneCode', 'bleCode' and 'showAs'
|
|
256
|
+
const subEntries = Object.entries(v)
|
|
257
|
+
if (subEntries.length > 0) {
|
|
258
|
+
this.deviceConf[id][k] = {}
|
|
259
|
+
subEntries.forEach((subSubEntry) => {
|
|
260
|
+
const [k1, v1] = subSubEntry
|
|
261
|
+
switch (k1) {
|
|
262
|
+
case 'bleCode':
|
|
263
|
+
case 'sceneCode':
|
|
264
|
+
if (typeof v1 !== 'string' || v1 === '') {
|
|
265
|
+
logIgnore(`${key}.${id}.${k}.${k1}`)
|
|
266
|
+
} else {
|
|
267
|
+
this.deviceConf[id][k][k1] = v1
|
|
268
|
+
}
|
|
269
|
+
break
|
|
270
|
+
case 'showAs': {
|
|
271
|
+
if (typeof v1 !== 'string' || !['default', 'switch'].includes(v1)) {
|
|
272
|
+
logIgnore(`${key}.${id}.${k}.${k1}`)
|
|
273
|
+
} else {
|
|
274
|
+
this.deviceConf[id][k][k1] = v1
|
|
275
|
+
}
|
|
276
|
+
break
|
|
277
|
+
}
|
|
278
|
+
default:
|
|
279
|
+
logIgnore(`${key}.${id}.${k}.${k1}`)
|
|
280
|
+
break
|
|
281
|
+
}
|
|
282
|
+
})
|
|
283
|
+
} else {
|
|
284
|
+
logIgnore(`${key}.${id}.${k}`)
|
|
285
|
+
}
|
|
286
|
+
} else {
|
|
287
|
+
logIgnore(`${key}.${id}.${k}`)
|
|
288
|
+
}
|
|
289
|
+
break
|
|
290
|
+
}
|
|
291
|
+
case 'ignoreDevice':
|
|
292
|
+
if (typeof v === 'string') {
|
|
293
|
+
logQuotes(`${key}.${id}.${k}`)
|
|
294
|
+
}
|
|
295
|
+
if (!!v && v !== 'false') {
|
|
296
|
+
this.ignoredDevices.push(id)
|
|
297
|
+
}
|
|
298
|
+
break
|
|
299
|
+
case 'label':
|
|
300
|
+
if (typeof v !== 'string' || v === '') {
|
|
301
|
+
logIgnore(`${key}.${id}.${k}`)
|
|
302
|
+
} else {
|
|
303
|
+
this.deviceConf[id][k] = v
|
|
304
|
+
}
|
|
305
|
+
break
|
|
306
|
+
default:
|
|
307
|
+
logRemove(`${key}.${id}.${k}`)
|
|
308
|
+
}
|
|
309
|
+
})
|
|
310
|
+
})
|
|
311
|
+
} else {
|
|
312
|
+
logIgnore(key)
|
|
313
|
+
}
|
|
314
|
+
break
|
|
315
|
+
case 'name':
|
|
316
|
+
case 'platform':
|
|
317
|
+
break
|
|
318
|
+
case 'password':
|
|
319
|
+
case 'username':
|
|
320
|
+
if (typeof val !== 'string' || val === '') {
|
|
321
|
+
logIgnore(key)
|
|
322
|
+
} else {
|
|
323
|
+
this.config[key] = val
|
|
324
|
+
}
|
|
325
|
+
break
|
|
326
|
+
default:
|
|
327
|
+
logRemove(key)
|
|
328
|
+
break
|
|
329
|
+
}
|
|
330
|
+
})
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async pluginSetup() {
|
|
334
|
+
// Plugin has finished initialising so now onto setup
|
|
335
|
+
try {
|
|
336
|
+
// Log that the plugin initialisation has been successful
|
|
337
|
+
this.log('%s.', platformLang.initialised)
|
|
338
|
+
|
|
339
|
+
// Sort out some logging functions
|
|
340
|
+
if (this.isBeta) {
|
|
341
|
+
this.log.debug = this.log
|
|
342
|
+
this.log.debugWarn = this.log.warn
|
|
343
|
+
|
|
344
|
+
// Log that using a beta will generate a lot of debug logs
|
|
345
|
+
if (this.isBeta) {
|
|
346
|
+
const divide = '*'.repeat(platformLang.beta.length + 1) // don't forget the full stop (+1!)
|
|
347
|
+
this.log.warn(divide)
|
|
348
|
+
this.log.warn(`${platformLang.beta}.`)
|
|
349
|
+
this.log.warn(divide)
|
|
350
|
+
}
|
|
351
|
+
} else {
|
|
352
|
+
this.log.debug = () => {}
|
|
353
|
+
this.log.debugWarn = () => {}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Require any libraries that the plugin uses
|
|
357
|
+
this.cusChar = new platformChars(this.api)
|
|
358
|
+
this.eveChar = new eveChars(this.api)
|
|
359
|
+
this.eveService = eveService(this.api)
|
|
360
|
+
|
|
361
|
+
const cachePath = join(this.api.user.storagePath(), '/bwp91_cache')
|
|
362
|
+
const persistPath = join(this.api.user.storagePath(), '/persist')
|
|
363
|
+
|
|
364
|
+
// Create folders if they don't exist
|
|
365
|
+
if (!existsSync(cachePath)) {
|
|
366
|
+
mkdirSync(cachePath)
|
|
367
|
+
}
|
|
368
|
+
if (!existsSync(persistPath)) {
|
|
369
|
+
mkdirSync(persistPath)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Persist files are used to store device info that can be used by my other plugins
|
|
373
|
+
try {
|
|
374
|
+
this.storageData = storage.create({
|
|
375
|
+
dir: cachePath,
|
|
376
|
+
forgiveParseErrors: true,
|
|
377
|
+
})
|
|
378
|
+
await this.storageData.init()
|
|
379
|
+
this.storageClientData = true
|
|
380
|
+
} catch (err) {
|
|
381
|
+
this.log.debugWarn('%s %s.', platformLang.storageSetupErr, parseError(err))
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Set up the LAN client and perform an initial scan for devices
|
|
385
|
+
try {
|
|
386
|
+
if (this.config.lanDisable) {
|
|
387
|
+
throw new Error(platformLang.disabledInConfig)
|
|
388
|
+
}
|
|
389
|
+
this.lanClient = new lanClient(this)
|
|
390
|
+
const devices = await this.lanClient.getDevices()
|
|
391
|
+
devices.forEach(device => lanDevices.push(device))
|
|
392
|
+
this.log('[LAN] %s.', platformLang.availableWithDevices(devices.length))
|
|
393
|
+
} catch (err) {
|
|
394
|
+
this.log.warn('[LAN] %s %s.', platformLang.disableClient, parseError(err, [
|
|
395
|
+
platformLang.disabledInConfig,
|
|
396
|
+
]))
|
|
397
|
+
this.lanClient = false
|
|
398
|
+
Object.keys(this.deviceConf).forEach((id) => {
|
|
399
|
+
delete this.deviceConf[id].customIPAddress
|
|
400
|
+
})
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Set up the HTTP client if Govee username and password have been provided
|
|
404
|
+
try {
|
|
405
|
+
if (!this.config.username || !this.config.password) {
|
|
406
|
+
throw new Error(platformLang.noCreds)
|
|
407
|
+
}
|
|
408
|
+
const iotFile = join(persistPath, 'govee.pfx')
|
|
409
|
+
|
|
410
|
+
const getDevices = async () => {
|
|
411
|
+
const devices = await this.httpClient.getDevices()
|
|
412
|
+
devices.forEach(device => httpDevices.push(device))
|
|
413
|
+
this.log('[HTTP] %s.', platformLang.availableWithDevices(devices.length))
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Try and get access token from the cache to get a device list
|
|
417
|
+
try {
|
|
418
|
+
const storedData = await this.storageData.getItem('Govee_All_Devices_temp')
|
|
419
|
+
const splitData = storedData?.split(':::')
|
|
420
|
+
if (!Array.isArray(splitData) || splitData.length !== 7) {
|
|
421
|
+
throw new Error(platformLang.accTokenNoExist)
|
|
422
|
+
}
|
|
423
|
+
if (splitData[2] !== this.config.username) {
|
|
424
|
+
// Username has changed so throw error to generate new token
|
|
425
|
+
throw new Error(platformLang.accTokenUserChange)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
try {
|
|
429
|
+
await promises.access(iotFile, 0)
|
|
430
|
+
} catch (err) {
|
|
431
|
+
throw new Error(platformLang.iotFileNoExist)
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
[
|
|
435
|
+
this.accountTopic,
|
|
436
|
+
this.accountToken,,
|
|
437
|
+
this.accountId,
|
|
438
|
+
this.iotEndpoint,
|
|
439
|
+
this.iotPass,
|
|
440
|
+
this.accountTokenTTR,
|
|
441
|
+
] = splitData
|
|
442
|
+
|
|
443
|
+
this.log.debug('[HTTP] %s.', platformLang.accTokenFromCache)
|
|
444
|
+
|
|
445
|
+
this.httpClient = new httpClient(this)
|
|
446
|
+
await getDevices()
|
|
447
|
+
} catch (err) {
|
|
448
|
+
this.log.warn('[HTTP] %s %s.', platformLang.accTokenFail, parseError(err, [
|
|
449
|
+
platformLang.accTokenUserChange,
|
|
450
|
+
platformLang.accTokenNoExist,
|
|
451
|
+
platformLang.iotFileNoExist,
|
|
452
|
+
]))
|
|
453
|
+
|
|
454
|
+
this.httpClient = new httpClient(this)
|
|
455
|
+
const data = await this.httpClient.login()
|
|
456
|
+
|
|
457
|
+
this.accountId = data.accountId
|
|
458
|
+
this.accountTopic = data.topic
|
|
459
|
+
const accountToken = data.token
|
|
460
|
+
const accountTokenTTR = data.tokenTTR
|
|
461
|
+
this.clientId = data.client
|
|
462
|
+
this.iotEndpoint = data.endpoint
|
|
463
|
+
this.iotPass = data.iotPass
|
|
464
|
+
|
|
465
|
+
// Save this to a file
|
|
466
|
+
await promises.writeFile(iotFile, Buffer.from(data.iot, 'base64'))
|
|
467
|
+
|
|
468
|
+
// Try and save these to the cache for future reference
|
|
469
|
+
try {
|
|
470
|
+
await this.storageData.setItem(
|
|
471
|
+
'Govee_All_Devices_temp',
|
|
472
|
+
`${this.accountTopic}:::${accountToken}:::${this.config.username}:::${this.accountId}:::${this.iotEndpoint}:::${this.iotPass}:::${accountTokenTTR}`,
|
|
473
|
+
)
|
|
474
|
+
} catch (e) {
|
|
475
|
+
this.log.warn('[HTTP] %s %s.', platformLang.accTokenStoreErr, parseError(e))
|
|
476
|
+
}
|
|
477
|
+
await getDevices()
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const iotFileData = await pfxToCertAndKey(iotFile, this.iotPass)
|
|
481
|
+
if (this.config.awsDisable) {
|
|
482
|
+
this.log.warn('[AWS] %s %s.', platformLang.disableClient, platformLang.disabledInConfig)
|
|
483
|
+
} else {
|
|
484
|
+
this.awsClient = new awsClient(this, iotFileData)
|
|
485
|
+
this.log('[AWS] %s.', platformLang.available)
|
|
486
|
+
}
|
|
487
|
+
} catch (err) {
|
|
488
|
+
if (err.message.includes('abnormal')) {
|
|
489
|
+
err.message = platformLang.abnormalMessage
|
|
490
|
+
}
|
|
491
|
+
this.log.warn('[HTTP] %s %s.', platformLang.disableClient, parseError(err, [
|
|
492
|
+
platformLang.abnormalMessage,
|
|
493
|
+
platformLang.noCreds,
|
|
494
|
+
]))
|
|
495
|
+
if (err.message.includes('Could not find openssl')) {
|
|
496
|
+
this.log.warn(platformLang.noOpenssl)
|
|
497
|
+
}
|
|
498
|
+
this.log.warn('[AWS] %s %s.', platformLang.disableClient, platformLang.needHTTPClient)
|
|
499
|
+
this.httpClient = false
|
|
500
|
+
this.awsClient = false
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Set up the BLE client, if enabled
|
|
504
|
+
try {
|
|
505
|
+
if (this.config.bleDisable) {
|
|
506
|
+
throw new Error(platformLang.disabledInConfig)
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const thisPlatform = process.platform
|
|
510
|
+
|
|
511
|
+
// Bluetooth not supported on Mac
|
|
512
|
+
if (thisPlatform === 'darwin') {
|
|
513
|
+
throw new Error(platformLang.bleMacNoSupp)
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// See if the bluetooth client is available
|
|
517
|
+
/*
|
|
518
|
+
Noble sends the plugin into a crash loop if there is no bluetooth adapter available
|
|
519
|
+
This if statement follows the logic of Noble up to the offending socket.bindRaw(device)
|
|
520
|
+
Put inside a try/catch now to check for error and disable ble control for rest of plugin
|
|
521
|
+
*/
|
|
522
|
+
if (['linux', 'freebsd', 'win32'].includes(thisPlatform)) {
|
|
523
|
+
const { default: BluetoothHciSocket } = await import('@abandonware/bluetooth-hci-socket')
|
|
524
|
+
const socket = new BluetoothHciSocket()
|
|
525
|
+
const device = process.env.NOBLE_HCI_DEVICE_ID
|
|
526
|
+
? Number.parseInt(process.env.NOBLE_HCI_DEVICE_ID, 10)
|
|
527
|
+
: undefined
|
|
528
|
+
socket.bindRaw(device)
|
|
529
|
+
}
|
|
530
|
+
try {
|
|
531
|
+
await import('@abandonware/noble')
|
|
532
|
+
} catch (err) {
|
|
533
|
+
throw new Error(platformLang.bleNoPackage)
|
|
534
|
+
}
|
|
535
|
+
const { default: bleClient } = await import('./connection/ble.js')
|
|
536
|
+
this.bleClient = new bleClient(this)
|
|
537
|
+
this.log('[BLE] %s.', platformLang.available)
|
|
538
|
+
} catch (err) {
|
|
539
|
+
// This error thrown from bluetooth-hci-socket does not contain an 'err.message'
|
|
540
|
+
if (err.code === 'ERR_DLOPEN_FAILED') {
|
|
541
|
+
err.message = 'ERR_DLOPEN_FAILED'
|
|
542
|
+
}
|
|
543
|
+
this.log.warn('[BLE] %s %s.', platformLang.disableClient, parseError(err, [
|
|
544
|
+
platformLang.bleMacNoSupp,
|
|
545
|
+
platformLang.bleNoPackage,
|
|
546
|
+
platformLang.disabledInConfig,
|
|
547
|
+
'ENODEV, No such device',
|
|
548
|
+
'ERR_DLOPEN_FAILED',
|
|
549
|
+
]))
|
|
550
|
+
this.bleClient = false
|
|
551
|
+
Object.keys(this.deviceConf).forEach((id) => {
|
|
552
|
+
delete this.deviceConf[id].customAddress
|
|
553
|
+
})
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Config changed from milliseconds to seconds, so convert if needed
|
|
557
|
+
this.config.bleControlInterval = this.config.bleControlInterval >= 500
|
|
558
|
+
? this.config.bleControlInterval / 1000
|
|
559
|
+
: this.config.bleControlInterval
|
|
560
|
+
|
|
561
|
+
this.queue = new PQueue({
|
|
562
|
+
concurrency: 1,
|
|
563
|
+
interval: this.config.bleControlInterval * 1000,
|
|
564
|
+
intervalCap: 1,
|
|
565
|
+
timeout: 10000,
|
|
566
|
+
throwOnTimeout: true,
|
|
567
|
+
})
|
|
568
|
+
|
|
569
|
+
// Initialise the devices
|
|
570
|
+
let bleSyncNeeded = false
|
|
571
|
+
let httpSyncNeeded = false
|
|
572
|
+
let lanDevicesWereInitialised = false
|
|
573
|
+
let httpDevicesWereInitialised = false
|
|
574
|
+
|
|
575
|
+
if (httpDevices && httpDevices.length > 0) {
|
|
576
|
+
// We have some devices from HTTP client
|
|
577
|
+
httpDevices.forEach((httpDevice) => {
|
|
578
|
+
// Format device id
|
|
579
|
+
if (!httpDevice.device.includes(':')) {
|
|
580
|
+
// Eg converts abcd1234abcd1234 to AB:CD:12:34:AB:CD:12:34
|
|
581
|
+
httpDevice.device = httpDevice.device.replace(/..\B/g, '$&:').toUpperCase()
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Check it's not a user-ignored device
|
|
585
|
+
if (this.ignoredDevices.includes(httpDevice.device)) {
|
|
586
|
+
return
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Sets the flag to see if we need to set up the BLE/HTTP syncs
|
|
590
|
+
if (platformConsts.models.sensorLeak.includes(httpDevice.sku)) {
|
|
591
|
+
httpSyncNeeded = true
|
|
592
|
+
}
|
|
593
|
+
if (platformConsts.models.sensorThermo.includes(httpDevice.sku)) {
|
|
594
|
+
bleSyncNeeded = true
|
|
595
|
+
httpSyncNeeded = true
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Find any matching device from the LAN client
|
|
599
|
+
const lanDevice = lanDevices.find(el => el.device === httpDevice.device)
|
|
600
|
+
|
|
601
|
+
if (lanDevice) {
|
|
602
|
+
// Device exists in LAN data so add the http info to the object and initialise
|
|
603
|
+
this.initialiseDevice({
|
|
604
|
+
...lanDevice,
|
|
605
|
+
httpInfo: httpDevice,
|
|
606
|
+
model: httpDevice.sku,
|
|
607
|
+
deviceName: httpDevice.deviceName,
|
|
608
|
+
isLANDevice: true,
|
|
609
|
+
})
|
|
610
|
+
lanDevicesWereInitialised = true
|
|
611
|
+
lanDevice.initialised = true
|
|
612
|
+
} else {
|
|
613
|
+
// Device doesn't exist in LAN data, but try to initialise as could be other device type
|
|
614
|
+
this.initialiseDevice({
|
|
615
|
+
device: httpDevice.device,
|
|
616
|
+
deviceName: httpDevice.deviceName,
|
|
617
|
+
model: httpDevice.sku,
|
|
618
|
+
httpInfo: httpDevice,
|
|
619
|
+
})
|
|
620
|
+
}
|
|
621
|
+
httpDevicesWereInitialised = true
|
|
622
|
+
})
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Some LAN devices may exist outside the HTTP client
|
|
626
|
+
const pendingLANDevices = lanDevices.filter(el => !el.initialised)
|
|
627
|
+
if (pendingLANDevices.length > 0) {
|
|
628
|
+
// No devices from HTTP client, but LAN devices exist
|
|
629
|
+
pendingLANDevices.forEach((lanDevice) => {
|
|
630
|
+
// Check it's not a user-ignored device
|
|
631
|
+
if (this.ignoredDevices.includes(lanDevice.device)) {
|
|
632
|
+
return
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Initialise the device into Homebridge
|
|
636
|
+
// Since LAN does not provide a name, we will use the configured label or device id
|
|
637
|
+
this.initialiseDevice({
|
|
638
|
+
device: lanDevice.device,
|
|
639
|
+
deviceName: this.deviceConf?.[lanDevice.device]?.label || lanDevice.device.replaceAll(':', ''),
|
|
640
|
+
model: lanDevice.sku || 'HXXXX', // In case the model is not provided
|
|
641
|
+
isLANDevice: true,
|
|
642
|
+
isLANOnly: true,
|
|
643
|
+
})
|
|
644
|
+
lanDevicesWereInitialised = true
|
|
645
|
+
})
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
if (!lanDevicesWereInitialised && !httpDevicesWereInitialised) {
|
|
649
|
+
// No devices either from HTTP client nor LAN client
|
|
650
|
+
throw new Error(platformLang.noDevs)
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// Check for redundant Homebridge accessories
|
|
654
|
+
devicesInHB.forEach((accessory) => {
|
|
655
|
+
// If the accessory doesn't exist in Govee then remove it
|
|
656
|
+
if (
|
|
657
|
+
(!httpDevices.some(el => el.device === accessory.context.gvDeviceId) && !lanDevices.some(el => el.device === accessory.context.gvDeviceId))
|
|
658
|
+
|| this.ignoredDevices.includes(accessory.context.gvDeviceId)
|
|
659
|
+
) {
|
|
660
|
+
this.removeAccessory(accessory)
|
|
661
|
+
}
|
|
662
|
+
})
|
|
663
|
+
|
|
664
|
+
// Set up the ble client sync needed for thermo sensor devices
|
|
665
|
+
if (bleSyncNeeded) {
|
|
666
|
+
try {
|
|
667
|
+
// Check BLE is available
|
|
668
|
+
if (!this.bleClient) {
|
|
669
|
+
throw new Error(platformLang.bleNoPackage)
|
|
670
|
+
}
|
|
671
|
+
// Import the required modules
|
|
672
|
+
const {
|
|
673
|
+
debug: GoveeDebug,
|
|
674
|
+
startDiscovery: sensorStartDiscovery,
|
|
675
|
+
stopDiscovery: sensorStopDiscovery,
|
|
676
|
+
} = await import('govee-bt-client')
|
|
677
|
+
|
|
678
|
+
if (this.isBeta) {
|
|
679
|
+
GoveeDebug(true)
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
this.sensorStartDiscovery = sensorStartDiscovery
|
|
683
|
+
this.sensorStopDiscovery = sensorStopDiscovery
|
|
684
|
+
|
|
685
|
+
this.refreshBLEInterval = setInterval(
|
|
686
|
+
() => this.goveeBLESync(),
|
|
687
|
+
this.config.bleRefreshTime * 1000,
|
|
688
|
+
)
|
|
689
|
+
} catch (err) {
|
|
690
|
+
this.log.warn('[BLE] %s %s.', platformLang.bleScanDisabled, parseError(err, [platformLang.bleNoPackage]))
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// Set up the http client sync needed for leak and thermo sensor devices
|
|
695
|
+
if (this.httpClient && httpSyncNeeded) {
|
|
696
|
+
this.goveeHTTPSync()
|
|
697
|
+
this.refreshHTTPInterval = setInterval(
|
|
698
|
+
() => this.goveeHTTPSync(),
|
|
699
|
+
this.config.httpRefreshTime * 1000,
|
|
700
|
+
)
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// Set up the AWS client sync if there are any compatible devices
|
|
704
|
+
if (this.awsClient && awsDevices.length > 0) {
|
|
705
|
+
// Set up the AWS client
|
|
706
|
+
await this.awsClient.connect()
|
|
707
|
+
|
|
708
|
+
// No need for await as catches its own errors, we poll specific models that need it
|
|
709
|
+
this.goveeAWSSync(true)
|
|
710
|
+
this.refreshAWSInterval = setInterval(
|
|
711
|
+
() => this.goveeAWSSync(),
|
|
712
|
+
60000,
|
|
713
|
+
)
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// Set up the LAN client device scanning and device status polling
|
|
717
|
+
if (lanDevicesWereInitialised) {
|
|
718
|
+
this.lanClient.startDevicesPolling()
|
|
719
|
+
this.lanClient.startStatusPolling()
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// Access a list of scene codes from the HTTP client
|
|
723
|
+
if (this.httpClient) {
|
|
724
|
+
try {
|
|
725
|
+
const scenes = await this.httpClient.getTapToRuns()
|
|
726
|
+
scenes.forEach((scene) => {
|
|
727
|
+
if (scene.oneClicks) {
|
|
728
|
+
scene.oneClicks.forEach((oneClick) => {
|
|
729
|
+
if (oneClick.iotRules) {
|
|
730
|
+
oneClick.iotRules.forEach((iotRule) => {
|
|
731
|
+
if (iotRule?.deviceObj?.sku) {
|
|
732
|
+
if (platformConsts.models.rgb.includes(iotRule.deviceObj.sku)) {
|
|
733
|
+
iotRule.rule.forEach((rule) => {
|
|
734
|
+
this.log.debugWarn(`[%s] [%s] ttr rule debug: ${JSON.stringify(rule)}.`, iotRule.deviceObj.name, oneClick.name)
|
|
735
|
+
if (rule.iotMsg) {
|
|
736
|
+
const iotMsg = JSON.parse(rule.iotMsg)
|
|
737
|
+
if (iotMsg.msg?.cmd === 'ptReal') {
|
|
738
|
+
this.log('[%s] [%s] [AWS] %s', iotRule.deviceObj.name, oneClick.name, iotMsg.msg.data.command.join(','))
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
if (rule.blueMsg) {
|
|
742
|
+
const bleMsg = JSON.parse(rule.blueMsg)
|
|
743
|
+
if (bleMsg.type === 'scene') {
|
|
744
|
+
this.log('[%s] [%s] [BLE] %s', iotRule.deviceObj.name, oneClick.name, bleMsg.modeCmd)
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
})
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
})
|
|
751
|
+
}
|
|
752
|
+
})
|
|
753
|
+
}
|
|
754
|
+
})
|
|
755
|
+
} catch (err) {
|
|
756
|
+
this.log.warn('%s %s.', 'Could not retrieve TTRs as', parseError(err))
|
|
757
|
+
}
|
|
758
|
+
} else {
|
|
759
|
+
this.log.debug('Skipping TTR retrieval as HTTP client not available')
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// Setup successful
|
|
763
|
+
this.log('%s. %s', platformLang.complete, platformLang.welcome)
|
|
764
|
+
} catch (err) {
|
|
765
|
+
// Catch any errors during setup
|
|
766
|
+
this.log.warn('***** %s [v%s]. *****', platformLang.disabling, plugin.version)
|
|
767
|
+
this.log.warn('***** %s. *****', parseError(err, [platformLang.noDevs]))
|
|
768
|
+
this.pluginShutdown()
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
pluginShutdown() {
|
|
773
|
+
// A function that is called when the plugin fails to load or Homebridge restarts
|
|
774
|
+
try {
|
|
775
|
+
// Stop the refresh intervals
|
|
776
|
+
if (this.refreshBLEInterval) {
|
|
777
|
+
clearInterval(this.refreshBLEInterval)
|
|
778
|
+
}
|
|
779
|
+
if (this.refreshHTTPInterval) {
|
|
780
|
+
clearInterval(this.refreshHTTPInterval)
|
|
781
|
+
|
|
782
|
+
// No need to await this since it catches its own errors
|
|
783
|
+
this.httpClient.logout()
|
|
784
|
+
}
|
|
785
|
+
if (this.refreshAWSInterval) {
|
|
786
|
+
clearInterval(this.refreshAWSInterval)
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// Close the LAN client
|
|
790
|
+
this.lanClient.close()
|
|
791
|
+
} catch (err) {
|
|
792
|
+
// No need to show errors at this point
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
applyAccessoryLogging(accessory) {
|
|
797
|
+
if (this.isBeta) {
|
|
798
|
+
accessory.log = msg => this.log('[%s] %s.', accessory.displayName, msg)
|
|
799
|
+
accessory.logWarn = msg => this.log.warn('[%s] %s.', accessory.displayName, msg)
|
|
800
|
+
accessory.logDebug = msg => this.log('[%s] %s.', accessory.displayName, msg)
|
|
801
|
+
accessory.logDebugWarn = msg => this.log.warn('[%s] %s.', accessory.displayName, msg)
|
|
802
|
+
} else {
|
|
803
|
+
if (this.config.disableDeviceLogging) {
|
|
804
|
+
accessory.log = () => {}
|
|
805
|
+
accessory.logWarn = () => {}
|
|
806
|
+
} else {
|
|
807
|
+
accessory.log = msg => this.log('[%s] %s.', accessory.displayName, msg)
|
|
808
|
+
accessory.logWarn = msg => this.log.warn('[%s] %s.', accessory.displayName, msg)
|
|
809
|
+
}
|
|
810
|
+
accessory.logDebug = () => {}
|
|
811
|
+
accessory.logDebugWarn = () => {}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
initialiseDevice(device) {
|
|
816
|
+
// Get the correct device type instance for the device
|
|
817
|
+
try {
|
|
818
|
+
const deviceConf = this.deviceConf[device.device.toUpperCase()] || {}
|
|
819
|
+
const uuid = this.api.hap.uuid.generate(device.device)
|
|
820
|
+
let accessory
|
|
821
|
+
let devInstance
|
|
822
|
+
let isLight = false
|
|
823
|
+
let isJustBLE = false
|
|
824
|
+
let doAWSPolling = false
|
|
825
|
+
if (platformConsts.models.rgb.includes(device.model)) {
|
|
826
|
+
// Device is a cloud-enabled (and maybe bluetooth) LED strip/bulb
|
|
827
|
+
isLight = true
|
|
828
|
+
devInstance = deviceConf.showAs === 'switch'
|
|
829
|
+
? deviceTypes.deviceLightSwitch
|
|
830
|
+
: deviceTypes.deviceLight
|
|
831
|
+
accessory = devicesInHB.get(uuid) || this.addAccessory(device)
|
|
832
|
+
} else if (platformConsts.models.rgbBT.includes(device.model)) {
|
|
833
|
+
// Device is a bluetooth-only LED strip/bulb
|
|
834
|
+
if (this.config.bleDisable) {
|
|
835
|
+
// BLE is disabled, so remove accessory if exists, log and return
|
|
836
|
+
if (devicesInHB.has(uuid)) {
|
|
837
|
+
this.removeAccessory(devicesInHB.get(uuid))
|
|
838
|
+
}
|
|
839
|
+
this.log('[%s] %s.', device.deviceName, platformLang.devNoBlePackage)
|
|
840
|
+
return
|
|
841
|
+
}
|
|
842
|
+
isLight = true
|
|
843
|
+
isJustBLE = true
|
|
844
|
+
devInstance = deviceConf.showAs === 'switch'
|
|
845
|
+
? deviceTypes.deviceLightSwitch
|
|
846
|
+
: deviceTypes.deviceLight
|
|
847
|
+
accessory = devicesInHB.get(uuid) || this.addAccessory(device)
|
|
848
|
+
if (!this.bleClient) {
|
|
849
|
+
this.log.warn('[%s] %s.', accessory.displayName, platformLang.bleNonControl)
|
|
850
|
+
}
|
|
851
|
+
} else if (platformConsts.models.switchSingle.includes(device.model)) {
|
|
852
|
+
// Device is an cloud enabled Wi-Fi switch
|
|
853
|
+
switch (deviceConf.showAs || platformConsts.defaultValues.showAs) {
|
|
854
|
+
case 'audio': {
|
|
855
|
+
if (devicesInHB.get(uuid)) {
|
|
856
|
+
this.removeAccessory(devicesInHB.get(uuid))
|
|
857
|
+
}
|
|
858
|
+
devInstance = deviceTypes.deviceTVSingle
|
|
859
|
+
accessory = this.addExternalAccessory(device, 34)
|
|
860
|
+
break
|
|
861
|
+
}
|
|
862
|
+
case 'box': {
|
|
863
|
+
if (devicesInHB.get(uuid)) {
|
|
864
|
+
this.removeAccessory(devicesInHB.get(uuid))
|
|
865
|
+
}
|
|
866
|
+
devInstance = deviceTypes.deviceTVSingle
|
|
867
|
+
accessory = this.addExternalAccessory(device, 35)
|
|
868
|
+
break
|
|
869
|
+
}
|
|
870
|
+
case 'stick': {
|
|
871
|
+
if (devicesInHB.get(uuid)) {
|
|
872
|
+
this.removeAccessory(devicesInHB.get(uuid))
|
|
873
|
+
}
|
|
874
|
+
devInstance = deviceTypes.deviceTVSingle
|
|
875
|
+
accessory = this.addExternalAccessory(device, 36)
|
|
876
|
+
break
|
|
877
|
+
}
|
|
878
|
+
case 'cooler': {
|
|
879
|
+
if (!deviceConf.temperatureSource) {
|
|
880
|
+
this.log.warn('[%s] %s.', device.deviceName, platformLang.heaterSimNoSensor)
|
|
881
|
+
if (devicesInHB.has(uuid)) {
|
|
882
|
+
this.removeAccessory(devicesInHB.get(uuid))
|
|
883
|
+
}
|
|
884
|
+
return
|
|
885
|
+
}
|
|
886
|
+
devInstance = deviceTypes.deviceCoolerSingle
|
|
887
|
+
accessory = devicesInHB.get(uuid) || this.addAccessory(device)
|
|
888
|
+
break
|
|
889
|
+
}
|
|
890
|
+
case 'heater': {
|
|
891
|
+
if (!deviceConf.temperatureSource) {
|
|
892
|
+
this.log.warn('[%s] %s.', device.deviceName, platformLang.heaterSimNoSensor)
|
|
893
|
+
if (devicesInHB.has(uuid)) {
|
|
894
|
+
this.removeAccessory(devicesInHB.get(uuid))
|
|
895
|
+
}
|
|
896
|
+
return
|
|
897
|
+
}
|
|
898
|
+
devInstance = deviceTypes.deviceHeater2Single
|
|
899
|
+
accessory = devicesInHB.get(uuid) || this.addAccessory(device)
|
|
900
|
+
break
|
|
901
|
+
}
|
|
902
|
+
case 'purifier': {
|
|
903
|
+
devInstance = deviceTypes.devicePurifierSingle
|
|
904
|
+
accessory = devicesInHB.get(uuid) || this.addAccessory(device)
|
|
905
|
+
break
|
|
906
|
+
}
|
|
907
|
+
case 'switch': {
|
|
908
|
+
devInstance = deviceTypes.deviceSwitchSingle
|
|
909
|
+
accessory = devicesInHB.get(uuid) || this.addAccessory(device)
|
|
910
|
+
break
|
|
911
|
+
}
|
|
912
|
+
case 'tap': {
|
|
913
|
+
devInstance = deviceTypes.deviceTapSingle
|
|
914
|
+
accessory = devicesInHB.get(uuid) || this.addAccessory(device)
|
|
915
|
+
break
|
|
916
|
+
}
|
|
917
|
+
case 'valve': {
|
|
918
|
+
devInstance = deviceTypes.deviceValveSingle
|
|
919
|
+
accessory = devicesInHB.get(uuid) || this.addAccessory(device)
|
|
920
|
+
break
|
|
921
|
+
}
|
|
922
|
+
default:
|
|
923
|
+
devInstance = deviceTypes.deviceOutletSingle
|
|
924
|
+
accessory = devicesInHB.get(uuid) || this.addAccessory(device)
|
|
925
|
+
break
|
|
926
|
+
}
|
|
927
|
+
} else if (platformConsts.models.switchDouble.includes(device.model)) {
|
|
928
|
+
// Device is an AWS enabled Wi-Fi double switch
|
|
929
|
+
switch (deviceConf.showAs || platformConsts.defaultValues.showAs) {
|
|
930
|
+
case 'switch': {
|
|
931
|
+
devInstance = deviceTypes.deviceSwitchDouble
|
|
932
|
+
accessory = devicesInHB.get(uuid) || this.addAccessory(device)
|
|
933
|
+
break
|
|
934
|
+
}
|
|
935
|
+
default: {
|
|
936
|
+
devInstance = deviceTypes.deviceOutletDouble
|
|
937
|
+
accessory = devicesInHB.get(uuid) || this.addAccessory(device)
|
|
938
|
+
break
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
} else if (platformConsts.models.switchTriple.includes(device.model)) {
|
|
942
|
+
// Device is an AWS enabled Wi-Fi double switch
|
|
943
|
+
switch (deviceConf.showAs || platformConsts.defaultValues.showAs) {
|
|
944
|
+
case 'switch': {
|
|
945
|
+
devInstance = deviceTypes.deviceSwitchTriple
|
|
946
|
+
accessory = devicesInHB.get(uuid) || this.addAccessory(device)
|
|
947
|
+
break
|
|
948
|
+
}
|
|
949
|
+
default: {
|
|
950
|
+
devInstance = deviceTypes.deviceOutletTriple
|
|
951
|
+
accessory = devicesInHB.get(uuid) || this.addAccessory(device)
|
|
952
|
+
break
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
} else if (platformConsts.models.sensorLeak.includes(device.model)) {
|
|
956
|
+
// Device is a leak sensor
|
|
957
|
+
devInstance = deviceTypes.deviceSensorLeak
|
|
958
|
+
accessory = devicesInHB.get(uuid) || this.addAccessory(device)
|
|
959
|
+
} else if (platformConsts.models.sensorPresence.includes(device.model)) {
|
|
960
|
+
// Device is a presence sensor
|
|
961
|
+
devInstance = deviceTypes.deviceSensorPresence
|
|
962
|
+
accessory = devicesInHB.get(uuid) || this.addAccessory(device)
|
|
963
|
+
} else if (platformConsts.models.sensorThermo.includes(device.model)) {
|
|
964
|
+
// Device is a thermo-hygrometer sensor
|
|
965
|
+
devInstance = deviceTypes.deviceSensorThermo
|
|
966
|
+
accessory = devicesInHB.get(uuid) || this.addAccessory(device)
|
|
967
|
+
} else if (platformConsts.models.sensorThermo4.includes(device.model)) {
|
|
968
|
+
// Device is a thermo-hygrometer sensor with 4 prongs and AWS support
|
|
969
|
+
devInstance = deviceTypes.deviceSensorThermo4
|
|
970
|
+
accessory = devicesInHB.get(uuid) || this.addAccessory(device)
|
|
971
|
+
} else if (platformConsts.models.sensorMonitor.includes(device.model)) {
|
|
972
|
+
devInstance = deviceTypes.deviceSensorMonitor
|
|
973
|
+
doAWSPolling = true
|
|
974
|
+
accessory = devicesInHB.get(uuid) || this.addAccessory(device)
|
|
975
|
+
} else if (platformConsts.models.fan.includes(device.model)) {
|
|
976
|
+
// Device is a fan
|
|
977
|
+
devInstance = deviceTypes[`deviceFan${device.model}`]
|
|
978
|
+
accessory = devicesInHB.get(uuid) || this.addAccessory(device)
|
|
979
|
+
} else if (platformConsts.models.heater1.includes(device.model)) {
|
|
980
|
+
// Device is a H7130
|
|
981
|
+
devInstance = deviceConf.tempReporting
|
|
982
|
+
? deviceTypes.deviceHeater1B
|
|
983
|
+
: deviceTypes.deviceHeater1A
|
|
984
|
+
doAWSPolling = true
|
|
985
|
+
accessory = devicesInHB.get(uuid) || this.addAccessory(device)
|
|
986
|
+
} else if (platformConsts.models.heater2.includes(device.model)) {
|
|
987
|
+
// Device is a H7131/H7132
|
|
988
|
+
devInstance = deviceTypes.deviceHeater2
|
|
989
|
+
doAWSPolling = true
|
|
990
|
+
accessory = devicesInHB.get(uuid) || this.addAccessory(device)
|
|
991
|
+
} else if (platformConsts.models.humidifier.includes(device.model)) {
|
|
992
|
+
// Device is a humidifier
|
|
993
|
+
doAWSPolling = true
|
|
994
|
+
devInstance = deviceTypes[`deviceHumidifier${device.model}`]
|
|
995
|
+
accessory = devicesInHB.get(uuid) || this.addAccessory(device)
|
|
996
|
+
} else if (platformConsts.models.dehumidifier.includes(device.model)) {
|
|
997
|
+
// Device is a dehumidifier
|
|
998
|
+
devInstance = deviceTypes[`deviceDehumidifier${device.model}`]
|
|
999
|
+
doAWSPolling = true
|
|
1000
|
+
accessory = devicesInHB.get(uuid) || this.addAccessory(device)
|
|
1001
|
+
} else if (platformConsts.models.purifier.includes(device.model)) {
|
|
1002
|
+
// Device is a purifier
|
|
1003
|
+
devInstance = deviceTypes[`devicePurifier${device.model}`]
|
|
1004
|
+
doAWSPolling = true
|
|
1005
|
+
accessory = devicesInHB.get(uuid) || this.addAccessory(device)
|
|
1006
|
+
} else if (platformConsts.models.diffuser.includes(device.model)) {
|
|
1007
|
+
// Device is a diffuser
|
|
1008
|
+
devInstance = deviceTypes[`deviceDiffuser${device.model}`]
|
|
1009
|
+
doAWSPolling = true
|
|
1010
|
+
accessory = devicesInHB.get(uuid) || this.addAccessory(device)
|
|
1011
|
+
} else if (platformConsts.models.sensorButton.includes(device.model)) {
|
|
1012
|
+
// Device is a button
|
|
1013
|
+
devInstance = deviceTypes.deviceSensorButton
|
|
1014
|
+
accessory = devicesInHB.get(uuid) || this.addAccessory(device)
|
|
1015
|
+
} else if (platformConsts.models.sensorContact.includes(device.model)) {
|
|
1016
|
+
// Device is a contact sensor
|
|
1017
|
+
devInstance = deviceTypes.deviceSensorContact
|
|
1018
|
+
accessory = devicesInHB.get(uuid) || this.addAccessory(device)
|
|
1019
|
+
} else if (platformConsts.models.kettle.includes(device.model)) {
|
|
1020
|
+
// Device is a kettle
|
|
1021
|
+
devInstance = deviceTypes.deviceKettle
|
|
1022
|
+
accessory = devicesInHB.get(uuid) || this.addAccessory(device)
|
|
1023
|
+
} else if (platformConsts.models.iceMaker.includes(device.model)) {
|
|
1024
|
+
// Device is an ice maker
|
|
1025
|
+
devInstance = deviceTypes[`deviceIceMaker${device.model}`]
|
|
1026
|
+
accessory = devicesInHB.get(uuid) || this.addAccessory(device)
|
|
1027
|
+
} else if (platformConsts.models.template.includes(device.model)) {
|
|
1028
|
+
// Device is a work-in-progress
|
|
1029
|
+
devInstance = deviceTypes.deviceTemplate
|
|
1030
|
+
accessory = devicesInHB.get(uuid) || this.addAccessory(device)
|
|
1031
|
+
} else {
|
|
1032
|
+
// Device is not in any supported model list but could be implemented into the plugin
|
|
1033
|
+
this.log.warn(
|
|
1034
|
+
'[%s] %s:\n%s',
|
|
1035
|
+
device.deviceName,
|
|
1036
|
+
platformLang.devMaySupp,
|
|
1037
|
+
JSON.stringify(device),
|
|
1038
|
+
)
|
|
1039
|
+
return
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// Final check the accessory now exists in Homebridge
|
|
1043
|
+
if (!accessory) {
|
|
1044
|
+
throw new Error(platformLang.accNotFound)
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// Set the logging level for this device
|
|
1048
|
+
this.applyAccessoryLogging(accessory)
|
|
1049
|
+
|
|
1050
|
+
// Add the temperatureSource config to the context if exists
|
|
1051
|
+
if (deviceConf.temperatureSource) {
|
|
1052
|
+
accessory.context.temperatureSource = deviceConf.temperatureSource
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// Get a supported command list if provided, with their options
|
|
1056
|
+
if (device.supportCmds && Array.isArray(device.supportCmds)) {
|
|
1057
|
+
accessory.context.supportedCmds = device.supportCmds
|
|
1058
|
+
accessory.context.supportedCmdsOpts = {}
|
|
1059
|
+
|
|
1060
|
+
device.supportCmds.forEach((cmd) => {
|
|
1061
|
+
if (device?.properties?.[cmd]) {
|
|
1062
|
+
accessory.context.supportedCmdsOpts[cmd] = device.properties[cmd]
|
|
1063
|
+
}
|
|
1064
|
+
})
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// Add some initial context information which is changed later
|
|
1068
|
+
accessory.context.hasAWSControl = false
|
|
1069
|
+
accessory.context.useAWSControl = false
|
|
1070
|
+
accessory.context.hasBLEControl = false
|
|
1071
|
+
accessory.context.useBLEControl = false
|
|
1072
|
+
accessory.context.firmware = false
|
|
1073
|
+
accessory.context.hardware = false
|
|
1074
|
+
accessory.context.image = false
|
|
1075
|
+
|
|
1076
|
+
const modelHasLanControl = platformConsts.lanModels.includes(device.model)
|
|
1077
|
+
accessory.context.hasLANControl = modelHasLanControl && device.isLANDevice
|
|
1078
|
+
accessory.context.useLANControl = accessory.context.hasLANControl
|
|
1079
|
+
|
|
1080
|
+
// Overrides for when a custom IP is provided, for a light which is not BLE only
|
|
1081
|
+
if (modelHasLanControl && deviceConf.customIPAddress && isLight && !isJustBLE) {
|
|
1082
|
+
accessory.context.hasLANControl = true
|
|
1083
|
+
accessory.context.useLANControl = true
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// If the device is LAN-only, then sync the display name with the label in the configuration
|
|
1087
|
+
if (device.isLANOnly) {
|
|
1088
|
+
accessory.displayName = device.deviceName
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
// See if we have extra HTTP client info for this device
|
|
1092
|
+
if (device.httpInfo) {
|
|
1093
|
+
// Save the hardware and firmware versions
|
|
1094
|
+
accessory.context.firmware = device.httpInfo.versionSoft
|
|
1095
|
+
accessory.context.hardware = device.httpInfo.versionHard
|
|
1096
|
+
|
|
1097
|
+
// It's possible to show a nice little icon of the device in the Homebridge UI
|
|
1098
|
+
if (device.httpInfo.deviceExt && device.httpInfo.deviceExt.extResources) {
|
|
1099
|
+
const parsed = JSON.parse(device.httpInfo.deviceExt.extResources)
|
|
1100
|
+
if (parsed && parsed.skuUrl) {
|
|
1101
|
+
accessory.context.image = parsed.skuUrl
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// HTTP info lets us see if AWS/BLE connection methods are available
|
|
1106
|
+
if (device.httpInfo.deviceExt && device.httpInfo.deviceExt.deviceSettings) {
|
|
1107
|
+
const parsed = JSON.parse(device.httpInfo.deviceExt.deviceSettings)
|
|
1108
|
+
|
|
1109
|
+
// Check to see if AWS is possible
|
|
1110
|
+
if (parsed) {
|
|
1111
|
+
if (parsed.topic) {
|
|
1112
|
+
accessory.context.hasAWSControl = true
|
|
1113
|
+
accessory.context.awsTopic = parsed.topic
|
|
1114
|
+
|
|
1115
|
+
if (this.awsClient) {
|
|
1116
|
+
accessory.context.useAWSControl = true
|
|
1117
|
+
accessory.context.awsBrightnessNoScale = deviceConf.awsBrightnessNoScale
|
|
1118
|
+
accessory.context.awsColourMode = deviceConf.awsColourMode || platformConsts.defaultValues.awsColourMode
|
|
1119
|
+
awsDevices.push(device.device)
|
|
1120
|
+
|
|
1121
|
+
// Certain models need AWS polling
|
|
1122
|
+
if (doAWSPolling) {
|
|
1123
|
+
awsDevicesToPoll.push(device.device)
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// Check to see if BLE is possible
|
|
1129
|
+
if (parsed.bleName) {
|
|
1130
|
+
const providedBle = parsed.address ? parsed.address.toLowerCase() : device.device.substring(6).toLowerCase()
|
|
1131
|
+
accessory.context.hasBLEControl = !!parsed.bleName
|
|
1132
|
+
accessory.context.bleAddress = deviceConf.customAddress
|
|
1133
|
+
? deviceConf.customAddress.toLowerCase()
|
|
1134
|
+
: providedBle
|
|
1135
|
+
accessory.context.bleName = parsed.bleName
|
|
1136
|
+
if (this.bleClient) {
|
|
1137
|
+
accessory.context.useBLEControl = true
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
// Get a min and max temperature/humidity range to show in the homebridge-ui
|
|
1142
|
+
if (hasProperty(parsed, 'temCali')) {
|
|
1143
|
+
accessory.context.minTemp = parsed.temMin / 100
|
|
1144
|
+
accessory.context.maxTemp = parsed.temMax / 100
|
|
1145
|
+
accessory.context.offTemp = parsed.temCali
|
|
1146
|
+
}
|
|
1147
|
+
if (hasProperty(parsed, 'humCali')) {
|
|
1148
|
+
accessory.context.minHumi = parsed.humMin / 100
|
|
1149
|
+
accessory.context.maxHumi = parsed.humMax / 100
|
|
1150
|
+
accessory.context.offHumi = parsed.humCali
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
// Create the instance for this device type
|
|
1157
|
+
accessory.control = new devInstance(this, accessory)
|
|
1158
|
+
|
|
1159
|
+
// Log the device initialisation
|
|
1160
|
+
this.log(
|
|
1161
|
+
'[%s] %s [%s] [%s].',
|
|
1162
|
+
accessory.displayName,
|
|
1163
|
+
platformLang.devInit,
|
|
1164
|
+
device.device,
|
|
1165
|
+
device.model,
|
|
1166
|
+
)
|
|
1167
|
+
|
|
1168
|
+
// Update any changes to the accessory to the platform
|
|
1169
|
+
this.api.updatePlatformAccessories([accessory])
|
|
1170
|
+
devicesInHB.set(accessory.UUID, accessory)
|
|
1171
|
+
} catch (err) {
|
|
1172
|
+
// Catch any errors during device initialisation
|
|
1173
|
+
this.log.warn('[%s] %s %s.', device.deviceName, platformLang.devNotInit, parseError(err, [
|
|
1174
|
+
platformLang.accNotFound,
|
|
1175
|
+
]))
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
async goveeAWSSync(allDevices = false) {
|
|
1180
|
+
const pollList = allDevices ? awsDevices : awsDevicesToPoll
|
|
1181
|
+
if (pollList.length === 0) {
|
|
1182
|
+
return
|
|
1183
|
+
}
|
|
1184
|
+
try {
|
|
1185
|
+
pollList.forEach(async (deviceId) => {
|
|
1186
|
+
// Generate the UUID from which we can match our Homebridge accessory
|
|
1187
|
+
const accessory = devicesInHB.get(this.api.hap.uuid.generate(deviceId))
|
|
1188
|
+
try {
|
|
1189
|
+
await this.awsClient.requestUpdate(accessory)
|
|
1190
|
+
} catch (err) {
|
|
1191
|
+
accessory.logDebugWarn(`[LAN] ${platformLang.syncFail} ${parseError(err)}`)
|
|
1192
|
+
}
|
|
1193
|
+
})
|
|
1194
|
+
} catch (err) {
|
|
1195
|
+
this.log.warn('[LAN] %s %s.', platformLang.syncFail, parseError(err))
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
async goveeBLESync() {
|
|
1200
|
+
try {
|
|
1201
|
+
await this.sensorStartDiscovery((goveeReading) => {
|
|
1202
|
+
const accessory = [...devicesInHB.values()].find(acc => acc.context.bleAddress === goveeReading.address)
|
|
1203
|
+
if (accessory && !platformConsts.models.sensorMonitor.includes(accessory.context.gvModel)) {
|
|
1204
|
+
this.receiveDeviceUpdate(accessory, {
|
|
1205
|
+
temperature: goveeReading.tempInC * 100,
|
|
1206
|
+
temperatureF: goveeReading.tempInF * 100,
|
|
1207
|
+
humidity: goveeReading.humidity * 100,
|
|
1208
|
+
battery: goveeReading.battery,
|
|
1209
|
+
source: 'BLE',
|
|
1210
|
+
})
|
|
1211
|
+
} else {
|
|
1212
|
+
this.log.debugWarn('[BLE] %s [%s].', platformLang.bleScanUnknown, goveeReading.address)
|
|
1213
|
+
}
|
|
1214
|
+
})
|
|
1215
|
+
|
|
1216
|
+
// Stop scanning after 5 seconds
|
|
1217
|
+
setTimeout(async () => {
|
|
1218
|
+
try {
|
|
1219
|
+
await this.sensorStopDiscovery()
|
|
1220
|
+
} catch (err) {
|
|
1221
|
+
this.log.warn('[BLE] %s %s.', platformLang.bleScanNoStop, parseError(err))
|
|
1222
|
+
}
|
|
1223
|
+
}, 5000)
|
|
1224
|
+
} catch (err) {
|
|
1225
|
+
this.log.warn('[BLE] %s %s.', platformLang.bleScanNoStart, parseError(err))
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
async goveeHTTPSync() {
|
|
1230
|
+
try {
|
|
1231
|
+
// Obtain a refreshed device list
|
|
1232
|
+
const devices = await this.httpClient.getDevices(true)
|
|
1233
|
+
|
|
1234
|
+
// Filter those which are leak sensors
|
|
1235
|
+
devices
|
|
1236
|
+
.filter(device => [...platformConsts.models.sensorLeak, ...platformConsts.models.sensorThermo].includes(device.sku))
|
|
1237
|
+
.forEach(async (device) => {
|
|
1238
|
+
try {
|
|
1239
|
+
// Reformat the device id
|
|
1240
|
+
if (!device.device.includes(':')) {
|
|
1241
|
+
// Eg converts abcd1234abcd1234 to AB:CD:12:34:AB:CD:12:34
|
|
1242
|
+
device.device = device.device.replace(/..\B/g, '$&:').toUpperCase()
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
// Generate the UIID from which we can match our Homebridge accessory
|
|
1246
|
+
const uiid = this.api.hap.uuid.generate(device.device)
|
|
1247
|
+
|
|
1248
|
+
// Don't continue if the accessory doesn't exist
|
|
1249
|
+
if (!devicesInHB.has(uiid)) {
|
|
1250
|
+
return
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// Retrieve the Homebridge accessory
|
|
1254
|
+
const accessory = devicesInHB.get(uiid)
|
|
1255
|
+
|
|
1256
|
+
// Make sure the data we need for the device exists
|
|
1257
|
+
if (!device.deviceExt || !device.deviceExt.deviceSettings || !device.deviceExt.lastDeviceData) {
|
|
1258
|
+
return
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
// Parse the data received
|
|
1262
|
+
const parsedSettings = JSON.parse(device.deviceExt.deviceSettings)
|
|
1263
|
+
const parsedData = JSON.parse(device.deviceExt.lastDeviceData)
|
|
1264
|
+
|
|
1265
|
+
const toReturn = { source: 'HTTP' }
|
|
1266
|
+
if (platformConsts.models.sensorLeak.includes(device.sku)) {
|
|
1267
|
+
accessory.logDebug(`raw data: ${JSON.stringify({ ...parsedData, ...parsedSettings })}`)
|
|
1268
|
+
|
|
1269
|
+
// Leak Sensors - check to see of any warnings if the lastTime is above 0
|
|
1270
|
+
let hasUnreadLeak = false
|
|
1271
|
+
if (parsedData.lastTime > 0) {
|
|
1272
|
+
// Obtain the leak warning messages for this device
|
|
1273
|
+
const msgs = await this.httpClient.getLeakDeviceWarning(device.device, device.sku)
|
|
1274
|
+
|
|
1275
|
+
accessory.logDebug(`raw messages: ${JSON.stringify(msgs)}`)
|
|
1276
|
+
|
|
1277
|
+
// Check to see if unread messages exist
|
|
1278
|
+
const unreadCount = msgs.filter(msg => !msg.read && msg.message.toLowerCase().replace(/\s+/g, '').startsWith('leakagealert'))
|
|
1279
|
+
if (unreadCount.length > 0) {
|
|
1280
|
+
hasUnreadLeak = true
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
// Generate the params to return
|
|
1285
|
+
toReturn.battery = parsedSettings.battery
|
|
1286
|
+
toReturn.leakDetected = hasUnreadLeak
|
|
1287
|
+
toReturn.online = parsedData.gwonline && parsedData.online
|
|
1288
|
+
} else if (platformConsts.models.sensorThermo.includes(device.sku)) {
|
|
1289
|
+
if (hasProperty(parsedSettings, 'battery')) {
|
|
1290
|
+
toReturn.battery = parsedSettings.battery
|
|
1291
|
+
}
|
|
1292
|
+
if (hasProperty(parsedData, 'tem')) {
|
|
1293
|
+
toReturn.temperature = parsedData.tem
|
|
1294
|
+
}
|
|
1295
|
+
if (hasProperty(parsedData, 'hum')) {
|
|
1296
|
+
toReturn.humidity = parsedData.hum
|
|
1297
|
+
}
|
|
1298
|
+
if (hasProperty(parsedData, 'online')) {
|
|
1299
|
+
toReturn.online = parsedData.online
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
// Send the information to the update receiver function
|
|
1304
|
+
this.receiveDeviceUpdate(accessory, toReturn)
|
|
1305
|
+
} catch (err) {
|
|
1306
|
+
this.log.warn('[%s] %s %s.', device.deviceName, platformLang.devNotRef, parseError(err))
|
|
1307
|
+
}
|
|
1308
|
+
})
|
|
1309
|
+
} catch (err) {
|
|
1310
|
+
this.log.warn('[HTTP] %s %s.', platformLang.syncFail, parseError(err))
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
addAccessory(device) {
|
|
1315
|
+
// Add an accessory to Homebridge
|
|
1316
|
+
try {
|
|
1317
|
+
const uuid = this.api.hap.uuid.generate(device.device)
|
|
1318
|
+
const accessory = new this.api.platformAccessory(device.deviceName, uuid)
|
|
1319
|
+
accessory
|
|
1320
|
+
.getService(this.api.hap.Service.AccessoryInformation)
|
|
1321
|
+
.setCharacteristic(this.api.hap.Characteristic.Name, device.deviceName)
|
|
1322
|
+
.setCharacteristic(this.api.hap.Characteristic.ConfiguredName, device.deviceName)
|
|
1323
|
+
.setCharacteristic(this.api.hap.Characteristic.Manufacturer, platformLang.brand)
|
|
1324
|
+
.setCharacteristic(this.api.hap.Characteristic.SerialNumber, device.device)
|
|
1325
|
+
.setCharacteristic(this.api.hap.Characteristic.Model, device.model)
|
|
1326
|
+
.setCharacteristic(this.api.hap.Characteristic.Identify, true)
|
|
1327
|
+
accessory.context.gvDeviceId = device.device
|
|
1328
|
+
accessory.context.gvModel = device.model
|
|
1329
|
+
this.api.registerPlatformAccessories(plugin.name, plugin.alias, [accessory])
|
|
1330
|
+
this.configureAccessory(accessory)
|
|
1331
|
+
this.log('[%s] %s.', device.deviceName, platformLang.devAdd)
|
|
1332
|
+
return accessory
|
|
1333
|
+
} catch (err) {
|
|
1334
|
+
// Catch any errors during add
|
|
1335
|
+
this.log.warn('[%s] %s %s.', device.deviceName, platformLang.devNotAdd, parseError(err))
|
|
1336
|
+
return false
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
addExternalAccessory(device, category) {
|
|
1341
|
+
try {
|
|
1342
|
+
// Add the new accessory to Homebridge
|
|
1343
|
+
const accessory = new this.api.platformAccessory(
|
|
1344
|
+
device.deviceName,
|
|
1345
|
+
this.api.hap.uuid.generate(device.device),
|
|
1346
|
+
category,
|
|
1347
|
+
)
|
|
1348
|
+
|
|
1349
|
+
// Set the accessory characteristics
|
|
1350
|
+
accessory
|
|
1351
|
+
.getService(this.api.hap.Service.AccessoryInformation)
|
|
1352
|
+
.setCharacteristic(this.api.hap.Characteristic.Name, device.deviceName)
|
|
1353
|
+
.setCharacteristic(this.api.hap.Characteristic.ConfiguredName, device.deviceName)
|
|
1354
|
+
.setCharacteristic(this.api.hap.Characteristic.Manufacturer, platformLang.brand)
|
|
1355
|
+
.setCharacteristic(this.api.hap.Characteristic.SerialNumber, device.device)
|
|
1356
|
+
.setCharacteristic(this.api.hap.Characteristic.Model, device.model)
|
|
1357
|
+
.setCharacteristic(this.api.hap.Characteristic.Identify, true)
|
|
1358
|
+
|
|
1359
|
+
// Register the accessory
|
|
1360
|
+
this.api.publishExternalAccessories(plugin.name, [accessory])
|
|
1361
|
+
this.log('[%s] %s.', device.name, platformLang.devAdd)
|
|
1362
|
+
|
|
1363
|
+
// Return the new accessory
|
|
1364
|
+
this.configureAccessory(accessory)
|
|
1365
|
+
return accessory
|
|
1366
|
+
} catch (err) {
|
|
1367
|
+
// Catch any errors during add
|
|
1368
|
+
this.log.warn('[%s] %s %s.', device.deviceName, platformLang.devNotAdd, parseError(err))
|
|
1369
|
+
return false
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
configureAccessory(accessory) {
|
|
1374
|
+
// Set the correct firmware version if we can
|
|
1375
|
+
if (this.api && accessory.context.firmware) {
|
|
1376
|
+
accessory
|
|
1377
|
+
.getService(this.api.hap.Service.AccessoryInformation)
|
|
1378
|
+
.updateCharacteristic(
|
|
1379
|
+
this.api.hap.Characteristic.FirmwareRevision,
|
|
1380
|
+
accessory.context.firmware,
|
|
1381
|
+
)
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
// Add the configured accessory to our global map
|
|
1385
|
+
devicesInHB.set(accessory.UUID, accessory)
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
removeAccessory(accessory) {
|
|
1389
|
+
// Remove an accessory from Homebridge
|
|
1390
|
+
try {
|
|
1391
|
+
this.api.unregisterPlatformAccessories(plugin.name, plugin.alias, [accessory])
|
|
1392
|
+
devicesInHB.delete(accessory.UUID)
|
|
1393
|
+
this.log('[%s] %s.', accessory.displayName, platformLang.devRemove)
|
|
1394
|
+
} catch (err) {
|
|
1395
|
+
// Catch any errors during remove
|
|
1396
|
+
this.log.warn('[%s] %s %s.', accessory.displayName, platformLang.devNotRemove, parseError(err))
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
async sendDeviceUpdate(accessory, params) {
|
|
1401
|
+
const data = {}
|
|
1402
|
+
// Construct the params for BLE/AWS
|
|
1403
|
+
switch (params.cmd) {
|
|
1404
|
+
case 'state': {
|
|
1405
|
+
/*
|
|
1406
|
+
ON/OFF
|
|
1407
|
+
<= INPUT params.value with values 'on' or 'off'
|
|
1408
|
+
AWS needs { cmd: 'turn', data: { val: 1/0 } }
|
|
1409
|
+
BLE needs { cmd: 0x01, data: 0x1/0x0 }
|
|
1410
|
+
LAN needs { cmd: 'turn', data: { value: 'on'/'off' } }
|
|
1411
|
+
*/
|
|
1412
|
+
data.awsParams = {
|
|
1413
|
+
cmd: 'turn',
|
|
1414
|
+
data: { val: params.value === 'on' ? 1 : 0 },
|
|
1415
|
+
}
|
|
1416
|
+
data.bleParams = {
|
|
1417
|
+
cmd: 0x01,
|
|
1418
|
+
data: params.value === 'on' ? 0x1 : 0x0,
|
|
1419
|
+
}
|
|
1420
|
+
data.lanParams = {
|
|
1421
|
+
cmd: 'turn',
|
|
1422
|
+
data: { value: params.value === 'on' ? 1 : 0 },
|
|
1423
|
+
}
|
|
1424
|
+
break
|
|
1425
|
+
}
|
|
1426
|
+
case 'stateDual': {
|
|
1427
|
+
data.awsParams = {
|
|
1428
|
+
cmd: 'turn',
|
|
1429
|
+
data: { val: params.value },
|
|
1430
|
+
}
|
|
1431
|
+
break
|
|
1432
|
+
}
|
|
1433
|
+
case 'stateOutlet': {
|
|
1434
|
+
if (platformConsts.awsOutlet1617.includes(accessory.context.gvModel)) {
|
|
1435
|
+
data.awsParams = {
|
|
1436
|
+
cmd: 'turn',
|
|
1437
|
+
data: { val: params.value === 'on' ? 17 : 16 },
|
|
1438
|
+
}
|
|
1439
|
+
} else {
|
|
1440
|
+
data.awsParams = {
|
|
1441
|
+
cmd: 'turn',
|
|
1442
|
+
data: { val: params.value === 'on' ? 1 : 0 },
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
break
|
|
1446
|
+
}
|
|
1447
|
+
case 'stateHumi':
|
|
1448
|
+
case 'statePuri': {
|
|
1449
|
+
data.awsParams = {
|
|
1450
|
+
cmd: 'turn',
|
|
1451
|
+
data: { val: params.value },
|
|
1452
|
+
}
|
|
1453
|
+
data.bleParams = {
|
|
1454
|
+
cmd: 0x01,
|
|
1455
|
+
data: params.value ? 0x1 : 0x0,
|
|
1456
|
+
}
|
|
1457
|
+
break
|
|
1458
|
+
}
|
|
1459
|
+
case 'stateHeat': {
|
|
1460
|
+
const fullCode = params.value ? 'MwEBAAAAAAAAAAAAAAAAAAAAADM=' : 'MwEAAAAAAAAAAAAAAAAAAAAAADI='
|
|
1461
|
+
data.awsParams = {
|
|
1462
|
+
cmd: 'multiSync',
|
|
1463
|
+
data: { command: [fullCode] },
|
|
1464
|
+
}
|
|
1465
|
+
data.bleParams = {
|
|
1466
|
+
cmd: 'ptReal',
|
|
1467
|
+
data: base64ToHex(fullCode),
|
|
1468
|
+
}
|
|
1469
|
+
break
|
|
1470
|
+
}
|
|
1471
|
+
case 'multiSync':
|
|
1472
|
+
case 'ptReal':
|
|
1473
|
+
data.awsParams = {
|
|
1474
|
+
cmd: params.cmd,
|
|
1475
|
+
data: { command: [params.value] },
|
|
1476
|
+
}
|
|
1477
|
+
data.bleParams = {
|
|
1478
|
+
cmd: 'ptReal',
|
|
1479
|
+
data: base64ToHex(params.value),
|
|
1480
|
+
}
|
|
1481
|
+
break
|
|
1482
|
+
case 'brightness': {
|
|
1483
|
+
/*
|
|
1484
|
+
BRIGHTNESS
|
|
1485
|
+
<= INPUT params.value INT in range [0, 100]
|
|
1486
|
+
AWS needs { cmd: 'brightness', data: { val: INT[0, 254] } }
|
|
1487
|
+
BLE needs { cmd: 0x04, data: (based on) INT[0, 100] }
|
|
1488
|
+
LAN needs { cmd: 'brightness', data: { value: INT[0, 100] } }
|
|
1489
|
+
*/
|
|
1490
|
+
data.awsParams = {
|
|
1491
|
+
cmd: 'brightness',
|
|
1492
|
+
data: {
|
|
1493
|
+
val: accessory.context.awsBrightnessNoScale
|
|
1494
|
+
? params.value
|
|
1495
|
+
: Math.round(params.value * 2.54),
|
|
1496
|
+
},
|
|
1497
|
+
}
|
|
1498
|
+
data.bleParams = {
|
|
1499
|
+
cmd: 0x04,
|
|
1500
|
+
data: Math.floor(
|
|
1501
|
+
platformConsts.bleBrightnessNoScale.includes(accessory.context.gvModel)
|
|
1502
|
+
? (params.value / 100) * 0x64
|
|
1503
|
+
: (params.value / 100) * 0xFF,
|
|
1504
|
+
),
|
|
1505
|
+
}
|
|
1506
|
+
data.lanParams = {
|
|
1507
|
+
cmd: 'brightness',
|
|
1508
|
+
data: {
|
|
1509
|
+
value: params.value,
|
|
1510
|
+
},
|
|
1511
|
+
}
|
|
1512
|
+
break
|
|
1513
|
+
}
|
|
1514
|
+
case 'color': {
|
|
1515
|
+
/*
|
|
1516
|
+
COLOUR (RGB)
|
|
1517
|
+
<= INPUT params.value OBJ with properties { r, g, b }
|
|
1518
|
+
AWS needs { cmd: 'color', data: { red, green, blue } }
|
|
1519
|
+
BLE needs { cmd: 0x05, data: [0x02, r, g, b] }
|
|
1520
|
+
H613B needs { cmd: 0x05, data: [0x0D, r, g, b] }
|
|
1521
|
+
LAN needs { cmd: 'colorwc', data: { color: {r, g, b}, colorTemInKelvin: 0 } }
|
|
1522
|
+
*/
|
|
1523
|
+
switch (accessory.context.awsColourMode) {
|
|
1524
|
+
case 'rgb': {
|
|
1525
|
+
data.awsParams = {
|
|
1526
|
+
cmd: 'color',
|
|
1527
|
+
data: params.value,
|
|
1528
|
+
}
|
|
1529
|
+
break
|
|
1530
|
+
}
|
|
1531
|
+
case 'redgreenblue': {
|
|
1532
|
+
data.awsParams = {
|
|
1533
|
+
cmd: 'color',
|
|
1534
|
+
data: {
|
|
1535
|
+
red: params.value.r,
|
|
1536
|
+
green: params.value.g,
|
|
1537
|
+
blue: params.value.b,
|
|
1538
|
+
},
|
|
1539
|
+
}
|
|
1540
|
+
break
|
|
1541
|
+
}
|
|
1542
|
+
default: {
|
|
1543
|
+
data.awsParams = {
|
|
1544
|
+
cmd: 'colorwc',
|
|
1545
|
+
data: {
|
|
1546
|
+
color: {
|
|
1547
|
+
r: params.value.r,
|
|
1548
|
+
g: params.value.g,
|
|
1549
|
+
b: params.value.b,
|
|
1550
|
+
red: params.value.r,
|
|
1551
|
+
green: params.value.g,
|
|
1552
|
+
blue: params.value.b,
|
|
1553
|
+
},
|
|
1554
|
+
colorTemInKelvin: 0,
|
|
1555
|
+
},
|
|
1556
|
+
}
|
|
1557
|
+
break
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
let firstCommand = [0x02]
|
|
1562
|
+
let lastCommand = []
|
|
1563
|
+
if (platformConsts.bleColourD.includes(accessory.context.gvModel)) {
|
|
1564
|
+
firstCommand = [0x0D]
|
|
1565
|
+
} else if (platformConsts.bleColour1501.includes(accessory.context.gvModel)) {
|
|
1566
|
+
firstCommand = [0x15, 0x01]
|
|
1567
|
+
lastCommand = [
|
|
1568
|
+
0x00,
|
|
1569
|
+
0x00,
|
|
1570
|
+
0x00,
|
|
1571
|
+
0x00,
|
|
1572
|
+
0x00,
|
|
1573
|
+
0xFF,
|
|
1574
|
+
0x7F,
|
|
1575
|
+
]
|
|
1576
|
+
}
|
|
1577
|
+
data.bleParams = {
|
|
1578
|
+
cmd: 0x05,
|
|
1579
|
+
data: [
|
|
1580
|
+
...firstCommand,
|
|
1581
|
+
params.value.r,
|
|
1582
|
+
params.value.g,
|
|
1583
|
+
params.value.b,
|
|
1584
|
+
...lastCommand,
|
|
1585
|
+
],
|
|
1586
|
+
}
|
|
1587
|
+
data.lanParams = {
|
|
1588
|
+
cmd: 'colorwc',
|
|
1589
|
+
data: {
|
|
1590
|
+
color: {
|
|
1591
|
+
r: params.value.r,
|
|
1592
|
+
g: params.value.g,
|
|
1593
|
+
b: params.value.b,
|
|
1594
|
+
},
|
|
1595
|
+
colorTemInKelvin: 0,
|
|
1596
|
+
},
|
|
1597
|
+
}
|
|
1598
|
+
break
|
|
1599
|
+
}
|
|
1600
|
+
case 'colorTem': {
|
|
1601
|
+
/*
|
|
1602
|
+
COLOUR TEMP (KELVIN)
|
|
1603
|
+
<= INPUT params.value INT in [2000, 7143]
|
|
1604
|
+
AWS needs { cmd: 'colorTem', data: { color: {},"colorTemInKelvin": } }
|
|
1605
|
+
BLE needs { cmd: 0x05, data: [0x02, 0xff, 0xff, 0xff, 0x01, r, g, b] }
|
|
1606
|
+
LAN needs { cmd: 'colorwc', data: { color: {r, g, b}, colorTemInKelvin: INT[2000, 9000] } }
|
|
1607
|
+
*/
|
|
1608
|
+
const [r, g, b] = k2rgb(params.value)
|
|
1609
|
+
switch (accessory.context.awsColourMode) {
|
|
1610
|
+
case 'rgb': {
|
|
1611
|
+
data.awsParams = {
|
|
1612
|
+
cmd: 'colorTem',
|
|
1613
|
+
data: {
|
|
1614
|
+
colorTemInKelvin: params.value,
|
|
1615
|
+
color: {
|
|
1616
|
+
r,
|
|
1617
|
+
g,
|
|
1618
|
+
b,
|
|
1619
|
+
},
|
|
1620
|
+
},
|
|
1621
|
+
}
|
|
1622
|
+
break
|
|
1623
|
+
}
|
|
1624
|
+
case 'redgreenblue': {
|
|
1625
|
+
data.awsParams = {
|
|
1626
|
+
cmd: 'colorTem',
|
|
1627
|
+
data: {
|
|
1628
|
+
color: {
|
|
1629
|
+
red: r,
|
|
1630
|
+
green: g,
|
|
1631
|
+
blue: b,
|
|
1632
|
+
},
|
|
1633
|
+
colorTemInKelvin: params.value,
|
|
1634
|
+
},
|
|
1635
|
+
}
|
|
1636
|
+
break
|
|
1637
|
+
}
|
|
1638
|
+
default: {
|
|
1639
|
+
data.awsParams = {
|
|
1640
|
+
cmd: 'colorwc',
|
|
1641
|
+
data: {
|
|
1642
|
+
color: {
|
|
1643
|
+
r,
|
|
1644
|
+
g,
|
|
1645
|
+
b,
|
|
1646
|
+
},
|
|
1647
|
+
colorTemInKelvin: params.value,
|
|
1648
|
+
},
|
|
1649
|
+
}
|
|
1650
|
+
break
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
data.bleParams = {
|
|
1655
|
+
cmd: 0x05,
|
|
1656
|
+
data: [
|
|
1657
|
+
platformConsts.bleColourD.includes(accessory.context.gvModel) ? 0x0D : 0x02,
|
|
1658
|
+
0xFF,
|
|
1659
|
+
0xFF,
|
|
1660
|
+
0xFF,
|
|
1661
|
+
0x01,
|
|
1662
|
+
r,
|
|
1663
|
+
g,
|
|
1664
|
+
b,
|
|
1665
|
+
],
|
|
1666
|
+
}
|
|
1667
|
+
data.lanParams = {
|
|
1668
|
+
cmd: 'colorwc',
|
|
1669
|
+
data: {
|
|
1670
|
+
color: {
|
|
1671
|
+
r,
|
|
1672
|
+
g,
|
|
1673
|
+
b,
|
|
1674
|
+
},
|
|
1675
|
+
colorTemInKelvin: params.value,
|
|
1676
|
+
},
|
|
1677
|
+
}
|
|
1678
|
+
break
|
|
1679
|
+
}
|
|
1680
|
+
case 'rgbScene': {
|
|
1681
|
+
// We get `params.value` as an array [awsCode, bleCode] either could be undefined
|
|
1682
|
+
// We get the AWS scene code in a string format, commands separated by a comma (base64)
|
|
1683
|
+
// The BLE scene code is still base64 but just one command (no commas)
|
|
1684
|
+
if (params.value[0]) {
|
|
1685
|
+
const splitCode = params.value[0].split(',')
|
|
1686
|
+
data.awsParams = {
|
|
1687
|
+
cmd: 'ptReal',
|
|
1688
|
+
data: {
|
|
1689
|
+
command: splitCode,
|
|
1690
|
+
},
|
|
1691
|
+
}
|
|
1692
|
+
data.lanParams = {
|
|
1693
|
+
cmd: 'ptReal',
|
|
1694
|
+
data: {
|
|
1695
|
+
command: splitCode,
|
|
1696
|
+
},
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
if (params.value[1]) {
|
|
1700
|
+
data.bleParams = {
|
|
1701
|
+
cmd: 'ptReal',
|
|
1702
|
+
data: params.value[1],
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
break
|
|
1706
|
+
}
|
|
1707
|
+
default:
|
|
1708
|
+
throw new Error('Invalid command')
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
// *********************************** //
|
|
1712
|
+
// ********* CONNECTION: LAN ********* //
|
|
1713
|
+
// *********************************** //
|
|
1714
|
+
// Check to see if we have the option to use LAN.
|
|
1715
|
+
if (accessory.context.useLANControl && data.lanParams) {
|
|
1716
|
+
try {
|
|
1717
|
+
await this.lanClient.updateDevice(accessory, data.lanParams)
|
|
1718
|
+
return true
|
|
1719
|
+
} catch (err) {
|
|
1720
|
+
accessory.logWarn(`${platformLang.notLANSent} ${parseError(err, [platformLang.lanDevNotFound])}`)
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
// *********************************** //
|
|
1725
|
+
// ********* CONNECTION: AWS ********* //
|
|
1726
|
+
// *********************************** //
|
|
1727
|
+
// Check to see if we have the option to use AWS
|
|
1728
|
+
if (accessory.context.useAWSControl && data.awsParams) {
|
|
1729
|
+
try {
|
|
1730
|
+
await this.awsClient.updateDevice(accessory, data.awsParams)
|
|
1731
|
+
return true
|
|
1732
|
+
} catch (err) {
|
|
1733
|
+
// Print the reason to the log if in debug mode, it's not always necessarily an error
|
|
1734
|
+
accessory.logWarn(`${platformLang.notAWSSent} ${parseError(err, [platformLang.notAWSConn])}`)
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
// We can return now, if there is no option to use BLE
|
|
1739
|
+
if (!data.bleParams) {
|
|
1740
|
+
return true
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
// We use a queue for BLE connections for different reasons
|
|
1744
|
+
// BLE: We don't want to send multiple commands at once, as it can cause issues
|
|
1745
|
+
return this.queue.add(async () => {
|
|
1746
|
+
// *********************************** //
|
|
1747
|
+
// ********* CONNECTION: BLE ********* //
|
|
1748
|
+
// *********************************** //
|
|
1749
|
+
// Try bluetooth if enabled, and we have the option to use it
|
|
1750
|
+
if (accessory.context.useBLEControl && data.bleParams) {
|
|
1751
|
+
try {
|
|
1752
|
+
// Send the command to the bluetooth client to send
|
|
1753
|
+
await this.bleClient.updateDevice(accessory, data.bleParams)
|
|
1754
|
+
return true
|
|
1755
|
+
} catch (err) {
|
|
1756
|
+
// Bluetooth didn't work or not enabled
|
|
1757
|
+
accessory.logDebugWarn(`${platformLang.notBLESent} ${parseError(err, [platformLang.bleTimeout])}`)
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
throw new Error(platformLang.noConnMethod)
|
|
1761
|
+
})
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
receiveUpdateLAN(accessoryId, params, ipAddress) {
|
|
1765
|
+
devicesInHB.forEach((accessory) => {
|
|
1766
|
+
if (accessory.context.gvDeviceId === accessoryId) {
|
|
1767
|
+
let update = false
|
|
1768
|
+
|
|
1769
|
+
// Is LAN enabled for this accessory already?
|
|
1770
|
+
if (!accessory.context.useLANControl) {
|
|
1771
|
+
accessory.context.hasLANControl = true
|
|
1772
|
+
accessory.context.useLANControl = true
|
|
1773
|
+
update = true
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
// If we have an IP address, update the IP address
|
|
1777
|
+
if (accessory.context.ipAddress !== ipAddress) {
|
|
1778
|
+
accessory.context.ipAddress = ipAddress
|
|
1779
|
+
if (accessory.log) {
|
|
1780
|
+
accessory.log(`[LAN] ${platformLang.curIP} [${ipAddress}]`)
|
|
1781
|
+
}
|
|
1782
|
+
update = true
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
if (update) {
|
|
1786
|
+
this.api.updatePlatformAccessories([accessory])
|
|
1787
|
+
devicesInHB.set(accessory.UUID, accessory)
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
if (Object.keys(params).length > 0) {
|
|
1791
|
+
this.receiveDeviceUpdate(accessory, {
|
|
1792
|
+
source: 'LAN',
|
|
1793
|
+
state: params, // matches the structure of the AWS payload
|
|
1794
|
+
})
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
})
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
receiveUpdateAWS(payload) {
|
|
1801
|
+
const accessoryUUID = this.api.hap.uuid.generate(payload.device)
|
|
1802
|
+
const accessory = devicesInHB.get(accessoryUUID)
|
|
1803
|
+
this.receiveDeviceUpdate(accessory, {
|
|
1804
|
+
source: 'AWS',
|
|
1805
|
+
...payload,
|
|
1806
|
+
})
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
receiveDeviceUpdate(accessory, params) {
|
|
1810
|
+
// No need to continue if the accessory doesn't have the receiver function setup
|
|
1811
|
+
if (!accessory?.control?.externalUpdate) {
|
|
1812
|
+
return
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
// Log the incoming update
|
|
1816
|
+
accessory.logDebug(`[${params.source}] ${platformLang.receivingUpdate} ${JSON.stringify(params)}`)
|
|
1817
|
+
|
|
1818
|
+
// Standardise the object for the receiver function
|
|
1819
|
+
const data = {}
|
|
1820
|
+
|
|
1821
|
+
/*
|
|
1822
|
+
ON/OFF
|
|
1823
|
+
*/
|
|
1824
|
+
if (params.state && hasProperty(params.state, 'onOff')) {
|
|
1825
|
+
if (platformConsts.models.switchDouble.includes(accessory.context.gvModel)) {
|
|
1826
|
+
switch (params.state.onOff) {
|
|
1827
|
+
case 0:
|
|
1828
|
+
data.state = ['off', 'off']
|
|
1829
|
+
break
|
|
1830
|
+
case 1:
|
|
1831
|
+
data.state = ['on', 'off']
|
|
1832
|
+
break
|
|
1833
|
+
case 2:
|
|
1834
|
+
data.state = ['off', 'on']
|
|
1835
|
+
break
|
|
1836
|
+
case 3:
|
|
1837
|
+
data.state = ['on', 'on']
|
|
1838
|
+
break
|
|
1839
|
+
}
|
|
1840
|
+
} else {
|
|
1841
|
+
data.state = [1, 17].includes(params.state.onOff) ? 'on' : 'off'
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
/*
|
|
1846
|
+
BRIGHTNESS
|
|
1847
|
+
*/
|
|
1848
|
+
if (params.state && hasProperty(params.state, 'brightness')) {
|
|
1849
|
+
if (params.source === 'LAN') {
|
|
1850
|
+
data.brightness = params.state.brightness
|
|
1851
|
+
} else if (params.source === 'AWS') {
|
|
1852
|
+
data.brightness = accessory.context.awsBrightnessNoScale
|
|
1853
|
+
? params.state.brightness
|
|
1854
|
+
: Math.round(params.state.brightness / 2.54)
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
// Sometimes Govee can provide a value out of range of [0, 100]
|
|
1859
|
+
if (hasProperty(data, 'brightness')) {
|
|
1860
|
+
data.brightness = Math.max(Math.min(data.brightness, 100), 0)
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
/*
|
|
1864
|
+
COLOUR (RGB)
|
|
1865
|
+
*/
|
|
1866
|
+
if (params.state && hasProperty(params.state, 'color')) {
|
|
1867
|
+
data.rgb = params.state.color
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
/*
|
|
1871
|
+
COLOUR TEMP (KELVIN)
|
|
1872
|
+
*/
|
|
1873
|
+
if (params.state && params.state.colorTemInKelvin) {
|
|
1874
|
+
// Ignore values of 0 in above check
|
|
1875
|
+
data.kelvin = params.state.colorTemInKelvin
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
// It seems sometimes Govee can provide a value out of range so just clamp it
|
|
1879
|
+
if (hasProperty(data, 'kelvin') && (data.kelvin < 2000 || data.kelvin > 7143)) {
|
|
1880
|
+
// Govee can go to kelvin 9000 but homekit only supports to 7143, try to keep the user logging nice
|
|
1881
|
+
if (data.kelvin > 9000) {
|
|
1882
|
+
accessory.logDebug(`govee provided a kelvin out of range [${data.kelvin}]`)
|
|
1883
|
+
}
|
|
1884
|
+
data.kelvin = Math.max(Math.min(data.kelvin, 7143), 2000)
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
/*
|
|
1888
|
+
BATTERY (leak and thermo sensors)
|
|
1889
|
+
*/
|
|
1890
|
+
if (hasProperty(params, 'battery')) {
|
|
1891
|
+
data.battery = Math.min(Math.max(params.battery, 0), 100)
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
/*
|
|
1895
|
+
LEAK DETECTED (leak sensors)
|
|
1896
|
+
*/
|
|
1897
|
+
if (hasProperty(params, 'leakDetected')) {
|
|
1898
|
+
data.leakDetected = params.leakDetected
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
/*
|
|
1902
|
+
CURRENT TEMPERATURE
|
|
1903
|
+
*/
|
|
1904
|
+
if (hasProperty(params, 'temperature')) {
|
|
1905
|
+
data.temperature = params.temperature
|
|
1906
|
+
} else if (params?.state?.sta && hasProperty(params.state.sta, 'curTem')) {
|
|
1907
|
+
data.temperature = params.state.sta.curTem
|
|
1908
|
+
}
|
|
1909
|
+
if (hasProperty(params, 'temperatureF')) {
|
|
1910
|
+
data.temperatureF = params.temperatureF
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
/*
|
|
1914
|
+
SET TEMPERATURE
|
|
1915
|
+
*/
|
|
1916
|
+
if (params.state?.sta && hasProperty(params.state.sta, 'setTem')) {
|
|
1917
|
+
data.setTemperature = params.state.sta.setTem
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
/*
|
|
1921
|
+
HUMIDITY (thermo sensors)
|
|
1922
|
+
*/
|
|
1923
|
+
if (hasProperty(params, 'humidity')) {
|
|
1924
|
+
data.humidity = params.humidity
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
/*
|
|
1928
|
+
COMMANDS (these can be light scenes)
|
|
1929
|
+
*/
|
|
1930
|
+
if (params.commands) {
|
|
1931
|
+
data.commands = params.commands
|
|
1932
|
+
params.baseCmd = 'none'
|
|
1933
|
+
} else if (params.op) {
|
|
1934
|
+
if (params.op.command) {
|
|
1935
|
+
data.commands = params.op.command
|
|
1936
|
+
data.baseCmd = 'op'
|
|
1937
|
+
} else if (params.op.mode && Array.isArray(params.op.value)) {
|
|
1938
|
+
data.commands = params.op.value
|
|
1939
|
+
data.baseCmd = 'opMode'
|
|
1940
|
+
} else if (params.op.opcode === 'mode' && Array.isArray(params.op.modeValue)) {
|
|
1941
|
+
data.commands = params.op.modeValue
|
|
1942
|
+
data.baseCmd = 'opCodeMode'
|
|
1943
|
+
}
|
|
1944
|
+
} else if (params.bulb) {
|
|
1945
|
+
data.commands = params.bulb
|
|
1946
|
+
data.baseCmd = 'bulb'
|
|
1947
|
+
} else if (params.data?.op === 'mode' && Array.isArray(params.data.value)) {
|
|
1948
|
+
data.commands = params.data.value
|
|
1949
|
+
data.baseCmd = 'opMode'
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
// Send the update to the receiver function
|
|
1953
|
+
data.source = params.source
|
|
1954
|
+
|
|
1955
|
+
// We may have received a command which we don't recognise
|
|
1956
|
+
// We can probably check by seeing if the data object has just one property
|
|
1957
|
+
if (Object.keys(data).length > 1) {
|
|
1958
|
+
try {
|
|
1959
|
+
accessory.control.externalUpdate(data)
|
|
1960
|
+
} catch (err) {
|
|
1961
|
+
this.log.warn('[%s] %s %s.', accessory.displayName, platformLang.devNotUpdated, parseError(err))
|
|
1962
|
+
}
|
|
1963
|
+
} else {
|
|
1964
|
+
accessory.logDebugWarn(`[${params.source}] ${platformLang.unknownCommand}: ${JSON.stringify(params)}`)
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
}
|