2 * Copyright (c) 2010-2020 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 long CONNECTION_TIMEOUT_MILLIS = TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES);
102 private static final int MAX_STATE_SIZE_FOR_LOGGING = 50;
104 private final Logger logger = LoggerFactory.getLogger(RemoteopenhabBridgeHandler.class);
106 private final HttpClient httpClientTrustingCert;
107 private final RemoteopenhabChannelTypeProvider channelTypeProvider;
108 private final RemoteopenhabStateDescriptionOptionProvider stateDescriptionProvider;
110 private final Object updateThingLock = new Object();
112 private @NonNullByDefault({}) RemoteopenhabServerConfiguration config;
114 private @Nullable ScheduledFuture<?> checkConnectionJob;
115 private RemoteopenhabRestClient restClient;
117 private Map<ChannelUID, State> channelsLastStates = new HashMap<>();
119 public RemoteopenhabBridgeHandler(Bridge bridge, HttpClient httpClient, HttpClient httpClientTrustingCert,
120 ClientBuilder clientBuilder, SseEventSourceFactory eventSourceFactory,
121 RemoteopenhabChannelTypeProvider channelTypeProvider,
122 RemoteopenhabStateDescriptionOptionProvider stateDescriptionProvider, final Gson jsonParser) {
124 this.httpClientTrustingCert = httpClientTrustingCert;
125 this.channelTypeProvider = channelTypeProvider;
126 this.stateDescriptionProvider = stateDescriptionProvider;
127 this.restClient = new RemoteopenhabRestClient(httpClient, clientBuilder, eventSourceFactory, jsonParser);
131 public void initialize() {
132 logger.debug("Initializing remote openHAB handler for bridge {}", getThing().getUID());
134 config = getConfigAs(RemoteopenhabServerConfiguration.class);
136 String host = config.host.trim();
137 if (host.length() == 0) {
138 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
139 "Undefined server address setting in the thing configuration");
142 List<String> localIpAddresses = NetUtil.getAllInterfaceAddresses().stream()
143 .filter(a -> !a.getAddress().isLinkLocalAddress())
144 .map(a -> a.getAddress().getHostAddress().split("%")[0]).collect(Collectors.toList());
145 if (localIpAddresses.contains(host)) {
146 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
147 "Do not use the local server as a remote server in the thing configuration");
150 String path = config.restPath.trim();
151 if (path.length() == 0 || !path.startsWith("/")) {
152 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
153 "Invalid REST API path setting in the thing configuration");
158 url = new URL(config.useHttps ? "https" : "http", host, config.port, path);
159 } catch (MalformedURLException e) {
160 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
161 "Invalid REST URL built from the settings in the thing configuration");
165 String urlStr = url.toString();
166 if (urlStr.endsWith("/")) {
167 urlStr = urlStr.substring(0, urlStr.length() - 1);
169 logger.debug("REST URL = {}", urlStr);
171 restClient.setRestUrl(urlStr);
172 restClient.setAccessToken(config.token);
173 if (config.useHttps && config.trustedCertificate) {
174 restClient.setHttpClient(httpClientTrustingCert);
175 restClient.setTrustedCertificate(true);
178 updateStatus(ThingStatus.UNKNOWN);
180 startCheckConnectionJob();
184 public void dispose() {
185 logger.debug("Disposing remote openHAB handler for bridge {}", getThing().getUID());
186 stopStreamingUpdates();
187 stopCheckConnectionJob();
188 channelsLastStates.clear();
192 public void handleCommand(ChannelUID channelUID, Command command) {
193 if (getThing().getStatus() != ThingStatus.ONLINE) {
198 if (command instanceof RefreshType) {
199 String state = restClient.getRemoteItemState(channelUID.getId());
200 updateChannelState(channelUID.getId(), null, state, false);
201 } else if (isLinked(channelUID)) {
202 restClient.sendCommandToRemoteItem(channelUID.getId(), command);
203 String commandStr = command.toFullString();
204 logger.debug("Sending command {} to remote item {} succeeded",
205 commandStr.length() < MAX_STATE_SIZE_FOR_LOGGING ? commandStr
206 : commandStr.substring(0, MAX_STATE_SIZE_FOR_LOGGING) + "...",
209 } catch (RemoteopenhabException e) {
210 logger.debug("{}", e.getMessage());
214 private void createChannels(List<RemoteopenhabItem> items, boolean replace) {
215 synchronized (updateThingLock) {
217 List<Channel> channels = new ArrayList<>();
218 for (RemoteopenhabItem item : items) {
219 String itemType = item.type;
220 boolean readOnly = false;
221 if ("Group".equals(itemType)) {
222 if (item.groupType.isEmpty()) {
223 // Standard groups are ignored
227 itemType = item.groupType;
230 if (item.stateDescription != null && item.stateDescription.readOnly) {
234 String channelTypeId = String.format("item%s%s", itemType.replace(":", ""), readOnly ? "RO" : "");
235 ChannelTypeUID channelTypeUID = new ChannelTypeUID(BINDING_ID, channelTypeId);
236 ChannelType channelType = channelTypeProvider.getChannelType(channelTypeUID, null);
239 if (channelType == null) {
240 logger.trace("Create the channel type {} for item type {}", channelTypeUID, itemType);
241 label = String.format("Remote %s Item", itemType);
242 description = String.format("An item of type %s from the remote server.", itemType);
243 channelType = ChannelTypeBuilder.state(channelTypeUID, label, itemType).withDescription(description)
244 .withStateDescriptionFragment(
245 StateDescriptionFragmentBuilder.create().withReadOnly(readOnly).build())
246 .withAutoUpdatePolicy(AutoUpdatePolicy.VETO).build();
247 channelTypeProvider.addChannelType(channelType);
249 ChannelUID channelUID = new ChannelUID(getThing().getUID(), item.name);
250 logger.trace("Create the channel {} of type {}", channelUID, channelTypeUID);
251 label = "Item " + item.name;
252 description = String.format("Item %s from the remote server.", item.name);
253 channels.add(ChannelBuilder.create(channelUID, itemType).withType(channelTypeUID)
254 .withKind(ChannelKind.STATE).withLabel(label).withDescription(description).build());
256 ThingBuilder thingBuilder = editThing();
258 thingBuilder.withChannels(channels);
259 updateThing(thingBuilder.build());
260 logger.debug("{} channels defined for the thing {} (from {} items including {} groups)",
261 channels.size(), getThing().getUID(), items.size(), nbGroups);
262 } else if (channels.size() > 0) {
264 for (Channel channel : channels) {
265 if (getThing().getChannel(channel.getUID()) != null) {
266 thingBuilder.withoutChannel(channel.getUID());
271 logger.debug("{} channels removed for the thing {} (from {} items)", nbRemoved, getThing().getUID(),
274 for (Channel channel : channels) {
275 thingBuilder.withChannel(channel);
277 updateThing(thingBuilder.build());
279 logger.debug("{} channels added for the thing {} (from {} items including {} groups)",
280 channels.size(), getThing().getUID(), items.size(), nbGroups);
282 logger.debug("{} channels added for the thing {} (from {} items)", channels.size(),
283 getThing().getUID(), items.size());
289 private void removeChannels(List<RemoteopenhabItem> items) {
290 synchronized (updateThingLock) {
292 ThingBuilder thingBuilder = editThing();
293 for (RemoteopenhabItem item : items) {
294 Channel channel = getThing().getChannel(item.name);
295 if (channel != null) {
296 thingBuilder.withoutChannel(channel.getUID());
301 updateThing(thingBuilder.build());
302 logger.debug("{} channels removed for the thing {} (from {} items)", nbRemoved, getThing().getUID(),
308 private void setStateOptions(List<RemoteopenhabItem> items) {
309 for (RemoteopenhabItem item : items) {
310 Channel channel = getThing().getChannel(item.name);
311 RemoteopenhabStateDescription descr = item.stateDescription;
312 List<RemoteopenhabStateOption> options = descr == null ? null : descr.options;
313 if (channel != null && options != null && options.size() > 0) {
314 List<StateOption> stateOptions = new ArrayList<>();
315 for (RemoteopenhabStateOption option : options) {
316 stateOptions.add(new StateOption(option.value, option.label));
318 stateDescriptionProvider.setStateOptions(channel.getUID(), stateOptions);
319 logger.trace("{} options set for the channel {}", options.size(), channel.getUID());
324 public void checkConnection() {
325 logger.debug("Try the root REST API...");
328 if (restClient.getRestApiVersion() == null) {
329 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
330 "OH 1.x server not supported by the binding");
331 } else if (getThing().getStatus() != ThingStatus.ONLINE) {
332 List<RemoteopenhabItem> items = restClient.getRemoteItems("name,type,groupType,stateDescription");
334 createChannels(items, true);
335 setStateOptions(items);
338 items = restClient.getRemoteItems("name,state");
339 for (RemoteopenhabItem item : items) {
340 updateChannelState(item.name, null, item.state, false);
342 } catch (RemoteopenhabException e) {
343 logger.debug("{}", e.getMessage());
346 updateStatus(ThingStatus.ONLINE);
348 restartStreamingUpdates();
350 } catch (RemoteopenhabException e) {
351 logger.debug("{}", e.getMessage());
352 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
353 stopStreamingUpdates();
357 private void startCheckConnectionJob() {
358 ScheduledFuture<?> localCheckConnectionJob = checkConnectionJob;
359 if (localCheckConnectionJob == null || localCheckConnectionJob.isCancelled()) {
360 checkConnectionJob = scheduler.scheduleWithFixedDelay(() -> {
361 long millisSinceLastEvent = System.currentTimeMillis() - restClient.getLastEventTimestamp();
362 if (millisSinceLastEvent > CONNECTION_TIMEOUT_MILLIS) {
363 logger.debug("Check: Maybe disconnected from streaming events, millisSinceLastEvent={}",
364 millisSinceLastEvent);
367 logger.debug("Check: Receiving streaming events, millisSinceLastEvent={}", millisSinceLastEvent);
369 }, 0, CONNECTION_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
373 private void stopCheckConnectionJob() {
374 ScheduledFuture<?> localCheckConnectionJob = checkConnectionJob;
375 if (localCheckConnectionJob != null) {
376 localCheckConnectionJob.cancel(true);
377 checkConnectionJob = null;
381 private void restartStreamingUpdates() {
382 synchronized (restClient) {
383 stopStreamingUpdates();
384 startStreamingUpdates();
388 private void startStreamingUpdates() {
389 synchronized (restClient) {
390 restClient.addStreamingDataListener(this);
391 restClient.addItemsDataListener(this);
396 private void stopStreamingUpdates() {
397 synchronized (restClient) {
399 restClient.removeStreamingDataListener(this);
400 restClient.removeItemsDataListener(this);
404 public RemoteopenhabRestClient gestRestClient() {
409 public void onConnected() {
410 updateStatus(ThingStatus.ONLINE);
414 public void onError(String message) {
415 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message);
419 public void onItemStateEvent(String itemName, String stateType, String state, boolean onlyIfStateChanged) {
420 updateChannelState(itemName, stateType, state, onlyIfStateChanged);
424 public void onItemAdded(RemoteopenhabItem item) {
425 createChannels(List.of(item), false);
429 public void onItemRemoved(RemoteopenhabItem item) {
430 removeChannels(List.of(item));
434 public void onItemUpdated(RemoteopenhabItem newItem, RemoteopenhabItem oldItem) {
435 if (!newItem.type.equals(oldItem.type)) {
436 createChannels(List.of(newItem), false);
438 logger.trace("Updated remote item {} ignored because item type {} is unchanged", newItem.name,
443 private void updateChannelState(String itemName, @Nullable String stateType, String state,
444 boolean onlyIfStateChanged) {
445 Channel channel = getThing().getChannel(itemName);
446 if (channel == null) {
447 logger.trace("No channel for item {}", itemName);
450 String acceptedItemType = channel.getAcceptedItemType();
451 if (acceptedItemType == null) {
452 logger.trace("Channel without accepted item type for item {}", itemName);
455 if (!isLinked(channel.getUID())) {
456 logger.trace("Unlinked channel {}", channel.getUID());
459 State channelState = null;
460 if (stateType == null && "NULL".equals(state)) {
461 channelState = UnDefType.NULL;
462 } else if (stateType == null && "UNDEF".equals(state)) {
463 channelState = UnDefType.UNDEF;
464 } else if ("UnDef".equals(stateType)) {
467 channelState = UnDefType.NULL;
470 channelState = UnDefType.UNDEF;
473 logger.debug("Invalid UnDef value {} for item {}", state, itemName);
476 } else if (acceptedItemType.startsWith(CoreItemFactory.NUMBER + ":")) {
477 // Item type Number with dimension
478 if (stateType == null || "Quantity".equals(stateType)) {
479 List<Class<? extends State>> stateTypes = Collections.singletonList(QuantityType.class);
480 channelState = TypeParser.parseState(stateTypes, state);
481 } else if ("Decimal".equals(stateType)) {
482 channelState = new DecimalType(state);
484 logger.debug("Unexpected value type {} for item {}", stateType, itemName);
487 switch (acceptedItemType) {
488 case CoreItemFactory.STRING:
489 if (checkStateType(itemName, stateType, "String")) {
490 channelState = new StringType(state);
493 case CoreItemFactory.NUMBER:
494 if (checkStateType(itemName, stateType, "Decimal")) {
495 channelState = new DecimalType(state);
498 case CoreItemFactory.SWITCH:
499 if (checkStateType(itemName, stateType, "OnOff")) {
500 channelState = "ON".equals(state) ? OnOffType.ON : OnOffType.OFF;
503 case CoreItemFactory.CONTACT:
504 if (checkStateType(itemName, stateType, "OpenClosed")) {
505 channelState = "OPEN".equals(state) ? OpenClosedType.OPEN : OpenClosedType.CLOSED;
508 case CoreItemFactory.DIMMER:
509 if (checkStateType(itemName, stateType, "Percent")) {
510 channelState = new PercentType(state);
513 case CoreItemFactory.COLOR:
514 if (checkStateType(itemName, stateType, "HSB")) {
515 channelState = HSBType.valueOf(state);
518 case CoreItemFactory.DATETIME:
519 if (checkStateType(itemName, stateType, "DateTime")) {
521 channelState = new DateTimeType(ZonedDateTime.parse(state, FORMATTER_DATE));
522 } catch (DateTimeParseException e) {
523 logger.debug("Failed to parse date {} for item {}", state, itemName);
528 case CoreItemFactory.LOCATION:
529 if (checkStateType(itemName, stateType, "Point")) {
530 channelState = new PointType(state);
533 case CoreItemFactory.IMAGE:
534 if (checkStateType(itemName, stateType, "Raw")) {
535 channelState = RawType.valueOf(state);
538 case CoreItemFactory.PLAYER:
539 if (checkStateType(itemName, stateType, "PlayPause")) {
542 channelState = PlayPauseType.PLAY;
545 channelState = PlayPauseType.PAUSE;
548 logger.debug("Unexpected value {} for item {}", state, itemName);
553 case CoreItemFactory.ROLLERSHUTTER:
554 if (checkStateType(itemName, stateType, "Percent")) {
555 channelState = new PercentType(state);
559 logger.debug("Item type {} is not yet supported", acceptedItemType);
563 if (channelState != null) {
564 if (onlyIfStateChanged && channelState.equals(channelsLastStates.get(channel.getUID()))) {
565 logger.trace("ItemStateChangedEvent ignored for item {} as state is identical to the last state",
569 channelsLastStates.put(channel.getUID(), channelState);
570 updateState(channel.getUID(), channelState);
571 String channelStateStr = channelState.toFullString();
572 logger.debug("updateState {} with {}", channel.getUID(),
573 channelStateStr.length() < MAX_STATE_SIZE_FOR_LOGGING ? channelStateStr
574 : channelStateStr.substring(0, MAX_STATE_SIZE_FOR_LOGGING) + "...");
578 private boolean checkStateType(String itemName, @Nullable String stateType, String expectedType) {
579 if (stateType != null && !expectedType.equals(stateType)) {
580 logger.debug("Unexpected value type {} for item {}", stateType, itemName);
588 public Collection<Class<? extends ThingHandlerService>> getServices() {
589 return Collections.singleton(RemoteopenhabDiscoveryService.class);