-
-
Notifications
You must be signed in to change notification settings - Fork 15
/
index.js
460 lines (404 loc) · 15.7 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
let Service, Characteristic, Accessory, UUID
const Leviton = require('./api.js')
const PLUGIN_NAME = 'homebridge-leviton'
const PLATFORM_NAME = 'LevitonDecoraSmart'
const levels = ['debug', 'info', 'warn', 'error']
class LevitonDecoraSmartPlatform {
constructor(log, config, api) {
this.config = config
this.api = api
this.accessories = []
const noop = function () {}
const logger = (level) => (msg) =>
levels.indexOf((config && levels.includes(config.loglevel) && config.loglevel) || 'info') <= levels.indexOf(level)
? log(msg)
: noop()
// create a level method for each on this.log
this.log = levels.reduce((a, l) => {
a[l] = logger(l)
return a
}, {})
if (config === null) {
this.log.error(`No config for ${PLUGIN_NAME} defined.`)
return
}
if (!config.email || !config.password) {
this.log.error(`email and password for ${PLUGIN_NAME} are required in config.json`)
return
}
// on launch, init api, iterate over new devices
api.on('didFinishLaunching', async () => {
this.log.debug('didFinishLaunching')
const { devices, token } = await this.initialize(config)
if (Array.isArray(devices) && devices.length > 0) {
devices.forEach((device) => {
if (!this.accessories.find((acc) => acc.context.device.serial === device.serial)) {
this.addAccessory(device, token)
}
})
} else {
this.log.error('Unable to initialize: no devices found')
}
})
}
subscriptionCallback(payload) {
const accessory = this.accessories.find((acc) => acc.context.device.id === payload.id)
const { id, power, brightness } = payload
this.log.debug(`Socket: ${accessory.displayName} (${id}): ${power} ${brightness ? `${brightness}%` : ''}`)
if (!accessory) return
const service =
accessory.getService(Service.Fan) ||
accessory.getService(Service.Switch) ||
accessory.getService(Service.Outlet) ||
accessory.getService(Service.Lightbulb)
const isFan = !!accessory.getService(Service.Fan)
if (payload.brightness)
service
.getCharacteristic(isFan ? Characteristic.RotationSpeed : Characteristic.Brightness)
.updateValue(payload.brightness)
service.getCharacteristic(Characteristic.On).updateValue(payload.power === 'ON')
}
// init function that sets up personID, accountID and residenceID to return token+devices
async initialize() {
this.log.debug('initialize')
try {
var login = await Leviton.postPersonLogin({
email: this.config['email'],
password: this.config['password'],
})
var { id: token, userId: personID } = login
this.log.debug(`personID: ${personID}, hasToken: ${!!token}`)
} catch (err) {
this.log.error(`Failed to login to leviton: ${err.message}`)
}
try {
const permissions = await Leviton.getPersonResidentialPermissions({
personID,
token,
})
var accountID = permissions[0].residentialAccountId
this.log.debug(`accountID: ${accountID}`)
} catch (err) {
this.log.error(`Failed to get leviton accountID: ${err.message}`)
}
try {
var { primaryResidenceId: residenceID, id: residenceObjectID } = await Leviton.getResidentialAccounts({
accountID,
token,
})
this.log.debug(`residenceID: ${residenceID}`)
} catch (err) {
this.log.error(`Failed to get leviton residenceID: ${err.message}`)
}
try {
var devices = await Leviton.getResidenceIotSwitches({
residenceID,
token,
})
this.log.debug(`devices: ${JSON.stringify(devices)}`)
} catch (err) {
this.log.error(`Failed to get leviton devices: ${err.message}`)
}
try {
if (!Array.isArray(devices) || devices.length < 1) {
this.log.info('No devices found for primary residence id. Trying residence v2')
const accountsV2Response = await Leviton.getResidentialAccountsV2({
residenceObjectID,
token,
})
if (accountsV2Response[0]) {
residenceID = accountsV2Response[0].id
devices = await Leviton.getResidenceIotSwitches({
residenceID,
token,
})
} else {
throw new Error('No residenceIDs found')
}
if (!Array.isArray(devices) || devices.length < 1) {
throw new Error(
`No devices found for residenceID: ${residenceID} or residenceIDV2 method: ${residenceObjectID}`
)
} else {
Leviton.subscribe(login, devices, this.subscriptionCallback.bind(this), this)
}
} else {
Leviton.subscribe(login, devices, this.subscriptionCallback.bind(this), this)
}
} catch (err) {
this.log.error(`Error subscribing devices to websocket updates: ${err.message}`)
}
return { devices, token }
}
// switch power state getter, closure with service, device and token
onGetPower(service, device, token) {
return function (callback) {
return Leviton.getIotSwitch({
switchID: device.id,
token,
})
.then((res) => {
this.log.debug(`onGetPower: ${device.name} ${res.power}`)
service.getCharacteristic(Characteristic.On).updateValue(res.power === 'ON')
callback(null, res.power === 'ON')
})
.catch((err) => {
this.log.error(`onGetPower error: ${err.message}`)
})
}
}
// switch power state setter, closure with service, device and token
onSetPower(service, device, token) {
return function (value, callback) {
return Leviton.putIotSwitch({
switchID: device.id,
power: value ? 'ON' : 'OFF',
token,
})
.then((res) => {
this.log.info(`onSetPower: ${device.name} ${res.power}`)
service.getCharacteristic(Characteristic.On).updateValue(res.power === 'ON')
callback()
})
.catch((err) => {
this.log.error(`onSetPower error: ${err.message}`)
})
}
}
// switch brightness getter closure with service, device and token
onGetBrightness(service, device, token) {
return function (callback) {
return Leviton.getIotSwitch({
switchID: device.id,
token,
})
.then((res) => {
this.log.debug(`onGetBrightness: ${device.name} @ ${res.brightness}%`)
service.getCharacteristic(Characteristic.Brightness).updateValue(res.brightness)
callback(null, res.brightness)
})
.catch((err) => {
this.log.error(`onGetBrightness error: ${err.message}`)
})
}
}
// switch brightness setter closure with service, device and token
onSetBrightness(service, device, token) {
return function (brightness, callback) {
return Leviton.putIotSwitch({
switchID: device.id,
brightness,
token,
})
.then((res) => {
this.log.info(`onSetBrightness: ${device.name} @ ${res.brightness}%`)
service.getCharacteristic(Characteristic.Brightness).updateValue(res.brightness)
callback()
})
.catch((err) => {
this.log.error(`onSetBrightness error: ${err.message}`)
})
}
}
// switch RotationSpeed getter closure with service, device and token
onGetRotationSpeed(service, device, token) {
return function (callback) {
return Leviton.getIotSwitch({
switchID: device.id,
token,
})
.then((res) => {
this.log.debug(`onGetRotationSpeed: ${device.name} @ ${res.brightness}%`)
service.getCharacteristic(Characteristic.RotationSpeed).updateValue(res.brightness)
callback(null, res.brightness)
})
.catch((err) => {
this.log.error(`onGetRotationSpeed error: ${err.message}`)
})
}
}
// switch RotationSpeed setter closure with service, device and token
onSetRotationSpeed(service, device, token) {
return function (brightness, callback) {
return Leviton.putIotSwitch({
switchID: device.id,
brightness,
token,
})
.then((res) => {
this.log.info(`onSetRotationSpeed: ${device.name} @ ${res.brightness}%`)
service.getCharacteristic(Characteristic.RotationSpeed).updateValue(res.brightness)
callback()
})
.catch((err) => {
this.log.error(`onSetRotationSpeed error: ${err.message}`)
})
}
}
async addAccessory(device, token) {
this.log.info(`addAccessory ${device.name}`)
// generate uuid based on device serial and create accessory
const uuid = UUID.generate(device.serial)
const accessory = new this.api.platformAccessory(device.name, uuid)
// save device and token information to context for later use
accessory.context.device = device
accessory.context.token = token
// save device info to AccessoryInformation service (which always exists?)
accessory
.getService(Service.AccessoryInformation)
.setCharacteristic(Characteristic.Name, device.name)
.setCharacteristic(Characteristic.SerialNumber, device.serial)
.setCharacteristic(Characteristic.Manufacturer, device.manufacturer)
.setCharacteristic(Characteristic.Model, device.model)
.setCharacteristic(Characteristic.FirmwareRevision, device.version)
// setupService adds services, characteristics and getters/setters
this.setupService(accessory)
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory])
// add configured accessory
this.accessories.push(accessory)
this.log.debug(`Finished adding accessory ${device.name}`)
}
// set up cached accessories
async configureAccessory(accessory) {
this.log.debug(`configureAccessory: ${accessory.displayName}`)
this.setupService(accessory)
this.accessories.push(accessory)
}
// fetch the status of a device to populate power state and brightness
async getStatus(device, token) {
this.log.debug(`getStatus: ${device.name}`)
return Leviton.getIotSwitch({
switchID: device.id,
token,
})
}
// setup service function
async setupService(accessory) {
this.log.debug(`setupService: ${accessory.displayName}`)
// get device and token out of context to update status
const device = accessory.context.device
const token = accessory.context.token
// Get the model number
this.log.debug(`Device Model: ${device.model}`)
switch (device.model) {
case 'DW4SF': // Fan Speed Control
this.setupFanService(accessory)
break
case 'DWVAA': // Voice Dimmer with Amazon Alexa
case 'DW1KD': // 1000W Dimmer
case 'DW6HD': // 600W Dimmer
case 'D26HD': // 600W Dimmer (2nd Gen)
case 'D23LP': // Plug-In Dimmer (2nd Gen)
case 'DW3HL': // Plug-In Dimmer
this.setupLightbulbService(accessory)
break
case 'DW15R': // Tamper Resistant Outlet
case 'DW15A': // Plug-in Outlet (1/2 HP)
case 'DW15P': // Plug-in Outlet (3/4 HP)
this.setupOutletService(accessory)
break
default:
// Set up anything else as a simple switch (i.e. - DW15S, etc)
this.setupSwitchService(accessory)
break
}
}
async setupSwitchService(accessory) {
this.log.debug(`Setting up device as Switch: ${accessory.displayName}`)
// get device and token out of context to update status
const device = accessory.context.device
const token = accessory.context.token
const status = await this.getStatus(device, token)
// get the accessory service, if not add it
const service =
accessory.getService(Service.Switch, device.name) || accessory.addService(Service.Switch, device.name)
// add handlers for on/off characteristic, set initial value
service
.getCharacteristic(Characteristic.On)
.on('get', this.onGetPower(service, device, token).bind(this))
.on('set', this.onSetPower(service, device, token).bind(this))
.updateValue(status.power === 'ON' ? true : false)
}
async setupOutletService(accessory) {
this.log.debug(`Setting up device as Outlet: ${accessory.displayName}`)
// get device and token out of context to update status
const device = accessory.context.device
const token = accessory.context.token
const status = await this.getStatus(device, token)
// get the accessory service, if not add it
const service =
accessory.getService(Service.Outlet, device.name) || accessory.addService(Service.Outlet, device.name)
// add handlers for on/off characteristic, set initial value
service
.getCharacteristic(Characteristic.On)
.on('get', this.onGetPower(service, device, token).bind(this))
.on('set', this.onSetPower(service, device, token).bind(this))
.updateValue(status.power === 'ON' ? true : false)
}
async setupLightbulbService(accessory) {
this.log.debug(`Setting up device as Lightbulb: ${accessory.displayName}`)
// get device and token out of context to update status
const device = accessory.context.device
const token = accessory.context.token
const status = await this.getStatus(device, token)
// get the accessory service, if not add it
const service =
accessory.getService(Service.Lightbulb, device.name) || accessory.addService(Service.Lightbulb, device.name)
// add handlers for on/off characteristic, set initial value
service
.getCharacteristic(Characteristic.On)
.on('get', this.onGetPower(service, device, token).bind(this))
.on('set', this.onSetPower(service, device, token).bind(this))
.updateValue(status.power === 'ON' ? true : false)
// set handlers for brightness, set initial value and min/max bounds
service
.getCharacteristic(Characteristic.Brightness)
.on('get', this.onGetBrightness(service, device, token).bind(this))
.on('set', this.onSetBrightness(service, device, token).bind(this))
.setProps({
minValue: status.minLevel,
maxValue: status.maxLevel,
minStep: 1,
})
.updateValue(status.brightness)
}
async setupFanService(accessory) {
this.log.debug(`Setting up device as Fan: ${accessory.displayName}`)
// get device and token out of context to update status
const device = accessory.context.device
const token = accessory.context.token
const status = await this.getStatus(device, token)
// get the accessory service, if not add it
const service = accessory.getService(Service.Fan, device.name) || accessory.addService(Service.Fan, device.name)
// add handlers for on/off characteristic, set initial value
service
.getCharacteristic(Characteristic.On)
.on('get', this.onGetPower(service, device, token).bind(this))
.on('set', this.onSetPower(service, device, token).bind(this))
.updateValue(status.power === 'ON' ? true : false)
// set handlers for brightness, set initial value and min/max bounds
service
.getCharacteristic(Characteristic.RotationSpeed)
.on('get', this.onGetRotationSpeed(service, device, token).bind(this))
.on('set', this.onSetRotationSpeed(service, device, token).bind(this))
.setProps({
minValue: 0,
maxValue: status.maxLevel,
minStep: status.minLevel,
})
.updateValue(status.brightness)
}
// remove accessories and unregister
removeAccessories() {
this.log.info('Removing all accessories')
this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, this.accessories)
this.accessories.splice(0, this.accessories.length)
}
}
module.exports = function (homebridge) {
Service = homebridge.hap.Service
Characteristic = homebridge.hap.Characteristic
Accessory = homebridge.hap.Accessory
UUID = homebridge.hap.uuid
homebridge.registerPlatform(PLUGIN_NAME, PLATFORM_NAME, LevitonDecoraSmartPlatform, true)
}