@benjaminmichoux/homebridge-leviton 2.2.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/README.md +56 -0
- package/api.js +118 -0
- package/config.schema.json +47 -0
- package/index.js +350 -0
- package/package.json +29 -0
package/README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# homebridge-leviton
|
|
2
|
+
|
|
3
|
+
[](https://github.com/homebridge/homebridge/wiki/Verified-Plugins)
|
|
4
|
+
Homebridge plugin for Leviton Decora Smart devices
|
|
5
|
+
|
|
6
|
+
## Supports
|
|
7
|
+
|
|
8
|
+
These models are tested, though any other WiFi model should work.
|
|
9
|
+
|
|
10
|
+
- DW6HD 600W Dimmer
|
|
11
|
+
- D26HD 600W Dimmer (2nd Gen)
|
|
12
|
+
- DW1KD 1000W Dimmer
|
|
13
|
+
- DW3HL Wi-Fi Plugin Dimmer
|
|
14
|
+
- D23LP Wi-Fi Plugin Dimmer (2nd Gen)
|
|
15
|
+
- DW15P Wi-Fi Plugin Outlet
|
|
16
|
+
- DW4SF Fan Speed Controller
|
|
17
|
+
|
|
18
|
+
## Requirements
|
|
19
|
+
|
|
20
|
+
- Node.js 18, 20, or 22
|
|
21
|
+
- Homebridge 1.8+ or 2.x
|
|
22
|
+
|
|
23
|
+
## Setup
|
|
24
|
+
|
|
25
|
+
_You must use the main "My Leviton" login credentials._
|
|
26
|
+
|
|
27
|
+
- add `homebridge-leviton` in your Homebridge Config UI X web interface
|
|
28
|
+
- Add to your config.json:
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
"platforms": [
|
|
32
|
+
{
|
|
33
|
+
"platform": "LevitonDecoraSmart",
|
|
34
|
+
"email": "your@email.com",
|
|
35
|
+
"password": "supersecretpassword",
|
|
36
|
+
"loglevel": "info",
|
|
37
|
+
"excludedModels": ["DWP15"],
|
|
38
|
+
"excludedSerials": ["1000_0023_CCE2"]
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
| Option | Required | Default | Description |
|
|
44
|
+
|--------|----------|---------|-------------|
|
|
45
|
+
| `email` | yes | — | My Leviton account email |
|
|
46
|
+
| `password` | yes | — | My Leviton account password |
|
|
47
|
+
| `loglevel` | no | `info` | `debug`, `info`, `warn`, or `error` |
|
|
48
|
+
| `excludedModels` | no | `[]` | Skip entire model families (e.g. fully HomeKit-native devices) |
|
|
49
|
+
| `excludedSerials` | no | `[]` | Skip individual devices by serial number |
|
|
50
|
+
|
|
51
|
+
## Features
|
|
52
|
+
|
|
53
|
+
- Automatically discovers devices on your My Leviton account
|
|
54
|
+
- On/Off, Brightness (with min/max limits), Fan speed
|
|
55
|
+
- Real-time state updates via WebSocket (auto-reconnects on connection drop)
|
|
56
|
+
- Shows serial/model/firmware in HomeKit accessory info
|
package/api.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
const SockJS = require('sockjs-client')
|
|
2
|
+
|
|
3
|
+
const baseURL = 'https://my.leviton.com/api'
|
|
4
|
+
const toQueryString = (params) =>
|
|
5
|
+
Object.keys(params)
|
|
6
|
+
.map((key) => `${key}=${params[key]}`)
|
|
7
|
+
.join('&')
|
|
8
|
+
|
|
9
|
+
function getResidenceIotSwitches({ residenceID, token }) {
|
|
10
|
+
return fetch(`${baseURL}/Residences/${residenceID}/iotSwitches`, {
|
|
11
|
+
method: 'GET',
|
|
12
|
+
headers: { 'Content-Type': 'application/json; charset=utf-8', Authorization: token },
|
|
13
|
+
}).then((res) => res.json())
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getIotSwitch({ switchID, token }) {
|
|
17
|
+
return fetch(`${baseURL}/IotSwitches/${switchID}`, {
|
|
18
|
+
method: 'GET',
|
|
19
|
+
headers: { 'Content-Type': 'application/json; charset=utf-8', Authorization: token },
|
|
20
|
+
}).then((res) => res.json())
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function putIotSwitch({ switchID, power, brightness, token }) {
|
|
24
|
+
const body = {}
|
|
25
|
+
if (brightness) body.brightness = brightness
|
|
26
|
+
if (power) body.power = power
|
|
27
|
+
return fetch(`${baseURL}/IotSwitches/${switchID}`, {
|
|
28
|
+
method: 'PUT',
|
|
29
|
+
body: JSON.stringify(body),
|
|
30
|
+
headers: { 'Content-Type': 'application/json; charset=utf-8', Authorization: token },
|
|
31
|
+
}).then((res) => res.json())
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getPersonResidentialPermissions({ personID, token }) {
|
|
35
|
+
return fetch(`${baseURL}/Person/${personID}/residentialPermissions`, {
|
|
36
|
+
method: 'GET',
|
|
37
|
+
headers: { 'Content-Type': 'application/json; charset=utf-8', Authorization: token },
|
|
38
|
+
}).then((res) => res.json())
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getResidentialAccounts({ accountID, token }) {
|
|
42
|
+
return fetch(`${baseURL}/ResidentialAccounts/${accountID}`, {
|
|
43
|
+
method: 'GET',
|
|
44
|
+
headers: { 'Content-Type': 'application/json; charset=utf-8', Authorization: token },
|
|
45
|
+
}).then((res) => res.json())
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getResidentialAccountsV2({ residenceObjectID, token }) {
|
|
49
|
+
return fetch(`${baseURL}/ResidentialAccounts/${residenceObjectID}/residences`, {
|
|
50
|
+
method: 'GET',
|
|
51
|
+
headers: { 'Content-Type': 'application/json; charset=utf-8', Authorization: token },
|
|
52
|
+
}).then((res) => res.json())
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function postPersonLogin({ email, password }) {
|
|
56
|
+
const query = toQueryString({ include: 'user' })
|
|
57
|
+
return fetch(`${baseURL}/Person/login?${query}`, {
|
|
58
|
+
method: 'POST',
|
|
59
|
+
body: JSON.stringify({ email, password }),
|
|
60
|
+
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
|
61
|
+
}).then((res) => res.json())
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function subscribe(login, devices, callback, scope, retryDelay = 5000) {
|
|
65
|
+
const ws = new SockJS('https://my.leviton.com/socket')
|
|
66
|
+
|
|
67
|
+
ws.onclose = function (ev) {
|
|
68
|
+
scope.log.error(`Socket connection closed: ${JSON.stringify(ev)}`)
|
|
69
|
+
const nextDelay = Math.min(retryDelay * 2, 60000)
|
|
70
|
+
scope.log.info(`Reconnecting in ${retryDelay / 1000}s...`)
|
|
71
|
+
setTimeout(() => subscribe(login, devices, callback, scope, nextDelay), retryDelay)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
ws.onerror = function (ev) {
|
|
75
|
+
scope.log.error(`Socket error: ${JSON.stringify(ev)}`)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
ws.onopen = function (ev) {
|
|
79
|
+
scope.log.debug(`Socket connection opened: ${JSON.stringify(ev)}`)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
ws.onmessage = function (message) {
|
|
83
|
+
let data
|
|
84
|
+
try {
|
|
85
|
+
data = JSON.parse(message.data)
|
|
86
|
+
} catch (err) {
|
|
87
|
+
scope.log.error(`Received bad json: ${String(message.data)}`)
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
if (data.type === 'challenge') {
|
|
91
|
+
ws.send([JSON.stringify({ token: login })])
|
|
92
|
+
}
|
|
93
|
+
if (data.type === 'status' && data.status === 'ready') {
|
|
94
|
+
devices.forEach((element) => {
|
|
95
|
+
ws.send([JSON.stringify({ type: 'subscribe', subscription: { modelName: 'IotSwitch', modelId: element.id } })])
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
if (data.type === 'notification' && data.notification?.data?.power) {
|
|
99
|
+
const payload = {
|
|
100
|
+
id: data.notification.modelId,
|
|
101
|
+
power: data.notification.data.power,
|
|
102
|
+
}
|
|
103
|
+
if (data.notification.data.brightness) payload.brightness = data.notification.data.brightness
|
|
104
|
+
callback(payload)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = {
|
|
110
|
+
getIotSwitch,
|
|
111
|
+
getPersonResidentialPermissions,
|
|
112
|
+
getResidenceIotSwitches,
|
|
113
|
+
getResidentialAccounts,
|
|
114
|
+
getResidentialAccountsV2,
|
|
115
|
+
postPersonLogin,
|
|
116
|
+
putIotSwitch,
|
|
117
|
+
subscribe,
|
|
118
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"pluginAlias": "LevitonDecoraSmart",
|
|
3
|
+
"pluginType": "platform",
|
|
4
|
+
"singular": false,
|
|
5
|
+
"schema": {
|
|
6
|
+
"type": "object",
|
|
7
|
+
"properties": {
|
|
8
|
+
"email": {
|
|
9
|
+
"title": "email",
|
|
10
|
+
"type": "string",
|
|
11
|
+
"required": true,
|
|
12
|
+
"x-schema-form": {
|
|
13
|
+
"type": "username"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"password": {
|
|
17
|
+
"title": "password",
|
|
18
|
+
"type": "string",
|
|
19
|
+
"required": true,
|
|
20
|
+
"x-schema-form": {
|
|
21
|
+
"type": "password"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"loglevel": {
|
|
25
|
+
"title": "Log Level",
|
|
26
|
+
"type": "string",
|
|
27
|
+
"default": "info",
|
|
28
|
+
"enum": ["debug", "info", "warn", "error"],
|
|
29
|
+
"description": "Verbosity of plugin logging. Default is info."
|
|
30
|
+
},
|
|
31
|
+
"excludedModels": {
|
|
32
|
+
"type": "array",
|
|
33
|
+
"description": "List of device models to exclude (e.g. fully HomeKit-native devices)",
|
|
34
|
+
"items": {
|
|
35
|
+
"type": "string"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"excludedSerials": {
|
|
39
|
+
"type": "array",
|
|
40
|
+
"description": "List of device serial numbers to exclude",
|
|
41
|
+
"items": {
|
|
42
|
+
"type": "string"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
let Service, Characteristic, Accessory, UUID
|
|
2
|
+
const Leviton = require('./api.js')
|
|
3
|
+
const PLUGIN_NAME = '@benjaminmichoux/homebridge-leviton'
|
|
4
|
+
const PLATFORM_NAME = 'LevitonDecoraSmart'
|
|
5
|
+
const levels = ['debug', 'info', 'warn', 'error']
|
|
6
|
+
|
|
7
|
+
class LevitonDecoraSmartPlatform {
|
|
8
|
+
constructor(log, config, api) {
|
|
9
|
+
this.config = config
|
|
10
|
+
this.api = api
|
|
11
|
+
this.accessories = []
|
|
12
|
+
|
|
13
|
+
const noop = function () {}
|
|
14
|
+
const logger = (level) => (msg) =>
|
|
15
|
+
levels.indexOf((config && levels.includes(config.loglevel) && config.loglevel) || 'info') <= levels.indexOf(level)
|
|
16
|
+
? log(msg)
|
|
17
|
+
: noop()
|
|
18
|
+
|
|
19
|
+
this.log = levels.reduce((a, l) => {
|
|
20
|
+
a[l] = logger(l)
|
|
21
|
+
return a
|
|
22
|
+
}, {})
|
|
23
|
+
|
|
24
|
+
if (!config) {
|
|
25
|
+
this.log.error(`No config for ${PLUGIN_NAME} defined.`)
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!config.email || !config.password) {
|
|
30
|
+
this.log.error(`email and password for ${PLUGIN_NAME} are required in config.json`)
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
api.on('didFinishLaunching', async () => {
|
|
35
|
+
this.log.debug('didFinishLaunching')
|
|
36
|
+
const { devices, token } = await this.initialize(config)
|
|
37
|
+
const excludedModels = (config.excludedModels || []).map((name) => name.toUpperCase())
|
|
38
|
+
const excludedSerials = (config.excludedSerials || []).map((name) => name.toUpperCase())
|
|
39
|
+
if (Array.isArray(devices) && devices.length > 0) {
|
|
40
|
+
devices.forEach((device) => {
|
|
41
|
+
if (!this.accessories.find((acc) => acc.context.device.serial === device.serial)) {
|
|
42
|
+
if (!excludedModels.includes(device.model) && !excludedSerials.includes(device.serial)) {
|
|
43
|
+
this.addAccessory(device, token)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
} else {
|
|
48
|
+
this.log.error('Unable to initialize: no devices found')
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
subscriptionCallback(payload) {
|
|
54
|
+
const accessory = this.accessories.find((acc) => acc.context.device.id === payload.id)
|
|
55
|
+
|
|
56
|
+
if (!accessory) return
|
|
57
|
+
|
|
58
|
+
const { id, power, brightness } = payload
|
|
59
|
+
this.log.debug(`Socket: ${accessory.displayName} (${id}): ${power} ${brightness ? `${brightness}%` : ''}`)
|
|
60
|
+
|
|
61
|
+
const service =
|
|
62
|
+
accessory.getService(Service.Fan) ||
|
|
63
|
+
accessory.getService(Service.Switch) ||
|
|
64
|
+
accessory.getService(Service.Outlet) ||
|
|
65
|
+
accessory.getService(Service.Lightbulb)
|
|
66
|
+
const isFan = !!accessory.getService(Service.Fan)
|
|
67
|
+
|
|
68
|
+
if (brightness)
|
|
69
|
+
service
|
|
70
|
+
.getCharacteristic(isFan ? Characteristic.RotationSpeed : Characteristic.Brightness)
|
|
71
|
+
.updateValue(brightness)
|
|
72
|
+
service.getCharacteristic(Characteristic.On).updateValue(power === 'ON')
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async initialize() {
|
|
76
|
+
this.log.debug('initialize')
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
var login = await Leviton.postPersonLogin({
|
|
80
|
+
email: this.config['email'],
|
|
81
|
+
password: this.config['password'],
|
|
82
|
+
})
|
|
83
|
+
var { id: token, userId: personID } = login
|
|
84
|
+
this.log.debug(`personID: ${personID}, hasToken: ${!!token}`)
|
|
85
|
+
} catch (err) {
|
|
86
|
+
this.log.error(`Failed to login to leviton: ${err.message}`)
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
const permissions = await Leviton.getPersonResidentialPermissions({
|
|
90
|
+
personID,
|
|
91
|
+
token,
|
|
92
|
+
})
|
|
93
|
+
var accountID = permissions[0].residentialAccountId
|
|
94
|
+
this.log.debug(`accountID: ${accountID}`)
|
|
95
|
+
} catch (err) {
|
|
96
|
+
this.log.error(`Failed to get leviton accountID: ${err.message}`)
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
var { primaryResidenceId: residenceID, id: residenceObjectID } = await Leviton.getResidentialAccounts({
|
|
100
|
+
accountID,
|
|
101
|
+
token,
|
|
102
|
+
})
|
|
103
|
+
this.log.debug(`residenceID: ${residenceID}`)
|
|
104
|
+
} catch (err) {
|
|
105
|
+
this.log.error(`Failed to get leviton residenceID: ${err.message}`)
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
var devices = await Leviton.getResidenceIotSwitches({
|
|
109
|
+
residenceID,
|
|
110
|
+
token,
|
|
111
|
+
})
|
|
112
|
+
this.log.debug(`devices: ${JSON.stringify(devices)}`)
|
|
113
|
+
} catch (err) {
|
|
114
|
+
this.log.error(`Failed to get leviton devices: ${err.message}`)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
if (!Array.isArray(devices) || devices.length < 1) {
|
|
119
|
+
this.log.info('No devices found for primary residence id. Trying residence v2')
|
|
120
|
+
|
|
121
|
+
const accountsV2Response = await Leviton.getResidentialAccountsV2({
|
|
122
|
+
residenceObjectID,
|
|
123
|
+
token,
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
if (accountsV2Response[0]) {
|
|
127
|
+
residenceID = accountsV2Response[0].id
|
|
128
|
+
devices = await Leviton.getResidenceIotSwitches({
|
|
129
|
+
residenceID,
|
|
130
|
+
token,
|
|
131
|
+
})
|
|
132
|
+
} else {
|
|
133
|
+
throw new Error('No residenceIDs found')
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!Array.isArray(devices) || devices.length < 1) {
|
|
137
|
+
throw new Error(
|
|
138
|
+
`No devices found for residenceID: ${residenceID} or residenceIDV2 method: ${residenceObjectID}`
|
|
139
|
+
)
|
|
140
|
+
} else {
|
|
141
|
+
Leviton.subscribe(login, devices, this.subscriptionCallback.bind(this), this)
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
Leviton.subscribe(login, devices, this.subscriptionCallback.bind(this), this)
|
|
145
|
+
}
|
|
146
|
+
} catch (err) {
|
|
147
|
+
this.log.error(`Error subscribing devices to websocket updates: ${err.message}`)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return { devices, token }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async addAccessory(device, token) {
|
|
154
|
+
this.log.info(`addAccessory ${device.name}`)
|
|
155
|
+
|
|
156
|
+
const uuid = UUID.generate(device.serial)
|
|
157
|
+
const accessory = new this.api.platformAccessory(device.name, uuid)
|
|
158
|
+
|
|
159
|
+
accessory.context.device = device
|
|
160
|
+
accessory.context.token = token
|
|
161
|
+
|
|
162
|
+
accessory
|
|
163
|
+
.getService(Service.AccessoryInformation)
|
|
164
|
+
.setCharacteristic(Characteristic.Name, device.name)
|
|
165
|
+
.setCharacteristic(Characteristic.SerialNumber, device.serial)
|
|
166
|
+
.setCharacteristic(Characteristic.Manufacturer, device.manufacturer)
|
|
167
|
+
.setCharacteristic(Characteristic.Model, device.model)
|
|
168
|
+
.setCharacteristic(Characteristic.FirmwareRevision, device.version)
|
|
169
|
+
|
|
170
|
+
await this.setupService(accessory)
|
|
171
|
+
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory])
|
|
172
|
+
|
|
173
|
+
this.accessories.push(accessory)
|
|
174
|
+
this.log.debug(`Finished adding accessory ${device.name}`)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async configureAccessory(accessory) {
|
|
178
|
+
this.log.debug(`configureAccessory: ${accessory.displayName}`)
|
|
179
|
+
await this.setupService(accessory)
|
|
180
|
+
this.accessories.push(accessory)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async getStatus(device, token) {
|
|
184
|
+
this.log.debug(`getStatus: ${device.name}`)
|
|
185
|
+
return Leviton.getIotSwitch({ switchID: device.id, token })
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async setupService(accessory) {
|
|
189
|
+
this.log.debug(`setupService: ${accessory.displayName}`)
|
|
190
|
+
|
|
191
|
+
const device = accessory.context.device
|
|
192
|
+
this.log.debug(`Device Model: ${device.model}`)
|
|
193
|
+
|
|
194
|
+
switch (device.model) {
|
|
195
|
+
case 'DW4SF':
|
|
196
|
+
await this.setupFanService(accessory)
|
|
197
|
+
break
|
|
198
|
+
case 'DWVAA':
|
|
199
|
+
case 'DW1KD':
|
|
200
|
+
case 'DW6HD':
|
|
201
|
+
case 'D26HD':
|
|
202
|
+
case 'D23LP':
|
|
203
|
+
case 'DW3HL':
|
|
204
|
+
await this.setupLightbulbService(accessory)
|
|
205
|
+
break
|
|
206
|
+
case 'DW15R':
|
|
207
|
+
case 'DW15A':
|
|
208
|
+
case 'DW15P':
|
|
209
|
+
await this.setupOutletService(accessory)
|
|
210
|
+
break
|
|
211
|
+
default:
|
|
212
|
+
await this.setupSwitchService(accessory)
|
|
213
|
+
break
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async setupSwitchService(accessory) {
|
|
218
|
+
this.log.debug(`Setting up device as Switch: ${accessory.displayName}`)
|
|
219
|
+
|
|
220
|
+
const device = accessory.context.device
|
|
221
|
+
const token = accessory.context.token
|
|
222
|
+
const status = await this.getStatus(device, token)
|
|
223
|
+
|
|
224
|
+
const service = accessory.getService(Service.Switch) || accessory.addService(Service.Switch, device.name)
|
|
225
|
+
|
|
226
|
+
service
|
|
227
|
+
.getCharacteristic(Characteristic.On)
|
|
228
|
+
.onGet(async () => {
|
|
229
|
+
const res = await Leviton.getIotSwitch({ switchID: device.id, token })
|
|
230
|
+
this.log.debug(`onGetPower: ${device.name} ${res.power}`)
|
|
231
|
+
return res.power === 'ON'
|
|
232
|
+
})
|
|
233
|
+
.onSet(async (value) => {
|
|
234
|
+
const res = await Leviton.putIotSwitch({ switchID: device.id, power: value ? 'ON' : 'OFF', token })
|
|
235
|
+
this.log.info(`onSetPower: ${device.name} ${res.power}`)
|
|
236
|
+
})
|
|
237
|
+
.updateValue(status.power === 'ON')
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async setupOutletService(accessory) {
|
|
241
|
+
this.log.debug(`Setting up device as Outlet: ${accessory.displayName}`)
|
|
242
|
+
|
|
243
|
+
const device = accessory.context.device
|
|
244
|
+
const token = accessory.context.token
|
|
245
|
+
const status = await this.getStatus(device, token)
|
|
246
|
+
|
|
247
|
+
const service = accessory.getService(Service.Outlet) || accessory.addService(Service.Outlet, device.name)
|
|
248
|
+
|
|
249
|
+
service
|
|
250
|
+
.getCharacteristic(Characteristic.On)
|
|
251
|
+
.onGet(async () => {
|
|
252
|
+
const res = await Leviton.getIotSwitch({ switchID: device.id, token })
|
|
253
|
+
this.log.debug(`onGetPower: ${device.name} ${res.power}`)
|
|
254
|
+
return res.power === 'ON'
|
|
255
|
+
})
|
|
256
|
+
.onSet(async (value) => {
|
|
257
|
+
const res = await Leviton.putIotSwitch({ switchID: device.id, power: value ? 'ON' : 'OFF', token })
|
|
258
|
+
this.log.info(`onSetPower: ${device.name} ${res.power}`)
|
|
259
|
+
})
|
|
260
|
+
.updateValue(status.power === 'ON')
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async setupLightbulbService(accessory) {
|
|
264
|
+
this.log.debug(`Setting up device as Lightbulb: ${accessory.displayName}`)
|
|
265
|
+
|
|
266
|
+
const device = accessory.context.device
|
|
267
|
+
const token = accessory.context.token
|
|
268
|
+
const status = await this.getStatus(device, token)
|
|
269
|
+
|
|
270
|
+
const service = accessory.getService(Service.Lightbulb) || accessory.addService(Service.Lightbulb, device.name)
|
|
271
|
+
|
|
272
|
+
service
|
|
273
|
+
.getCharacteristic(Characteristic.On)
|
|
274
|
+
.onGet(async () => {
|
|
275
|
+
const res = await Leviton.getIotSwitch({ switchID: device.id, token })
|
|
276
|
+
this.log.debug(`onGetPower: ${device.name} ${res.power}`)
|
|
277
|
+
return res.power === 'ON'
|
|
278
|
+
})
|
|
279
|
+
.onSet(async (value) => {
|
|
280
|
+
const res = await Leviton.putIotSwitch({ switchID: device.id, power: value ? 'ON' : 'OFF', token })
|
|
281
|
+
this.log.info(`onSetPower: ${device.name} ${res.power}`)
|
|
282
|
+
})
|
|
283
|
+
.updateValue(status.power === 'ON')
|
|
284
|
+
|
|
285
|
+
service
|
|
286
|
+
.getCharacteristic(Characteristic.Brightness)
|
|
287
|
+
.onGet(async () => {
|
|
288
|
+
const res = await Leviton.getIotSwitch({ switchID: device.id, token })
|
|
289
|
+
this.log.debug(`onGetBrightness: ${device.name} @ ${res.brightness}%`)
|
|
290
|
+
return res.brightness
|
|
291
|
+
})
|
|
292
|
+
.onSet(async (brightness) => {
|
|
293
|
+
const res = await Leviton.putIotSwitch({ switchID: device.id, brightness, token })
|
|
294
|
+
this.log.info(`onSetBrightness: ${device.name} @ ${res.brightness}%`)
|
|
295
|
+
})
|
|
296
|
+
.setProps({ minValue: status.minLevel, maxValue: status.maxLevel, minStep: 1 })
|
|
297
|
+
.updateValue(status.brightness)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async setupFanService(accessory) {
|
|
301
|
+
this.log.debug(`Setting up device as Fan: ${accessory.displayName}`)
|
|
302
|
+
|
|
303
|
+
const device = accessory.context.device
|
|
304
|
+
const token = accessory.context.token
|
|
305
|
+
const status = await this.getStatus(device, token)
|
|
306
|
+
|
|
307
|
+
const service = accessory.getService(Service.Fan) || accessory.addService(Service.Fan, device.name)
|
|
308
|
+
|
|
309
|
+
service
|
|
310
|
+
.getCharacteristic(Characteristic.On)
|
|
311
|
+
.onGet(async () => {
|
|
312
|
+
const res = await Leviton.getIotSwitch({ switchID: device.id, token })
|
|
313
|
+
this.log.debug(`onGetPower: ${device.name} ${res.power}`)
|
|
314
|
+
return res.power === 'ON'
|
|
315
|
+
})
|
|
316
|
+
.onSet(async (value) => {
|
|
317
|
+
const res = await Leviton.putIotSwitch({ switchID: device.id, power: value ? 'ON' : 'OFF', token })
|
|
318
|
+
this.log.info(`onSetPower: ${device.name} ${res.power}`)
|
|
319
|
+
})
|
|
320
|
+
.updateValue(status.power === 'ON')
|
|
321
|
+
|
|
322
|
+
service
|
|
323
|
+
.getCharacteristic(Characteristic.RotationSpeed)
|
|
324
|
+
.onGet(async () => {
|
|
325
|
+
const res = await Leviton.getIotSwitch({ switchID: device.id, token })
|
|
326
|
+
this.log.debug(`onGetRotationSpeed: ${device.name} @ ${res.brightness}%`)
|
|
327
|
+
return res.brightness
|
|
328
|
+
})
|
|
329
|
+
.onSet(async (brightness) => {
|
|
330
|
+
const res = await Leviton.putIotSwitch({ switchID: device.id, brightness, token })
|
|
331
|
+
this.log.info(`onSetRotationSpeed: ${device.name} @ ${res.brightness}%`)
|
|
332
|
+
})
|
|
333
|
+
.setProps({ minValue: 0, maxValue: status.maxLevel, minStep: status.minLevel })
|
|
334
|
+
.updateValue(status.brightness)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
removeAccessories() {
|
|
338
|
+
this.log.info('Removing all accessories')
|
|
339
|
+
this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, this.accessories)
|
|
340
|
+
this.accessories.splice(0, this.accessories.length)
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
module.exports = function (homebridge) {
|
|
345
|
+
Service = homebridge.hap.Service
|
|
346
|
+
Characteristic = homebridge.hap.Characteristic
|
|
347
|
+
Accessory = homebridge.hap.Accessory
|
|
348
|
+
UUID = homebridge.hap.uuid
|
|
349
|
+
homebridge.registerPlatform(PLUGIN_NAME, PLATFORM_NAME, LevitonDecoraSmartPlatform, true)
|
|
350
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@benjaminmichoux/homebridge-leviton",
|
|
3
|
+
"version": "2.2.0",
|
|
4
|
+
"description": "A homebridge plugin for Leviton Decora Smart devices",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"homebridge-plugin"
|
|
8
|
+
],
|
|
9
|
+
"author": "tabrindle@gmail.com",
|
|
10
|
+
"contributors": [
|
|
11
|
+
"Benjamin Michoux (https://github.com/benjaminmichoux)"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/benjaminmichoux/homebridge-leviton.git"
|
|
17
|
+
},
|
|
18
|
+
"homepage": "https://github.com/benjaminmichoux/homebridge-leviton#readme",
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/benjaminmichoux/homebridge-leviton/issues"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"sockjs-client": "^1.6.0"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": "^18.20.4 || ^20.14.0 || ^22 || ^24",
|
|
27
|
+
"homebridge": "^1.8.0 || ^2.0.0"
|
|
28
|
+
}
|
|
29
|
+
}
|