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