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