]> git.basschouten.com Git - openhab-addons.git/blob
ff82b104504572ff6a04f71f9c8a41f1bf6fcad6
[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.Collection;
24 import java.util.Collections;
25 import java.util.List;
26 import java.util.concurrent.ScheduledFuture;
27 import java.util.concurrent.TimeUnit;
28 import java.util.stream.Collectors;
29
30 import javax.ws.rs.client.ClientBuilder;
31
32 import org.eclipse.jdt.annotation.NonNullByDefault;
33 import org.eclipse.jdt.annotation.Nullable;
34 import org.eclipse.jetty.client.HttpClient;
35 import org.openhab.binding.remoteopenhab.internal.RemoteopenhabChannelTypeProvider;
36 import org.openhab.binding.remoteopenhab.internal.RemoteopenhabStateDescriptionOptionProvider;
37 import org.openhab.binding.remoteopenhab.internal.config.RemoteopenhabServerConfiguration;
38 import org.openhab.binding.remoteopenhab.internal.data.RemoteopenhabItem;
39 import org.openhab.binding.remoteopenhab.internal.data.RemoteopenhabStateDescription;
40 import org.openhab.binding.remoteopenhab.internal.data.RemoteopenhabStateOption;
41 import org.openhab.binding.remoteopenhab.internal.discovery.RemoteopenhabDiscoveryService;
42 import org.openhab.binding.remoteopenhab.internal.exceptions.RemoteopenhabException;
43 import org.openhab.binding.remoteopenhab.internal.listener.RemoteopenhabItemsDataListener;
44 import org.openhab.binding.remoteopenhab.internal.listener.RemoteopenhabStreamingDataListener;
45 import org.openhab.binding.remoteopenhab.internal.rest.RemoteopenhabRestClient;
46 import org.openhab.core.library.CoreItemFactory;
47 import org.openhab.core.library.types.DateTimeType;
48 import org.openhab.core.library.types.DecimalType;
49 import org.openhab.core.library.types.HSBType;
50 import org.openhab.core.library.types.OnOffType;
51 import org.openhab.core.library.types.OpenClosedType;
52 import org.openhab.core.library.types.PercentType;
53 import org.openhab.core.library.types.PlayPauseType;
54 import org.openhab.core.library.types.PointType;
55 import org.openhab.core.library.types.QuantityType;
56 import org.openhab.core.library.types.RawType;
57 import org.openhab.core.library.types.StringType;
58 import org.openhab.core.net.NetUtil;
59 import org.openhab.core.thing.Bridge;
60 import org.openhab.core.thing.Channel;
61 import org.openhab.core.thing.ChannelUID;
62 import org.openhab.core.thing.ThingStatus;
63 import org.openhab.core.thing.ThingStatusDetail;
64 import org.openhab.core.thing.binding.BaseBridgeHandler;
65 import org.openhab.core.thing.binding.ThingHandlerService;
66 import org.openhab.core.thing.binding.builder.ChannelBuilder;
67 import org.openhab.core.thing.binding.builder.ThingBuilder;
68 import org.openhab.core.thing.type.AutoUpdatePolicy;
69 import org.openhab.core.thing.type.ChannelKind;
70 import org.openhab.core.thing.type.ChannelType;
71 import org.openhab.core.thing.type.ChannelTypeBuilder;
72 import org.openhab.core.thing.type.ChannelTypeUID;
73 import org.openhab.core.types.Command;
74 import org.openhab.core.types.RefreshType;
75 import org.openhab.core.types.State;
76 import org.openhab.core.types.StateDescriptionFragmentBuilder;
77 import org.openhab.core.types.StateOption;
78 import org.openhab.core.types.TypeParser;
79 import org.openhab.core.types.UnDefType;
80 import org.osgi.service.jaxrs.client.SseEventSourceFactory;
81 import org.slf4j.Logger;
82 import org.slf4j.LoggerFactory;
83
84 import com.google.gson.Gson;
85
86 /**
87  * The {@link RemoteopenhabBridgeHandler} is responsible for handling commands and updating states
88  * using the REST API of the remote openHAB server.
89  *
90  * @author Laurent Garnier - Initial contribution
91  */
92 @NonNullByDefault
93 public class RemoteopenhabBridgeHandler extends BaseBridgeHandler
94         implements RemoteopenhabStreamingDataListener, RemoteopenhabItemsDataListener {
95
96     private static final String DATE_FORMAT_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSSZ";
97     private static final DateTimeFormatter FORMATTER_DATE = DateTimeFormatter.ofPattern(DATE_FORMAT_PATTERN);
98
99     private static final long CONNECTION_TIMEOUT_MILLIS = TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES);
100     private static final int MAX_STATE_SIZE_FOR_LOGGING = 50;
101
102     private final Logger logger = LoggerFactory.getLogger(RemoteopenhabBridgeHandler.class);
103
104     private final HttpClient httpClientTrustingCert;
105     private final RemoteopenhabChannelTypeProvider channelTypeProvider;
106     private final RemoteopenhabStateDescriptionOptionProvider stateDescriptionProvider;
107
108     private final Object updateThingLock = new Object();
109
110     private @NonNullByDefault({}) RemoteopenhabServerConfiguration config;
111
112     private @Nullable ScheduledFuture<?> checkConnectionJob;
113     private RemoteopenhabRestClient restClient;
114
115     public RemoteopenhabBridgeHandler(Bridge bridge, HttpClient httpClient, HttpClient httpClientTrustingCert,
116             ClientBuilder clientBuilder, SseEventSourceFactory eventSourceFactory,
117             RemoteopenhabChannelTypeProvider channelTypeProvider,
118             RemoteopenhabStateDescriptionOptionProvider stateDescriptionProvider, final Gson jsonParser) {
119         super(bridge);
120         this.httpClientTrustingCert = httpClientTrustingCert;
121         this.channelTypeProvider = channelTypeProvider;
122         this.stateDescriptionProvider = stateDescriptionProvider;
123         this.restClient = new RemoteopenhabRestClient(httpClient, clientBuilder, eventSourceFactory, jsonParser);
124     }
125
126     @Override
127     public void initialize() {
128         logger.debug("Initializing remote openHAB handler for bridge {}", getThing().getUID());
129
130         config = getConfigAs(RemoteopenhabServerConfiguration.class);
131
132         String host = config.host.trim();
133         if (host.length() == 0) {
134             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
135                     "Undefined server address setting in the thing configuration");
136             return;
137         }
138         List<String> localIpAddresses = NetUtil.getAllInterfaceAddresses().stream()
139                 .filter(a -> !a.getAddress().isLinkLocalAddress())
140                 .map(a -> a.getAddress().getHostAddress().split("%")[0]).collect(Collectors.toList());
141         if (localIpAddresses.contains(host)) {
142             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
143                     "Do not use the local server as a remote server in the thing configuration");
144             return;
145         }
146         String path = config.restPath.trim();
147         if (path.length() == 0 || !path.startsWith("/")) {
148             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
149                     "Invalid REST API path setting in the thing configuration");
150             return;
151         }
152         URL url;
153         try {
154             url = new URL(config.useHttps ? "https" : "http", host, config.port, path);
155         } catch (MalformedURLException e) {
156             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
157                     "Invalid REST URL built from the settings in the thing configuration");
158             return;
159         }
160
161         String urlStr = url.toString();
162         if (urlStr.endsWith("/")) {
163             urlStr = urlStr.substring(0, urlStr.length() - 1);
164         }
165         logger.debug("REST URL = {}", urlStr);
166
167         restClient.setRestUrl(urlStr);
168         restClient.setAccessToken(config.token);
169         if (config.useHttps && config.trustedCertificate) {
170             restClient.setHttpClient(httpClientTrustingCert);
171             restClient.setTrustedCertificate(true);
172         }
173
174         updateStatus(ThingStatus.UNKNOWN);
175
176         startCheckConnectionJob();
177     }
178
179     @Override
180     public void dispose() {
181         logger.debug("Disposing remote openHAB handler for bridge {}", getThing().getUID());
182         stopStreamingUpdates();
183         stopCheckConnectionJob();
184     }
185
186     @Override
187     public void handleCommand(ChannelUID channelUID, Command command) {
188         if (getThing().getStatus() != ThingStatus.ONLINE) {
189             return;
190         }
191
192         try {
193             if (command instanceof RefreshType) {
194                 String state = restClient.getRemoteItemState(channelUID.getId());
195                 updateChannelState(channelUID.getId(), null, state);
196             } else if (isLinked(channelUID)) {
197                 restClient.sendCommandToRemoteItem(channelUID.getId(), command);
198                 String commandStr = command.toFullString();
199                 logger.debug("Sending command {} to remote item {} succeeded",
200                         commandStr.length() < MAX_STATE_SIZE_FOR_LOGGING ? commandStr
201                                 : commandStr.substring(0, MAX_STATE_SIZE_FOR_LOGGING) + "...",
202                         channelUID.getId());
203             }
204         } catch (RemoteopenhabException e) {
205             logger.debug("{}", e.getMessage());
206         }
207     }
208
209     private void createChannels(List<RemoteopenhabItem> items, boolean replace) {
210         synchronized (updateThingLock) {
211             int nbGroups = 0;
212             List<Channel> channels = new ArrayList<>();
213             for (RemoteopenhabItem item : items) {
214                 String itemType = item.type;
215                 boolean readOnly = false;
216                 if ("Group".equals(itemType)) {
217                     if (item.groupType.isEmpty()) {
218                         // Standard groups are ignored
219                         nbGroups++;
220                         continue;
221                     } else {
222                         itemType = item.groupType;
223                     }
224                 } else {
225                     if (item.stateDescription != null && item.stateDescription.readOnly) {
226                         readOnly = true;
227                     }
228                 }
229                 String channelTypeId = String.format("item%s%s", itemType.replace(":", ""), readOnly ? "RO" : "");
230                 ChannelTypeUID channelTypeUID = new ChannelTypeUID(BINDING_ID, channelTypeId);
231                 ChannelType channelType = channelTypeProvider.getChannelType(channelTypeUID, null);
232                 String label;
233                 String description;
234                 if (channelType == null) {
235                     logger.trace("Create the channel type {} for item type {}", channelTypeUID, itemType);
236                     label = String.format("Remote %s Item", itemType);
237                     description = String.format("An item of type %s from the remote server.", itemType);
238                     channelType = ChannelTypeBuilder.state(channelTypeUID, label, itemType).withDescription(description)
239                             .withStateDescriptionFragment(
240                                     StateDescriptionFragmentBuilder.create().withReadOnly(readOnly).build())
241                             .withAutoUpdatePolicy(AutoUpdatePolicy.VETO).build();
242                     channelTypeProvider.addChannelType(channelType);
243                 }
244                 ChannelUID channelUID = new ChannelUID(getThing().getUID(), item.name);
245                 logger.trace("Create the channel {} of type {}", channelUID, channelTypeUID);
246                 label = "Item " + item.name;
247                 description = String.format("Item %s from the remote server.", item.name);
248                 channels.add(ChannelBuilder.create(channelUID, itemType).withType(channelTypeUID)
249                         .withKind(ChannelKind.STATE).withLabel(label).withDescription(description).build());
250             }
251             ThingBuilder thingBuilder = editThing();
252             if (replace) {
253                 thingBuilder.withChannels(channels);
254                 updateThing(thingBuilder.build());
255                 logger.debug("{} channels defined for the thing {} (from {} items including {} groups)",
256                         channels.size(), getThing().getUID(), items.size(), nbGroups);
257             } else if (channels.size() > 0) {
258                 int nbRemoved = 0;
259                 for (Channel channel : channels) {
260                     if (getThing().getChannel(channel.getUID()) != null) {
261                         thingBuilder.withoutChannel(channel.getUID());
262                         nbRemoved++;
263                     }
264                 }
265                 if (nbRemoved > 0) {
266                     logger.debug("{} channels removed for the thing {} (from {} items)", nbRemoved, getThing().getUID(),
267                             items.size());
268                 }
269                 for (Channel channel : channels) {
270                     thingBuilder.withChannel(channel);
271                 }
272                 updateThing(thingBuilder.build());
273                 if (nbGroups > 0) {
274                     logger.debug("{} channels added for the thing {} (from {} items including {} groups)",
275                             channels.size(), getThing().getUID(), items.size(), nbGroups);
276                 } else {
277                     logger.debug("{} channels added for the thing {} (from {} items)", channels.size(),
278                             getThing().getUID(), items.size());
279                 }
280             }
281         }
282     }
283
284     private void removeChannels(List<RemoteopenhabItem> items) {
285         synchronized (updateThingLock) {
286             int nbRemoved = 0;
287             ThingBuilder thingBuilder = editThing();
288             for (RemoteopenhabItem item : items) {
289                 Channel channel = getThing().getChannel(item.name);
290                 if (channel != null) {
291                     thingBuilder.withoutChannel(channel.getUID());
292                     nbRemoved++;
293                 }
294             }
295             if (nbRemoved > 0) {
296                 updateThing(thingBuilder.build());
297                 logger.debug("{} channels removed for the thing {} (from {} items)", nbRemoved, getThing().getUID(),
298                         items.size());
299             }
300         }
301     }
302
303     private void setStateOptions(List<RemoteopenhabItem> items) {
304         for (RemoteopenhabItem item : items) {
305             Channel channel = getThing().getChannel(item.name);
306             RemoteopenhabStateDescription descr = item.stateDescription;
307             List<RemoteopenhabStateOption> options = descr == null ? null : descr.options;
308             if (channel != null && options != null && options.size() > 0) {
309                 List<StateOption> stateOptions = new ArrayList<>();
310                 for (RemoteopenhabStateOption option : options) {
311                     stateOptions.add(new StateOption(option.value, option.label));
312                 }
313                 stateDescriptionProvider.setStateOptions(channel.getUID(), stateOptions);
314                 logger.trace("{} options set for the channel {}", options.size(), channel.getUID());
315             }
316         }
317     }
318
319     public void checkConnection() {
320         logger.debug("Try the root REST API...");
321         try {
322             restClient.tryApi();
323             if (restClient.getRestApiVersion() == null) {
324                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
325                         "OH 1.x server not supported by the binding");
326             } else if (getThing().getStatus() != ThingStatus.ONLINE) {
327                 List<RemoteopenhabItem> items = restClient.getRemoteItems("name,type,groupType,stateDescription");
328
329                 createChannels(items, true);
330                 setStateOptions(items);
331
332                 try {
333                     items = restClient.getRemoteItems("name,state");
334                     for (RemoteopenhabItem item : items) {
335                         updateChannelState(item.name, null, item.state);
336                     }
337                 } catch (RemoteopenhabException e) {
338                     logger.debug("{}", e.getMessage());
339                 }
340
341                 updateStatus(ThingStatus.ONLINE);
342
343                 restartStreamingUpdates();
344             }
345         } catch (RemoteopenhabException e) {
346             logger.debug("{}", e.getMessage());
347             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
348             stopStreamingUpdates();
349         }
350     }
351
352     private void startCheckConnectionJob() {
353         ScheduledFuture<?> localCheckConnectionJob = checkConnectionJob;
354         if (localCheckConnectionJob == null || localCheckConnectionJob.isCancelled()) {
355             checkConnectionJob = scheduler.scheduleWithFixedDelay(() -> {
356                 long millisSinceLastEvent = System.currentTimeMillis() - restClient.getLastEventTimestamp();
357                 if (millisSinceLastEvent > CONNECTION_TIMEOUT_MILLIS) {
358                     logger.debug("Check: Maybe disconnected from streaming events, millisSinceLastEvent={}",
359                             millisSinceLastEvent);
360                     checkConnection();
361                 } else {
362                     logger.debug("Check: Receiving streaming events, millisSinceLastEvent={}", millisSinceLastEvent);
363                 }
364             }, 0, CONNECTION_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
365         }
366     }
367
368     private void stopCheckConnectionJob() {
369         ScheduledFuture<?> localCheckConnectionJob = checkConnectionJob;
370         if (localCheckConnectionJob != null) {
371             localCheckConnectionJob.cancel(true);
372             checkConnectionJob = null;
373         }
374     }
375
376     private void restartStreamingUpdates() {
377         synchronized (restClient) {
378             stopStreamingUpdates();
379             startStreamingUpdates();
380         }
381     }
382
383     private void startStreamingUpdates() {
384         synchronized (restClient) {
385             restClient.addStreamingDataListener(this);
386             restClient.addItemsDataListener(this);
387             restClient.start();
388         }
389     }
390
391     private void stopStreamingUpdates() {
392         synchronized (restClient) {
393             restClient.stop();
394             restClient.removeStreamingDataListener(this);
395             restClient.removeItemsDataListener(this);
396         }
397     }
398
399     public RemoteopenhabRestClient gestRestClient() {
400         return restClient;
401     }
402
403     @Override
404     public void onConnected() {
405         updateStatus(ThingStatus.ONLINE);
406     }
407
408     @Override
409     public void onError(String message) {
410         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message);
411     }
412
413     @Override
414     public void onItemStateEvent(String itemName, String stateType, String state) {
415         updateChannelState(itemName, stateType, state);
416     }
417
418     @Override
419     public void onItemAdded(RemoteopenhabItem item) {
420         createChannels(List.of(item), false);
421     }
422
423     @Override
424     public void onItemRemoved(RemoteopenhabItem item) {
425         removeChannels(List.of(item));
426     }
427
428     @Override
429     public void onItemUpdated(RemoteopenhabItem newItem, RemoteopenhabItem oldItem) {
430         if (!newItem.type.equals(oldItem.type)) {
431             createChannels(List.of(newItem), false);
432         } else {
433             logger.trace("Updated remote item {} ignored because item type {} is unchanged", newItem.name,
434                     newItem.type);
435         }
436     }
437
438     private void updateChannelState(String itemName, @Nullable String stateType, String state) {
439         Channel channel = getThing().getChannel(itemName);
440         if (channel == null) {
441             logger.trace("No channel for item {}", itemName);
442             return;
443         }
444         String acceptedItemType = channel.getAcceptedItemType();
445         if (acceptedItemType == null) {
446             logger.trace("Channel without accepted item type for item {}", itemName);
447             return;
448         }
449         if (!isLinked(channel.getUID())) {
450             logger.trace("Unlinked channel {}", channel.getUID());
451             return;
452         }
453         State channelState = null;
454         if (stateType == null && "NULL".equals(state)) {
455             channelState = UnDefType.NULL;
456         } else if (stateType == null && "UNDEF".equals(state)) {
457             channelState = UnDefType.UNDEF;
458         } else if ("UnDef".equals(stateType)) {
459             switch (state) {
460                 case "NULL":
461                     channelState = UnDefType.NULL;
462                     break;
463                 case "UNDEF":
464                     channelState = UnDefType.UNDEF;
465                     break;
466                 default:
467                     logger.debug("Invalid UnDef value {} for item {}", state, itemName);
468                     break;
469             }
470         } else if (acceptedItemType.startsWith(CoreItemFactory.NUMBER + ":")) {
471             // Item type Number with dimension
472             if (stateType == null || "Quantity".equals(stateType)) {
473                 List<Class<? extends State>> stateTypes = Collections.singletonList(QuantityType.class);
474                 channelState = TypeParser.parseState(stateTypes, state);
475             } else if ("Decimal".equals(stateType)) {
476                 channelState = new DecimalType(state);
477             } else {
478                 logger.debug("Unexpected value type {} for item {}", stateType, itemName);
479             }
480         } else {
481             switch (acceptedItemType) {
482                 case CoreItemFactory.STRING:
483                     if (checkStateType(itemName, stateType, "String")) {
484                         channelState = new StringType(state);
485                     }
486                     break;
487                 case CoreItemFactory.NUMBER:
488                     if (checkStateType(itemName, stateType, "Decimal")) {
489                         channelState = new DecimalType(state);
490                     }
491                     break;
492                 case CoreItemFactory.SWITCH:
493                     if (checkStateType(itemName, stateType, "OnOff")) {
494                         channelState = "ON".equals(state) ? OnOffType.ON : OnOffType.OFF;
495                     }
496                     break;
497                 case CoreItemFactory.CONTACT:
498                     if (checkStateType(itemName, stateType, "OpenClosed")) {
499                         channelState = "OPEN".equals(state) ? OpenClosedType.OPEN : OpenClosedType.CLOSED;
500                     }
501                     break;
502                 case CoreItemFactory.DIMMER:
503                     if (checkStateType(itemName, stateType, "Percent")) {
504                         channelState = new PercentType(state);
505                     }
506                     break;
507                 case CoreItemFactory.COLOR:
508                     if (checkStateType(itemName, stateType, "HSB")) {
509                         channelState = HSBType.valueOf(state);
510                     }
511                     break;
512                 case CoreItemFactory.DATETIME:
513                     if (checkStateType(itemName, stateType, "DateTime")) {
514                         try {
515                             channelState = new DateTimeType(ZonedDateTime.parse(state, FORMATTER_DATE));
516                         } catch (DateTimeParseException e) {
517                             logger.debug("Failed to parse date {} for item {}", state, itemName);
518                             channelState = null;
519                         }
520                     }
521                     break;
522                 case CoreItemFactory.LOCATION:
523                     if (checkStateType(itemName, stateType, "Point")) {
524                         channelState = new PointType(state);
525                     }
526                     break;
527                 case CoreItemFactory.IMAGE:
528                     if (checkStateType(itemName, stateType, "Raw")) {
529                         channelState = RawType.valueOf(state);
530                     }
531                     break;
532                 case CoreItemFactory.PLAYER:
533                     if (checkStateType(itemName, stateType, "PlayPause")) {
534                         switch (state) {
535                             case "PLAY":
536                                 channelState = PlayPauseType.PLAY;
537                                 break;
538                             case "PAUSE":
539                                 channelState = PlayPauseType.PAUSE;
540                                 break;
541                             default:
542                                 logger.debug("Unexpected value {} for item {}", state, itemName);
543                                 break;
544                         }
545                     }
546                     break;
547                 case CoreItemFactory.ROLLERSHUTTER:
548                     if (checkStateType(itemName, stateType, "Percent")) {
549                         channelState = new PercentType(state);
550                     }
551                     break;
552                 default:
553                     logger.debug("Item type {} is not yet supported", acceptedItemType);
554                     break;
555             }
556         }
557         if (channelState != null) {
558             updateState(channel.getUID(), channelState);
559             String channelStateStr = channelState.toFullString();
560             logger.debug("updateState {} with {}", channel.getUID(),
561                     channelStateStr.length() < MAX_STATE_SIZE_FOR_LOGGING ? channelStateStr
562                             : channelStateStr.substring(0, MAX_STATE_SIZE_FOR_LOGGING) + "...");
563         }
564     }
565
566     private boolean checkStateType(String itemName, @Nullable String stateType, String expectedType) {
567         if (stateType != null && !expectedType.equals(stateType)) {
568             logger.debug("Unexpected value type {} for item {}", stateType, itemName);
569             return false;
570         } else {
571             return true;
572         }
573     }
574
575     @Override
576     public Collection<Class<? extends ThingHandlerService>> getServices() {
577         return Collections.singleton(RemoteopenhabDiscoveryService.class);
578     }
579 }