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.tplinksmarthome.internal.handler;
15 import static org.openhab.binding.tplinksmarthome.internal.TPLinkSmartHomeBindingConstants.CHANNEL_RSSI;
16 import static org.openhab.binding.tplinksmarthome.internal.TPLinkSmartHomeBindingConstants.CONFIG_DEVICE_ID;
17 import static org.openhab.binding.tplinksmarthome.internal.TPLinkSmartHomeBindingConstants.CONFIG_IP;
18 import static org.openhab.binding.tplinksmarthome.internal.TPLinkSmartHomeBindingConstants.FORCED_REFRESH_BOUNDERY_SECONDS;
19 import static org.openhab.binding.tplinksmarthome.internal.TPLinkSmartHomeBindingConstants.FORCED_REFRESH_BOUNDERY_SWITCHED_SECONDS;
21 import java.io.IOException;
22 import java.time.Duration;
23 import java.util.Collection;
24 import java.util.List;
25 import java.util.concurrent.ScheduledFuture;
26 import java.util.concurrent.TimeUnit;
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.eclipse.jetty.util.StringUtil;
31 import org.openhab.binding.tplinksmarthome.internal.Connection;
32 import org.openhab.binding.tplinksmarthome.internal.TPLinkIpAddressService;
33 import org.openhab.binding.tplinksmarthome.internal.TPLinkSmartHomeConfiguration;
34 import org.openhab.binding.tplinksmarthome.internal.TPLinkSmartHomeThingType;
35 import org.openhab.binding.tplinksmarthome.internal.TPLinkSmartHomeThingType.DeviceType;
36 import org.openhab.binding.tplinksmarthome.internal.device.DeviceState;
37 import org.openhab.binding.tplinksmarthome.internal.device.SmartHomeDevice;
38 import org.openhab.core.cache.ExpiringCache;
39 import org.openhab.core.config.core.Configuration;
40 import org.openhab.core.library.types.QuantityType;
41 import org.openhab.core.library.unit.Units;
42 import org.openhab.core.thing.ChannelUID;
43 import org.openhab.core.thing.Thing;
44 import org.openhab.core.thing.ThingStatus;
45 import org.openhab.core.thing.ThingStatusDetail;
46 import org.openhab.core.thing.binding.BaseThingHandler;
47 import org.openhab.core.thing.binding.ThingHandlerService;
48 import org.openhab.core.types.Command;
49 import org.openhab.core.types.RefreshType;
50 import org.openhab.core.types.State;
51 import org.openhab.core.types.UnDefType;
52 import org.slf4j.Logger;
53 import org.slf4j.LoggerFactory;
56 * Handler class for TP-Link Smart Home devices.
58 * @author Christian Fischer - Initial contribution
59 * @author Hilbrand Bouwkamp - Rewrite to generic TP-Link Smart Home Handler
62 public class SmartHomeHandler extends BaseThingHandler {
64 private static final Duration ONE_SECOND = Duration.ofSeconds(1);
65 private static final int CONNECTION_IO_RETRIES = 5;
67 private final Logger logger = LoggerFactory.getLogger(SmartHomeHandler.class);
69 private final SmartHomeDevice smartHomeDevice;
70 private final TPLinkIpAddressService ipAddressService;
71 private final int forceRefreshThreshold;
73 private @NonNullByDefault({}) TPLinkSmartHomeConfiguration configuration;
74 private @NonNullByDefault({}) Connection connection;
75 private @NonNullByDefault({}) ScheduledFuture<?> refreshJob;
76 private @NonNullByDefault({}) ExpiringCache<@Nullable DeviceState> cache;
78 * Cache to avoid refresh is called multiple time in 1 second.
80 private @NonNullByDefault({}) ExpiringCache<@Nullable DeviceState> fastCache;
85 * @param thing The thing to handle
86 * @param smartHomeDevice Specific Smart Home device handler
87 * @param type The device type
88 * @param ipAddressService Cache keeping track of ip addresses of tp link devices
90 public SmartHomeHandler(final Thing thing, final SmartHomeDevice smartHomeDevice,
91 final TPLinkSmartHomeThingType type, final TPLinkIpAddressService ipAddressService) {
93 this.smartHomeDevice = smartHomeDevice;
94 this.ipAddressService = ipAddressService;
95 this.forceRefreshThreshold = type.getDeviceType() == DeviceType.SWITCH
96 || type.getDeviceType() == DeviceType.DIMMER ? FORCED_REFRESH_BOUNDERY_SWITCHED_SECONDS
97 : FORCED_REFRESH_BOUNDERY_SECONDS;
101 public Collection<Class<? extends ThingHandlerService>> getServices() {
102 return List.of(TPLinkSmartHomeActions.class);
105 Connection getConnection() {
110 public void handleCommand(final ChannelUID channelUid, final Command command) {
112 if (command instanceof RefreshType) {
113 updateChannelState(channelUid, fastCache.getValue());
114 } else if (smartHomeDevice.handleCommand(channelUid, command)) {
115 // After a command always refresh the cache to make sure the cache has the latest data
116 updateChannelState(channelUid, forceCacheUpdate());
118 logger.debug("Command {} is not supported for channel: {}", command, channelUid.getId());
120 } catch (final IOException e) {
121 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
126 public void dispose() {
127 if (refreshJob != null && !refreshJob.isCancelled()) {
128 refreshJob.cancel(true);
134 public void initialize() {
135 configuration = getConfigAs(TPLinkSmartHomeConfiguration.class);
136 if (StringUtil.isBlank(configuration.ipAddress) && StringUtil.isBlank(configuration.deviceId)) {
137 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
138 "No ip address or the device id configured.");
141 logger.debug("Initializing TP-Link Smart device on ip '{}' or deviceId '{}' ", configuration.ipAddress,
142 configuration.deviceId);
143 connection = createConnection(configuration);
144 smartHomeDevice.initialize(connection, configuration);
145 cache = new ExpiringCache<>(Duration.ofSeconds(configuration.refresh), this::refreshCache);
146 // If refresh > threshold fast cache invalidates after 1 second, else it behaves just as the 'normal' cache
147 fastCache = configuration.refresh > forceRefreshThreshold
148 ? new ExpiringCache<>(ONE_SECOND, this::forceCacheUpdate)
150 updateStatus(ThingStatus.UNKNOWN);
151 // While config.xml defines refresh as min 1, this check is used to run a test that doesn't start refresh.
152 if (configuration.refresh > 0) {
153 startAutomaticRefresh(configuration);
158 * Creates new Connection. Methods makes mocking of the connection in tests possible.
160 * @param config configuration to be used by the connection
161 * @return new Connection object
163 Connection createConnection(final TPLinkSmartHomeConfiguration config) {
164 return new Connection(config.ipAddress);
168 * Invalidates the cache to force an update. It returns the refreshed cached value.
170 * @return the refreshed value
172 private @Nullable DeviceState forceCacheUpdate() {
173 cache.invalidateValue();
174 return cache.getValue();
177 private @Nullable DeviceState refreshCache() {
183 final DeviceState deviceState = new DeviceState(
184 connection.sendCommand(smartHomeDevice.getUpdateCommand()));
185 updateDeviceId(deviceState.getSysinfo().getDeviceId());
186 smartHomeDevice.refreshedDeviceState(deviceState);
187 if (getThing().getStatus() != ThingStatus.ONLINE) {
188 updateStatus(ThingStatus.ONLINE);
191 } catch (final IOException e) {
192 // If there is a connection problem retry before throwing an exception
193 if (retry < CONNECTION_IO_RETRIES) {
194 logger.trace("Communication error, retry {}", retry, e);
197 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
200 } catch (final RuntimeException e) {
201 logger.debug("Obtaining new device data unexpectedly crashed. If this keeps happening please report: ",
203 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.DISABLED, e.getMessage());
210 * Checks if the current configured ip addres is still the same as by which the device is registered on the network.
211 * If there is a different ip address for this device it will update the configuration with this ip and start using
214 private void updateIpAddress() {
215 if (configuration.deviceId == null) {
216 // The device id is needed to get the ip address so if not known no need to continue.
219 final String lastKnownIpAddress = ipAddressService.getLastKnownIpAddress(configuration.deviceId);
221 if (lastKnownIpAddress != null && !lastKnownIpAddress.equals(configuration.ipAddress)) {
222 final Configuration editConfig = editConfiguration();
223 editConfig.put(CONFIG_IP, lastKnownIpAddress);
224 updateConfiguration(editConfig);
225 configuration.ipAddress = lastKnownIpAddress;
226 connection.setIpAddress(lastKnownIpAddress);
231 * Updates the device id configuration if it's not set or throws an {@link IllegalArgumentException} if the
232 * configured device id doesn't match with the id reported by the device.
234 * @param actualDeviceId The id of the device as actual returned by the device.
235 * @throws IllegalArgumentException if the configured device id doesn't match with the id reported by the device
238 private void updateDeviceId(final String actualDeviceId) {
239 if (StringUtil.isBlank(configuration.deviceId)) {
240 final Configuration editConfig = editConfiguration();
241 editConfig.put(CONFIG_DEVICE_ID, actualDeviceId);
242 updateConfiguration(editConfig);
243 configuration.deviceId = actualDeviceId;
244 } else if (!StringUtil.isBlank(actualDeviceId) && !actualDeviceId.equals(configuration.deviceId)) {
245 throw new IllegalArgumentException(
246 String.format("The configured device '%s' doesn't match with the id the device reports: '%s'.",
247 configuration.deviceId, actualDeviceId));
252 * Starts the background refresh thread.
254 private void startAutomaticRefresh(final TPLinkSmartHomeConfiguration config) {
255 if (refreshJob == null || refreshJob.isCancelled()) {
256 refreshJob = scheduler.scheduleWithFixedDelay(this::refreshChannels, config.refresh, config.refresh,
261 void refreshChannels() {
262 logger.trace("Update Channels for:{}", thing.getUID());
263 getThing().getChannels().forEach(channel -> updateChannelState(channel.getUID(), cache.getValue()));
267 * Updates the state from the device data for the channel given the data..
269 * @param channelUID channel to update
270 * @param deviceState the state object containing the value to set of the channel
273 private void updateChannelState(final ChannelUID channelUID, @Nullable final DeviceState deviceState) {
274 if (!isLinked(channelUID)) {
277 final String channelId = channelUID.isInGroup() ? channelUID.getIdWithoutGroup() : channelUID.getId();
280 if (deviceState == null) {
281 state = UnDefType.UNDEF;
282 } else if (CHANNEL_RSSI.equals(channelId)) {
283 state = new QuantityType<>(deviceState.getSysinfo().getRssi(), Units.DECIBEL_MILLIWATTS);
285 state = smartHomeDevice.updateChannel(channelUID, deviceState);
287 updateState(channelUID, state);