@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 +2 -0
- package/lib/device/index.js +2 -0
- package/lib/device/thermostat-mts215.js +594 -0
- package/lib/homebridge-ui/server.js +1 -1
- package/lib/platform.js +11 -0
- package/lib/utils/constants.js +2 -1
- package/lib/utils/lang-en.js +2 -0
- package/package.json +2 -1
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
|
|
package/lib/device/index.js
CHANGED
|
@@ -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
|
********
|
package/lib/utils/constants.js
CHANGED
|
@@ -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', '
|
|
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'],
|
package/lib/utils/lang-en.js
CHANGED
|
@@ -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.
|
|
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",
|