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