]> git.basschouten.com Git - openhab-addons.git/blob
60f3c5e301fd5f786e7dc40c0560d08211b11091
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.*;
16
17 import java.io.IOException;
18 import java.time.Duration;
19 import java.util.concurrent.ScheduledFuture;
20 import java.util.concurrent.TimeUnit;
21
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;
47
48 /**
49  * Handler class for TP-Link Smart Home devices.
50  *
51  * @author Christian Fischer - Initial contribution
52  * @author Hilbrand Bouwkamp - Rewrite to generic TP-Link Smart Home Handler
53  */
54 @NonNullByDefault
55 public class SmartHomeHandler extends BaseThingHandler {
56
57     private static final Duration ONE_SECOND = Duration.ofSeconds(1);
58
59     private final Logger logger = LoggerFactory.getLogger(SmartHomeHandler.class);
60
61     private final SmartHomeDevice smartHomeDevice;
62     private final TPLinkIpAddressService ipAddressService;
63     private final int forceRefreshThreshold;
64
65     private @NonNullByDefault({}) TPLinkSmartHomeConfiguration configuration;
66     private @NonNullByDefault({}) Connection connection;
67     private @NonNullByDefault({}) ScheduledFuture<?> refreshJob;
68     private @NonNullByDefault({}) ExpiringCache<@Nullable DeviceState> cache;
69     /**
70      * Cache to avoid refresh is called multiple time in 1 second.
71      */
72     private @NonNullByDefault({}) ExpiringCache<@Nullable DeviceState> fastCache;
73
74     /**
75      * Constructor.
76      *
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
81      */
82     public SmartHomeHandler(Thing thing, SmartHomeDevice smartHomeDevice, TPLinkSmartHomeThingType type,
83             TPLinkIpAddressService ipAddressService) {
84         super(thing);
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;
90     }
91
92     @Override
93     public void handleCommand(ChannelUID channelUid, Command command) {
94         try {
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());
100             } else {
101                 logger.debug("Command {} is not supported for channel: {}", command, channelUid.getId());
102             }
103         } catch (IOException e) {
104             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
105         }
106     }
107
108     @Override
109     public void dispose() {
110         if (refreshJob != null && !refreshJob.isCancelled()) {
111             refreshJob.cancel(true);
112             refreshJob = null;
113         }
114     }
115
116     @Override
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.");
122             return;
123         }
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)
132                 : cache;
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);
137         }
138     }
139
140     /**
141      * Creates new Connection. Methods makes mocking of the connection in tests possible.
142      *
143      * @param config configuration to be used by the connection
144      * @return new Connection object
145      */
146     Connection createConnection(TPLinkSmartHomeConfiguration config) {
147         return new Connection(config.ipAddress);
148     }
149
150     /**
151      * Invalidates the cache to force an update. It returns the refreshed cached value.
152      *
153      * @return the refreshed value
154      */
155     private @Nullable DeviceState forceCacheUpdate() {
156         cache.invalidateValue();
157         return cache.getValue();
158     }
159
160     private @Nullable DeviceState refreshCache() {
161         try {
162             updateIpAddress();
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);
168             }
169             return deviceState;
170         } catch (IOException e) {
171             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
172             return null;
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());
176             return null;
177         }
178     }
179
180     /**
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
183      * this ip address.
184      */
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.
188             return;
189         }
190         String lastKnownIpAddress = ipAddressService.getLastKnownIpAddress(configuration.deviceId);
191
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);
198         }
199     }
200
201     /**
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.
204      *
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
207      *             itself.
208      */
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));
219         }
220     }
221
222     /**
223      * Starts the background refresh thread.
224      */
225     private void startAutomaticRefresh(TPLinkSmartHomeConfiguration config) {
226         if (refreshJob == null || refreshJob.isCancelled()) {
227             refreshJob = scheduler.scheduleWithFixedDelay(this::refreshChannels, config.refresh, config.refresh,
228                     TimeUnit.SECONDS);
229         }
230     }
231
232     void refreshChannels() {
233         logger.trace("Update Channels for:{}", thing.getUID());
234         getThing().getChannels().forEach(channel -> updateChannelState(channel.getUID(), cache.getValue()));
235     }
236
237     /**
238      * Updates the state from the device data for the channel given the data..
239      *
240      * @param channelUID channel to update
241      * @param deviceState the state object containing the value to set of the channel
242      *
243      */
244     private void updateChannelState(ChannelUID channelUID, @Nullable DeviceState deviceState) {
245         if (!isLinked(channelUID)) {
246             return;
247         }
248         String channelId = channelUID.isInGroup() ? channelUID.getIdWithoutGroup() : channelUID.getId();
249         final State state;
250
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);
255         } else {
256             state = smartHomeDevice.updateChannel(channelUID, deviceState);
257         }
258         updateState(channelUID, state);
259     }
260 }