]> git.basschouten.com Git - openhab-addons.git/blob
8e1c7cc61db1472ff96327542d3b52809234c86e
[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.webthing.internal;
14
15 import java.io.IOException;
16 import java.net.URI;
17 import java.time.Duration;
18 import java.time.Instant;
19 import java.util.Map;
20 import java.util.Optional;
21 import java.util.concurrent.ConcurrentHashMap;
22 import java.util.concurrent.ScheduledFuture;
23 import java.util.concurrent.TimeUnit;
24 import java.util.concurrent.atomic.AtomicBoolean;
25 import java.util.concurrent.atomic.AtomicReference;
26
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.eclipse.jetty.client.HttpClient;
30 import org.eclipse.jetty.websocket.client.WebSocketClient;
31 import org.openhab.binding.webthing.internal.channel.Channels;
32 import org.openhab.binding.webthing.internal.client.ConsumedThing;
33 import org.openhab.binding.webthing.internal.client.ConsumedThingFactory;
34 import org.openhab.binding.webthing.internal.link.ChannelToPropertyLink;
35 import org.openhab.binding.webthing.internal.link.PropertyToChannelLink;
36 import org.openhab.binding.webthing.internal.link.UnknownPropertyException;
37 import org.openhab.core.thing.ChannelUID;
38 import org.openhab.core.thing.Thing;
39 import org.openhab.core.thing.ThingStatus;
40 import org.openhab.core.thing.ThingStatusDetail;
41 import org.openhab.core.thing.binding.BaseThingHandler;
42 import org.openhab.core.types.Command;
43 import org.openhab.core.types.RefreshType;
44 import org.openhab.core.types.State;
45 import org.slf4j.Logger;
46 import org.slf4j.LoggerFactory;
47
48 /**
49  * The {@link WebThingHandler} is responsible for handling commands, which are
50  * sent to one of the channels.
51  *
52  * @author Gregor Roth - Initial contribution
53  */
54 @NonNullByDefault
55 public class WebThingHandler extends BaseThingHandler implements ChannelHandler {
56     private static final Duration RECONNECT_PERIOD = Duration.ofHours(23);
57     private static final Duration HEALTH_CHECK_PERIOD = Duration.ofSeconds(70);
58     private static final ItemChangedListener EMPTY_ITEM_CHANGED_LISTENER = (channelUID, stateCommand) -> {
59     };
60
61     private final Logger logger = LoggerFactory.getLogger(WebThingHandler.class);
62     private final HttpClient httpClient;
63     private final WebSocketClient webSocketClient;
64     private final AtomicBoolean isActivated = new AtomicBoolean(true);
65     private final Map<ChannelUID, ItemChangedListener> itemChangedListenerMap = new ConcurrentHashMap<>();
66     private final AtomicReference<Optional<ConsumedThing>> webThingConnectionRef = new AtomicReference<>(
67             Optional.empty());
68     private final AtomicReference<Instant> lastReconnect = new AtomicReference<>(Instant.now());
69     private final AtomicReference<Optional<ScheduledFuture<?>>> watchdogHandle = new AtomicReference<>(
70             Optional.empty());
71     private @Nullable URI webThingURI = null;
72
73     public WebThingHandler(Thing thing, HttpClient httpClient, WebSocketClient webSocketClient) {
74         super(thing);
75         this.httpClient = httpClient;
76         this.webSocketClient = webSocketClient;
77     }
78
79     private boolean isOnline() {
80         return getThing().getStatus() == ThingStatus.ONLINE;
81     }
82
83     private boolean isDisconnected() {
84         return (getThing().getStatus() == ThingStatus.OFFLINE) || (getThing().getStatus() == ThingStatus.UNKNOWN);
85     }
86
87     private boolean isAlive() {
88         return webThingConnectionRef.get().map(ConsumedThing::isAlive).orElse(false);
89     }
90
91     @Override
92     public void initialize() {
93         updateStatus(ThingStatus.UNKNOWN);
94         isActivated.set(true); // set with true, even though the connect may fail. In this case retries will be
95                                // triggered
96
97         // perform connect in background
98         scheduler.execute(() -> {
99             // WebThing URI present?
100             var uri = toUri(getConfigAs(WebThingConfiguration.class).webThingURI);
101             if (uri != null) {
102                 logger.debug("try to connect WebThing {}", uri);
103                 var connected = tryReconnect(uri);
104                 if (connected) {
105                     logger.debug("WebThing {} connected", getWebThingLabel());
106                 }
107             } else {
108                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
109                         "webThing uri has not been set");
110                 logger.warn("could not initialize WebThing. URI is not set or invalid. {}", this.webThingURI);
111             }
112         });
113
114         // starting watchdog that checks the healthiness of the WebThing connection, periodically
115         watchdogHandle
116                 .getAndSet(Optional.of(scheduler.scheduleWithFixedDelay(this::checkWebThingConnection,
117                         HEALTH_CHECK_PERIOD.getSeconds(), HEALTH_CHECK_PERIOD.getSeconds(), TimeUnit.SECONDS)))
118                 .ifPresent(future -> future.cancel(true));
119     }
120
121     private @Nullable URI toUri(@Nullable String uri) {
122         try {
123             if (uri != null) {
124                 return URI.create(uri);
125             }
126         } catch (IllegalArgumentException illegalURIException) {
127             return null;
128         }
129         return null;
130     }
131
132     @Override
133     public void dispose() {
134         try {
135             isActivated.set(false); // set to false to avoid reconnecting
136
137             // terminate WebThing connection as well as the alive watchdog
138             webThingConnectionRef.getAndSet(Optional.empty()).ifPresent(ConsumedThing::close);
139             watchdogHandle.getAndSet(Optional.empty()).ifPresent(future -> future.cancel(true));
140         } finally {
141             super.dispose();
142         }
143     }
144
145     private boolean tryReconnect(@Nullable URI uri) {
146         if (isActivated.get()) { // will try reconnect only, if activated
147             try {
148                 // create the client-side WebThing representation
149                 if (uri != null) {
150                     var webThing = ConsumedThingFactory.instance().create(webSocketClient, httpClient, uri, scheduler,
151                             this::onError);
152                     this.webThingConnectionRef.getAndSet(Optional.of(webThing)).ifPresent(ConsumedThing::close);
153
154                     // update the Thing structure based on the WebThing description
155                     thingStructureChanged(webThing);
156
157                     // link the Thing's channels with the WebThing properties to forward properties/item updates
158                     establishWebThingChannelLinks(webThing);
159
160                     lastReconnect.set(Instant.now());
161                     updateStatus(ThingStatus.ONLINE);
162                     return true;
163                 }
164             } catch (IOException e) {
165                 var msg = e.getMessage();
166                 if (msg == null) {
167                     msg = "";
168                 }
169                 onError(msg);
170             }
171         }
172         return false;
173     }
174
175     public void onError(String reason) {
176         var wasConnectedBefore = isOnline();
177         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason);
178
179         // close the WebThing connection. If the handler is still active, the WebThing connection
180         // will be re-established within the periodically watchdog task
181         webThingConnectionRef.getAndSet(Optional.empty()).ifPresent(ConsumedThing::close);
182
183         if (wasConnectedBefore) { // to reduce log messages, just log in case of connection state changed
184             logger.debug("WebThing {} disconnected {}. Try reconnect (each {} sec)", getWebThingLabel(), reason,
185                     HEALTH_CHECK_PERIOD.getSeconds());
186         } else {
187             logger.debug("WebThing {} is offline {}. Try reconnect (each {} sec)", getWebThingLabel(), reason,
188                     HEALTH_CHECK_PERIOD.getSeconds());
189         }
190     }
191
192     private String getWebThingLabel() {
193         if (getThing().getLabel() == null) {
194             return "" + webThingURI;
195         } else {
196             return "'" + getThing().getLabel() + "' (" + webThingURI + ")";
197         }
198     }
199
200     /**
201      * updates the thing structure. Refer https://www.openhab.org/docs/developer/bindings/#updating-the-thing-structure
202      *
203      * @param webThing the WebThing that is used for the new structure
204      */
205     private void thingStructureChanged(ConsumedThing webThing) {
206         var thingBuilder = editThing().withLabel(webThing.getThingDescription().title);
207
208         // create a channel for each WebThing property
209         for (var entry : webThing.getThingDescription().properties.entrySet()) {
210             var channel = Channels.createChannel(thing.getUID(), entry.getKey(), entry.getValue());
211             // add channel (and remove a previous one, if exist)
212             thingBuilder.withoutChannel(channel.getUID()).withChannel(channel);
213         }
214         var thing = thingBuilder.build();
215
216         // and update the thing
217         updateThing(thing);
218     }
219
220     /**
221      * connects each WebThing property with a corresponding openHAB channel. After this changes will be synchronized
222      * between a WebThing property and the openHAB channel
223      *
224      * @param webThing the WebThing to be connected
225      * @throws IOException if the channels can not be connected
226      */
227     private void establishWebThingChannelLinks(ConsumedThing webThing) throws IOException {
228         // remove all registered listeners
229         itemChangedListenerMap.clear();
230
231         // create new links (listeners will be registered, implicitly)
232         for (var namePropertyPair : webThing.getThingDescription().properties.entrySet()) {
233             try {
234                 // determine the name of the associated channel
235                 var channelUID = Channels.createChannelUID(getThing().getUID(), namePropertyPair.getKey());
236
237                 // will try to establish a link, if channel is present
238                 var channel = getThing().getChannel(channelUID);
239                 if (channel != null) {
240                     // establish downstream link
241                     PropertyToChannelLink.establish(webThing, namePropertyPair.getKey(), this, channel);
242
243                     // establish upstream link
244                     if (!namePropertyPair.getValue().readOnly) {
245                         ChannelToPropertyLink.establish(this, channel, webThing, namePropertyPair.getKey());
246                     }
247                 }
248             } catch (UnknownPropertyException upe) {
249                 logger.warn("WebThing {} property {} could not be linked with a channel", getWebThingLabel(),
250                         namePropertyPair.getKey(), upe);
251             }
252         }
253     }
254
255     @Override
256     public void handleCommand(ChannelUID channelUID, Command command) {
257         if (command instanceof State) {
258             itemChangedListenerMap.getOrDefault(channelUID, EMPTY_ITEM_CHANGED_LISTENER).onItemStateChanged(channelUID,
259                     (State) command);
260         } else if (command instanceof RefreshType) {
261             tryReconnect(webThingURI);
262         }
263     }
264
265     /////////////
266     // ChannelHandler methods
267     @Override
268     public void observeChannel(ChannelUID channelUID, ItemChangedListener listener) {
269         itemChangedListenerMap.put(channelUID, listener);
270     }
271
272     @Override
273     public void updateItemState(ChannelUID channelUID, Command command) {
274         if (isActivated.get()) {
275             postCommand(channelUID, command);
276         }
277     }
278     //
279     /////////////
280
281     private void checkWebThingConnection() {
282         // try reconnect, if necessary
283         if (isDisconnected() || (isOnline() && !isAlive())) {
284             logger.debug("try reconnecting WebThing {}", getWebThingLabel());
285             if (tryReconnect(webThingURI)) {
286                 logger.debug("WebThing {} reconnected", getWebThingLabel());
287             }
288
289         } else {
290             // force reconnecting periodically, to fix erroneous states that occurs for unknown reasons
291             var elapsedSinceLastReconnect = Duration.between(lastReconnect.get(), Instant.now());
292             if (isOnline() && (elapsedSinceLastReconnect.getSeconds() > RECONNECT_PERIOD.getSeconds())) {
293                 if (tryReconnect(webThingURI)) {
294                     logger.debug("WebThing {} reconnected. Initiated by periodic reconnect", getWebThingLabel());
295                 } else {
296                     logger.debug("could not reconnect WebThing {} (periodic reconnect failed). Next trial in {} sec",
297                             getWebThingLabel(), HEALTH_CHECK_PERIOD.getSeconds());
298                 }
299
300             }
301         }
302     }
303 }