]> git.basschouten.com Git - openhab-addons.git/blob
3fa13510d8764afb30aa31fa90ec6c1c14c2d0e3
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.tplinksmarthome.internal.handler;
14
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;
20
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;
27
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;
54
55 /**
56  * Handler class for TP-Link Smart Home devices.
57  *
58  * @author Christian Fischer - Initial contribution
59  * @author Hilbrand Bouwkamp - Rewrite to generic TP-Link Smart Home Handler
60  */
61 @NonNullByDefault
62 public class SmartHomeHandler extends BaseThingHandler {
63
64     private static final Duration ONE_SECOND = Duration.ofSeconds(1);
65     private static final int CONNECTION_IO_RETRIES = 5;
66
67     private final Logger logger = LoggerFactory.getLogger(SmartHomeHandler.class);
68
69     private final SmartHomeDevice smartHomeDevice;
70     private final TPLinkIpAddressService ipAddressService;
71     private final int forceRefreshThreshold;
72
73     private @NonNullByDefault({}) TPLinkSmartHomeConfiguration configuration;
74     private @NonNullByDefault({}) Connection connection;
75     private @NonNullByDefault({}) ScheduledFuture<?> refreshJob;
76     private @NonNullByDefault({}) ExpiringCache<@Nullable DeviceState> cache;
77     /**
78      * Cache to avoid refresh is called multiple time in 1 second.
79      */
80     private @NonNullByDefault({}) ExpiringCache<@Nullable DeviceState> fastCache;
81
82     /**
83      * Constructor.
84      *
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
89      */
90     public SmartHomeHandler(final Thing thing, final SmartHomeDevice smartHomeDevice,
91             final TPLinkSmartHomeThingType type, final TPLinkIpAddressService ipAddressService) {
92         super(thing);
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;
98     }
99
100     @Override
101     public Collection<Class<? extends ThingHandlerService>> getServices() {
102         return List.of(TPLinkSmartHomeActions.class);
103     }
104
105     Connection getConnection() {
106         return connection;
107     }
108
109     @Override
110     public void handleCommand(final ChannelUID channelUid, final Command command) {
111         try {
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());
117             } else {
118                 logger.debug("Command {} is not supported for channel: {}", command, channelUid.getId());
119             }
120         } catch (final IOException e) {
121             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
122         }
123     }
124
125     @Override
126     public void dispose() {
127         if (refreshJob != null && !refreshJob.isCancelled()) {
128             refreshJob.cancel(true);
129             refreshJob = null;
130         }
131     }
132
133     @Override
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.");
139             return;
140         }
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)
149                 : cache;
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);
154         }
155     }
156
157     /**
158      * Creates new Connection. Methods makes mocking of the connection in tests possible.
159      *
160      * @param config configuration to be used by the connection
161      * @return new Connection object
162      */
163     Connection createConnection(final TPLinkSmartHomeConfiguration config) {
164         return new Connection(config.ipAddress);
165     }
166
167     /**
168      * Invalidates the cache to force an update. It returns the refreshed cached value.
169      *
170      * @return the refreshed value
171      */
172     private @Nullable DeviceState forceCacheUpdate() {
173         cache.invalidateValue();
174         return cache.getValue();
175     }
176
177     private @Nullable DeviceState refreshCache() {
178         int retry = 1;
179
180         while (true) {
181             try {
182                 updateIpAddress();
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);
189                 }
190                 return deviceState;
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);
195                     retry++;
196                 } else {
197                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
198                     return null;
199                 }
200             } catch (final RuntimeException e) {
201                 logger.debug("Obtaining new device data unexpectedly crashed. If this keeps happening please report: ",
202                         e);
203                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.DISABLED, e.getMessage());
204                 return null;
205             }
206         }
207     }
208
209     /**
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
212      * this ip address.
213      */
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.
217             return;
218         }
219         final String lastKnownIpAddress = ipAddressService.getLastKnownIpAddress(configuration.deviceId);
220
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);
227         }
228     }
229
230     /**
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.
233      *
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
236      *             itself.
237      */
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));
248         }
249     }
250
251     /**
252      * Starts the background refresh thread.
253      */
254     private void startAutomaticRefresh(final TPLinkSmartHomeConfiguration config) {
255         if (refreshJob == null || refreshJob.isCancelled()) {
256             refreshJob = scheduler.scheduleWithFixedDelay(this::refreshChannels, config.refresh, config.refresh,
257                     TimeUnit.SECONDS);
258         }
259     }
260
261     void refreshChannels() {
262         logger.trace("Update Channels for:{}", thing.getUID());
263         getThing().getChannels().forEach(channel -> updateChannelState(channel.getUID(), cache.getValue()));
264     }
265
266     /**
267      * Updates the state from the device data for the channel given the data..
268      *
269      * @param channelUID channel to update
270      * @param deviceState the state object containing the value to set of the channel
271      *
272      */
273     private void updateChannelState(final ChannelUID channelUID, @Nullable final DeviceState deviceState) {
274         if (!isLinked(channelUID)) {
275             return;
276         }
277         final String channelId = channelUID.isInGroup() ? channelUID.getIdWithoutGroup() : channelUID.getId();
278         final State state;
279
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);
284         } else {
285             state = smartHomeDevice.updateChannel(channelUID, deviceState);
286         }
287         updateState(channelUID, state);
288     }
289 }