@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,838 @@
1
+ import { hs2rgb, rgb2hs } from '../utils/colour.js'
2
+ import {
3
+ base64ToHex,
4
+ farToCen,
5
+ generateCodeFromHexValues,
6
+ generateRandomString,
7
+ getTwoItemPosition,
8
+ hasProperty,
9
+ hexToDecimal,
10
+ hexToTwoItems,
11
+ nearestHalf,
12
+ parseError,
13
+ sleep,
14
+ } from '../utils/functions.js'
15
+ import platformLang from '../utils/lang-en.js'
16
+
17
+ /*
18
+ H7131 and H7132
19
+ {
20
+ "mode": {
21
+ "options": [
22
+ {
23
+ "name": "Low",
24
+ "value": "1"
25
+ },
26
+ {
27
+ "name": "Medium",
28
+ "value": "2"
29
+ },
30
+ {
31
+ "name": "High",
32
+ "value": "3"
33
+ },
34
+ {
35
+ "name": "Fan",
36
+ "value": "4"
37
+ },
38
+ {
39
+ "name": "Auto",
40
+ "value": "5"
41
+ }
42
+ ]
43
+ }
44
+ }
45
+ */
46
+ export default class {
47
+ constructor(platform, accessory) {
48
+ // Set up variables from the platform
49
+ this.hapChar = platform.api.hap.Characteristic
50
+ this.hapErr = platform.api.hap.HapStatusError
51
+ this.hapServ = platform.api.hap.Service
52
+ this.platform = platform
53
+
54
+ // Set up variables from the accessory
55
+ this.accessory = accessory
56
+
57
+ // Set up objects
58
+ this.speedCode = {
59
+ 25: 'OgUJAAAAAAAAAAAAAAAAAAAAADY=',
60
+ 50: 'OgUBAQAAAAAAAAAAAAAAAAAAAD8=',
61
+ 75: 'OgUBAgAAAAAAAAAAAAAAAAAAADw=',
62
+ 100: 'OgUBAwAAAAAAAAAAAAAAAAAAAD0=',
63
+ }
64
+
65
+ this.speedCodeLabel = {
66
+ 0: 'auto',
67
+ 25: 'fan-only',
68
+ 50: 'low',
69
+ 75: 'medium',
70
+ 100: 'high',
71
+ }
72
+
73
+ this.tempCodeAuto = {
74
+ 5: 'MwUDAZAEAAAAAAAAAAAAAAAAAKA=',
75
+ 6: 'MwUDAZBoAAAAAAAAAAAAAAAAAMw=',
76
+ 7: 'MwUDAZEwAAAAAAAAAAAAAAAAAJU=',
77
+ 8: 'MwUDAZH4AAAAAAAAAAAAAAAAAF0=',
78
+ 9: 'MwUDAZLAAAAAAAAAAAAAAAAAAGY=',
79
+ 10: 'MwUDAZOIAAAAAAAAAAAAAAAAAC8=',
80
+ 11: 'MwUDAZPsAAAAAAAAAAAAAAAAAEs=',
81
+ 12: 'MwUDAZS0AAAAAAAAAAAAAAAAABQ=',
82
+ 13: 'MwUDAZV8AAAAAAAAAAAAAAAAAN0=',
83
+ 14: 'MwUDAZZEAAAAAAAAAAAAAAAAAOY=',
84
+ 15: 'MwUDAZcMAAAAAAAAAAAAAAAAAK8=',
85
+ 16: 'MwUDAZdwAAAAAAAAAAAAAAAAANM=',
86
+ 17: 'MwUDAZg4AAAAAAAAAAAAAAAAAJQ=',
87
+ 18: 'MwUDAZkAAAAAAAAAAAAAAAAAAK0=',
88
+ 19: 'MwUDAZnIAAAAAAAAAAAAAAAAAGU=',
89
+ 20: 'MwUDAZqQAAAAAAAAAAAAAAAAAD4=',
90
+ 21: 'MwUDAZr0AAAAAAAAAAAAAAAAAFo=',
91
+ 22: 'MwUDAZu8AAAAAAAAAAAAAAAAABM=',
92
+ 23: 'MwUDAZyEAAAAAAAAAAAAAAAAACw=',
93
+ 24: 'MwUDAZ1MAAAAAAAAAAAAAAAAAOU=',
94
+ 25: 'MwUDAZ4UAAAAAAAAAAAAAAAAAL4=',
95
+ 26: 'MwUDAZ54AAAAAAAAAAAAAAAAANI=',
96
+ 27: 'MwUDAZ9AAAAAAAAAAAAAAAAAAOs=',
97
+ 28: 'MwUDAaAIAAAAAAAAAAAAAAAAAJw=',
98
+ 29: 'MwUDAaDQAAAAAAAAAAAAAAAAAEQ=',
99
+ 30: 'MwUDAaGYAAAAAAAAAAAAAAAAAA0=',
100
+ }
101
+
102
+ this.tempCodeAutoTurn = {
103
+ 5: 'OgUDAZAEAAAAAAAAAAAAAAAAAKk=',
104
+ 6: 'OgUDAZBoAAAAAAAAAAAAAAAAAMU=',
105
+ 7: 'OgUDAZEwAAAAAAAAAAAAAAAAAJw=',
106
+ 8: 'OgUDAZH4AAAAAAAAAAAAAAAAAFQ=',
107
+ 9: 'OgUDAZLAAAAAAAAAAAAAAAAAAG8=',
108
+ 10: 'OgUDAZOIAAAAAAAAAAAAAAAAACY=',
109
+ 11: 'OgUDAZPsAAAAAAAAAAAAAAAAAEI=',
110
+ 12: 'OgUDAZS0AAAAAAAAAAAAAAAAAB0=',
111
+ 13: 'OgUDAZV8AAAAAAAAAAAAAAAAANQ=',
112
+ 14: 'OgUDAZZEAAAAAAAAAAAAAAAAAO8=',
113
+ 15: 'OgUDAZcMAAAAAAAAAAAAAAAAAKY=',
114
+ 16: 'OgUDAZdwAAAAAAAAAAAAAAAAANo=',
115
+ 17: 'OgUDAZg4AAAAAAAAAAAAAAAAAJ0=',
116
+ 18: 'OgUDAZkAAAAAAAAAAAAAAAAAAKQ=',
117
+ 19: 'OgUDAZnIAAAAAAAAAAAAAAAAAGw=',
118
+ 20: 'OgUDAZqQAAAAAAAAAAAAAAAAADc=',
119
+ 21: 'OgUDAZr0AAAAAAAAAAAAAAAAAFM=',
120
+ 22: 'OgUDAZu8AAAAAAAAAAAAAAAAABo=',
121
+ 23: 'OgUDAZyEAAAAAAAAAAAAAAAAACU=',
122
+ 24: 'OgUDAZ1MAAAAAAAAAAAAAAAAAOw=',
123
+ 25: 'OgUDAZ4UAAAAAAAAAAAAAAAAALc=',
124
+ 26: 'OgUDAZ54AAAAAAAAAAAAAAAAANs=',
125
+ 27: 'OgUDAZ9AAAAAAAAAAAAAAAAAAOI=',
126
+ 28: 'OgUDAaAIAAAAAAAAAAAAAAAAAJU=',
127
+ 29: 'OgUDAaDQAAAAAAAAAAAAAAAAAE0=',
128
+ 30: 'OgUDAaGYAAAAAAAAAAAAAAAAAAQ=',
129
+ }
130
+
131
+ // Fix v9.4.0 -> v9.4.1 bug
132
+ // Remove any light service if it exists
133
+ if (this.accessory.getService(this.hapServ.Lightbulb)) {
134
+ this.accessory.removeService(this.accessory.getService(this.hapServ.Lightbulb))
135
+ }
136
+
137
+ // Add the heater service if it doesn't already exist
138
+ this.service = this.accessory.getService(this.hapServ.HeaterCooler)
139
+ if (!this.service) {
140
+ this.service = this.accessory.addService(this.hapServ.HeaterCooler)
141
+ this.service.updateCharacteristic(this.hapChar.CurrentTemperature, 20)
142
+ this.service.updateCharacteristic(this.hapChar.HeatingThresholdTemperature, 20)
143
+ }
144
+
145
+ // Add the fan service if it doesn't already exist
146
+ this.fanService = this.accessory.getService(this.hapServ.Fan)
147
+ || this.accessory.addService(this.hapServ.Fan)
148
+
149
+ // Add the night light service if it doesn't already exist
150
+ this.lightService = this.accessory.getService(this.hapServ.Lightbulb)
151
+ || this.accessory.addService(this.hapServ.Lightbulb)
152
+
153
+ // Add the set handler to the heater active characteristic
154
+ this.service
155
+ .getCharacteristic(this.hapChar.Active)
156
+ .onSet(async value => this.internalStateUpdate(value))
157
+ this.cacheState = this.service.getCharacteristic(this.hapChar.Active).value === 1 ? 'on' : 'off'
158
+
159
+ // Add options to the heater target state characteristic
160
+ this.service
161
+ .getCharacteristic(this.hapChar.TargetHeaterCoolerState)
162
+ .setProps({
163
+ minValue: 0,
164
+ maxValue: 1,
165
+ validValues: [0, 1],
166
+ })
167
+ .onSet(async value => this.internalModeUpdate(value))
168
+
169
+ this.cacheTemp = this.service.getCharacteristic(this.hapChar.CurrentTemperature).value
170
+
171
+ // Add the set handler and a range to the heater target temperature characteristic
172
+ this.service
173
+ .getCharacteristic(this.hapChar.HeatingThresholdTemperature)
174
+ .setProps({
175
+ minValue: 5,
176
+ maxValue: 30,
177
+ minStep: 1,
178
+ })
179
+ .onSet(async value => this.internalTempUpdate(value))
180
+ this.cacheTarg = this.service.getCharacteristic(this.hapChar.HeatingThresholdTemperature).value
181
+
182
+ // Add the set handler to the heater swing mode characteristic (for oscillation)
183
+ this.service
184
+ .getCharacteristic(this.hapChar.SwingMode)
185
+ .onSet(async value => this.internalSwingUpdate(value))
186
+ this.cacheSwing = this.service.getCharacteristic(this.hapChar.SwingMode).value === 1 ? 'on' : 'off'
187
+
188
+ // Add the set handler to the heater lock characteristic (for oscillation)
189
+ this.service
190
+ .getCharacteristic(this.hapChar.LockPhysicalControls)
191
+ .onSet(async value => this.internalLockUpdate(value))
192
+ this.cacheLock = this.service.getCharacteristic(this.hapChar.LockPhysicalControls).value === 1 ? 'on' : 'off'
193
+
194
+ // Add the set handler to the fan on/off characteristic
195
+ this.fanService
196
+ .getCharacteristic(this.hapChar.On)
197
+ .onSet(async value => this.internalFanStateUpdate(value))
198
+ this.cacheFanState = this.fanService.getCharacteristic(this.hapChar.On).value ? 'on' : 'off'
199
+
200
+ // Add the set handler to the fan rotation speed characteristic
201
+ this.fanService
202
+ .getCharacteristic(this.hapChar.RotationSpeed)
203
+ .setProps({
204
+ minStep: 25,
205
+ validValues: [0, 25, 50, 75, 100],
206
+ })
207
+ .onSet(async value => this.internalSpeedUpdate(value))
208
+ this.cacheSpeed = this.fanService.getCharacteristic(this.hapChar.RotationSpeed).value
209
+
210
+ // Obtain the current mode
211
+ this.cacheMode = this.speedCodeLabel[this.cacheSpeed]
212
+
213
+ // Add the set handler to the lightbulb on/off characteristic
214
+ this.lightService.getCharacteristic(this.hapChar.On).onSet(async (value) => {
215
+ await this.internalLightStateUpdate(value)
216
+ })
217
+ this.cacheLightState = this.lightService.getCharacteristic(this.hapChar.On).value ? 'on' : 'off'
218
+
219
+ // Add the set handler to the lightbulb brightness characteristic
220
+ this.lightService
221
+ .getCharacteristic(this.hapChar.Brightness)
222
+ .onSet(async (value) => {
223
+ await this.internalBrightnessUpdate(value)
224
+ })
225
+ this.cacheBright = this.lightService.getCharacteristic(this.hapChar.Brightness).value
226
+
227
+ // Add the set handler to the lightbulb hue characteristic
228
+ this.lightService.getCharacteristic(this.hapChar.Hue).onSet(async (value) => {
229
+ await this.internalColourUpdate(value)
230
+ })
231
+ this.cacheHue = this.lightService.getCharacteristic(this.hapChar.Hue).value
232
+ this.cacheSat = this.lightService.getCharacteristic(this.hapChar.Saturation).value
233
+
234
+ // Output the customised options to the log
235
+ const opts = JSON.stringify({})
236
+ platform.log('[%s] %s %s.', accessory.displayName, platformLang.devInitOpts, opts)
237
+ }
238
+
239
+ async internalStateUpdate(value) {
240
+ try {
241
+ const newValue = value === 1 ? 'on' : 'off'
242
+
243
+ // Don't continue if the new value is the same as before
244
+ if (this.cacheState === newValue) {
245
+ return
246
+ }
247
+
248
+ // Send the request to the platform sender function
249
+ await this.platform.sendDeviceUpdate(this.accessory, {
250
+ cmd: 'stateHeat',
251
+ value: value === 1,
252
+ })
253
+
254
+ // If turning off then we also want to show the fan as off
255
+ if (newValue === 'off') {
256
+ this.fanService.updateCharacteristic(this.hapChar.On, false)
257
+ }
258
+
259
+ // Cache the new state and log if appropriate
260
+ if (this.cacheState !== newValue) {
261
+ this.cacheState = newValue
262
+ this.accessory.log(`${platformLang.curState} [${newValue}]`)
263
+ }
264
+ } catch (err) {
265
+ // Catch any errors during the process
266
+ this.accessory.logWarn(`${platformLang.devNotUpdated} ${parseError(err)}`)
267
+
268
+ // Throw a 'no response' error and set a timeout to revert this after 2 seconds
269
+ setTimeout(() => {
270
+ this.service.updateCharacteristic(this.hapChar.Active, this.cacheState === 'on' ? 1 : 0)
271
+ }, 2000)
272
+ throw new this.hapErr(-70402)
273
+ }
274
+ }
275
+
276
+ async internalModeUpdate(value) {
277
+ try {
278
+ // value can be 0 (auto) or 1 (heat)
279
+ let codeToSend
280
+ let newMode
281
+ if (value === 0) {
282
+ // Need to set to auto - hopefully we have a cacheTarg
283
+ // Need to use the turn version of the code since we are changing to this mode
284
+ codeToSend = this.tempCodeAutoTurn[this.cacheTarg]
285
+ newMode = 'auto'
286
+ } else {
287
+ // Need to set to heat - hopefully we have a cacheSpeed, if we don't set to 25 (fan-only)
288
+ codeToSend = this.speedCode[this.cacheSpeed] || this.speedCode[25]
289
+ newMode = this.speedCodeLabel[this.cacheSpeed] || this.speedCodeLabel[25]
290
+ }
291
+
292
+ // Send the request to the platform sender function
293
+ await this.platform.sendDeviceUpdate(this.accessory, {
294
+ cmd: 'multiSync',
295
+ value: codeToSend,
296
+ })
297
+
298
+ // If the new mode is auto then we should turn off the fan
299
+ if (newMode === 'auto') {
300
+ this.fanService.updateCharacteristic(this.hapChar.On, false)
301
+ }
302
+
303
+ // Cache the new state and log if appropriate
304
+ if (this.cacheMode !== newMode) {
305
+ this.cacheTarg = newMode
306
+ this.accessory.log(`${platformLang.curMode} [${this.cacheMode}]`)
307
+ }
308
+ } catch (err) {
309
+ // Catch any errors during the process
310
+ this.accessory.logWarn(`${platformLang.devNotUpdated} ${parseError(err)}`)
311
+
312
+ // Throw a 'no response' error and set a timeout to revert this after 2 seconds
313
+ setTimeout(() => {
314
+ this.service.updateCharacteristic(
315
+ this.hapChar.TargetHeaterCoolerState,
316
+ this.cacheMode === 'auto' ? 0 : 1,
317
+ )
318
+ }, 2000)
319
+ throw new this.hapErr(-70402)
320
+ }
321
+ }
322
+
323
+ async internalTempUpdate(value) {
324
+ try {
325
+ // Don't continue if the new value is the same as before
326
+ if (this.cacheTarg === value) {
327
+ return
328
+ }
329
+
330
+ // If the current mode is not auto then we need to change it to auto and use the turn version of the code
331
+ const codeToSend = this.cacheMode === 'auto' ? this.tempCodeAuto[value] : this.tempCodeAutoTurn[value]
332
+
333
+ // Send the request to the platform sender function
334
+ await this.platform.sendDeviceUpdate(this.accessory, {
335
+ cmd: 'multiSync',
336
+ value: codeToSend,
337
+ })
338
+
339
+ // Cache the new state and log if appropriate
340
+ this.cacheMode = 'auto'
341
+ if (this.cacheTarg !== value) {
342
+ this.cacheTarg = value
343
+ this.accessory.log(`${platformLang.curTarg} [${this.cacheTarg}°C]`)
344
+ }
345
+ } catch (err) {
346
+ // Catch any errors during the process
347
+ this.accessory.logWarn(`${platformLang.devNotUpdated} ${parseError(err)}`)
348
+
349
+ // Throw a 'no response' error and set a timeout to revert this after 2 seconds
350
+ setTimeout(() => {
351
+ this.service.updateCharacteristic(this.hapChar.HeatingThresholdTemperature, this.cacheTarg)
352
+ }, 2000)
353
+ throw new this.hapErr(-70402)
354
+ }
355
+ }
356
+
357
+ async internalSwingUpdate(value) {
358
+ try {
359
+ // value === 0 -> swing mode OFF
360
+ // value === 1 -> swing mode ON
361
+ const newValue = value === 1 ? 'on' : 'off'
362
+
363
+ // Don't continue if the new value is the same as before
364
+ if (this.cacheSwing === newValue) {
365
+ return
366
+ }
367
+
368
+ // Send the request to the platform sender function
369
+ await this.platform.sendDeviceUpdate(this.accessory, {
370
+ cmd: 'multiSync',
371
+ value: value === 1 ? 'Mx8BAQAAAAAAAAAAAAAAAAAAACw=' : 'Mx8BAAAAAAAAAAAAAAAAAAAAAC0=',
372
+ })
373
+
374
+ // Cache the new state and log if appropriate
375
+ if (this.cacheSwing !== newValue) {
376
+ this.cacheSwing = newValue
377
+ this.accessory.log(`${platformLang.curSwing} [${newValue}]`)
378
+ }
379
+ } catch (err) {
380
+ // Catch any errors during the process
381
+ this.accessory.logWarn(`${platformLang.devNotUpdated} ${parseError(err)}`)
382
+
383
+ // Throw a 'no response' error and set a timeout to revert this after 2 seconds
384
+ setTimeout(() => {
385
+ this.service.updateCharacteristic(
386
+ this.hapChar.SwingMode,
387
+ this.cacheSwing === 'on' ? 1 : 0,
388
+ )
389
+ }, 2000)
390
+ throw new this.hapErr(-70402)
391
+ }
392
+ }
393
+
394
+ async internalLockUpdate(value) {
395
+ try {
396
+ // value === 0 -> child lock OFF
397
+ // value === 1 -> child lock ON
398
+ const newValue = value === 1 ? 'on' : 'off'
399
+
400
+ // Don't continue if the new value is the same as before
401
+ if (this.cacheLock === newValue) {
402
+ return
403
+ }
404
+
405
+ // Send the request to the platform sender function
406
+ await this.platform.sendDeviceUpdate(this.accessory, {
407
+ cmd: 'multiSync',
408
+ value: value === 1 ? 'Mx8CAQAAAAAAAAAAAAAAAAAAAC8=' : 'Mx8CAAAAAAAAAAAAAAAAAAAAAC4=',
409
+ })
410
+
411
+ // Cache the new state and log if appropriate
412
+ if (this.cacheLock !== newValue) {
413
+ this.cacheLock = newValue
414
+ this.accessory.log(`${platformLang.curLock} [${newValue}]`)
415
+ }
416
+ } catch (err) {
417
+ // Catch any errors during the process
418
+ this.accessory.logWarn(`${platformLang.devNotUpdated} ${parseError(err)}`)
419
+
420
+ // Throw a 'no response' error and set a timeout to revert this after 2 seconds
421
+ setTimeout(() => {
422
+ this.service.updateCharacteristic(
423
+ this.hapChar.LockPhysicalControls,
424
+ this.cacheLock === 'on' ? 1 : 0,
425
+ )
426
+ }, 2000)
427
+ throw new this.hapErr(-70402)
428
+ }
429
+ }
430
+
431
+ async internalFanStateUpdate(value) {
432
+ try {
433
+ const newValue = value ? 'on' : 'off'
434
+
435
+ // The fan is used for the following modes:
436
+ // - 0%__: Auto
437
+ // - 25%_: Fan Only Mode
438
+ // - 50%_: Low Mode
439
+ // - 75%_: Medium Mode
440
+ // - 100%: High Mode
441
+ // If the main heater is turned off then this fan should be turned off too
442
+ // If the main heater is turned on then this fan speed should revert to the current mode
443
+
444
+ // Don't continue if the new value is the same as before
445
+ if (this.cacheFanState === newValue) {
446
+ return
447
+ }
448
+
449
+ // Turning fan on:
450
+ // We should not need to worry about a command for turning the fan ON, since
451
+ // the correct mode command will be sent when selecting the fan speed
452
+ // However we should mark the heater "active" characteristic as ON
453
+ if (newValue === 'on') {
454
+ if (this.cacheState !== 'on') {
455
+ this.cacheState = 'on'
456
+ this.service.updateCharacteristic(this.hapChar.Active, 1)
457
+ this.accessory.log(`${platformLang.curState} [${this.cacheState}]`)
458
+ }
459
+ this.cacheFanState = 'on'
460
+ return
461
+ }
462
+
463
+ // Turning fan off:
464
+ // We should send a command to set the heater mode to AUTO
465
+ this.cacheFanState = 'off'
466
+ this.cacheMode = 'auto'
467
+ this.service.updateCharacteristic(this.hapChar.TargetHeaterCoolerState, 0)
468
+ this.accessory.log(`${platformLang.curMode} [${this.cacheMode}]`)
469
+ } catch (err) {
470
+ // Catch any errors during the process
471
+ this.accessory.logWarn(`${platformLang.devNotUpdated} ${parseError(err)}`)
472
+
473
+ // Throw a 'no response' error and set a timeout to revert this after 2 seconds
474
+ setTimeout(() => {
475
+ this.fanService.updateCharacteristic(this.hapChar.On, this.cacheFanState === 'on')
476
+ }, 2000)
477
+ throw new this.hapErr(-70402)
478
+ }
479
+ }
480
+
481
+ async internalSpeedUpdate(value) {
482
+ try {
483
+ // Don't continue if the new value is the same as before
484
+ if (this.cacheSpeed === value || value === 0) {
485
+ return
486
+ }
487
+
488
+ // Value should be one of 25, 50, 75, 100
489
+ const codeToSend = this.speedCode[value]
490
+
491
+ // Send the request to the platform sender function
492
+ await this.platform.sendDeviceUpdate(this.accessory, {
493
+ cmd: 'multiSync',
494
+ value: codeToSend,
495
+ })
496
+
497
+ // Cache the new state and log if appropriate
498
+ this.cacheFanState = 'on'
499
+ this.service.updateCharacteristic(this.hapChar.Active, 1)
500
+ this.service.updateCharacteristic(this.hapChar.TargetHeaterCoolerState, 1)
501
+ if (this.cacheSpeed !== value) {
502
+ this.cacheSpeed = value
503
+ this.cacheMode = this.speedCodeLabel[value]
504
+ this.accessory.log(`${platformLang.curMode} [${this.cacheMode}]`)
505
+ }
506
+ } catch (err) {
507
+ // Catch any errors during the process
508
+ this.accessory.logWarn(`${platformLang.devNotUpdated} ${parseError(err)}`)
509
+
510
+ // Throw a 'no response' error and set a timeout to revert this after 2 seconds
511
+ setTimeout(() => {
512
+ this.fanService.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheSpeed)
513
+ }, 2000)
514
+ throw new this.hapErr(-70402)
515
+ }
516
+ }
517
+
518
+ async internalLightStateUpdate(value) {
519
+ try {
520
+ const newValue = value ? 'on' : 'off'
521
+
522
+ // Don't continue if the new value is the same as before
523
+ if (this.cacheLightState === newValue) {
524
+ return
525
+ }
526
+
527
+ // Generate the hex values for the code
528
+ const hexValues = [0x3A, 0x1B, 0x01, 0x01, `0x0${value ? '1' : '0'}`]
529
+
530
+ // Send the request to the platform sender function
531
+ await this.platform.sendDeviceUpdate(this.accessory, {
532
+ cmd: 'multiSync',
533
+ value: generateCodeFromHexValues(hexValues),
534
+ })
535
+
536
+ // Cache the new state and log if appropriate
537
+ if (this.cacheLightState !== newValue) {
538
+ this.cacheLightState = newValue
539
+ this.accessory.log(`${platformLang.curLight} [${newValue}]`)
540
+ }
541
+ } catch (err) {
542
+ // Catch any errors during the process
543
+ this.accessory.logWarn(`${platformLang.devNotUpdated} ${parseError(err)}`)
544
+
545
+ // Throw a 'no response' error and set a timeout to revert this after 2 seconds
546
+ setTimeout(() => {
547
+ this.lightService.updateCharacteristic(this.hapChar.On, this.cacheLightState === 'on')
548
+ }, 2000)
549
+ throw new this.hapErr(-70402)
550
+ }
551
+ }
552
+
553
+ async internalBrightnessUpdate(value) {
554
+ try {
555
+ // This acts like a debounce function when endlessly sliding the brightness scale
556
+ const updateKeyBright = generateRandomString(5)
557
+ this.updateKeyBright = updateKeyBright
558
+ await sleep(350)
559
+ if (updateKeyBright !== this.updateKeyBright) {
560
+ return
561
+ }
562
+
563
+ // Don't continue if the new value is the same as before
564
+ if (value === this.cacheBright) {
565
+ return
566
+ }
567
+
568
+ // Generate the hex values for the code
569
+ const hexValues = [0x3A, 0x1B, 0x01, 0x02, value]
570
+
571
+ // Send the request to the platform sender function
572
+ await this.platform.sendDeviceUpdate(this.accessory, {
573
+ cmd: 'multiSync',
574
+ value: generateCodeFromHexValues(hexValues),
575
+ })
576
+
577
+ // Govee considers 0% brightness to be off
578
+ if (value === 0) {
579
+ setTimeout(() => {
580
+ this.cacheLightState = 'off'
581
+ if (this.lightService.getCharacteristic(this.hapChar.On).value) {
582
+ this.lightService.updateCharacteristic(this.hapChar.On, false)
583
+ this.accessory.log(`${platformLang.curLight} [${this.cacheLightState}]`)
584
+ }
585
+ this.lightService.updateCharacteristic(this.hapChar.Brightness, this.cacheBright)
586
+ }, 1500)
587
+ return
588
+ }
589
+
590
+ // Cache the new state and log if appropriate
591
+ if (this.cacheBright !== value) {
592
+ this.cacheBright = value
593
+ this.accessory.log(`${platformLang.curBright} [${value}%]`)
594
+ }
595
+ } catch (err) {
596
+ // Catch any errors during the process
597
+ this.accessory.logWarn(`${platformLang.devNotUpdated} ${parseError(err)}`)
598
+
599
+ // Throw a 'no response' error and set a timeout to revert this after 2 seconds
600
+ setTimeout(() => {
601
+ this.lightService.updateCharacteristic(this.hapChar.Brightness, this.cacheBright)
602
+ }, 2000)
603
+ throw new this.hapErr(-70402)
604
+ }
605
+ }
606
+
607
+ async internalColourUpdate(value) {
608
+ try {
609
+ // This acts like a debounce function when endlessly sliding the colour wheel
610
+ const updateKeyColour = generateRandomString(5)
611
+ this.updateKeyColour = updateKeyColour
612
+ await sleep(300)
613
+ if (updateKeyColour !== this.updateKeyColour) {
614
+ return
615
+ }
616
+
617
+ // Don't continue if the new value is the same as before
618
+ if (value === this.cacheHue) {
619
+ return
620
+ }
621
+
622
+ // Calculate RGB values
623
+ const newRGB = hs2rgb(value, this.lightService.getCharacteristic(this.hapChar.Saturation).value)
624
+
625
+ // Generate the hex values for the code
626
+ const hexValues = [0x3A, 0x1B, 0x05, 0x0D, ...newRGB]
627
+
628
+ // Send the request to the platform sender function
629
+ await this.platform.sendDeviceUpdate(this.accessory, {
630
+ cmd: 'multiSync',
631
+ value: generateCodeFromHexValues(hexValues),
632
+ })
633
+
634
+ // Cache the new state and log if appropriate
635
+ if (this.cacheHue !== value) {
636
+ this.cacheHue = value
637
+ this.accessory.log(`${platformLang.curColour} [rgb ${newRGB.join(' ')}]`)
638
+ }
639
+ } catch (err) {
640
+ // Catch any errors during the process
641
+ this.accessory.logWarn(`${platformLang.devNotUpdated} ${parseError(err)}`)
642
+
643
+ // Throw a 'no response' error and set a timeout to revert this after 2 seconds
644
+ setTimeout(() => {
645
+ this.lightService.updateCharacteristic(this.hapChar.Hue, this.cacheHue)
646
+ }, 2000)
647
+ throw new this.hapErr(-70402)
648
+ }
649
+ }
650
+
651
+ externalUpdate(params) {
652
+ // Update the active characteristic
653
+ if (params.state && params.state !== this.cacheState) {
654
+ this.cacheState = params.state
655
+ this.service.updateCharacteristic(this.hapChar.Active, this.cacheState === 'on' ? 1 : 0)
656
+ this.accessory.log(`${platformLang.curState} [${this.cacheState}]`)
657
+ }
658
+
659
+ // Update the current temperature characteristic
660
+ if (hasProperty(params, 'temperature')) {
661
+ const newTemp = nearestHalf(farToCen(params.temperature / 100))
662
+ if (newTemp !== this.cacheTemp) {
663
+ this.cacheTemp = newTemp
664
+ this.service.updateCharacteristic(this.hapChar.CurrentTemperature, this.cacheTemp)
665
+ this.accessory.log(`${platformLang.curTemp} [${this.cacheTemp}°C]`)
666
+ }
667
+ }
668
+
669
+ // Update the target temperature characteristic
670
+ if (hasProperty(params, 'setTemperature')) {
671
+ const newTemp = Math.round(farToCen(params.setTemperature / 100))
672
+ if (newTemp !== this.cacheTarg) {
673
+ // A change in target temperature would suggest the device is in auto mode
674
+ // If incorrect, the code change will pick this up later
675
+ this.cacheTarg = newTemp
676
+ this.cacheFanState = 'off'
677
+ this.cacheSpeed = 0
678
+ this.cacheMode = 'auto'
679
+ this.service.updateCharacteristic(this.hapChar.HeatingThresholdTemperature, this.cacheTarg)
680
+ this.accessory.log(`${platformLang.curTarg} [${this.cacheTarg}°C]`)
681
+ }
682
+ }
683
+
684
+ // Check for some other scene/mode change
685
+ (params.commands || []).forEach((command) => {
686
+ const hexString = base64ToHex(command)
687
+ const hexParts = hexToTwoItems(hexString)
688
+
689
+ // Return now if not a device query update code
690
+ if (getTwoItemPosition(hexParts, 1) !== 'aa') {
691
+ return
692
+ }
693
+
694
+ const deviceFunction = `${getTwoItemPosition(hexParts, 2)}${getTwoItemPosition(hexParts, 3)}`
695
+
696
+ switch (deviceFunction) {
697
+ case '1b01': {
698
+ const newLightState = getTwoItemPosition(hexParts, 4) === '01' ? 'on' : 'off'
699
+ if (this.cacheLightState !== newLightState) {
700
+ this.cacheLightState = newLightState
701
+ this.lightService.updateCharacteristic(this.hapChar.On, this.cacheLightState === 'on')
702
+ this.accessory.log(`${platformLang.curLight} [${this.cacheLightState}]`)
703
+ }
704
+ const newBrightness = hexToDecimal(getTwoItemPosition(hexParts, 5))
705
+ if (this.cacheBright !== newBrightness) {
706
+ this.cacheBright = newBrightness
707
+ this.lightService.updateCharacteristic(this.hapChar.Brightness, this.cacheBright)
708
+ this.accessory.log(`${platformLang.curBright} [${this.cacheBright}%]`)
709
+ }
710
+ break
711
+ }
712
+ case '1b05': {
713
+ // Night light colour
714
+ const newR = hexToDecimal(getTwoItemPosition(hexParts, 5))
715
+ const newG = hexToDecimal(getTwoItemPosition(hexParts, 6))
716
+ const newB = hexToDecimal(getTwoItemPosition(hexParts, 7))
717
+
718
+ const hs = rgb2hs(newR, newG, newB)
719
+
720
+ // Check for a colour change
721
+ if (hs[0] !== this.cacheHue) {
722
+ // Colour is different so update Homebridge with new values
723
+ this.lightService.updateCharacteristic(this.hapChar.Hue, hs[0])
724
+ this.lightService.updateCharacteristic(this.hapChar.Saturation, hs[1]);
725
+ [this.cacheHue] = hs
726
+
727
+ // Log the change
728
+ this.accessory.log(`${platformLang.curColour} [rgb ${newR} ${newG} ${newB}]`)
729
+ }
730
+ break
731
+ }
732
+ case '1f00':
733
+ case '1f01': {
734
+ // Swing Mode
735
+ const newSwing = getTwoItemPosition(hexParts, 3) === '01' ? 'on' : 'off'
736
+ if (this.cacheSwing !== newSwing) {
737
+ this.cacheSwing = newSwing
738
+ this.service.updateCharacteristic(this.hapChar.SwingMode, this.cacheSwing === 'on' ? 1 : 0)
739
+ this.accessory.log(`${platformLang.curSwing} [${this.cacheSwing}]`)
740
+ }
741
+
742
+ // Child Lock
743
+ const newLock = getTwoItemPosition(hexParts, 4) === '01' ? 'on' : 'off'
744
+ if (this.cacheLock !== newLock) {
745
+ this.cacheLock = newLock
746
+ this.service.updateCharacteristic(this.hapChar.LockPhysicalControls, this.cacheLock === 'on' ? 1 : 0)
747
+ this.accessory.log(`${platformLang.curLock} [${this.cacheLock}]`)
748
+ }
749
+ break
750
+ }
751
+ case '0509': {
752
+ // Fan-only mode
753
+ const newModeLabel = 'fan-only'
754
+ if (this.cacheMode !== newModeLabel) {
755
+ this.cacheMode = newModeLabel
756
+ this.cacheFanState = 'on'
757
+ this.cacheSpeed = 25
758
+ this.fanService.updateCharacteristic(this.hapChar.On, true)
759
+ this.fanService.updateCharacteristic(this.hapChar.RotationSpeed, 25)
760
+ this.service.updateCharacteristic(this.hapChar.TargetHeaterCoolerState, 1)
761
+ this.accessory.log(`${platformLang.curMode} [${this.cacheMode}]`)
762
+ }
763
+ break
764
+ }
765
+ case '0501': { // Speed
766
+ const newMode = getTwoItemPosition(hexParts, 4)
767
+ switch (newMode) {
768
+ case '01': {
769
+ const newModeLabel = 'low'
770
+ if (this.cacheMode !== newModeLabel) {
771
+ this.cacheMode = newModeLabel
772
+ this.cacheFanState = 'on'
773
+ this.cacheSpeed = 50
774
+ this.fanService.updateCharacteristic(this.hapChar.On, true)
775
+ this.fanService.updateCharacteristic(this.hapChar.RotationSpeed, 50)
776
+ this.service.updateCharacteristic(this.hapChar.TargetHeaterCoolerState, 1)
777
+ this.accessory.log(`${platformLang.curMode} [${this.cacheMode}]`)
778
+ }
779
+ break
780
+ }
781
+ case '02': {
782
+ const newModeLabel = 'medium'
783
+ if (this.cacheMode !== newModeLabel) {
784
+ this.cacheMode = newModeLabel
785
+ this.cacheFanState = 'on'
786
+ this.cacheSpeed = 75
787
+ this.fanService.updateCharacteristic(this.hapChar.On, true)
788
+ this.fanService.updateCharacteristic(this.hapChar.RotationSpeed, 75)
789
+ this.service.updateCharacteristic(this.hapChar.TargetHeaterCoolerState, 1)
790
+ this.accessory.log(`${platformLang.curMode} [${this.cacheMode}]`)
791
+ }
792
+ break
793
+ }
794
+ case '03': {
795
+ const newModeLabel = 'high'
796
+ if (this.cacheMode !== newModeLabel) {
797
+ this.cacheMode = newModeLabel
798
+ this.cacheFanState = 'on'
799
+ this.cacheSpeed = 100
800
+ this.fanService.updateCharacteristic(this.hapChar.On, true)
801
+ this.fanService.updateCharacteristic(this.hapChar.RotationSpeed, 100)
802
+ this.service.updateCharacteristic(this.hapChar.TargetHeaterCoolerState, 1)
803
+ this.accessory.log(`${platformLang.curMode} [${this.cacheMode}]`)
804
+ }
805
+ break
806
+ }
807
+ default:
808
+ break
809
+ }
810
+ break
811
+ }
812
+ case '0503': { // Target temperature (thermostat mode on)
813
+ const newModeLabel = 'auto'
814
+ if (this.cacheMode !== newModeLabel) {
815
+ this.cacheMode = newModeLabel
816
+ this.cacheFanState = 'off'
817
+ this.cacheSpeed = 0
818
+ this.fanService.updateCharacteristic(this.hapChar.On, false)
819
+ this.fanService.updateCharacteristic(this.hapChar.RotationSpeed, 0)
820
+ this.service.updateCharacteristic(this.hapChar.TargetHeaterCoolerState, 0)
821
+ this.accessory.log(`${platformLang.curMode} [${this.cacheMode}]`)
822
+ }
823
+ break
824
+ }
825
+ case '1a00': // Target temperature (thermostat mode off)
826
+ case '1100': // Timer off
827
+ case '1101': // Timer on
828
+ case '1600': // Display mode off
829
+ case '1601': // Display mode on
830
+ // We do not need to do anything for these
831
+ break
832
+ default:
833
+ this.accessory.logDebugWarn(`${platformLang.newScene}: [${command}] [${hexString}]`)
834
+ break
835
+ }
836
+ })
837
+ }
838
+ }