2 * Copyright (c) 2010-2024 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.unifi.internal.api;
15 import java.util.Collection;
16 import java.util.List;
18 import org.eclipse.jdt.annotation.NonNullByDefault;
19 import org.eclipse.jdt.annotation.Nullable;
20 import org.eclipse.jetty.client.HttpClient;
21 import org.eclipse.jetty.http.HttpMethod;
22 import org.openhab.binding.unifi.internal.api.cache.UniFiControllerCache;
23 import org.openhab.binding.unifi.internal.api.dto.UnfiPortOverrideJsonObject;
24 import org.openhab.binding.unifi.internal.api.dto.UniFiClient;
25 import org.openhab.binding.unifi.internal.api.dto.UniFiDevice;
26 import org.openhab.binding.unifi.internal.api.dto.UniFiSite;
27 import org.openhab.binding.unifi.internal.api.dto.UniFiSwitchPorts;
28 import org.openhab.binding.unifi.internal.api.dto.UniFiUnknownClient;
29 import org.openhab.binding.unifi.internal.api.dto.UniFiVoucher;
30 import org.openhab.binding.unifi.internal.api.dto.UniFiWiredClient;
31 import org.openhab.binding.unifi.internal.api.dto.UniFiWirelessClient;
32 import org.openhab.binding.unifi.internal.api.dto.UniFiWlan;
33 import org.openhab.binding.unifi.internal.api.util.UnfiPortOverrideJsonElementDeserializer;
34 import org.openhab.binding.unifi.internal.api.util.UniFiClientDeserializer;
35 import org.openhab.binding.unifi.internal.api.util.UniFiClientInstanceCreator;
36 import org.openhab.binding.unifi.internal.api.util.UniFiDeviceInstanceCreator;
37 import org.openhab.binding.unifi.internal.api.util.UniFiSiteInstanceCreator;
38 import org.openhab.binding.unifi.internal.api.util.UniFiVoucherInstanceCreator;
39 import org.openhab.binding.unifi.internal.api.util.UniFiWlanInstanceCreator;
40 import org.slf4j.Logger;
41 import org.slf4j.LoggerFactory;
43 import com.google.gson.FieldNamingPolicy;
44 import com.google.gson.Gson;
45 import com.google.gson.GsonBuilder;
46 import com.google.gson.JsonObject;
49 * The {@link UniFiController} is the main communication point with an external instance of the Ubiquiti Networks
50 * Controller Software.
52 * @author Matthew Bowman - Initial contribution
53 * @author Patrik Wimnell - Blocking / Unblocking client support
54 * @author Jacob Laursen - Fix online/blocked channels (broken by UniFi Controller 5.12.35)
55 * @author Hilbrand Bouwkamp - Added POEPort support, moved generic cache related code to cache object
56 * @author Mark Herwege - Added guest vouchers
59 public class UniFiController {
61 private static final int INSIGHT_WITHIN_HOURS = 7 * 24; // scurb: Changed to 7 days.
63 private final Logger logger = LoggerFactory.getLogger(UniFiController.class);
65 private final HttpClient httpClient;
66 private final UniFiControllerCache cache = new UniFiControllerCache();
68 private final String host;
69 private final int port;
70 private final String username;
71 private final String password;
72 private final boolean unifios;
73 private final Gson gson;
74 private final Gson poeGson;
76 private String csrfToken;
78 public UniFiController(final HttpClient httpClient, final String host, final int port, final String username,
79 final String password, final boolean unifios) {
80 this.httpClient = httpClient;
83 this.username = username;
84 this.password = password;
85 this.unifios = unifios;
87 final UniFiSiteInstanceCreator siteInstanceCreator = new UniFiSiteInstanceCreator(cache);
88 final UniFiWlanInstanceCreator wlanInstanceCreator = new UniFiWlanInstanceCreator(cache);
89 final UniFiDeviceInstanceCreator deviceInstanceCreator = new UniFiDeviceInstanceCreator(cache);
90 final UniFiClientInstanceCreator clientInstanceCreator = new UniFiClientInstanceCreator(cache);
91 final UniFiVoucherInstanceCreator voucherInstanceCreator = new UniFiVoucherInstanceCreator(cache);
92 this.gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
93 .registerTypeAdapter(UniFiSite.class, siteInstanceCreator)
94 .registerTypeAdapter(UniFiWlan.class, wlanInstanceCreator)
95 .registerTypeAdapter(UniFiDevice.class, deviceInstanceCreator)
96 .registerTypeAdapter(UniFiClient.class, new UniFiClientDeserializer())
97 .registerTypeAdapter(UniFiUnknownClient.class, clientInstanceCreator)
98 .registerTypeAdapter(UniFiWiredClient.class, clientInstanceCreator)
99 .registerTypeAdapter(UniFiWirelessClient.class, clientInstanceCreator)
100 .registerTypeAdapter(UniFiVoucher.class, voucherInstanceCreator).create();
101 this.poeGson = new GsonBuilder()
102 .registerTypeAdapter(UnfiPortOverrideJsonObject.class, new UnfiPortOverrideJsonElementDeserializer())
108 public void start() throws UniFiException {
116 public void stop() throws UniFiException {
120 public void obtainCsrfToken() throws UniFiException {
123 final UniFiControllerRequest<Void> req = newRequest(Void.class, HttpMethod.GET, gson);
128 public void login() throws UniFiException {
129 final UniFiControllerRequest<Void> req = newRequest(Void.class, HttpMethod.POST, gson);
130 req.setPath(unifios ? "/api/auth/login" : "/api/login");
131 req.setBodyParameter("username", username);
132 req.setBodyParameter("password", password);
133 // scurb: Changed strict = false to make blocking feature work
134 req.setBodyParameter("strict", false);
135 req.setBodyParameter("remember", false);
136 executeRequest(req, true);
139 public void logout() throws UniFiException {
141 final UniFiControllerRequest<Void> req = newRequest(Void.class, HttpMethod.POST, gson);
142 req.setPath(unifios ? "/api/auth/logout" : "/logout");
146 public void refresh() throws UniFiException {
147 synchronized (this) {
149 final Collection<UniFiSite> sites = refreshSites();
151 refreshDevices(sites);
152 refreshClients(sites);
153 refreshInsights(sites);
154 refreshVouchers(sites);
158 public UniFiControllerCache getCache() {
162 public @Nullable UniFiSwitchPorts getSwitchPorts(@Nullable final String deviceId) {
163 return cache.getSwitchPorts(deviceId);
166 public void block(final UniFiClient client, final boolean blocked) throws UniFiException {
167 final UniFiControllerRequest<Void> req = newRequest(Void.class, HttpMethod.POST, gson);
168 req.setAPIPath(String.format("/api/s/%s/cmd/stamgr", client.getSite().getName()));
169 req.setBodyParameter("cmd", blocked ? "block-sta" : "unblock-sta");
170 req.setBodyParameter("mac", client.getMac());
175 public void reconnect(final UniFiClient client) throws UniFiException {
176 final UniFiControllerRequest<Void> req = newRequest(Void.class, HttpMethod.POST, gson);
177 req.setAPIPath(String.format("/api/s/%s/cmd/stamgr", client.getSite().getName()));
178 req.setBodyParameter("cmd", "kick-sta");
179 req.setBodyParameter("mac", client.getMac());
184 public boolean poeMode(final UniFiDevice device, final List<JsonObject> data) throws UniFiException {
185 // Safety check to make sure no empty data is send to avoid corrupting override data on the device.
186 if (data.isEmpty() || data.stream().anyMatch(p -> p.entrySet().isEmpty())) {
187 logger.info("Not overriding port for '{}', because port data contains empty JSON: {}", device.getName(),
188 poeGson.toJson(data));
191 final UniFiControllerRequest<Void> req = newRequest(Void.class, HttpMethod.PUT, poeGson);
192 req.setAPIPath(String.format("/api/s/%s/rest/device/%s", device.getSite().getName(), device.getId()));
193 req.setBodyParameter("port_overrides", data);
199 public void poePowerCycle(final UniFiDevice device, final Integer portIdx) throws UniFiException {
200 final UniFiControllerRequest<Void> req = newRequest(Void.class, HttpMethod.POST, gson);
201 req.setAPIPath(String.format("/api/s/%s/cmd/devmgr", device.getSite().getName()));
202 req.setBodyParameter("cmd", "power-cycle");
203 req.setBodyParameter("mac", device.getMac());
204 req.setBodyParameter("port_idx", portIdx);
209 public void enableWifi(final UniFiWlan wlan, final boolean enable) throws UniFiException {
210 final UniFiControllerRequest<Void> req = newRequest(Void.class, HttpMethod.PUT, poeGson);
211 req.setAPIPath(String.format("/api/s/%s/rest/wlanconf/%s", wlan.getSite().getName(), wlan.getId()));
212 req.setBodyParameter("_id", wlan.getId());
213 req.setBodyParameter("enabled", enable ? "true" : "false");
218 public void generateVouchers(final UniFiSite site, final int count, final int expiration, final int users,
219 @Nullable Integer upLimit, @Nullable Integer downLimit, @Nullable Integer dataQuota) throws UniFiException {
220 final UniFiControllerRequest<Void> req = newRequest(Void.class, HttpMethod.POST, gson);
221 req.setAPIPath(String.format("/api/s/%s/cmd/hotspot", site.getName()));
222 req.setBodyParameter("cmd", "create-voucher");
223 req.setBodyParameter("expire", expiration);
224 req.setBodyParameter("n", count);
225 req.setBodyParameter("quota", users);
226 if (upLimit != null) {
227 req.setBodyParameter("up", upLimit);
229 if (downLimit != null) {
230 req.setBodyParameter("down", downLimit);
232 if (dataQuota != null) {
233 req.setBodyParameter("bytes", dataQuota);
239 public void revokeVouchers(final UniFiSite site, final List<UniFiVoucher> vouchers) throws UniFiException {
240 for (UniFiVoucher voucher : vouchers) {
241 final UniFiControllerRequest<Void> req = newRequest(Void.class, HttpMethod.POST, gson);
242 req.setAPIPath(String.format("/api/s/%s/cmd/hotspot", site.getName()));
243 req.setBodyParameter("cmd", "delete-voucher");
244 req.setBodyParameter("_id", voucher.getId());
252 private <T> UniFiControllerRequest<T> newRequest(final Class<T> responseType, final HttpMethod method,
254 return new UniFiControllerRequest<>(responseType, gson, httpClient, method, host, port, csrfToken, unifios);
257 private <T> @Nullable T executeRequest(final UniFiControllerRequest<T> request) throws UniFiException {
258 return executeRequest(request, false);
261 private <T> @Nullable T executeRequest(final UniFiControllerRequest<T> request, final boolean fromLogin)
262 throws UniFiException {
265 result = (T) request.execute();
266 csrfToken = request.getCsrfToken();
267 } catch (final UniFiExpiredSessionException e) {
269 // if this exception is thrown from a login attempt something is wrong, because the login should init
271 throw new UniFiCommunicationException(e);
274 result = (T) executeRequest(request);
276 } catch (final UniFiNotAuthorizedException e) {
277 logger.warn("Not Authorized! Please make sure your controller credentials have administrator rights");
283 private List<UniFiSite> refreshSites() throws UniFiException {
284 final UniFiControllerRequest<UniFiSite[]> req = newRequest(UniFiSite[].class, HttpMethod.GET, gson);
285 req.setAPIPath("/api/self/sites");
286 return cache.setSites(executeRequest(req));
289 private void refreshWlans(final Collection<UniFiSite> sites) throws UniFiException {
290 for (final UniFiSite site : sites) {
291 cache.putWlans(getWlans(site));
295 private UniFiWlan @Nullable [] getWlans(final UniFiSite site) throws UniFiException {
296 final UniFiControllerRequest<UniFiWlan[]> req = newRequest(UniFiWlan[].class, HttpMethod.GET, gson);
297 req.setAPIPath(String.format("/api/s/%s/rest/wlanconf", site.getName()));
298 return executeRequest(req);
301 private void refreshDevices(final Collection<UniFiSite> sites) throws UniFiException {
302 for (final UniFiSite site : sites) {
303 cache.putDevices(getDevices(site));
307 private UniFiDevice @Nullable [] getDevices(final UniFiSite site) throws UniFiException {
308 final UniFiControllerRequest<UniFiDevice[]> req = newRequest(UniFiDevice[].class, HttpMethod.GET, gson);
309 req.setAPIPath(String.format("/api/s/%s/stat/device", site.getName()));
310 return executeRequest(req);
313 private void refreshClients(final Collection<UniFiSite> sites) throws UniFiException {
314 for (final UniFiSite site : sites) {
315 cache.putClients(getClients(site));
319 private UniFiClient @Nullable [] getClients(final UniFiSite site) throws UniFiException {
320 final UniFiControllerRequest<UniFiClient[]> req = newRequest(UniFiClient[].class, HttpMethod.GET, gson);
321 req.setAPIPath(String.format("/api/s/%s/stat/sta", site.getName()));
322 return executeRequest(req);
325 private void refreshVouchers(final Collection<UniFiSite> sites) throws UniFiException {
326 for (final UniFiSite site : sites) {
327 cache.putVouchers(getVouchers(site));
331 private UniFiVoucher @Nullable [] getVouchers(final UniFiSite site) throws UniFiException {
332 final UniFiControllerRequest<UniFiVoucher[]> req = newRequest(UniFiVoucher[].class, HttpMethod.GET, gson);
333 req.setAPIPath(String.format("/api/s/%s/stat/voucher", site.getName()));
334 return executeRequest(req);
337 private void refreshInsights(final Collection<UniFiSite> sites) throws UniFiException {
338 for (final UniFiSite site : sites) {
339 cache.putInsights(getInsights(site));
343 private UniFiClient @Nullable [] getInsights(final UniFiSite site) throws UniFiException {
344 final UniFiControllerRequest<UniFiClient[]> req = newRequest(UniFiClient[].class, HttpMethod.GET, gson);
345 req.setAPIPath(String.format("/api/s/%s/stat/alluser", site.getName()));
346 req.setQueryParameter("within", INSIGHT_WITHIN_HOURS);
347 return executeRequest(req);