2 * Copyright (c) 2010-2023 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.handler;
15 import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_AP;
16 import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_BLOCKED;
17 import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_CMD;
18 import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_CMD_RECONNECT;
19 import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_ESSID;
20 import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_EXPERIENCE;
21 import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_GUEST;
22 import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_HOSTNAME;
23 import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_IP_ADDRESS;
24 import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_LAST_SEEN;
25 import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_MAC_ADDRESS;
26 import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_NAME;
27 import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_ONLINE;
28 import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_RECONNECT;
29 import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_RSSI;
30 import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_SITE;
31 import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_UPTIME;
32 import static org.openhab.core.thing.ThingStatus.OFFLINE;
33 import static org.openhab.core.thing.ThingStatusDetail.CONFIGURATION_ERROR;
35 import java.time.Instant;
36 import java.time.ZoneId;
37 import java.time.ZonedDateTime;
39 import org.eclipse.jdt.annotation.NonNullByDefault;
40 import org.eclipse.jdt.annotation.Nullable;
41 import org.openhab.binding.unifi.internal.UniFiClientThingConfig;
42 import org.openhab.binding.unifi.internal.api.UniFiController;
43 import org.openhab.binding.unifi.internal.api.UniFiException;
44 import org.openhab.binding.unifi.internal.api.cache.UniFiControllerCache;
45 import org.openhab.binding.unifi.internal.api.dto.UniFiClient;
46 import org.openhab.binding.unifi.internal.api.dto.UniFiDevice;
47 import org.openhab.binding.unifi.internal.api.dto.UniFiSite;
48 import org.openhab.binding.unifi.internal.api.dto.UniFiWiredClient;
49 import org.openhab.binding.unifi.internal.api.dto.UniFiWirelessClient;
50 import org.openhab.core.library.types.DateTimeType;
51 import org.openhab.core.library.types.DecimalType;
52 import org.openhab.core.library.types.OnOffType;
53 import org.openhab.core.library.types.QuantityType;
54 import org.openhab.core.library.types.StringType;
55 import org.openhab.core.library.unit.Units;
56 import org.openhab.core.thing.ChannelUID;
57 import org.openhab.core.thing.Thing;
58 import org.openhab.core.types.Command;
59 import org.openhab.core.types.State;
60 import org.openhab.core.types.UnDefType;
61 import org.slf4j.Logger;
62 import org.slf4j.LoggerFactory;
65 * The {@link UniFiClientThingHandler} is responsible for handling commands and status
66 * updates for UniFi Wireless Devices.
68 * @author Matthew Bowman - Initial contribution
69 * @author Patrik Wimnell - Blocking / Unblocking client support
72 public class UniFiClientThingHandler extends UniFiBaseThingHandler<UniFiClient, UniFiClientThingConfig> {
74 private final Logger logger = LoggerFactory.getLogger(UniFiClientThingHandler.class);
76 private UniFiClientThingConfig config = new UniFiClientThingConfig();
78 public UniFiClientThingHandler(final Thing thing) {
83 protected boolean initialize(final UniFiClientThingConfig config) {
84 // mgb: called when the config changes
85 logger.debug("Initializing the UniFi Client Handler with config = {}", config);
86 if (!config.isValid()) {
87 updateStatus(OFFLINE, CONFIGURATION_ERROR, "@text/error.thing.client.offline.configuration_error");
94 private static boolean belongsToSite(final UniFiClient client, final String siteName) {
95 boolean result = true; // mgb: assume true = proof by contradiction
96 if (!siteName.isEmpty()) {
97 final UniFiSite site = client.getSite();
98 // mgb: if the 'site' can't be found or the name doesn't match...
99 if (site == null || !site.matchesName(siteName)) {
100 // mgb: ... then the client doesn't belong to this thing's configured 'site' and we 'filter' it
108 protected @Nullable UniFiClient getEntity(final UniFiControllerCache cache) {
109 final UniFiClient client = cache.getClient(config.getClientID());
110 // mgb: short circuit
111 if (client == null || !belongsToSite(client, config.getSite())) {
118 protected State getDefaultState(final String channelID) {
125 case CHANNEL_MAC_ADDRESS:
126 case CHANNEL_IP_ADDRESS:
127 case CHANNEL_BLOCKED:
128 state = UnDefType.UNDEF;
131 // mgb: uptime should default to 0 seconds
132 state = new QuantityType<>(0, Units.SECOND);
134 case CHANNEL_EXPERIENCE:
135 // mgb: uptime + experience should default to 0
136 state = new QuantityType<>(0, Units.PERCENT);
138 case CHANNEL_LAST_SEEN:
139 // mgb: lastSeen should keep the last state no matter what
140 state = UnDefType.NULL;
142 case CHANNEL_RECONNECT:
143 state = OnOffType.OFF;
146 state = UnDefType.NULL;
152 private synchronized boolean isClientHome(final UniFiClient client) {
153 final boolean online;
155 final Instant lastSeen = client.getLastSeen();
156 if (lastSeen == null) {
158 logger.warn("Could not determine if client is online: cid = {}, lastSeen = null", config.getClientID());
160 final Instant considerHomeExpiry = lastSeen.plusSeconds(config.getConsiderHome());
161 online = Instant.now().isBefore(considerHomeExpiry);
167 protected State getChannelState(final UniFiClient client, final String channelId) {
168 final boolean clientHome = isClientHome(client);
169 final UniFiDevice device = client.getDevice();
170 final UniFiSite site = device == null ? null : device.getSite();
171 State state = getDefaultState(channelId);
174 // mgb: common wired + wireless client channels
178 state = OnOffType.from(clientHome);
183 if (client.getName() != null) {
184 state = StringType.valueOf(client.getName());
189 case CHANNEL_HOSTNAME:
190 if (client.getHostname() != null) {
191 state = StringType.valueOf(client.getHostname());
196 if (site != null && site.getDescription() != null && !site.getDescription().isBlank()) {
197 state = StringType.valueOf(site.getDescription());
202 case CHANNEL_MAC_ADDRESS:
203 if (client.getMac() != null && !client.getMac().isBlank()) {
204 state = StringType.valueOf(client.getMac());
209 case CHANNEL_IP_ADDRESS:
210 if (client.getIp() != null && !client.getIp().isBlank()) {
211 state = StringType.valueOf(client.getIp());
217 if (client.getUptime() != null) {
218 state = new QuantityType<>(client.getUptime(), Units.SECOND);
223 case CHANNEL_LAST_SEEN:
224 // mgb: we don't check clientOnline as lastSeen is also included in the Insights data
225 if (client.getLastSeen() != null) {
226 state = new DateTimeType(ZonedDateTime.ofInstant(client.getLastSeen(), ZoneId.systemDefault()));
231 case CHANNEL_BLOCKED:
232 state = OnOffType.from(client.isBlocked());
237 state = OnOffType.from(client.isGuest());
241 case CHANNEL_EXPERIENCE:
242 if (client.getExperience() != null) {
243 state = new QuantityType<>(client.getExperience(), Units.PERCENT);
248 // mgb: additional wired client channels
249 if (client.isWired() && (client instanceof UniFiWiredClient)) {
250 state = getWiredChannelState((UniFiWiredClient) client, channelId, state);
253 // mgb: additional wireless client channels
254 else if (client.isWireless() && (client instanceof UniFiWirelessClient)) {
255 state = getWirelessChannelState((UniFiWirelessClient) client, channelId, state);
262 private State getWiredChannelState(final UniFiWiredClient client, final String channelId,
263 final State defaultState) {
267 private State getWirelessChannelState(final UniFiWirelessClient client, final String channelId,
268 final State defaultState) {
269 State state = defaultState;
273 final UniFiDevice device = client.getDevice();
274 if (device != null && device.getName() != null && !device.getName().isBlank()) {
275 state = StringType.valueOf(device.getName());
281 if (client.getEssid() != null && !client.getEssid().isBlank()) {
282 state = StringType.valueOf(client.getEssid());
288 if (client.getRssi() != null) {
289 state = new DecimalType(client.getRssi());
294 case CHANNEL_RECONNECT:
295 // nop - trigger channel so it's always OFF by default
296 state = OnOffType.OFF;
303 protected boolean handleCommand(final UniFiController controller, final UniFiClient client,
304 final ChannelUID channelUID, final Command command) throws UniFiException {
305 final String channelID = channelUID.getIdWithoutGroup();
307 case CHANNEL_BLOCKED:
308 return handleBlockedCommand(controller, client, channelUID, command);
310 return handleReconnectCommand(controller, client, channelUID, command);
311 case CHANNEL_RECONNECT:
312 return handleReconnectSwitch(controller, client, channelUID, command);
318 private boolean handleBlockedCommand(final UniFiController controller, final UniFiClient client,
319 final ChannelUID channelUID, final Command command) throws UniFiException {
320 if (command instanceof OnOffType) {
321 controller.block(client, command == OnOffType.ON);
328 private boolean handleReconnectCommand(final UniFiController controller, final UniFiClient client,
329 final ChannelUID channelUID, final Command command) throws UniFiException {
330 if (command instanceof StringType && CHANNEL_CMD_RECONNECT.equalsIgnoreCase(command.toFullString())) {
331 controller.reconnect(client);
334 logger.info("Unknown command '{}' given to wireless client thing '{}': client {}", command,
335 getThing().getUID(), client);
340 private boolean handleReconnectSwitch(final UniFiController controller, final UniFiClient client,
341 final ChannelUID channelUID, final Command command) throws UniFiException {
342 if (command instanceof OnOffType && command == OnOffType.ON) {
343 controller.reconnect(client);
344 updateState(channelUID, OnOffType.OFF);