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