@homebridge-plugins/homebridge-meross 10.8.0

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 (54) hide show
  1. package/CHANGELOG.md +1346 -0
  2. package/LICENSE +21 -0
  3. package/README.md +68 -0
  4. package/config.schema.json +2066 -0
  5. package/eslint.config.js +49 -0
  6. package/lib/connection/http.js +345 -0
  7. package/lib/connection/mqtt.js +174 -0
  8. package/lib/device/baby.js +532 -0
  9. package/lib/device/cooler-single.js +447 -0
  10. package/lib/device/diffuser.js +730 -0
  11. package/lib/device/fan.js +530 -0
  12. package/lib/device/garage-main.js +225 -0
  13. package/lib/device/garage-single.js +495 -0
  14. package/lib/device/garage-sub.js +376 -0
  15. package/lib/device/heater-single.js +445 -0
  16. package/lib/device/hub-contact.js +56 -0
  17. package/lib/device/hub-leak.js +86 -0
  18. package/lib/device/hub-main.js +403 -0
  19. package/lib/device/hub-sensor.js +115 -0
  20. package/lib/device/hub-smoke.js +40 -0
  21. package/lib/device/hub-valve.js +377 -0
  22. package/lib/device/humidifier.js +521 -0
  23. package/lib/device/index.js +63 -0
  24. package/lib/device/light-cct.js +474 -0
  25. package/lib/device/light-dimmer.js +312 -0
  26. package/lib/device/light-rgb.js +528 -0
  27. package/lib/device/outlet-multi.js +383 -0
  28. package/lib/device/outlet-single.js +405 -0
  29. package/lib/device/power-strip.js +282 -0
  30. package/lib/device/purifier-single.js +372 -0
  31. package/lib/device/purifier.js +403 -0
  32. package/lib/device/roller-location.js +317 -0
  33. package/lib/device/roller.js +234 -0
  34. package/lib/device/sensor-presence.js +201 -0
  35. package/lib/device/switch-multi.js +403 -0
  36. package/lib/device/switch-single.js +371 -0
  37. package/lib/device/template.js +177 -0
  38. package/lib/device/thermostat.js +493 -0
  39. package/lib/fakegato/LICENSE +21 -0
  40. package/lib/fakegato/fakegato-history.js +814 -0
  41. package/lib/fakegato/fakegato-storage.js +108 -0
  42. package/lib/fakegato/fakegato-timer.js +125 -0
  43. package/lib/fakegato/uuid.js +27 -0
  44. package/lib/homebridge-ui/public/index.html +316 -0
  45. package/lib/homebridge-ui/server.js +10 -0
  46. package/lib/index.js +8 -0
  47. package/lib/platform.js +1256 -0
  48. package/lib/utils/colour.js +581 -0
  49. package/lib/utils/constants.js +377 -0
  50. package/lib/utils/custom-chars.js +165 -0
  51. package/lib/utils/eve-chars.js +130 -0
  52. package/lib/utils/functions.js +39 -0
  53. package/lib/utils/lang-en.js +114 -0
  54. package/package.json +70 -0
