@homebridge-plugins/homebridge-meross 10.17.0-beta.2 → 10.17.0-beta.4

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.
package/CHANGELOG.md CHANGED
@@ -9,6 +9,8 @@ All notable changes to `@homebridge-plugins/homebridge-meross` will be documente
9
9
  - feat(ui): add, remove and hide devices from the config via the my devices tab
10
10
  - style(ui): standardise the custom ui layout and sync the support tab with the readme
11
11
  - feat(ui): add a remove all devices action to the my devices tab
12
+ - chore: declare the supports-hap transport keyword for the homebridge ui
13
+ - feat: add humidity, window-open, frost and overheat protection for the MTS215 and MTS215B thermostats (#798) (@marcaicart)
12
14
 
13
15
  ## v10.16.5 (2026-07-18)
14
16
 
@@ -28,6 +28,7 @@ import deviceSensorPresence from './sensor-presence.js'
28
28
  import deviceSwitchMulti from './switch-multi.js'
29
29
  import deviceSwitchSingle from './switch-single.js'
30
30
  import deviceTemplate from './template.js'
31
+ import deviceThermostatMts215 from './thermostat-mts215.js'
31
32
  import deviceThermostatMts300 from './thermostat-mts300.js'
32
33
  import deviceThermostat from './thermostat.js'
33
34
 
@@ -64,4 +65,5 @@ export default {
64
65
  deviceTemplate,
65
66
  deviceThermostat,
66
67
  deviceThermostatMts300,
68
+ deviceThermostatMts215,
67
69
  }
@@ -0,0 +1,594 @@
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.cusChar = platform.cusChar
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.name = accessory.displayName
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
+ this.mode2Label = {
32
+ 0: 'manual',
33
+ 1: 'heat',
34
+ 2: 'cool',
35
+ 3: 'auto',
36
+ 4: 'economy',
37
+ }
38
+ this.mode2Char = {
39
+ 0: false,
40
+ 1: this.cusChar.ValveHeatMode,
41
+ 2: this.cusChar.ValveCoolMode,
42
+ 3: this.cusChar.ValveAutoMode,
43
+ 4: this.cusChar.ValveEconomyMode,
44
+ }
45
+
46
+ // Add the thermostat service if it doesn't already exist
47
+ this.service = this.accessory.getService(this.hapServ.Thermostat)
48
+ || this.accessory.addService(this.hapServ.Thermostat)
49
+
50
+ this.service
51
+ .getCharacteristic(this.hapChar.TargetHeatingCoolingState)
52
+ .setProps({
53
+ minValue: 0,
54
+ maxValue: 1,
55
+ validValues: [0, 1],
56
+ })
57
+ .onSet(async value => this.internalStateUpdate(value))
58
+ this.cacheState = this.service.getCharacteristic(this.hapChar.TargetHeatingCoolingState).value
59
+
60
+ this.service
61
+ .getCharacteristic(this.hapChar.TargetTemperature)
62
+ .setProps({
63
+ minValue: 5,
64
+ maxValue: 35,
65
+ minStep: 0.5,
66
+ })
67
+ .onSet(async value => this.internalTargetUpdate(value))
68
+ this.cacheTarg = this.service.getCharacteristic(this.hapChar.TargetTemperature).value
69
+
70
+ this.cacheTemp = this.service.getCharacteristic(this.hapChar.CurrentTemperature).value
71
+ this.updateCache()
72
+
73
+ if (!this.service.testCharacteristic(this.cusChar.ValveHeatMode)) {
74
+ this.service.addCharacteristic(this.cusChar.ValveHeatMode)
75
+ }
76
+ this.service
77
+ .getCharacteristic(this.cusChar.ValveHeatMode)
78
+ .onSet(async value => this.internalModeUpdate(value, 1))
79
+ if (!this.service.testCharacteristic(this.cusChar.ValveCoolMode)) {
80
+ this.service.addCharacteristic(this.cusChar.ValveCoolMode)
81
+ }
82
+ this.service
83
+ .getCharacteristic(this.cusChar.ValveCoolMode)
84
+ .onSet(async value => this.internalModeUpdate(value, 2))
85
+ if (!this.service.testCharacteristic(this.cusChar.ValveAutoMode)) {
86
+ this.service.addCharacteristic(this.cusChar.ValveAutoMode)
87
+ }
88
+ this.service
89
+ .getCharacteristic(this.cusChar.ValveAutoMode)
90
+ .onSet(async value => this.internalModeUpdate(value, 3))
91
+ if (!this.service.testCharacteristic(this.cusChar.ValveEconomyMode)) {
92
+ this.service.addCharacteristic(this.cusChar.ValveEconomyMode)
93
+ }
94
+ this.cacheMode = 0
95
+ this.service
96
+ .getCharacteristic(this.cusChar.ValveEconomyMode)
97
+ .onSet(async value => this.internalModeUpdate(value, 4))
98
+
99
+ // Add the humidity service if it doesn't already exist (separate namespace,
100
+ // Appliance.Control.Sensor.Latest -- not bundled into Appliance.System.All)
101
+ this.humidityService = this.accessory.getService(this.hapServ.HumiditySensor)
102
+ || this.accessory.addService(this.hapServ.HumiditySensor)
103
+ this.cacheHumi = this.humidityService.getCharacteristic(this.hapChar.CurrentRelativeHumidity).value
104
+
105
+ // Window-open detection, frost protection and overheat protection are only exposed
106
+ // as ContactSensors if they're actually armed on the device -- checked once against
107
+ // the real device state on the first poll after startup (see requestUpdate/firstRun
108
+ // below), rather than always creating the service and trying to signal "disabled"
109
+ // through it. If armed/disarmed at the device between Homebridge restarts, this is
110
+ // picked up the next time Homebridge restarts.
111
+ this.windowService = this.accessory.getServiceById(this.hapServ.ContactSensor, 'window-open') || null
112
+ this.frostService = this.accessory.getServiceById(this.hapServ.ContactSensor, 'frost-warning') || null
113
+ this.overheatService = this.accessory.getServiceById(this.hapServ.ContactSensor, 'overheat-warning') || null
114
+
115
+ // Pass the accessory to Fakegato to set up with Eve
116
+ this.accessory.eveService = new platform.eveService('custom', this.accessory, { log: () => {} })
117
+
118
+ // Create the queue used for sending device requests
119
+ this.updateInProgress = false
120
+ this.queue = new PQueue({
121
+ concurrency: 1,
122
+ interval: 250,
123
+ intervalCap: 1,
124
+ timeout: 10000,
125
+ throwOnTimeout: true,
126
+ })
127
+ this.queue.on('idle', () => {
128
+ this.updateInProgress = false
129
+ })
130
+
131
+ // Set up the mqtt client for cloud devices to send and receive device updates
132
+ if (accessory.context.connection !== 'local') {
133
+ this.accessory.mqtt = new mqttClient(platform, this.accessory)
134
+ this.accessory.mqtt.connect()
135
+ }
136
+
137
+ // Always request a device update on startup, then start the interval for polling
138
+ setTimeout(() => this.requestUpdate(true), 2000)
139
+ this.accessory.refreshInterval = setInterval(
140
+ () => this.requestUpdate(),
141
+ this.pollInterval * 1000,
142
+ )
143
+
144
+ // Output the customised options to the log
145
+ const opts = JSON.stringify({
146
+ connection: this.accessory.context.connection,
147
+ })
148
+ platform.log('[%s] %s %s.', this.name, platformLang.devInitOpts, opts)
149
+ }
150
+
151
+ async internalStateUpdate(value) {
152
+ try {
153
+ // Add the request to the queue so updates are sent apart
154
+ await this.queue.add(async () => {
155
+ // Don't continue if the state is the same as before
156
+ if (value === this.cacheState) {
157
+ return
158
+ }
159
+
160
+ // This flag stops the plugin from requesting updates while pending on others
161
+ this.updateInProgress = true
162
+
163
+ // Generate the payload and namespace
164
+ const namespace = 'Appliance.Control.Thermostat.Mode'
165
+ const payload = {
166
+ mode: [
167
+ {
168
+ channel: 0,
169
+ onoff: value ? 1 : 0,
170
+ },
171
+ ],
172
+ }
173
+
174
+ // Use the platform function to send the update to the device
175
+ await this.platform.sendUpdate(this.accessory, {
176
+ namespace,
177
+ payload,
178
+ })
179
+
180
+ // Update the cache and log the update has been successful
181
+ this.cacheState = value
182
+ this.accessory.log(`${platformLang.curState} [${value ? 'on' : 'off'}]`)
183
+ })
184
+ } catch (err) {
185
+ // Catch any errors whilst updating the device
186
+ const eText = err instanceof TimeoutError ? platformLang.timeout : parseError(err)
187
+ this.accessory.logWarn(`${platformLang.sendFailed} ${eText}`)
188
+ setTimeout(() => {
189
+ this.service.updateCharacteristic(this.hapChar.TargetHeatingCoolingState, this.cacheState)
190
+ }, 2000)
191
+ throw new this.hapErr(-70402)
192
+ }
193
+ }
194
+
195
+ async internalModeUpdate(value, newMode) {
196
+ try {
197
+ // If turning off then set to manual mode
198
+ if (!value) {
199
+ newMode = 0
200
+ }
201
+
202
+ // Add the request to the queue so updates are sent apart
203
+ await this.queue.add(async () => {
204
+ // Don't continue if the state is the same as before
205
+ if (newMode === this.cacheMode) {
206
+ return
207
+ }
208
+
209
+ // This flag stops the plugin from requesting updates while pending on others
210
+ this.updateInProgress = true
211
+
212
+ // Generate the payload and namespace
213
+ const namespace = 'Appliance.Control.Thermostat.Mode'
214
+ const payload = {
215
+ mode: [
216
+ {
217
+ state: newMode,
218
+ },
219
+ ],
220
+ }
221
+
222
+ // Use the platform function to send the update to the device
223
+ await this.platform.sendUpdate(this.accessory, {
224
+ namespace,
225
+ payload,
226
+ })
227
+
228
+ // Update the cache and log the update has been successful
229
+ this.cacheState = value
230
+ this.accessory.log(`${platformLang.curMode} [${this.mode2Label[newMode]}]`)
231
+
232
+ // Turn the other modes off
233
+ Object.entries(this.mode2Char).forEach((entry) => {
234
+ const [mode, char] = entry
235
+ if (char && mode !== newMode.toString()) {
236
+ this.service.updateCharacteristic(char, false)
237
+ }
238
+ })
239
+ })
240
+ } catch (err) {
241
+ // Catch any errors whilst updating the device
242
+ const eText = err instanceof TimeoutError ? platformLang.timeout : parseError(err)
243
+ this.accessory.logWarn(`${platformLang.sendFailed} ${eText}`)
244
+ setTimeout(() => {
245
+ this.service.updateCharacteristic(this.mode2Char[newMode], false)
246
+ }, 2000)
247
+ throw new this.hapErr(-70402)
248
+ }
249
+ }
250
+
251
+ async internalTargetUpdate(value) {
252
+ try {
253
+ // Add the request to the queue so updates are sent apart
254
+ await this.queue.add(async () => {
255
+ // Don't continue if the state is the same as before
256
+ if (value === this.cacheTarg) {
257
+ return
258
+ }
259
+
260
+ // This flag stops the plugin from requesting updates while pending on others
261
+ this.updateInProgress = true
262
+
263
+ // Generate the payload and namespace
264
+ const namespace = 'Appliance.Control.Thermostat.Mode'
265
+ const payload = {
266
+ mode: [
267
+ {
268
+ channel: 0,
269
+ mode: 4,
270
+ manualTemp: value * 10,
271
+ },
272
+ ],
273
+ }
274
+
275
+ // Use the platform function to send the update to the device
276
+ await this.platform.sendUpdate(this.accessory, {
277
+ namespace,
278
+ payload,
279
+ })
280
+
281
+ // Update the cache and log the update has been successful
282
+ this.cacheTarg = value
283
+ this.accessory.log(`${platformLang.curTarg} [${value}°C]`)
284
+
285
+ // Update the current heating state
286
+ this.service.updateCharacteristic(
287
+ this.hapChar.CurrentHeatingCoolingState,
288
+ value > this.cacheTemp ? 1 : 0,
289
+ )
290
+
291
+ // Turn the modes off as back to manual mode
292
+ Object.values(this.mode2Char).forEach((char) => {
293
+ if (char) {
294
+ this.service.updateCharacteristic(char, false)
295
+ }
296
+ })
297
+ })
298
+ } catch (err) {
299
+ // Catch any errors whilst updating the device
300
+ const eText = err instanceof TimeoutError ? platformLang.timeout : parseError(err)
301
+ this.accessory.logWarn(`${platformLang.sendFailed} ${eText}`)
302
+ setTimeout(() => {
303
+ this.service.updateCharacteristic(this.hapChar.TargetTemperature, this.cacheTarg)
304
+ }, 2000)
305
+ throw new this.hapErr(-70402)
306
+ }
307
+ }
308
+
309
+ async updateCache() {
310
+ // Don't continue if the storage client hasn't initialised properly
311
+ if (!this.platform.storageClientData) {
312
+ return
313
+ }
314
+
315
+ // Attempt to save the new temperature to the cache
316
+ try {
317
+ await this.platform.storageData.setItem(
318
+ `${this.accessory.context.serialNumber}_temp`,
319
+ this.cacheTemp,
320
+ )
321
+ } catch (err) {
322
+ this.accessory.logWarn(`${platformLang.storageWriteErr} ${parseError(err)}`)
323
+ }
324
+ }
325
+
326
+ // Creates or removes a ContactSensor based on whether the corresponding protection
327
+ // is armed on the device. Only called on the first poll after startup: these are
328
+ // manually-configured device settings that don't change often, and re-evaluating on
329
+ // every poll would mean live-adding/removing HomeKit services as they're toggled at
330
+ // the device, which HomeKit clients don't always handle gracefully. If armed/disarmed
331
+ // between Homebridge restarts, this is picked up the next time Homebridge restarts.
332
+ ensureContactSensor(current, subtype, displayName, armed) {
333
+ if (armed) {
334
+ if (current) {
335
+ return current
336
+ }
337
+ const service = this.accessory.getServiceById(this.hapServ.ContactSensor, subtype)
338
+ || this.accessory.addService(this.hapServ.ContactSensor, displayName, subtype)
339
+ service.updateCharacteristic(this.hapChar.Name, displayName)
340
+ return service
341
+ }
342
+ if (current) {
343
+ this.accessory.removeService(current)
344
+ }
345
+ return null
346
+ }
347
+
348
+ async requestUpdate(firstRun = false) {
349
+ try {
350
+ // Don't continue if an update is currently being sent to the device
351
+ if (this.updateInProgress) {
352
+ return
353
+ }
354
+
355
+ // Add the request to the queue so updates are sent apart
356
+ await this.queue.add(async () => {
357
+ // This flag stops the plugin from requesting updates while pending on others
358
+ this.updateInProgress = true
359
+
360
+ // Send the main status request
361
+ const res = await this.platform.sendUpdate(this.accessory, {
362
+ namespace: 'Appliance.System.All',
363
+ payload: {},
364
+ })
365
+
366
+ // Log the received data
367
+ this.accessory.logDebug(`${platformLang.incPoll}: ${JSON.stringify(res.data)}`)
368
+
369
+ // Check the response is in a useful format
370
+ const data = res.data.payload
371
+ if (data.all) {
372
+ if (data.all.digest?.thermostat) {
373
+ // Only re-check whether window-open detection is armed on the first poll
374
+ // after startup -- see ensureContactSensor for why
375
+ if (firstRun) {
376
+ const windowArmed = data.all.digest.thermostat.windowOpened?.[0]?.detect === 1
377
+ this.windowService = this.ensureContactSensor(this.windowService, 'window-open', 'Window Open', windowArmed)
378
+ }
379
+ this.applyUpdate(data.all.digest.thermostat)
380
+ }
381
+
382
+ // A flag to check if we need to update the accessory context
383
+ let needsUpdate = false
384
+
385
+ // Get the mac address and hardware version of the device
386
+ if (data.all.system) {
387
+ // Mac address and hardware don't change regularly so only get on first poll
388
+ if (firstRun && data.all.system.hardware) {
389
+ this.accessory.context.macAddress = data.all.system.hardware.macAddress.toUpperCase()
390
+ this.accessory.context.hardware = data.all.system.hardware.version
391
+ }
392
+
393
+ // Get the ip address and firmware of the device
394
+ if (data.all.system.firmware) {
395
+ // Check for an IP change each and every time the device is polled
396
+ if (this.accessory.context.ipAddress !== data.all.system.firmware.innerIp) {
397
+ this.accessory.context.ipAddress = data.all.system.firmware.innerIp
398
+ needsUpdate = true
399
+ }
400
+
401
+ // Firmware doesn't change regularly so only get on first poll
402
+ if (firstRun) {
403
+ this.accessory.context.firmware = data.all.system.firmware.version
404
+ }
405
+ }
406
+ }
407
+
408
+ // Get the cloud online status of the device
409
+ if (data.all.system.online) {
410
+ const isOnline = data.all.system.online.status === 1
411
+ if (this.accessory.context.isOnline !== isOnline) {
412
+ this.accessory.context.isOnline = isOnline
413
+ needsUpdate = true
414
+ }
415
+ }
416
+
417
+ // Update the accessory cache if anything has changed
418
+ if (needsUpdate || firstRun) {
419
+ this.platform.updateAccessory(this.accessory)
420
+ }
421
+ }
422
+
423
+ // Humidity, frost protection and overheat protection all live behind
424
+ // separate namespaces for this device family -- not bundled into
425
+ // Appliance.System.All, so they need their own requests.
426
+ try {
427
+ const humRes = await this.platform.sendUpdate(this.accessory, {
428
+ namespace: 'Appliance.Control.Sensor.Latest',
429
+ payload: { latest: [{ channel: 0 }] },
430
+ })
431
+ this.applyUpdate(humRes.data.payload)
432
+ } catch (err) {
433
+ this.accessory.logDebugWarn(`${platformLang.reqFailed} [humidity]: ${parseError(err)}`)
434
+ }
435
+
436
+ try {
437
+ const frostRes = await this.platform.sendUpdate(this.accessory, {
438
+ namespace: 'Appliance.Control.Thermostat.Frost',
439
+ payload: { frost: [{ channel: 0 }] },
440
+ })
441
+ if (firstRun) {
442
+ const frostArmed = frostRes.data.payload.frost?.[0]?.onoff === 1
443
+ this.frostService = this.ensureContactSensor(this.frostService, 'frost-warning', 'Frost Protection', frostArmed)
444
+ }
445
+ this.applyUpdate(frostRes.data.payload)
446
+ } catch (err) {
447
+ this.accessory.logDebugWarn(`${platformLang.reqFailed} [frost]: ${parseError(err)}`)
448
+ }
449
+
450
+ try {
451
+ const overheatRes = await this.platform.sendUpdate(this.accessory, {
452
+ namespace: 'Appliance.Control.Thermostat.Overheat',
453
+ payload: { overheat: [{ channel: 0 }] },
454
+ })
455
+ if (firstRun) {
456
+ const overheatArmed = overheatRes.data.payload.overheat?.[0]?.onoff === 1
457
+ this.overheatService = this.ensureContactSensor(this.overheatService, 'overheat-warning', 'Overheat Protection', overheatArmed)
458
+ }
459
+ this.applyUpdate(overheatRes.data.payload)
460
+ } catch (err) {
461
+ this.accessory.logDebugWarn(`${platformLang.reqFailed} [overheat]: ${parseError(err)}`)
462
+ }
463
+ })
464
+ } catch (err) {
465
+ const eText = err instanceof TimeoutError ? platformLang.timeout : parseError(err)
466
+ this.accessory.logDebugWarn(`${platformLang.reqFailed}: ${eText}`)
467
+
468
+ // Set the homebridge-ui status of the device to offline if local and error is timeout
469
+ if (
470
+ (this.accessory.context.isOnline || firstRun)
471
+ && ['EHOSTUNREACH', 'timed out'].some(el => eText.includes(el))
472
+ ) {
473
+ this.accessory.context.isOnline = false
474
+ this.platform.updateAccessory(this.accessory)
475
+ }
476
+ }
477
+ }
478
+
479
+ receiveUpdate(params) {
480
+ try {
481
+ // Log the received data
482
+ this.accessory.logDebug(`${platformLang.incMQTT}: ${JSON.stringify(params)}`)
483
+ if (params.payload) {
484
+ this.applyUpdate(params.payload)
485
+ }
486
+ } catch (err) {
487
+ this.accessory.logWarn(`${platformLang.refFailed} ${parseError(err)}`)
488
+ }
489
+ }
490
+
491
+ applyUpdate(data) {
492
+ try {
493
+ const modeData = data.mode?.[0]
494
+ if (modeData) {
495
+ let needsUpdate = false
496
+ if (hasProperty(modeData, 'state')) {
497
+ const newState = modeData.state
498
+
499
+ // Check against the cache and update HomeKit and the cache if needed
500
+ if (this.cacheState !== newState) {
501
+ this.service.updateCharacteristic(this.hapChar.TargetHeatingCoolingState, newState)
502
+ this.cacheState = newState
503
+ this.accessory.log(`${platformLang.curState} [${newState === 1 ? 'on' : 'off'}]`)
504
+ needsUpdate = true
505
+ }
506
+ }
507
+ if (hasProperty(modeData, 'targetTemp')) {
508
+ const newTarg = modeData.targetTemp / 10
509
+
510
+ // Check against the cache and update HomeKit and the cache if needed
511
+ if (this.cacheTarg !== newTarg) {
512
+ this.service.updateCharacteristic(this.hapChar.TargetTemperature, newTarg)
513
+ this.cacheTarg = newTarg
514
+ this.accessory.log(`${platformLang.curTarg} [${newTarg}°C]`)
515
+ needsUpdate = true
516
+ }
517
+ }
518
+ if (hasProperty(modeData, 'currentTemp')) {
519
+ const newTemp = modeData.currentTemp / 10
520
+
521
+ // Check against the cache and update HomeKit and the cache if needed
522
+ if (this.cacheTemp !== newTemp) {
523
+ this.service.updateCharacteristic(this.hapChar.CurrentTemperature, newTemp)
524
+ this.cacheTemp = newTemp
525
+ this.accessory.eveService.addEntry({ temp: newTemp })
526
+ this.accessory.log(`${platformLang.curTemp} [${newTemp}°C]`)
527
+ needsUpdate = true
528
+
529
+ // Update the cache file with the new temperature
530
+ this.updateCache()
531
+ }
532
+ }
533
+
534
+ // Update the current heating state
535
+ if (needsUpdate) {
536
+ this.service.updateCharacteristic(
537
+ this.hapChar.CurrentHeatingCoolingState,
538
+ this.cacheState === 1 && this.cacheTarg > this.cacheTemp ? 1 : 0,
539
+ )
540
+ }
541
+ }
542
+
543
+ // Window-open detection arrives bundled inside the thermostat digest for a poll,
544
+ // or standalone via an Appliance.Control.Thermostat.WindowOpened push -- same key either way
545
+ // this.windowService only exists if window-open detection is armed on the device
546
+ // (see ensureContactSensor)
547
+ const windowData = data.windowOpened?.[0]
548
+ if (windowData && this.windowService && hasProperty(windowData, 'status')) {
549
+ const newWindow = windowData.status === 1 ? 1 : 0
550
+ if (this.cacheWindow !== newWindow) {
551
+ this.windowService.updateCharacteristic(this.hapChar.ContactSensorState, newWindow)
552
+ this.cacheWindow = newWindow
553
+ this.accessory.log(`${platformLang.curWindow} [${newWindow ? 'open' : 'closed'}]`)
554
+ }
555
+ }
556
+
557
+ // this.frostService only exists if frost protection is armed on the device
558
+ const frostData = data.frost?.[0]
559
+ if (frostData && this.frostService && hasProperty(frostData, 'warning')) {
560
+ // warning: 0 = no warning, 1 = warning, 2 = alarm unavailable (no sensor) -- only
561
+ // 1 represents something actionable to surface as a triggered contact.
562
+ const newFrostWarning = frostData.warning === 1 ? 1 : 0
563
+ if (this.cacheFrostWarning !== newFrostWarning) {
564
+ this.frostService.updateCharacteristic(this.hapChar.ContactSensorState, newFrostWarning)
565
+ this.cacheFrostWarning = newFrostWarning
566
+ this.accessory.log(`${platformLang.curFrostWarning} [${newFrostWarning ? 'triggered' : 'normal'}]`)
567
+ }
568
+ }
569
+
570
+ // this.overheatService only exists if overheat protection is armed on the device
571
+ const overheatData = data.overheat?.[0]
572
+ if (overheatData && this.overheatService && hasProperty(overheatData, 'warning')) {
573
+ const newOverheatWarning = overheatData.warning === 1 ? 1 : 0
574
+ if (this.cacheOverheatWarning !== newOverheatWarning) {
575
+ this.overheatService.updateCharacteristic(this.hapChar.ContactSensorState, newOverheatWarning)
576
+ this.cacheOverheatWarning = newOverheatWarning
577
+ this.accessory.log(`${platformLang.curOverheatWarning} [${newOverheatWarning ? 'triggered' : 'normal'}]`)
578
+ }
579
+ }
580
+
581
+ const humData = data.latest?.[0]?.value?.[0]
582
+ if (humData && hasProperty(humData, 'humi')) {
583
+ const newHumi = humData.humi / 10
584
+ if (this.cacheHumi !== newHumi) {
585
+ this.humidityService.updateCharacteristic(this.hapChar.CurrentRelativeHumidity, newHumi)
586
+ this.cacheHumi = newHumi
587
+ this.accessory.log(`${platformLang.curHumi} [${newHumi}%]`)
588
+ }
589
+ }
590
+ } catch (err) {
591
+ this.accessory.logWarn(`${platformLang.refFailed} ${parseError(err)}`)
592
+ }
593
+ }
594
+ }
@@ -28,7 +28,7 @@ function getDeviceConfigKey(model) {
28
28
  if (m.humidifier.includes(model)) {
29
29
  return 'humidifierDevices'
30
30
  }
31
- if (m.thermostat.includes(model)) {
31
+ if (m.thermostat.includes(model) || m.thermostatMts215.includes(model)) {
32
32
  return 'thermostatDevices'
33
33
  }
34
34
  if (m.garage.includes(model)) {
package/lib/platform.js CHANGED
@@ -847,6 +847,17 @@ export default class {
847
847
  this.applyAccessoryLogging(accessory)
848
848
  accessory.control = new deviceTypes.deviceThermostatMts300(this, accessory)
849
849
  /** */
850
+ } else if (platformConsts.models.thermostatMts215.includes(device.model)) {
851
+ /**
852
+ ********
853
+ THERMOSTATS (MTS215/MTS215B)
854
+ *********
855
+ */
856
+ accessory = this.devicesInHB.get(hbUUID) || this.addAccessory(device)
857
+ accessory.context = { ...accessory.context, ...context }
858
+ this.applyAccessoryLogging(accessory)
859
+ accessory.control = new deviceTypes.deviceThermostatMts215(this, accessory)
860
+ /** */
850
861
  } else if (platformConsts.models.thermostat.includes(device.model)) {
851
862
  /**
852
863
  ********
@@ -307,8 +307,9 @@ export default {
307
307
  garage: ['MSG100', 'MSG150', 'MSG200'],
308
308
  roller: ['MRS100', 'MRS105', 'MRS200'],
309
309
  baby: ['HP110A', 'HP110AHK'],
310
- thermostat: ['MTS200', 'MTS200B', 'MTS205', 'MTS205B', 'MTS215', 'MTS215B', 'MTS960'],
310
+ thermostat: ['MTS200', 'MTS200B', 'MTS205', 'MTS205B', 'MTS960'],
311
311
  thermostatMts300: ['MTS300'],
312
+ thermostatMts215: ['MTS215', 'MTS215B'],
312
313
  sensorPresence: ['MS600'],
313
314
  hubMain: ['MSH300', 'MSH300HK', 'MSH400', 'MSH450'],
314
315
  hubSub: ['GS559A', 'MS100', 'MS100F', 'MS130', 'MS130H', 'MS200', 'MS400', 'MST100', 'MST200', 'MTS100', 'MTS100V3', 'MTS150', 'MTS150P'],
@@ -22,6 +22,7 @@ export default {
22
22
  curCool: 'current cooling',
23
23
  curDiffSpray: 'current diffuser spray',
24
24
  curDiffState: 'current diffuser state',
25
+ curFrostWarning: 'current frost warning',
25
26
  curHeat: 'current heating',
26
27
  curHumi: 'current humidity',
27
28
  curInUse: 'current in use',
@@ -35,6 +36,7 @@ export default {
35
36
  curLock: 'current lock',
36
37
  curOcc: 'current occupancy',
37
38
  curMode: 'current mode',
39
+ curOverheatWarning: 'current overheat warning',
38
40
  curPos: 'current position',
39
41
  curPower: 'current power',
40
42
  curScene: 'current scene',
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@homebridge-plugins/homebridge-meross",
3
3
  "alias": "Meross",
4
4
  "type": "module",
5
- "version": "10.17.0-beta.2",
5
+ "version": "10.17.0-beta.4",
6
6
  "description": "Homebridge plugin to integrate Meross devices into HomeKit.",
7
7
  "author": {
8
8
  "name": "bwp91",
@@ -38,6 +38,7 @@
38
38
  "keywords": [
39
39
  "homebridge",
40
40
  "homebridge-plugin",
41
+ "supports-hap",
41
42
  "homebridge-meross",
42
43
  "hoobs",
43
44
  "hoobs-plugin",