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