@@ -0,0 +1,405 @@
1
+ import PQueue from 'p-queue'
2
+ import { TimeoutError } from 'p-timeout'
3
+
4
+ import mqttClient from '../connection/mqtt.js'
5
+ import platformConsts from '../utils/constants.js'
6
+ import { hasProperty, parseError } from '../utils/functions.js'
7
+ import platformLang from '../utils/lang-en.js'
8
+
9
+ export default class {
10
+ constructor(platform, accessory) {
11
+ // Set up variables from the platform
12
+ this.eveChar = platform.eveChar
13
+ this.hapChar = platform.api.hap.Characteristic
14
+ this.hapErr = platform.api.hap.HapStatusError
15
+ this.hapServ = platform.api.hap.Service
16
+ this.platform = platform
17
+
18
+ // Set up variables from the accessory
19
+ this.accessory = accessory
20
+ this.inUsePowerThreshold = this.accessory.context.options.inUsePowerThreshold
21
+ || platformConsts.defaultValues.inUsePowerThreshold
22
+ this.name = accessory.displayName
23
+ const cloudRefreshRate = hasProperty(platform.config, 'cloudRefreshRate')
24
+ ? platform.config.cloudRefreshRate
25
+ : platformConsts.defaultValues.cloudRefreshRate
26
+ const localRefreshRate = hasProperty(platform.config, 'refreshRate')
27
+ ? platform.config.refreshRate
28
+ : platformConsts.defaultValues.refreshRate
29
+ this.pollInterval = accessory.context.connection === 'local'
30
+ ? localRefreshRate
31
+ : cloudRefreshRate;
32
+
33
+ // If the accessory has any old services then remove them
34
+ ['Switch', 'HeaterCooler', 'AirPurifier'].forEach((service) => {
35
+ if (this.accessory.getService(this.hapServ[service])) {
36
+ this.accessory.removeService(this.accessory.getService(this.hapServ[service]))
37
+ }
38
+ })
39
+
40
+ // Add the outlet service if it doesn't already exist
41
+ this.service = this.accessory.getService(this.hapServ.Outlet)
42
+ || this.accessory.addService(this.hapServ.Outlet)
43
+
44
+ // Add the set handler to the switch on/off characteristic
45
+ this.service
46
+ .getCharacteristic(this.hapChar.On)
47
+ .onSet(async value => this.internalStateUpdate(value))
48
+ this.cacheState = this.service.getCharacteristic(this.hapChar.On).value
49
+
50
+ // Pass the accessory to Fakegato to set up with Eve
51
+ this.accessory.eveService = new platform.eveService('energy', this.accessory, { log: () => {} })
52
+
53
+ // Create the queue used for sending device requests
54
+ this.updateInProgress = false
55
+ this.queue = new PQueue({
56
+ concurrency: 1,
57
+ interval: 250,
58
+ intervalCap: 1,
59
+ timeout: 10000,
60
+ throwOnTimeout: true,
61
+ })
62
+ this.queue.on('idle', () => {
63
+ this.updateInProgress = false
64
+ })
65
+
66
+ // Set up the mqtt client for cloud devices to send and receive device updates
67
+ if (accessory.context.connection !== 'local') {
68
+ this.accessory.mqtt = new mqttClient(platform, this.accessory)
69
+ this.accessory.mqtt.connect()
70
+ }
71
+
72
+ // Always request a device update on startup, then start the interval for polling
73
+ setTimeout(() => this.requestUpdate(true), 2000)
74
+ this.accessory.refreshInterval = setInterval(
75
+ () => this.requestUpdate(),
76
+ this.pollInterval * 1000,
77
+ )
78
+
79
+ // Test to see if the device supports power usage
80
+ setTimeout(() => this.setupPowerReadings(), 5000)
81
+
82
+ // Output the customised options to the log
83
+ const opts = JSON.stringify({
84
+ connection: this.accessory.context.connection,
85
+ inUsePowerThreshold: this.inUsePowerThreshold,
86
+ showAs: 'outlet',
87
+ })
88
+ platform.log('[%s] %s %s.', this.name, platformLang.devInitOpts, opts)
89
+ }
90
+
91
+ async internalStateUpdate(value) {
92
+ try {
93
+ // Add the request to the queue so updates are sent apart
94
+ await this.queue.add(async () => {
95
+ // Don't continue if the state is the same as before
96
+ if (value === this.cacheState) {
97
+ return
98
+ }
99
+
100
+ // This flag stops the plugin from requesting updates while pending on others
101
+ this.updateInProgress = true
102
+
103
+ // The plugin should have determined if it's 'toggle' or 'togglex' on the first poll run
104
+ let namespace
105
+ let payload
106
+ if (this.isToggleX) {
107
+ namespace = 'Appliance.Control.ToggleX'
108
+ payload = {
109
+ togglex: {
110
+ onoff: value ? 1 : 0,
111
+ channel: 0,
112
+ },
113
+ }
114
+ } else {
115
+ namespace = 'Appliance.Control.Toggle'
116
+ payload = {
117
+ toggle: {
118
+ onoff: value ? 1 : 0,
119
+ },
120
+ }
121
+ }
122
+
123
+ // Use the platform function to send the update to the device
124
+ await this.platform.sendUpdate(this.accessory, {
125
+ namespace,
126
+ payload,
127
+ })
128
+
129
+ // Update the cache and log the update has been successful
130
+ this.cacheState = value
131
+ this.accessory.log(`${platformLang.curState} [${value ? 'on' : 'off'}]`)
132
+ })
133
+ } catch (err) {
134
+ // Catch any errors whilst updating the device
135
+ const eText = err instanceof TimeoutError ? platformLang.timeout : parseError(err)
136
+ this.accessory.logWarn(`${platformLang.sendFailed} ${eText}`)
137
+ setTimeout(() => {
138
+ this.service.updateCharacteristic(this.hapChar.On, this.cacheState)
139
+ }, 2000)
140
+ throw new this.hapErr(-70402)
141
+ }
142
+ }
143
+
144
+ async requestUpdate(firstRun = false) {
145
+ try {
146
+ // Don't continue if an update is currently being sent to the device
147
+ if (this.updateInProgress) {
148
+ return
149
+ }
150
+
151
+ // Add the request to the queue so updates are sent apart
152
+ await this.queue.add(async () => {
153
+ // This flag stops the plugin from requesting updates while pending on others
154
+ this.updateInProgress = true
155
+
156
+ // Send the request
157
+ const res = await this.platform.sendUpdate(this.accessory, {
158
+ namespace: 'Appliance.System.All',
159
+ payload: {},
160
+ })
161
+
162
+ // Log the received data
163
+ this.accessory.logDebug(`${platformLang.incPoll}: ${JSON.stringify(res.data)}`)
164
+
165
+ // Check the response is in a useful format
166
+ const data = res.data.payload
167
+ if (data.all) {
168
+ if (data.all.digest) {
169
+ if (data.all.digest.togglex && data.all.digest.togglex[0]) {
170
+ this.isToggleX = true
171
+ this.applyUpdate(data.all.digest.togglex[0])
172
+ } else if (data.all.digest.toggle) {
173
+ this.isToggleX = false
174
+ this.applyUpdate(data.all.digest.toggle)
175
+ }
176
+ }
177
+
178
+ // A flag to check if we need to update the accessory context
179
+ let needsUpdate = false
180
+
181
+ // Get the mac address and hardware version of the device
182
+ if (data.all.system) {
183
+ // Mac address and hardware don't change regularly so only get on first poll
184
+ if (firstRun && data.all.system.hardware) {
185
+ this.accessory.context.macAddress = data.all.system.hardware.macAddress.toUpperCase()
186
+ this.accessory.context.hardware = data.all.system.hardware.version
187
+ }
188
+
189
+ // Get the ip address and firmware of the device
190
+ if (data.all.system.firmware) {
191
+ // Check for an IP change each and every time the device is polled
192
+ if (this.accessory.context.ipAddress !== data.all.system.firmware.innerIp) {
193
+ this.accessory.context.ipAddress = data.all.system.firmware.innerIp
194
+ needsUpdate = true
195
+ }
196
+
197
+ // Firmware doesn't change regularly so only get on first poll
198
+ if (firstRun) {
199
+ this.accessory.context.firmware = data.all.system.firmware.version
200
+ }
201
+ }
202
+ }
203
+
204
+ // Get the cloud online status of the device
205
+ if (data.all.system.online) {
206
+ const isOnline = data.all.system.online.status === 1
207
+ if (this.accessory.context.isOnline !== isOnline) {
208
+ this.accessory.context.isOnline = isOnline
209
+ needsUpdate = true
210
+ }
211
+ }
212
+
213
+ // Update the accessory cache if anything has changed
214
+ if (needsUpdate || firstRun) {
215
+ this.platform.updateAccessory(this.accessory)
216
+ }
217
+ }
218
+ })
219
+ } catch (err) {
220
+ const eText = err instanceof TimeoutError ? platformLang.timeout : parseError(err)
221
+ this.accessory.logDebugWarn(`${platformLang.reqFailed}: ${eText}`)
222
+
223
+ // Set the homebridge-ui status of the device to offline if local and error is timeout
224
+ if (
225
+ (this.accessory.context.isOnline || firstRun)
226
+ && ['EHOSTUNREACH', 'timed out'].some(el => eText.includes(el))
227
+ ) {
228
+ this.accessory.context.isOnline = false
229
+ this.platform.updateAccessory(this.accessory)
230
+ }
231
+ }
232
+ }
233
+
234
+ receiveUpdate(params) {
235
+ try {
236
+ // Log the received data
237
+ this.accessory.logDebug(`${platformLang.incMQTT}: ${JSON.stringify(params)}`)
238
+ if (params.payload) {
239
+ if (params.payload.togglex && params.payload.togglex[0]) {
240
+ this.applyUpdate(params.payload.togglex[0])
241
+ } else if (params.payload.toggle) {
242
+ this.applyUpdate(params.payload.toggle)
243
+ }
244
+ }
245
+ } catch (err) {
246
+ this.accessory.logWarn(`${platformLang.refFailed} ${parseError(err)}`)
247
+ }
248
+ }
249
+
250
+ applyUpdate(data) {
251
+ // Check the data is in a format which contains the value we need
252
+ if (hasProperty(data, 'onoff')) {
253
+ // newState is given as 0 or 1 -> convert to bool for HomeKit
254
+ const newState = data.onoff === 1
255
+
256
+ // Check against the cache and update HomeKit and the cache if needed
257
+ if (this.cacheState !== newState) {
258
+ this.service.updateCharacteristic(this.hapChar.On, newState)
259
+ this.cacheState = newState
260
+ this.accessory.log(`${platformLang.curState} [${newState ? 'on' : 'off'}]`)
261
+ }
262
+ }
263
+ if (hasProperty(data, 'power')) {
264
+ const newPower = data.power
265
+ const scaledPower = Math.round(newPower / 10) / 100
266
+ const newkWh = scaledPower / 60000
267
+
268
+ // Check against the cache and update HomeKit and the cache if needed
269
+ let newInUse = this.cacheInUse
270
+ let doLog = false
271
+ if (this.cachePower !== newPower) {
272
+ newInUse = this.cacheState && scaledPower > this.inUsePowerThreshold
273
+ this.service.updateCharacteristic(this.eveChar.CurrentConsumption, scaledPower)
274
+ this.accessory.eveService.addEntry({ power: scaledPower })
275
+ this.cachePower = newPower
276
+ if (this.cacheScaledPower !== scaledPower) {
277
+ this.cacheScaledPower = scaledPower
278
+ doLog = true
279
+ }
280
+ }
281
+
282
+ // Update the total consumption, approximating by using this power value as during a minute
283
+ const newTotalkWh = newkWh + this.accessory.context.totalkWh
284
+ this.accessory.context.totalkWh = newTotalkWh
285
+ if (newTotalkWh !== this.cacheTotal) {
286
+ this.service.updateCharacteristic(this.eveChar.TotalConsumption, newTotalkWh)
287
+ this.cacheTotal = newTotalkWh
288
+ const newScaledTotal = Math.round(newTotalkWh * 100) / 100
289
+ if (this.cacheScaledTotal !== newScaledTotal) {
290
+ this.cacheScaledTotal = newScaledTotal
291
+ doLog = true
292
+ }
293
+ }
294
+
295
+ // Log both the W and kWh if either have changed
296
+ if (doLog) {
297
+ this.accessory.logDebug(`${platformLang.curPower} [${this.cacheScaledPower}W] [${this.cacheScaledTotal}kWh]`)
298
+ }
299
+
300
+ // Update the OutletInUse property
301
+ if (this.cacheInUse !== newInUse) {
302
+ this.cacheInUse = newInUse
303
+ this.service.updateCharacteristic(this.hapChar.OutletInUse, !!newInUse)
304
+ this.accessory.log(`${platformLang.curInUse} [${newInUse ? 'yes' : 'no'}]`)
305
+ }
306
+ }
307
+ if (hasProperty(data, 'voltage')) {
308
+ // newState is given as 0 or 1 -> convert to bool for HomeKit
309
+ const newVoltage = data.voltage
310
+
311
+ // Check against the cache and update HomeKit and the cache if needed
312
+ if (this.cacheVoltage !== newVoltage) {
313
+ const scaledVoltage = Math.round(newVoltage * 10) / 100
314
+ this.service.updateCharacteristic(this.eveChar.Voltage, scaledVoltage)
315
+ this.cacheVoltage = newVoltage
316
+ this.accessory.logDebug(`${platformLang.curVolt} [${scaledVoltage}V]`)
317
+ }
318
+ }
319
+ }
320
+
321
+ async setupPowerReadings() {
322
+ try {
323
+ // Add the request to the queue so updates are sent apart
324
+ await this.queue.add(async () => {
325
+ // This flag stops the plugin from requesting updates while pending on others
326
+ this.updateInProgress = true
327
+ // Send the request
328
+ const res = await this.platform.sendUpdate(this.accessory, {
329
+ namespace: 'Appliance.Control.Electricity',
330
+ payload: {},
331
+ })
332
+ // Check the response is in a useful format
333
+ if (!res.data.payload || !res.data.payload.electricity) {
334
+ throw new Error('no data on initial run')
335
+ }
336
+
337
+ // Setup the outlet in use and Eve characteristics
338
+ if (!this.service.testCharacteristic(this.hapChar.OutletInUse)) {
339
+ this.service.addCharacteristic(this.hapChar.OutletInUse)
340
+ }
341
+ this.cacheInUse = this.service.getCharacteristic(this.hapChar.OutletInUse).value
342
+ if (!this.service.testCharacteristic(this.eveChar.CurrentConsumption)) {
343
+ this.service.addCharacteristic(this.eveChar.CurrentConsumption)
344
+ }
345
+ if (!this.service.testCharacteristic(this.eveChar.TotalConsumption)) {
346
+ this.service.addCharacteristic(this.eveChar.TotalConsumption)
347
+ }
348
+ if (!this.service.testCharacteristic(this.eveChar.Voltage)) {
349
+ this.service.addCharacteristic(this.eveChar.Voltage)
350
+ }
351
+ if (!this.service.testCharacteristic(this.eveChar.ResetTotal)) {
352
+ this.service.addCharacteristic(this.eveChar.ResetTotal)
353
+ }
354
+ if (!hasProperty(this.accessory.context, 'totalkWh')) {
355
+ this.accessory.context.totalkWh = 0
356
+ }
357
+
358
+ // Add the set handler to the outlet eve reset total energy characteristic
359
+ this.service.getCharacteristic(this.eveChar.ResetTotal).onSet(() => {
360
+ this.accessory.context.totalkWh = 0
361
+ this.service.updateCharacteristic(this.eveChar.TotalConsumption, 0)
362
+ })
363
+
364
+ // Create the poll
365
+ this.requestPowerReadings()
366
+ this.accessory.powerInterval = setInterval(() => this.requestPowerReadings(), 60000)
367
+ })
368
+ } catch (err) {
369
+ const eText = err instanceof TimeoutError
370
+ ? platformLang.timeout
371
+ : parseError(err, ['no data on initial run'])
372
+ this.accessory.logDebug(`${platformLang.disablingPower} ${eText}`)
373
+ }
374
+ }
375
+
376
+ async requestPowerReadings() {
377
+ try {
378
+ // Add the request to the queue so updates are sent apart
379
+ await this.queue.add(async () => {
380
+ // This flag stops the plugin from requesting updates while pending on others
381
+ this.updateInProgress = true
382
+ // Send the request
383
+ const res = await this.platform.sendUpdate(this.accessory, {
384
+ namespace: 'Appliance.Control.Electricity',
385
+ payload: {},
386
+ })
387
+
388
+ // Log the received data
389
+ this.accessory.logDebug(`${platformLang.incPoll}: ${JSON.stringify(res.data)}`)
390
+
391
+ // Check the response is in a useful format
392
+ const data = res.data.payload
393
+ if (data && data.electricity) {
394
+ this.applyUpdate(data.electricity)
395
+ }
396
+ })
397
+ } catch (err) {
398
+ const eText = err instanceof TimeoutError ? platformLang.timeout : parseError(err)
399
+ this.accessory.logDebugWarn(`${platformLang.powerFail} ${eText}`)
400
+
401
+ // Also don't increase the measured total consumption in case of any error
402
+ this.accessory.eveService.addEntry({ power: 0 })
403
+ }
404
+ }
405
+ }
@@ -0,0 +1,282 @@
1
+ import PQueue from 'p-queue'
2
+ import { TimeoutError } from 'p-timeout'
3
+
4
+ import mqttClient from '../connection/mqtt.js'
5
+ import platformConsts from '../utils/constants.js'
6
+ import { hasProperty, parseError } from '../utils/functions.js'
7
+ import platformLang from '../utils/lang-en.js'
8
+
9
+ export default class {
10
+ constructor(platform, accessory) {
11
+ // Set up variables from the platform
12
+ this.hapChar = platform.api.hap.Characteristic
13
+ this.hapErr = platform.api.hap.HapStatusError
14
+ this.hapServ = platform.api.hap.Service
15
+ this.platform = platform
16
+
17
+ // Set up variables from the accessory
18
+ this.accessory = accessory
19
+ this.name = accessory.displayName
20
+ const cloudRefreshRate = hasProperty(platform.config, 'cloudRefreshRate')
21
+ ? platform.config.cloudRefreshRate
22
+ : platformConsts.defaultValues.cloudRefreshRate
23
+ const localRefreshRate = hasProperty(platform.config, 'refreshRate')
24
+ ? platform.config.refreshRate
25
+ : platformConsts.defaultValues.refreshRate
26
+ this.pollInterval = accessory.context.connection === 'local'
27
+ ? localRefreshRate
28
+ : cloudRefreshRate
29
+
30
+ // Add the outlet services for each channel
31
+ for (let i = 0; i < accessory.context.channelCount; i += 1) {
32
+ if (i !== 0) {
33
+ const outletService = this.accessory.getService(`outlet-${i}`)
34
+ if (!outletService) {
35
+ const channelName = accessory.context.channels[i]?.devName || `Outlet ${i}`
36
+ this.accessory.addService(this.hapServ.Outlet, channelName, `outlet-${i}`)
37
+ }
38
+ }
39
+ }
40
+
41
+ // Add the set handler to the switch on/off characteristic for each channel
42
+ for (let i = 1; i < accessory.context.channelCount; i += 1) {
43
+ const outletService = this.accessory.getService(`outlet-${i}`)
44
+ outletService
45
+ .getCharacteristic(this.hapChar.On)
46
+ .onSet(async value => this.internalStateUpdate(value, i))
47
+ }
48
+
49
+ // Create the queue used for sending device requests
50
+ this.updateInProgress = false
51
+ this.queue = new PQueue({
52
+ concurrency: 1,
53
+ interval: 250,
54
+ intervalCap: 1,
55
+ timeout: 10000,
56
+ throwOnTimeout: true,
57
+ })
58
+ this.queue.on('idle', () => {
59
+ this.updateInProgress = false
60
+ })
61
+
62
+ // Set up the mqtt client for cloud devices to send and receive device updates
63
+ if (accessory.context.connection !== 'local') {
64
+ this.accessory.mqtt = new mqttClient(platform, this.accessory)
65
+ this.accessory.mqtt.connect()
66
+ }
67
+
68
+ // Always request a device update on startup, then start the interval for polling
69
+ setTimeout(() => this.requestUpdate(true), 2000)
70
+ this.accessory.refreshInterval = setInterval(
71
+ () => this.requestUpdate(),
72
+ this.pollInterval * 1000,
73
+ )
74
+
75
+ // Output the customised options to the log
76
+ const opts = JSON.stringify({
77
+ connection: this.accessory.context.connection,
78
+ showAs: 'power-strip',
79
+ })
80
+ platform.log('[%s] %s %s.', this.name, platformLang.devInitOpts, opts)
81
+ }
82
+
83
+ async internalStateUpdate(value, channel) {
84
+ try {
85
+ // Add the request to the queue so updates are sent apart
86
+ await this.queue.add(async () => {
87
+ // Don't continue if the state is the same as before
88
+ if (value === this.accessory.getService(`outlet-${channel}`)
89
+ .getCharacteristic(this.hapChar.On)
90
+ .value) {
91
+ return
92
+ }
93
+
94
+ // This flag stops the plugin from requesting updates while pending on others
95
+ this.updateInProgress = true
96
+
97
+ // Generate the payload and namespace for the correct device model
98
+ const namespace = 'Appliance.Control.ToggleX'
99
+ const payload = {
100
+ togglex: {
101
+ onoff: value ? 1 : 0,
102
+ channel,
103
+ },
104
+ }
105
+
106
+ // Use the platform function to send the update to the device
107
+ await this.platform.sendUpdate(this.accessory, {
108
+ namespace,
109
+ payload,
110
+ })
111
+
112
+ // Update the cache and log the update has been successful
113
+ this.accessory.getService(`outlet-${channel}`)
114
+ .getCharacteristic(this.hapChar.On)
115
+ .updateValue(value)
116
+
117
+ this.accessory.log(`${platformLang.curState} [${value ? 'on' : 'off'}]`)
118
+ })
119
+ } catch (err) {
120
+ // Catch any errors whilst updating the device
121
+ const eText = err instanceof TimeoutError ? platformLang.timeout : parseError(err)
122
+ this.accessory.logWarn(`${platformLang.sendFailed} ${eText}`)
123
+ setTimeout(() => {
124
+ this.accessory.getService(`outlet-${channel}`)
125
+ .getCharacteristic(this.hapChar.On)
126
+ .updateValue(
127
+ this.accessory.getService(`outlet-${channel}`).getCharacteristic(this.hapChar.On).value,
128
+ )
129
+ }, 2000)
130
+ throw new this.hapErr(-70402)
131
+ }
132
+ }
133
+
134
+ async requestUpdate(firstRun = false) {
135
+ try {
136
+ // Don't continue if an update is currently being sent to the device
137
+ if (this.updateInProgress) {
138
+ return
139
+ }
140
+
141
+ // Add the request to the queue so updates are sent apart
142
+ await this.queue.add(async () => {
143
+ // This flag stops the plugin from requesting updates while pending on others
144
+ this.updateInProgress = true
145
+
146
+ // Send the request
147
+ const res = await this.platform.sendUpdate(this.accessory, {
148
+ namespace: 'Appliance.System.All',
149
+ payload: {},
150
+ })
151
+
152
+ // Log the received data
153
+ this.accessory.logDebug(`${platformLang.incPoll}: ${JSON.stringify(res.data)}`)
154
+
155
+ // Check the response is in a useful format
156
+ const data = res.data.payload
157
+ if (data.all) {
158
+ if (data.all.digest && data.all.digest.togglex && Array.isArray(data.all.digest.togglex)) {
159
+ this.applyUpdate(data.all.digest.togglex)
160
+ }
161
+
162
+ // A flag to check if we need to update the accessory context
163
+ let needsUpdate = false
164
+
165
+ // Get the mac address and hardware version of the device
166
+ if (data.all.system) {
167
+ // Mac address and hardware don't change regularly so only get on first poll
168
+ if (firstRun && data.all.system.hardware) {
169
+ this.accessory.context.macAddress = data.all.system.hardware.macAddress.toUpperCase()
170
+ this.accessory.context.hardware = data.all.system.hardware.version
171
+ }
172
+
173
+ // Get the ip address and firmware of the device
174
+ if (data.all.system.firmware) {
175
+ // Check for an IP change each and every time the device is polled
176
+ if (this.accessory.context.ipAddress !== data.all.system.firmware.innerIp) {
177
+ this.accessory.context.ipAddress = data.all.system.firmware.innerIp
178
+ needsUpdate = true
179
+ }
180
+
181
+ // Firmware doesn't change regularly so only get on first poll
182
+ if (firstRun) {
183
+ this.accessory.context.firmware = data.all.system.firmware.version
184
+ }
185
+ }
186
+ }
187
+
188
+ // Get the cloud online status of the device
189
+ if (data.all.system.online) {
190
+ const isOnline = data.all.system.online.status === 1
191
+ if (this.accessory.context.isOnline !== isOnline) {
192
+ this.accessory.context.isOnline = isOnline
193
+ needsUpdate = true
194
+ }
195
+ }
196
+
197
+ // Update the accessory cache if anything has changed
198
+ if (needsUpdate || firstRun) {
199
+ this.accessory.context = {
200
+ ...this.accessory.context,
201
+
202
+ macAddress: this.accessory.context.macAddress,
203
+ hardware: this.accessory.context.hardware,
204
+ ipAddress: this.accessory.context.ipAddress,
205
+ firmware: this.accessory.context.firmware,
206
+ isOnline: this.accessory.context.isOnline
207
+ ,
208
+ }
209
+ this.platform.updateAccessory(this.accessory)
210
+ }
211
+ }
212
+ })
213
+ } catch (err) {
214
+ const eText = err instanceof TimeoutError ? platformLang.timeout : parseError(err)
215
+ this.accessory.logDebugWarn(`${platformLang.reqFailed}: ${eText}`)
216
+
217
+ // Set the homebridge-ui status of the device to offline if local and error is timeout
218
+ if (
219
+ (this.accessory.context.isOnline || firstRun)
220
+ && ['EHOSTUNREACH', 'timed out'].some(el => eText.includes(el))
221
+ ) {
222
+ this.accessory.context.isOnline = false
223
+ this.platform.updateAccessory(this.accessory)
224
+ }
225
+ }
226
+ }
227
+
228
+ receiveUpdate(params) {
229
+ try {
230
+ // Log the received data
231
+ this.accessory.logDebug(`${platformLang.incMQTT}: ${JSON.stringify(params)}`)
232
+
233
+ // Validate the response, checking for payload property
234
+ if (!params.payload) {
235
+ throw new Error('invalid response received')
236
+ }
237
+ const data = params.payload
238
+
239
+ // Check the data is in a format which contains the value we need
240
+ if (data.togglex) {
241
+ // payload.togglex can either be an array of objects (multiple channels) or a single object
242
+ // Either way, push all items into one array
243
+ const toUpdate = []
244
+ if (Array.isArray(data.togglex)) {
245
+ data.togglex.forEach(item => toUpdate.push(item))
246
+ } else {
247
+ toUpdate.push(data.togglex)
248
+ }
249
+ this.applyUpdate(toUpdate)
250
+ }
251
+ } catch (err) {
252
+ this.accessory.logWarn(`${platformLang.refFailed} ${parseError(err)}`)
253
+ }
254
+ }
255
+
256
+ applyUpdate(data) {
257
+ data.forEach((channel) => {
258
+ if (channel.channel === 0) {
259
+ return
260
+ }
261
+
262
+ // Attempt to find the accessory this channel relates to
263
+ const { accessory } = this
264
+
265
+ // Obtain the service and current value
266
+ const hapServ = accessory.getService(`outlet-${channel.channel}`)
267
+ const hapChar = hapServ.getCharacteristic(this.hapChar.On)
268
+
269
+ // Read the current state
270
+ const newState = channel.onoff === 1
271
+
272
+ // Don't continue if the state is the same as before
273
+ if (hapChar.value === newState) {
274
+ return
275
+ }
276
+
277
+ // Update the HomeKit characteristics and log
278
+ hapChar.updateValue(newState)
279
+ this.accessory.log(`${platformLang.curState} [${newState ? 'on' : 'off'}]`)
280
+ })
281
+ }
282
+ }