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_IP_ADDRESS;
23 import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_LAST_SEEN;
24 import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_MAC_ADDRESS;
25 import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_ONLINE;
26 import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_RECONNECT;
27 import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_RSSI;
28 import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_SITE;
29 import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_UPTIME;
30 import static org.openhab.core.thing.ThingStatus.OFFLINE;
31 import static org.openhab.core.thing.ThingStatusDetail.CONFIGURATION_ERROR;
33 import java.time.Instant;
34 import java.time.ZoneId;
35 import java.time.ZonedDateTime;
37 import org.eclipse.jdt.annotation.NonNullByDefault;
38 import org.eclipse.jdt.annotation.Nullable;
39 import org.openhab.binding.unifi.internal.UniFiClientThingConfig;
40 import org.openhab.binding.unifi.internal.api.UniFiController;
41 import org.openhab.binding.unifi.internal.api.UniFiException;
42 import org.openhab.binding.unifi.internal.api.cache.UniFiControllerCache;
43 import org.openhab.binding.unifi.internal.api.dto.UniFiClient;
44 import org.openhab.binding.unifi.internal.api.dto.UniFiDevice;
45 import org.openhab.binding.unifi.internal.api.dto.UniFiSite;
46 import org.openhab.binding.unifi.internal.api.dto.UniFiWiredClient;
47 import org.openhab.binding.unifi.internal.api.dto.UniFiWirelessClient;
48 import org.openhab.core.library.types.DateTimeType;
49 import org.openhab.core.library.types.DecimalType;
50 import org.openhab.core.library.types.OnOffType;
51 import org.openhab.core.library.types.QuantityType;
52 import org.openhab.core.library.types.StringType;
53 import org.openhab.core.library.unit.Units;
54 import org.openhab.core.thing.ChannelUID;
55 import org.openhab.core.thing.Thing;
56 import org.openhab.core.types.Command;
57 import org.openhab.core.types.State;
58 import org.openhab.core.types.UnDefType;
59 import org.slf4j.Logger;
60 import org.slf4j.LoggerFactory;
63 * The {@link UniFiClientThingHandler} is responsible for handling commands and status
64 * updates for UniFi Wireless Devices.
66 * @author Matthew Bowman - Initial contribution
67 * @author Patrik Wimnell - Blocking / Unblocking client support
70 public class UniFiClientThingHandler extends UniFiBaseThingHandler<UniFiClient, UniFiClientThingConfig> {
72 private final Logger logger = LoggerFactory.getLogger(UniFiClientThingHandler.class);
74 private UniFiClientThingConfig config = new UniFiClientThingConfig();
76 public UniFiClientThingHandler(final Thing thing) {
81 protected boolean initialize(final UniFiClientThingConfig config) {
82 // mgb: called when the config changes
83 logger.debug("Initializing the UniFi Client Handler with config = {}", config);
84 if (!config.isValid()) {
85 updateStatus(OFFLINE, CONFIGURATION_ERROR, "@text/error.thing.client.offline.configuration_error");
92 private static boolean belongsToSite(final UniFiClient client, final String siteName) {
93 boolean result = true; // mgb: assume true = proof by contradiction
94 if (!siteName.isEmpty()) {
95 final UniFiSite site = client.getSite();
96 // mgb: if the 'site' can't be found or the name doesn't match...
97 if (site == null || !site.matchesName(siteName)) {
98 // mgb: ... then the client doesn't belong to this thing's configured 'site' and we 'filter' it
106 protected @Nullable UniFiClient getEntity(final UniFiControllerCache cache) {
107 final UniFiClient client = cache.getClient(config.getClientID());
108 // mgb: short circuit
109 if (client == null || !belongsToSite(client, config.getSite())) {
116 protected State getDefaultState(final String channelID) {
123 case CHANNEL_MAC_ADDRESS:
124 case CHANNEL_IP_ADDRESS:
125 case CHANNEL_BLOCKED:
126 state = UnDefType.UNDEF;
129 // mgb: uptime should default to 0 seconds
130 state = new QuantityType<>(0, Units.SECOND);
132 case CHANNEL_EXPERIENCE:
133 // mgb: uptime + experience should default to 0
134 state = new QuantityType<>(0, Units.PERCENT);
136 case CHANNEL_LAST_SEEN:
137 // mgb: lastSeen should keep the last state no matter what
138 state = UnDefType.NULL;
140 case CHANNEL_RECONNECT:
141 state = OnOffType.OFF;
144 state = UnDefType.NULL;
150 private synchronized boolean isClientHome(final UniFiClient client) {
151 final boolean online;
153 final Instant lastSeen = client.getLastSeen();
154 if (lastSeen == null) {
156 logger.warn("Could not determine if client is online: cid = {}, lastSeen = null", config.getClientID());
158 final Instant considerHomeExpiry = lastSeen.plusSeconds(config.getConsiderHome());
159 online = Instant.now().isBefore(considerHomeExpiry);
165 protected State getChannelState(final UniFiClient client, final String channelId) {
166 final boolean clientHome = isClientHome(client);
167 final UniFiDevice device = client.getDevice();
168 final UniFiSite site = device == null ? null : device.getSite();
169 State state = getDefaultState(channelId);
172 // mgb: common wired + wireless client channels
176 state = OnOffType.from(clientHome);
181 if (site != null && site.getDescription() != null && !site.getDescription().isBlank()) {
182 state = StringType.valueOf(site.getDescription());
187 case CHANNEL_MAC_ADDRESS:
188 if (client.getMac() != null && !client.getMac().isBlank()) {
189 state = StringType.valueOf(client.getMac());
194 case CHANNEL_IP_ADDRESS:
195 if (client.getIp() != null && !client.getIp().isBlank()) {
196 state = StringType.valueOf(client.getIp());
202 if (client.getUptime() != null) {
203 state = new QuantityType<>(client.getUptime(), Units.SECOND);
208 case CHANNEL_LAST_SEEN:
209 // mgb: we don't check clientOnline as lastSeen is also included in the Insights data
210 if (client.getLastSeen() != null) {
211 state = new DateTimeType(ZonedDateTime.ofInstant(client.getLastSeen(), ZoneId.systemDefault()));
216 case CHANNEL_BLOCKED:
217 state = OnOffType.from(client.isBlocked());
222 state = OnOffType.from(client.isGuest());
226 case CHANNEL_EXPERIENCE:
227 if (client.getExperience() != null) {
228 state = new QuantityType<>(client.getExperience(), Units.PERCENT);
233 // mgb: additional wired client channels
234 if (client.isWired() && (client instanceof UniFiWiredClient)) {
235 state = getWiredChannelState((UniFiWiredClient) client, channelId, state);
238 // mgb: additional wireless client channels
239 else if (client.isWireless() && (client instanceof UniFiWirelessClient)) {
240 state = getWirelessChannelState((UniFiWirelessClient) client, channelId, state);
247 private State getWiredChannelState(final UniFiWiredClient client, final String channelId,
248 final State defaultState) {
252 private State getWirelessChannelState(final UniFiWirelessClient client, final String channelId,
253 final State defaultState) {
254 State state = defaultState;
258 final UniFiDevice device = client.getDevice();
259 if (device != null && device.getName() != null && !device.getName().isBlank()) {
260 state = StringType.valueOf(device.getName());
266 if (client.getEssid() != null && !client.getEssid().isBlank()) {
267 state = StringType.valueOf(client.getEssid());
273 if (client.getRssi() != null) {
274 state = new DecimalType(client.getRssi());
279 case CHANNEL_RECONNECT:
280 // nop - trigger channel so it's always OFF by default
281 state = OnOffType.OFF;
288 protected boolean handleCommand(final UniFiController controller, final UniFiClient client,
289 final ChannelUID channelUID, final Command command) throws UniFiException {
290 final String channelID = channelUID.getIdWithoutGroup();
292 case CHANNEL_BLOCKED:
293 return handleBlockedCommand(controller, client, channelUID, command);
295 return handleReconnectCommand(controller, client, channelUID, command);
296 case CHANNEL_RECONNECT:
297 return handleReconnectSwitch(controller, client, channelUID, command);
303 private boolean handleBlockedCommand(final UniFiController controller, final UniFiClient client,
304 final ChannelUID channelUID, final Command command) throws UniFiException {
305 if (command instanceof OnOffType) {
306 controller.block(client, command == OnOffType.ON);
313 private boolean handleReconnectCommand(final UniFiController controller, final UniFiClient client,
314 final ChannelUID channelUID, final Command command) throws UniFiException {
315 if (command instanceof StringType && CHANNEL_CMD_RECONNECT.equalsIgnoreCase(command.toFullString())) {
316 controller.reconnect(client);
319 logger.info("Unknown command '{}' given to wireless client thing '{}': client {}", command,
320 getThing().getUID(), client);
325 private boolean handleReconnectSwitch(final UniFiController controller, final UniFiClient client,
326 final ChannelUID channelUID, final Command command) throws UniFiException {
327 if (command instanceof OnOffType && command == OnOffType.ON) {
328 controller.reconnect(client);
329 updateState(channelUID, OnOffType.OFF);