]> git.basschouten.com Git - openhab-addons.git/blob
a4fc75f89339b9a39419b44847f398618d94744e
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.remoteopenhab.internal.handler;
14
15 import static org.openhab.binding.remoteopenhab.internal.RemoteopenhabBindingConstants.BINDING_ID;
16
17 import java.net.MalformedURLException;
18 import java.net.URL;
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;
28
29 import javax.ws.rs.client.ClientBuilder;
30
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;
78
79 import com.google.gson.Gson;
80
81 /**
82  * The {@link RemoteopenhabBridgeHandler} is responsible for handling commands and updating states
83  * using the REST API of the remote openHAB server.
84  *
85  * @author Laurent Garnier - Initial contribution
86  */
87 @NonNullByDefault
88 public class RemoteopenhabBridgeHandler extends BaseBridgeHandler implements RemoteopenhabStreamingDataListener {
89
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);
92
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;
95
96     private final Logger logger = LoggerFactory.getLogger(RemoteopenhabBridgeHandler.class);
97
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;
103
104     private final Object updateThingLock = new Object();
105
106     private @NonNullByDefault({}) RemoteopenhabInstanceConfiguration config;
107
108     private @Nullable ScheduledFuture<?> checkConnectionJob;
109     private @Nullable RemoteopenhabRestClient restClient;
110
111     public RemoteopenhabBridgeHandler(Bridge bridge, ClientBuilder clientBuilder,
112             SseEventSourceFactory eventSourceFactory, RemoteopenhabChannelTypeProvider channelTypeProvider,
113             RemoteopenhabStateDescriptionOptionProvider stateDescriptionProvider, final Gson jsonParser) {
114         super(bridge);
115         this.clientBuilder = clientBuilder;
116         this.eventSourceFactory = eventSourceFactory;
117         this.channelTypeProvider = channelTypeProvider;
118         this.stateDescriptionProvider = stateDescriptionProvider;
119         this.jsonParser = jsonParser;
120     }
121
122     @Override
123     public void initialize() {
124         logger.debug("Initializing remote openHAB handler for bridge {}", getThing().getUID());
125
126         config = getConfigAs(RemoteopenhabInstanceConfiguration.class);
127
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");
132             return;
133         }
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");
140             return;
141         }
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");
146             return;
147         }
148         URL url;
149         try {
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");
154             return;
155         }
156
157         String urlStr = url.toString();
158         if (urlStr.endsWith("/")) {
159             urlStr = urlStr.substring(0, urlStr.length() - 1);
160         }
161         logger.debug("REST URL = {}", urlStr);
162
163         RemoteopenhabRestClient client = new RemoteopenhabRestClient(clientBuilder, eventSourceFactory, jsonParser,
164                 config.token, urlStr);
165         restClient = client;
166
167         updateStatus(ThingStatus.UNKNOWN);
168
169         startCheckConnectionJob(client);
170     }
171
172     @Override
173     public void dispose() {
174         logger.debug("Disposing remote openHAB handler for bridge {}", getThing().getUID());
175         stopStreamingUpdates();
176         stopCheckConnectionJob();
177         this.restClient = null;
178     }
179
180     @Override
181     public void handleCommand(ChannelUID channelUID, Command command) {
182         if (getThing().getStatus() != ThingStatus.ONLINE) {
183             return;
184         }
185         RemoteopenhabRestClient client = restClient;
186         if (client == null) {
187             return;
188         }
189
190         try {
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) + "...",
200                         channelUID.getId());
201             }
202         } catch (RemoteopenhabException e) {
203             logger.debug("{}", e.getMessage());
204         }
205     }
206
207     private void createChannels(List<Item> items, boolean replace) {
208         synchronized (updateThingLock) {
209             int nbGroups = 0;
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
217                         nbGroups++;
218                         continue;
219                     } else {
220                         itemType = item.groupType;
221                     }
222                 } else {
223                     if (item.stateDescription != null && item.stateDescription.readOnly) {
224                         readOnly = true;
225                     }
226                 }
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);
230                 String label;
231                 String description;
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);
241                 }
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());
248             }
249             ThingBuilder thingBuilder = editThing();
250             if (replace) {
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) {
256                 int nbRemoved = 0;
257                 for (Channel channel : channels) {
258                     if (getThing().getChannel(channel.getUID()) != null) {
259                         thingBuilder.withoutChannel(channel.getUID());
260                         nbRemoved++;
261                     }
262                 }
263                 if (nbRemoved > 0) {
264                     logger.debug("{} channels removed for the thing {} (from {} items)", nbRemoved, getThing().getUID(),
265                             items.size());
266                 }
267                 for (Channel channel : channels) {
268                     thingBuilder.withChannel(channel);
269                 }
270                 updateThing(thingBuilder.build());
271                 if (nbGroups > 0) {
272                     logger.debug("{} channels added for the thing {} (from {} items including {} groups)",
273                             channels.size(), getThing().getUID(), items.size(), nbGroups);
274                 } else {
275                     logger.debug("{} channels added for the thing {} (from {} items)", channels.size(),
276                             getThing().getUID(), items.size());
277                 }
278             }
279         }
280     }
281
282     private void removeChannels(List<Item> items) {
283         synchronized (updateThingLock) {
284             int nbRemoved = 0;
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());
290                     nbRemoved++;
291                 }
292             }
293             if (nbRemoved > 0) {
294                 updateThing(thingBuilder.build());
295                 logger.debug("{} channels removed for the thing {} (from {} items)", nbRemoved, getThing().getUID(),
296                         items.size());
297             }
298         }
299     }
300
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));
310                 }
311                 stateDescriptionProvider.setStateOptions(channel.getUID(), stateOptions);
312                 logger.trace("{} options set for the channel {}", options.size(), channel.getUID());
313             }
314         }
315     }
316
317     public void checkConnection(RemoteopenhabRestClient client) {
318         logger.debug("Try the root REST API...");
319         try {
320             client.tryApi();
321             if (client.getRestApiVersion() == null) {
322                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
323                         "OH 1.x server not supported by the binding");
324             } else {
325                 List<Item> items = client.getRemoteItems();
326
327                 createChannels(items, true);
328                 setStateOptions(items);
329                 for (Item item : items) {
330                     updateChannelState(item.name, null, item.state);
331                 }
332
333                 updateStatus(ThingStatus.ONLINE);
334
335                 restartStreamingUpdates();
336             }
337         } catch (RemoteopenhabException e) {
338             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
339             stopStreamingUpdates();
340         }
341     }
342
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);
352                 } else {
353                     logger.debug("Check: Receiving streaming events, millisSinceLastEvent={}", millisSinceLastEvent);
354                 }
355             }, 0, CONNECTION_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
356         }
357     }
358
359     private void stopCheckConnectionJob() {
360         ScheduledFuture<?> localCheckConnectionJob = checkConnectionJob;
361         if (localCheckConnectionJob != null) {
362             localCheckConnectionJob.cancel(true);
363             checkConnectionJob = null;
364         }
365     }
366
367     private void restartStreamingUpdates() {
368         RemoteopenhabRestClient client = restClient;
369         if (client != null) {
370             synchronized (client) {
371                 stopStreamingUpdates();
372                 startStreamingUpdates();
373             }
374         }
375     }
376
377     private void startStreamingUpdates() {
378         RemoteopenhabRestClient client = restClient;
379         if (client != null) {
380             synchronized (client) {
381                 client.addStreamingDataListener(this);
382                 client.start();
383             }
384         }
385     }
386
387     private void stopStreamingUpdates() {
388         RemoteopenhabRestClient client = restClient;
389         if (client != null) {
390             synchronized (client) {
391                 client.stop();
392                 client.removeStreamingDataListener(this);
393             }
394         }
395     }
396
397     @Override
398     public void onConnected() {
399         updateStatus(ThingStatus.ONLINE);
400     }
401
402     @Override
403     public void onError(String message) {
404         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message);
405     }
406
407     @Override
408     public void onItemStateEvent(String itemName, String stateType, String state) {
409         updateChannelState(itemName, stateType, state);
410     }
411
412     @Override
413     public void onItemAdded(Item item) {
414         createChannels(List.of(item), false);
415     }
416
417     @Override
418     public void onItemRemoved(Item item) {
419         removeChannels(List.of(item));
420     }
421
422     @Override
423     public void onItemUpdated(Item newItem, Item oldItem) {
424         if (!newItem.type.equals(oldItem.type)) {
425             createChannels(List.of(newItem), false);
426         } else {
427             logger.trace("Updated remote item {} ignored because item type {} is unchanged", newItem.name,
428                     newItem.type);
429         }
430     }
431
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);
436             return;
437         }
438         String acceptedItemType = channel.getAcceptedItemType();
439         if (acceptedItemType == null) {
440             logger.trace("Channel without accepted item type for item {}", itemName);
441             return;
442         }
443         if (!isLinked(channel.getUID())) {
444             logger.trace("Unlinked channel {}", channel.getUID());
445             return;
446         }
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)) {
453             switch (state) {
454                 case "NULL":
455                     channelState = UnDefType.NULL;
456                     break;
457                 case "UNDEF":
458                     channelState = UnDefType.UNDEF;
459                     break;
460                 default:
461                     logger.debug("Invalid UnDef value {} for item {}", state, itemName);
462                     break;
463             }
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);
469             }
470         } else {
471             switch (acceptedItemType) {
472                 case CoreItemFactory.STRING:
473                     if (checkStateType(itemName, stateType, "String")) {
474                         channelState = new StringType(state);
475                     }
476                     break;
477                 case CoreItemFactory.NUMBER:
478                     if (checkStateType(itemName, stateType, "Decimal")) {
479                         channelState = new DecimalType(state);
480                     }
481                     break;
482                 case CoreItemFactory.SWITCH:
483                     if (checkStateType(itemName, stateType, "OnOff")) {
484                         channelState = "ON".equals(state) ? OnOffType.ON : OnOffType.OFF;
485                     }
486                     break;
487                 case CoreItemFactory.CONTACT:
488                     if (checkStateType(itemName, stateType, "OpenClosed")) {
489                         channelState = "OPEN".equals(state) ? OpenClosedType.OPEN : OpenClosedType.CLOSED;
490                     }
491                     break;
492                 case CoreItemFactory.DIMMER:
493                     if (checkStateType(itemName, stateType, "Percent")) {
494                         channelState = new PercentType(state);
495                     }
496                     break;
497                 case CoreItemFactory.COLOR:
498                     if (checkStateType(itemName, stateType, "HSB")) {
499                         channelState = HSBType.valueOf(state);
500                     }
501                     break;
502                 case CoreItemFactory.DATETIME:
503                     if (checkStateType(itemName, stateType, "DateTime")) {
504                         try {
505                             channelState = new DateTimeType(ZonedDateTime.parse(state, FORMATTER_DATE));
506                         } catch (DateTimeParseException e) {
507                             logger.debug("Failed to parse date {} for item {}", state, itemName);
508                             channelState = null;
509                         }
510                     }
511                     break;
512                 case CoreItemFactory.LOCATION:
513                     if (checkStateType(itemName, stateType, "Point")) {
514                         channelState = new PointType(state);
515                     }
516                     break;
517                 case CoreItemFactory.IMAGE:
518                     if (checkStateType(itemName, stateType, "Raw")) {
519                         channelState = RawType.valueOf(state);
520                     }
521                     break;
522                 case CoreItemFactory.PLAYER:
523                     if (checkStateType(itemName, stateType, "PlayPause")) {
524                         switch (state) {
525                             case "PLAY":
526                                 channelState = PlayPauseType.PLAY;
527                                 break;
528                             case "PAUSE":
529                                 channelState = PlayPauseType.PAUSE;
530                                 break;
531                             default:
532                                 logger.debug("Unexpected value {} for item {}", state, itemName);
533                                 break;
534                         }
535                     }
536                     break;
537                 case CoreItemFactory.ROLLERSHUTTER:
538                     if (checkStateType(itemName, stateType, "Percent")) {
539                         channelState = new PercentType(state);
540                     }
541                     break;
542                 default:
543                     logger.debug("Item type {} is not yet supported", acceptedItemType);
544                     break;
545             }
546         }
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) + "...");
553
554         }
555     }
556
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);
560             return false;
561         } else {
562             return true;
563         }
564     }
565 }