@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 ADDED
@@ -0,0 +1,56 @@
1
+ # homebridge-leviton
2
+
3
+ [![verified-by-homebridge](https://badgen.net/badge/homebridge/verified/purple)](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
+ }