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.remoteopenhab.internal.handler;
15 import static org.openhab.binding.remoteopenhab.internal.RemoteopenhabBindingConstants.BINDING_ID;
17 import java.net.MalformedURLException;
19 import java.time.ZonedDateTime;
20 import java.time.format.DateTimeFormatter;
21 import java.time.format.DateTimeParseException;
22 import java.util.ArrayList;
23 import java.util.Collection;
24 import java.util.Collections;
25 import java.util.HashMap;
26 import java.util.List;
28 import java.util.concurrent.ScheduledFuture;
29 import java.util.concurrent.TimeUnit;
30 import java.util.stream.Collectors;
32 import javax.ws.rs.client.ClientBuilder;
34 import org.eclipse.jdt.annotation.NonNullByDefault;
35 import org.eclipse.jdt.annotation.Nullable;
36 import org.eclipse.jetty.client.HttpClient;
37 import org.openhab.binding.remoteopenhab.internal.RemoteopenhabChannelTypeProvider;
38 import org.openhab.binding.remoteopenhab.internal.RemoteopenhabStateDescriptionOptionProvider;
39 import org.openhab.binding.remoteopenhab.internal.config.RemoteopenhabServerConfiguration;
40 import org.openhab.binding.remoteopenhab.internal.data.RemoteopenhabItem;
41 import org.openhab.binding.remoteopenhab.internal.data.RemoteopenhabStateDescription;
42 import org.openhab.binding.remoteopenhab.internal.data.RemoteopenhabStateOption;
43 import org.openhab.binding.remoteopenhab.internal.discovery.RemoteopenhabDiscoveryService;
44 import org.openhab.binding.remoteopenhab.internal.exceptions.RemoteopenhabException;
45 import org.openhab.binding.remoteopenhab.internal.listener.RemoteopenhabItemsDataListener;
46 import org.openhab.binding.remoteopenhab.internal.listener.RemoteopenhabStreamingDataListener;
47 import org.openhab.binding.remoteopenhab.internal.rest.RemoteopenhabRestClient;
48 import org.openhab.core.library.CoreItemFactory;
49 import org.openhab.core.library.types.DateTimeType;
50 import org.openhab.core.library.types.DecimalType;
51 import org.openhab.core.library.types.HSBType;
52 import org.openhab.core.library.types.OnOffType;
53 import org.openhab.core.library.types.OpenClosedType;
54 import org.openhab.core.library.types.PercentType;
55 import org.openhab.core.library.types.PlayPauseType;
56 import org.openhab.core.library.types.PointType;
57 import org.openhab.core.library.types.QuantityType;
58 import org.openhab.core.library.types.RawType;
59 import org.openhab.core.library.types.StringType;
60 import org.openhab.core.net.NetUtil;
61 import org.openhab.core.thing.Bridge;
62 import org.openhab.core.thing.Channel;
63 import org.openhab.core.thing.ChannelUID;
64 import org.openhab.core.thing.ThingStatus;
65 import org.openhab.core.thing.ThingStatusDetail;
66 import org.openhab.core.thing.binding.BaseBridgeHandler;
67 import org.openhab.core.thing.binding.ThingHandlerService;
68 import org.openhab.core.thing.binding.builder.ChannelBuilder;
69 import org.openhab.core.thing.binding.builder.ThingBuilder;
70 import org.openhab.core.thing.type.AutoUpdatePolicy;
71 import org.openhab.core.thing.type.ChannelKind;
72 import org.openhab.core.thing.type.ChannelType;
73 import org.openhab.core.thing.type.ChannelTypeBuilder;
74 import org.openhab.core.thing.type.ChannelTypeUID;
75 import org.openhab.core.types.Command;
76 import org.openhab.core.types.RefreshType;
77 import org.openhab.core.types.State;
78 import org.openhab.core.types.StateDescriptionFragmentBuilder;
79 import org.openhab.core.types.StateOption;
80 import org.openhab.core.types.TypeParser;
81 import org.openhab.core.types.UnDefType;
82 import org.osgi.service.jaxrs.client.SseEventSourceFactory;
83 import org.slf4j.Logger;
84 import org.slf4j.LoggerFactory;
86 import com.google.gson.Gson;
89 * The {@link RemoteopenhabBridgeHandler} is responsible for handling commands and updating states
90 * using the REST API of the remote openHAB server.
92 * @author Laurent Garnier - Initial contribution
95 public class RemoteopenhabBridgeHandler extends BaseBridgeHandler
96 implements RemoteopenhabStreamingDataListener, RemoteopenhabItemsDataListener {
98 private static final String DATE_FORMAT_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSSZ";
99 private static final DateTimeFormatter FORMATTER_DATE = DateTimeFormatter.ofPattern(DATE_FORMAT_PATTERN);
101 private static final int MAX_STATE_SIZE_FOR_LOGGING = 50;
103 private final Logger logger = LoggerFactory.getLogger(RemoteopenhabBridgeHandler.class);
105 private final HttpClient httpClientTrustingCert;
106 private final RemoteopenhabChannelTypeProvider channelTypeProvider;
107 private final RemoteopenhabStateDescriptionOptionProvider stateDescriptionProvider;
109 private final Object updateThingLock = new Object();
111 private @NonNullByDefault({}) RemoteopenhabServerConfiguration config;
113 private @Nullable ScheduledFuture<?> checkConnectionJob;
114 private RemoteopenhabRestClient restClient;
116 private Map<ChannelUID, State> channelsLastStates = new HashMap<>();
118 public RemoteopenhabBridgeHandler(Bridge bridge, HttpClient httpClient, HttpClient httpClientTrustingCert,
119 ClientBuilder clientBuilder, SseEventSourceFactory eventSourceFactory,
120 RemoteopenhabChannelTypeProvider channelTypeProvider,
121 RemoteopenhabStateDescriptionOptionProvider stateDescriptionProvider, final Gson jsonParser) {
123 this.httpClientTrustingCert = httpClientTrustingCert;
124 this.channelTypeProvider = channelTypeProvider;
125 this.stateDescriptionProvider = stateDescriptionProvider;
126 this.restClient = new RemoteopenhabRestClient(httpClient, clientBuilder, eventSourceFactory, jsonParser);
130 public void initialize() {
131 logger.debug("Initializing remote openHAB handler for bridge {}", getThing().getUID());
133 config = getConfigAs(RemoteopenhabServerConfiguration.class);
135 String host = config.host.trim();
136 if (host.length() == 0) {
137 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
138 "Undefined server address setting in the thing configuration");
141 List<String> localIpAddresses = NetUtil.getAllInterfaceAddresses().stream()
142 .filter(a -> !a.getAddress().isLinkLocalAddress())
143 .map(a -> a.getAddress().getHostAddress().split("%")[0]).collect(Collectors.toList());
144 if (localIpAddresses.contains(host)) {
145 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
146 "Do not use the local server as a remote server in the thing configuration");
149 String path = config.restPath.trim();
150 if (path.length() == 0 || !path.startsWith("/")) {
151 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
152 "Invalid REST API path setting in the thing configuration");
157 url = new URL(config.useHttps ? "https" : "http", host, config.port, path);
158 } catch (MalformedURLException e) {
159 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
160 "Invalid REST URL built from the settings in the thing configuration");
164 String urlStr = url.toString();
165 if (urlStr.endsWith("/")) {
166 urlStr = urlStr.substring(0, urlStr.length() - 1);
168 logger.debug("REST URL = {}", urlStr);
170 restClient.setRestUrl(urlStr);
171 restClient.setAccessToken(config.token);
172 if (config.useHttps && config.trustedCertificate) {
173 restClient.setHttpClient(httpClientTrustingCert);
174 restClient.setTrustedCertificate(true);
177 updateStatus(ThingStatus.UNKNOWN);
179 scheduler.submit(this::checkConnection);
180 if (config.accessibilityInterval > 0) {
181 startCheckConnectionJob(config.accessibilityInterval, config.aliveInterval);
186 public void dispose() {
187 logger.debug("Disposing remote openHAB handler for bridge {}", getThing().getUID());
188 stopStreamingUpdates();
189 stopCheckConnectionJob();
190 channelsLastStates.clear();
194 public void handleCommand(ChannelUID channelUID, Command command) {
195 if (getThing().getStatus() != ThingStatus.ONLINE) {
200 if (command instanceof RefreshType) {
201 String state = restClient.getRemoteItemState(channelUID.getId());
202 updateChannelState(channelUID.getId(), null, state, false);
203 } else if (isLinked(channelUID)) {
204 restClient.sendCommandToRemoteItem(channelUID.getId(), command);
205 String commandStr = command.toFullString();
206 logger.debug("Sending command {} to remote item {} succeeded",
207 commandStr.length() < MAX_STATE_SIZE_FOR_LOGGING ? commandStr
208 : commandStr.substring(0, MAX_STATE_SIZE_FOR_LOGGING) + "...",
211 } catch (RemoteopenhabException e) {
212 logger.debug("{}", e.getMessage());
216 private boolean createChannels(List<RemoteopenhabItem> items, boolean replace) {
217 synchronized (updateThingLock) {
220 List<Channel> channels = new ArrayList<>();
221 for (RemoteopenhabItem item : items) {
222 String itemType = item.type;
223 boolean readOnly = false;
224 if ("Group".equals(itemType)) {
225 if (item.groupType.isEmpty()) {
226 // Standard groups are ignored
230 itemType = item.groupType;
233 if (item.stateDescription != null && item.stateDescription.readOnly) {
237 String channelTypeId = String.format("item%s%s", itemType.replace(":", ""), readOnly ? "RO" : "");
238 ChannelTypeUID channelTypeUID = new ChannelTypeUID(BINDING_ID, channelTypeId);
239 ChannelType channelType = channelTypeProvider.getChannelType(channelTypeUID, null);
242 if (channelType == null) {
243 logger.trace("Create the channel type {} for item type {}", channelTypeUID, itemType);
244 label = String.format("Remote %s Item", itemType);
245 description = String.format("An item of type %s from the remote server.", itemType);
246 channelType = ChannelTypeBuilder.state(channelTypeUID, label, itemType)
247 .withDescription(description)
248 .withStateDescriptionFragment(
249 StateDescriptionFragmentBuilder.create().withReadOnly(readOnly).build())
250 .withAutoUpdatePolicy(AutoUpdatePolicy.VETO).build();
251 channelTypeProvider.addChannelType(channelType);
253 ChannelUID channelUID = new ChannelUID(getThing().getUID(), item.name);
254 logger.trace("Create the channel {} of type {}", channelUID, channelTypeUID);
255 label = "Item " + item.name;
256 description = String.format("Item %s from the remote server.", item.name);
257 channels.add(ChannelBuilder.create(channelUID, itemType).withType(channelTypeUID)
258 .withKind(ChannelKind.STATE).withLabel(label).withDescription(description).build());
260 ThingBuilder thingBuilder = editThing();
262 thingBuilder.withChannels(channels);
263 updateThing(thingBuilder.build());
264 logger.debug("{} channels defined for the thing {} (from {} items including {} groups)",
265 channels.size(), getThing().getUID(), items.size(), nbGroups);
266 } else if (channels.size() > 0) {
268 for (Channel channel : channels) {
269 if (getThing().getChannel(channel.getUID()) != null) {
270 thingBuilder.withoutChannel(channel.getUID());
275 logger.debug("{} channels removed for the thing {} (from {} items)", nbRemoved,
276 getThing().getUID(), items.size());
278 for (Channel channel : channels) {
279 thingBuilder.withChannel(channel);
281 updateThing(thingBuilder.build());
283 logger.debug("{} channels added for the thing {} (from {} items including {} groups)",
284 channels.size(), getThing().getUID(), items.size(), nbGroups);
286 logger.debug("{} channels added for the thing {} (from {} items)", channels.size(),
287 getThing().getUID(), items.size());
291 } catch (IllegalArgumentException e) {
292 logger.warn("An error occurred while creating the channels for the server {}: {}", getThing().getUID(),
299 private void removeChannels(List<RemoteopenhabItem> items) {
300 synchronized (updateThingLock) {
302 ThingBuilder thingBuilder = editThing();
303 for (RemoteopenhabItem item : items) {
304 Channel channel = getThing().getChannel(item.name);
305 if (channel != null) {
306 thingBuilder.withoutChannel(channel.getUID());
311 updateThing(thingBuilder.build());
312 logger.debug("{} channels removed for the thing {} (from {} items)", nbRemoved, getThing().getUID(),
318 private void setStateOptions(List<RemoteopenhabItem> items) {
319 for (RemoteopenhabItem item : items) {
320 Channel channel = getThing().getChannel(item.name);
321 RemoteopenhabStateDescription descr = item.stateDescription;
322 List<RemoteopenhabStateOption> options = descr == null ? null : descr.options;
323 if (channel != null && options != null && options.size() > 0) {
324 List<StateOption> stateOptions = new ArrayList<>();
325 for (RemoteopenhabStateOption option : options) {
326 stateOptions.add(new StateOption(option.value, option.label));
328 stateDescriptionProvider.setStateOptions(channel.getUID(), stateOptions);
329 logger.trace("{} options set for the channel {}", options.size(), channel.getUID());
334 public void checkConnection() {
335 logger.debug("Try the root REST API...");
338 if (restClient.getRestApiVersion() == null) {
339 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
340 "OH 1.x server not supported by the binding");
341 } else if (getThing().getStatus() != ThingStatus.ONLINE) {
342 List<RemoteopenhabItem> items = restClient.getRemoteItems("name,type,groupType,state,stateDescription");
344 if (createChannels(items, true)) {
345 setStateOptions(items);
346 for (RemoteopenhabItem item : items) {
347 updateChannelState(item.name, null, item.state, false);
350 updateStatus(ThingStatus.ONLINE);
352 restartStreamingUpdates();
354 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
355 "Dynamic creation of the channels for the remote server items failed");
356 stopStreamingUpdates();
359 } catch (RemoteopenhabException e) {
360 logger.debug("{}", e.getMessage());
361 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
362 stopStreamingUpdates();
366 private void startCheckConnectionJob(int accessibilityInterval, int aliveInterval) {
367 ScheduledFuture<?> localCheckConnectionJob = checkConnectionJob;
368 if (localCheckConnectionJob == null || localCheckConnectionJob.isCancelled()) {
369 checkConnectionJob = scheduler.scheduleWithFixedDelay(() -> {
370 long millisSinceLastEvent = System.currentTimeMillis() - restClient.getLastEventTimestamp();
371 if (aliveInterval == 0 || restClient.getLastEventTimestamp() == 0) {
372 logger.debug("Time to check server accessibility");
374 } else if (millisSinceLastEvent > (aliveInterval * 60000)) {
376 "Time to check server accessibility (maybe disconnected from streaming events, millisSinceLastEvent={})",
377 millisSinceLastEvent);
381 "Bypass server accessibility check (receiving streaming events, millisSinceLastEvent={})",
382 millisSinceLastEvent);
384 }, accessibilityInterval, accessibilityInterval, TimeUnit.MINUTES);
388 private void stopCheckConnectionJob() {
389 ScheduledFuture<?> localCheckConnectionJob = checkConnectionJob;
390 if (localCheckConnectionJob != null) {
391 localCheckConnectionJob.cancel(true);
392 checkConnectionJob = null;
396 private void restartStreamingUpdates() {
397 synchronized (restClient) {
398 stopStreamingUpdates();
399 startStreamingUpdates();
403 private void startStreamingUpdates() {
404 synchronized (restClient) {
405 restClient.addStreamingDataListener(this);
406 restClient.addItemsDataListener(this);
411 private void stopStreamingUpdates() {
412 synchronized (restClient) {
414 restClient.removeStreamingDataListener(this);
415 restClient.removeItemsDataListener(this);
419 public RemoteopenhabRestClient gestRestClient() {
424 public void onConnected() {
425 updateStatus(ThingStatus.ONLINE);
429 public void onError(String message) {
430 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message);
434 public void onItemStateEvent(String itemName, String stateType, String state, boolean onlyIfStateChanged) {
435 updateChannelState(itemName, stateType, state, onlyIfStateChanged);
439 public void onItemAdded(RemoteopenhabItem item) {
440 createChannels(List.of(item), false);
444 public void onItemRemoved(RemoteopenhabItem item) {
445 removeChannels(List.of(item));
449 public void onItemUpdated(RemoteopenhabItem newItem, RemoteopenhabItem oldItem) {
450 if (!newItem.type.equals(oldItem.type)) {
451 createChannels(List.of(newItem), false);
453 logger.trace("Updated remote item {} ignored because item type {} is unchanged", newItem.name,
458 private void updateChannelState(String itemName, @Nullable String stateType, String state,
459 boolean onlyIfStateChanged) {
460 Channel channel = getThing().getChannel(itemName);
461 if (channel == null) {
462 logger.trace("No channel for item {}", itemName);
465 String acceptedItemType = channel.getAcceptedItemType();
466 if (acceptedItemType == null) {
467 logger.trace("Channel without accepted item type for item {}", itemName);
470 if (!isLinked(channel.getUID())) {
471 logger.trace("Unlinked channel {}", channel.getUID());
474 State channelState = null;
475 if (stateType == null && "NULL".equals(state)) {
476 channelState = UnDefType.NULL;
477 } else if (stateType == null && "UNDEF".equals(state)) {
478 channelState = UnDefType.UNDEF;
479 } else if ("UnDef".equals(stateType)) {
482 channelState = UnDefType.NULL;
485 channelState = UnDefType.UNDEF;
488 logger.debug("Invalid UnDef value {} for item {}", state, itemName);
491 } else if (acceptedItemType.startsWith(CoreItemFactory.NUMBER + ":")) {
492 // Item type Number with dimension
493 if (stateType == null || "Quantity".equals(stateType)) {
494 List<Class<? extends State>> stateTypes = Collections.singletonList(QuantityType.class);
495 channelState = TypeParser.parseState(stateTypes, state);
496 } else if ("Decimal".equals(stateType)) {
497 channelState = new DecimalType(state);
499 logger.debug("Unexpected value type {} for item {}", stateType, itemName);
502 switch (acceptedItemType) {
503 case CoreItemFactory.STRING:
504 if (checkStateType(itemName, stateType, "String")) {
505 channelState = new StringType(state);
508 case CoreItemFactory.NUMBER:
509 if (checkStateType(itemName, stateType, "Decimal")) {
510 channelState = new DecimalType(state);
513 case CoreItemFactory.SWITCH:
514 if (checkStateType(itemName, stateType, "OnOff")) {
515 channelState = "ON".equals(state) ? OnOffType.ON : OnOffType.OFF;
518 case CoreItemFactory.CONTACT:
519 if (checkStateType(itemName, stateType, "OpenClosed")) {
520 channelState = "OPEN".equals(state) ? OpenClosedType.OPEN : OpenClosedType.CLOSED;
523 case CoreItemFactory.DIMMER:
524 if (checkStateType(itemName, stateType, "Percent")) {
525 channelState = new PercentType(state);
528 case CoreItemFactory.COLOR:
529 if (checkStateType(itemName, stateType, "HSB")) {
530 channelState = HSBType.valueOf(state);
533 case CoreItemFactory.DATETIME:
534 if (checkStateType(itemName, stateType, "DateTime")) {
536 channelState = new DateTimeType(ZonedDateTime.parse(state, FORMATTER_DATE));
537 } catch (DateTimeParseException e) {
538 logger.debug("Failed to parse date {} for item {}", state, itemName);
543 case CoreItemFactory.LOCATION:
544 if (checkStateType(itemName, stateType, "Point")) {
545 channelState = new PointType(state);
548 case CoreItemFactory.IMAGE:
549 if (checkStateType(itemName, stateType, "Raw")) {
550 channelState = RawType.valueOf(state);
553 case CoreItemFactory.PLAYER:
554 if (checkStateType(itemName, stateType, "PlayPause")) {
557 channelState = PlayPauseType.PLAY;
560 channelState = PlayPauseType.PAUSE;
563 logger.debug("Unexpected value {} for item {}", state, itemName);
568 case CoreItemFactory.ROLLERSHUTTER:
569 if (checkStateType(itemName, stateType, "Percent")) {
570 channelState = new PercentType(state);
574 logger.debug("Item type {} is not yet supported", acceptedItemType);
578 if (channelState != null) {
579 if (onlyIfStateChanged && channelState.equals(channelsLastStates.get(channel.getUID()))) {
580 logger.trace("ItemStateChangedEvent ignored for item {} as state is identical to the last state",
584 channelsLastStates.put(channel.getUID(), channelState);
585 updateState(channel.getUID(), channelState);
586 String channelStateStr = channelState.toFullString();
587 logger.debug("updateState {} with {}", channel.getUID(),
588 channelStateStr.length() < MAX_STATE_SIZE_FOR_LOGGING ? channelStateStr
589 : channelStateStr.substring(0, MAX_STATE_SIZE_FOR_LOGGING) + "...");
593 private boolean checkStateType(String itemName, @Nullable String stateType, String expectedType) {
594 if (stateType != null && !expectedType.equals(stateType)) {
595 logger.debug("Unexpected value type {} for item {}", stateType, itemName);
603 public Collection<Class<? extends ThingHandlerService>> getServices() {
604 return Collections.singleton(RemoteopenhabDiscoveryService.class);