@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,317 @@
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
+ // Remove any switch services that are not needed
31
+ if (this.accessory.getService('Open')) {
32
+ this.accessory.removeService(this.accessory.getService('Open'))
33
+ }
34
+ if (this.accessory.getService('Close')) {
35
+ this.accessory.removeService(this.accessory.getService('Close'))
36
+ }
37
+ if (this.accessory.getService('Stop')) {
38
+ this.accessory.removeService(this.accessory.getService('Stop'))
39
+ }
40
+
41
+ // Set up the correct service
42
+ let service
43
+ switch (accessory.context.options?.showAs) {
44
+ case 'door':
45
+ service = this.hapServ.Door
46
+ if (this.accessory.getService(this.hapServ.Window)) {
47
+ this.accessory.removeService(this.accessory.getService(this.hapServ.Window))
48
+ }
49
+ if (this.accessory.getService(this.hapServ.WindowCovering)) {
50
+ this.accessory.removeService(this.accessory.getService(this.hapServ.WindowCovering))
51
+ }
52
+ break
53
+ case 'window':
54
+ service = this.hapServ.Window
55
+ if (this.accessory.getService(this.hapServ.Door)) {
56
+ this.accessory.removeService(this.accessory.getService(this.hapServ.Door))
57
+ }
58
+ if (this.accessory.getService(this.hapServ.WindowCovering)) {
59
+ this.accessory.removeService(this.accessory.getService(this.hapServ.WindowCovering))
60
+ }
61
+ break
62
+ default: // window covering or undefined
63
+ service = this.hapServ.WindowCovering
64
+ if (this.accessory.getService(this.hapServ.Door)) {
65
+ this.accessory.removeService(this.accessory.getService(this.hapServ.Door))
66
+ }
67
+ if (this.accessory.getService(this.hapServ.Window)) {
68
+ this.accessory.removeService(this.accessory.getService(this.hapServ.Window))
69
+ }
70
+ break
71
+ }
72
+
73
+ // Obtain the correct service
74
+ this.service = this.accessory.getService(service) || this.accessory.addService(service)
75
+
76
+ // Add the set handler to the selected service
77
+ this.service
78
+ .getCharacteristic(this.hapChar.TargetPosition)
79
+ .onSet(async value => this.internalLocationUpdate(value))
80
+
81
+ this.cachePos = this.service.getCharacteristic(this.hapChar.CurrentPosition).value
82
+ this.cacheTarg = this.service.getCharacteristic(this.hapChar.TargetPosition).value
83
+
84
+ // Create the queue used for sending device requests
85
+ this.updateInProgress = false
86
+ this.queue = new PQueue({
87
+ concurrency: 1,
88
+ interval: 250,
89
+ intervalCap: 1,
90
+ timeout: 10000,
91
+ throwOnTimeout: true,
92
+ })
93
+ this.queue.on('idle', () => {
94
+ this.updateInProgress = false
95
+ })
96
+
97
+ // Set up the mqtt client for cloud devices to send and receive device updates
98
+ if (accessory.context.connection !== 'local') {
99
+ this.accessory.mqtt = new mqttClient(platform, this.accessory)
100
+ this.accessory.mqtt.connect()
101
+ }
102
+
103
+ // Always request a device update on startup, then start the interval for polling
104
+ setTimeout(() => this.requestUpdate(true), 2000)
105
+ this.accessory.refreshInterval = setInterval(
106
+ () => this.requestUpdate(),
107
+ this.pollInterval * 1000,
108
+ )
109
+
110
+ // Output the customised options to the log
111
+ const opts = JSON.stringify({
112
+ connection: this.accessory.context.connection,
113
+ showAs: this.accessory.context.options?.showAs || 'default',
114
+ })
115
+ platform.log('[%s] %s %s.', this.name, platformLang.devInitOpts, opts)
116
+ }
117
+
118
+ async internalLocationUpdate(value) {
119
+ try {
120
+ // Add the request to the queue so updates are sent apart
121
+ await this.queue.add(async () => {
122
+ // This flag stops the plugin from requesting updates while pending on others
123
+ this.updateInProgress = true
124
+
125
+ // Generate the payload and namespace for the correct device model
126
+ const namespace = 'Appliance.RollerShutter.Position'
127
+ const payload = {
128
+ position: {
129
+ position: value,
130
+ channel: 0,
131
+ },
132
+ }
133
+
134
+ // Use the platform function to send the update to the device
135
+ await this.platform.sendUpdate(this.accessory, {
136
+ namespace,
137
+ payload,
138
+ })
139
+
140
+ // Update the cache and log the update has been successful
141
+ this.cacheTarg = value
142
+ this.accessory.log(`${platformLang.curTarg} [${this.cacheTarg}%]`)
143
+
144
+ this.isFromHomeKit = true
145
+ setTimeout(() => {
146
+ this.isFromHomeKit = false
147
+ }, 2000)
148
+ })
149
+ } catch (err) {
150
+ // Catch any errors whilst updating the device
151
+ const eText = err instanceof TimeoutError ? platformLang.timeout : parseError(err)
152
+ this.accessory.logWarn(`${platformLang.sendFailed} ${eText}`)
153
+ setTimeout(() => {
154
+ this.service.updateCharacteristic(this.hapChar.TargetPosition, this.cacheTarg)
155
+ }, 2000)
156
+ throw new this.hapErr(-70402)
157
+ }
158
+ }
159
+
160
+ async requestUpdate(firstRun = false) {
161
+ try {
162
+ // Don't continue if an update is currently being sent to the device
163
+ if (this.updateInProgress) {
164
+ return
165
+ }
166
+
167
+ // Add the request to the queue so updates are sent apart
168
+ await this.queue.add(async () => {
169
+ // This flag stops the plugin from requesting updates while pending on others
170
+ this.updateInProgress = true
171
+
172
+ // Send the request
173
+ const res = await this.platform.sendUpdate(this.accessory, {
174
+ namespace: 'Appliance.System.All',
175
+ payload: {},
176
+ })
177
+
178
+ // Log the received data
179
+ this.accessory.logDebug(`${platformLang.incPoll}: ${JSON.stringify(res.data)}`)
180
+
181
+ // Check the response is in a useful format
182
+ const data = res.data.payload
183
+ if (data.all) {
184
+ if (data.all.digest) {
185
+ if (data.all.digest.position && data.all.digest.state[0]) {
186
+ this.applyUpdate(data.all.digest.state[0])
187
+ }
188
+ if (data.all.digest.position && data.all.digest.position[0]) {
189
+ this.applyUpdate(data.all.digest.position[0])
190
+ }
191
+ }
192
+ // A flag to check if we need to update the accessory context
193
+ let needsUpdate = false
194
+
195
+ // Get the mac address and hardware version of the device
196
+ if (data.all.system) {
197
+ // Mac address and hardware don't change regularly so only get on first poll
198
+ if (firstRun && data.all.system.hardware) {
199
+ this.accessory.context.macAddress = data.all.system.hardware.macAddress.toUpperCase()
200
+ this.accessory.context.hardware = data.all.system.hardware.version
201
+ }
202
+
203
+ // Get the ip address and firmware of the device
204
+ if (data.all.system.firmware) {
205
+ // Check for an IP change each and every time the device is polled
206
+ if (this.accessory.context.ipAddress !== data.all.system.firmware.innerIp) {
207
+ this.accessory.context.ipAddress = data.all.system.firmware.innerIp
208
+ needsUpdate = true
209
+ }
210
+
211
+ // Firmware doesn't change regularly so only get on first poll
212
+ if (firstRun) {
213
+ this.accessory.context.firmware = data.all.system.firmware.version
214
+ }
215
+ }
216
+ }
217
+
218
+ // Get the cloud online status of the device
219
+ if (data.all.system.online) {
220
+ const isOnline = data.all.system.online.status === 1
221
+ if (this.accessory.context.isOnline !== isOnline) {
222
+ this.accessory.context.isOnline = isOnline
223
+ needsUpdate = true
224
+ }
225
+ }
226
+
227
+ // Update the accessory cache if anything has changed
228
+ if (needsUpdate || firstRun) {
229
+ this.platform.updateAccessory(this.accessory)
230
+ }
231
+ }
232
+ })
233
+ } catch (err) {
234
+ const eText = err instanceof TimeoutError ? platformLang.timeout : parseError(err)
235
+ this.accessory.logDebugWarn(`${platformLang.reqFailed}: ${eText}`)
236
+
237
+ // Set the homebridge-ui status of the device to offline if local and error is timeout
238
+ if (
239
+ (this.accessory.context.isOnline || firstRun)
240
+ && ['EHOSTUNREACH', 'timed out'].some(el => eText.includes(el))
241
+ ) {
242
+ this.accessory.context.isOnline = false
243
+ this.platform.updateAccessory(this.accessory)
244
+ }
245
+ }
246
+ }
247
+
248
+ receiveUpdate(params) {
249
+ try {
250
+ // Log the received data
251
+ this.accessory.logDebug(`${platformLang.incMQTT}: ${JSON.stringify(params)}`)
252
+ if (params.payload) {
253
+ if (params.payload.state && params.payload.state[0]) {
254
+ this.applyUpdate(params.payload.state[0])
255
+ }
256
+ if (params.payload.position && params.payload.position[0]) {
257
+ this.applyUpdate(params.payload.position[0])
258
+ }
259
+ }
260
+ } catch (err) {
261
+ this.accessory.logWarn(`${platformLang.refFailed} ${parseError(err)}`)
262
+ }
263
+ }
264
+
265
+ applyUpdate(data) {
266
+ if (hasProperty(data, 'state')) {
267
+ // 0 -> stopped
268
+ // 1 -> opening
269
+ // 2 -> closing
270
+ if (this.cacheState !== data.state) {
271
+ this.cacheState = data.state
272
+ switch (this.cacheState) {
273
+ case 0: {
274
+ // Device has stopped, so the current position is now the target position
275
+ this.cacheTarg = this.cachePos
276
+ this.service.updateCharacteristic(this.hapChar.TargetPosition, this.cacheTarg)
277
+ this.service.updateCharacteristic(this.hapChar.PositionState, 2)
278
+ this.accessory.log(`${platformLang.curTarg} [${this.cacheTarg}%]`)
279
+ this.accessory.log(`${platformLang.curState} [stopped]`)
280
+ break
281
+ }
282
+ case 1: {
283
+ if (!this.isFromHomeKit) {
284
+ // Device is opening, so hacky set the target position to 100%, don't log this
285
+ this.cacheTarg = 100
286
+ this.service.updateCharacteristic(this.hapChar.TargetPosition, this.cacheTarg)
287
+ }
288
+ this.service.updateCharacteristic(this.hapChar.PositionState, 1)
289
+ this.accessory.log(`${platformLang.curState} [opening]`)
290
+ break
291
+ }
292
+ case 2: {
293
+ if (!this.isFromHomeKit) {
294
+ // Device is opening, so hacky set the target position to 0%, don't log this
295
+ this.cacheTarg = 0
296
+ this.service.updateCharacteristic(this.hapChar.TargetPosition, this.cacheTarg)
297
+ }
298
+ this.service.updateCharacteristic(this.hapChar.PositionState, 0)
299
+ this.accessory.log(`${platformLang.curState} [closing]`)
300
+ break
301
+ }
302
+ default: {
303
+ this.accessory.logWarn(`unknown state received [${this.cacheState}], please report on GitHub`)
304
+ }
305
+ }
306
+ }
307
+ }
308
+
309
+ if (hasProperty(data, 'position')) {
310
+ if (this.cachePos !== data.position) {
311
+ this.cachePos = data.position
312
+ this.service.updateCharacteristic(this.hapChar.CurrentPosition, this.cachePos)
313
+ this.accessory.log(`${platformLang.curPos} [${this.cachePos}%]`)
314
+ }
315
+ }
316
+ }
317
+ }
@@ -0,0 +1,234 @@
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
+ this.reversePolarity = this.accessory.context.options.reversePolarity
21
+ const cloudRefreshRate = hasProperty(platform.config, 'cloudRefreshRate')
22
+ ? platform.config.cloudRefreshRate
23
+ : platformConsts.defaultValues.cloudRefreshRate
24
+ const localRefreshRate = hasProperty(platform.config, 'refreshRate')
25
+ ? platform.config.refreshRate
26
+ : platformConsts.defaultValues.refreshRate
27
+ this.pollInterval = accessory.context.connection === 'local'
28
+ ? localRefreshRate
29
+ : cloudRefreshRate;
30
+
31
+ // Remove any potential leftover services
32
+ ['Window', 'Door', 'WindowCovering'].forEach((service) => {
33
+ if (this.accessory.getService(this.hapServ[service])) {
34
+ this.accessory.removeService(this.accessory.getService(this.hapServ[service]))
35
+ }
36
+ })
37
+
38
+ // Add the switch services
39
+ this.serviceOpen = this.accessory.getService('Open')
40
+ || this.accessory.addService(this.hapServ.Switch, 'Open', 'open')
41
+ this.serviceClose = this.accessory.getService('Close')
42
+ || this.accessory.addService(this.hapServ.Switch, 'Close', 'close')
43
+ this.serviceStop = this.accessory.getService('Stop')
44
+ || this.accessory.addService(this.hapServ.Switch, 'Stop', 'stop')
45
+
46
+ // Add the set handler to the open switch service
47
+ this.serviceOpen
48
+ .getCharacteristic(this.hapChar.On)
49
+ .onSet(async (value) => {
50
+ await this.internalStateUpdate(value, this.reversePolarity ? 0 : 100, this.serviceOpen)
51
+ })
52
+ .updateValue(false)
53
+
54
+ // Add the set handler to the close switch service
55
+ this.serviceClose
56
+ .getCharacteristic(this.hapChar.On)
57
+ .onSet(async (value) => {
58
+ await this.internalStateUpdate(value, this.reversePolarity ? 100 : 0, this.serviceClose)
59
+ })
60
+ .updateValue(false)
61
+
62
+ // Add the set handler to the stop switch service
63
+ this.serviceStop
64
+ .getCharacteristic(this.hapChar.On)
65
+ .onSet(async value => this.internalStateUpdate(value, -1, this.serviceStop))
66
+ .updateValue(false)
67
+
68
+ // Create the queue used for sending device requests
69
+ this.updateInProgress = false
70
+ this.queue = new PQueue({
71
+ concurrency: 1,
72
+ interval: 250,
73
+ intervalCap: 1,
74
+ timeout: 10000,
75
+ throwOnTimeout: true,
76
+ })
77
+ this.queue.on('idle', () => {
78
+ this.updateInProgress = false
79
+ })
80
+ this.states = {
81
+ 1: 'stopped',
82
+ 0: 'closing',
83
+ 100: 'opening',
84
+ }
85
+
86
+ // Set up the mqtt client for cloud devices to send and receive device updates
87
+ if (accessory.context.connection !== 'local') {
88
+ this.accessory.mqtt = new mqttClient(platform, this.accessory)
89
+ this.accessory.mqtt.connect()
90
+ }
91
+
92
+ // Always request a device update on startup, then start the interval for polling
93
+ setTimeout(() => this.requestUpdate(true), 2000)
94
+ this.accessory.refreshInterval = setInterval(
95
+ () => this.requestUpdate(),
96
+ this.pollInterval * 1000,
97
+ )
98
+
99
+ // Output the customised options to the log
100
+ const opts = JSON.stringify({
101
+ connection: this.accessory.context.connection,
102
+ reversePolarity: this.reversePolarity,
103
+ showAs: 'switch',
104
+ })
105
+ platform.log('[%s] %s %s.', this.name, platformLang.devInitOpts, opts)
106
+ }
107
+
108
+ async internalStateUpdate(value, newPosition, hapServ) {
109
+ try {
110
+ // Only control when turning on a switch
111
+ if (!value) {
112
+ return
113
+ }
114
+
115
+ // Add the request to the queue so updates are sent apart
116
+ await this.queue.add(async () => {
117
+ // This flag stops the plugin from requesting updates while pending on others
118
+ this.updateInProgress = true
119
+
120
+ // Generate the payload and namespace for the correct device model
121
+ const namespace = 'Appliance.RollerShutter.Position'
122
+ const payload = {
123
+ position: {
124
+ position: newPosition,
125
+ channel: 0,
126
+ },
127
+ }
128
+
129
+ // Use the platform function to send the update to the device
130
+ await this.platform.sendUpdate(this.accessory, {
131
+ namespace,
132
+ payload,
133
+ })
134
+
135
+ // Update the cache and log the update has been successful
136
+ this.cacheState = value
137
+ this.accessory.log(`${platformLang.curState} [${this.states[Math.abs(newPosition)]}]`)
138
+
139
+ // Turn the switch off again after two seconds
140
+ setTimeout(() => {
141
+ hapServ.updateCharacteristic(this.hapChar.On, false)
142
+ }, 2000)
143
+ })
144
+ } catch (err) {
145
+ // Catch any errors whilst updating the device
146
+ const eText = err instanceof TimeoutError ? platformLang.timeout : parseError(err)
147
+ this.accessory.logWarn(`${platformLang.sendFailed} ${eText}`)
148
+ setTimeout(() => {
149
+ hapServ.updateCharacteristic(this.hapChar.On, false)
150
+ }, 2000)
151
+ throw new this.hapErr(-70402)
152
+ }
153
+ }
154
+
155
+ async requestUpdate(firstRun = false) {
156
+ try {
157
+ // Don't continue if an update is currently being sent to the device
158
+ if (this.updateInProgress) {
159
+ return
160
+ }
161
+
162
+ // Add the request to the queue so updates are sent apart
163
+ await this.queue.add(async () => {
164
+ // This flag stops the plugin from requesting updates while pending on others
165
+ this.updateInProgress = true
166
+
167
+ // Send the request
168
+ const res = await this.platform.sendUpdate(this.accessory, {
169
+ namespace: 'Appliance.System.All',
170
+ payload: {},
171
+ })
172
+
173
+ // Log the received data
174
+ this.accessory.logDebug(`${platformLang.incPoll}: ${JSON.stringify(res.data)}`)
175
+
176
+ // Check the response is in a useful format
177
+ const data = res.data.payload
178
+ if (data.all) {
179
+ // A flag to check if we need to update the accessory context
180
+ let needsUpdate = false
181
+
182
+ // Get the mac address and hardware version of the device
183
+ if (data.all.system) {
184
+ // Mac address and hardware don't change regularly so only get on first poll
185
+ if (firstRun && data.all.system.hardware) {
186
+ this.accessory.context.macAddress = data.all.system.hardware.macAddress.toUpperCase()
187
+ this.accessory.context.hardware = data.all.system.hardware.version
188
+ }
189
+
190
+ // Get the ip address and firmware of the device
191
+ if (data.all.system.firmware) {
192
+ // Check for an IP change each and every time the device is polled
193
+ if (this.accessory.context.ipAddress !== data.all.system.firmware.innerIp) {
194
+ this.accessory.context.ipAddress = data.all.system.firmware.innerIp
195
+ needsUpdate = true
196
+ }
197
+
198
+ // Firmware doesn't change regularly so only get on first poll
199
+ if (firstRun) {
200
+ this.accessory.context.firmware = data.all.system.firmware.version
201
+ }
202
+ }
203
+ }
204
+
205
+ // Get the cloud online status of the device
206
+ if (data.all.system.online) {
207
+ const isOnline = data.all.system.online.status === 1
208
+ if (this.accessory.context.isOnline !== isOnline) {
209
+ this.accessory.context.isOnline = isOnline
210
+ needsUpdate = true
211
+ }
212
+ }
213
+
214
+ // Update the accessory cache if anything has changed
215
+ if (needsUpdate || firstRun) {
216
+ this.platform.updateAccessory(this.accessory)
217
+ }
218
+ }
219
+ })
220
+ } catch (err) {
221
+ const eText = err instanceof TimeoutError ? platformLang.timeout : parseError(err)
222
+ this.accessory.logDebugWarn(`${platformLang.reqFailed}: ${eText}`)
223
+
224
+ // Set the homebridge-ui status of the device to offline if local and error is timeout
225
+ if (
226
+ (this.accessory.context.isOnline || firstRun)
227
+ && ['EHOSTUNREACH', 'timed out'].some(el => eText.includes(el))
228
+ ) {
229
+ this.accessory.context.isOnline = false
230
+ this.platform.updateAccessory(this.accessory)
231
+ }
232
+ }
233
+ }
234
+ }