@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,312 @@
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 {
7
+ generateRandomString,
8
+ hasProperty,
9
+ parseError,
10
+ sleep,
11
+ } from '../utils/functions.js'
12
+ import platformLang from '../utils/lang-en.js'
13
+
14
+ export default class {
15
+ constructor(platform, accessory) {
16
+ // Set up variables from the platform
17
+ this.hapChar = platform.api.hap.Characteristic
18
+ this.hapErr = platform.api.hap.HapStatusError
19
+ this.hapServ = platform.api.hap.Service
20
+ this.platform = platform
21
+
22
+ // Set up variables from the accessory
23
+ this.accessory = accessory
24
+ this.brightnessStep = this.accessory.context.options.brightnessStep || platformConsts.defaultValues.brightnessStep
25
+ this.brightnessStep = Math.min(this.brightnessStep, 100)
26
+ this.brightnessCapacity = ['MSL100D', 'MSL100R', 'MSS560M'].includes(accessory.context.model)
27
+ ? 4
28
+ : undefined
29
+ this.name = accessory.displayName
30
+ const cloudRefreshRate = hasProperty(platform.config, 'cloudRefreshRate')
31
+ ? platform.config.cloudRefreshRate
32
+ : platformConsts.defaultValues.cloudRefreshRate
33
+ const localRefreshRate = hasProperty(platform.config, 'refreshRate')
34
+ ? platform.config.refreshRate
35
+ : platformConsts.defaultValues.refreshRate
36
+ this.pollInterval = accessory.context.connection === 'local'
37
+ ? localRefreshRate
38
+ : cloudRefreshRate
39
+
40
+ // Add the lightbulb service if it doesn't already exist
41
+ this.service = this.accessory.getService(this.hapServ.Lightbulb)
42
+ || this.accessory.addService(this.hapServ.Lightbulb)
43
+
44
+ // Add the set handler to the lightbulb 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
+ // Add the set handler to the lightbulb brightness characteristic
51
+ this.service
52
+ .getCharacteristic(this.hapChar.Brightness)
53
+ .setProps({ minStep: this.brightnessStep })
54
+ .onSet(async value => this.internalBrightnessUpdate(value))
55
+ this.cacheBright = this.service.getCharacteristic(this.hapChar.Brightness).value
56
+
57
+ // Create the queue used for sending device requests
58
+ this.updateInProgress = false
59
+ this.queue = new PQueue({
60
+ concurrency: 1,
61
+ interval: 250,
62
+ intervalCap: 1,
63
+ timeout: 10000,
64
+ throwOnTimeout: true,
65
+ })
66
+ this.queue.on('idle', () => {
67
+ this.updateInProgress = false
68
+ })
69
+
70
+ // Set up the mqtt client for cloud devices to send and receive device updates
71
+ if (accessory.context.connection !== 'local') {
72
+ this.accessory.mqtt = new mqttClient(platform, this.accessory)
73
+ this.accessory.mqtt.connect()
74
+ }
75
+
76
+ // Always request a device update on startup, then start the interval for polling
77
+ setTimeout(() => this.requestUpdate(true), 2000)
78
+ this.accessory.refreshInterval = setInterval(
79
+ () => this.requestUpdate(),
80
+ this.pollInterval * 1000,
81
+ )
82
+
83
+ // Output the customised options to the log
84
+ const opts = JSON.stringify({
85
+ brightnessStep: this.brightnessStep,
86
+ connection: this.accessory.context.connection,
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
+ // Generate the payload and namespace
104
+ const namespace = 'Appliance.Control.ToggleX'
105
+ const payload = {
106
+ togglex: {
107
+ onoff: value ? 1 : 0,
108
+ channel: 0,
109
+ },
110
+ }
111
+
112
+ // Use the platform function to send the update to the device
113
+ await this.platform.sendUpdate(this.accessory, {
114
+ namespace,
115
+ payload,
116
+ })
117
+
118
+ // Update the cache and log the update has been successful
119
+ this.cacheState = value
120
+ this.accessory.log(`${platformLang.curState} [${value ? 'on' : 'off'}]`)
121
+ })
122
+ } catch (err) {
123
+ // Catch any errors whilst updating the device
124
+ const eText = err instanceof TimeoutError ? platformLang.timeout : parseError(err)
125
+ this.accessory.logWarn(`${platformLang.sendFailed} ${eText}`)
126
+ setTimeout(() => {
127
+ this.service.updateCharacteristic(this.hapChar.On, this.cacheState)
128
+ }, 2000)
129
+ throw new this.hapErr(-70402)
130
+ }
131
+ }
132
+
133
+ async internalBrightnessUpdate(value) {
134
+ try {
135
+ // Add the request to the queue so updates are sent apart
136
+ await this.queue.add(async () => {
137
+ // Don't continue if the state is the same as before
138
+ if (this.cacheBright === value) {
139
+ return
140
+ }
141
+
142
+ // Avoid multiple changes in short space of time
143
+ const updateKey = generateRandomString(5)
144
+ this.updateKeyBright = updateKey
145
+ await sleep(300)
146
+ if (updateKey !== this.updateKeyBright) {
147
+ return
148
+ }
149
+
150
+ // This flag stops the plugin from requesting updates while pending on others
151
+ this.updateInProgress = true
152
+
153
+ // Generate the payload to send for the correct device model
154
+ const payload = {
155
+ light: {
156
+ capacity: this.brightnessCapacity,
157
+ luminance: value,
158
+ channel: 0,
159
+ },
160
+ }
161
+
162
+ // Generate the namespace
163
+ const namespace = 'Appliance.Control.Light'
164
+
165
+ // Use the platform function to send the update to the device
166
+ await this.platform.sendUpdate(this.accessory, {
167
+ namespace,
168
+ payload,
169
+ })
170
+
171
+ // Update the cache and log the update has been successful
172
+ this.cacheBright = value
173
+ this.accessory.log(`${platformLang.curBright} [${value}%]`)
174
+ })
175
+ } catch (err) {
176
+ const eText = parseError(err)
177
+ this.accessory.logWarn(`${platformLang.sendFailed} ${eText}`)
178
+ setTimeout(() => {
179
+ this.service.updateCharacteristic(this.hapChar.Brightness, this.cacheBright)
180
+ }, 2000)
181
+ throw new this.hapErr(-70402)
182
+ }
183
+ }
184
+
185
+ async requestUpdate(firstRun = false) {
186
+ try {
187
+ // Don't continue if an update is currently being sent to the device
188
+ if (this.updateInProgress) {
189
+ return
190
+ }
191
+
192
+ // Add the request to the queue so updates are sent apart
193
+ await this.queue.add(async () => {
194
+ // This flag stops the plugin from requesting updates while pending on others
195
+ this.updateInProgress = true
196
+
197
+ // Send the request
198
+ const res = await this.platform.sendUpdate(this.accessory, {
199
+ namespace: 'Appliance.System.All',
200
+ payload: {},
201
+ })
202
+
203
+ // Log the received data
204
+ this.accessory.logDebug(`${platformLang.incPoll}: ${JSON.stringify(res.data)}`)
205
+
206
+ // Check the response is in a useful format
207
+ const data = res.data.payload
208
+ if (data.all) {
209
+ if (data.all.digest) {
210
+ this.applyUpdate(data.all.digest)
211
+ }
212
+
213
+ // A flag to check if we need to update the accessory context
214
+ let needsUpdate = false
215
+
216
+ // Get the mac address and hardware version of the device
217
+ if (data.all.system) {
218
+ // Mac address and hardware don't change regularly so only get on first poll
219
+ if (firstRun && data.all.system.hardware) {
220
+ this.accessory.context.macAddress = data.all.system.hardware.macAddress.toUpperCase()
221
+ this.accessory.context.hardware = data.all.system.hardware.version
222
+ }
223
+
224
+ // Get the ip address and firmware of the device
225
+ if (data.all.system.firmware) {
226
+ // Check for an IP change each and every time the device is polled
227
+ if (this.accessory.context.ipAddress !== data.all.system.firmware.innerIp) {
228
+ this.accessory.context.ipAddress = data.all.system.firmware.innerIp
229
+ needsUpdate = true
230
+ }
231
+
232
+ // Firmware doesn't change regularly so only get on first poll
233
+ if (firstRun) {
234
+ this.accessory.context.firmware = data.all.system.firmware.version
235
+ }
236
+ }
237
+ }
238
+
239
+ // Get the cloud online status of the device
240
+ if (data.all.system.online) {
241
+ const isOnline = data.all.system.online.status === 1
242
+ if (this.accessory.context.isOnline !== isOnline) {
243
+ this.accessory.context.isOnline = isOnline
244
+ needsUpdate = true
245
+ }
246
+ }
247
+
248
+ // Update the accessory cache if anything has changed
249
+ if (needsUpdate || firstRun) {
250
+ this.platform.updateAccessory(this.accessory)
251
+ }
252
+ }
253
+ })
254
+ } catch (err) {
255
+ const eText = err instanceof TimeoutError ? platformLang.timeout : parseError(err)
256
+ this.accessory.logDebugWarn(`${platformLang.reqFailed}: ${eText}`)
257
+
258
+ // Set the homebridge-ui status of the device to offline if local and error is timeout
259
+ if (
260
+ (this.accessory.context.isOnline || firstRun)
261
+ && ['EHOSTUNREACH', 'timed out'].some(el => eText.includes(el))
262
+ ) {
263
+ this.accessory.context.isOnline = false
264
+ this.platform.updateAccessory(this.accessory)
265
+ }
266
+ }
267
+ }
268
+
269
+ receiveUpdate(params) {
270
+ try {
271
+ // Log the received data
272
+ this.accessory.logDebug(`${platformLang.incMQTT}: ${JSON.stringify(params)}`)
273
+
274
+ // Validate the response, checking for payload property
275
+ if (!params.payload) {
276
+ throw new Error('invalid response received')
277
+ }
278
+ const data = params.payload
279
+ if (data.togglex || data.light) {
280
+ this.applyUpdate(data)
281
+ }
282
+ } catch (err) {
283
+ this.accessory.logWarn(`${platformLang.refFailed} ${parseError(err)}`)
284
+ }
285
+ }
286
+
287
+ applyUpdate(data) {
288
+ if (data.togglex && data.togglex[0] && hasProperty(data.togglex[0], 'onoff')) {
289
+ // newState is given as 0 or 1 -> convert to bool for HomeKit
290
+ const newState = data.togglex[0].onoff === 1
291
+
292
+ // Check against the cache and update HomeKit and the cache if needed
293
+ if (this.cacheState !== newState) {
294
+ this.service.updateCharacteristic(this.hapChar.On, newState)
295
+ this.cacheState = newState
296
+ this.accessory.log(`${platformLang.curState} [${this.cacheState ? 'on' : 'off'}]`)
297
+ }
298
+ }
299
+ if (data.light) {
300
+ if (hasProperty(data.light, 'luminance')) {
301
+ const newBright = data.light.luminance
302
+
303
+ // Check against the cache and update HomeKit and the cache if needed
304
+ if (this.cacheBright !== newBright) {
305
+ this.service.updateCharacteristic(this.hapChar.Brightness, newBright)
306
+ this.cacheBright = newBright
307
+ this.accessory.log(`${platformLang.curBright} [${this.cacheBright}%]`)
308
+ }
309
+ }
310
+ }
311
+ }
312
+ }