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.Collections;
24 import java.util.List;
25 import java.util.concurrent.ScheduledFuture;
26 import java.util.concurrent.TimeUnit;
27 import java.util.stream.Collectors;
29 import javax.ws.rs.client.ClientBuilder;
31 import org.eclipse.jdt.annotation.NonNullByDefault;
32 import org.eclipse.jdt.annotation.Nullable;
33 import org.openhab.binding.remoteopenhab.internal.RemoteopenhabChannelTypeProvider;
34 import org.openhab.binding.remoteopenhab.internal.RemoteopenhabStateDescriptionOptionProvider;
35 import org.openhab.binding.remoteopenhab.internal.config.RemoteopenhabInstanceConfiguration;
36 import org.openhab.binding.remoteopenhab.internal.data.Item;
37 import org.openhab.binding.remoteopenhab.internal.data.Option;
38 import org.openhab.binding.remoteopenhab.internal.data.StateDescription;
39 import org.openhab.binding.remoteopenhab.internal.exceptions.RemoteopenhabException;
40 import org.openhab.binding.remoteopenhab.internal.listener.RemoteopenhabStreamingDataListener;
41 import org.openhab.binding.remoteopenhab.internal.rest.RemoteopenhabRestClient;
42 import org.openhab.core.library.CoreItemFactory;
43 import org.openhab.core.library.types.DateTimeType;
44 import org.openhab.core.library.types.DecimalType;
45 import org.openhab.core.library.types.HSBType;
46 import org.openhab.core.library.types.OnOffType;
47 import org.openhab.core.library.types.OpenClosedType;
48 import org.openhab.core.library.types.PercentType;
49 import org.openhab.core.library.types.PlayPauseType;
50 import org.openhab.core.library.types.PointType;
51 import org.openhab.core.library.types.QuantityType;
52 import org.openhab.core.library.types.RawType;
53 import org.openhab.core.library.types.StringType;
54 import org.openhab.core.net.NetUtil;
55 import org.openhab.core.thing.Bridge;
56 import org.openhab.core.thing.Channel;
57 import org.openhab.core.thing.ChannelUID;
58 import org.openhab.core.thing.ThingStatus;
59 import org.openhab.core.thing.ThingStatusDetail;
60 import org.openhab.core.thing.binding.BaseBridgeHandler;
61 import org.openhab.core.thing.binding.builder.ChannelBuilder;
62 import org.openhab.core.thing.binding.builder.ThingBuilder;
63 import org.openhab.core.thing.type.AutoUpdatePolicy;
64 import org.openhab.core.thing.type.ChannelKind;
65 import org.openhab.core.thing.type.ChannelType;
66 import org.openhab.core.thing.type.ChannelTypeBuilder;
67 import org.openhab.core.thing.type.ChannelTypeUID;
68 import org.openhab.core.types.Command;
69 import org.openhab.core.types.RefreshType;
70 import org.openhab.core.types.State;
71 import org.openhab.core.types.StateDescriptionFragmentBuilder;
72 import org.openhab.core.types.StateOption;
73 import org.openhab.core.types.TypeParser;
74 import org.openhab.core.types.UnDefType;
75 import org.osgi.service.jaxrs.client.SseEventSourceFactory;
76 import org.slf4j.Logger;
77 import org.slf4j.LoggerFactory;
79 import com.google.gson.Gson;
82 * The {@link RemoteopenhabBridgeHandler} is responsible for handling commands and updating states
83 * using the REST API of the remote openHAB server.
85 * @author Laurent Garnier - Initial contribution
88 public class RemoteopenhabBridgeHandler extends BaseBridgeHandler implements RemoteopenhabStreamingDataListener {
90 private static final String DATE_FORMAT_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSSZ";
91 private static final DateTimeFormatter FORMATTER_DATE = DateTimeFormatter.ofPattern(DATE_FORMAT_PATTERN);
93 private static final long CONNECTION_TIMEOUT_MILLIS = TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES);
94 private static final int MAX_STATE_SIZE_FOR_LOGGING = 50;
96 private final Logger logger = LoggerFactory.getLogger(RemoteopenhabBridgeHandler.class);
98 private final ClientBuilder clientBuilder;
99 private final SseEventSourceFactory eventSourceFactory;
100 private final RemoteopenhabChannelTypeProvider channelTypeProvider;
101 private final RemoteopenhabStateDescriptionOptionProvider stateDescriptionProvider;
102 private final Gson jsonParser;
104 private final Object updateThingLock = new Object();
106 private @NonNullByDefault({}) RemoteopenhabInstanceConfiguration config;
108 private @Nullable ScheduledFuture<?> checkConnectionJob;
109 private @Nullable RemoteopenhabRestClient restClient;
111 public RemoteopenhabBridgeHandler(Bridge bridge, ClientBuilder clientBuilder,
112 SseEventSourceFactory eventSourceFactory, RemoteopenhabChannelTypeProvider channelTypeProvider,
113 RemoteopenhabStateDescriptionOptionProvider stateDescriptionProvider, final Gson jsonParser) {
115 this.clientBuilder = clientBuilder;
116 this.eventSourceFactory = eventSourceFactory;
117 this.channelTypeProvider = channelTypeProvider;
118 this.stateDescriptionProvider = stateDescriptionProvider;
119 this.jsonParser = jsonParser;
123 public void initialize() {
124 logger.debug("Initializing remote openHAB handler for bridge {}", getThing().getUID());
126 config = getConfigAs(RemoteopenhabInstanceConfiguration.class);
128 String host = config.host.trim();
129 if (host.length() == 0) {
130 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
131 "Undefined server address setting in the thing configuration");
134 List<String> localIpAddresses = NetUtil.getAllInterfaceAddresses().stream()
135 .filter(a -> !a.getAddress().isLinkLocalAddress())
136 .map(a -> a.getAddress().getHostAddress().split("%")[0]).collect(Collectors.toList());
137 if (localIpAddresses.contains(host)) {
138 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
139 "Do not use the local server as a remote server in the thing configuration");
142 String path = config.restPath.trim();
143 if (path.length() == 0 || !path.startsWith("/")) {
144 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
145 "Invalid REST API path setting in the thing configuration");
150 url = new URL("http", host, config.port, path);
151 } catch (MalformedURLException e) {
152 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
153 "Invalid REST URL built from the settings in the thing configuration");
157 String urlStr = url.toString();
158 if (urlStr.endsWith("/")) {
159 urlStr = urlStr.substring(0, urlStr.length() - 1);
161 logger.debug("REST URL = {}", urlStr);
163 RemoteopenhabRestClient client = new RemoteopenhabRestClient(clientBuilder, eventSourceFactory, jsonParser,
164 config.token, urlStr);
167 updateStatus(ThingStatus.UNKNOWN);
169 startCheckConnectionJob(client);
173 public void dispose() {
174 logger.debug("Disposing remote openHAB handler for bridge {}", getThing().getUID());
175 stopStreamingUpdates();
176 stopCheckConnectionJob();
177 this.restClient = null;
181 public void handleCommand(ChannelUID channelUID, Command command) {
182 if (getThing().getStatus() != ThingStatus.ONLINE) {
185 RemoteopenhabRestClient client = restClient;
186 if (client == null) {
191 if (command instanceof RefreshType) {
192 String state = client.getRemoteItemState(channelUID.getId());
193 updateChannelState(channelUID.getId(), null, state);
194 } else if (isLinked(channelUID)) {
195 client.sendCommandToRemoteItem(channelUID.getId(), command);
196 String commandStr = command.toFullString();
197 logger.debug("Sending command {} to remote item {} succeeded",
198 commandStr.length() < MAX_STATE_SIZE_FOR_LOGGING ? commandStr
199 : commandStr.substring(0, MAX_STATE_SIZE_FOR_LOGGING) + "...",
202 } catch (RemoteopenhabException e) {
203 logger.debug("{}", e.getMessage());
207 private void createChannels(List<Item> items, boolean replace) {
208 synchronized (updateThingLock) {
210 List<Channel> channels = new ArrayList<>();
211 for (Item item : items) {
212 String itemType = item.type;
213 boolean readOnly = false;
214 if ("Group".equals(itemType)) {
215 if (item.groupType.isEmpty()) {
216 // Standard groups are ignored
220 itemType = item.groupType;
223 if (item.stateDescription != null && item.stateDescription.readOnly) {
227 String channelTypeId = String.format("item%s%s", itemType.replace(":", ""), readOnly ? "RO" : "");
228 ChannelTypeUID channelTypeUID = new ChannelTypeUID(BINDING_ID, channelTypeId);
229 ChannelType channelType = channelTypeProvider.getChannelType(channelTypeUID, null);
232 if (channelType == null) {
233 logger.trace("Create the channel type {} for item type {}", channelTypeUID, itemType);
234 label = String.format("Remote %s Item", itemType);
235 description = String.format("An item of type %s from the remote server.", itemType);
236 channelType = ChannelTypeBuilder.state(channelTypeUID, label, itemType).withDescription(description)
237 .withStateDescriptionFragment(
238 StateDescriptionFragmentBuilder.create().withReadOnly(readOnly).build())
239 .withAutoUpdatePolicy(AutoUpdatePolicy.VETO).build();
240 channelTypeProvider.addChannelType(channelType);
242 ChannelUID channelUID = new ChannelUID(getThing().getUID(), item.name);
243 logger.trace("Create the channel {} of type {}", channelUID, channelTypeUID);
244 label = "Item " + item.name;
245 description = String.format("Item %s from the remote server.", item.name);
246 channels.add(ChannelBuilder.create(channelUID, itemType).withType(channelTypeUID)
247 .withKind(ChannelKind.STATE).withLabel(label).withDescription(description).build());
249 ThingBuilder thingBuilder = editThing();
251 thingBuilder.withChannels(channels);
252 updateThing(thingBuilder.build());
253 logger.debug("{} channels defined for the thing {} (from {} items including {} groups)",
254 channels.size(), getThing().getUID(), items.size(), nbGroups);
255 } else if (channels.size() > 0) {
257 for (Channel channel : channels) {
258 if (getThing().getChannel(channel.getUID()) != null) {
259 thingBuilder.withoutChannel(channel.getUID());
264 logger.debug("{} channels removed for the thing {} (from {} items)", nbRemoved, getThing().getUID(),
267 for (Channel channel : channels) {
268 thingBuilder.withChannel(channel);
270 updateThing(thingBuilder.build());
272 logger.debug("{} channels added for the thing {} (from {} items including {} groups)",
273 channels.size(), getThing().getUID(), items.size(), nbGroups);
275 logger.debug("{} channels added for the thing {} (from {} items)", channels.size(),
276 getThing().getUID(), items.size());
282 private void removeChannels(List<Item> items) {
283 synchronized (updateThingLock) {
285 ThingBuilder thingBuilder = editThing();
286 for (Item item : items) {
287 Channel channel = getThing().getChannel(item.name);
288 if (channel != null) {
289 thingBuilder.withoutChannel(channel.getUID());
294 updateThing(thingBuilder.build());
295 logger.debug("{} channels removed for the thing {} (from {} items)", nbRemoved, getThing().getUID(),
301 private void setStateOptions(List<Item> items) {
302 for (Item item : items) {
303 Channel channel = getThing().getChannel(item.name);
304 StateDescription descr = item.stateDescription;
305 List<Option> options = descr == null ? null : descr.options;
306 if (channel != null && options != null && options.size() > 0) {
307 List<StateOption> stateOptions = new ArrayList<>();
308 for (Option option : options) {
309 stateOptions.add(new StateOption(option.value, option.label));
311 stateDescriptionProvider.setStateOptions(channel.getUID(), stateOptions);
312 logger.trace("{} options set for the channel {}", options.size(), channel.getUID());
317 public void checkConnection(RemoteopenhabRestClient client) {
318 logger.debug("Try the root REST API...");
321 if (client.getRestApiVersion() == null) {
322 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
323 "OH 1.x server not supported by the binding");
325 List<Item> items = client.getRemoteItems();
327 createChannels(items, true);
328 setStateOptions(items);
329 for (Item item : items) {
330 updateChannelState(item.name, null, item.state);
333 updateStatus(ThingStatus.ONLINE);
335 restartStreamingUpdates();
337 } catch (RemoteopenhabException e) {
338 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
339 stopStreamingUpdates();
343 private void startCheckConnectionJob(RemoteopenhabRestClient client) {
344 ScheduledFuture<?> localCheckConnectionJob = checkConnectionJob;
345 if (localCheckConnectionJob == null || localCheckConnectionJob.isCancelled()) {
346 checkConnectionJob = scheduler.scheduleWithFixedDelay(() -> {
347 long millisSinceLastEvent = System.currentTimeMillis() - client.getLastEventTimestamp();
348 if (millisSinceLastEvent > CONNECTION_TIMEOUT_MILLIS) {
349 logger.debug("Check: Disconnected from streaming events, millisSinceLastEvent={}",
350 millisSinceLastEvent);
351 checkConnection(client);
353 logger.debug("Check: Receiving streaming events, millisSinceLastEvent={}", millisSinceLastEvent);
355 }, 0, CONNECTION_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
359 private void stopCheckConnectionJob() {
360 ScheduledFuture<?> localCheckConnectionJob = checkConnectionJob;
361 if (localCheckConnectionJob != null) {
362 localCheckConnectionJob.cancel(true);
363 checkConnectionJob = null;
367 private void restartStreamingUpdates() {
368 RemoteopenhabRestClient client = restClient;
369 if (client != null) {
370 synchronized (client) {
371 stopStreamingUpdates();
372 startStreamingUpdates();
377 private void startStreamingUpdates() {
378 RemoteopenhabRestClient client = restClient;
379 if (client != null) {
380 synchronized (client) {
381 client.addStreamingDataListener(this);
387 private void stopStreamingUpdates() {
388 RemoteopenhabRestClient client = restClient;
389 if (client != null) {
390 synchronized (client) {
392 client.removeStreamingDataListener(this);
398 public void onConnected() {
399 updateStatus(ThingStatus.ONLINE);
403 public void onError(String message) {
404 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message);
408 public void onItemStateEvent(String itemName, String stateType, String state) {
409 updateChannelState(itemName, stateType, state);
413 public void onItemAdded(Item item) {
414 createChannels(List.of(item), false);
418 public void onItemRemoved(Item item) {
419 removeChannels(List.of(item));
423 public void onItemUpdated(Item newItem, Item oldItem) {
424 if (!newItem.type.equals(oldItem.type)) {
425 createChannels(List.of(newItem), false);
427 logger.trace("Updated remote item {} ignored because item type {} is unchanged", newItem.name,
432 private void updateChannelState(String itemName, @Nullable String stateType, String state) {
433 Channel channel = getThing().getChannel(itemName);
434 if (channel == null) {
435 logger.trace("No channel for item {}", itemName);
438 String acceptedItemType = channel.getAcceptedItemType();
439 if (acceptedItemType == null) {
440 logger.trace("Channel without accepted item type for item {}", itemName);
443 if (!isLinked(channel.getUID())) {
444 logger.trace("Unlinked channel {}", channel.getUID());
447 State channelState = null;
448 if (stateType == null && "NULL".equals(state)) {
449 channelState = UnDefType.NULL;
450 } else if (stateType == null && "UNDEF".equals(state)) {
451 channelState = UnDefType.UNDEF;
452 } else if ("UnDef".equals(stateType)) {
455 channelState = UnDefType.NULL;
458 channelState = UnDefType.UNDEF;
461 logger.debug("Invalid UnDef value {} for item {}", state, itemName);
464 } else if (acceptedItemType.startsWith(CoreItemFactory.NUMBER + ":")) {
465 // Item type Number with dimension
466 if (checkStateType(itemName, stateType, "Quantity")) {
467 List<Class<? extends State>> stateTypes = Collections.singletonList(QuantityType.class);
468 channelState = TypeParser.parseState(stateTypes, state);
471 switch (acceptedItemType) {
472 case CoreItemFactory.STRING:
473 if (checkStateType(itemName, stateType, "String")) {
474 channelState = new StringType(state);
477 case CoreItemFactory.NUMBER:
478 if (checkStateType(itemName, stateType, "Decimal")) {
479 channelState = new DecimalType(state);
482 case CoreItemFactory.SWITCH:
483 if (checkStateType(itemName, stateType, "OnOff")) {
484 channelState = "ON".equals(state) ? OnOffType.ON : OnOffType.OFF;
487 case CoreItemFactory.CONTACT:
488 if (checkStateType(itemName, stateType, "OpenClosed")) {
489 channelState = "OPEN".equals(state) ? OpenClosedType.OPEN : OpenClosedType.CLOSED;
492 case CoreItemFactory.DIMMER:
493 if (checkStateType(itemName, stateType, "Percent")) {
494 channelState = new PercentType(state);
497 case CoreItemFactory.COLOR:
498 if (checkStateType(itemName, stateType, "HSB")) {
499 channelState = HSBType.valueOf(state);
502 case CoreItemFactory.DATETIME:
503 if (checkStateType(itemName, stateType, "DateTime")) {
505 channelState = new DateTimeType(ZonedDateTime.parse(state, FORMATTER_DATE));
506 } catch (DateTimeParseException e) {
507 logger.debug("Failed to parse date {} for item {}", state, itemName);
512 case CoreItemFactory.LOCATION:
513 if (checkStateType(itemName, stateType, "Point")) {
514 channelState = new PointType(state);
517 case CoreItemFactory.IMAGE:
518 if (checkStateType(itemName, stateType, "Raw")) {
519 channelState = RawType.valueOf(state);
522 case CoreItemFactory.PLAYER:
523 if (checkStateType(itemName, stateType, "PlayPause")) {
526 channelState = PlayPauseType.PLAY;
529 channelState = PlayPauseType.PAUSE;
532 logger.debug("Unexpected value {} for item {}", state, itemName);
537 case CoreItemFactory.ROLLERSHUTTER:
538 if (checkStateType(itemName, stateType, "Percent")) {
539 channelState = new PercentType(state);
543 logger.debug("Item type {} is not yet supported", acceptedItemType);
547 if (channelState != null) {
548 updateState(channel.getUID(), channelState);
549 String channelStateStr = channelState.toFullString();
550 logger.debug("updateState {} with {}", channel.getUID(),
551 channelStateStr.length() < MAX_STATE_SIZE_FOR_LOGGING ? channelStateStr
552 : channelStateStr.substring(0, MAX_STATE_SIZE_FOR_LOGGING) + "...");
557 private boolean checkStateType(String itemName, @Nullable String stateType, String expectedType) {
558 if (stateType != null && !expectedType.equals(stateType)) {
559 logger.debug("Unexpected value type {} for item {}", stateType, itemName);