@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.
Files changed (78) hide show
  1. package/CHANGELOG.md +1937 -0
  2. package/LICENSE +21 -0
  3. package/README.md +72 -0
  4. package/config.schema.json +1727 -0
  5. package/eslint.config.js +49 -0
  6. package/lib/connection/aws.js +174 -0
  7. package/lib/connection/ble.js +208 -0
  8. package/lib/connection/cert/AmazonRootCA1.pem +20 -0
  9. package/lib/connection/http.js +240 -0
  10. package/lib/connection/lan.js +284 -0
  11. package/lib/device/cooler-single.js +300 -0
  12. package/lib/device/dehumidifier-H7150.js +182 -0
  13. package/lib/device/dehumidifier-H7151.js +157 -0
  14. package/lib/device/diffuser-H7161.js +117 -0
  15. package/lib/device/diffuser-H7162.js +117 -0
  16. package/lib/device/fan-H7100.js +274 -0
  17. package/lib/device/fan-H7101.js +330 -0
  18. package/lib/device/fan-H7102.js +274 -0
  19. package/lib/device/fan-H7105.js +503 -0
  20. package/lib/device/fan-H7106.js +274 -0
  21. package/lib/device/fan-H7111.js +335 -0
  22. package/lib/device/heater-single.js +300 -0
  23. package/lib/device/heater1a.js +353 -0
  24. package/lib/device/heater1b.js +616 -0
  25. package/lib/device/heater2.js +838 -0
  26. package/lib/device/humidifier-H7140.js +224 -0
  27. package/lib/device/humidifier-H7141.js +257 -0
  28. package/lib/device/humidifier-H7142.js +522 -0
  29. package/lib/device/humidifier-H7143.js +157 -0
  30. package/lib/device/humidifier-H7148.js +157 -0
  31. package/lib/device/humidifier-H7160.js +446 -0
  32. package/lib/device/ice-maker-H7162.js +46 -0
  33. package/lib/device/index.js +105 -0
  34. package/lib/device/kettle.js +269 -0
  35. package/lib/device/light-switch.js +86 -0
  36. package/lib/device/light.js +617 -0
  37. package/lib/device/outlet-double.js +121 -0
  38. package/lib/device/outlet-single.js +172 -0
  39. package/lib/device/outlet-triple.js +160 -0
  40. package/lib/device/purifier-H7120.js +336 -0
  41. package/lib/device/purifier-H7121.js +336 -0
  42. package/lib/device/purifier-H7122.js +449 -0
  43. package/lib/device/purifier-H7123.js +411 -0
  44. package/lib/device/purifier-H7124.js +411 -0
  45. package/lib/device/purifier-H7126.js +296 -0
  46. package/lib/device/purifier-H7127.js +296 -0
  47. package/lib/device/purifier-H712C.js +296 -0
  48. package/lib/device/purifier-single.js +119 -0
  49. package/lib/device/sensor-button.js +22 -0
  50. package/lib/device/sensor-contact.js +22 -0
  51. package/lib/device/sensor-leak.js +87 -0
  52. package/lib/device/sensor-monitor.js +190 -0
  53. package/lib/device/sensor-presence.js +53 -0
  54. package/lib/device/sensor-thermo.js +144 -0
  55. package/lib/device/sensor-thermo4.js +55 -0
  56. package/lib/device/switch-double.js +121 -0
  57. package/lib/device/switch-single.js +95 -0
  58. package/lib/device/switch-triple.js +160 -0
  59. package/lib/device/tap-single.js +108 -0
  60. package/lib/device/template.js +43 -0
  61. package/lib/device/tv-single.js +84 -0
  62. package/lib/device/valve-single.js +155 -0
  63. package/lib/fakegato/LICENSE +21 -0
  64. package/lib/fakegato/fakegato-history.js +814 -0
  65. package/lib/fakegato/fakegato-storage.js +108 -0
  66. package/lib/fakegato/fakegato-timer.js +125 -0
  67. package/lib/fakegato/uuid.js +27 -0
  68. package/lib/homebridge-ui/public/index.html +433 -0
  69. package/lib/homebridge-ui/server.js +10 -0
  70. package/lib/index.js +8 -0
  71. package/lib/platform.js +1967 -0
  72. package/lib/utils/colour.js +564 -0
  73. package/lib/utils/constants.js +579 -0
  74. package/lib/utils/custom-chars.js +225 -0
  75. package/lib/utils/eve-chars.js +68 -0
  76. package/lib/utils/functions.js +117 -0
  77. package/lib/utils/lang-en.js +131 -0
  78. package/package.json +75 -0
@@ -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
+ }