2 * Copyright (c) 2010-2022 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.tplinksmarthome.internal.handler;
15 import static org.openhab.binding.tplinksmarthome.internal.TPLinkSmartHomeBindingConstants.*;
17 import java.io.IOException;
18 import java.time.Duration;
19 import java.util.concurrent.ScheduledFuture;
20 import java.util.concurrent.TimeUnit;
22 import org.eclipse.jdt.annotation.NonNullByDefault;
23 import org.eclipse.jdt.annotation.Nullable;
24 import org.eclipse.jetty.util.StringUtil;
25 import org.openhab.binding.tplinksmarthome.internal.Connection;
26 import org.openhab.binding.tplinksmarthome.internal.TPLinkIpAddressService;
27 import org.openhab.binding.tplinksmarthome.internal.TPLinkSmartHomeConfiguration;
28 import org.openhab.binding.tplinksmarthome.internal.TPLinkSmartHomeThingType;
29 import org.openhab.binding.tplinksmarthome.internal.TPLinkSmartHomeThingType.DeviceType;
30 import org.openhab.binding.tplinksmarthome.internal.device.DeviceState;
31 import org.openhab.binding.tplinksmarthome.internal.device.SmartHomeDevice;
32 import org.openhab.core.cache.ExpiringCache;
33 import org.openhab.core.config.core.Configuration;
34 import org.openhab.core.library.types.QuantityType;
35 import org.openhab.core.library.unit.Units;
36 import org.openhab.core.thing.ChannelUID;
37 import org.openhab.core.thing.Thing;
38 import org.openhab.core.thing.ThingStatus;
39 import org.openhab.core.thing.ThingStatusDetail;
40 import org.openhab.core.thing.binding.BaseThingHandler;
41 import org.openhab.core.types.Command;
42 import org.openhab.core.types.RefreshType;
43 import org.openhab.core.types.State;
44 import org.openhab.core.types.UnDefType;
45 import org.slf4j.Logger;
46 import org.slf4j.LoggerFactory;
49 * Handler class for TP-Link Smart Home devices.
51 * @author Christian Fischer - Initial contribution
52 * @author Hilbrand Bouwkamp - Rewrite to generic TP-Link Smart Home Handler
55 public class SmartHomeHandler extends BaseThingHandler {
57 private static final Duration ONE_SECOND = Duration.ofSeconds(1);
59 private final Logger logger = LoggerFactory.getLogger(SmartHomeHandler.class);
61 private final SmartHomeDevice smartHomeDevice;
62 private final TPLinkIpAddressService ipAddressService;
63 private final int forceRefreshThreshold;
65 private @NonNullByDefault({}) TPLinkSmartHomeConfiguration configuration;
66 private @NonNullByDefault({}) Connection connection;
67 private @NonNullByDefault({}) ScheduledFuture<?> refreshJob;
68 private @NonNullByDefault({}) ExpiringCache<@Nullable DeviceState> cache;
70 * Cache to avoid refresh is called multiple time in 1 second.
72 private @NonNullByDefault({}) ExpiringCache<@Nullable DeviceState> fastCache;
77 * @param thing The thing to handle
78 * @param smartHomeDevice Specific Smart Home device handler
79 * @param type The device type
80 * @param ipAddressService Cache keeping track of ip addresses of tp link devices
82 public SmartHomeHandler(Thing thing, SmartHomeDevice smartHomeDevice, TPLinkSmartHomeThingType type,
83 TPLinkIpAddressService ipAddressService) {
85 this.smartHomeDevice = smartHomeDevice;
86 this.ipAddressService = ipAddressService;
87 this.forceRefreshThreshold = type.getDeviceType() == DeviceType.SWITCH
88 || type.getDeviceType() == DeviceType.DIMMER ? FORCED_REFRESH_BOUNDERY_SWITCHED_SECONDS
89 : FORCED_REFRESH_BOUNDERY_SECONDS;
93 public void handleCommand(ChannelUID channelUid, Command command) {
95 if (command instanceof RefreshType) {
96 updateChannelState(channelUid, fastCache.getValue());
97 } else if (smartHomeDevice.handleCommand(channelUid, command)) {
98 // After a command always refresh the cache to make sure the cache has the latest data
99 updateChannelState(channelUid, forceCacheUpdate());
101 logger.debug("Command {} is not supported for channel: {}", command, channelUid.getId());
103 } catch (IOException e) {
104 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
109 public void dispose() {
110 if (refreshJob != null && !refreshJob.isCancelled()) {
111 refreshJob.cancel(true);
117 public void initialize() {
118 configuration = getConfigAs(TPLinkSmartHomeConfiguration.class);
119 if (StringUtil.isBlank(configuration.ipAddress) && StringUtil.isBlank(configuration.deviceId)) {
120 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
121 "No ip address or the device id configured.");
124 logger.debug("Initializing TP-Link Smart device on ip '{}' or deviceId '{}' ", configuration.ipAddress,
125 configuration.deviceId);
126 connection = createConnection(configuration);
127 smartHomeDevice.initialize(connection, configuration);
128 cache = new ExpiringCache<>(Duration.ofSeconds(configuration.refresh), this::refreshCache);
129 // If refresh > threshold fast cache invalidates after 1 second, else it behaves just as the 'normal' cache
130 fastCache = configuration.refresh > forceRefreshThreshold
131 ? new ExpiringCache<>(ONE_SECOND, this::forceCacheUpdate)
133 updateStatus(ThingStatus.UNKNOWN);
134 // While config.xml defines refresh as min 1, this check is used to run a test that doesn't start refresh.
135 if (configuration.refresh > 0) {
136 startAutomaticRefresh(configuration);
141 * Creates new Connection. Methods makes mocking of the connection in tests possible.
143 * @param config configuration to be used by the connection
144 * @return new Connection object
146 Connection createConnection(TPLinkSmartHomeConfiguration config) {
147 return new Connection(config.ipAddress);
151 * Invalidates the cache to force an update. It returns the refreshed cached value.
153 * @return the refreshed value
155 private @Nullable DeviceState forceCacheUpdate() {
156 cache.invalidateValue();
157 return cache.getValue();
160 private @Nullable DeviceState refreshCache() {
163 final DeviceState deviceState = new DeviceState(connection.sendCommand(smartHomeDevice.getUpdateCommand()));
164 updateDeviceId(deviceState.getSysinfo().getDeviceId());
165 smartHomeDevice.refreshedDeviceState(deviceState);
166 if (getThing().getStatus() != ThingStatus.ONLINE) {
167 updateStatus(ThingStatus.ONLINE);
170 } catch (IOException e) {
171 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
173 } catch (RuntimeException e) {
174 logger.debug("Obtaining new device data unexpectedly crashed. If this keeps happening please report: ", e);
175 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.DISABLED, e.getMessage());
181 * Checks if the current configured ip addres is still the same as by which the device is registered on the network.
182 * If there is a different ip address for this device it will update the configuration with this ip and start using
185 private void updateIpAddress() {
186 if (configuration.deviceId == null) {
187 // The device id is needed to get the ip address so if not known no need to continue.
190 String lastKnownIpAddress = ipAddressService.getLastKnownIpAddress(configuration.deviceId);
192 if (lastKnownIpAddress != null && !lastKnownIpAddress.equals(configuration.ipAddress)) {
193 Configuration editConfig = editConfiguration();
194 editConfig.put(CONFIG_IP, lastKnownIpAddress);
195 updateConfiguration(editConfig);
196 configuration.ipAddress = lastKnownIpAddress;
197 connection.setIpAddress(lastKnownIpAddress);
202 * Updates the device id configuration if it's not set or throws an {@link IllegalArgumentException} if the
203 * configured device id doesn't match with the id reported by the device.
205 * @param actualDeviceId The id of the device as actual returned by the device.
206 * @throws IllegalArgumentException if the configured device id doesn't match with the id reported by the device
209 private void updateDeviceId(String actualDeviceId) {
210 if (StringUtil.isBlank(configuration.deviceId)) {
211 Configuration editConfig = editConfiguration();
212 editConfig.put(CONFIG_DEVICE_ID, actualDeviceId);
213 updateConfiguration(editConfig);
214 configuration.deviceId = actualDeviceId;
215 } else if (!StringUtil.isBlank(actualDeviceId) && !actualDeviceId.equals(configuration.deviceId)) {
216 throw new IllegalArgumentException(
217 String.format("The configured device '%s' doesn't match with the id the device reports: '%s'.",
218 configuration.deviceId, actualDeviceId));
223 * Starts the background refresh thread.
225 private void startAutomaticRefresh(TPLinkSmartHomeConfiguration config) {
226 if (refreshJob == null || refreshJob.isCancelled()) {
227 refreshJob = scheduler.scheduleWithFixedDelay(this::refreshChannels, config.refresh, config.refresh,
232 void refreshChannels() {
233 logger.trace("Update Channels for:{}", thing.getUID());
234 getThing().getChannels().forEach(channel -> updateChannelState(channel.getUID(), cache.getValue()));
238 * Updates the state from the device data for the channel given the data..
240 * @param channelUID channel to update
241 * @param deviceState the state object containing the value to set of the channel
244 private void updateChannelState(ChannelUID channelUID, @Nullable DeviceState deviceState) {
245 if (!isLinked(channelUID)) {
248 String channelId = channelUID.isInGroup() ? channelUID.getIdWithoutGroup() : channelUID.getId();
251 if (deviceState == null) {
252 state = UnDefType.UNDEF;
253 } else if (CHANNEL_RSSI.equals(channelId)) {
254 state = new QuantityType<>(deviceState.getSysinfo().getRssi(), Units.DECIBEL_MILLIWATTS);
256 state = smartHomeDevice.updateChannel(channelUID, deviceState);
258 updateState(channelUID, state);