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