@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,377 @@
1
+ import PQueue from 'p-queue'
2
+ import { TimeoutError } from 'p-timeout'
3
+
4
+ import platformConsts from '../utils/constants.js'
5
+ import { hasProperty, parseError } from '../utils/functions.js'
6
+ import platformLang from '../utils/lang-en.js'
7
+
8
+ export default class {
9
+ constructor(platform, accessory, priAcc) {
10
+ // Set up variables from the platform
11
+ this.cusChar = platform.cusChar
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.lowBattThreshold = accessory.context.options.lowBattThreshold
20
+ ? Math.min(accessory.context.options.lowBattThreshold, 100)
21
+ : platformConsts.defaultValues.lowBattThreshold
22
+ this.name = accessory.displayName
23
+ this.priAcc = priAcc
24
+
25
+ this.mode2Label = {
26
+ 0: 'manual',
27
+ 1: 'heat',
28
+ 2: 'cool',
29
+ 3: 'auto',
30
+ 4: 'economy',
31
+ }
32
+ this.mode2Char = {
33
+ 0: false,
34
+ 1: this.cusChar.ValveHeatMode,
35
+ 2: this.cusChar.ValveCoolMode,
36
+ 3: this.cusChar.ValveAutoMode,
37
+ 4: this.cusChar.ValveEconomyMode,
38
+ }
39
+
40
+ // Add the thermostat service if it doesn't already exist
41
+ this.service = this.accessory.getService(this.hapServ.Thermostat)
42
+ || this.accessory.addService(this.hapServ.Thermostat)
43
+
44
+ this.service
45
+ .getCharacteristic(this.hapChar.TargetHeatingCoolingState)
46
+ .setProps({
47
+ minValue: 0,
48
+ maxValue: 1,
49
+ validValues: [0, 1],
50
+ })
51
+ .onSet(async value => this.internalStateUpdate(value))
52
+ this.cacheState = this.service.getCharacteristic(this.hapChar.TargetHeatingCoolingState).value
53
+
54
+ this.service
55
+ .getCharacteristic(this.hapChar.TargetTemperature)
56
+ .setProps({
57
+ minValue: 5,
58
+ maxValue: 35,
59
+ minStep: 0.5,
60
+ })
61
+ .onSet(async value => this.internalTargetUpdate(value))
62
+ this.cacheTarg = this.service.getCharacteristic(this.hapChar.TargetTemperature).value
63
+
64
+ this.cacheTemp = this.service.getCharacteristic(this.hapChar.CurrentTemperature).value
65
+ this.updateCache()
66
+
67
+ if (!this.service.testCharacteristic(this.cusChar.ValveHeatMode)) {
68
+ this.service.addCharacteristic(this.cusChar.ValveHeatMode)
69
+ }
70
+ this.service
71
+ .getCharacteristic(this.cusChar.ValveHeatMode)
72
+ .onSet(async value => this.internalModeUpdate(value, 1))
73
+ if (!this.service.testCharacteristic(this.cusChar.ValveCoolMode)) {
74
+ this.service.addCharacteristic(this.cusChar.ValveCoolMode)
75
+ }
76
+ this.service
77
+ .getCharacteristic(this.cusChar.ValveCoolMode)
78
+ .onSet(async value => this.internalModeUpdate(value, 2))
79
+ if (!this.service.testCharacteristic(this.cusChar.ValveAutoMode)) {
80
+ this.service.addCharacteristic(this.cusChar.ValveAutoMode)
81
+ }
82
+ this.service
83
+ .getCharacteristic(this.cusChar.ValveAutoMode)
84
+ .onSet(async value => this.internalModeUpdate(value, 3))
85
+ if (!this.service.testCharacteristic(this.cusChar.ValveEconomyMode)) {
86
+ this.service.addCharacteristic(this.cusChar.ValveEconomyMode)
87
+ }
88
+ this.cacheMode = 0
89
+ this.service
90
+ .getCharacteristic(this.cusChar.ValveEconomyMode)
91
+ .onSet(async value => this.internalModeUpdate(value, 4))
92
+ if (!this.service.testCharacteristic(this.cusChar.ValveWindowOpen)) {
93
+ this.service.addCharacteristic(this.cusChar.ValveWindowOpen)
94
+ }
95
+ this.cacheWindow = this.service.getCharacteristic(this.cusChar.ValveWindowOpen).value
96
+
97
+ // Pass the accessory to Fakegato to set up with Eve
98
+ this.accessory.eveService = new platform.eveService('custom', this.accessory, { log: () => {} })
99
+
100
+ // Create the queue used for sending device requests
101
+ this.updateInProgress = false
102
+ this.queue = new PQueue({
103
+ concurrency: 1,
104
+ interval: 250,
105
+ intervalCap: 1,
106
+ timeout: 10000,
107
+ throwOnTimeout: true,
108
+ })
109
+ this.queue.on('idle', () => {
110
+ this.updateInProgress = false
111
+ })
112
+
113
+ // Output the customised options to the log
114
+ const opts = JSON.stringify({
115
+ connection: this.accessory.context.connection,
116
+ lowBattThreshold: this.lowBattThreshold,
117
+ })
118
+ platform.log('[%s] %s %s.', this.name, platformLang.devInitOpts, opts)
119
+ }
120
+
121
+ async internalStateUpdate(value) {
122
+ try {
123
+ // Add the request to the queue so updates are sent apart
124
+ await this.queue.add(async () => {
125
+ // Don't continue if the state is the same as before
126
+ if (value === this.cacheState) {
127
+ return
128
+ }
129
+
130
+ // This flag stops the plugin from requesting updates while pending on others
131
+ this.updateInProgress = true
132
+
133
+ // Generate the payload and namespace
134
+ const namespace = 'Appliance.Hub.ToggleX'
135
+ const payload = {
136
+ togglex: [
137
+ {
138
+ id: this.accessory.context.subSerialNumber,
139
+ onoff: value ? 1 : 0,
140
+ },
141
+ ],
142
+ }
143
+
144
+ // Use the platform function to send the update to the device
145
+ await this.platform.sendUpdate(this.priAcc, {
146
+ namespace,
147
+ payload,
148
+ })
149
+
150
+ // Update the cache and log the update has been successful
151
+ this.cacheState = value
152
+
153
+ this.accessory.log(`${platformLang.curState} [${value ? 'on' : 'off'}]`)
154
+ })
155
+ } catch (err) {
156
+ // Catch any errors whilst updating the device
157
+ const eText = err instanceof TimeoutError ? platformLang.timeout : parseError(err)
158
+ this.accessory.logWarn(`${platformLang.sendFailed} ${eText}`)
159
+ setTimeout(() => {
160
+ this.service.updateCharacteristic(this.hapChar.TargetHeatingCoolingState, this.cacheState)
161
+ }, 2000)
162
+ throw new this.hapErr(-70402)
163
+ }
164
+ }
165
+
166
+ async internalModeUpdate(value, newMode) {
167
+ try {
168
+ // If turning off then set to manual mode
169
+ if (!value) {
170
+ newMode = 0
171
+ }
172
+
173
+ // Add the request to the queue so updates are sent apart
174
+ await this.queue.add(async () => {
175
+ // Don't continue if the state is the same as before
176
+ if (newMode === this.cacheMode) {
177
+ return
178
+ }
179
+
180
+ // This flag stops the plugin from requesting updates while pending on others
181
+ this.updateInProgress = true
182
+
183
+ // Generate the payload and namespace
184
+ const namespace = 'Appliance.Hub.Mts100.Mode'
185
+ const payload = {
186
+ mode: [
187
+ {
188
+ id: this.accessory.context.subSerialNumber,
189
+ state: newMode,
190
+ },
191
+ ],
192
+ }
193
+
194
+ // Use the platform function to send the update to the device
195
+ await this.platform.sendUpdate(this.priAcc, {
196
+ namespace,
197
+ payload,
198
+ })
199
+
200
+ // Update the cache and log the update has been successful
201
+ this.cacheState = value
202
+ this.accessory.log(`${platformLang.curMode} [${this.mode2Label[newMode]}]`)
203
+
204
+ // Turn the other modes off
205
+ Object.entries(this.mode2Char).forEach((entry) => {
206
+ const [mode, char] = entry
207
+ if (char && mode !== newMode.toString()) {
208
+ this.service.updateCharacteristic(char, false)
209
+ }
210
+ })
211
+ })
212
+ } catch (err) {
213
+ // Catch any errors whilst updating the device
214
+ const eText = err instanceof TimeoutError ? platformLang.timeout : parseError(err)
215
+ this.accessory.logWarn(`${platformLang.sendFailed} ${eText}`)
216
+ setTimeout(() => {
217
+ this.service.updateCharacteristic(this.mode2Char[newMode], false)
218
+ }, 2000)
219
+ throw new this.hapErr(-70402)
220
+ }
221
+ }
222
+
223
+ async internalTargetUpdate(value) {
224
+ try {
225
+ // Add the request to the queue so updates are sent apart
226
+ await this.queue.add(async () => {
227
+ // Don't continue if the state is the same as before
228
+ if (value === this.cacheTarg) {
229
+ return
230
+ }
231
+
232
+ // This flag stops the plugin from requesting updates while pending on others
233
+ this.updateInProgress = true
234
+
235
+ // Generate the payload and namespace
236
+ const namespace = 'Appliance.Hub.Mts100.Temperature'
237
+ const payload = {
238
+ temperature: [
239
+ {
240
+ custom: value * 10,
241
+ id: this.accessory.context.subSerialNumber,
242
+ },
243
+ ],
244
+ }
245
+
246
+ // Use the platform function to send the update to the device
247
+ await this.platform.sendUpdate(this.priAcc, {
248
+ namespace,
249
+ payload,
250
+ })
251
+
252
+ // Update the cache and log the update has been successful
253
+ this.cacheTarg = value
254
+ this.accessory.log(`${platformLang.curTarg} [${value}°C]`)
255
+
256
+ // Update the current heating state
257
+ this.service.updateCharacteristic(
258
+ this.hapChar.CurrentHeatingCoolingState,
259
+ value > this.cacheTemp ? 1 : 0,
260
+ )
261
+
262
+ // Turn the modes off as back to manual mode
263
+ Object.values(this.mode2Char).forEach((char) => {
264
+ if (char) {
265
+ this.service.updateCharacteristic(char, false)
266
+ }
267
+ })
268
+ })
269
+ } catch (err) {
270
+ // Catch any errors whilst updating the device
271
+ const eText = err instanceof TimeoutError ? platformLang.timeout : parseError(err)
272
+ this.accessory.logWarn(`${platformLang.sendFailed} ${eText}`)
273
+ setTimeout(() => {
274
+ this.service.updateCharacteristic(this.hapChar.TargetTemperature, this.cacheTarg)
275
+ }, 2000)
276
+ throw new this.hapErr(-70402)
277
+ }
278
+ }
279
+
280
+ applyUpdate(data) {
281
+ try {
282
+ let needsUpdate = false
283
+ if (hasProperty(data, 'state')) {
284
+ const newState = data.state
285
+
286
+ // Check against the cache and update HomeKit and the cache if needed
287
+ if (this.cacheState !== newState) {
288
+ this.service.updateCharacteristic(this.hapChar.TargetHeatingCoolingState, newState)
289
+ this.cacheState = newState
290
+ this.accessory.log(`${platformLang.curState} [${newState === 1 ? 'on' : 'off'}]`)
291
+ needsUpdate = true
292
+ }
293
+ }
294
+ if (hasProperty(data, 'targTemperature')) {
295
+ const newTarg = data.targTemperature
296
+
297
+ // Check against the cache and update HomeKit and the cache if needed
298
+ if (this.cacheTarg !== newTarg) {
299
+ this.service.updateCharacteristic(this.hapChar.TargetTemperature, newTarg)
300
+ this.cacheTarg = newTarg
301
+ this.accessory.log(`${platformLang.curTarg} [${newTarg}°C]`)
302
+ needsUpdate = true
303
+ }
304
+ }
305
+ if (hasProperty(data, 'currTemperature')) {
306
+ const newTemp = data.currTemperature
307
+
308
+ // Check against the cache and update HomeKit and the cache if needed
309
+ if (this.cacheTemp !== newTemp) {
310
+ this.service.updateCharacteristic(this.hapChar.CurrentTemperature, newTemp)
311
+ this.cacheTemp = newTemp
312
+ this.accessory.eveService.addEntry({ temp: newTemp })
313
+ this.accessory.log(`${platformLang.curTemp} [${newTemp}°C]`)
314
+ needsUpdate = true
315
+
316
+ // Update the cache file with the new temperature
317
+ this.updateCache()
318
+ }
319
+ }
320
+
321
+ // Update the current heating state
322
+ if (needsUpdate) {
323
+ this.service.updateCharacteristic(
324
+ this.hapChar.CurrentHeatingCoolingState,
325
+ this.cacheState === 1 && this.cacheTarg > this.cacheTemp ? 1 : 0,
326
+ )
327
+ }
328
+
329
+ // Todo - data.openWindow and data.mode
330
+ if (hasProperty(data, 'openWindow')) {
331
+ const newWindow = data.openWindow === 1
332
+
333
+ // Check against the cache and update HomeKit and the cache if needed
334
+ if (this.cacheWindow !== newWindow) {
335
+ this.service.updateCharacteristic(this.cusChar.ValveWindowOpen, newWindow)
336
+ this.cacheWindow = newWindow
337
+ this.accessory.log(`${platformLang.curWindow} [${newWindow ? 'open' : 'closed'}]`)
338
+ }
339
+ }
340
+
341
+ if (hasProperty(data, 'mode')) {
342
+ const newMode = data.mode
343
+
344
+ // Check against the cache and update HomeKit and the cache if needed
345
+ if (this.cacheMode !== newMode) {
346
+ Object.entries(this.mode2Char).forEach((entry) => {
347
+ const [mode, char] = entry
348
+ if (char) {
349
+ this.service.updateCharacteristic(char, mode === newMode.toString())
350
+ }
351
+ })
352
+ this.cacheMode = newMode
353
+ this.accessory.log(`${platformLang.curMode} [${this.mode2Label[newMode]}]`)
354
+ }
355
+ }
356
+ } catch (err) {
357
+ this.accessory.logWarn(`${platformLang.refFailed} ${parseError(err)}`)
358
+ }
359
+ }
360
+
361
+ async updateCache() {
362
+ // Don't continue if the storage client hasn't initialised properly
363
+ if (!this.platform.storageClientData) {
364
+ return
365
+ }
366
+
367
+ // Attempt to save the new temperature to the cache
368
+ try {
369
+ await this.platform.storageData.setItem(
370
+ `${this.accessory.context.subSerialNumber}_temp`,
371
+ this.cacheTemp,
372
+ )
373
+ } catch (err) {
374
+ this.accessory.logWarn(`${platformLang.storageWriteErr} ${parseError(err)}`)
375
+ }
376
+ }
377
+ }