2 * Copyright (c) 2010-2021 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.webthing.internal;
15 import java.io.IOException;
17 import java.time.Duration;
18 import java.time.Instant;
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;
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;
49 * The {@link WebThingHandler} is responsible for handling commands, which are
50 * sent to one of the channels.
52 * @author Gregor Roth - Initial contribution
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) -> {
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<>(
68 private final AtomicReference<Instant> lastReconnect = new AtomicReference<>(Instant.now());
69 private final AtomicReference<Optional<ScheduledFuture<?>>> watchdogHandle = new AtomicReference<>(
71 private @Nullable URI webThingURI = null;
73 public WebThingHandler(Thing thing, HttpClient httpClient, WebSocketClient webSocketClient) {
75 this.httpClient = httpClient;
76 this.webSocketClient = webSocketClient;
79 private boolean isOnline() {
80 return getThing().getStatus() == ThingStatus.ONLINE;
83 private boolean isDisconnected() {
84 return (getThing().getStatus() == ThingStatus.OFFLINE) || (getThing().getStatus() == ThingStatus.UNKNOWN);
87 private boolean isAlive() {
88 return webThingConnectionRef.get().map(ConsumedThing::isAlive).orElse(false);
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
97 // perform connect in background
98 scheduler.execute(() -> {
99 // WebThing URI present?
100 var uri = toUri(getConfigAs(WebThingConfiguration.class).webThingURI);
102 logger.debug("try to connect WebThing {}", uri);
103 var connected = tryReconnect(uri);
105 logger.debug("WebThing {} connected", getWebThingLabel());
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);
114 // starting watchdog that checks the healthiness of the WebThing connection, periodically
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));
121 private @Nullable URI toUri(@Nullable String uri) {
124 return URI.create(uri);
126 } catch (IllegalArgumentException illegalURIException) {
133 public void dispose() {
135 isActivated.set(false); // set to false to avoid reconnecting
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));
145 private boolean tryReconnect(@Nullable URI uri) {
146 if (isActivated.get()) { // will try reconnect only, if activated
148 // create the client-side WebThing representation
150 var webThing = ConsumedThingFactory.instance().create(webSocketClient, httpClient, uri, scheduler,
152 this.webThingConnectionRef.getAndSet(Optional.of(webThing)).ifPresent(ConsumedThing::close);
154 // update the Thing structure based on the WebThing description
155 thingStructureChanged(webThing);
157 // link the Thing's channels with the WebThing properties to forward properties/item updates
158 establishWebThingChannelLinks(webThing);
160 lastReconnect.set(Instant.now());
161 updateStatus(ThingStatus.ONLINE);
164 } catch (IOException e) {
165 var msg = e.getMessage();
175 public void onError(String reason) {
176 var wasConnectedBefore = isOnline();
177 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason);
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);
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());
187 logger.debug("WebThing {} is offline {}. Try reconnect (each {} sec)", getWebThingLabel(), reason,
188 HEALTH_CHECK_PERIOD.getSeconds());
192 private String getWebThingLabel() {
193 if (getThing().getLabel() == null) {
194 return "" + webThingURI;
196 return "'" + getThing().getLabel() + "' (" + webThingURI + ")";
201 * updates the thing structure. Refer https://www.openhab.org/docs/developer/bindings/#updating-the-thing-structure
203 * @param webThing the WebThing that is used for the new structure
205 private void thingStructureChanged(ConsumedThing webThing) {
206 var thingBuilder = editThing().withLabel(webThing.getThingDescription().title);
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);
214 var thing = thingBuilder.build();
216 // and update the thing
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
224 * @param webThing the WebThing to be connected
225 * @throws IOException if the channels can not be connected
227 private void establishWebThingChannelLinks(ConsumedThing webThing) throws IOException {
228 // remove all registered listeners
229 itemChangedListenerMap.clear();
231 // create new links (listeners will be registered, implicitly)
232 for (var namePropertyPair : webThing.getThingDescription().properties.entrySet()) {
234 // determine the name of the associated channel
235 var channelUID = Channels.createChannelUID(getThing().getUID(), namePropertyPair.getKey());
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);
243 // establish upstream link
244 if (!namePropertyPair.getValue().readOnly) {
245 ChannelToPropertyLink.establish(this, channel, webThing, namePropertyPair.getKey());
248 } catch (UnknownPropertyException upe) {
249 logger.warn("WebThing {} property {} could not be linked with a channel", getWebThingLabel(),
250 namePropertyPair.getKey(), upe);
256 public void handleCommand(ChannelUID channelUID, Command command) {
257 if (command instanceof State) {
258 itemChangedListenerMap.getOrDefault(channelUID, EMPTY_ITEM_CHANGED_LISTENER).onItemStateChanged(channelUID,
260 } else if (command instanceof RefreshType) {
261 tryReconnect(webThingURI);
266 // ChannelHandler methods
268 public void observeChannel(ChannelUID channelUID, ItemChangedListener listener) {
269 itemChangedListenerMap.put(channelUID, listener);
273 public void updateItemState(ChannelUID channelUID, Command command) {
274 if (isActivated.get()) {
275 postCommand(channelUID, command);
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());
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());
296 logger.debug("could not reconnect WebThing {} (periodic reconnect failed). Next trial in {} sec",
297 getWebThingLabel(), HEALTH_CHECK_PERIOD.getSeconds());