@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,405 @@
|
|
|
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.eveChar = platform.eveChar
|
|
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.inUsePowerThreshold = this.accessory.context.options.inUsePowerThreshold
|
|
21
|
+
|| platformConsts.defaultValues.inUsePowerThreshold
|
|
22
|
+
this.name = accessory.displayName
|
|
23
|
+
const cloudRefreshRate = hasProperty(platform.config, 'cloudRefreshRate')
|
|
24
|
+
? platform.config.cloudRefreshRate
|
|
25
|
+
: platformConsts.defaultValues.cloudRefreshRate
|
|
26
|
+
const localRefreshRate = hasProperty(platform.config, 'refreshRate')
|
|
27
|
+
? platform.config.refreshRate
|
|
28
|
+
: platformConsts.defaultValues.refreshRate
|
|
29
|
+
this.pollInterval = accessory.context.connection === 'local'
|
|
30
|
+
? localRefreshRate
|
|
31
|
+
: cloudRefreshRate;
|
|
32
|
+
|
|
33
|
+
// If the accessory has any old services then remove them
|
|
34
|
+
['Switch', 'HeaterCooler', '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
|
+
// Add the outlet service if it doesn't already exist
|
|
41
|
+
this.service = this.accessory.getService(this.hapServ.Outlet)
|
|
42
|
+
|| this.accessory.addService(this.hapServ.Outlet)
|
|
43
|
+
|
|
44
|
+
// Add the set handler to the switch on/off characteristic
|
|
45
|
+
this.service
|
|
46
|
+
.getCharacteristic(this.hapChar.On)
|
|
47
|
+
.onSet(async value => this.internalStateUpdate(value))
|
|
48
|
+
this.cacheState = this.service.getCharacteristic(this.hapChar.On).value
|
|
49
|
+
|
|
50
|
+
// Pass the accessory to Fakegato to set up with Eve
|
|
51
|
+
this.accessory.eveService = new platform.eveService('energy', this.accessory, { log: () => {} })
|
|
52
|
+
|
|
53
|
+
// Create the queue used for sending device requests
|
|
54
|
+
this.updateInProgress = false
|
|
55
|
+
this.queue = new PQueue({
|
|
56
|
+
concurrency: 1,
|
|
57
|
+
interval: 250,
|
|
58
|
+
intervalCap: 1,
|
|
59
|
+
timeout: 10000,
|
|
60
|
+
throwOnTimeout: true,
|
|
61
|
+
})
|
|
62
|
+
this.queue.on('idle', () => {
|
|
63
|
+
this.updateInProgress = false
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
// Set up the mqtt client for cloud devices to send and receive device updates
|
|
67
|
+
if (accessory.context.connection !== 'local') {
|
|
68
|
+
this.accessory.mqtt = new mqttClient(platform, this.accessory)
|
|
69
|
+
this.accessory.mqtt.connect()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Always request a device update on startup, then start the interval for polling
|
|
73
|
+
setTimeout(() => this.requestUpdate(true), 2000)
|
|
74
|
+
this.accessory.refreshInterval = setInterval(
|
|
75
|
+
() => this.requestUpdate(),
|
|
76
|
+
this.pollInterval * 1000,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
// Test to see if the device supports power usage
|
|
80
|
+
setTimeout(() => this.setupPowerReadings(), 5000)
|
|
81
|
+
|
|
82
|
+
// Output the customised options to the log
|
|
83
|
+
const opts = JSON.stringify({
|
|
84
|
+
connection: this.accessory.context.connection,
|
|
85
|
+
inUsePowerThreshold: this.inUsePowerThreshold,
|
|
86
|
+
showAs: 'outlet',
|
|
87
|
+
})
|
|
88
|
+
platform.log('[%s] %s %s.', this.name, platformLang.devInitOpts, opts)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async internalStateUpdate(value) {
|
|
92
|
+
try {
|
|
93
|
+
// Add the request to the queue so updates are sent apart
|
|
94
|
+
await this.queue.add(async () => {
|
|
95
|
+
// Don't continue if the state is the same as before
|
|
96
|
+
if (value === this.cacheState) {
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// This flag stops the plugin from requesting updates while pending on others
|
|
101
|
+
this.updateInProgress = true
|
|
102
|
+
|
|
103
|
+
// The plugin should have determined if it's 'toggle' or 'togglex' on the first poll run
|
|
104
|
+
let namespace
|
|
105
|
+
let payload
|
|
106
|
+
if (this.isToggleX) {
|
|
107
|
+
namespace = 'Appliance.Control.ToggleX'
|
|
108
|
+
payload = {
|
|
109
|
+
togglex: {
|
|
110
|
+
onoff: value ? 1 : 0,
|
|
111
|
+
channel: 0,
|
|
112
|
+
},
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
namespace = 'Appliance.Control.Toggle'
|
|
116
|
+
payload = {
|
|
117
|
+
toggle: {
|
|
118
|
+
onoff: value ? 1 : 0,
|
|
119
|
+
},
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Use the platform function to send the update to the device
|
|
124
|
+
await this.platform.sendUpdate(this.accessory, {
|
|
125
|
+
namespace,
|
|
126
|
+
payload,
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
// Update the cache and log the update has been successful
|
|
130
|
+
this.cacheState = value
|
|
131
|
+
this.accessory.log(`${platformLang.curState} [${value ? 'on' : 'off'}]`)
|
|
132
|
+
})
|
|
133
|
+
} catch (err) {
|
|
134
|
+
// Catch any errors whilst updating the device
|
|
135
|
+
const eText = err instanceof TimeoutError ? platformLang.timeout : parseError(err)
|
|
136
|
+
this.accessory.logWarn(`${platformLang.sendFailed} ${eText}`)
|
|
137
|
+
setTimeout(() => {
|
|
138
|
+
this.service.updateCharacteristic(this.hapChar.On, this.cacheState)
|
|
139
|
+
}, 2000)
|
|
140
|
+
throw new this.hapErr(-70402)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async requestUpdate(firstRun = false) {
|
|
145
|
+
try {
|
|
146
|
+
// Don't continue if an update is currently being sent to the device
|
|
147
|
+
if (this.updateInProgress) {
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Add the request to the queue so updates are sent apart
|
|
152
|
+
await this.queue.add(async () => {
|
|
153
|
+
// This flag stops the plugin from requesting updates while pending on others
|
|
154
|
+
this.updateInProgress = true
|
|
155
|
+
|
|
156
|
+
// Send the request
|
|
157
|
+
const res = await this.platform.sendUpdate(this.accessory, {
|
|
158
|
+
namespace: 'Appliance.System.All',
|
|
159
|
+
payload: {},
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
// Log the received data
|
|
163
|
+
this.accessory.logDebug(`${platformLang.incPoll}: ${JSON.stringify(res.data)}`)
|
|
164
|
+
|
|
165
|
+
// Check the response is in a useful format
|
|
166
|
+
const data = res.data.payload
|
|
167
|
+
if (data.all) {
|
|
168
|
+
if (data.all.digest) {
|
|
169
|
+
if (data.all.digest.togglex && data.all.digest.togglex[0]) {
|
|
170
|
+
this.isToggleX = true
|
|
171
|
+
this.applyUpdate(data.all.digest.togglex[0])
|
|
172
|
+
} else if (data.all.digest.toggle) {
|
|
173
|
+
this.isToggleX = false
|
|
174
|
+
this.applyUpdate(data.all.digest.toggle)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// A flag to check if we need to update the accessory context
|
|
179
|
+
let needsUpdate = false
|
|
180
|
+
|
|
181
|
+
// Get the mac address and hardware version of the device
|
|
182
|
+
if (data.all.system) {
|
|
183
|
+
// Mac address and hardware don't change regularly so only get on first poll
|
|
184
|
+
if (firstRun && data.all.system.hardware) {
|
|
185
|
+
this.accessory.context.macAddress = data.all.system.hardware.macAddress.toUpperCase()
|
|
186
|
+
this.accessory.context.hardware = data.all.system.hardware.version
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Get the ip address and firmware of the device
|
|
190
|
+
if (data.all.system.firmware) {
|
|
191
|
+
// Check for an IP change each and every time the device is polled
|
|
192
|
+
if (this.accessory.context.ipAddress !== data.all.system.firmware.innerIp) {
|
|
193
|
+
this.accessory.context.ipAddress = data.all.system.firmware.innerIp
|
|
194
|
+
needsUpdate = true
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Firmware doesn't change regularly so only get on first poll
|
|
198
|
+
if (firstRun) {
|
|
199
|
+
this.accessory.context.firmware = data.all.system.firmware.version
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Get the cloud online status of the device
|
|
205
|
+
if (data.all.system.online) {
|
|
206
|
+
const isOnline = data.all.system.online.status === 1
|
|
207
|
+
if (this.accessory.context.isOnline !== isOnline) {
|
|
208
|
+
this.accessory.context.isOnline = isOnline
|
|
209
|
+
needsUpdate = true
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Update the accessory cache if anything has changed
|
|
214
|
+
if (needsUpdate || firstRun) {
|
|
215
|
+
this.platform.updateAccessory(this.accessory)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
})
|
|
219
|
+
} catch (err) {
|
|
220
|
+
const eText = err instanceof TimeoutError ? platformLang.timeout : parseError(err)
|
|
221
|
+
this.accessory.logDebugWarn(`${platformLang.reqFailed}: ${eText}`)
|
|
222
|
+
|
|
223
|
+
// Set the homebridge-ui status of the device to offline if local and error is timeout
|
|
224
|
+
if (
|
|
225
|
+
(this.accessory.context.isOnline || firstRun)
|
|
226
|
+
&& ['EHOSTUNREACH', 'timed out'].some(el => eText.includes(el))
|
|
227
|
+
) {
|
|
228
|
+
this.accessory.context.isOnline = false
|
|
229
|
+
this.platform.updateAccessory(this.accessory)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
receiveUpdate(params) {
|
|
235
|
+
try {
|
|
236
|
+
// Log the received data
|
|
237
|
+
this.accessory.logDebug(`${platformLang.incMQTT}: ${JSON.stringify(params)}`)
|
|
238
|
+
if (params.payload) {
|
|
239
|
+
if (params.payload.togglex && params.payload.togglex[0]) {
|
|
240
|
+
this.applyUpdate(params.payload.togglex[0])
|
|
241
|
+
} else if (params.payload.toggle) {
|
|
242
|
+
this.applyUpdate(params.payload.toggle)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
} catch (err) {
|
|
246
|
+
this.accessory.logWarn(`${platformLang.refFailed} ${parseError(err)}`)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
applyUpdate(data) {
|
|
251
|
+
// Check the data is in a format which contains the value we need
|
|
252
|
+
if (hasProperty(data, 'onoff')) {
|
|
253
|
+
// newState is given as 0 or 1 -> convert to bool for HomeKit
|
|
254
|
+
const newState = data.onoff === 1
|
|
255
|
+
|
|
256
|
+
// Check against the cache and update HomeKit and the cache if needed
|
|
257
|
+
if (this.cacheState !== newState) {
|
|
258
|
+
this.service.updateCharacteristic(this.hapChar.On, newState)
|
|
259
|
+
this.cacheState = newState
|
|
260
|
+
this.accessory.log(`${platformLang.curState} [${newState ? 'on' : 'off'}]`)
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (hasProperty(data, 'power')) {
|
|
264
|
+
const newPower = data.power
|
|
265
|
+
const scaledPower = Math.round(newPower / 10) / 100
|
|
266
|
+
const newkWh = scaledPower / 60000
|
|
267
|
+
|
|
268
|
+
// Check against the cache and update HomeKit and the cache if needed
|
|
269
|
+
let newInUse = this.cacheInUse
|
|
270
|
+
let doLog = false
|
|
271
|
+
if (this.cachePower !== newPower) {
|
|
272
|
+
newInUse = this.cacheState && scaledPower > this.inUsePowerThreshold
|
|
273
|
+
this.service.updateCharacteristic(this.eveChar.CurrentConsumption, scaledPower)
|
|
274
|
+
this.accessory.eveService.addEntry({ power: scaledPower })
|
|
275
|
+
this.cachePower = newPower
|
|
276
|
+
if (this.cacheScaledPower !== scaledPower) {
|
|
277
|
+
this.cacheScaledPower = scaledPower
|
|
278
|
+
doLog = true
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Update the total consumption, approximating by using this power value as during a minute
|
|
283
|
+
const newTotalkWh = newkWh + this.accessory.context.totalkWh
|
|
284
|
+
this.accessory.context.totalkWh = newTotalkWh
|
|
285
|
+
if (newTotalkWh !== this.cacheTotal) {
|
|
286
|
+
this.service.updateCharacteristic(this.eveChar.TotalConsumption, newTotalkWh)
|
|
287
|
+
this.cacheTotal = newTotalkWh
|
|
288
|
+
const newScaledTotal = Math.round(newTotalkWh * 100) / 100
|
|
289
|
+
if (this.cacheScaledTotal !== newScaledTotal) {
|
|
290
|
+
this.cacheScaledTotal = newScaledTotal
|
|
291
|
+
doLog = true
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Log both the W and kWh if either have changed
|
|
296
|
+
if (doLog) {
|
|
297
|
+
this.accessory.logDebug(`${platformLang.curPower} [${this.cacheScaledPower}W] [${this.cacheScaledTotal}kWh]`)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Update the OutletInUse property
|
|
301
|
+
if (this.cacheInUse !== newInUse) {
|
|
302
|
+
this.cacheInUse = newInUse
|
|
303
|
+
this.service.updateCharacteristic(this.hapChar.OutletInUse, !!newInUse)
|
|
304
|
+
this.accessory.log(`${platformLang.curInUse} [${newInUse ? 'yes' : 'no'}]`)
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (hasProperty(data, 'voltage')) {
|
|
308
|
+
// newState is given as 0 or 1 -> convert to bool for HomeKit
|
|
309
|
+
const newVoltage = data.voltage
|
|
310
|
+
|
|
311
|
+
// Check against the cache and update HomeKit and the cache if needed
|
|
312
|
+
if (this.cacheVoltage !== newVoltage) {
|
|
313
|
+
const scaledVoltage = Math.round(newVoltage * 10) / 100
|
|
314
|
+
this.service.updateCharacteristic(this.eveChar.Voltage, scaledVoltage)
|
|
315
|
+
this.cacheVoltage = newVoltage
|
|
316
|
+
this.accessory.logDebug(`${platformLang.curVolt} [${scaledVoltage}V]`)
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async setupPowerReadings() {
|
|
322
|
+
try {
|
|
323
|
+
// Add the request to the queue so updates are sent apart
|
|
324
|
+
await this.queue.add(async () => {
|
|
325
|
+
// This flag stops the plugin from requesting updates while pending on others
|
|
326
|
+
this.updateInProgress = true
|
|
327
|
+
// Send the request
|
|
328
|
+
const res = await this.platform.sendUpdate(this.accessory, {
|
|
329
|
+
namespace: 'Appliance.Control.Electricity',
|
|
330
|
+
payload: {},
|
|
331
|
+
})
|
|
332
|
+
// Check the response is in a useful format
|
|
333
|
+
if (!res.data.payload || !res.data.payload.electricity) {
|
|
334
|
+
throw new Error('no data on initial run')
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Setup the outlet in use and Eve characteristics
|
|
338
|
+
if (!this.service.testCharacteristic(this.hapChar.OutletInUse)) {
|
|
339
|
+
this.service.addCharacteristic(this.hapChar.OutletInUse)
|
|
340
|
+
}
|
|
341
|
+
this.cacheInUse = this.service.getCharacteristic(this.hapChar.OutletInUse).value
|
|
342
|
+
if (!this.service.testCharacteristic(this.eveChar.CurrentConsumption)) {
|
|
343
|
+
this.service.addCharacteristic(this.eveChar.CurrentConsumption)
|
|
344
|
+
}
|
|
345
|
+
if (!this.service.testCharacteristic(this.eveChar.TotalConsumption)) {
|
|
346
|
+
this.service.addCharacteristic(this.eveChar.TotalConsumption)
|
|
347
|
+
}
|
|
348
|
+
if (!this.service.testCharacteristic(this.eveChar.Voltage)) {
|
|
349
|
+
this.service.addCharacteristic(this.eveChar.Voltage)
|
|
350
|
+
}
|
|
351
|
+
if (!this.service.testCharacteristic(this.eveChar.ResetTotal)) {
|
|
352
|
+
this.service.addCharacteristic(this.eveChar.ResetTotal)
|
|
353
|
+
}
|
|
354
|
+
if (!hasProperty(this.accessory.context, 'totalkWh')) {
|
|
355
|
+
this.accessory.context.totalkWh = 0
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Add the set handler to the outlet eve reset total energy characteristic
|
|
359
|
+
this.service.getCharacteristic(this.eveChar.ResetTotal).onSet(() => {
|
|
360
|
+
this.accessory.context.totalkWh = 0
|
|
361
|
+
this.service.updateCharacteristic(this.eveChar.TotalConsumption, 0)
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
// Create the poll
|
|
365
|
+
this.requestPowerReadings()
|
|
366
|
+
this.accessory.powerInterval = setInterval(() => this.requestPowerReadings(), 60000)
|
|
367
|
+
})
|
|
368
|
+
} catch (err) {
|
|
369
|
+
const eText = err instanceof TimeoutError
|
|
370
|
+
? platformLang.timeout
|
|
371
|
+
: parseError(err, ['no data on initial run'])
|
|
372
|
+
this.accessory.logDebug(`${platformLang.disablingPower} ${eText}`)
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async requestPowerReadings() {
|
|
377
|
+
try {
|
|
378
|
+
// Add the request to the queue so updates are sent apart
|
|
379
|
+
await this.queue.add(async () => {
|
|
380
|
+
// This flag stops the plugin from requesting updates while pending on others
|
|
381
|
+
this.updateInProgress = true
|
|
382
|
+
// Send the request
|
|
383
|
+
const res = await this.platform.sendUpdate(this.accessory, {
|
|
384
|
+
namespace: 'Appliance.Control.Electricity',
|
|
385
|
+
payload: {},
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
// Log the received data
|
|
389
|
+
this.accessory.logDebug(`${platformLang.incPoll}: ${JSON.stringify(res.data)}`)
|
|
390
|
+
|
|
391
|
+
// Check the response is in a useful format
|
|
392
|
+
const data = res.data.payload
|
|
393
|
+
if (data && data.electricity) {
|
|
394
|
+
this.applyUpdate(data.electricity)
|
|
395
|
+
}
|
|
396
|
+
})
|
|
397
|
+
} catch (err) {
|
|
398
|
+
const eText = err instanceof TimeoutError ? platformLang.timeout : parseError(err)
|
|
399
|
+
this.accessory.logDebugWarn(`${platformLang.powerFail} ${eText}`)
|
|
400
|
+
|
|
401
|
+
// Also don't increase the measured total consumption in case of any error
|
|
402
|
+
this.accessory.eveService.addEntry({ power: 0 })
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
@@ -0,0 +1,282 @@
|
|
|
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.name = accessory.displayName
|
|
20
|
+
const cloudRefreshRate = hasProperty(platform.config, 'cloudRefreshRate')
|
|
21
|
+
? platform.config.cloudRefreshRate
|
|
22
|
+
: platformConsts.defaultValues.cloudRefreshRate
|
|
23
|
+
const localRefreshRate = hasProperty(platform.config, 'refreshRate')
|
|
24
|
+
? platform.config.refreshRate
|
|
25
|
+
: platformConsts.defaultValues.refreshRate
|
|
26
|
+
this.pollInterval = accessory.context.connection === 'local'
|
|
27
|
+
? localRefreshRate
|
|
28
|
+
: cloudRefreshRate
|
|
29
|
+
|
|
30
|
+
// Add the outlet services for each channel
|
|
31
|
+
for (let i = 0; i < accessory.context.channelCount; i += 1) {
|
|
32
|
+
if (i !== 0) {
|
|
33
|
+
const outletService = this.accessory.getService(`outlet-${i}`)
|
|
34
|
+
if (!outletService) {
|
|
35
|
+
const channelName = accessory.context.channels[i]?.devName || `Outlet ${i}`
|
|
36
|
+
this.accessory.addService(this.hapServ.Outlet, channelName, `outlet-${i}`)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Add the set handler to the switch on/off characteristic for each channel
|
|
42
|
+
for (let i = 1; i < accessory.context.channelCount; i += 1) {
|
|
43
|
+
const outletService = this.accessory.getService(`outlet-${i}`)
|
|
44
|
+
outletService
|
|
45
|
+
.getCharacteristic(this.hapChar.On)
|
|
46
|
+
.onSet(async value => this.internalStateUpdate(value, i))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Create the queue used for sending device requests
|
|
50
|
+
this.updateInProgress = false
|
|
51
|
+
this.queue = new PQueue({
|
|
52
|
+
concurrency: 1,
|
|
53
|
+
interval: 250,
|
|
54
|
+
intervalCap: 1,
|
|
55
|
+
timeout: 10000,
|
|
56
|
+
throwOnTimeout: true,
|
|
57
|
+
})
|
|
58
|
+
this.queue.on('idle', () => {
|
|
59
|
+
this.updateInProgress = false
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
// Set up the mqtt client for cloud devices to send and receive device updates
|
|
63
|
+
if (accessory.context.connection !== 'local') {
|
|
64
|
+
this.accessory.mqtt = new mqttClient(platform, this.accessory)
|
|
65
|
+
this.accessory.mqtt.connect()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Always request a device update on startup, then start the interval for polling
|
|
69
|
+
setTimeout(() => this.requestUpdate(true), 2000)
|
|
70
|
+
this.accessory.refreshInterval = setInterval(
|
|
71
|
+
() => this.requestUpdate(),
|
|
72
|
+
this.pollInterval * 1000,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
// Output the customised options to the log
|
|
76
|
+
const opts = JSON.stringify({
|
|
77
|
+
connection: this.accessory.context.connection,
|
|
78
|
+
showAs: 'power-strip',
|
|
79
|
+
})
|
|
80
|
+
platform.log('[%s] %s %s.', this.name, platformLang.devInitOpts, opts)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async internalStateUpdate(value, channel) {
|
|
84
|
+
try {
|
|
85
|
+
// Add the request to the queue so updates are sent apart
|
|
86
|
+
await this.queue.add(async () => {
|
|
87
|
+
// Don't continue if the state is the same as before
|
|
88
|
+
if (value === this.accessory.getService(`outlet-${channel}`)
|
|
89
|
+
.getCharacteristic(this.hapChar.On)
|
|
90
|
+
.value) {
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// This flag stops the plugin from requesting updates while pending on others
|
|
95
|
+
this.updateInProgress = true
|
|
96
|
+
|
|
97
|
+
// Generate the payload and namespace for the correct device model
|
|
98
|
+
const namespace = 'Appliance.Control.ToggleX'
|
|
99
|
+
const payload = {
|
|
100
|
+
togglex: {
|
|
101
|
+
onoff: value ? 1 : 0,
|
|
102
|
+
channel,
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Use the platform function to send the update to the device
|
|
107
|
+
await this.platform.sendUpdate(this.accessory, {
|
|
108
|
+
namespace,
|
|
109
|
+
payload,
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
// Update the cache and log the update has been successful
|
|
113
|
+
this.accessory.getService(`outlet-${channel}`)
|
|
114
|
+
.getCharacteristic(this.hapChar.On)
|
|
115
|
+
.updateValue(value)
|
|
116
|
+
|
|
117
|
+
this.accessory.log(`${platformLang.curState} [${value ? 'on' : 'off'}]`)
|
|
118
|
+
})
|
|
119
|
+
} catch (err) {
|
|
120
|
+
// Catch any errors whilst updating the device
|
|
121
|
+
const eText = err instanceof TimeoutError ? platformLang.timeout : parseError(err)
|
|
122
|
+
this.accessory.logWarn(`${platformLang.sendFailed} ${eText}`)
|
|
123
|
+
setTimeout(() => {
|
|
124
|
+
this.accessory.getService(`outlet-${channel}`)
|
|
125
|
+
.getCharacteristic(this.hapChar.On)
|
|
126
|
+
.updateValue(
|
|
127
|
+
this.accessory.getService(`outlet-${channel}`).getCharacteristic(this.hapChar.On).value,
|
|
128
|
+
)
|
|
129
|
+
}, 2000)
|
|
130
|
+
throw new this.hapErr(-70402)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async requestUpdate(firstRun = false) {
|
|
135
|
+
try {
|
|
136
|
+
// Don't continue if an update is currently being sent to the device
|
|
137
|
+
if (this.updateInProgress) {
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Add the request to the queue so updates are sent apart
|
|
142
|
+
await this.queue.add(async () => {
|
|
143
|
+
// This flag stops the plugin from requesting updates while pending on others
|
|
144
|
+
this.updateInProgress = true
|
|
145
|
+
|
|
146
|
+
// Send the request
|
|
147
|
+
const res = await this.platform.sendUpdate(this.accessory, {
|
|
148
|
+
namespace: 'Appliance.System.All',
|
|
149
|
+
payload: {},
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
// Log the received data
|
|
153
|
+
this.accessory.logDebug(`${platformLang.incPoll}: ${JSON.stringify(res.data)}`)
|
|
154
|
+
|
|
155
|
+
// Check the response is in a useful format
|
|
156
|
+
const data = res.data.payload
|
|
157
|
+
if (data.all) {
|
|
158
|
+
if (data.all.digest && data.all.digest.togglex && Array.isArray(data.all.digest.togglex)) {
|
|
159
|
+
this.applyUpdate(data.all.digest.togglex)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// A flag to check if we need to update the accessory context
|
|
163
|
+
let needsUpdate = false
|
|
164
|
+
|
|
165
|
+
// Get the mac address and hardware version of the device
|
|
166
|
+
if (data.all.system) {
|
|
167
|
+
// Mac address and hardware don't change regularly so only get on first poll
|
|
168
|
+
if (firstRun && data.all.system.hardware) {
|
|
169
|
+
this.accessory.context.macAddress = data.all.system.hardware.macAddress.toUpperCase()
|
|
170
|
+
this.accessory.context.hardware = data.all.system.hardware.version
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Get the ip address and firmware of the device
|
|
174
|
+
if (data.all.system.firmware) {
|
|
175
|
+
// Check for an IP change each and every time the device is polled
|
|
176
|
+
if (this.accessory.context.ipAddress !== data.all.system.firmware.innerIp) {
|
|
177
|
+
this.accessory.context.ipAddress = data.all.system.firmware.innerIp
|
|
178
|
+
needsUpdate = true
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Firmware doesn't change regularly so only get on first poll
|
|
182
|
+
if (firstRun) {
|
|
183
|
+
this.accessory.context.firmware = data.all.system.firmware.version
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Get the cloud online status of the device
|
|
189
|
+
if (data.all.system.online) {
|
|
190
|
+
const isOnline = data.all.system.online.status === 1
|
|
191
|
+
if (this.accessory.context.isOnline !== isOnline) {
|
|
192
|
+
this.accessory.context.isOnline = isOnline
|
|
193
|
+
needsUpdate = true
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Update the accessory cache if anything has changed
|
|
198
|
+
if (needsUpdate || firstRun) {
|
|
199
|
+
this.accessory.context = {
|
|
200
|
+
...this.accessory.context,
|
|
201
|
+
|
|
202
|
+
macAddress: this.accessory.context.macAddress,
|
|
203
|
+
hardware: this.accessory.context.hardware,
|
|
204
|
+
ipAddress: this.accessory.context.ipAddress,
|
|
205
|
+
firmware: this.accessory.context.firmware,
|
|
206
|
+
isOnline: this.accessory.context.isOnline
|
|
207
|
+
,
|
|
208
|
+
}
|
|
209
|
+
this.platform.updateAccessory(this.accessory)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
})
|
|
213
|
+
} catch (err) {
|
|
214
|
+
const eText = err instanceof TimeoutError ? platformLang.timeout : parseError(err)
|
|
215
|
+
this.accessory.logDebugWarn(`${platformLang.reqFailed}: ${eText}`)
|
|
216
|
+
|
|
217
|
+
// Set the homebridge-ui status of the device to offline if local and error is timeout
|
|
218
|
+
if (
|
|
219
|
+
(this.accessory.context.isOnline || firstRun)
|
|
220
|
+
&& ['EHOSTUNREACH', 'timed out'].some(el => eText.includes(el))
|
|
221
|
+
) {
|
|
222
|
+
this.accessory.context.isOnline = false
|
|
223
|
+
this.platform.updateAccessory(this.accessory)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
receiveUpdate(params) {
|
|
229
|
+
try {
|
|
230
|
+
// Log the received data
|
|
231
|
+
this.accessory.logDebug(`${platformLang.incMQTT}: ${JSON.stringify(params)}`)
|
|
232
|
+
|
|
233
|
+
// Validate the response, checking for payload property
|
|
234
|
+
if (!params.payload) {
|
|
235
|
+
throw new Error('invalid response received')
|
|
236
|
+
}
|
|
237
|
+
const data = params.payload
|
|
238
|
+
|
|
239
|
+
// Check the data is in a format which contains the value we need
|
|
240
|
+
if (data.togglex) {
|
|
241
|
+
// payload.togglex can either be an array of objects (multiple channels) or a single object
|
|
242
|
+
// Either way, push all items into one array
|
|
243
|
+
const toUpdate = []
|
|
244
|
+
if (Array.isArray(data.togglex)) {
|
|
245
|
+
data.togglex.forEach(item => toUpdate.push(item))
|
|
246
|
+
} else {
|
|
247
|
+
toUpdate.push(data.togglex)
|
|
248
|
+
}
|
|
249
|
+
this.applyUpdate(toUpdate)
|
|
250
|
+
}
|
|
251
|
+
} catch (err) {
|
|
252
|
+
this.accessory.logWarn(`${platformLang.refFailed} ${parseError(err)}`)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
applyUpdate(data) {
|
|
257
|
+
data.forEach((channel) => {
|
|
258
|
+
if (channel.channel === 0) {
|
|
259
|
+
return
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Attempt to find the accessory this channel relates to
|
|
263
|
+
const { accessory } = this
|
|
264
|
+
|
|
265
|
+
// Obtain the service and current value
|
|
266
|
+
const hapServ = accessory.getService(`outlet-${channel.channel}`)
|
|
267
|
+
const hapChar = hapServ.getCharacteristic(this.hapChar.On)
|
|
268
|
+
|
|
269
|
+
// Read the current state
|
|
270
|
+
const newState = channel.onoff === 1
|
|
271
|
+
|
|
272
|
+
// Don't continue if the state is the same as before
|
|
273
|
+
if (hapChar.value === newState) {
|
|
274
|
+
return
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Update the HomeKit characteristics and log
|
|
278
|
+
hapChar.updateValue(newState)
|
|
279
|
+
this.accessory.log(`${platformLang.curState} [${newState ? 'on' : 'off'}]`)
|
|
280
|
+
})
|
|
281
|
+
}
|
|
282
|
+
}
|