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 void createChannels(List<RemoteopenhabItem> items, boolean replace) {
217 synchronized (updateThingLock) {
219 List<Channel> channels = new ArrayList<>();
220 for (RemoteopenhabItem item : items) {
221 String itemType = item.type;
222 boolean readOnly = false;
223 if ("Group".equals(itemType)) {
224 if (item.groupType.isEmpty()) {
225 // Standard groups are ignored
229 itemType = item.groupType;
232 if (item.stateDescription != null && item.stateDescription.readOnly) {
236 String channelTypeId = String.format("item%s%s", itemType.replace(":", ""), readOnly ? "RO" : "");
237 ChannelTypeUID channelTypeUID = new ChannelTypeUID(BINDING_ID, channelTypeId);
238 ChannelType channelType = channelTypeProvider.getChannelType(channelTypeUID, null);
241 if (channelType == null) {
242 logger.trace("Create the channel type {} for item type {}", channelTypeUID, itemType);
243 label = String.format("Remote %s Item", itemType);
244 description = String.format("An item of type %s from the remote server.", itemType);
245 channelType = ChannelTypeBuilder.state(channelTypeUID, label, itemType).withDescription(description)
246 .withStateDescriptionFragment(
247 StateDescriptionFragmentBuilder.create().withReadOnly(readOnly).build())
248 .withAutoUpdatePolicy(AutoUpdatePolicy.VETO).build();
249 channelTypeProvider.addChannelType(channelType);
251 ChannelUID channelUID = new ChannelUID(getThing().getUID(), item.name);
252 logger.trace("Create the channel {} of type {}", channelUID, channelTypeUID);
253 label = "Item " + item.name;
254 description = String.format("Item %s from the remote server.", item.name);
255 channels.add(ChannelBuilder.create(channelUID, itemType).withType(channelTypeUID)
256 .withKind(ChannelKind.STATE).withLabel(label).withDescription(description).build());
258 ThingBuilder thingBuilder = editThing();
260 thingBuilder.withChannels(channels);
261 updateThing(thingBuilder.build());
262 logger.debug("{} channels defined for the thing {} (from {} items including {} groups)",
263 channels.size(), getThing().getUID(), items.size(), nbGroups);
264 } else if (channels.size() > 0) {
266 for (Channel channel : channels) {
267 if (getThing().getChannel(channel.getUID()) != null) {
268 thingBuilder.withoutChannel(channel.getUID());
273 logger.debug("{} channels removed for the thing {} (from {} items)", nbRemoved, getThing().getUID(),
276 for (Channel channel : channels) {
277 thingBuilder.withChannel(channel);
279 updateThing(thingBuilder.build());
281 logger.debug("{} channels added for the thing {} (from {} items including {} groups)",
282 channels.size(), getThing().getUID(), items.size(), nbGroups);
284 logger.debug("{} channels added for the thing {} (from {} items)", channels.size(),
285 getThing().getUID(), items.size());
291 private void removeChannels(List<RemoteopenhabItem> items) {
292 synchronized (updateThingLock) {
294 ThingBuilder thingBuilder = editThing();
295 for (RemoteopenhabItem item : items) {
296 Channel channel = getThing().getChannel(item.name);
297 if (channel != null) {
298 thingBuilder.withoutChannel(channel.getUID());
303 updateThing(thingBuilder.build());
304 logger.debug("{} channels removed for the thing {} (from {} items)", nbRemoved, getThing().getUID(),
310 private void setStateOptions(List<RemoteopenhabItem> items) {
311 for (RemoteopenhabItem item : items) {
312 Channel channel = getThing().getChannel(item.name);
313 RemoteopenhabStateDescription descr = item.stateDescription;
314 List<RemoteopenhabStateOption> options = descr == null ? null : descr.options;
315 if (channel != null && options != null && options.size() > 0) {
316 List<StateOption> stateOptions = new ArrayList<>();
317 for (RemoteopenhabStateOption option : options) {
318 stateOptions.add(new StateOption(option.value, option.label));
320 stateDescriptionProvider.setStateOptions(channel.getUID(), stateOptions);
321 logger.trace("{} options set for the channel {}", options.size(), channel.getUID());
326 public void checkConnection() {
327 logger.debug("Try the root REST API...");
330 if (restClient.getRestApiVersion() == null) {
331 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
332 "OH 1.x server not supported by the binding");
333 } else if (getThing().getStatus() != ThingStatus.ONLINE) {
334 List<RemoteopenhabItem> items = restClient.getRemoteItems("name,type,groupType,state,stateDescription");
336 createChannels(items, true);
337 setStateOptions(items);
338 for (RemoteopenhabItem item : items) {
339 updateChannelState(item.name, null, item.state, false);
342 updateStatus(ThingStatus.ONLINE);
344 restartStreamingUpdates();
346 } catch (RemoteopenhabException e) {
347 logger.debug("{}", e.getMessage());
348 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
349 stopStreamingUpdates();
353 private void startCheckConnectionJob(int accessibilityInterval, int aliveInterval) {
354 ScheduledFuture<?> localCheckConnectionJob = checkConnectionJob;
355 if (localCheckConnectionJob == null || localCheckConnectionJob.isCancelled()) {
356 checkConnectionJob = scheduler.scheduleWithFixedDelay(() -> {
357 long millisSinceLastEvent = System.currentTimeMillis() - restClient.getLastEventTimestamp();
358 if (aliveInterval == 0 || restClient.getLastEventTimestamp() == 0) {
359 logger.debug("Time to check server accessibility");
361 } else if (millisSinceLastEvent > (aliveInterval * 60000)) {
363 "Time to check server accessibility (maybe disconnected from streaming events, millisSinceLastEvent={})",
364 millisSinceLastEvent);
368 "Bypass server accessibility check (receiving streaming events, millisSinceLastEvent={})",
369 millisSinceLastEvent);
371 }, accessibilityInterval, accessibilityInterval, TimeUnit.MINUTES);
375 private void stopCheckConnectionJob() {
376 ScheduledFuture<?> localCheckConnectionJob = checkConnectionJob;
377 if (localCheckConnectionJob != null) {
378 localCheckConnectionJob.cancel(true);
379 checkConnectionJob = null;
383 private void restartStreamingUpdates() {
384 synchronized (restClient) {
385 stopStreamingUpdates();
386 startStreamingUpdates();
390 private void startStreamingUpdates() {
391 synchronized (restClient) {
392 restClient.addStreamingDataListener(this);
393 restClient.addItemsDataListener(this);
398 private void stopStreamingUpdates() {
399 synchronized (restClient) {
401 restClient.removeStreamingDataListener(this);
402 restClient.removeItemsDataListener(this);
406 public RemoteopenhabRestClient gestRestClient() {
411 public void onConnected() {
412 updateStatus(ThingStatus.ONLINE);
416 public void onError(String message) {
417 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message);
421 public void onItemStateEvent(String itemName, String stateType, String state, boolean onlyIfStateChanged) {
422 updateChannelState(itemName, stateType, state, onlyIfStateChanged);
426 public void onItemAdded(RemoteopenhabItem item) {
427 createChannels(List.of(item), false);
431 public void onItemRemoved(RemoteopenhabItem item) {
432 removeChannels(List.of(item));
436 public void onItemUpdated(RemoteopenhabItem newItem, RemoteopenhabItem oldItem) {
437 if (!newItem.type.equals(oldItem.type)) {
438 createChannels(List.of(newItem), false);
440 logger.trace("Updated remote item {} ignored because item type {} is unchanged", newItem.name,
445 private void updateChannelState(String itemName, @Nullable String stateType, String state,
446 boolean onlyIfStateChanged) {
447 Channel channel = getThing().getChannel(itemName);
448 if (channel == null) {
449 logger.trace("No channel for item {}", itemName);
452 String acceptedItemType = channel.getAcceptedItemType();
453 if (acceptedItemType == null) {
454 logger.trace("Channel without accepted item type for item {}", itemName);
457 if (!isLinked(channel.getUID())) {
458 logger.trace("Unlinked channel {}", channel.getUID());
461 State channelState = null;
462 if (stateType == null && "NULL".equals(state)) {
463 channelState = UnDefType.NULL;
464 } else if (stateType == null && "UNDEF".equals(state)) {
465 channelState = UnDefType.UNDEF;
466 } else if ("UnDef".equals(stateType)) {
469 channelState = UnDefType.NULL;
472 channelState = UnDefType.UNDEF;
475 logger.debug("Invalid UnDef value {} for item {}", state, itemName);
478 } else if (acceptedItemType.startsWith(CoreItemFactory.NUMBER + ":")) {
479 // Item type Number with dimension
480 if (stateType == null || "Quantity".equals(stateType)) {
481 List<Class<? extends State>> stateTypes = Collections.singletonList(QuantityType.class);
482 channelState = TypeParser.parseState(stateTypes, state);
483 } else if ("Decimal".equals(stateType)) {
484 channelState = new DecimalType(state);
486 logger.debug("Unexpected value type {} for item {}", stateType, itemName);
489 switch (acceptedItemType) {
490 case CoreItemFactory.STRING:
491 if (checkStateType(itemName, stateType, "String")) {
492 channelState = new StringType(state);
495 case CoreItemFactory.NUMBER:
496 if (checkStateType(itemName, stateType, "Decimal")) {
497 channelState = new DecimalType(state);
500 case CoreItemFactory.SWITCH:
501 if (checkStateType(itemName, stateType, "OnOff")) {
502 channelState = "ON".equals(state) ? OnOffType.ON : OnOffType.OFF;
505 case CoreItemFactory.CONTACT:
506 if (checkStateType(itemName, stateType, "OpenClosed")) {
507 channelState = "OPEN".equals(state) ? OpenClosedType.OPEN : OpenClosedType.CLOSED;
510 case CoreItemFactory.DIMMER:
511 if (checkStateType(itemName, stateType, "Percent")) {
512 channelState = new PercentType(state);
515 case CoreItemFactory.COLOR:
516 if (checkStateType(itemName, stateType, "HSB")) {
517 channelState = HSBType.valueOf(state);
520 case CoreItemFactory.DATETIME:
521 if (checkStateType(itemName, stateType, "DateTime")) {
523 channelState = new DateTimeType(ZonedDateTime.parse(state, FORMATTER_DATE));
524 } catch (DateTimeParseException e) {
525 logger.debug("Failed to parse date {} for item {}", state, itemName);
530 case CoreItemFactory.LOCATION:
531 if (checkStateType(itemName, stateType, "Point")) {
532 channelState = new PointType(state);
535 case CoreItemFactory.IMAGE:
536 if (checkStateType(itemName, stateType, "Raw")) {
537 channelState = RawType.valueOf(state);
540 case CoreItemFactory.PLAYER:
541 if (checkStateType(itemName, stateType, "PlayPause")) {
544 channelState = PlayPauseType.PLAY;
547 channelState = PlayPauseType.PAUSE;
550 logger.debug("Unexpected value {} for item {}", state, itemName);
555 case CoreItemFactory.ROLLERSHUTTER:
556 if (checkStateType(itemName, stateType, "Percent")) {
557 channelState = new PercentType(state);
561 logger.debug("Item type {} is not yet supported", acceptedItemType);
565 if (channelState != null) {
566 if (onlyIfStateChanged && channelState.equals(channelsLastStates.get(channel.getUID()))) {
567 logger.trace("ItemStateChangedEvent ignored for item {} as state is identical to the last state",
571 channelsLastStates.put(channel.getUID(), channelState);
572 updateState(channel.getUID(), channelState);
573 String channelStateStr = channelState.toFullString();
574 logger.debug("updateState {} with {}", channel.getUID(),
575 channelStateStr.length() < MAX_STATE_SIZE_FOR_LOGGING ? channelStateStr
576 : channelStateStr.substring(0, MAX_STATE_SIZE_FOR_LOGGING) + "...");
580 private boolean checkStateType(String itemName, @Nullable String stateType, String expectedType) {
581 if (stateType != null && !expectedType.equals(stateType)) {
582 logger.debug("Unexpected value type {} for item {}", stateType, itemName);
590 public Collection<Class<? extends ThingHandlerService>> getServices() {
591 return Collections.singleton(RemoteopenhabDiscoveryService.class);