@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
package/lib/platform.js
ADDED
|
@@ -0,0 +1,1256 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import { existsSync, mkdirSync } from 'node:fs'
|
|
3
|
+
import { createRequire } from 'node:module'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import process from 'node:process'
|
|
6
|
+
|
|
7
|
+
import axios from 'axios'
|
|
8
|
+
import storage from 'node-persist'
|
|
9
|
+
|
|
10
|
+
import httpClient from './connection/http.js'
|
|
11
|
+
import deviceTypes from './device/index.js'
|
|
12
|
+
import eveService from './fakegato/fakegato-history.js'
|
|
13
|
+
import platformConsts from './utils/constants.js'
|
|
14
|
+
import platformChars from './utils/custom-chars.js'
|
|
15
|
+
import eveChars from './utils/eve-chars.js'
|
|
16
|
+
import { generateRandomString, hasProperty, parseError } from './utils/functions.js'
|
|
17
|
+
import platformLang from './utils/lang-en.js'
|
|
18
|
+
|
|
19
|
+
const require = createRequire(import.meta.url)
|
|
20
|
+
const plugin = require('../package.json')
|
|
21
|
+
|
|
22
|
+
export default class {
|
|
23
|
+
constructor(log, config, api) {
|
|
24
|
+
if (!log || !api) {
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Begin plugin initialisation
|
|
29
|
+
try {
|
|
30
|
+
this.api = api
|
|
31
|
+
this.log = log
|
|
32
|
+
this.isBeta = plugin.version.includes('beta')
|
|
33
|
+
this.cloudClient = false
|
|
34
|
+
this.deviceConf = {}
|
|
35
|
+
this.devicesInHB = new Map()
|
|
36
|
+
this.hideChannels = []
|
|
37
|
+
this.hideMasters = []
|
|
38
|
+
this.ignoredDevices = []
|
|
39
|
+
this.localUUIDs = []
|
|
40
|
+
|
|
41
|
+
// Make sure user is running Homebridge v1.4 or above
|
|
42
|
+
if (!api?.versionGreaterOrEqual('1.4.0')) {
|
|
43
|
+
throw new Error(platformLang.hbVersionFail)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Check the user has configured the plugin
|
|
47
|
+
if (!config) {
|
|
48
|
+
throw new Error(platformLang.pluginNotConf)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Log some environment info for debugging
|
|
52
|
+
this.log(
|
|
53
|
+
'%s v%s | System %s | Node %s | HB v%s | HAPNodeJS v%s...',
|
|
54
|
+
platformLang.initialising,
|
|
55
|
+
plugin.version,
|
|
56
|
+
process.platform,
|
|
57
|
+
process.version,
|
|
58
|
+
api.serverVersion,
|
|
59
|
+
api.hap.HAPLibraryVersion(),
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
// Apply the user's configuration
|
|
63
|
+
this.config = platformConsts.defaultConfig
|
|
64
|
+
this.applyUserConfig(config)
|
|
65
|
+
|
|
66
|
+
// Set up the Homebridge events
|
|
67
|
+
this.api.on('didFinishLaunching', () => this.pluginSetup())
|
|
68
|
+
this.api.on('shutdown', () => this.pluginShutdown())
|
|
69
|
+
} catch (err) {
|
|
70
|
+
// Catch any errors during initialisation
|
|
71
|
+
const eText = parseError(err, [platformLang.hbVersionFail, platformLang.pluginNotConf])
|
|
72
|
+
log.warn('***** %s. *****', platformLang.disabling)
|
|
73
|
+
log.warn('***** %s. *****', eText)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
applyUserConfig(config) {
|
|
78
|
+
// These shorthand functions save line space during config parsing
|
|
79
|
+
const logDefault = (k, def) => {
|
|
80
|
+
this.log.warn('%s [%s] %s %s.', platformLang.cfgItem, k, platformLang.cfgDef, def)
|
|
81
|
+
}
|
|
82
|
+
const logDuplicate = (k) => {
|
|
83
|
+
this.log.warn('%s [%s] %s.', platformLang.cfgItem, k, platformLang.cfgDup)
|
|
84
|
+
}
|
|
85
|
+
const logIgnore = (k) => {
|
|
86
|
+
this.log.warn('%s [%s] %s.', platformLang.cfgItem, k, platformLang.cfgIgn)
|
|
87
|
+
}
|
|
88
|
+
const logIgnoreItem = (k) => {
|
|
89
|
+
this.log.warn('%s [%s] %s.', platformLang.cfgItem, k, platformLang.cfgIgnItem)
|
|
90
|
+
}
|
|
91
|
+
const logIncrease = (k, min) => {
|
|
92
|
+
this.log.warn('%s [%s] %s %s.', platformLang.cfgItem, k, platformLang.cfgLow, min)
|
|
93
|
+
}
|
|
94
|
+
const logQuotes = (k) => {
|
|
95
|
+
this.log.warn('%s [%s] %s.', platformLang.cfgItem, k, platformLang.cfgQts)
|
|
96
|
+
}
|
|
97
|
+
const logRemove = (k) => {
|
|
98
|
+
this.log.warn('%s [%s] %s.', platformLang.cfgItem, k, platformLang.cfgRmv)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Begin applying the user's config
|
|
102
|
+
Object.entries(config).forEach((entry) => {
|
|
103
|
+
const [key, val] = entry
|
|
104
|
+
switch (key) {
|
|
105
|
+
case 'babyDevices':
|
|
106
|
+
case 'diffuserDevices':
|
|
107
|
+
case 'fanDevices':
|
|
108
|
+
case 'garageDevices':
|
|
109
|
+
case 'humidifierDevices':
|
|
110
|
+
case 'lightDevices':
|
|
111
|
+
case 'multiDevices':
|
|
112
|
+
case 'purifierDevices':
|
|
113
|
+
case 'rollerDevices':
|
|
114
|
+
case 'sensorDevices':
|
|
115
|
+
case 'singleDevices':
|
|
116
|
+
case 'thermostatDevices':
|
|
117
|
+
if (Array.isArray(val) && val.length > 0) {
|
|
118
|
+
val.forEach((x) => {
|
|
119
|
+
if (
|
|
120
|
+
!(
|
|
121
|
+
x.serialNumber
|
|
122
|
+
&& x.name
|
|
123
|
+
&& (
|
|
124
|
+
(config.connection !== 'local' && x.connection !== 'local')
|
|
125
|
+
|| (config.connection !== 'local' && x.connection === 'local' && x.deviceUrl)
|
|
126
|
+
|| (config.connection === 'local' && x.model && x.deviceUrl)
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
) {
|
|
130
|
+
logIgnoreItem(key)
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
const id = x.serialNumber.toLowerCase().replace(/[^a-z\d]+/g, '')
|
|
134
|
+
if (Object.keys(this.deviceConf).includes(id)) {
|
|
135
|
+
logDuplicate(`${key}.${id}`)
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
const entries = Object.entries(x)
|
|
139
|
+
if (entries.length < 3) {
|
|
140
|
+
logRemove(`${key}.${id}`)
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
this.deviceConf[id] = {}
|
|
144
|
+
entries.forEach((subEntry) => {
|
|
145
|
+
const [k, v] = subEntry
|
|
146
|
+
switch (k) {
|
|
147
|
+
case 'adaptiveLightingShift':
|
|
148
|
+
case 'brightnessStep':
|
|
149
|
+
case 'garageDoorOpeningTime':
|
|
150
|
+
case 'inUsePowerThreshold':
|
|
151
|
+
case 'lowBattThreshold': {
|
|
152
|
+
if (typeof v === 'string') {
|
|
153
|
+
logQuotes(`${key}.${id}.${k}`)
|
|
154
|
+
}
|
|
155
|
+
const intVal = Number.parseInt(v, 10)
|
|
156
|
+
if (Number.isNaN(intVal)) {
|
|
157
|
+
logDefault(`${key}.${id}.${k}`, platformConsts.defaultValues[k])
|
|
158
|
+
this.deviceConf[id][k] = platformConsts.defaultValues[k]
|
|
159
|
+
} else if (intVal < platformConsts.minValues[k]) {
|
|
160
|
+
logIncrease(`${key}.${id}.${k}`, platformConsts.minValues[k])
|
|
161
|
+
this.deviceConf[id][k] = platformConsts.minValues[k]
|
|
162
|
+
} else {
|
|
163
|
+
this.deviceConf[id][k] = intVal
|
|
164
|
+
}
|
|
165
|
+
break
|
|
166
|
+
}
|
|
167
|
+
case 'connection':
|
|
168
|
+
case 'showAs': {
|
|
169
|
+
const inSet = platformConsts.allowed[k].includes(v)
|
|
170
|
+
if (typeof v !== 'string' || !inSet) {
|
|
171
|
+
logIgnore(`${key}.${id}.${k}`)
|
|
172
|
+
} else {
|
|
173
|
+
this.deviceConf[id][k] = v === 'default' ? platformConsts.defaultValues[k] : v
|
|
174
|
+
}
|
|
175
|
+
break
|
|
176
|
+
}
|
|
177
|
+
case 'deviceUrl':
|
|
178
|
+
case 'firmwareRevision':
|
|
179
|
+
case 'ignoreSubdevices':
|
|
180
|
+
case 'model':
|
|
181
|
+
case 'name':
|
|
182
|
+
case 'serialNumber':
|
|
183
|
+
case 'temperatureSource':
|
|
184
|
+
case 'userkey':
|
|
185
|
+
if (typeof v !== 'string' || v === '') {
|
|
186
|
+
logIgnore(`${key}.${id}.${k}`)
|
|
187
|
+
} else {
|
|
188
|
+
this.deviceConf[id][k] = v.trim()
|
|
189
|
+
if (k === 'deviceUrl') {
|
|
190
|
+
this.localUUIDs.push(id)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
break
|
|
194
|
+
case 'hideChannels': {
|
|
195
|
+
if (typeof v !== 'string' || v === '') {
|
|
196
|
+
logIgnore(`${key}.${id}.${k}`)
|
|
197
|
+
} else {
|
|
198
|
+
const channels = v.split(',')
|
|
199
|
+
channels.forEach((channel) => {
|
|
200
|
+
this.hideChannels.push(id + channel.replace(/\D+/g, ''))
|
|
201
|
+
this.deviceConf[id][k] = v
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
break
|
|
205
|
+
}
|
|
206
|
+
case 'ignoreDevice':
|
|
207
|
+
if (typeof v === 'string') {
|
|
208
|
+
logQuotes(`${key}.${id}.${k}`)
|
|
209
|
+
}
|
|
210
|
+
if (!!v && v !== 'false') {
|
|
211
|
+
this.ignoredDevices.push(id)
|
|
212
|
+
}
|
|
213
|
+
break
|
|
214
|
+
case 'reversePolarity':
|
|
215
|
+
if (typeof v === 'string') {
|
|
216
|
+
logQuotes(`${key}.${id}.${k}`)
|
|
217
|
+
}
|
|
218
|
+
this.deviceConf[id][k] = v === 'false' ? false : !!v
|
|
219
|
+
break
|
|
220
|
+
default:
|
|
221
|
+
logRemove(`${key}.${id}.${k}`)
|
|
222
|
+
}
|
|
223
|
+
})
|
|
224
|
+
})
|
|
225
|
+
} else {
|
|
226
|
+
logIgnore(key)
|
|
227
|
+
}
|
|
228
|
+
break
|
|
229
|
+
case 'cloudRefreshRate':
|
|
230
|
+
case 'refreshRate': {
|
|
231
|
+
if (typeof val === 'string') {
|
|
232
|
+
logQuotes(key)
|
|
233
|
+
}
|
|
234
|
+
const intVal = Number.parseInt(val, 10)
|
|
235
|
+
if (Number.isNaN(intVal)) {
|
|
236
|
+
logDefault(key, platformConsts.defaultValues[key])
|
|
237
|
+
} else if (intVal !== 0 && intVal < platformConsts.minValues[key]) {
|
|
238
|
+
logIncrease(key, platformConsts.minValues[key])
|
|
239
|
+
} else if (intVal === 0 || intVal > 600) {
|
|
240
|
+
this.config[key] = 600
|
|
241
|
+
} else {
|
|
242
|
+
this.config[key] = intVal
|
|
243
|
+
}
|
|
244
|
+
break
|
|
245
|
+
}
|
|
246
|
+
case 'connection': {
|
|
247
|
+
const inSet = platformConsts.allowed[key].includes(val)
|
|
248
|
+
if (typeof val !== 'string' || !inSet) {
|
|
249
|
+
logIgnore(key)
|
|
250
|
+
} else {
|
|
251
|
+
this.config[key] = val === 'default' ? platformConsts.defaultValues[key] : val
|
|
252
|
+
}
|
|
253
|
+
break
|
|
254
|
+
}
|
|
255
|
+
case 'disableDeviceLogging':
|
|
256
|
+
case 'ignoreHKNative':
|
|
257
|
+
case 'ignoreMatter':
|
|
258
|
+
case 'showUserKey':
|
|
259
|
+
if (typeof val === 'string') {
|
|
260
|
+
logQuotes(key)
|
|
261
|
+
}
|
|
262
|
+
this.config[key] = val === 'false' ? false : !!val
|
|
263
|
+
break
|
|
264
|
+
case 'domain':
|
|
265
|
+
case 'mfaCode':
|
|
266
|
+
case 'password':
|
|
267
|
+
case 'username':
|
|
268
|
+
if (typeof val !== 'string') {
|
|
269
|
+
logIgnore(key)
|
|
270
|
+
} else {
|
|
271
|
+
this.config[key] = val
|
|
272
|
+
}
|
|
273
|
+
break
|
|
274
|
+
case 'name':
|
|
275
|
+
case 'platform':
|
|
276
|
+
break
|
|
277
|
+
case 'userkey':
|
|
278
|
+
if (typeof val !== 'string') {
|
|
279
|
+
logIgnore(key)
|
|
280
|
+
} else {
|
|
281
|
+
const userkey = val.toLowerCase().replace(/[^a-z\d]+/g, '')
|
|
282
|
+
if (userkey.length === 32) {
|
|
283
|
+
this.config[key] = userkey
|
|
284
|
+
} else {
|
|
285
|
+
logIgnore(key)
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
break
|
|
289
|
+
default:
|
|
290
|
+
logRemove(key)
|
|
291
|
+
break
|
|
292
|
+
}
|
|
293
|
+
})
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async pluginSetup() {
|
|
297
|
+
// Plugin has finished initialising so now onto setup
|
|
298
|
+
try {
|
|
299
|
+
// Log that the plugin initialisation has been successful
|
|
300
|
+
this.log('%s.', platformLang.initialised)
|
|
301
|
+
|
|
302
|
+
// Sort out some logging functions
|
|
303
|
+
if (this.isBeta) {
|
|
304
|
+
this.log.debug = this.log
|
|
305
|
+
this.log.debugWarn = this.log.warn
|
|
306
|
+
|
|
307
|
+
// Log that using a beta will generate a lot of debug logs
|
|
308
|
+
if (this.isBeta) {
|
|
309
|
+
const divide = '*'.repeat(platformLang.beta.length + 1) // don't forget the full stop (+1!)
|
|
310
|
+
this.log.warn(divide)
|
|
311
|
+
this.log.warn(`${platformLang.beta}.`)
|
|
312
|
+
this.log.warn(divide)
|
|
313
|
+
}
|
|
314
|
+
} else {
|
|
315
|
+
this.log.debug = () => {}
|
|
316
|
+
this.log.debugWarn = () => {}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Require any libraries that the accessory instances use
|
|
320
|
+
this.cusChar = new platformChars(this.api)
|
|
321
|
+
this.eveChar = new eveChars(this.api)
|
|
322
|
+
this.eveService = eveService(this.api)
|
|
323
|
+
|
|
324
|
+
const cachePath = join(this.api.user.storagePath(), '/bwp91_cache')
|
|
325
|
+
|
|
326
|
+
// Create folders if they don't exist
|
|
327
|
+
if (!existsSync(cachePath)) {
|
|
328
|
+
mkdirSync(cachePath)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Persist files are used to store device info that can be used by my other plugins
|
|
332
|
+
try {
|
|
333
|
+
this.storageData = storage.create({
|
|
334
|
+
dir: cachePath,
|
|
335
|
+
forgiveParseErrors: true,
|
|
336
|
+
})
|
|
337
|
+
await this.storageData.init()
|
|
338
|
+
this.storageClientData = true
|
|
339
|
+
} catch (err) {
|
|
340
|
+
this.log.debugWarn('%s %s.', platformLang.storageSetupErr, parseError(err))
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// If the user has configured cloud username and password then get a device list
|
|
344
|
+
this.accountDetails = {}
|
|
345
|
+
let cloudDevices = []
|
|
346
|
+
try {
|
|
347
|
+
if (!this.config.username || !this.config.password) {
|
|
348
|
+
throw new Error(platformLang.missingCreds)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Try and get token from the cache to get a device list
|
|
352
|
+
try {
|
|
353
|
+
const storedData = await this.storageData.getItem('Meross_All_Devices_temp')
|
|
354
|
+
const splitData = storedData?.split(':::')
|
|
355
|
+
if (!Array.isArray(splitData) || splitData.length !== 5) {
|
|
356
|
+
throw new Error(platformLang.accTokenNoExist)
|
|
357
|
+
}
|
|
358
|
+
if (splitData[0] !== this.config.username) {
|
|
359
|
+
// Username has changed so throw error to generate new token
|
|
360
|
+
throw new Error(platformLang.accTokenUserChange)
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
this.accountDetails.key = splitData[1]
|
|
364
|
+
this.accountDetails.token = splitData[2]
|
|
365
|
+
this.accountDetails.userId = splitData[3]
|
|
366
|
+
this.accountDetails.domain = splitData[4]
|
|
367
|
+
|
|
368
|
+
this.log.debug('[HTTP] %s.', platformLang.accTokenFromCache)
|
|
369
|
+
|
|
370
|
+
this.cloudClient = new httpClient(this)
|
|
371
|
+
cloudDevices = await this.cloudClient.getDevices()
|
|
372
|
+
} catch (err) {
|
|
373
|
+
this.log.warn('[HTTP] %s %s.', platformLang.accTokenFail, parseError(err, [
|
|
374
|
+
platformLang.accTokenUserChange,
|
|
375
|
+
platformLang.accTokenNoExist,
|
|
376
|
+
platformLang.accTokenInvalid,
|
|
377
|
+
]))
|
|
378
|
+
|
|
379
|
+
// Remove existing cache info if it exists
|
|
380
|
+
await this.storageData.removeItem('Meross_All_Devices_temp')
|
|
381
|
+
|
|
382
|
+
this.cloudClient = new httpClient(this)
|
|
383
|
+
this.accountDetails = await this.cloudClient.login()
|
|
384
|
+
cloudDevices = await this.cloudClient.getDevices()
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Initialise the cloud configured devices into Homebridge
|
|
388
|
+
cloudDevices.forEach(device => this.initialiseDevice(device))
|
|
389
|
+
} catch (err) {
|
|
390
|
+
const eText = parseError(err, [platformLang.mfaFail, platformLang.missingCreds, platformLang.accTokenInvalid])
|
|
391
|
+
this.log.warn('%s %s.', platformLang.disablingCloud, eText)
|
|
392
|
+
this.cloudClient = false
|
|
393
|
+
this.accountDetails = {
|
|
394
|
+
key: this.config.userkey,
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Check if a user key has been configured if the credentials aren't present
|
|
399
|
+
if (!this.cloudClient) {
|
|
400
|
+
if (this.config.userkey) {
|
|
401
|
+
// Initialise the local configured devices into Homebridge
|
|
402
|
+
Object.values(this.deviceConf)
|
|
403
|
+
.filter(el => el.deviceUrl)
|
|
404
|
+
.forEach((device) => {
|
|
405
|
+
// Ensure we have a model property if a user key is configured, and credentials are not
|
|
406
|
+
if (!this.config.username && this.config.userkey && !device.model) {
|
|
407
|
+
this.log.warn('[%s] %s.', device.name, platformLang.missingModal)
|
|
408
|
+
return
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Rename some properties to fit the format of a cloud device
|
|
412
|
+
// Local devices don't have the uuid already set
|
|
413
|
+
device.uuid = device.serialNumber
|
|
414
|
+
device.deviceType = device.model.toUpperCase().replace(/-+/g, '')
|
|
415
|
+
device.devName = device.name
|
|
416
|
+
device.channels = []
|
|
417
|
+
|
|
418
|
+
// Retrieve how many channels this device has
|
|
419
|
+
const garageCount = device.deviceType === 'MSG200' ? 3 : 1
|
|
420
|
+
const channelCount = platformConsts.models.switchMulti[device.deviceType] || garageCount
|
|
421
|
+
|
|
422
|
+
// Create a list of channels to fit the format of a cloud device
|
|
423
|
+
if (channelCount > 1) {
|
|
424
|
+
for (let index = 0; index <= channelCount; index += 1) {
|
|
425
|
+
device.channels.push({})
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
this.initialiseDevice(device)
|
|
429
|
+
})
|
|
430
|
+
} else {
|
|
431
|
+
// Cloud client disabled and no user key - plugin will be useless
|
|
432
|
+
throw new Error(platformLang.noCredentials)
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Check for redundant accessories or those that have been ignored but exist
|
|
437
|
+
this.devicesInHB.forEach((accessory) => {
|
|
438
|
+
switch (accessory.context.connection) {
|
|
439
|
+
case 'cloud':
|
|
440
|
+
case 'hybrid':
|
|
441
|
+
if (!cloudDevices.some(el => el.uuid === accessory.context.serialNumber)) {
|
|
442
|
+
this.removeAccessory(accessory)
|
|
443
|
+
}
|
|
444
|
+
break
|
|
445
|
+
case 'local':
|
|
446
|
+
if (!this.localUUIDs.includes(accessory.context.serialNumber)) {
|
|
447
|
+
this.removeAccessory(accessory)
|
|
448
|
+
}
|
|
449
|
+
break
|
|
450
|
+
default:
|
|
451
|
+
// Should never happen
|
|
452
|
+
this.removeAccessory(accessory)
|
|
453
|
+
break
|
|
454
|
+
}
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
// Setup successful
|
|
458
|
+
this.log('%s. %s', platformLang.complete, platformLang.welcome)
|
|
459
|
+
} catch (err) {
|
|
460
|
+
// Catch any errors during setup
|
|
461
|
+
const eText = parseError(err, [platformLang.noCredentials])
|
|
462
|
+
this.log.warn('***** %s. *****', platformLang.disabling)
|
|
463
|
+
this.log.warn('***** %s. *****', eText)
|
|
464
|
+
this.pluginShutdown()
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
pluginShutdown() {
|
|
469
|
+
// A function that is called when the plugin fails to load or Homebridge restarts
|
|
470
|
+
try {
|
|
471
|
+
// Close the mqtt connection for the accessories with an open connection
|
|
472
|
+
if (this.cloudClient) {
|
|
473
|
+
this.devicesInHB.forEach((accessory) => {
|
|
474
|
+
if (accessory.mqtt) {
|
|
475
|
+
accessory.mqtt.disconnect()
|
|
476
|
+
}
|
|
477
|
+
if (accessory.refreshInterval) {
|
|
478
|
+
clearInterval(accessory.refreshInterval)
|
|
479
|
+
}
|
|
480
|
+
if (accessory.powerInterval) {
|
|
481
|
+
clearInterval(accessory.powerInterval)
|
|
482
|
+
}
|
|
483
|
+
})
|
|
484
|
+
}
|
|
485
|
+
} catch (err) {
|
|
486
|
+
// No need to show errors at this point
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
applyAccessoryLogging(accessory) {
|
|
491
|
+
if (this.isBeta) {
|
|
492
|
+
accessory.log = msg => this.log('[%s] %s.', accessory.displayName, msg)
|
|
493
|
+
accessory.logWarn = msg => this.log.warn('[%s] %s.', accessory.displayName, msg)
|
|
494
|
+
accessory.logDebug = msg => this.log('[%s] %s.', accessory.displayName, msg)
|
|
495
|
+
accessory.logDebugWarn = msg => this.log.warn('[%s] %s.', accessory.displayName, msg)
|
|
496
|
+
} else {
|
|
497
|
+
if (this.config.disableDeviceLogging) {
|
|
498
|
+
accessory.log = () => {}
|
|
499
|
+
accessory.logWarn = () => {}
|
|
500
|
+
} else {
|
|
501
|
+
accessory.log = msg => this.log('[%s] %s.', accessory.displayName, msg)
|
|
502
|
+
accessory.logWarn = msg => this.log.warn('[%s] %s.', accessory.displayName, msg)
|
|
503
|
+
}
|
|
504
|
+
accessory.logDebug = () => {}
|
|
505
|
+
accessory.logDebugWarn = () => {}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
async initialiseDevice(device) {
|
|
510
|
+
try {
|
|
511
|
+
// Get any user configured entry for this device
|
|
512
|
+
const deviceConf = this.deviceConf[device.uuid.toLowerCase()] || {}
|
|
513
|
+
|
|
514
|
+
// Generate a unique id for the accessory
|
|
515
|
+
const hbUUID = this.api.hap.uuid.generate(device.uuid)
|
|
516
|
+
device.firmware = deviceConf.firmwareRevision || device.fmwareVersion
|
|
517
|
+
device.hbDeviceId = device.uuid
|
|
518
|
+
device.model = device.deviceType.toUpperCase().replace(/-+/g, '')
|
|
519
|
+
|
|
520
|
+
// Add context information for the plugin-ui and instance to use
|
|
521
|
+
const context = {
|
|
522
|
+
channel: 0,
|
|
523
|
+
channelCount: device.channels.length,
|
|
524
|
+
connection: deviceConf.deviceUrl
|
|
525
|
+
? 'local'
|
|
526
|
+
: deviceConf.connection || this.config.connection,
|
|
527
|
+
deviceUrl: deviceConf.deviceUrl,
|
|
528
|
+
domain: device.domain,
|
|
529
|
+
firmware: device.firmware,
|
|
530
|
+
hidden: false,
|
|
531
|
+
isOnline: false,
|
|
532
|
+
model: device.model,
|
|
533
|
+
options: deviceConf,
|
|
534
|
+
serialNumber: device.uuid,
|
|
535
|
+
userkey: deviceConf.userkey || this.accountDetails.key,
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Find the correct instance determined by the device model
|
|
539
|
+
let accessory
|
|
540
|
+
if (platformConsts.models.switchSingle.includes(device.model)) {
|
|
541
|
+
/**
|
|
542
|
+
**************
|
|
543
|
+
SWITCHES (SINGLE)
|
|
544
|
+
***************
|
|
545
|
+
*/
|
|
546
|
+
// Set up the accessory and instance
|
|
547
|
+
accessory = this.devicesInHB.get(hbUUID) || this.addAccessory(device)
|
|
548
|
+
accessory.context = { ...accessory.context, ...context }
|
|
549
|
+
this.applyAccessoryLogging(accessory)
|
|
550
|
+
switch (deviceConf.showAs) {
|
|
551
|
+
case 'cooler':
|
|
552
|
+
accessory.control = new deviceTypes.deviceCoolerSingle(this, accessory)
|
|
553
|
+
break
|
|
554
|
+
case 'heater':
|
|
555
|
+
accessory.control = new deviceTypes.deviceHeaterSingle(this, accessory)
|
|
556
|
+
break
|
|
557
|
+
case 'outlet':
|
|
558
|
+
accessory.control = new deviceTypes.deviceOutletSingle(this, accessory)
|
|
559
|
+
break
|
|
560
|
+
case 'purifier':
|
|
561
|
+
accessory.control = new deviceTypes.devicePurifierSingle(this, accessory)
|
|
562
|
+
break
|
|
563
|
+
default:
|
|
564
|
+
accessory.control = new deviceTypes.deviceSwitchSingle(this, accessory)
|
|
565
|
+
}
|
|
566
|
+
/** */
|
|
567
|
+
} else if (hasProperty(platformConsts.models.switchMulti, device.model)) {
|
|
568
|
+
/**
|
|
569
|
+
*************
|
|
570
|
+
SWITCHES (MULTI)
|
|
571
|
+
**************
|
|
572
|
+
*/
|
|
573
|
+
// If the user has enabled the option to configure multi-outlet devices as power strips
|
|
574
|
+
if (deviceConf.showAs === 'power-strip') {
|
|
575
|
+
// Set up the main power strip accessory
|
|
576
|
+
accessory = this.devicesInHB.get(hbUUID) || this.addAccessory(device)
|
|
577
|
+
accessory.context = { ...accessory.context, ...context, channels: device.channels }
|
|
578
|
+
this.applyAccessoryLogging(accessory)
|
|
579
|
+
accessory.control = new deviceTypes.devicePowerStrip(this, accessory)
|
|
580
|
+
|
|
581
|
+
// Check to see if there are any leftover accessory instances from before enabling this option
|
|
582
|
+
for (let i = 0; i <= 7; i += 1) {
|
|
583
|
+
const uuidSub = device.uuid + i
|
|
584
|
+
const hbUUIDSub = this.api.hap.uuid.generate(uuidSub)
|
|
585
|
+
if (this.devicesInHB.has(hbUUIDSub)) {
|
|
586
|
+
this.removeAccessory(this.devicesInHB.get(hbUUIDSub))
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
} else {
|
|
590
|
+
// Check to see if we need to remove any leftover power strip accessory instances
|
|
591
|
+
if (this.devicesInHB.has(hbUUID)) {
|
|
592
|
+
this.removeAccessory(this.devicesInHB.get(hbUUID))
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Loop through the channels
|
|
596
|
+
device.channels.forEach((channel, index) => {
|
|
597
|
+
const subdeviceObj = { ...device }
|
|
598
|
+
const extraContext = {}
|
|
599
|
+
|
|
600
|
+
// Generate the Homebridge UUID from the device uuid and channel index
|
|
601
|
+
const uuidSub = device.uuid + index
|
|
602
|
+
subdeviceObj.hbDeviceId = uuidSub
|
|
603
|
+
const hbUUIDSub = this.api.hap.uuid.generate(uuidSub)
|
|
604
|
+
|
|
605
|
+
// Supply a device name for the channel accessories
|
|
606
|
+
if (index > 0) {
|
|
607
|
+
subdeviceObj.devName = channel.devName || `${device.devName} SW${index}`
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// Check if the user has chosen to hide any channels for this device
|
|
611
|
+
let subAcc
|
|
612
|
+
if (this.hideChannels.includes(device.uuid + index)) {
|
|
613
|
+
// The user has hidden this channel so if it exists then remove it
|
|
614
|
+
if (this.devicesInHB.has(hbUUIDSub)) {
|
|
615
|
+
this.removeAccessory(this.devicesInHB.get(hbUUIDSub))
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// If this is the main channel then add it to the array of hidden masters
|
|
619
|
+
if (index === 0) {
|
|
620
|
+
this.hideMasters.push(device.uuid)
|
|
621
|
+
|
|
622
|
+
// Add the sub accessory, but hidden, to Homebridge
|
|
623
|
+
extraContext.hidden = true
|
|
624
|
+
subAcc = this.addAccessory(subdeviceObj, true)
|
|
625
|
+
} else {
|
|
626
|
+
return
|
|
627
|
+
}
|
|
628
|
+
} else {
|
|
629
|
+
// The user has not hidden this channel
|
|
630
|
+
subAcc = this.devicesInHB.get(hbUUIDSub) || this.addAccessory(subdeviceObj)
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// Add the context information to the accessory
|
|
634
|
+
extraContext.channel = index
|
|
635
|
+
subAcc.context = { ...subAcc.context, ...context, ...extraContext }
|
|
636
|
+
this.applyAccessoryLogging(subAcc)
|
|
637
|
+
|
|
638
|
+
// Create the device type instance for this accessory
|
|
639
|
+
switch (deviceConf.showAs) {
|
|
640
|
+
case 'outlet':
|
|
641
|
+
subAcc.control = new deviceTypes.deviceOutletMulti(this, subAcc)
|
|
642
|
+
break
|
|
643
|
+
default:
|
|
644
|
+
subAcc.control = new deviceTypes.deviceSwitchMulti(this, subAcc)
|
|
645
|
+
break
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// This is used for later in this function for logging
|
|
649
|
+
if (index === 0) {
|
|
650
|
+
accessory = subAcc
|
|
651
|
+
} else {
|
|
652
|
+
// Update any changes to the accessory to the platform
|
|
653
|
+
this.api.updatePlatformAccessories([subAcc])
|
|
654
|
+
this.devicesInHB.set(subAcc.UUID, subAcc)
|
|
655
|
+
}
|
|
656
|
+
})
|
|
657
|
+
}
|
|
658
|
+
/** */
|
|
659
|
+
} else if (platformConsts.models.lightDimmer.includes(device.model)) {
|
|
660
|
+
/**
|
|
661
|
+
************
|
|
662
|
+
LIGHTS (DIMMER)
|
|
663
|
+
*************
|
|
664
|
+
*/
|
|
665
|
+
accessory = this.devicesInHB.get(hbUUID) || this.addAccessory(device)
|
|
666
|
+
accessory.context = { ...accessory.context, ...context }
|
|
667
|
+
this.applyAccessoryLogging(accessory)
|
|
668
|
+
accessory.control = new deviceTypes.deviceLightDimmer(this, accessory)
|
|
669
|
+
/** */
|
|
670
|
+
} else if (platformConsts.models.lightRGB.includes(device.model)) {
|
|
671
|
+
/**
|
|
672
|
+
*********
|
|
673
|
+
LIGHTS (RGB)
|
|
674
|
+
**********
|
|
675
|
+
*/
|
|
676
|
+
accessory = this.devicesInHB.get(hbUUID) || this.addAccessory(device)
|
|
677
|
+
accessory.context = { ...accessory.context, ...context }
|
|
678
|
+
this.applyAccessoryLogging(accessory)
|
|
679
|
+
accessory.control = new deviceTypes.deviceLightRGB(this, accessory)
|
|
680
|
+
/** */
|
|
681
|
+
} else if (platformConsts.models.lightCCT.includes(device.model)) {
|
|
682
|
+
/**
|
|
683
|
+
*********
|
|
684
|
+
LIGHTS (CCT)
|
|
685
|
+
**********
|
|
686
|
+
*/
|
|
687
|
+
accessory = this.devicesInHB.get(hbUUID) || this.addAccessory(device)
|
|
688
|
+
accessory.context = { ...accessory.context, ...context }
|
|
689
|
+
this.applyAccessoryLogging(accessory)
|
|
690
|
+
accessory.control = new deviceTypes.deviceLightCCT(this, accessory)
|
|
691
|
+
/** */
|
|
692
|
+
} else if (platformConsts.models.garage.includes(device.model)) {
|
|
693
|
+
/**
|
|
694
|
+
*********
|
|
695
|
+
GARAGE DOORS
|
|
696
|
+
**********
|
|
697
|
+
*/
|
|
698
|
+
if (device.model === 'MSG200') {
|
|
699
|
+
// If a main accessory exists from before then remove it so re-added as hidden
|
|
700
|
+
if (this.devicesInHB.has(hbUUID)) {
|
|
701
|
+
this.removeAccessory(this.devicesInHB.get(hbUUID))
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// First, set up the main, hidden, accessory that will process the control and updates
|
|
705
|
+
accessory = this.addAccessory(device, true)
|
|
706
|
+
accessory.context = { ...accessory.context, ...context, hidden: true }
|
|
707
|
+
this.applyAccessoryLogging(accessory)
|
|
708
|
+
accessory.control = new deviceTypes.deviceGarageMain(this, accessory)
|
|
709
|
+
|
|
710
|
+
// Loop through the channels
|
|
711
|
+
device.channels.forEach((channel, index) => {
|
|
712
|
+
// Skip the channel 0 entry
|
|
713
|
+
if (index === 0) {
|
|
714
|
+
return
|
|
715
|
+
}
|
|
716
|
+
const subdeviceObj = { ...device }
|
|
717
|
+
const extraContext = {}
|
|
718
|
+
|
|
719
|
+
// Generate the Homebridge UUID from the device uuid and channel index
|
|
720
|
+
const uuidSub = device.uuid + index
|
|
721
|
+
subdeviceObj.hbDeviceId = uuidSub
|
|
722
|
+
const hbUUIDSub = this.api.hap.uuid.generate(uuidSub)
|
|
723
|
+
|
|
724
|
+
// Supply a device name for the channel accessories
|
|
725
|
+
if (index > 0) {
|
|
726
|
+
device.devName = channel.devName || `${device.devName} SW${index}`
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// Check if the user has chosen to hide any channels for this device
|
|
730
|
+
if (this.hideChannels.includes(device.uuid + index)) {
|
|
731
|
+
// The user has hidden this channel so if it exists then remove it
|
|
732
|
+
if (this.devicesInHB.has(hbUUIDSub)) {
|
|
733
|
+
this.removeAccessory(this.devicesInHB.get(hbUUIDSub))
|
|
734
|
+
}
|
|
735
|
+
return
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// The user has not hidden this channel
|
|
739
|
+
const subAcc = this.devicesInHB.get(hbUUIDSub) || this.addAccessory(subdeviceObj)
|
|
740
|
+
|
|
741
|
+
// Add the context information to the accessory
|
|
742
|
+
extraContext.channel = index
|
|
743
|
+
subAcc.context = { ...subAcc.context, ...context, ...extraContext }
|
|
744
|
+
this.applyAccessoryLogging(subAcc)
|
|
745
|
+
|
|
746
|
+
// Create the device type instance for this accessory
|
|
747
|
+
subAcc.control = new deviceTypes.deviceGarageSub(this, subAcc, accessory)
|
|
748
|
+
|
|
749
|
+
// Update any changes to the accessory to the platform
|
|
750
|
+
this.api.updatePlatformAccessories([subAcc])
|
|
751
|
+
this.devicesInHB.set(subAcc.UUID, subAcc)
|
|
752
|
+
})
|
|
753
|
+
} else {
|
|
754
|
+
accessory = this.devicesInHB.get(hbUUID) || this.addAccessory(device)
|
|
755
|
+
accessory.context = { ...accessory.context, ...context }
|
|
756
|
+
this.applyAccessoryLogging(accessory)
|
|
757
|
+
accessory.control = new deviceTypes.deviceGarageSingle(this, accessory)
|
|
758
|
+
}
|
|
759
|
+
/** */
|
|
760
|
+
} else if (platformConsts.models.roller.includes(device.model)) {
|
|
761
|
+
/**
|
|
762
|
+
***********
|
|
763
|
+
ROLLING MOTORS
|
|
764
|
+
************
|
|
765
|
+
*/
|
|
766
|
+
accessory = this.devicesInHB.get(hbUUID) || this.addAccessory(device)
|
|
767
|
+
accessory.context = { ...accessory.context, ...context }
|
|
768
|
+
this.applyAccessoryLogging(accessory)
|
|
769
|
+
accessory.control = ['6.0.0', '7.0.0', '8.0.0'].includes(device.hdwareVersion)
|
|
770
|
+
? new deviceTypes.deviceRollerLocation(this, accessory)
|
|
771
|
+
: new deviceTypes.deviceRoller(this, accessory)
|
|
772
|
+
/** */
|
|
773
|
+
} else if (platformConsts.models.purifier.includes(device.model)) {
|
|
774
|
+
/**
|
|
775
|
+
******
|
|
776
|
+
PURIFIERS
|
|
777
|
+
*******
|
|
778
|
+
*/
|
|
779
|
+
accessory = this.devicesInHB.get(hbUUID) || this.addAccessory(device)
|
|
780
|
+
accessory.context = { ...accessory.context, ...context }
|
|
781
|
+
this.applyAccessoryLogging(accessory)
|
|
782
|
+
accessory.control = new deviceTypes.devicePurifier(this, accessory)
|
|
783
|
+
/** */
|
|
784
|
+
} else if (platformConsts.models.fan.includes(device.model)) {
|
|
785
|
+
/**
|
|
786
|
+
FANS
|
|
787
|
+
*
|
|
788
|
+
*/
|
|
789
|
+
accessory = this.devicesInHB.get(hbUUID) || this.addAccessory(device)
|
|
790
|
+
accessory.context = { ...accessory.context, ...context }
|
|
791
|
+
this.applyAccessoryLogging(accessory)
|
|
792
|
+
accessory.control = new deviceTypes.deviceFan(this, accessory)
|
|
793
|
+
/** */
|
|
794
|
+
} else if (platformConsts.models.diffuser.includes(device.model)) {
|
|
795
|
+
/**
|
|
796
|
+
******
|
|
797
|
+
DIFFUSERS
|
|
798
|
+
*******
|
|
799
|
+
*/
|
|
800
|
+
accessory = this.devicesInHB.get(hbUUID) || this.addAccessory(device)
|
|
801
|
+
accessory.context = { ...accessory.context, ...context }
|
|
802
|
+
this.applyAccessoryLogging(accessory)
|
|
803
|
+
accessory.control = new deviceTypes.deviceDiffuser(this, accessory)
|
|
804
|
+
/** */
|
|
805
|
+
} else if (platformConsts.models.humidifier.includes(device.model)) {
|
|
806
|
+
/**
|
|
807
|
+
********
|
|
808
|
+
HUMIDIFIERS
|
|
809
|
+
*********
|
|
810
|
+
*/
|
|
811
|
+
accessory = this.devicesInHB.get(hbUUID) || this.addAccessory(device)
|
|
812
|
+
accessory.context = { ...accessory.context, ...context }
|
|
813
|
+
this.applyAccessoryLogging(accessory)
|
|
814
|
+
accessory.control = new deviceTypes.deviceHumidifier(this, accessory)
|
|
815
|
+
/** */
|
|
816
|
+
} else if (platformConsts.models.baby.includes(device.model)) {
|
|
817
|
+
/**
|
|
818
|
+
**********
|
|
819
|
+
BABY MONITORS
|
|
820
|
+
***********
|
|
821
|
+
*/
|
|
822
|
+
accessory = this.addExternalAccessory(device, 26)
|
|
823
|
+
accessory.context = { ...accessory.context, ...context }
|
|
824
|
+
this.applyAccessoryLogging(accessory)
|
|
825
|
+
|
|
826
|
+
// Create a second accessory for the baby light
|
|
827
|
+
const deviceLightHBID = `${device.uuid}_light`
|
|
828
|
+
const deviceLightHBUUID = this.api.hap.uuid.generate(deviceLightHBID)
|
|
829
|
+
const deviceLight = {
|
|
830
|
+
...device,
|
|
831
|
+
hbDeviceId: deviceLightHBID,
|
|
832
|
+
}
|
|
833
|
+
const accessoryLight = this.devicesInHB.get(deviceLightHBUUID) || this.addAccessory(deviceLight)
|
|
834
|
+
accessoryLight.context = { ...accessory.context, ...context }
|
|
835
|
+
this.applyAccessoryLogging(accessoryLight)
|
|
836
|
+
|
|
837
|
+
// Update any changes to the accessory to the platform
|
|
838
|
+
this.api.updatePlatformAccessories([accessoryLight])
|
|
839
|
+
this.devicesInHB.set(accessoryLight.UUID, accessoryLight)
|
|
840
|
+
|
|
841
|
+
// Set up the main accessory for the baby monitor
|
|
842
|
+
accessory.control = new deviceTypes.deviceBaby(this, accessory, accessoryLight)
|
|
843
|
+
/** */
|
|
844
|
+
} else if (platformConsts.models.thermostat.includes(device.model)) {
|
|
845
|
+
/**
|
|
846
|
+
********
|
|
847
|
+
THERMOSTATS
|
|
848
|
+
*********
|
|
849
|
+
*/
|
|
850
|
+
accessory = this.devicesInHB.get(hbUUID) || this.addAccessory(device)
|
|
851
|
+
accessory.context = { ...accessory.context, ...context }
|
|
852
|
+
this.applyAccessoryLogging(accessory)
|
|
853
|
+
accessory.control = new deviceTypes.deviceThermostat(this, accessory)
|
|
854
|
+
/** */
|
|
855
|
+
} else if (platformConsts.models.hubMain.includes(device.model)) {
|
|
856
|
+
/**
|
|
857
|
+
********
|
|
858
|
+
SENSOR HUBS
|
|
859
|
+
*********
|
|
860
|
+
*/
|
|
861
|
+
// At the moment, cloud connection is necessary to get a subdevice list
|
|
862
|
+
if (!this.cloudClient) {
|
|
863
|
+
throw new Error(platformLang.sensorNoCloud)
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// Obtain array of any subdevices to ignore
|
|
867
|
+
const subdevicesToIgnore = []
|
|
868
|
+
if (context.options.ignoreSubdevices) {
|
|
869
|
+
context.options.ignoreSubdevices
|
|
870
|
+
.split(',')
|
|
871
|
+
.forEach(subdeviceId => subdevicesToIgnore.push(subdeviceId.trim()))
|
|
872
|
+
}
|
|
873
|
+
context.ignoreSubdevices = subdevicesToIgnore
|
|
874
|
+
|
|
875
|
+
// First, set up the main, hidden, accessory that will process the incoming updates
|
|
876
|
+
accessory = this.addAccessory(device, true)
|
|
877
|
+
accessory.context = { ...accessory.context, ...context, hidden: true }
|
|
878
|
+
this.applyAccessoryLogging(accessory)
|
|
879
|
+
accessory.control = new deviceTypes.deviceHubMain(this, accessory)
|
|
880
|
+
|
|
881
|
+
// Then request and initialise a list of subdevices
|
|
882
|
+
const subdevices = await this.cloudClient.getSubDevices(device)
|
|
883
|
+
if (!Array.isArray(subdevices)) {
|
|
884
|
+
throw new TypeError(platformLang.sensorNoSubs)
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// Initialise subdevices into HB
|
|
888
|
+
subdevices.forEach((subdevice) => {
|
|
889
|
+
try {
|
|
890
|
+
// Create an object to mimic the addAccessory data
|
|
891
|
+
const subdeviceObj = { ...device }
|
|
892
|
+
const uuidSub = device.uuid + subdevice.subDeviceId
|
|
893
|
+
const hbUUIDSub = this.api.hap.uuid.generate(uuidSub)
|
|
894
|
+
|
|
895
|
+
// Check if it's ignored device
|
|
896
|
+
if (subdevicesToIgnore.includes(subdevice.subDeviceId)) {
|
|
897
|
+
// Is ignored, remove if exists
|
|
898
|
+
if (this.devicesInHB.has(hbUUIDSub)) {
|
|
899
|
+
this.removeAccessory(this.devicesInHB.get(hbUUIDSub))
|
|
900
|
+
}
|
|
901
|
+
return
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// Not ignored, so continue initialising
|
|
905
|
+
subdeviceObj.devName = subdevice.subDeviceName || subdevice.subDeviceId
|
|
906
|
+
subdeviceObj.hbDeviceId = uuidSub
|
|
907
|
+
subdeviceObj.model = subdevice.subDeviceType.toUpperCase().replace(/-+/g, '')
|
|
908
|
+
|
|
909
|
+
// Check the subdevice model is supported
|
|
910
|
+
if (!platformConsts.models.hubSub.includes(subdeviceObj.model)) {
|
|
911
|
+
// Not supported, so show a log message with helpful info for a GitHub issue
|
|
912
|
+
this.log.warn(
|
|
913
|
+
'[%s] %s:\n%s',
|
|
914
|
+
subdeviceObj.devName,
|
|
915
|
+
platformLang.notSupp,
|
|
916
|
+
JSON.stringify(subdeviceObj),
|
|
917
|
+
)
|
|
918
|
+
return
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// Obtain or add this subdevice to Homebridge
|
|
922
|
+
const subAcc = this.devicesInHB.get(hbUUIDSub) || this.addAccessory(subdeviceObj)
|
|
923
|
+
|
|
924
|
+
// Add helpful context info to the accessory object
|
|
925
|
+
subAcc.context = {
|
|
926
|
+
...subAcc.context,
|
|
927
|
+
...context,
|
|
928
|
+
subSerialNumber: subdevice.subDeviceId,
|
|
929
|
+
}
|
|
930
|
+
this.applyAccessoryLogging(subAcc)
|
|
931
|
+
|
|
932
|
+
// Create the device type instance for this accessory
|
|
933
|
+
switch (subdeviceObj.model) {
|
|
934
|
+
case 'GS559A':
|
|
935
|
+
subAcc.control = new deviceTypes.deviceHubSmoke(this, subAcc)
|
|
936
|
+
break
|
|
937
|
+
case 'MS100':
|
|
938
|
+
case 'MS100F':
|
|
939
|
+
subAcc.control = new deviceTypes.deviceHubSensor(this, subAcc)
|
|
940
|
+
break
|
|
941
|
+
case 'MS200':
|
|
942
|
+
subAcc.control = new deviceTypes.deviceHubContact(this, subAcc)
|
|
943
|
+
break
|
|
944
|
+
case 'MS400':
|
|
945
|
+
subAcc.control = new deviceTypes.deviceHubLeak(this, subAcc)
|
|
946
|
+
break
|
|
947
|
+
case 'MTS100V3':
|
|
948
|
+
case 'MTS150':
|
|
949
|
+
subAcc.control = new deviceTypes.deviceHubValve(this, subAcc, accessory)
|
|
950
|
+
break
|
|
951
|
+
default:
|
|
952
|
+
return
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// Update any changes to the accessory to the platform
|
|
956
|
+
this.api.updatePlatformAccessories([subAcc])
|
|
957
|
+
this.devicesInHB.set(subAcc.UUID, subAcc)
|
|
958
|
+
|
|
959
|
+
// Log the subdevice id so a user can use it to ignore device if wanted
|
|
960
|
+
this.log(
|
|
961
|
+
'[%s] [%s] %s [%s].',
|
|
962
|
+
device.devName,
|
|
963
|
+
subdeviceObj.devName,
|
|
964
|
+
platformLang.devSubInit,
|
|
965
|
+
subdevice.subDeviceId,
|
|
966
|
+
)
|
|
967
|
+
} catch (err) {
|
|
968
|
+
this.log.warn('[%s] %s %s.', subdevice.subDeviceName, platformLang.devNotAdd, parseError(err))
|
|
969
|
+
}
|
|
970
|
+
})
|
|
971
|
+
/** */
|
|
972
|
+
} else if (platformConsts.models.sensorPresence.includes(device.model)) {
|
|
973
|
+
/**
|
|
974
|
+
*****************
|
|
975
|
+
SENSOR (PRESENCE)
|
|
976
|
+
*****************
|
|
977
|
+
*/
|
|
978
|
+
accessory = this.devicesInHB.get(hbUUID) || this.addAccessory(device)
|
|
979
|
+
accessory.context = { ...accessory.context, ...context }
|
|
980
|
+
this.applyAccessoryLogging(accessory)
|
|
981
|
+
accessory.control = new deviceTypes.deviceSensorPresence(this, accessory)
|
|
982
|
+
/** */
|
|
983
|
+
} else if (platformConsts.models.template.includes(device.model)) {
|
|
984
|
+
/**
|
|
985
|
+
****************
|
|
986
|
+
WORK IN PROGRESS
|
|
987
|
+
****************
|
|
988
|
+
*/
|
|
989
|
+
accessory = this.devicesInHB.get(hbUUID) || this.addAccessory(device)
|
|
990
|
+
accessory.context = { ...accessory.context, ...context }
|
|
991
|
+
this.applyAccessoryLogging(accessory)
|
|
992
|
+
accessory.control = new deviceTypes.deviceTemplate(this, accessory)
|
|
993
|
+
/** */
|
|
994
|
+
} else {
|
|
995
|
+
/**
|
|
996
|
+
*************
|
|
997
|
+
UNSUPPORTED YET
|
|
998
|
+
*************
|
|
999
|
+
*/
|
|
1000
|
+
this.log.warn('[%s] %s:\n%s', device.devName, platformLang.notSupp, JSON.stringify(device))
|
|
1001
|
+
return
|
|
1002
|
+
/** */
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// Log the device initialisation
|
|
1006
|
+
accessory.log(`${platformLang.devInit} [${device.uuid}]`)
|
|
1007
|
+
|
|
1008
|
+
// Extra debug logging when set, show the device JSON info
|
|
1009
|
+
accessory.logDebug(`${platformLang.jsonInfo}: ${JSON.stringify(device)}`)
|
|
1010
|
+
|
|
1011
|
+
// Update any changes to the accessory to the platform
|
|
1012
|
+
this.api.updatePlatformAccessories([accessory])
|
|
1013
|
+
this.devicesInHB.set(accessory.UUID, accessory)
|
|
1014
|
+
} catch (err) {
|
|
1015
|
+
// Catch any errors during device initialisation
|
|
1016
|
+
const eText = parseError(err, [
|
|
1017
|
+
platformLang.accNotFound,
|
|
1018
|
+
platformLang.sensorNoCloud,
|
|
1019
|
+
platformLang.sensorNoSubs,
|
|
1020
|
+
])
|
|
1021
|
+
this.log.warn('[%s] %s %s.', device.devName, platformLang.devNotInit, eText)
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
addAccessory(device, hidden = false) {
|
|
1026
|
+
// Add an accessory to Homebridge
|
|
1027
|
+
try {
|
|
1028
|
+
const accessory = new this.api.platformAccessory(
|
|
1029
|
+
device.devName,
|
|
1030
|
+
this.api.hap.uuid.generate(device.hbDeviceId),
|
|
1031
|
+
)
|
|
1032
|
+
|
|
1033
|
+
// If it isn't a hidden device then set the accessory characteristics
|
|
1034
|
+
if (!hidden) {
|
|
1035
|
+
accessory
|
|
1036
|
+
.getService(this.api.hap.Service.AccessoryInformation)
|
|
1037
|
+
.setCharacteristic(this.api.hap.Characteristic.Name, device.devName)
|
|
1038
|
+
.setCharacteristic(this.api.hap.Characteristic.ConfiguredName, device.devName)
|
|
1039
|
+
.setCharacteristic(this.api.hap.Characteristic.SerialNumber, device.uuid)
|
|
1040
|
+
.setCharacteristic(this.api.hap.Characteristic.Manufacturer, platformLang.brand)
|
|
1041
|
+
.setCharacteristic(this.api.hap.Characteristic.Model, device.model)
|
|
1042
|
+
.setCharacteristic(
|
|
1043
|
+
this.api.hap.Characteristic.FirmwareRevision,
|
|
1044
|
+
device.firmware || plugin.version,
|
|
1045
|
+
)
|
|
1046
|
+
.setCharacteristic(this.api.hap.Characteristic.Identify, true)
|
|
1047
|
+
|
|
1048
|
+
// Register the accessory if it hasn't been hidden by the user
|
|
1049
|
+
this.api.registerPlatformAccessories(plugin.name, plugin.alias, [accessory])
|
|
1050
|
+
this.log('[%s] %s.', device.devName, platformLang.devAdd)
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
// Configure for good practice
|
|
1054
|
+
this.configureAccessory(accessory)
|
|
1055
|
+
|
|
1056
|
+
// Return the new accessory
|
|
1057
|
+
return accessory
|
|
1058
|
+
} catch (err) {
|
|
1059
|
+
// Catch any errors during add
|
|
1060
|
+
this.log.warn('[%s] %s %s.', device.devName, platformLang.devNotAdd, parseError(err))
|
|
1061
|
+
return false
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
addExternalAccessory(device, category) {
|
|
1066
|
+
try {
|
|
1067
|
+
// Add the new accessory to Homebridge
|
|
1068
|
+
const accessory = new this.api.platformAccessory(
|
|
1069
|
+
device.devName,
|
|
1070
|
+
this.api.hap.uuid.generate(device.hbDeviceId),
|
|
1071
|
+
category,
|
|
1072
|
+
)
|
|
1073
|
+
|
|
1074
|
+
// Set the accessory characteristics
|
|
1075
|
+
accessory
|
|
1076
|
+
.getService(this.api.hap.Service.AccessoryInformation)
|
|
1077
|
+
.setCharacteristic(this.api.hap.Characteristic.Name, device.devName)
|
|
1078
|
+
.setCharacteristic(this.api.hap.Characteristic.ConfiguredName, device.devName)
|
|
1079
|
+
.setCharacteristic(this.api.hap.Characteristic.SerialNumber, device.uuid)
|
|
1080
|
+
.setCharacteristic(this.api.hap.Characteristic.Manufacturer, platformLang.brand)
|
|
1081
|
+
.setCharacteristic(this.api.hap.Characteristic.Model, device.model)
|
|
1082
|
+
.setCharacteristic(
|
|
1083
|
+
this.api.hap.Characteristic.FirmwareRevision,
|
|
1084
|
+
device.firmware || plugin.version,
|
|
1085
|
+
)
|
|
1086
|
+
.setCharacteristic(this.api.hap.Characteristic.Identify, true)
|
|
1087
|
+
|
|
1088
|
+
// Register the accessory
|
|
1089
|
+
this.api.publishExternalAccessories(plugin.name, [accessory])
|
|
1090
|
+
this.log('[%s] %s.', device.devName, platformLang.devAdd)
|
|
1091
|
+
|
|
1092
|
+
// Return the new accessory
|
|
1093
|
+
this.configureAccessory(accessory)
|
|
1094
|
+
return accessory
|
|
1095
|
+
} catch (err) {
|
|
1096
|
+
// Catch any errors during add
|
|
1097
|
+
this.log.warn('[%s] %s %s.', device.name, platformLang.devNotAdd, parseError(err))
|
|
1098
|
+
return false
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
configureAccessory(accessory) {
|
|
1103
|
+
// Set the correct firmware version if we can
|
|
1104
|
+
if (this.api && accessory.context.firmware) {
|
|
1105
|
+
accessory
|
|
1106
|
+
.getService(this.api.hap.Service.AccessoryInformation)
|
|
1107
|
+
.updateCharacteristic(
|
|
1108
|
+
this.api.hap.Characteristic.FirmwareRevision,
|
|
1109
|
+
accessory.context.firmware,
|
|
1110
|
+
)
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// Add the configured accessory to our global map
|
|
1114
|
+
this.devicesInHB.set(accessory.UUID, accessory)
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
updateAccessory(accessory) {
|
|
1118
|
+
this.api.updatePlatformAccessories([accessory])
|
|
1119
|
+
if (accessory.context.isOnline) {
|
|
1120
|
+
this.log('[%s] %s.', accessory.displayName, platformLang.repOnline)
|
|
1121
|
+
} else {
|
|
1122
|
+
this.log.warn('[%s] %s.', accessory.displayName, platformLang.repOffline)
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
removeAccessory(accessory) {
|
|
1127
|
+
try {
|
|
1128
|
+
// Remove an accessory from Homebridge
|
|
1129
|
+
if (!accessory.context.hidden) {
|
|
1130
|
+
this.api.unregisterPlatformAccessories(plugin.name, plugin.alias, [accessory])
|
|
1131
|
+
}
|
|
1132
|
+
this.devicesInHB.delete(accessory.UUID)
|
|
1133
|
+
this.log('[%s] %s.', accessory.displayName, platformLang.devRemove)
|
|
1134
|
+
} catch (err) {
|
|
1135
|
+
// Catch any errors during remove
|
|
1136
|
+
this.log.warn('[%s] %s %s.', accessory.displayName, platformLang.devNotRemove, parseError(err))
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
async sendUpdate(accessory, toSend) {
|
|
1141
|
+
// Variable res is the response from either the cloud mqtt update or local http request
|
|
1142
|
+
let res
|
|
1143
|
+
|
|
1144
|
+
// Generate the method variable determined from an empty payload or not
|
|
1145
|
+
toSend.method = toSend.method || (Object.keys(toSend.payload).length === 0 ? 'GET' : 'SET')
|
|
1146
|
+
|
|
1147
|
+
// Always try local control first, even for cloud devices
|
|
1148
|
+
try {
|
|
1149
|
+
// Check the user has this mode turned on
|
|
1150
|
+
if (accessory.context.connection === 'cloud') {
|
|
1151
|
+
throw new Error(platformLang.noHybridMode)
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// Check we have the user key
|
|
1155
|
+
if (!accessory.context.userkey) {
|
|
1156
|
+
throw new Error(platformLang.noUserKey)
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
// Certain models aren't supported for local control
|
|
1160
|
+
if (platformConsts.noLocalControl.includes(accessory.context.model)) {
|
|
1161
|
+
throw new Error(platformLang.notSuppLocal)
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
// Obtain the IP address, either manually configured or from Meross polling data
|
|
1165
|
+
const ipAddress = accessory.context.deviceUrl || accessory.context.ipAddress
|
|
1166
|
+
|
|
1167
|
+
// Check the IP address exists
|
|
1168
|
+
if (!ipAddress) {
|
|
1169
|
+
throw new Error(platformLang.noIP)
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
// Generate the timestamp, messageId and sign from the userkey
|
|
1173
|
+
const timestamp = Math.floor(Date.now() / 1000)
|
|
1174
|
+
const messageId = generateRandomString(32)
|
|
1175
|
+
const sign = createHash('md5')
|
|
1176
|
+
.update(messageId + accessory.context.userkey + timestamp)
|
|
1177
|
+
.digest('hex')
|
|
1178
|
+
|
|
1179
|
+
// Generate the payload to send
|
|
1180
|
+
const data = {
|
|
1181
|
+
header: {
|
|
1182
|
+
from: `http://${ipAddress}/config`,
|
|
1183
|
+
messageId,
|
|
1184
|
+
method: toSend.method,
|
|
1185
|
+
namespace: toSend.namespace,
|
|
1186
|
+
payloadVersion: 1,
|
|
1187
|
+
sign,
|
|
1188
|
+
timestamp,
|
|
1189
|
+
triggerSrc: 'iOSLocal',
|
|
1190
|
+
uuid: accessory.context.serialNumber,
|
|
1191
|
+
},
|
|
1192
|
+
payload: toSend.payload || {},
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
// Log the update if user enabled
|
|
1196
|
+
accessory.logDebug(`${platformLang.sendUpdate}: ${JSON.stringify(data)}`)
|
|
1197
|
+
|
|
1198
|
+
// Send the request to the device
|
|
1199
|
+
res = await axios({
|
|
1200
|
+
url: `http://${ipAddress}/config`,
|
|
1201
|
+
method: 'post',
|
|
1202
|
+
headers: { 'content-type': 'application/json' },
|
|
1203
|
+
data,
|
|
1204
|
+
responseType: 'json',
|
|
1205
|
+
timeout: toSend.method === 'GET' || accessory.context.connection === 'local' ? 9000 : 4000,
|
|
1206
|
+
})
|
|
1207
|
+
|
|
1208
|
+
// Check the response properties based on whether it is a control or request update
|
|
1209
|
+
switch (toSend.method) {
|
|
1210
|
+
case 'SET': {
|
|
1211
|
+
// Check the response
|
|
1212
|
+
if (!res.data || !res.data.header || res.data.header.method === 'ERROR') {
|
|
1213
|
+
throw new Error(`${platformLang.reqFail} - ${JSON.stringify(res.data.payload.error)}`)
|
|
1214
|
+
}
|
|
1215
|
+
break
|
|
1216
|
+
}
|
|
1217
|
+
default: { // GET
|
|
1218
|
+
// Validate the response, checking for payload property
|
|
1219
|
+
if (!res.data || !res.data.payload) {
|
|
1220
|
+
throw new Error(platformLang.invalidResponse)
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
// Check we are sending the command to the correct device
|
|
1224
|
+
if (
|
|
1225
|
+
res.data.header.from
|
|
1226
|
+
!== `/appliance/${accessory.context.serialNumber}/publish`
|
|
1227
|
+
) {
|
|
1228
|
+
throw new Error(platformLang.wrongDevice)
|
|
1229
|
+
}
|
|
1230
|
+
break
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
} catch (err) {
|
|
1234
|
+
if (accessory.context.connection === 'local') {
|
|
1235
|
+
// An error occurred and cloud mode is disabled so report the error back
|
|
1236
|
+
throw err
|
|
1237
|
+
} else {
|
|
1238
|
+
// An error occurred, so we can try sending the request via the cloud
|
|
1239
|
+
const eText = parseError(err, [
|
|
1240
|
+
platformLang.noHybridMode,
|
|
1241
|
+
platformLang.notSuppLocal,
|
|
1242
|
+
platformLang.noUserKey,
|
|
1243
|
+
platformLang.noIP,
|
|
1244
|
+
platformLang.wrongDevice,
|
|
1245
|
+
])
|
|
1246
|
+
accessory.logDebug(`${platformLang.revertToCloud} ${eText}`)
|
|
1247
|
+
|
|
1248
|
+
// Send the update via cloud mqtt
|
|
1249
|
+
res = await accessory.mqtt.sendUpdate(accessory, toSend)
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// Return the response
|
|
1254
|
+
return res
|
|
1255
|
+
}
|
|
1256
|
+
}
|