2 * Copyright (c) 2010-2022 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.remoteopenhab.internal.handler;
15 import java.net.MalformedURLException;
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;
26 import java.util.concurrent.ScheduledFuture;
27 import java.util.concurrent.TimeUnit;
29 import javax.ws.rs.client.ClientBuilder;
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;
90 import com.google.gson.Gson;
93 * The {@link RemoteopenhabBridgeHandler} is responsible for handling commands and updating states
94 * using the REST API of the remote openHAB server.
96 * @author Laurent Garnier - Initial contribution
99 public class RemoteopenhabBridgeHandler extends BaseBridgeHandler
100 implements RemoteopenhabStreamingDataListener, RemoteopenhabItemsDataListener {
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);
105 private static final int MAX_STATE_SIZE_FOR_LOGGING = 50;
107 private final Logger logger = LoggerFactory.getLogger(RemoteopenhabBridgeHandler.class);
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;
117 private final Object updateThingLock = new Object();
119 private @NonNullByDefault({}) RemoteopenhabServerConfiguration config;
121 private @Nullable ScheduledFuture<?> checkConnectionJob;
122 private RemoteopenhabRestClient restClient;
124 private Map<ChannelUID, State> channelsLastStates = new HashMap<>();
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) {
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,
145 public void initialize() {
146 logger.debug("Initializing remote openHAB handler for bridge {}", getThing().getUID());
148 config = getConfigAs(RemoteopenhabServerConfiguration.class);
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");
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");
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");
171 String urlStr = url.toString();
172 logger.debug("REST URL = {}", urlStr);
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);
181 updateStatus(ThingStatus.UNKNOWN);
183 scheduler.submit(() -> checkConnection(false));
184 if (config.accessibilityInterval > 0) {
185 startCheckConnectionJob(config.accessibilityInterval, config.aliveInterval, config.restartIfNoActivity);
190 public void dispose() {
191 logger.debug("Disposing remote openHAB handler for bridge {}", getThing().getUID());
192 stopStreamingUpdates(false);
193 stopCheckConnectionJob();
194 channelsLastStates.clear();
198 public void handleCommand(ChannelUID channelUID, Command command) {
199 if (getThing().getStatus() != ThingStatus.ONLINE) {
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) + "...",
215 } catch (RemoteopenhabException e) {
216 logger.debug("Handling command for channel {} failed: {}", channelUID.getId(),
217 e.getMessage(bundle, i18nProvider));
221 private boolean createChannels(List<RemoteopenhabItem> items, boolean replace) {
222 synchronized (updateThingLock) {
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
236 itemType = item.groupType;
239 if (item.stateDescription != null && item.stateDescription.readOnly) {
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);
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);
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++;
275 channelTypeUID = channelType.getUID();
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(),
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());
289 ThingBuilder thingBuilder = editThing();
291 thingBuilder.withChannels(channels);
292 updateThing(thingBuilder.build());
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()) {
298 for (Channel channel : channels) {
299 if (getThing().getChannel(channel.getUID()) != null) {
300 thingBuilder.withoutChannel(channel.getUID());
305 logger.debug("{} channels removed for the thing {} (from {} items)", nbRemoved,
306 getThing().getUID(), items.size());
308 for (Channel channel : channels) {
309 thingBuilder.withChannel(channel);
311 updateThing(thingBuilder.build());
313 logger.debug("{} channels added for the thing {} (from {} items including {} groups)",
314 channels.size(), getThing().getUID(), items.size(), nbGroups);
316 logger.debug("{} channels added for the thing {} (from {} items)", channels.size(),
317 getThing().getUID(), items.size());
321 } catch (IllegalArgumentException e) {
322 logger.warn("An error occurred while creating the channels for the server {}: {}", getThing().getUID(),
329 private void removeChannels(List<RemoteopenhabItem> items) {
330 synchronized (updateThingLock) {
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());
341 updateThing(thingBuilder.build());
342 logger.debug("{} channels removed for the thing {} (from {} items)", nbRemoved, getThing().getUID(),
348 private void setDynamicOptions(List<RemoteopenhabItem> items) {
349 for (RemoteopenhabItem item : items) {
350 Channel channel = getThing().getChannel(item.name);
351 if (channel == null) {
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));
361 stateDescriptionProvider.setStateOptions(channel.getUID(), options);
362 logger.trace("{} state options set for the channel {}", options.size(), channel.getUID());
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));
371 commandDescriptionProvider.setCommandOptions(channel.getUID(), options);
372 logger.trace("{} command options set for the channel {}", options.size(), channel.getUID());
377 public void checkConnection(boolean restartSse) {
378 logger.debug("Try the root REST API...");
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");
387 if (createChannels(items, true)) {
388 setDynamicOptions(items);
389 for (RemoteopenhabItem item : items) {
390 updateChannelState(item.name, null, item.state, false);
393 updateStatus(ThingStatus.ONLINE);
395 restartStreamingUpdates();
397 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/offline.error-channels-creation");
398 stopStreamingUpdates();
400 } else if (restartSse) {
401 logger.debug("The SSE connection is restarted because there was no recent event received");
402 restartStreamingUpdates();
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();
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)) {
423 "Time to check server accessibility (maybe disconnected from streaming events, millisSinceLastEvent={})",
424 millisSinceLastEvent);
425 checkConnection(restartIfNoActivity);
428 "Bypass server accessibility check (receiving streaming events, millisSinceLastEvent={})",
429 millisSinceLastEvent);
431 }, accessibilityInterval, accessibilityInterval, TimeUnit.MINUTES);
435 private void stopCheckConnectionJob() {
436 ScheduledFuture<?> localCheckConnectionJob = checkConnectionJob;
437 if (localCheckConnectionJob != null) {
438 localCheckConnectionJob.cancel(true);
439 checkConnectionJob = null;
443 private void restartStreamingUpdates() {
444 synchronized (restClient) {
445 stopStreamingUpdates();
446 startStreamingUpdates();
450 private void startStreamingUpdates() {
451 synchronized (restClient) {
452 restClient.addStreamingDataListener(this);
453 restClient.addItemsDataListener(this);
458 private void stopStreamingUpdates() {
459 stopStreamingUpdates(true);
462 private void stopStreamingUpdates(boolean waitingForCompletion) {
463 synchronized (restClient) {
464 restClient.stop(waitingForCompletion);
465 restClient.removeStreamingDataListener(this);
466 restClient.removeItemsDataListener(this);
470 public RemoteopenhabRestClient gestRestClient() {
475 public void onConnected() {
476 updateStatus(ThingStatus.ONLINE);
480 public void onDisconnected() {
481 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
482 "@text/offline.comm-error-disconnected");
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");
493 public void onItemStateEvent(String itemName, String stateType, String state, boolean onlyIfStateChanged) {
494 updateChannelState(itemName, stateType, state, onlyIfStateChanged);
498 public void onItemAdded(RemoteopenhabItem item) {
499 createChannels(List.of(item), false);
503 public void onItemRemoved(RemoteopenhabItem item) {
504 removeChannels(List.of(item));
508 public void onItemUpdated(RemoteopenhabItem newItem, RemoteopenhabItem oldItem) {
509 if (!newItem.type.equals(oldItem.type)) {
510 createChannels(List.of(newItem), false);
512 logger.trace("Updated remote item {} ignored because item type {} is unchanged", newItem.name,
518 public void onItemOptionsUpdatedd(RemoteopenhabItem item) {
519 setDynamicOptions(List.of(item));
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);
529 String acceptedItemType = channel.getAcceptedItemType();
530 if (acceptedItemType == null) {
531 logger.trace("Channel without accepted item type for item {}", itemName);
534 if (!isLinked(channel.getUID())) {
535 logger.trace("Unlinked channel {}", channel.getUID());
538 State channelState = null;
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)) {
547 channelState = UnDefType.NULL;
550 channelState = UnDefType.UNDEF;
553 logger.debug("Invalid UnDef value {} for item {}", state, itemName);
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);
564 logger.debug("Unexpected value type {} for item {}", stateType, itemName);
567 switch (acceptedItemType) {
568 case CoreItemFactory.STRING:
569 if (checkStateType(itemName, stateType, "String")) {
570 channelState = new StringType(state);
573 case CoreItemFactory.NUMBER:
574 if (checkStateType(itemName, stateType, "Decimal")) {
575 channelState = new DecimalType(state);
578 case CoreItemFactory.SWITCH:
579 if (checkStateType(itemName, stateType, "OnOff")) {
580 channelState = "ON".equals(state) ? OnOffType.ON : OnOffType.OFF;
583 case CoreItemFactory.CONTACT:
584 if (checkStateType(itemName, stateType, "OpenClosed")) {
585 channelState = "OPEN".equals(state) ? OpenClosedType.OPEN : OpenClosedType.CLOSED;
588 case CoreItemFactory.DIMMER:
589 if (checkStateType(itemName, stateType, "Percent")) {
590 channelState = new PercentType(state);
593 case CoreItemFactory.COLOR:
594 if (checkStateType(itemName, stateType, "HSB")) {
595 channelState = HSBType.valueOf(state);
598 case CoreItemFactory.DATETIME:
599 if (checkStateType(itemName, stateType, "DateTime")) {
600 channelState = new DateTimeType(ZonedDateTime.parse(state, FORMATTER_DATE));
603 case CoreItemFactory.LOCATION:
604 if (checkStateType(itemName, stateType, "Point")) {
605 channelState = new PointType(state);
608 case CoreItemFactory.IMAGE:
609 if (checkStateType(itemName, stateType, "Raw")) {
610 channelState = RawType.valueOf(state);
613 case CoreItemFactory.PLAYER:
614 if (checkStateType(itemName, stateType, "PlayPause")) {
617 channelState = PlayPauseType.PLAY;
620 channelState = PlayPauseType.PAUSE;
623 logger.debug("Unexpected value {} for item {}", state, itemName);
628 case CoreItemFactory.ROLLERSHUTTER:
629 if (checkStateType(itemName, stateType, "Percent")) {
630 channelState = new PercentType(state);
634 logger.debug("Item type {} is not yet supported", acceptedItemType);
638 } catch (IllegalArgumentException | DateTimeException e) {
639 logger.warn("Failed to parse state \"{}\" for item {}: {}", state, itemName, e.getMessage());
640 channelState = UnDefType.UNDEF;
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",
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) + "...");
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);
667 public Collection<Class<? extends ThingHandlerService>> getServices() {
668 return Collections.singleton(RemoteopenhabDiscoveryService.class);