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.OnOffType;
52 import org.openhab.core.library.types.QuantityType;
53 import org.openhab.core.library.types.StringType;
54 import org.openhab.core.library.unit.Units;
55 import org.openhab.core.thing.ChannelUID;
56 import org.openhab.core.thing.Thing;
57 import org.openhab.core.types.Command;
58 import org.openhab.core.types.State;
59 import org.openhab.core.types.UnDefType;
60 import org.slf4j.Logger;
61 import org.slf4j.LoggerFactory;
64 * The {@link UniFiClientThingHandler} is responsible for handling commands and status
65 * updates for UniFi Wireless Devices.
67 * @author Matthew Bowman - Initial contribution
68 * @author Patrik Wimnell - Blocking / Unblocking client support
71 public class UniFiClientThingHandler extends UniFiBaseThingHandler<UniFiClient, UniFiClientThingConfig> {
73 private final Logger logger = LoggerFactory.getLogger(UniFiClientThingHandler.class);
75 private UniFiClientThingConfig config = new UniFiClientThingConfig();
77 public UniFiClientThingHandler(final Thing thing) {
82 protected boolean initialize(final UniFiClientThingConfig config) {
83 // mgb: called when the config changes
84 logger.debug("Initializing the UniFi Client Handler with config = {}", config);
85 if (!config.isValid()) {
86 updateStatus(OFFLINE, CONFIGURATION_ERROR, "@text/error.thing.client.offline.configuration_error");
93 private static boolean belongsToSite(final UniFiClient client, final String siteName) {
94 boolean result = true; // mgb: assume true = proof by contradiction
95 if (!siteName.isEmpty()) {
96 final UniFiSite site = client.getSite();
97 // mgb: if the 'site' can't be found or the name doesn't match...
98 if (site == null || !site.matchesName(siteName)) {
99 // mgb: ... then the client doesn't belong to this thing's configured 'site' and we 'filter' it
107 protected @Nullable UniFiClient getEntity(final UniFiControllerCache cache) {
108 final UniFiClient client = cache.getClient(config.getClientID());
109 // mgb: short circuit
110 if (client == null || !belongsToSite(client, config.getSite())) {
117 protected State getDefaultState(final String channelID) {
124 case CHANNEL_MAC_ADDRESS:
125 case CHANNEL_IP_ADDRESS:
126 case CHANNEL_BLOCKED:
127 state = UnDefType.UNDEF;
130 // mgb: uptime should default to 0 seconds
131 state = new QuantityType<>(0, Units.SECOND);
133 case CHANNEL_EXPERIENCE:
134 // mgb: uptime + experience should default to 0
135 state = new QuantityType<>(0, Units.PERCENT);
137 case CHANNEL_LAST_SEEN:
138 // mgb: lastSeen should keep the last state no matter what
139 state = UnDefType.NULL;
141 case CHANNEL_RECONNECT:
142 state = OnOffType.OFF;
145 state = UnDefType.NULL;
151 private synchronized boolean isClientHome(final UniFiClient client) {
152 final boolean online;
154 final Instant lastSeen = client.getLastSeen();
155 if (lastSeen == null) {
157 logger.warn("Could not determine if client is online: cid = {}, lastSeen = null", config.getClientID());
159 final Instant considerHomeExpiry = lastSeen.plusSeconds(config.getConsiderHome());
160 online = Instant.now().isBefore(considerHomeExpiry);
166 protected State getChannelState(final UniFiClient client, final String channelId) {
167 final boolean clientHome = isClientHome(client);
168 final UniFiDevice device = client.getDevice();
169 final UniFiSite site = device == null ? null : device.getSite();
170 State state = getDefaultState(channelId);
173 // mgb: common wired + wireless client channels
177 state = OnOffType.from(clientHome);
182 if (client.getName() != null) {
183 state = StringType.valueOf(client.getName());
188 case CHANNEL_HOSTNAME:
189 if (client.getHostname() != null) {
190 state = StringType.valueOf(client.getHostname());
195 if (site != null && site.getDescription() != null && !site.getDescription().isBlank()) {
196 state = StringType.valueOf(site.getDescription());
201 case CHANNEL_MAC_ADDRESS:
202 if (client.getMac() != null && !client.getMac().isBlank()) {
203 state = StringType.valueOf(client.getMac());
208 case CHANNEL_IP_ADDRESS:
209 if (client.getIp() != null && !client.getIp().isBlank()) {
210 state = StringType.valueOf(client.getIp());
216 if (client.getUptime() != null) {
217 state = new QuantityType<>(client.getUptime(), Units.SECOND);
222 case CHANNEL_LAST_SEEN:
223 // mgb: we don't check clientOnline as lastSeen is also included in the Insights data
224 if (client.getLastSeen() != null) {
225 state = new DateTimeType(ZonedDateTime.ofInstant(client.getLastSeen(), ZoneId.systemDefault()));
230 case CHANNEL_BLOCKED:
231 state = OnOffType.from(client.isBlocked());
236 state = OnOffType.from(client.isGuest());
240 case CHANNEL_EXPERIENCE:
241 if (client.getExperience() != null) {
242 state = new QuantityType<>(client.getExperience(), Units.PERCENT);
247 // mgb: additional wired client channels
248 if (client.isWired() && (client instanceof UniFiWiredClient wiredClient)) {
249 state = getWiredChannelState(wiredClient, channelId, state);
252 // mgb: additional wireless client channels
253 else if (client.isWireless() && (client instanceof UniFiWirelessClient wirelessClient)) {
254 state = getWirelessChannelState(wirelessClient, channelId, state);
261 private State getWiredChannelState(final UniFiWiredClient client, final String channelId,
262 final State defaultState) {
266 private State getWirelessChannelState(final UniFiWirelessClient client, final String channelId,
267 final State defaultState) {
268 State state = defaultState;
272 final UniFiDevice device = client.getDevice();
273 if (device != null && device.getName() != null && !device.getName().isBlank()) {
274 state = StringType.valueOf(device.getName());
280 if (client.getEssid() != null && !client.getEssid().isBlank()) {
281 state = StringType.valueOf(client.getEssid());
287 if (client.getRssi() != null) {
288 state = new QuantityType<>(client.getRssi(), Units.DECIBEL_MILLIWATTS);
293 case CHANNEL_RECONNECT:
294 // nop - trigger channel so it's always OFF by default
295 state = OnOffType.OFF;
302 protected boolean handleCommand(final UniFiController controller, final UniFiClient client,
303 final ChannelUID channelUID, final Command command) throws UniFiException {
304 final String channelID = channelUID.getIdWithoutGroup();
306 case CHANNEL_BLOCKED:
307 return handleBlockedCommand(controller, client, channelUID, command);
309 return handleReconnectCommand(controller, client, channelUID, command);
310 case CHANNEL_RECONNECT:
311 return handleReconnectSwitch(controller, client, channelUID, command);
317 private boolean handleBlockedCommand(final UniFiController controller, final UniFiClient client,
318 final ChannelUID channelUID, final Command command) throws UniFiException {
319 if (command instanceof OnOffType) {
320 controller.block(client, command == OnOffType.ON);
327 private boolean handleReconnectCommand(final UniFiController controller, final UniFiClient client,
328 final ChannelUID channelUID, final Command command) throws UniFiException {
329 if (command instanceof StringType && CHANNEL_CMD_RECONNECT.equalsIgnoreCase(command.toFullString())) {
330 controller.reconnect(client);
333 logger.info("Unknown command '{}' given to wireless client thing '{}': client {}", command,
334 getThing().getUID(), client);
339 private boolean handleReconnectSwitch(final UniFiController controller, final UniFiClient client,
340 final ChannelUID channelUID, final Command command) throws UniFiException {
341 if (command instanceof OnOffType && command == OnOffType.ON) {
342 controller.reconnect(client);
343 updateState(channelUID, OnOffType.OFF);