@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.
- package/CHANGELOG.md +1346 -0
- package/LICENSE +21 -0
- package/README.md +68 -0
- package/config.schema.json +2066 -0
- package/eslint.config.js +49 -0
- package/lib/connection/http.js +345 -0
- package/lib/connection/mqtt.js +174 -0
- package/lib/device/baby.js +532 -0
- package/lib/device/cooler-single.js +447 -0
- package/lib/device/diffuser.js +730 -0
- package/lib/device/fan.js +530 -0
- package/lib/device/garage-main.js +225 -0
- package/lib/device/garage-single.js +495 -0
- package/lib/device/garage-sub.js +376 -0
- package/lib/device/heater-single.js +445 -0
- package/lib/device/hub-contact.js +56 -0
- package/lib/device/hub-leak.js +86 -0
- package/lib/device/hub-main.js +403 -0
- package/lib/device/hub-sensor.js +115 -0
- package/lib/device/hub-smoke.js +40 -0
- package/lib/device/hub-valve.js +377 -0
- package/lib/device/humidifier.js +521 -0
- package/lib/device/index.js +63 -0
- package/lib/device/light-cct.js +474 -0
- package/lib/device/light-dimmer.js +312 -0
- package/lib/device/light-rgb.js +528 -0
- package/lib/device/outlet-multi.js +383 -0
- package/lib/device/outlet-single.js +405 -0
- package/lib/device/power-strip.js +282 -0
- package/lib/device/purifier-single.js +372 -0
- package/lib/device/purifier.js +403 -0
- package/lib/device/roller-location.js +317 -0
- package/lib/device/roller.js +234 -0
- package/lib/device/sensor-presence.js +201 -0
- package/lib/device/switch-multi.js +403 -0
- package/lib/device/switch-single.js +371 -0
- package/lib/device/template.js +177 -0
- package/lib/device/thermostat.js +493 -0
- package/lib/fakegato/LICENSE +21 -0
- package/lib/fakegato/fakegato-history.js +814 -0
- package/lib/fakegato/fakegato-storage.js +108 -0
- package/lib/fakegato/fakegato-timer.js +125 -0
- package/lib/fakegato/uuid.js +27 -0
- package/lib/homebridge-ui/public/index.html +316 -0
- package/lib/homebridge-ui/server.js +10 -0
- package/lib/index.js +8 -0
- package/lib/platform.js +1256 -0
- package/lib/utils/colour.js +581 -0
- package/lib/utils/constants.js +377 -0
- package/lib/utils/custom-chars.js +165 -0
- package/lib/utils/eve-chars.js +130 -0
- package/lib/utils/functions.js +39 -0
- package/lib/utils/lang-en.js +114 -0
- package/package.json +70 -0
|
@@ -0,0 +1,445 @@
|
|
|
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.inUsePowerThreshold = this.accessory.context.options.inUsePowerThreshold
|
|
20
|
+
|| platformConsts.defaultValues.inUsePowerThreshold
|
|
21
|
+
this.name = accessory.displayName
|
|
22
|
+
const cloudRefreshRate = hasProperty(platform.config, 'cloudRefreshRate')
|
|
23
|
+
? platform.config.cloudRefreshRate
|
|
24
|
+
: platformConsts.defaultValues.cloudRefreshRate
|
|
25
|
+
const localRefreshRate = hasProperty(platform.config, 'refreshRate')
|
|
26
|
+
? platform.config.refreshRate
|
|
27
|
+
: platformConsts.defaultValues.refreshRate
|
|
28
|
+
this.pollInterval = accessory.context.connection === 'local'
|
|
29
|
+
? localRefreshRate
|
|
30
|
+
: cloudRefreshRate
|
|
31
|
+
this.temperatureSource = accessory.context.options.temperatureSource;
|
|
32
|
+
|
|
33
|
+
// If the accessory has any old services then remove them
|
|
34
|
+
['Switch', 'Outlet', 'AirPurifier'].forEach((service) => {
|
|
35
|
+
if (this.accessory.getService(this.hapServ[service])) {
|
|
36
|
+
this.accessory.removeService(this.accessory.getService(this.hapServ[service]))
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
// Set up the accessory with default target temp when added the first time
|
|
41
|
+
if (!hasProperty(this.accessory.context, 'cacheTarget')) {
|
|
42
|
+
this.accessory.context.cacheTarget = 20
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Check to make sure user has not switched from cooler to heater
|
|
46
|
+
if (this.accessory.context.cacheType !== 'heater') {
|
|
47
|
+
// Remove and re-setup as a HeaterCooler
|
|
48
|
+
if (this.accessory.getService(this.hapServ.HeaterCooler)) {
|
|
49
|
+
this.accessory.removeService(this.accessory.getService(this.hapServ.HeaterCooler))
|
|
50
|
+
}
|
|
51
|
+
this.accessory.context.cacheType = 'heater'
|
|
52
|
+
this.accessory.context.cacheTarget = 20
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Add the heater service if it doesn't already exist
|
|
56
|
+
this.service = this.accessory.getService(this.hapServ.HeaterCooler)
|
|
57
|
+
|| this.accessory.addService(this.hapServ.HeaterCooler)
|
|
58
|
+
|
|
59
|
+
// Set custom properties of the current temperature characteristic
|
|
60
|
+
this.service.getCharacteristic(this.hapChar.CurrentTemperature).setProps({
|
|
61
|
+
minStep: 0.1,
|
|
62
|
+
})
|
|
63
|
+
this.cacheTemp = this.service.getCharacteristic(this.hapChar.CurrentTemperature).value
|
|
64
|
+
|
|
65
|
+
// Add the set handler to the heater active characteristic
|
|
66
|
+
this.service
|
|
67
|
+
.getCharacteristic(this.hapChar.Active)
|
|
68
|
+
.onSet(async value => this.internalStateUpdate(value))
|
|
69
|
+
|
|
70
|
+
// Add options to the target state characteristic
|
|
71
|
+
this.service.getCharacteristic(this.hapChar.TargetHeaterCoolerState).setProps({
|
|
72
|
+
minValue: 0,
|
|
73
|
+
maxValue: 0,
|
|
74
|
+
validValues: [0],
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
// Add the set handler to the target temperature characteristic
|
|
78
|
+
this.service
|
|
79
|
+
.getCharacteristic(this.hapChar.HeatingThresholdTemperature)
|
|
80
|
+
.updateValue(this.accessory.context.cacheTarget)
|
|
81
|
+
.setProps({ minStep: 0.5 })
|
|
82
|
+
.onSet(async value => this.internalTargetTempUpdate(value))
|
|
83
|
+
|
|
84
|
+
// Initialise these caches now since they aren't determined by the initial externalUpdate()
|
|
85
|
+
this.cacheState = this.service.getCharacteristic(this.hapChar.Active).value === 1
|
|
86
|
+
this.cacheHeat = this.cacheState
|
|
87
|
+
&& this.service.getCharacteristic(this.hapChar.TargetHeaterCoolerState).value === 2
|
|
88
|
+
|
|
89
|
+
// Pass the accessory to Fakegato to set up with Eve
|
|
90
|
+
this.accessory.eveService = new platform.eveService('custom', this.accessory, { log: () => {} })
|
|
91
|
+
|
|
92
|
+
// Create the queue used for sending device requests
|
|
93
|
+
this.updateInProgress = false
|
|
94
|
+
this.queue = new PQueue({
|
|
95
|
+
concurrency: 1,
|
|
96
|
+
interval: 250,
|
|
97
|
+
intervalCap: 1,
|
|
98
|
+
timeout: 10000,
|
|
99
|
+
throwOnTimeout: true,
|
|
100
|
+
})
|
|
101
|
+
this.queue.on('idle', () => {
|
|
102
|
+
this.updateInProgress = false
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
// Set up the mqtt client for cloud devices to send and receive device updates
|
|
106
|
+
if (accessory.context.connection !== 'local') {
|
|
107
|
+
this.accessory.mqtt = new mqttClient(platform, this.accessory)
|
|
108
|
+
this.accessory.mqtt.connect()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Always request a device update on startup, then start the interval for polling
|
|
112
|
+
setTimeout(() => this.requestUpdate(true), 2000)
|
|
113
|
+
this.accessory.refreshInterval = setInterval(
|
|
114
|
+
() => this.requestUpdate(),
|
|
115
|
+
this.pollInterval * 1000,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
// Set up an interval to get regular temperature updates
|
|
119
|
+
setTimeout(() => {
|
|
120
|
+
this.getTemperature()
|
|
121
|
+
this.accessory.powerInterval = setInterval(
|
|
122
|
+
() => this.getTemperature(),
|
|
123
|
+
120000,
|
|
124
|
+
)
|
|
125
|
+
}, 5000)
|
|
126
|
+
|
|
127
|
+
// Output the customised options to the log
|
|
128
|
+
const opts = JSON.stringify({
|
|
129
|
+
connection: this.accessory.context.connection,
|
|
130
|
+
showAs: 'heater',
|
|
131
|
+
temperatureSource: this.temperatureSource,
|
|
132
|
+
})
|
|
133
|
+
platform.log('[%s] %s %s.', this.name, platformLang.devInitOpts, opts)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async internalStateUpdate(value) {
|
|
137
|
+
try {
|
|
138
|
+
// Add the request to the queue so updates are sent apart
|
|
139
|
+
await this.queue.add(async () => {
|
|
140
|
+
let newState
|
|
141
|
+
let newHeat
|
|
142
|
+
let newValue
|
|
143
|
+
if (value !== 0) {
|
|
144
|
+
newState = true
|
|
145
|
+
if (this.cacheTemp < this.accessory.context.cacheTarget) {
|
|
146
|
+
newValue = true
|
|
147
|
+
newHeat = true
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Only send the update if either:
|
|
152
|
+
// * The new value (state) is OFF and the cacheHeat was ON
|
|
153
|
+
// * The new value (state) is ON and newHeat is 'on'
|
|
154
|
+
if ((value === 0 && this.cacheHeat) || (value === 1 && newHeat)) {
|
|
155
|
+
// This flag stops the plugin from requesting updates while pending on others
|
|
156
|
+
this.updateInProgress = true
|
|
157
|
+
|
|
158
|
+
// The plugin should have determined if it's 'toggle' or 'togglex' on the first poll run
|
|
159
|
+
let namespace
|
|
160
|
+
let payload
|
|
161
|
+
if (this.isToggleX) {
|
|
162
|
+
namespace = 'Appliance.Control.ToggleX'
|
|
163
|
+
payload = {
|
|
164
|
+
togglex: {
|
|
165
|
+
onoff: newValue ? 1 : 0,
|
|
166
|
+
channel: 0,
|
|
167
|
+
},
|
|
168
|
+
}
|
|
169
|
+
} else {
|
|
170
|
+
namespace = 'Appliance.Control.Toggle'
|
|
171
|
+
payload = {
|
|
172
|
+
toggle: {
|
|
173
|
+
onoff: newValue ? 1 : 0,
|
|
174
|
+
},
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Use the platform function to send the update to the device
|
|
179
|
+
await this.platform.sendUpdate(this.accessory, {
|
|
180
|
+
namespace,
|
|
181
|
+
payload,
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
if (newState !== this.cacheState) {
|
|
185
|
+
this.cacheState = newState
|
|
186
|
+
this.accessory.log(`${platformLang.curState} [${this.cacheState ? 'on' : 'off'}]`)
|
|
187
|
+
}
|
|
188
|
+
if (newHeat !== this.cacheHeat) {
|
|
189
|
+
this.cacheHeat = newHeat
|
|
190
|
+
this.accessory.log(`${platformLang.curHeat} [${this.cacheHeat ? 'on' : 'off'}]`)
|
|
191
|
+
}
|
|
192
|
+
const newOnState = this.cacheHeat ? 2 : 1
|
|
193
|
+
this.service.updateCharacteristic(
|
|
194
|
+
this.hapChar.CurrentHeaterCoolerState,
|
|
195
|
+
value === 1 ? newOnState : 0,
|
|
196
|
+
)
|
|
197
|
+
})
|
|
198
|
+
} catch (err) {
|
|
199
|
+
const eText = err instanceof TimeoutError ? platformLang.timeout : parseError(err)
|
|
200
|
+
this.accessory.logWarn(`${platformLang.sendFailed} ${eText}`)
|
|
201
|
+
setTimeout(() => {
|
|
202
|
+
this.service.updateCharacteristic(this.hapChar.Active, this.cacheState ? 1 : 0)
|
|
203
|
+
}, 2000)
|
|
204
|
+
throw new this.hapErr(-70402)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async internalTargetTempUpdate(value) {
|
|
209
|
+
try {
|
|
210
|
+
// Add the request to the queue so updates are sent apart
|
|
211
|
+
await this.queue.add(async () => {
|
|
212
|
+
if (value === this.accessory.context.cacheTarget) {
|
|
213
|
+
return
|
|
214
|
+
}
|
|
215
|
+
this.accessory.context.cacheTarget = value
|
|
216
|
+
this.accessory.log(`${platformLang.curTarg} [${value}°C]`)
|
|
217
|
+
|
|
218
|
+
if (!this.cacheState) {
|
|
219
|
+
return
|
|
220
|
+
}
|
|
221
|
+
let newHeat
|
|
222
|
+
let newValue
|
|
223
|
+
if (this.cacheTemp < value) {
|
|
224
|
+
newValue = true
|
|
225
|
+
newHeat = true
|
|
226
|
+
}
|
|
227
|
+
if (newHeat === this.cacheHeat) {
|
|
228
|
+
return
|
|
229
|
+
}
|
|
230
|
+
// This flag stops the plugin from requesting updates while pending on others
|
|
231
|
+
this.updateInProgress = true
|
|
232
|
+
|
|
233
|
+
// The plugin should have determined if it's 'toggle' or 'togglex' on the first poll run
|
|
234
|
+
let namespace
|
|
235
|
+
let payload
|
|
236
|
+
if (this.isToggleX) {
|
|
237
|
+
namespace = 'Appliance.Control.ToggleX'
|
|
238
|
+
payload = {
|
|
239
|
+
togglex: {
|
|
240
|
+
onoff: newValue ? 1 : 0,
|
|
241
|
+
channel: 0,
|
|
242
|
+
},
|
|
243
|
+
}
|
|
244
|
+
} else {
|
|
245
|
+
namespace = 'Appliance.Control.Toggle'
|
|
246
|
+
payload = {
|
|
247
|
+
toggle: {
|
|
248
|
+
onoff: newValue ? 1 : 0,
|
|
249
|
+
},
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Use the platform function to send the update to the device
|
|
254
|
+
await this.platform.sendUpdate(this.accessory, {
|
|
255
|
+
namespace,
|
|
256
|
+
payload,
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
// Cache and log
|
|
260
|
+
this.cacheHeat = newHeat
|
|
261
|
+
this.accessory.log(`${platformLang.curHeat} [${this.cacheHeat ? 'on' : 'off'}]`)
|
|
262
|
+
this.service.updateCharacteristic(
|
|
263
|
+
this.hapChar.CurrentHeaterCoolerState,
|
|
264
|
+
this.cacheHeat ? 2 : 1,
|
|
265
|
+
)
|
|
266
|
+
})
|
|
267
|
+
} catch (err) {
|
|
268
|
+
const eText = err instanceof TimeoutError ? platformLang.timeout : parseError(err)
|
|
269
|
+
this.accessory.logWarn(`${platformLang.sendFailed} ${eText}`)
|
|
270
|
+
setTimeout(() => {
|
|
271
|
+
this.service.updateCharacteristic(
|
|
272
|
+
this.hapChar.HeatingThresholdTemperature,
|
|
273
|
+
this.accessory.context.cacheTarget,
|
|
274
|
+
)
|
|
275
|
+
}, 2000)
|
|
276
|
+
throw new this.hapErr(-70402)
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async internalCurrentTempUpdate() {
|
|
281
|
+
try {
|
|
282
|
+
// Add the request to the queue so updates are sent apart
|
|
283
|
+
await this.queue.add(async () => {
|
|
284
|
+
if (!this.cacheState) {
|
|
285
|
+
return
|
|
286
|
+
}
|
|
287
|
+
let newHeat
|
|
288
|
+
let newValue
|
|
289
|
+
if (this.cacheTemp < this.accessory.context.cacheTarget) {
|
|
290
|
+
newValue = true
|
|
291
|
+
newHeat = true
|
|
292
|
+
}
|
|
293
|
+
if (newHeat === this.cacheHeat) {
|
|
294
|
+
return
|
|
295
|
+
}
|
|
296
|
+
// This flag stops the plugin from requesting updates while pending on others
|
|
297
|
+
this.updateInProgress = true
|
|
298
|
+
|
|
299
|
+
// The plugin should have determined if it's 'toggle' or 'togglex' on the first poll run
|
|
300
|
+
let namespace
|
|
301
|
+
let payload
|
|
302
|
+
if (this.isToggleX) {
|
|
303
|
+
namespace = 'Appliance.Control.ToggleX'
|
|
304
|
+
payload = {
|
|
305
|
+
togglex: {
|
|
306
|
+
onoff: newValue ? 1 : 0,
|
|
307
|
+
channel: 0,
|
|
308
|
+
},
|
|
309
|
+
}
|
|
310
|
+
} else {
|
|
311
|
+
namespace = 'Appliance.Control.Toggle'
|
|
312
|
+
payload = {
|
|
313
|
+
toggle: {
|
|
314
|
+
onoff: newValue ? 1 : 0,
|
|
315
|
+
},
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Use the platform function to send the update to the device
|
|
320
|
+
await this.platform.sendUpdate(this.accessory, {
|
|
321
|
+
namespace,
|
|
322
|
+
payload,
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
// Cache and log
|
|
326
|
+
this.cacheHeat = newHeat
|
|
327
|
+
this.accessory.log(`${platformLang.curHeat} [${this.cacheHeat ? 'on' : 'off'}]`)
|
|
328
|
+
this.service.updateCharacteristic(
|
|
329
|
+
this.hapChar.CurrentHeaterCoolerState,
|
|
330
|
+
this.cacheHeat ? 2 : 1,
|
|
331
|
+
)
|
|
332
|
+
})
|
|
333
|
+
} catch (err) {
|
|
334
|
+
const eText = err instanceof TimeoutError ? platformLang.timeout : parseError(err)
|
|
335
|
+
this.accessory.logWarn(`${platformLang.sendFailed} ${eText}`)
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async getTemperature() {
|
|
340
|
+
try {
|
|
341
|
+
// Skip polling if the storage hasn't initialised properly
|
|
342
|
+
if (!this.platform.storageClientData) {
|
|
343
|
+
return
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const newTemp = await this.platform.storageData.getItem(`${this.temperatureSource}_temp`)
|
|
347
|
+
if (newTemp && newTemp !== this.cacheTemp) {
|
|
348
|
+
this.cacheTemp = newTemp
|
|
349
|
+
this.service.updateCharacteristic(this.hapChar.CurrentTemperature, this.cacheTemp)
|
|
350
|
+
this.accessory.eveService.addEntry({ temp: this.cacheTemp })
|
|
351
|
+
|
|
352
|
+
this.accessory.log(`${platformLang.curTemp} [${this.cacheTemp}°C]`)
|
|
353
|
+
await this.internalCurrentTempUpdate()
|
|
354
|
+
}
|
|
355
|
+
} catch (err) {
|
|
356
|
+
this.accessory.logWarn(parseError(err))
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async requestUpdate(firstRun = false) {
|
|
361
|
+
try {
|
|
362
|
+
// Don't continue if an update is currently being sent to the device
|
|
363
|
+
if (this.updateInProgress) {
|
|
364
|
+
return
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Add the request to the queue so updates are sent apart
|
|
368
|
+
await this.queue.add(async () => {
|
|
369
|
+
// This flag stops the plugin from requesting updates while pending on others
|
|
370
|
+
this.updateInProgress = true
|
|
371
|
+
|
|
372
|
+
// Send the request
|
|
373
|
+
const res = await this.platform.sendUpdate(this.accessory, {
|
|
374
|
+
namespace: 'Appliance.System.All',
|
|
375
|
+
payload: {},
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
// Log the received data
|
|
379
|
+
this.accessory.logDebug(`${platformLang.incPoll}: ${JSON.stringify(res.data)}`)
|
|
380
|
+
|
|
381
|
+
// Check the response is in a useful format
|
|
382
|
+
const data = res.data.payload
|
|
383
|
+
if (data.all) {
|
|
384
|
+
if (firstRun && data.all.digest) {
|
|
385
|
+
if (data.all.digest.togglex && data.all.digest.togglex[0]) {
|
|
386
|
+
this.isToggleX = true
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// A flag to check if we need to update the accessory context
|
|
391
|
+
let needsUpdate = false
|
|
392
|
+
|
|
393
|
+
// Get the mac address and hardware version of the device
|
|
394
|
+
if (data.all.system) {
|
|
395
|
+
// Mac address and hardware don't change regularly so only get on first poll
|
|
396
|
+
if (firstRun && data.all.system.hardware) {
|
|
397
|
+
this.accessory.context.macAddress = data.all.system.hardware.macAddress.toUpperCase()
|
|
398
|
+
this.accessory.context.hardware = data.all.system.hardware.version
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Get the ip address and firmware of the device
|
|
402
|
+
if (data.all.system.firmware) {
|
|
403
|
+
// Check for an IP change each and every time the device is polled
|
|
404
|
+
if (this.accessory.context.ipAddress !== data.all.system.firmware.innerIp) {
|
|
405
|
+
this.accessory.context.ipAddress = data.all.system.firmware.innerIp
|
|
406
|
+
needsUpdate = true
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Firmware doesn't change regularly so only get on first poll
|
|
410
|
+
if (firstRun) {
|
|
411
|
+
this.accessory.context.firmware = data.all.system.firmware.version
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Get the cloud online status of the device
|
|
417
|
+
if (data.all.system.online) {
|
|
418
|
+
const isOnline = data.all.system.online.status === 1
|
|
419
|
+
if (this.accessory.context.isOnline !== isOnline) {
|
|
420
|
+
this.accessory.context.isOnline = isOnline
|
|
421
|
+
needsUpdate = true
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Update the accessory cache if anything has changed
|
|
426
|
+
if (needsUpdate || firstRun) {
|
|
427
|
+
this.platform.updateAccessory(this.accessory)
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
})
|
|
431
|
+
} catch (err) {
|
|
432
|
+
const eText = err instanceof TimeoutError ? platformLang.timeout : parseError(err)
|
|
433
|
+
this.accessory.logDebugWarn(`${platformLang.reqFailed}: ${eText}`)
|
|
434
|
+
|
|
435
|
+
// Set the homebridge-ui status of the device to offline if local and error is timeout
|
|
436
|
+
if (
|
|
437
|
+
(this.accessory.context.isOnline || firstRun)
|
|
438
|
+
&& ['EHOSTUNREACH', 'timed out'].some(el => eText.includes(el))
|
|
439
|
+
) {
|
|
440
|
+
this.accessory.context.isOnline = false
|
|
441
|
+
this.platform.updateAccessory(this.accessory)
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import platformConsts from '../utils/constants.js'
|
|
2
|
+
import { hasProperty, parseError } from '../utils/functions.js'
|
|
3
|
+
import platformLang from '../utils/lang-en.js'
|
|
4
|
+
|
|
5
|
+
export default class {
|
|
6
|
+
constructor(platform, accessory) {
|
|
7
|
+
// Set up variables from the platform
|
|
8
|
+
this.hapChar = platform.api.hap.Characteristic
|
|
9
|
+
this.hapErr = platform.api.hap.HapStatusError
|
|
10
|
+
this.hapServ = platform.api.hap.Service
|
|
11
|
+
this.platform = platform
|
|
12
|
+
|
|
13
|
+
// Set up variables from the accessory
|
|
14
|
+
this.accessory = accessory
|
|
15
|
+
this.lowBattThreshold = accessory.context.options.lowBattThreshold
|
|
16
|
+
? Math.min(accessory.context.options.lowBattThreshold, 100)
|
|
17
|
+
: platformConsts.defaultValues.lowBattThreshold
|
|
18
|
+
this.name = accessory.displayName
|
|
19
|
+
|
|
20
|
+
// Add the battery service if it doesn't already exist
|
|
21
|
+
this.battService = this.accessory.getService(this.hapServ.Battery) || this.accessory.addService(this.hapServ.Battery)
|
|
22
|
+
this.cacheBatt = this.battService.getCharacteristic(this.hapChar.BatteryLevel).value
|
|
23
|
+
|
|
24
|
+
// Output the customised options to the log
|
|
25
|
+
const opts = JSON.stringify({
|
|
26
|
+
connection: this.accessory.context.connection,
|
|
27
|
+
lowBattThreshold: this.lowBattThreshold,
|
|
28
|
+
})
|
|
29
|
+
platform.log('[%s] %s %s.', this.name, platformLang.devInitOpts, opts)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
applyUpdate(data) {
|
|
33
|
+
try {
|
|
34
|
+
// Battery % from reported voltage
|
|
35
|
+
if (hasProperty(data, 'voltage')) {
|
|
36
|
+
// 1. Reduce/enlarge value so in [2000, 3000]
|
|
37
|
+
let newVoltage = Math.min(Math.max(data.voltage, 2000), 3000)
|
|
38
|
+
|
|
39
|
+
// 2. Scale this from [2000, 3000] to [0, 100] and round to nearest whole number
|
|
40
|
+
newVoltage = Math.round((newVoltage - 2000) / 10)
|
|
41
|
+
|
|
42
|
+
// This should be a rough estimate of the battery %
|
|
43
|
+
if (newVoltage !== this.cacheBatt) {
|
|
44
|
+
this.cacheBatt = newVoltage
|
|
45
|
+
this.battService.updateCharacteristic(this.hapChar.BatteryLevel, this.cacheBatt)
|
|
46
|
+
this.battService.updateCharacteristic(
|
|
47
|
+
this.hapChar.StatusLowBattery,
|
|
48
|
+
this.cacheBatt < this.lowBattThreshold ? 1 : 0,
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
} catch (err) {
|
|
53
|
+
this.accessory.logWarn(`${platformLang.refFailed} ${parseError(err)}`)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { hasProperty, parseError } from '../utils/functions.js'
|
|
2
|
+
import platformLang from '../utils/lang-en.js'
|
|
3
|
+
|
|
4
|
+
export default class {
|
|
5
|
+
constructor(platform, accessory) {
|
|
6
|
+
// Set up variables from the platform
|
|
7
|
+
this.eveChar = platform.eveChar
|
|
8
|
+
this.hapChar = platform.api.hap.Characteristic
|
|
9
|
+
this.hapErr = platform.api.hap.HapStatusError
|
|
10
|
+
this.hapServ = platform.api.hap.Service
|
|
11
|
+
this.platform = platform
|
|
12
|
+
|
|
13
|
+
// Set up variables from the accessory
|
|
14
|
+
this.accessory = accessory
|
|
15
|
+
|
|
16
|
+
// Battery service is not available with this device, so remove the service if exists
|
|
17
|
+
if (this.accessory.getService(this.hapServ.Battery)) {
|
|
18
|
+
this.accessory.removeService(this.accessory.getService(this.hapServ.Battery))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Add the leak sensor service if it doesn't already exist
|
|
22
|
+
this.service = this.accessory.getService(this.hapServ.LeakSensor)
|
|
23
|
+
if (!this.service) {
|
|
24
|
+
this.service = this.accessory.addService(this.hapServ.LeakSensor)
|
|
25
|
+
this.service.addCharacteristic(this.eveChar.LastActivation)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Pass the accessory to Fakegato to set up with Eve
|
|
29
|
+
this.accessory.eveService = new platform.eveService('motion', this.accessory, { log: () => {} })
|
|
30
|
+
|
|
31
|
+
// Reset to no leak when homebridge starts
|
|
32
|
+
this.service.updateCharacteristic(this.hapChar.LeakDetected, 0)
|
|
33
|
+
this.accessory.eveService.addEntry({ status: 0 })
|
|
34
|
+
this.cacheLeak = 0
|
|
35
|
+
|
|
36
|
+
// Output the customised options to the log
|
|
37
|
+
const opts = JSON.stringify({
|
|
38
|
+
connection: this.accessory.context.connection,
|
|
39
|
+
})
|
|
40
|
+
platform.log('[%s] %s %s.', accessory.displayName, platformLang.devInitOpts, opts)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
applyUpdate(data) {
|
|
44
|
+
try {
|
|
45
|
+
// data.waterLeak.latestWaterLeak from cloud is 1 or 0
|
|
46
|
+
// data.latestWaterLeak from mqtt is 1 or 0
|
|
47
|
+
let newStatus
|
|
48
|
+
|
|
49
|
+
if (hasProperty(data, 'latestWaterLeak')) {
|
|
50
|
+
newStatus = data.latestWaterLeak
|
|
51
|
+
} else if (hasProperty(data, 'waterLeak') && hasProperty(data.waterLeak, 'latestWaterLeak')) {
|
|
52
|
+
newStatus = data.waterLeak.latestWaterLeak
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
switch (newStatus) {
|
|
56
|
+
case 1: {
|
|
57
|
+
// Leak detected
|
|
58
|
+
if (this.cacheLeak !== 1) {
|
|
59
|
+
this.service.updateCharacteristic(this.hapChar.LeakDetected, 1)
|
|
60
|
+
this.accessory.eveService.addEntry({ status: 1 })
|
|
61
|
+
this.cacheLeak = 1
|
|
62
|
+
this.accessory.log(`${platformLang.curLeak} [yes]`)
|
|
63
|
+
}
|
|
64
|
+
break
|
|
65
|
+
}
|
|
66
|
+
case 0: {
|
|
67
|
+
// No leak detected
|
|
68
|
+
if (this.cacheLeak !== 0) {
|
|
69
|
+
this.service.updateCharacteristic(this.hapChar.LeakDetected, 0)
|
|
70
|
+
this.accessory.eveService.addEntry({ status: 0 })
|
|
71
|
+
this.cacheLeak = 0
|
|
72
|
+
this.accessory.log(`${platformLang.curLeak} [no]`)
|
|
73
|
+
}
|
|
74
|
+
break
|
|
75
|
+
}
|
|
76
|
+
default: {
|
|
77
|
+
// Unknown status if possible
|
|
78
|
+
this.accessory.logWarn(`unknown latestWaterLeak status received: ${JSON.stringify(data)}`)
|
|
79
|
+
break
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
} catch (err) {
|
|
83
|
+
this.accessory.logWarn(`${platformLang.refFailed} ${parseError(err)}`)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|