@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,202 @@
|
|
|
1
|
+
import { $, escapeHtml, setButtonLoading, showScreen } from "./dom-helpers.js";
|
|
2
|
+
import { getControllers, saveConfig, state } from "./state.js";
|
|
3
|
+
import { openFeatureOptions } from "./feature-options.js";
|
|
4
|
+
|
|
5
|
+
export const renderControllers = () => {
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
const controllers = getControllers();
|
|
9
|
+
const list = $("controllersList");
|
|
10
|
+
const noMsg = $("noControllersMessage");
|
|
11
|
+
|
|
12
|
+
if(!controllers.length) {
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
noMsg.style.display = "block";
|
|
16
|
+
list.style.display = "none";
|
|
17
|
+
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
noMsg.style.display = "none";
|
|
22
|
+
list.style.display = "block";
|
|
23
|
+
list.innerHTML = "";
|
|
24
|
+
|
|
25
|
+
controllers.forEach((ctrl, index) => {
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
const li = document.createElement("li");
|
|
29
|
+
|
|
30
|
+
li.className = "list-group-item";
|
|
31
|
+
/* eslint-disable no-restricted-syntax */
|
|
32
|
+
li.innerHTML = `
|
|
33
|
+
<div class="d-flex w-100 justify-content-between align-items-center">
|
|
34
|
+
<div>
|
|
35
|
+
<h6 class="mb-1">
|
|
36
|
+
<i class="fas fa-server mr-2"></i> ${escapeHtml(ctrl.name || ctrl.address)}
|
|
37
|
+
<span class="status-badge badge rounded-pill bg-secondary ms-2" style="font-size: 0.65rem;"><i class="fas fa-circle-notch fa-spin"></i></span>
|
|
38
|
+
</h6>
|
|
39
|
+
<small class="text-muted"><i class="fas fa-network-wired mr-1"></i> ${escapeHtml(ctrl.address)}</small>
|
|
40
|
+
</div>
|
|
41
|
+
<div class="d-flex gap-1">
|
|
42
|
+
<button class="btn btn-sm btn-primary feature-options-btn"><i class="fas fa-sliders-h"></i> Options</button>
|
|
43
|
+
<button class="btn btn-sm btn-secondary edit-ctrl-btn"><i class="fas fa-edit"></i> Edit</button>
|
|
44
|
+
<button class="btn btn-sm btn-danger delete-ctrl-btn"><i class="fas fa-trash"></i></button>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
`;
|
|
48
|
+
/* eslint-enable no-restricted-syntax */
|
|
49
|
+
|
|
50
|
+
li.querySelector(".feature-options-btn").addEventListener("click", () => openFeatureOptions(index));
|
|
51
|
+
li.querySelector(".edit-ctrl-btn").addEventListener("click", () => openEditController(index));
|
|
52
|
+
li.querySelector(".delete-ctrl-btn").addEventListener("click", async function() {
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
setButtonLoading(this, true, "...");
|
|
56
|
+
controllers.splice(index, 1);
|
|
57
|
+
await saveConfig();
|
|
58
|
+
homebridge.toast.success("Controller removed");
|
|
59
|
+
renderControllers();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
list.appendChild(li);
|
|
63
|
+
|
|
64
|
+
// Check controller status asynchronously.
|
|
65
|
+
const badge = li.querySelector(".status-badge");
|
|
66
|
+
|
|
67
|
+
homebridge.request("/checkStatus", { address: ctrl.address }).then((result) => {
|
|
68
|
+
|
|
69
|
+
if(result?.online) {
|
|
70
|
+
|
|
71
|
+
badge.className = "status-badge badge rounded-pill bg-success ms-2";
|
|
72
|
+
badge.style.fontSize = "0.65rem";
|
|
73
|
+
badge.innerHTML = "<i class=\"fas fa-check-circle\"></i> Online";
|
|
74
|
+
} else {
|
|
75
|
+
|
|
76
|
+
badge.className = "status-badge badge rounded-pill bg-danger ms-2";
|
|
77
|
+
badge.style.fontSize = "0.65rem";
|
|
78
|
+
badge.innerHTML = "<i class=\"fas fa-times-circle\"></i> Offline";
|
|
79
|
+
}
|
|
80
|
+
}).catch(() => {
|
|
81
|
+
|
|
82
|
+
badge.className = "status-badge badge rounded-pill bg-warning ms-2";
|
|
83
|
+
badge.style.fontSize = "0.65rem";
|
|
84
|
+
badge.innerHTML = "<i class=\"fas fa-question-circle\"></i> Unknown";
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export const openAddController = (prefillAddress) => {
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
state.editingIndex = null;
|
|
93
|
+
$("setupTitle").textContent = "Add UniFi Access Controller";
|
|
94
|
+
$("setupSubtitle").textContent = "Enter your UniFi Access controller details and login credentials.";
|
|
95
|
+
$("inputAddress").value = prefillAddress || "";
|
|
96
|
+
$("inputUsername").value = "";
|
|
97
|
+
$("inputPassword").value = "";
|
|
98
|
+
$("setupError").style.display = "none";
|
|
99
|
+
$("cancelSetupBtn").style.display = getControllers().length ? "inline-block" : "none";
|
|
100
|
+
showScreen("setupScreen");
|
|
101
|
+
|
|
102
|
+
if(prefillAddress) {
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
$("inputUsername").focus();
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export const openEditController = (index) => {
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
state.editingIndex = index;
|
|
113
|
+
const ctrl = getControllers()[index];
|
|
114
|
+
|
|
115
|
+
$("setupTitle").textContent = "Edit Controller";
|
|
116
|
+
$("setupSubtitle").textContent = "Editing " + (ctrl.name || ctrl.address);
|
|
117
|
+
$("inputAddress").value = ctrl.address || "";
|
|
118
|
+
$("inputUsername").value = ctrl.username || "";
|
|
119
|
+
$("inputPassword").value = ctrl.password || "";
|
|
120
|
+
$("setupError").style.display = "none";
|
|
121
|
+
$("cancelSetupBtn").style.display = "inline-block";
|
|
122
|
+
showScreen("setupScreen");
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export const handleSetupSubmit = async (event) => {
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
event.preventDefault();
|
|
129
|
+
event.stopPropagation();
|
|
130
|
+
|
|
131
|
+
const address = $("inputAddress").value.trim();
|
|
132
|
+
const username = $("inputUsername").value.trim();
|
|
133
|
+
const password = $("inputPassword").value.trim();
|
|
134
|
+
|
|
135
|
+
if(!address || !username || !password) {
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
$("setupErrorText").textContent = "Please fill in all fields.";
|
|
139
|
+
$("setupError").style.display = "block";
|
|
140
|
+
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const btn = $("saveControllerBtn");
|
|
145
|
+
|
|
146
|
+
setButtonLoading(btn, true, "Validating...");
|
|
147
|
+
$("setupError").style.display = "none";
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
const devices = await homebridge.request("/getDevices", { address, password, username });
|
|
153
|
+
|
|
154
|
+
if(!devices?.length) {
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
const errorDetail = await homebridge.request("/getErrorMessage");
|
|
158
|
+
|
|
159
|
+
$("setupErrorText").textContent = "Unable to connect. " + (errorDetail || "Check your address and credentials.");
|
|
160
|
+
$("setupError").style.display = "block";
|
|
161
|
+
setButtonLoading(btn, false);
|
|
162
|
+
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
state.pluginConfig[0].controllers ||= [];
|
|
167
|
+
|
|
168
|
+
const controllerData = { address, password, username };
|
|
169
|
+
|
|
170
|
+
if(devices[0]?.host?.hostname) {
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
controllerData.name = devices[0].host.hostname;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if(state.editingIndex !== null) {
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
const existing = state.pluginConfig[0].controllers[state.editingIndex];
|
|
180
|
+
|
|
181
|
+
state.pluginConfig[0].controllers[state.editingIndex] = { ...existing, ...controllerData };
|
|
182
|
+
} else {
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
state.pluginConfig[0].controllers.push(controllerData);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
await saveConfig();
|
|
189
|
+
homebridge.toast.success(state.editingIndex !== null ? "Controller updated" : "Controller added successfully!");
|
|
190
|
+
showScreen("controllersScreen");
|
|
191
|
+
renderControllers();
|
|
192
|
+
} catch(e) {
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
$("setupErrorText").textContent = "Error: " + e.message;
|
|
196
|
+
$("setupError").style.display = "block";
|
|
197
|
+
} finally {
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
setButtonLoading(btn, false);
|
|
201
|
+
}
|
|
202
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { $, escapeHtml } from "./dom-helpers.js";
|
|
2
|
+
import { getControllers } from "./state.js";
|
|
3
|
+
import { openAddController } from "./controllers.js";
|
|
4
|
+
|
|
5
|
+
export const handleDiscover = async () => {
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
const btn = $("discoverBtn");
|
|
9
|
+
const btnText = $("discoverBtnText");
|
|
10
|
+
const spinner = $("discoverSpinner");
|
|
11
|
+
const container = $("deviceListContainer");
|
|
12
|
+
const listDiv = $("deviceList");
|
|
13
|
+
|
|
14
|
+
btn.disabled = true;
|
|
15
|
+
btnText.textContent = " Searching...";
|
|
16
|
+
spinner.style.display = "inline-block";
|
|
17
|
+
container.innerHTML = "";
|
|
18
|
+
listDiv.style.display = "none";
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
const devices = await homebridge.request("/discover");
|
|
24
|
+
const configuredIps = getControllers().map(c => c.address);
|
|
25
|
+
const available = devices.filter(d => !configuredIps.includes(d.ip));
|
|
26
|
+
|
|
27
|
+
if(!available.length) {
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
const msg = devices.length ?
|
|
31
|
+
"All discovered devices are already configured." :
|
|
32
|
+
"No devices found. Make sure your UniFi console is on and connected.";
|
|
33
|
+
|
|
34
|
+
/* eslint-disable-next-line no-restricted-syntax */
|
|
35
|
+
container.innerHTML = `<div class="alert alert-${devices.length ? "success" : "warning"}">${msg}</div>`;
|
|
36
|
+
listDiv.style.display = "block";
|
|
37
|
+
} else {
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
container.innerHTML = "<h6>Found Devices:</h6><ul class=\"list-group mb-3\" id=\"discoveredDevices\"></ul>";
|
|
41
|
+
const ul = $("discoveredDevices");
|
|
42
|
+
|
|
43
|
+
available.forEach(d => ul.appendChild(createDiscoveredDeviceItem(d)));
|
|
44
|
+
listDiv.style.display = "block";
|
|
45
|
+
}
|
|
46
|
+
} catch(e) {
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
/* eslint-disable-next-line no-restricted-syntax */
|
|
50
|
+
container.innerHTML = `<div class="alert alert-danger">Discovery error: ${escapeHtml(e.message)}</div>`;
|
|
51
|
+
listDiv.style.display = "block";
|
|
52
|
+
} finally {
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
btn.disabled = false;
|
|
56
|
+
btnText.innerHTML = "<i class=\"fas fa-search\"></i> Discover Controllers";
|
|
57
|
+
spinner.style.display = "none";
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const createDiscoveredDeviceItem = (device) => {
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
const li = document.createElement("li");
|
|
65
|
+
|
|
66
|
+
li.className = "list-group-item list-group-item-action";
|
|
67
|
+
li.style.cursor = "pointer";
|
|
68
|
+
/* eslint-disable no-restricted-syntax */
|
|
69
|
+
li.innerHTML = `
|
|
70
|
+
<div class="d-flex w-100 justify-content-between align-items-center">
|
|
71
|
+
<div>
|
|
72
|
+
<h6 class="mb-1"><i class="fas fa-server mr-2"></i> ${escapeHtml(device.name)}</h6>
|
|
73
|
+
<small class="text-muted"><i class="fas fa-network-wired mr-1"></i> ${escapeHtml(device.ip)}</small>
|
|
74
|
+
${device.model ? `<span class="badge bg-secondary ms-2">${escapeHtml(device.model)}</span>` : ""}
|
|
75
|
+
${device.mac ? `<small class="text-muted ms-2"><i class="fas fa-barcode mr-1"></i> ${escapeHtml(device.mac)}</small>` : ""}
|
|
76
|
+
</div>
|
|
77
|
+
<span class="badge bg-primary rounded-pill">Select</span>
|
|
78
|
+
</div>
|
|
79
|
+
`;
|
|
80
|
+
/* eslint-enable no-restricted-syntax */
|
|
81
|
+
|
|
82
|
+
li.addEventListener("click", () => {
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
openAddController(device.ip);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
return li;
|
|
89
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { SCREENS } from "./constants.js";
|
|
2
|
+
|
|
3
|
+
export const $ = (id) => document.getElementById(id);
|
|
4
|
+
|
|
5
|
+
export const showScreen = (screenId) => {
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
SCREENS.forEach((id) => {
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
$(id).style.display = id === screenId ? "block" : "none";
|
|
12
|
+
});
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const setButtonLoading = (btn, loading, loadingText = "Loading...") => {
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
if(loading) {
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
btn.dataset.originalContent = btn.innerHTML;
|
|
22
|
+
btn.disabled = true;
|
|
23
|
+
/* eslint-disable-next-line no-restricted-syntax */
|
|
24
|
+
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${loadingText}`;
|
|
25
|
+
} else {
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
btn.disabled = false;
|
|
29
|
+
btn.innerHTML = btn.dataset.originalContent;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const escapeHtml = (str) => {
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
const div = document.createElement("div");
|
|
37
|
+
|
|
38
|
+
div.textContent = str;
|
|
39
|
+
|
|
40
|
+
return div.innerHTML;
|
|
41
|
+
};
|