@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,300 @@
1
+ import { generateRandomString, hasProperty, parseError } from '../utils/functions.js'
2
+ import platformLang from '../utils/lang-en.js'
3
+
4
+ export default class {
5
+ constructor(platform, accessory) {
6
+ // Set up variables from the platform
7
+ this.hapChar = platform.api.hap.Characteristic
8
+ this.hapErr = platform.api.hap.HapStatusError
9
+ this.hapServ = platform.api.hap.Service
10
+ this.platform = platform
11
+
12
+ // Set up variables from the accessory
13
+ this.accessory = accessory
14
+ this.temperatureSource = accessory.context.temperatureSource;
15
+
16
+ // Remove any old services from simulations
17
+ ['AirPurifier', 'Lightbulb', 'Outlet', 'Switch', 'Valve'].forEach((service) => {
18
+ if (this.accessory.getService(this.hapServ[service])) {
19
+ this.accessory.removeService(this.accessory.getService(this.hapServ[service]))
20
+ }
21
+ })
22
+
23
+ // Set up the accessory with default target temp when added the first time
24
+ if (!hasProperty(this.accessory.context, 'cacheTarget')) {
25
+ this.accessory.context.cacheTarget = 20
26
+ }
27
+
28
+ // Check to make sure user has not switched from cooler to heater
29
+ if (this.accessory.context.cacheType !== 'heater') {
30
+ // Remove and re-setup as a HeaterCooler
31
+ if (this.accessory.getService(this.hapServ.HeaterCooler)) {
32
+ this.accessory.removeService(this.accessory.getService(this.hapServ.HeaterCooler))
33
+ }
34
+ this.accessory.context.cacheType = 'heater'
35
+ this.accessory.context.cacheTarget = 20
36
+ }
37
+
38
+ // Add the heater service if it doesn't already exist
39
+ this.service = this.accessory.getService(this.hapServ.HeaterCooler)
40
+ || this.accessory.addService(this.hapServ.HeaterCooler)
41
+
42
+ // Set custom properties of the current temperature characteristic
43
+ this.service.getCharacteristic(this.hapChar.CurrentTemperature).setProps({
44
+ minStep: 0.1,
45
+ })
46
+ this.cacheTemp = this.service.getCharacteristic(this.hapChar.CurrentTemperature).value
47
+
48
+ // Add the set handler to the heater active characteristic
49
+ this.service
50
+ .getCharacteristic(this.hapChar.Active)
51
+ .onSet(async value => this.internalStateUpdate(value))
52
+
53
+ // Add options to the target state characteristic
54
+ this.service.getCharacteristic(this.hapChar.TargetHeaterCoolerState).setProps({
55
+ minValue: 0,
56
+ maxValue: 0,
57
+ validValues: [0],
58
+ })
59
+
60
+ // Add the set handler to the target temperature characteristic
61
+ this.service
62
+ .getCharacteristic(this.hapChar.HeatingThresholdTemperature)
63
+ .updateValue(this.accessory.context.cacheTarget)
64
+ .setProps({ minStep: 0.5 })
65
+ .onSet(async value => this.internalTargetTempUpdate(value))
66
+
67
+ // Initialise these caches now since they aren't determined by the initial externalUpdate()
68
+ this.cacheState = this.service.getCharacteristic(this.hapChar.Active).value === 1 ? 'on' : 'off'
69
+ this.cacheHeat = this.cacheState === 'on'
70
+ && this.service.getCharacteristic(this.hapChar.TargetHeaterCoolerState).value === 2
71
+ ? 'on'
72
+ : 'off'
73
+
74
+ // Pass the accessory to Fakegato to set up with Eve
75
+ this.accessory.eveService = new platform.eveService('custom', this.accessory, {
76
+ log: () => {},
77
+ })
78
+
79
+ // Set up an interval to get regular temperature updates
80
+ setTimeout(() => {
81
+ this.getTemperature()
82
+ this.intervalPoll = setInterval(() => this.getTemperature(), 120000)
83
+ }, 5000)
84
+
85
+ // Stop the intervals on Homebridge shutdown
86
+ platform.api.on('shutdown', () => {
87
+ clearInterval(this.intervalPoll)
88
+ })
89
+
90
+ // Output the customised options to the log
91
+ const opts = JSON.stringify({
92
+ showAs: 'heater',
93
+ temperatureSource: this.temperatureSource,
94
+ })
95
+ platform.log('[%s] %s %s.', accessory.displayName, platformLang.devInitOpts, opts)
96
+ }
97
+
98
+ async internalStateUpdate(value) {
99
+ try {
100
+ let newState
101
+ let newHeat
102
+ let newValue
103
+ if (value === 0) {
104
+ newValue = 'off'
105
+ newState = 'off'
106
+ newHeat = 'off'
107
+ } else if (this.cacheTemp < this.accessory.context.cacheTarget) {
108
+ newValue = 'on'
109
+ newState = 'on'
110
+ newHeat = 'on'
111
+ } else {
112
+ newValue = 'off'
113
+ newState = 'on'
114
+ newHeat = 'off'
115
+ }
116
+
117
+ // Only send the update if either:
118
+ // * The new value (state) is OFF and the cacheHeat was ON
119
+ // * The new value (state) is ON and newHeat is 'on'
120
+ if ((value === 0 && this.cacheHeat === 'on') || (value === 1 && newHeat === 'on')) {
121
+ // Set up a one-minute timeout for the plugin to ignore incoming updates
122
+ const timerKey = generateRandomString(5)
123
+ this.updateTimeout = timerKey
124
+ setTimeout(() => {
125
+ if (this.updateTimeout === timerKey) {
126
+ this.updateTimeout = false
127
+ }
128
+ }, 60000)
129
+
130
+ // Send the request to the platform sender function
131
+ await this.platform.sendDeviceUpdate(this.accessory, {
132
+ cmd: 'stateOutlet',
133
+ value: newValue,
134
+ })
135
+ }
136
+
137
+ // Cache and log
138
+ if (newState !== this.cacheState) {
139
+ this.cacheState = newState
140
+ this.accessory.log(`${platformLang.curState} [${this.cacheState}]`)
141
+ }
142
+ if (newHeat !== this.cacheHeat) {
143
+ this.cacheHeat = newHeat
144
+ this.accessory.log(`${platformLang.curHeat} [${this.cacheHeat}]`)
145
+ }
146
+ const newOnState = this.cacheHeat === 'on' ? 2 : 1
147
+ this.service.updateCharacteristic(
148
+ this.hapChar.CurrentHeaterCoolerState,
149
+ value === 1 ? newOnState : 0,
150
+ )
151
+ } catch (err) {
152
+ // Catch any errors during the process
153
+ this.accessory.logWarn(`${platformLang.devNotUpdated} ${parseError(err)}`)
154
+
155
+ // Throw a 'no response' error and set a timeout to revert this after 2 seconds
156
+ setTimeout(() => {
157
+ this.service.updateCharacteristic(this.hapChar.Active, this.cacheState === 'on' ? 1 : 0)
158
+ }, 2000)
159
+ throw new this.hapErr(-70402)
160
+ }
161
+ }
162
+
163
+ async internalTargetTempUpdate(value) {
164
+ try {
165
+ // Don't continue if the new value is the same as before
166
+ if (value === this.accessory.context.cacheTarget) {
167
+ return
168
+ }
169
+ this.accessory.context.cacheTarget = value
170
+ this.accessory.log(`${platformLang.curTarg} [${value}°C]`)
171
+ if (this.cacheState === 'off') {
172
+ return
173
+ }
174
+
175
+ // Check to see if we need to turn on or off
176
+ let newValue
177
+ let newHeat
178
+ if (this.cacheTemp < value) {
179
+ newValue = 'on'
180
+ newHeat = 'on'
181
+ } else {
182
+ newValue = 'off'
183
+ newHeat = 'off'
184
+ }
185
+
186
+ // Don't continue if no change needed to device state
187
+ if (newHeat === this.cacheHeat) {
188
+ return
189
+ }
190
+
191
+ // Set up a one-minute timeout for the plugin to ignore incoming updates
192
+ const timerKey = generateRandomString(5)
193
+ this.updateTimeout = timerKey
194
+ setTimeout(() => {
195
+ if (this.updateTimeout === timerKey) {
196
+ this.updateTimeout = false
197
+ }
198
+ }, 60000)
199
+
200
+ // Send the request to the platform sender function
201
+ await this.platform.sendDeviceUpdate(this.accessory, {
202
+ cmd: 'stateOutlet',
203
+ value: newValue,
204
+ })
205
+
206
+ // Cache and log
207
+ this.cacheHeat = newHeat
208
+ this.accessory.log(`${platformLang.curHeat} [${this.cacheHeat}]`)
209
+ this.service.updateCharacteristic(
210
+ this.hapChar.CurrentHeaterCoolerState,
211
+ this.cacheHeat === 'on' ? 2 : 1,
212
+ )
213
+ } catch (err) {
214
+ // Catch any errors during the process
215
+ this.accessory.logWarn(`${platformLang.devNotUpdated} ${parseError(err)}`)
216
+
217
+ // Throw a 'no response' error and set a timeout to revert this after 2 seconds
218
+ setTimeout(() => {
219
+ this.service.updateCharacteristic(
220
+ this.hapChar.HeatingThresholdTemperature,
221
+ this.accessory.context.cacheTarget,
222
+ )
223
+ }, 2000)
224
+ throw new this.hapErr(-70402)
225
+ }
226
+ }
227
+
228
+ async internalCurrentTempUpdate() {
229
+ try {
230
+ // Don't continue if the device is off
231
+ if (this.cacheState === 'off') {
232
+ return
233
+ }
234
+
235
+ // Check to see if we need to turn on or off
236
+ let newValue
237
+ let newHeat
238
+ if (this.cacheTemp < this.accessory.context.cacheTarget) {
239
+ newValue = 'on'
240
+ newHeat = 'on'
241
+ } else {
242
+ newValue = 'off'
243
+ newHeat = 'off'
244
+ }
245
+
246
+ // Don't continue if no change needed to device state
247
+ if (newHeat === this.cacheHeat) {
248
+ return
249
+ }
250
+
251
+ // Set up a one-minute timeout for the plugin to ignore incoming updates
252
+ const timerKey = generateRandomString(5)
253
+ this.updateTimeout = timerKey
254
+ setTimeout(() => {
255
+ if (this.updateTimeout === timerKey) {
256
+ this.updateTimeout = false
257
+ }
258
+ }, 60000)
259
+
260
+ // Send the request to the platform sender function
261
+ await this.platform.sendDeviceUpdate(this.accessory, {
262
+ cmd: 'stateOutlet',
263
+ value: newValue,
264
+ })
265
+
266
+ // Log and cache
267
+ this.cacheHeat = newHeat
268
+ this.accessory.log(`${platformLang.curHeat} [${this.cacheHeat}]`)
269
+ this.service.updateCharacteristic(
270
+ this.hapChar.CurrentHeaterCoolerState,
271
+ this.cacheHeat === 'on' ? 2 : 1,
272
+ )
273
+ } catch (err) {
274
+ // Catch any errors during the process
275
+ this.accessory.logWarn(parseError(err))
276
+ }
277
+ }
278
+
279
+ async getTemperature() {
280
+ try {
281
+ // Skip polling if the storage hasn't initialised properly
282
+ if (!this.platform.storageClientData) {
283
+ return
284
+ }
285
+
286
+ const newTemp = await this.platform.storageData.getItem(`${this.temperatureSource}_temp`)
287
+ if (newTemp && newTemp !== this.cacheTemp) {
288
+ this.cacheTemp = newTemp
289
+ this.service.updateCharacteristic(this.hapChar.CurrentTemperature, this.cacheTemp)
290
+ this.accessory.eveService.addEntry({ temp: this.cacheTemp })
291
+ this.accessory.log(`${platformLang.curTemp} [${this.cacheTemp}°C]`)
292
+ await this.internalCurrentTempUpdate()
293
+ }
294
+ } catch (err) {
295
+ this.accessory.logWarn(parseError(err))
296
+ }
297
+ }
298
+
299
+ externalUpdate() {}
300
+ }
@@ -0,0 +1,353 @@
1
+ import {
2
+ base64ToHex,
3
+ farToCen,
4
+ getTwoItemPosition,
5
+ hasProperty,
6
+ hexToTwoItems,
7
+ nearestHalf,
8
+ parseError,
9
+ } from '../utils/functions.js'
10
+ import platformLang from '../utils/lang-en.js'
11
+
12
+ /*
13
+ H7130 (without temperature reporting)
14
+ {
15
+ "mode": {
16
+ "options": [
17
+ {
18
+ "name": "Low",
19
+ "value": "1"
20
+ },
21
+ {
22
+ "name": "Medium",
23
+ "value": "2"
24
+ },
25
+ {
26
+ "name": "High",
27
+ "value": "3"
28
+ }
29
+ ]
30
+ }
31
+ }
32
+ */
33
+ export default class {
34
+ constructor(platform, accessory) {
35
+ // Set up variables from the platform
36
+ this.hapChar = platform.api.hap.Characteristic
37
+ this.hapErr = platform.api.hap.HapStatusError
38
+ this.hapServ = platform.api.hap.Service
39
+ this.platform = platform
40
+
41
+ this.log = platform.log
42
+
43
+ // Set up variables from the accessory
44
+ this.accessory = accessory
45
+
46
+ // Set up objects
47
+ this.speedCode = {
48
+ 33: 'MwUBAAAAAAAAAAAAAAAAAAAAADc=',
49
+ 66: 'MwUCAAAAAAAAAAAAAAAAAAAAADQ=',
50
+ 99: 'MwUDAAAAAAAAAAAAAAAAAAAAADU=',
51
+ }
52
+
53
+ this.speedCodeLabel = {
54
+ 33: 'low',
55
+ 66: 'medium',
56
+ 99: 'high',
57
+ }
58
+
59
+ // Remove any old light service
60
+ if (this.accessory.getService(this.hapServ.Lightbulb)) {
61
+ this.accessory.removeService(this.accessory.getService(this.hapServ.Lightbulb))
62
+ }
63
+
64
+ // Remove any old heater service
65
+ if (this.accessory.getService(this.hapServ.HeaterCooler)) {
66
+ this.accessory.removeService(this.accessory.getService(this.hapServ.HeaterCooler))
67
+ }
68
+
69
+ // Remove any old fan service
70
+ if (this.accessory.getService(this.hapServ.Fan)) {
71
+ this.accessory.removeService(this.accessory.getService(this.hapServ.Fan))
72
+ }
73
+
74
+ // Add the fan v2 service if it doesn't already exist
75
+ this.service = this.accessory.getService(this.hapServ.Fanv2) || this.accessory.addService(this.hapServ.Fanv2)
76
+
77
+ // Add the set handler to the fan active characteristic
78
+ this.service
79
+ .getCharacteristic(this.hapChar.Active)
80
+ .onSet(async value => this.internalStateUpdate(value))
81
+ this.cacheState = this.service.getCharacteristic(this.hapChar.Active).value === 1 ? 'on' : 'off'
82
+
83
+ // Add the set handler to the fan rotation speed characteristic
84
+ this.service
85
+ .getCharacteristic(this.hapChar.RotationSpeed)
86
+ .setProps({
87
+ minStep: 33,
88
+ validValues: [0, 33, 66, 99],
89
+ })
90
+ .onSet(async value => this.internalSpeedUpdate(value))
91
+ this.cacheSpeed = this.service.getCharacteristic(this.hapChar.RotationSpeed).value
92
+
93
+ // Add the set handler to the heater swing mode characteristic (for oscillation)
94
+ this.service
95
+ .getCharacteristic(this.hapChar.SwingMode)
96
+ .onSet(async value => this.internalSwingUpdate(value))
97
+ this.cacheSwing = this.service.getCharacteristic(this.hapChar.SwingMode).value === 1 ? 'on' : 'off'
98
+
99
+ // Add the set handler to the heater lock characteristic (for oscillation)
100
+ this.service
101
+ .getCharacteristic(this.hapChar.LockPhysicalControls)
102
+ .onSet(async value => this.internalLockUpdate(value))
103
+ this.cacheLock = this.service.getCharacteristic(this.hapChar.LockPhysicalControls).value === 1 ? 'on' : 'off'
104
+
105
+ // Output the customised options to the log
106
+ const opts = JSON.stringify({
107
+ tempReporting: false,
108
+ })
109
+ platform.log('[%s] %s %s.', accessory.displayName, platformLang.devInitOpts, opts)
110
+ }
111
+
112
+ async internalStateUpdate(value) {
113
+ try {
114
+ const newValue = value === 1 ? 'on' : 'off'
115
+
116
+ // Don't continue if the new value is the same as before
117
+ if (this.cacheState === newValue) {
118
+ return
119
+ }
120
+
121
+ // Send the request to the platform sender function
122
+ await this.platform.sendDeviceUpdate(this.accessory, {
123
+ cmd: 'ptReal',
124
+ value: value ? 'MwEBAAAAAAAAAAAAAAAAAAAAADM=' : 'MwEAAAAAAAAAAAAAAAAAAAAAADI=',
125
+ })
126
+
127
+ // Cache the new state and log if appropriate
128
+ if (this.cacheState !== newValue) {
129
+ this.cacheState = newValue
130
+ this.accessory.log(`${platformLang.curState} [${newValue}]`)
131
+ }
132
+ } catch (err) {
133
+ // Catch any errors during the process
134
+ this.accessory.logWarn(`${platformLang.devNotUpdated} ${parseError(err)}`)
135
+
136
+ // Throw a 'no response' error and set a timeout to revert this after 2 seconds
137
+ setTimeout(() => {
138
+ this.service.updateCharacteristic(this.hapChar.Active, this.cacheState === 'on' ? 1 : 0)
139
+ }, 2000)
140
+ throw new this.hapErr(-70402)
141
+ }
142
+ }
143
+
144
+ async internalSwingUpdate(value) {
145
+ try {
146
+ // value === 0 -> swing mode OFF
147
+ // value === 1 -> swing mode ON
148
+ const newValue = value === 1 ? 'on' : 'off'
149
+
150
+ // Don't continue if the new value is the same as before
151
+ if (this.cacheSwing === newValue) {
152
+ return
153
+ }
154
+
155
+ // Send the request to the platform sender function
156
+ await this.platform.sendDeviceUpdate(this.accessory, {
157
+ cmd: 'ptReal',
158
+ value: value ? 'MxgBAAAAAAAAAAAAAAAAAAAAACo=' : 'MxgAAAAAAAAAAAAAAAAAAAAAACs=',
159
+ })
160
+
161
+ // Cache the new state and log if appropriate
162
+ if (this.cacheSwing !== newValue) {
163
+ this.cacheSwing = newValue
164
+ this.accessory.log(`${platformLang.curSwing} [${newValue}]`)
165
+ }
166
+ } catch (err) {
167
+ // Catch any errors during the process
168
+ this.accessory.logWarn(`${platformLang.devNotUpdated} ${parseError(err)}`)
169
+
170
+ // Throw a 'no response' error and set a timeout to revert this after 2 seconds
171
+ setTimeout(() => {
172
+ this.service.updateCharacteristic(
173
+ this.hapChar.SwingMode,
174
+ this.cacheSwing === 'on' ? 1 : 0,
175
+ )
176
+ }, 2000)
177
+ throw new this.hapErr(-70402)
178
+ }
179
+ }
180
+
181
+ async internalLockUpdate(value) {
182
+ try {
183
+ // value === 0 -> child lock OFF
184
+ // value === 1 -> child lock ON
185
+ const newValue = value === 1 ? 'on' : 'off'
186
+
187
+ // Don't continue if the new value is the same as before
188
+ if (this.cacheLock === newValue) {
189
+ return
190
+ }
191
+
192
+ // Send the request to the platform sender function
193
+ await this.platform.sendDeviceUpdate(this.accessory, {
194
+ cmd: 'ptReal',
195
+ value: value ? 'MxABAAAAAAAAAAAAAAAAAAAAACI=' : 'MxAAAAAAAAAAAAAAAAAAAAAAACM=',
196
+ })
197
+
198
+ // Cache the new state and log if appropriate
199
+ if (this.cacheLock !== newValue) {
200
+ this.cacheLock = newValue
201
+ this.accessory.log(`${platformLang.curLock} [${newValue}]`)
202
+ }
203
+ } catch (err) {
204
+ // Catch any errors during the process
205
+ this.accessory.logWarn(`${platformLang.devNotUpdated} ${parseError(err)}`)
206
+
207
+ // Throw a 'no response' error and set a timeout to revert this after 2 seconds
208
+ setTimeout(() => {
209
+ this.service.updateCharacteristic(
210
+ this.hapChar.LockPhysicalControls,
211
+ this.cacheLock === 'on' ? 1 : 0,
212
+ )
213
+ }, 2000)
214
+ throw new this.hapErr(-70402)
215
+ }
216
+ }
217
+
218
+ async internalSpeedUpdate(value) {
219
+ try {
220
+ // The fan is used for the following modes (basically all except Auto):
221
+ // - 0%_: Not sure what to do with this yet
222
+ // - 33%: Low Mode
223
+ // - 66%: Medium Mode
224
+ // - 99%: High Mode
225
+ // If the main heater is turned off then this fan should be turned off too
226
+ // If the main heater is turned on then this fan speed should revert to the current mode
227
+
228
+ // Don't continue if the new value is the same as before
229
+ // If the new speed is 0, the on/off handler should take care of resetting to the speed before (home app only)
230
+ if (this.cacheSpeed === value || value === 0) {
231
+ return
232
+ }
233
+
234
+ // Send the request to the platform sender function
235
+ await this.platform.sendDeviceUpdate(this.accessory, {
236
+ cmd: 'ptReal',
237
+ value: this.speedCode[value],
238
+ })
239
+
240
+ // Cache the new state and log if appropriate
241
+ if (this.cacheSpeed !== value) {
242
+ this.cacheSpeed = value
243
+ this.accessory.log(`${platformLang.curSpeed} [${this.speedCodeLabel[value]}]`)
244
+ }
245
+ } catch (err) {
246
+ // Catch any errors during the process
247
+ this.accessory.logWarn(`${platformLang.devNotUpdated} ${parseError(err)}`)
248
+
249
+ // Throw a 'no response' error and set a timeout to revert this after 2 seconds
250
+ setTimeout(() => {
251
+ this.service.updateCharacteristic(this.hapChar.On, this.cacheState === 'on')
252
+ }, 2000)
253
+ throw new this.hapErr(-70402)
254
+ }
255
+ }
256
+
257
+ externalUpdate(params) {
258
+ // Update the active characteristic
259
+ if (params.state && params.state !== this.cacheState) {
260
+ this.cacheState = params.state
261
+ this.service.updateCharacteristic(this.hapChar.Active, this.cacheState === 'on')
262
+ this.accessory.log(`${platformLang.curState} [${this.cacheState}]`)
263
+ }
264
+
265
+ // Update the current temperature characteristic
266
+ if (hasProperty(params, 'temperature')) {
267
+ const newTemp = nearestHalf(farToCen(params.temperature / 100))
268
+ if (newTemp <= 100) {
269
+ // Device must be one that DOES support ambient temperature
270
+ this.accessory.logWarn('you should enable `tempReporting` in the config for this device')
271
+ }
272
+ }
273
+
274
+ // Check for some other scene/mode change
275
+ (params.commands || []).forEach((command) => {
276
+ const hexString = base64ToHex(command)
277
+ const hexParts = hexToTwoItems(hexString)
278
+
279
+ // Return now if not a device query update code
280
+ if (getTwoItemPosition(hexParts, 1) !== 'aa') {
281
+ return
282
+ }
283
+
284
+ const deviceFunction = `${getTwoItemPosition(hexParts, 2)}${getTwoItemPosition(hexParts, 3)}`
285
+
286
+ switch (deviceFunction) {
287
+ case '1800':
288
+ case '1801': {
289
+ // Swing Mode
290
+ const newSwing = getTwoItemPosition(hexParts, 3) === '01' ? 'on' : 'off'
291
+ if (this.cacheSwing !== newSwing) {
292
+ this.cacheSwing = newSwing
293
+ this.service.updateCharacteristic(this.hapChar.SwingMode, this.cacheSwing === 'on' ? 1 : 0)
294
+ this.accessory.log(`${platformLang.curSwing} [${this.cacheSwing}]`)
295
+ }
296
+ break
297
+ }
298
+ case '1000':
299
+ case '1001': {
300
+ // Child Lock
301
+ const newLock = getTwoItemPosition(hexParts, 3) === '01' ? 'on' : 'off'
302
+ if (this.cacheLock !== newLock) {
303
+ this.cacheLock = newLock
304
+ this.service.updateCharacteristic(this.hapChar.LockPhysicalControls, this.cacheLock === 'on' ? 1 : 0)
305
+ this.accessory.log(`${platformLang.curLock} [${this.cacheLock}]`)
306
+ }
307
+ break
308
+ }
309
+ case '0501': // fan speed low
310
+ case '0502': // fan speed medium
311
+ case '0503': { // fan speed high
312
+ switch (getTwoItemPosition(hexParts, 3)) {
313
+ case '01': {
314
+ // Fan is low
315
+ if (this.cacheSpeed !== 33) {
316
+ this.cacheSpeed = 33
317
+ this.service.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheSpeed)
318
+ this.accessory.log(`${platformLang.curSpeed} [${this.speedCodeLabel[this.cacheSpeed]}]`)
319
+ }
320
+ break
321
+ }
322
+ case '02': {
323
+ // Fan is medium
324
+ if (this.cacheSpeed !== 66) {
325
+ this.cacheSpeed = 66
326
+ this.service.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheSpeed)
327
+ this.accessory.log(`${platformLang.curSpeed} [${this.speedCodeLabel[this.cacheSpeed]}]`)
328
+ }
329
+ break
330
+ }
331
+ case '03': {
332
+ // Fan is high
333
+ if (this.cacheSpeed !== 99) {
334
+ this.cacheSpeed = 99
335
+ this.service.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheSpeed)
336
+ this.accessory.log(`${platformLang.curSpeed} [${this.speedCodeLabel[this.cacheSpeed]}]`)
337
+ }
338
+ break
339
+ }
340
+ }
341
+ break
342
+ }
343
+ case '1a00': // Target temperature (thermostat mode off)
344
+ case '1a01': { // Target temperature (thermostat mode on)
345
+ break
346
+ }
347
+ default:
348
+ this.accessory.logDebugWarn(`${platformLang.newScene}: [${command}] [${hexString}]`)
349
+ break
350
+ }
351
+ })
352
+ }
353
+ }