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.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;
89 import com.google.gson.Gson;
92 * The {@link RemoteopenhabBridgeHandler} is responsible for handling commands and updating states
93 * using the REST API of the remote openHAB server.
95 * @author Laurent Garnier - Initial contribution
98 public class RemoteopenhabBridgeHandler extends BaseBridgeHandler
99 implements RemoteopenhabStreamingDataListener, RemoteopenhabItemsDataListener {
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);
104 private static final int MAX_STATE_SIZE_FOR_LOGGING = 50;
106 private final Logger logger = LoggerFactory.getLogger(RemoteopenhabBridgeHandler.class);
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;
116 private final Object updateThingLock = new Object();
118 private @NonNullByDefault({}) RemoteopenhabServerConfiguration config;
120 private @Nullable ScheduledFuture<?> checkConnectionJob;
121 private RemoteopenhabRestClient restClient;
123 private Map<ChannelUID, State> channelsLastStates = new HashMap<>();
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) {
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,
144 public void initialize() {
145 logger.debug("Initializing remote openHAB handler for bridge {}", getThing().getUID());
147 config = getConfigAs(RemoteopenhabServerConfiguration.class);
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");
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");
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");
170 String urlStr = url.toString();
171 logger.debug("REST URL = {}", urlStr);
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);
180 updateStatus(ThingStatus.UNKNOWN);
182 scheduler.submit(() -> checkConnection(false));
183 if (config.accessibilityInterval > 0) {
184 startCheckConnectionJob(config.accessibilityInterval, config.aliveInterval, config.restartIfNoActivity);
189 public void dispose() {
190 logger.debug("Disposing remote openHAB handler for bridge {}", getThing().getUID());
191 stopStreamingUpdates(false);
192 stopCheckConnectionJob();
193 channelsLastStates.clear();
197 public void handleCommand(ChannelUID channelUID, Command command) {
198 if (getThing().getStatus() != ThingStatus.ONLINE) {
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) + "...",
211 } catch (RemoteopenhabException e) {
212 logger.debug("Handling command for channel {} failed: {}", channelUID.getId(),
213 e.getMessage(bundle, i18nProvider));
217 private boolean createChannels(List<RemoteopenhabItem> items, boolean replace) {
218 synchronized (updateThingLock) {
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
232 itemType = item.groupType;
235 if (item.stateDescription != null && item.stateDescription.readOnly) {
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);
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);
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++;
271 channelTypeUID = channelType.getUID();
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(),
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());
285 ThingBuilder thingBuilder = editThing();
287 thingBuilder.withChannels(channels);
288 updateThing(thingBuilder.build());
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()) {
294 for (Channel channel : channels) {
295 if (getThing().getChannel(channel.getUID()) != null) {
296 thingBuilder.withoutChannel(channel.getUID());
301 logger.debug("{} channels removed for the thing {} (from {} items)", nbRemoved,
302 getThing().getUID(), items.size());
304 for (Channel channel : channels) {
305 thingBuilder.withChannel(channel);
307 updateThing(thingBuilder.build());
309 logger.debug("{} channels added for the thing {} (from {} items including {} groups)",
310 channels.size(), getThing().getUID(), items.size(), nbGroups);
312 logger.debug("{} channels added for the thing {} (from {} items)", channels.size(),
313 getThing().getUID(), items.size());
317 } catch (IllegalArgumentException e) {
318 logger.warn("An error occurred while creating the channels for the server {}: {}", getThing().getUID(),
325 private void removeChannels(List<RemoteopenhabItem> items) {
326 synchronized (updateThingLock) {
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());
337 updateThing(thingBuilder.build());
338 logger.debug("{} channels removed for the thing {} (from {} items)", nbRemoved, getThing().getUID(),
344 private void setDynamicOptions(List<RemoteopenhabItem> items) {
345 for (RemoteopenhabItem item : items) {
346 Channel channel = getThing().getChannel(item.name);
347 if (channel == null) {
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));
357 stateDescriptionProvider.setStateOptions(channel.getUID(), options);
358 logger.trace("{} state options set for the channel {}", options.size(), channel.getUID());
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));
367 commandDescriptionProvider.setCommandOptions(channel.getUID(), options);
368 logger.trace("{} command options set for the channel {}", options.size(), channel.getUID());
373 public void checkConnection(boolean restartSse) {
374 logger.debug("Try the root REST API...");
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");
383 if (createChannels(items, true)) {
384 setDynamicOptions(items);
385 for (RemoteopenhabItem item : items) {
386 updateChannelState(item.name, null, item.state, false);
389 updateStatus(ThingStatus.ONLINE);
391 restartStreamingUpdates();
393 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/offline.error-channels-creation");
394 stopStreamingUpdates();
396 } else if (restartSse) {
397 logger.debug("The SSE connection is restarted because there was no recent event received");
398 restartStreamingUpdates();
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();
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)) {
419 "Time to check server accessibility (maybe disconnected from streaming events, millisSinceLastEvent={})",
420 millisSinceLastEvent);
421 checkConnection(restartIfNoActivity);
424 "Bypass server accessibility check (receiving streaming events, millisSinceLastEvent={})",
425 millisSinceLastEvent);
427 }, accessibilityInterval, accessibilityInterval, TimeUnit.MINUTES);
431 private void stopCheckConnectionJob() {
432 ScheduledFuture<?> localCheckConnectionJob = checkConnectionJob;
433 if (localCheckConnectionJob != null) {
434 localCheckConnectionJob.cancel(true);
435 checkConnectionJob = null;
439 private void restartStreamingUpdates() {
440 synchronized (restClient) {
441 stopStreamingUpdates();
442 startStreamingUpdates();
446 private void startStreamingUpdates() {
447 synchronized (restClient) {
448 restClient.addStreamingDataListener(this);
449 restClient.addItemsDataListener(this);
454 private void stopStreamingUpdates() {
455 stopStreamingUpdates(true);
458 private void stopStreamingUpdates(boolean waitingForCompletion) {
459 synchronized (restClient) {
460 restClient.stop(waitingForCompletion);
461 restClient.removeStreamingDataListener(this);
462 restClient.removeItemsDataListener(this);
466 public RemoteopenhabRestClient gestRestClient() {
471 public void onConnected() {
472 updateStatus(ThingStatus.ONLINE);
476 public void onDisconnected() {
477 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
478 "@text/offline.comm-error-disconnected");
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");
489 public void onItemStateEvent(String itemName, String stateType, String state, boolean onlyIfStateChanged) {
490 updateChannelState(itemName, stateType, state, onlyIfStateChanged);
494 public void onItemAdded(RemoteopenhabItem item) {
495 createChannels(List.of(item), false);
499 public void onItemRemoved(RemoteopenhabItem item) {
500 removeChannels(List.of(item));
504 public void onItemUpdated(RemoteopenhabItem newItem, RemoteopenhabItem oldItem) {
505 if (!newItem.type.equals(oldItem.type)) {
506 createChannels(List.of(newItem), false);
508 logger.trace("Updated remote item {} ignored because item type {} is unchanged", newItem.name,
514 public void onItemOptionsUpdatedd(RemoteopenhabItem item) {
515 setDynamicOptions(List.of(item));
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);
525 String acceptedItemType = channel.getAcceptedItemType();
526 if (acceptedItemType == null) {
527 logger.trace("Channel without accepted item type for item {}", itemName);
530 if (!isLinked(channel.getUID())) {
531 logger.trace("Unlinked channel {}", channel.getUID());
534 State channelState = null;
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)) {
543 channelState = UnDefType.NULL;
546 channelState = UnDefType.UNDEF;
549 logger.debug("Invalid UnDef value {} for item {}", state, itemName);
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 = Collections.singletonList(QuantityType.class);
556 channelState = TypeParser.parseState(stateTypes, state);
557 } else if ("Decimal".equals(stateType)) {
558 channelState = new DecimalType(state);
560 logger.debug("Unexpected value type {} for item {}", stateType, itemName);
563 switch (acceptedItemType) {
564 case CoreItemFactory.STRING:
565 if (checkStateType(itemName, stateType, "String")) {
566 channelState = new StringType(state);
569 case CoreItemFactory.NUMBER:
570 if (checkStateType(itemName, stateType, "Decimal")) {
571 channelState = new DecimalType(state);
574 case CoreItemFactory.SWITCH:
575 if (checkStateType(itemName, stateType, "OnOff")) {
576 channelState = "ON".equals(state) ? OnOffType.ON : OnOffType.OFF;
579 case CoreItemFactory.CONTACT:
580 if (checkStateType(itemName, stateType, "OpenClosed")) {
581 channelState = "OPEN".equals(state) ? OpenClosedType.OPEN : OpenClosedType.CLOSED;
584 case CoreItemFactory.DIMMER:
585 if (checkStateType(itemName, stateType, "Percent")) {
586 channelState = new PercentType(state);
589 case CoreItemFactory.COLOR:
590 if (checkStateType(itemName, stateType, "HSB")) {
591 channelState = HSBType.valueOf(state);
594 case CoreItemFactory.DATETIME:
595 if (checkStateType(itemName, stateType, "DateTime")) {
596 channelState = new DateTimeType(ZonedDateTime.parse(state, FORMATTER_DATE));
599 case CoreItemFactory.LOCATION:
600 if (checkStateType(itemName, stateType, "Point")) {
601 channelState = new PointType(state);
604 case CoreItemFactory.IMAGE:
605 if (checkStateType(itemName, stateType, "Raw")) {
606 channelState = RawType.valueOf(state);
609 case CoreItemFactory.PLAYER:
610 if (checkStateType(itemName, stateType, "PlayPause")) {
613 channelState = PlayPauseType.PLAY;
616 channelState = PlayPauseType.PAUSE;
619 logger.debug("Unexpected value {} for item {}", state, itemName);
624 case CoreItemFactory.ROLLERSHUTTER:
625 if (checkStateType(itemName, stateType, "Percent")) {
626 channelState = new PercentType(state);
630 logger.debug("Item type {} is not yet supported", acceptedItemType);
634 } catch (IllegalArgumentException | DateTimeException e) {
635 logger.warn("Failed to parse state \"{}\" for item {}: {}", state, itemName, e.getMessage());
636 channelState = UnDefType.UNDEF;
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",
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) + "...");
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);
663 public Collection<Class<? extends ThingHandlerService>> getServices() {
664 return Collections.singleton(RemoteopenhabDiscoveryService.class);