]> git.basschouten.com Git - openhab-addons.git/blob
abd7082bac7c5c0795b3e705924405d60e40d941
[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.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             if (getWebThingURI() != null) {
101                 logger.debug("try to connect WebThing {}", webThingURI);
102                 var connected = tryReconnect();
103                 if (connected) {
104                     logger.debug("WebThing {} connected", getWebThingLabel());
105                 }
106             } else {
107                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
108                         "webThing uri has not been set");
109                 logger.warn("could not initialize WebThing. URI is not set or invalid. {}", this.webThingURI);
110             }
111         });
112
113         // starting watchdog that checks the healthiness of the WebThing connection, periodically
114         watchdogHandle
115                 .getAndSet(Optional.of(scheduler.scheduleWithFixedDelay(this::checkWebThingConnection,
116                         HEALTH_CHECK_PERIOD.getSeconds(), HEALTH_CHECK_PERIOD.getSeconds(), TimeUnit.SECONDS)))
117                 .ifPresent(future -> future.cancel(true));
118     }
119
120     private @Nullable URI getWebThingURI() {
121         if (webThingURI == null) {
122             webThingURI = toUri(getConfigAs(WebThingConfiguration.class).webThingURI);
123         }
124         return webThingURI;
125     }
126
127     private @Nullable URI toUri(@Nullable String uri) {
128         try {
129             if (uri != null) {
130                 return URI.create(uri);
131             }
132         } catch (IllegalArgumentException illegalURIException) {
133             return null;
134         }
135         return null;
136     }
137
138     @Override
139     public void dispose() {
140         try {
141             isActivated.set(false); // set to false to avoid reconnecting
142
143             // terminate WebThing connection as well as the alive watchdog
144             webThingConnectionRef.getAndSet(Optional.empty()).ifPresent(ConsumedThing::close);
145             watchdogHandle.getAndSet(Optional.empty()).ifPresent(future -> future.cancel(true));
146         } finally {
147             super.dispose();
148         }
149     }
150
151     private boolean tryReconnect() {
152         if (isActivated.get()) { // will try reconnect only, if activated
153             try {
154                 // create the client-side WebThing representation
155                 var uri = getWebThingURI();
156                 if (uri != null) {
157                     var webThing = ConsumedThingFactory.instance().create(webSocketClient, httpClient, uri, scheduler,
158                             this::onError);
159                     this.webThingConnectionRef.getAndSet(Optional.of(webThing)).ifPresent(ConsumedThing::close);
160
161                     // update the Thing structure based on the WebThing description
162                     thingStructureChanged(webThing);
163
164                     // link the Thing's channels with the WebThing properties to forward properties/item updates
165                     establishWebThingChannelLinks(webThing);
166
167                     lastReconnect.set(Instant.now());
168                     updateStatus(ThingStatus.ONLINE);
169                     return true;
170                 }
171             } catch (IOException e) {
172                 var msg = e.getMessage();
173                 if (msg == null) {
174                     msg = "";
175                 }
176                 onError(msg);
177             }
178         }
179         return false;
180     }
181
182     public void onError(String reason) {
183         var wasConnectedBefore = isOnline();
184         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason);
185
186         // close the WebThing connection. If the handler is still active, the WebThing connection
187         // will be re-established within the periodically watchdog task
188         webThingConnectionRef.getAndSet(Optional.empty()).ifPresent(ConsumedThing::close);
189
190         if (wasConnectedBefore) { // to reduce log messages, just log in case of connection state changed
191             logger.debug("WebThing {} disconnected {}. Try reconnect (each {} sec)", getWebThingLabel(), reason,
192                     HEALTH_CHECK_PERIOD.getSeconds());
193         } else {
194             logger.debug("WebThing {} is offline {}. Try reconnect (each {} sec)", getWebThingLabel(), reason,
195                     HEALTH_CHECK_PERIOD.getSeconds());
196         }
197     }
198
199     private String getWebThingLabel() {
200         if (getThing().getLabel() == null) {
201             return "" + webThingURI;
202         } else {
203             return "'" + getThing().getLabel() + "' (" + webThingURI + ")";
204         }
205     }
206
207     /**
208      * updates the thing structure. Refer https://www.openhab.org/docs/developer/bindings/#updating-the-thing-structure
209      *
210      * @param webThing the WebThing that is used for the new structure
211      */
212     private void thingStructureChanged(ConsumedThing webThing) {
213         var thingBuilder = editThing().withLabel(webThing.getThingDescription().title);
214
215         // create a channel for each WebThing property
216         for (var entry : webThing.getThingDescription().properties.entrySet()) {
217             var channel = Channels.createChannel(thing.getUID(), entry.getKey(), entry.getValue());
218             // add channel (and remove a previous one, if exist)
219             thingBuilder.withoutChannel(channel.getUID()).withChannel(channel);
220         }
221         var thing = thingBuilder.build();
222
223         // and update the thing
224         updateThing(thing);
225     }
226
227     /**
228      * connects each WebThing property with a corresponding openHAB channel. After this changes will be synchronized
229      * between a WebThing property and the openHAB channel
230      *
231      * @param webThing the WebThing to be connected
232      * @throws IOException if the channels can not be connected
233      */
234     private void establishWebThingChannelLinks(ConsumedThing webThing) throws IOException {
235         // remove all registered listeners
236         itemChangedListenerMap.clear();
237
238         // create new links (listeners will be registered, implicitly)
239         for (var namePropertyPair : webThing.getThingDescription().properties.entrySet()) {
240             try {
241                 // determine the name of the associated channel
242                 var channelUID = Channels.createChannelUID(getThing().getUID(), namePropertyPair.getKey());
243
244                 // will try to establish a link, if channel is present
245                 var channel = getThing().getChannel(channelUID);
246                 if (channel != null) {
247                     // establish downstream link
248                     PropertyToChannelLink.establish(webThing, namePropertyPair.getKey(), this, channel);
249
250                     // establish upstream link
251                     if (!namePropertyPair.getValue().readOnly) {
252                         ChannelToPropertyLink.establish(this, channel, webThing, namePropertyPair.getKey());
253                     }
254                 }
255             } catch (UnknownPropertyException upe) {
256                 logger.warn("WebThing {} property {} could not be linked with a channel", getWebThingLabel(),
257                         namePropertyPair.getKey(), upe);
258             }
259         }
260     }
261
262     @Override
263     public void handleCommand(ChannelUID channelUID, Command command) {
264         if (command instanceof State) {
265             itemChangedListenerMap.getOrDefault(channelUID, EMPTY_ITEM_CHANGED_LISTENER).onItemStateChanged(channelUID,
266                     (State) command);
267         } else if (command instanceof RefreshType) {
268             tryReconnect();
269         }
270     }
271
272     /////////////
273     // ChannelHandler methods
274     @Override
275     public void observeChannel(ChannelUID channelUID, ItemChangedListener listener) {
276         itemChangedListenerMap.put(channelUID, listener);
277     }
278
279     @Override
280     public void updateItemState(ChannelUID channelUID, Command command) {
281         if (isActivated.get()) {
282             postCommand(channelUID, command);
283         }
284     }
285     //
286     /////////////
287
288     private void checkWebThingConnection() {
289         // try reconnect, if necessary
290         if (isDisconnected() || (isOnline() && !isAlive())) {
291             logger.debug("try reconnecting WebThing {}", getWebThingLabel());
292             if (tryReconnect()) {
293                 logger.debug("WebThing {} reconnected", getWebThingLabel());
294             }
295
296         } else {
297             // force reconnecting periodically, to fix erroneous states that occurs for unknown reasons
298             var elapsedSinceLastReconnect = Duration.between(lastReconnect.get(), Instant.now());
299             if (isOnline() && (elapsedSinceLastReconnect.getSeconds() > RECONNECT_PERIOD.getSeconds())) {
300                 if (tryReconnect()) {
301                     logger.debug("WebThing {} reconnected. Initiated by periodic reconnect", getWebThingLabel());
302                 } else {
303                     logger.debug("could not reconnect WebThing {} (periodic reconnect failed). Next trial in {} sec",
304                             getWebThingLabel(), HEALTH_CHECK_PERIOD.getSeconds());
305                 }
306
307             }
308         }
309     }
310 }