@mp-consulting/homebridge-unifi-access 1.0.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/.claude/settings.local.json +91 -0
- package/CHANGELOG.md +13 -0
- package/LICENSE.md +22 -0
- package/README.md +159 -0
- package/config.schema.json +202 -0
- package/dist/access-controller.d.ts +41 -0
- package/dist/access-controller.js +342 -0
- package/dist/access-controller.js.map +1 -0
- package/dist/access-device-catalog.d.ts +43 -0
- package/dist/access-device-catalog.js +151 -0
- package/dist/access-device-catalog.js.map +1 -0
- package/dist/access-device.d.ts +68 -0
- package/dist/access-device.js +330 -0
- package/dist/access-device.js.map +1 -0
- package/dist/access-events.d.ts +27 -0
- package/dist/access-events.js +152 -0
- package/dist/access-events.js.map +1 -0
- package/dist/access-options.d.ts +32 -0
- package/dist/access-options.js +65 -0
- package/dist/access-options.js.map +1 -0
- package/dist/access-platform.d.ts +15 -0
- package/dist/access-platform.js +74 -0
- package/dist/access-platform.js.map +1 -0
- package/dist/access-types.d.ts +30 -0
- package/dist/access-types.js +42 -0
- package/dist/access-types.js.map +1 -0
- package/dist/hub/access-hub-api.d.ts +13 -0
- package/dist/hub/access-hub-api.js +140 -0
- package/dist/hub/access-hub-api.js.map +1 -0
- package/dist/hub/access-hub-events.d.ts +2 -0
- package/dist/hub/access-hub-events.js +229 -0
- package/dist/hub/access-hub-events.js.map +1 -0
- package/dist/hub/access-hub-mqtt.d.ts +2 -0
- package/dist/hub/access-hub-mqtt.js +137 -0
- package/dist/hub/access-hub-mqtt.js.map +1 -0
- package/dist/hub/access-hub-services.d.ts +4 -0
- package/dist/hub/access-hub-services.js +451 -0
- package/dist/hub/access-hub-services.js.map +1 -0
- package/dist/hub/access-hub-types.d.ts +145 -0
- package/dist/hub/access-hub-types.js +35 -0
- package/dist/hub/access-hub-types.js.map +1 -0
- package/dist/hub/access-hub-utils.d.ts +20 -0
- package/dist/hub/access-hub-utils.js +128 -0
- package/dist/hub/access-hub-utils.js.map +1 -0
- package/dist/hub/access-hub.d.ts +39 -0
- package/dist/hub/access-hub.js +185 -0
- package/dist/hub/access-hub.js.map +1 -0
- package/dist/hub/index.d.ts +4 -0
- package/dist/hub/index.js +7 -0
- package/dist/hub/index.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/settings.d.ts +16 -0
- package/dist/settings.js +49 -0
- package/dist/settings.js.map +1 -0
- package/docs/FeatureOptions.md +120 -0
- package/docs/MQTT.md +116 -0
- package/docs/api_reference.pdf +0 -0
- package/docs/media/homebridge-unifi-access.png +0 -0
- package/docs/media/homebridge-unifi-access.svg +21 -0
- package/eslint.config.mjs +99 -0
- package/homebridge-ui/public/app.js +104 -0
- package/homebridge-ui/public/index.html +267 -0
- package/homebridge-ui/public/modules/constants.js +22 -0
- package/homebridge-ui/public/modules/controllers.js +202 -0
- package/homebridge-ui/public/modules/discovery.js +89 -0
- package/homebridge-ui/public/modules/dom-helpers.js +41 -0
- package/homebridge-ui/public/modules/feature-options.js +625 -0
- package/homebridge-ui/public/modules/state.js +26 -0
- package/homebridge-ui/public/styles.css +533 -0
- package/homebridge-ui/server.js +374 -0
- package/package.json +83 -0
- package/scripts/event-schema-monitor.ts +350 -0
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
/* server.js: @mp-consulting/homebridge-unifi-access webUI server API. */
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
import { featureOptionCategories, featureOptions } from "../dist/access-options.js";
|
|
5
|
+
import { AccessApi } from "unifi-access";
|
|
6
|
+
import { HomebridgePluginUiServer } from "@homebridge/plugin-ui-utils";
|
|
7
|
+
import dgram from "node:dgram";
|
|
8
|
+
import https from "node:https";
|
|
9
|
+
import os from "node:os";
|
|
10
|
+
import util from "node:util";
|
|
11
|
+
|
|
12
|
+
// Validate a controller address, rejecting loopback, link-local, and unspecified addresses.
|
|
13
|
+
function isValidAddress(address) {
|
|
14
|
+
|
|
15
|
+
if(!address || (typeof address !== "string")) {
|
|
16
|
+
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const trimmed = address.trim().toLowerCase();
|
|
21
|
+
|
|
22
|
+
if(!trimmed || (trimmed === "localhost") || trimmed.startsWith("127.") || trimmed.startsWith("169.254.") || (trimmed === "0.0.0.0") ||
|
|
23
|
+
trimmed.startsWith("[") || trimmed.includes("::")) {
|
|
24
|
+
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Number of adjacent /24 subnets to scan in each direction from each local interface.
|
|
32
|
+
// A value of 5 means scanning ±5 subnets (up to 2,540 unicast probes per local subnet).
|
|
33
|
+
// Only used for discovering UniFi devices on routed subnets where broadcasts don't reach.
|
|
34
|
+
const ADJACENT_SUBNET_RANGE = 5;
|
|
35
|
+
const DISCOVERY_TIMEOUT = 5000;
|
|
36
|
+
const UBNT_DISCOVERY_PORT = 10001;
|
|
37
|
+
|
|
38
|
+
// Ubiquiti L2 Discovery Protocol: send a 4-byte packet, devices respond with TLV data.
|
|
39
|
+
const UBNT_DISCOVERY_PACKET = globalThis.Buffer.from([ 0x01, 0x00, 0x00, 0x00 ]);
|
|
40
|
+
|
|
41
|
+
// TLV field types in the Ubiquiti discovery response.
|
|
42
|
+
const UBNT_TLV = {
|
|
43
|
+
|
|
44
|
+
FIRMWARE: 0x03, // Firmware version string
|
|
45
|
+
HOSTNAME: 0x0B, // Device hostname
|
|
46
|
+
MAC_IP: 0x02, // 6-byte MAC + 4-byte IP
|
|
47
|
+
MODEL_LONG: 0x14, // Full model name
|
|
48
|
+
MODEL_SHORT: 0x0C // Short model name (e.g. "UNVR")
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
class PluginUiServer extends HomebridgePluginUiServer {
|
|
52
|
+
|
|
53
|
+
errorInfo;
|
|
54
|
+
|
|
55
|
+
constructor() {
|
|
56
|
+
|
|
57
|
+
super();
|
|
58
|
+
|
|
59
|
+
this.errorInfo = "";
|
|
60
|
+
|
|
61
|
+
// Register getErrorMessage() with the Homebridge server API.
|
|
62
|
+
this.#registerGetErrorMessage();
|
|
63
|
+
|
|
64
|
+
// Register getDevices() with the Homebridge server API.
|
|
65
|
+
this.#registerGetDevices();
|
|
66
|
+
|
|
67
|
+
// Register getOptions() with the Homebridge server API.
|
|
68
|
+
this.#registerGetOptions();
|
|
69
|
+
|
|
70
|
+
// Register discover() with the Homebridge server API.
|
|
71
|
+
this.#registerDiscover();
|
|
72
|
+
|
|
73
|
+
// Register checkStatus() with the Homebridge server API.
|
|
74
|
+
this.#registerCheckStatus();
|
|
75
|
+
|
|
76
|
+
this.ready();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Register the getErrorMessage() webUI server API endpoint.
|
|
80
|
+
#registerGetErrorMessage() {
|
|
81
|
+
|
|
82
|
+
// Return the most recent error message generated by the Access API.
|
|
83
|
+
this.onRequest("/getErrorMessage", () => this.errorInfo);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Register the getDevices() webUI server API endpoint.
|
|
87
|
+
#registerGetDevices() {
|
|
88
|
+
|
|
89
|
+
// Return the list of Access devices.
|
|
90
|
+
this.onRequest("/getDevices", async (controller) => {
|
|
91
|
+
|
|
92
|
+
// Validate the controller address before attempting a connection.
|
|
93
|
+
if(!isValidAddress(controller.address)) {
|
|
94
|
+
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
|
|
100
|
+
const log = {
|
|
101
|
+
|
|
102
|
+
debug: () => {},
|
|
103
|
+
error: (message, parameters = []) => {
|
|
104
|
+
|
|
105
|
+
// Save the error to inform the user in the webUI.
|
|
106
|
+
this.errorInfo = util.format(message, ...(Array.isArray(parameters) ? parameters : [parameters]));
|
|
107
|
+
|
|
108
|
+
// eslint-disable-next-line no-console
|
|
109
|
+
console.error(this.errorInfo);
|
|
110
|
+
},
|
|
111
|
+
info: () => {},
|
|
112
|
+
warn: () => {}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// Connect to the Access controller.
|
|
116
|
+
const udaApi = new AccessApi(log);
|
|
117
|
+
|
|
118
|
+
if(!(await udaApi.login(controller.address, controller.username, controller.password))) {
|
|
119
|
+
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Bootstrap the controller. It will emit a message once it's received the bootstrap JSON, or you can alternatively wait for the Promise to resolve.
|
|
124
|
+
if(!(await udaApi.getBootstrap())) {
|
|
125
|
+
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const devices = udaApi.devices.filter(x => x.is_managed);
|
|
130
|
+
|
|
131
|
+
devices.sort((a, b) => {
|
|
132
|
+
|
|
133
|
+
const nonEmpty = (...args) => args.find(v => (v !== undefined) && (v !== null) && (v !== ""));
|
|
134
|
+
|
|
135
|
+
const aCase = nonEmpty(a.alias, a.name, a.model).toLowerCase();
|
|
136
|
+
const bCase = nonEmpty(b.alias, b.name, b.model).toLowerCase();
|
|
137
|
+
|
|
138
|
+
return aCase > bCase ? 1 : (bCase > aCase ? -1 : 0);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const result = [ udaApi.controller, ...devices ];
|
|
142
|
+
|
|
143
|
+
// Clean up the API session.
|
|
144
|
+
udaApi.logout();
|
|
145
|
+
|
|
146
|
+
return result;
|
|
147
|
+
} catch(err) {
|
|
148
|
+
|
|
149
|
+
// eslint-disable-next-line no-console
|
|
150
|
+
console.log(err);
|
|
151
|
+
|
|
152
|
+
// Return nothing if we error out for some reason.
|
|
153
|
+
return [];
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Register the getOptions() webUI server API endpoint.
|
|
159
|
+
#registerGetOptions() {
|
|
160
|
+
|
|
161
|
+
// Return the list of options configured for a given Access device.
|
|
162
|
+
this.onRequest("/getOptions", () => ({ categories: featureOptionCategories, options: featureOptions }));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Register the discover() webUI server API endpoint using the Ubiquiti L2 Discovery Protocol.
|
|
166
|
+
#registerDiscover() {
|
|
167
|
+
|
|
168
|
+
this.onRequest("/discover", () => {
|
|
169
|
+
|
|
170
|
+
return new Promise((resolve) => {
|
|
171
|
+
|
|
172
|
+
const devices = [];
|
|
173
|
+
const seen = new Set();
|
|
174
|
+
|
|
175
|
+
const socket = dgram.createSocket({ reuseAddr: true, type: "udp4" });
|
|
176
|
+
|
|
177
|
+
socket.on("message", (msg) => {
|
|
178
|
+
|
|
179
|
+
// Parse the Ubiquiti discovery response (TLV format after 4-byte header).
|
|
180
|
+
if(msg.length < 4) { return; }
|
|
181
|
+
|
|
182
|
+
const device = {};
|
|
183
|
+
let offset = 4;
|
|
184
|
+
|
|
185
|
+
while(offset + 3 <= msg.length) {
|
|
186
|
+
|
|
187
|
+
const type = msg[offset];
|
|
188
|
+
const length = msg.readUInt16BE(offset + 1);
|
|
189
|
+
|
|
190
|
+
offset += 3;
|
|
191
|
+
|
|
192
|
+
if(offset + length > msg.length) { break; }
|
|
193
|
+
|
|
194
|
+
const value = msg.subarray(offset, offset + length);
|
|
195
|
+
|
|
196
|
+
switch(type) {
|
|
197
|
+
|
|
198
|
+
case UBNT_TLV.MAC_IP:
|
|
199
|
+
|
|
200
|
+
if(length >= 10) {
|
|
201
|
+
|
|
202
|
+
device.mac = [...value.subarray(0, 6)].map(b => b.toString(16).padStart(2, "0")).join(":");
|
|
203
|
+
device.ip = value[6] + "." + value[7] + "." + value[8] + "." + value[9];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
break;
|
|
207
|
+
|
|
208
|
+
case UBNT_TLV.HOSTNAME:
|
|
209
|
+
|
|
210
|
+
device.hostname = value.toString("utf8");
|
|
211
|
+
|
|
212
|
+
break;
|
|
213
|
+
|
|
214
|
+
case UBNT_TLV.MODEL_SHORT:
|
|
215
|
+
|
|
216
|
+
device.model = value.toString("utf8");
|
|
217
|
+
|
|
218
|
+
break;
|
|
219
|
+
|
|
220
|
+
case UBNT_TLV.MODEL_LONG:
|
|
221
|
+
|
|
222
|
+
device.modelLong = value.toString("utf8");
|
|
223
|
+
|
|
224
|
+
break;
|
|
225
|
+
|
|
226
|
+
case UBNT_TLV.FIRMWARE:
|
|
227
|
+
|
|
228
|
+
device.firmware = value.toString("utf8");
|
|
229
|
+
|
|
230
|
+
break;
|
|
231
|
+
|
|
232
|
+
default:
|
|
233
|
+
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
offset += length;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if(device.ip && !seen.has(device.ip)) {
|
|
241
|
+
|
|
242
|
+
seen.add(device.ip);
|
|
243
|
+
|
|
244
|
+
devices.push({
|
|
245
|
+
|
|
246
|
+
firmware: device.firmware || "",
|
|
247
|
+
ip: device.ip,
|
|
248
|
+
mac: device.mac || "",
|
|
249
|
+
model: device.model || "",
|
|
250
|
+
modelLong: device.modelLong || "",
|
|
251
|
+
name: device.hostname || device.model || device.ip
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
socket.on("error", () => {
|
|
257
|
+
|
|
258
|
+
try { socket.close(); } catch{ /* ignore */ }
|
|
259
|
+
resolve([]);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
socket.bind(() => {
|
|
263
|
+
|
|
264
|
+
socket.setBroadcast(true);
|
|
265
|
+
|
|
266
|
+
// Broadcast to all local subnet broadcast addresses to reach devices on directly-connected subnets.
|
|
267
|
+
const broadcastAddresses = new Set(["255.255.255.255"]);
|
|
268
|
+
const localSubnets = [];
|
|
269
|
+
|
|
270
|
+
for(const iface of Object.values(os.networkInterfaces())) {
|
|
271
|
+
|
|
272
|
+
for(const info of (iface || [])) {
|
|
273
|
+
|
|
274
|
+
if((info.family === "IPv4") && !info.internal) {
|
|
275
|
+
|
|
276
|
+
const ipParts = info.address.split(".").map(Number);
|
|
277
|
+
const maskParts = info.netmask.split(".").map(Number);
|
|
278
|
+
const broadcast = ipParts.map((ip, i) => (ip | (~maskParts[i] & 0xFF))).join(".");
|
|
279
|
+
|
|
280
|
+
broadcastAddresses.add(broadcast);
|
|
281
|
+
|
|
282
|
+
// Track /24+ subnets for adjacent unicast scanning.
|
|
283
|
+
if(maskParts[2] === 255) {
|
|
284
|
+
|
|
285
|
+
localSubnets.push([ ipParts[0], ipParts[1], ipParts[2] ]);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
for(const addr of broadcastAddresses) {
|
|
292
|
+
|
|
293
|
+
socket.send(UBNT_DISCOVERY_PACKET, 0, UBNT_DISCOVERY_PACKET.length, UBNT_DISCOVERY_PORT, addr);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Unicast scan adjacent /24 subnets to find devices across routed subnets (broadcasts don't cross routers).
|
|
297
|
+
const scannedSubnets = new Set(localSubnets.map(s => s.join(".")));
|
|
298
|
+
|
|
299
|
+
for(const [ a, b, c ] of localSubnets) {
|
|
300
|
+
|
|
301
|
+
for(let offset = -ADJACENT_SUBNET_RANGE; offset <= ADJACENT_SUBNET_RANGE; offset++) {
|
|
302
|
+
|
|
303
|
+
const adjacentC = c + offset;
|
|
304
|
+
|
|
305
|
+
if((adjacentC < 0) || (adjacentC > 255)) {
|
|
306
|
+
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const subnetKey = a + "." + b + "." + adjacentC;
|
|
311
|
+
|
|
312
|
+
if(scannedSubnets.has(subnetKey)) {
|
|
313
|
+
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
scannedSubnets.add(subnetKey);
|
|
318
|
+
|
|
319
|
+
for(let host = 1; host <= 254; host++) {
|
|
320
|
+
|
|
321
|
+
socket.send(UBNT_DISCOVERY_PACKET, 0, UBNT_DISCOVERY_PACKET.length, UBNT_DISCOVERY_PORT, a + "." + b + "." + adjacentC + "." + host);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// Stop after timeout and return results.
|
|
328
|
+
globalThis.setTimeout(() => {
|
|
329
|
+
|
|
330
|
+
try { socket.close(); } catch{ /* ignore */ }
|
|
331
|
+
resolve(devices);
|
|
332
|
+
}, DISCOVERY_TIMEOUT);
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Register the checkStatus() webUI server API endpoint.
|
|
338
|
+
#registerCheckStatus() {
|
|
339
|
+
|
|
340
|
+
this.onRequest("/checkStatus", (payload) => {
|
|
341
|
+
|
|
342
|
+
return new Promise((resolve) => {
|
|
343
|
+
|
|
344
|
+
if(!isValidAddress(payload?.address)) {
|
|
345
|
+
|
|
346
|
+
resolve({ online: false });
|
|
347
|
+
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const req = https.request({
|
|
352
|
+
|
|
353
|
+
hostname: payload.address,
|
|
354
|
+
method: "HEAD",
|
|
355
|
+
path: "/",
|
|
356
|
+
port: 443,
|
|
357
|
+
rejectUnauthorized: false,
|
|
358
|
+
timeout: 5000
|
|
359
|
+
}, () => resolve({ online: true }));
|
|
360
|
+
|
|
361
|
+
req.on("error", () => resolve({ online: false }));
|
|
362
|
+
req.on("timeout", () => {
|
|
363
|
+
|
|
364
|
+
req.destroy();
|
|
365
|
+
resolve({ online: false });
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
req.end();
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
(() => new PluginUiServer())();
|
package/package.json
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mp-consulting/homebridge-unifi-access",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"displayName": "UniFi Access",
|
|
5
|
+
"description": "Complete HomeKit support for UniFi Access devices — locks, doorbells, sensors, and readers — with automatic device discovery and realtime events.",
|
|
6
|
+
"author": "Mickael Palma",
|
|
7
|
+
"homepage": "https://github.com/mp-consulting/homebridge-unifi-access#readme",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"funding": {
|
|
10
|
+
"type": "paypal",
|
|
11
|
+
"url": "https://paypal.me/mickaelpalma"
|
|
12
|
+
},
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git://github.com/mp-consulting/homebridge-unifi-access.git"
|
|
16
|
+
},
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/mp-consulting/homebridge-unifi-access/issues"
|
|
19
|
+
},
|
|
20
|
+
"type": "module",
|
|
21
|
+
"engines": {
|
|
22
|
+
"homebridge": ">=1.8.0 || >=2.0.0",
|
|
23
|
+
"node": ">=18"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"homebridge",
|
|
27
|
+
"homebridge-plugin",
|
|
28
|
+
"homebridge-unifi-access",
|
|
29
|
+
"homekit",
|
|
30
|
+
"camera",
|
|
31
|
+
"doorbell",
|
|
32
|
+
"ubiquiti",
|
|
33
|
+
"unifi",
|
|
34
|
+
"unifi access",
|
|
35
|
+
"unifi protect",
|
|
36
|
+
"motion",
|
|
37
|
+
"motion sensor",
|
|
38
|
+
"security",
|
|
39
|
+
"udm",
|
|
40
|
+
"udm pro",
|
|
41
|
+
"udm-pro",
|
|
42
|
+
"unifi camera",
|
|
43
|
+
"unifi cloud key",
|
|
44
|
+
"unifi dream machine pro",
|
|
45
|
+
"unifios"
|
|
46
|
+
],
|
|
47
|
+
"scripts": {
|
|
48
|
+
"prebuild": "npm run clean",
|
|
49
|
+
"build": "tsc",
|
|
50
|
+
"clean": "shx rm -rf dist",
|
|
51
|
+
"lint": "eslint . --max-warnings=0",
|
|
52
|
+
"postpublish": "npm run clean",
|
|
53
|
+
"prepublishOnly": "npm test && npm run lint && npm run build",
|
|
54
|
+
"test": "vitest run",
|
|
55
|
+
"test:watch": "vitest",
|
|
56
|
+
"test:coverage": "vitest run --coverage",
|
|
57
|
+
"watch": "npm run build && npm link && nodemon",
|
|
58
|
+
"start": "npm run build && homebridge -U ./test/hbConfig -D -I -P .",
|
|
59
|
+
"monitor:events": "npx tsx scripts/event-schema-monitor.ts"
|
|
60
|
+
},
|
|
61
|
+
"main": "dist/index.js",
|
|
62
|
+
"dependencies": {
|
|
63
|
+
"@homebridge/plugin-ui-utils": "2.1.2",
|
|
64
|
+
"homebridge-plugin-utils": "1.30.0",
|
|
65
|
+
"unifi-access": "1.5.3"
|
|
66
|
+
},
|
|
67
|
+
"devDependencies": {
|
|
68
|
+
"@stylistic/eslint-plugin": "5.7.1",
|
|
69
|
+
"@types/node": "25.0.10",
|
|
70
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
71
|
+
"eslint": "9.39.2",
|
|
72
|
+
"homebridge": "1.11.1",
|
|
73
|
+
"homebridge-config-ui-x": "^4.80.0",
|
|
74
|
+
"nodemon": "^3.1.11",
|
|
75
|
+
"shx": "0.4.0",
|
|
76
|
+
"typescript": "5.9.3",
|
|
77
|
+
"typescript-eslint": "8.53.1",
|
|
78
|
+
"vitest": "^4.0.18"
|
|
79
|
+
},
|
|
80
|
+
"overrides": {
|
|
81
|
+
"@types/readable-stream": "4.0.23"
|
|
82
|
+
}
|
|
83
|
+
}
|