diff --git a/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/ConfigUtil.java b/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/ConfigUtil.java index 1bca555b4..f662f6d31 100644 --- a/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/ConfigUtil.java +++ b/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/ConfigUtil.java @@ -206,18 +206,14 @@ public class ConfigUtil { * @param configuration the configuration to be normalized * @param configDescriptions the configuration descriptions that should be applied (must not be empty). * @return the normalized configuration or null if given configuration was null - * @throws IllegalArgumentExcetpion if given config description is null + * @throws IllegalArgumentException if given config description is null */ - public static @Nullable Map normalizeTypes(@Nullable Map configuration, + public static Map normalizeTypes(Map configuration, List configDescriptions) { if (configDescriptions.isEmpty()) { throw new IllegalArgumentException("Config description must not be empty."); } - if (configuration == null) { - return null; - } - Map convertedConfiguration = new HashMap<>(); Map configParams = new HashMap<>(); diff --git a/bundles/org.openhab.core.config.core/src/main/resources/OH-INF/i18n/validation.properties b/bundles/org.openhab.core.config.core/src/main/resources/OH-INF/i18n/validation.properties index 2cd7c54f2..aff65cbe0 100644 --- a/bundles/org.openhab.core.config.core/src/main/resources/OH-INF/i18n/validation.properties +++ b/bundles/org.openhab.core.config.core/src/main/resources/OH-INF/i18n/validation.properties @@ -11,3 +11,8 @@ min_value_numeric_violated=The value must not be less than {0}. pattern_violated=The value {0} does not match the pattern {1}. options_violated=The value {0} does not match allowed parameter options. Allowed options are: {1} multiple_limit_violated=Only {0} elements are allowed but {1} are provided. + +bridge_not_configured=Configuring a bridge is mandatory. +type_description_missing=Type description for '{0}' not found also we checked the presence before. +config_description_missing=Config description for '{0}' not found also we checked the presence before. + diff --git a/bundles/org.openhab.core.thing/pom.xml b/bundles/org.openhab.core.thing/pom.xml index 7b08a3e55..5aafa6dab 100644 --- a/bundles/org.openhab.core.thing/pom.xml +++ b/bundles/org.openhab.core.thing/pom.xml @@ -25,6 +25,11 @@ org.openhab.core.io.console ${project.version} + + jakarta.xml.bind + jakarta.xml.bind-api + 2.3.3 + org.openhab.core.bundles org.openhab.core.test @@ -33,4 +38,48 @@ + + + + org.jvnet.jaxb2.maven2 + maven-jaxb2-plugin + 0.15.2 + + src/main/resources/xsd + true + en + false + true + + -Xxew + -Xxew:instantiate early + + + + com.github.jaxb-xew-plugin + jaxb-xew-plugin + 1.10 + + + + + + + org.glassfish.jaxb + jaxb-runtime + 2.3.6 + + + + + generate-jaxb-sources + + generate + + + + + + + diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/ThingStatusDetail.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/ThingStatusDetail.java index b50ec2adb..a6b0347b4 100644 --- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/ThingStatusDetail.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/ThingStatusDetail.java @@ -23,6 +23,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; @NonNullByDefault public enum ThingStatusDetail { NONE, + NOT_YET_READY, HANDLER_MISSING_ERROR, HANDLER_REGISTERING_ERROR, HANDLER_INITIALIZING_ERROR, diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/builder/ThingBuilder.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/builder/ThingBuilder.java index fdc3e0981..72439c8e2 100644 --- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/builder/ThingBuilder.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/binding/builder/ThingBuilder.java @@ -14,9 +14,11 @@ package org.openhab.core.thing.binding.builder; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Objects; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -74,6 +76,19 @@ public class ThingBuilder { return new ThingBuilder(thingTypeUID, thingUID); } + /** + * Create a new thing {@link ThingBuilder} for a copy of the given thing + * + * @param thing the {@link Thing} to create this builder from + * @return the created {@link ThingBuilder} + * + */ + public static ThingBuilder create(Thing thing) { + return ThingBuilder.create(thing.getThingTypeUID(), thing.getUID()).withBridge(thing.getBridgeUID()) + .withChannels(thing.getChannels()).withConfiguration(thing.getConfiguration()) + .withLabel(thing.getLabel()).withLocation(thing.getLocation()).withProperties(thing.getProperties()); + } + /** * Build the thing * @@ -200,6 +215,20 @@ public class ThingBuilder { return this; } + /** + * Set / replace a single property for this thing + * + * @param key the key / name of the property + * @param value the value of the property + * @return the {@link ThingBuilder} itself + */ + public ThingBuilder withProperty(String key, String value) { + Map oldProperties = Objects.requireNonNullElse(this.properties, Map.of()); + Map newProperties = new HashMap<>(oldProperties); + newProperties.put(key, value); + return withProperties(newProperties); + } + /** * Set/replace the properties for this thing * diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/ThingHandlerCallbackImpl.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/ThingHandlerCallbackImpl.java new file mode 100644 index 000000000..1ffab2b1a --- /dev/null +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/ThingHandlerCallbackImpl.java @@ -0,0 +1,269 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.thing.internal; + +import java.net.URI; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.config.core.ConfigDescription; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.ChannelGroupUID; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusInfo; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.ThingHandlerCallback; +import org.openhab.core.thing.binding.builder.ChannelBuilder; +import org.openhab.core.thing.type.ChannelDefinition; +import org.openhab.core.thing.type.ChannelGroupType; +import org.openhab.core.thing.type.ChannelGroupTypeUID; +import org.openhab.core.thing.type.ChannelType; +import org.openhab.core.thing.type.ChannelTypeUID; +import org.openhab.core.thing.type.ThingType; +import org.openhab.core.thing.util.ThingHandlerHelper; +import org.openhab.core.types.Command; +import org.openhab.core.types.State; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link ThingHandlerCallbackImpl} implements the {@link ThingHandlerCallback} interface + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +class ThingHandlerCallbackImpl implements ThingHandlerCallback { + private final Logger logger = LoggerFactory.getLogger(ThingHandlerCallbackImpl.class); + + private final ThingManagerImpl thingManager; + + public ThingHandlerCallbackImpl(ThingManagerImpl thingManager) { + this.thingManager = thingManager; + } + + @Override + public void stateUpdated(ChannelUID channelUID, State state) { + thingManager.communicationManager.stateUpdated(channelUID, state); + } + + @Override + public void postCommand(ChannelUID channelUID, Command command) { + thingManager.communicationManager.postCommand(channelUID, command); + } + + @Override + public void channelTriggered(Thing thing, ChannelUID channelUID, String event) { + thingManager.communicationManager.channelTriggered(thing, channelUID, event); + } + + @Override + public void statusUpdated(Thing thing, ThingStatusInfo statusInfo) { + // note: all operations based on a status update should be executed asynchronously! + ThingStatusInfo oldStatusInfo = thing.getStatusInfo(); + ensureValidStatus(oldStatusInfo.getStatus(), statusInfo.getStatus()); + + if (ThingStatus.REMOVING.equals(oldStatusInfo.getStatus()) + && !ThingStatus.REMOVED.equals(statusInfo.getStatus())) { + // if we go to ONLINE and are still in REMOVING, notify handler about required removal + if (ThingStatus.ONLINE.equals(statusInfo.getStatus())) { + logger.debug("Handler is initialized now and we try to remove it, because it is in REMOVING state."); + thingManager.notifyThingHandlerAboutRemoval(thing); + } + // only allow REMOVING -> REMOVED transition, all others are ignored because they are illegal + logger.debug( + "Ignoring illegal status transition for thing {} from REMOVING to {}, only REMOVED would have been allowed.", + thing.getUID(), statusInfo.getStatus()); + return; + } + + // update thing status and send event about new status + thingManager.setThingStatus(thing, statusInfo); + + // if thing is a bridge + if (thing instanceof Bridge bridge) { + handleBridgeStatusUpdate(bridge, statusInfo, oldStatusInfo); + } + // if thing has a bridge + if (thing.getBridgeUID() != null) { + handleBridgeChildStatusUpdate(thing, oldStatusInfo); + } + // notify thing registry about thing removal + if (ThingStatus.REMOVED.equals(thing.getStatus())) { + thingManager.notifyRegistryAboutForceRemove(thing); + } + } + + private void ensureValidStatus(ThingStatus oldStatus, ThingStatus newStatus) { + if (!(ThingStatus.UNKNOWN.equals(newStatus) || ThingStatus.ONLINE.equals(newStatus) + || ThingStatus.OFFLINE.equals(newStatus) || ThingStatus.REMOVED.equals(newStatus))) { + throw new IllegalArgumentException( + MessageFormat.format("Illegal status {0}. Bindings only may set {1}, {2}, {3} or {4}.", newStatus, + ThingStatus.UNKNOWN, ThingStatus.ONLINE, ThingStatus.OFFLINE, ThingStatus.REMOVED)); + } + if (ThingStatus.REMOVED.equals(newStatus) && !ThingStatus.REMOVING.equals(oldStatus)) { + throw new IllegalArgumentException( + MessageFormat.format("Illegal status {0}. The thing was in state {1} and not in {2}", newStatus, + oldStatus, ThingStatus.REMOVING)); + } + } + + private void handleBridgeStatusUpdate(Bridge bridge, ThingStatusInfo statusInfo, ThingStatusInfo oldStatusInfo) { + if (ThingHandlerHelper.isHandlerInitialized(bridge) + && (ThingStatus.INITIALIZING.equals(oldStatusInfo.getStatus()))) { + // bridge has just been initialized: initialize child things as well + thingManager.registerChildHandlers(bridge); + } else if (!statusInfo.equals(oldStatusInfo)) { + // bridge status has been changed: notify child things about status change + thingManager.notifyThingsAboutBridgeStatusChange(bridge, statusInfo); + } + } + + private void handleBridgeChildStatusUpdate(Thing thing, ThingStatusInfo oldStatusInfo) { + if (ThingHandlerHelper.isHandlerInitialized(thing) + && ThingStatus.INITIALIZING.equals(oldStatusInfo.getStatus())) { + // child thing has just been initialized: notify bridge about it + thingManager.notifyBridgeAboutChildHandlerInitialization(thing); + } + } + + @Override + public void thingUpdated(final Thing thing) { + thingManager.thingUpdated(thing); + } + + @Override + public void validateConfigurationParameters(Thing thing, Map configurationParameters) { + ThingType thingType = thingManager.thingTypeRegistry.getThingType(thing.getThingTypeUID()); + if (thingType != null) { + URI configDescriptionURI = thingType.getConfigDescriptionURI(); + if (configDescriptionURI != null) { + thingManager.configDescriptionValidator.validate(configurationParameters, configDescriptionURI); + } + } + } + + @Override + public void validateConfigurationParameters(Channel channel, Map configurationParameters) { + ChannelType channelType = thingManager.channelTypeRegistry.getChannelType(channel.getChannelTypeUID()); + if (channelType != null) { + URI configDescriptionURI = channelType.getConfigDescriptionURI(); + if (configDescriptionURI != null) { + thingManager.configDescriptionValidator.validate(configurationParameters, configDescriptionURI); + } + } + } + + @Override + public @Nullable ConfigDescription getConfigDescription(ChannelTypeUID channelTypeUID) { + ChannelType channelType = thingManager.channelTypeRegistry.getChannelType(channelTypeUID); + if (channelType != null) { + URI configDescriptionUri = channelType.getConfigDescriptionURI(); + if (configDescriptionUri != null) { + return thingManager.configDescriptionRegistry.getConfigDescription(configDescriptionUri); + } + } + return null; + } + + @Override + public @Nullable ConfigDescription getConfigDescription(ThingTypeUID thingTypeUID) { + ThingType thingType = thingManager.thingTypeRegistry.getThingType(thingTypeUID); + if (thingType != null) { + URI configDescriptionUri = thingType.getConfigDescriptionURI(); + if (configDescriptionUri != null) { + return thingManager.configDescriptionRegistry.getConfigDescription(configDescriptionUri); + } + } + return null; + } + + @Override + public void configurationUpdated(Thing thing) { + if (!ThingHandlerHelper.isHandlerInitialized(thing)) { + thingManager.initializeHandler(thing); + } + } + + @Override + public void migrateThingType(final Thing thing, final ThingTypeUID thingTypeUID, + final Configuration configuration) { + thingManager.migrateThingType(thing, thingTypeUID, configuration); + } + + @Override + public ChannelBuilder createChannelBuilder(ChannelUID channelUID, ChannelTypeUID channelTypeUID) { + ChannelType channelType = thingManager.channelTypeRegistry.getChannelType(channelTypeUID); + if (channelType == null) { + throw new IllegalArgumentException(String.format("Channel type '%s' is not known", channelTypeUID)); + } + return ThingFactoryHelper.createChannelBuilder(channelUID, channelType, thingManager.configDescriptionRegistry); + } + + @Override + public ChannelBuilder editChannel(Thing thing, ChannelUID channelUID) { + Channel channel = thing.getChannel(channelUID.getId()); + if (channel == null) { + throw new IllegalArgumentException( + String.format("Channel '%s' does not exist for thing '%s'", channelUID, thing.getUID())); + } + return ChannelBuilder.create(channel); + } + + @Override + public List createChannelBuilders(ChannelGroupUID channelGroupUID, + ChannelGroupTypeUID channelGroupTypeUID) { + ChannelGroupType channelGroupType = thingManager.channelGroupTypeRegistry + .getChannelGroupType(channelGroupTypeUID); + if (channelGroupType == null) { + throw new IllegalArgumentException( + String.format("Channel group type '%s' is not known", channelGroupTypeUID)); + } + List channelBuilders = new ArrayList<>(); + for (ChannelDefinition channelDefinition : channelGroupType.getChannelDefinitions()) { + ChannelType channelType = thingManager.channelTypeRegistry + .getChannelType(channelDefinition.getChannelTypeUID()); + if (channelType != null) { + ChannelUID channelUID = new ChannelUID(channelGroupUID, channelDefinition.getId()); + ChannelBuilder channelBuilder = ThingFactoryHelper.createChannelBuilder(channelUID, channelDefinition, + thingManager.configDescriptionRegistry); + if (channelBuilder != null) { + channelBuilders.add(channelBuilder); + } + } + } + return channelBuilders; + } + + @Override + public boolean isChannelLinked(ChannelUID channelUID) { + return thingManager.itemChannelLinkRegistry.isLinked(channelUID); + } + + @Override + public @Nullable Bridge getBridge(ThingUID bridgeUID) { + Thing bridgeThing = thingManager.thingRegistry.get(bridgeUID); + if (bridgeThing instanceof Bridge bridge) { + return bridge; + } + return null; + } +} diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/ThingManagerImpl.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/ThingManagerImpl.java index c00c274c9..22f39a59b 100644 --- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/ThingManagerImpl.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/ThingManagerImpl.java @@ -14,10 +14,9 @@ package org.openhab.core.thing.internal; import java.net.URI; import java.text.MessageFormat; -import java.util.ArrayList; -import java.util.Collection; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -25,19 +24,16 @@ import java.util.Objects; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; -import java.util.function.Function; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.common.SafeCaller; import org.openhab.core.common.ThreadPoolManager; -import org.openhab.core.common.registry.Identifiable; import org.openhab.core.common.registry.ManagedProvider; import org.openhab.core.common.registry.Provider; import org.openhab.core.config.core.ConfigDescription; @@ -46,10 +42,11 @@ import org.openhab.core.config.core.ConfigUtil; import org.openhab.core.config.core.Configuration; import org.openhab.core.config.core.validation.ConfigDescriptionValidator; import org.openhab.core.config.core.validation.ConfigValidationException; +import org.openhab.core.config.core.validation.ConfigValidationMessage; import org.openhab.core.events.EventPublisher; +import org.openhab.core.i18n.TranslationProvider; import org.openhab.core.service.ReadyMarker; import org.openhab.core.service.ReadyMarkerFilter; -import org.openhab.core.service.ReadyMarkerUtils; import org.openhab.core.service.ReadyService; import org.openhab.core.service.ReadyService.ReadyTracker; import org.openhab.core.service.StartLevelService; @@ -57,8 +54,6 @@ import org.openhab.core.storage.Storage; import org.openhab.core.storage.StorageService; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.Channel; -import org.openhab.core.thing.ChannelGroupUID; -import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingManager; import org.openhab.core.thing.ThingRegistry; @@ -73,25 +68,24 @@ import org.openhab.core.thing.binding.BridgeHandler; import org.openhab.core.thing.binding.ThingHandler; import org.openhab.core.thing.binding.ThingHandlerCallback; import org.openhab.core.thing.binding.ThingHandlerFactory; -import org.openhab.core.thing.binding.builder.ChannelBuilder; +import org.openhab.core.thing.binding.builder.ThingBuilder; import org.openhab.core.thing.binding.builder.ThingStatusInfoBuilder; import org.openhab.core.thing.events.ThingEventFactory; import org.openhab.core.thing.i18n.ThingStatusInfoI18nLocalizationService; +import org.openhab.core.thing.internal.update.ThingUpdateInstruction; +import org.openhab.core.thing.internal.update.ThingUpdateInstructionReader; +import org.openhab.core.thing.internal.update.ThingUpdateInstructionReader.UpdateInstructionKey; import org.openhab.core.thing.link.ItemChannelLinkRegistry; -import org.openhab.core.thing.type.ChannelDefinition; -import org.openhab.core.thing.type.ChannelGroupType; +import org.openhab.core.thing.type.AbstractDescriptionType; import org.openhab.core.thing.type.ChannelGroupTypeRegistry; -import org.openhab.core.thing.type.ChannelGroupTypeUID; import org.openhab.core.thing.type.ChannelType; import org.openhab.core.thing.type.ChannelTypeRegistry; import org.openhab.core.thing.type.ChannelTypeUID; import org.openhab.core.thing.type.ThingType; import org.openhab.core.thing.type.ThingTypeRegistry; import org.openhab.core.thing.util.ThingHandlerHelper; -import org.openhab.core.types.Command; -import org.openhab.core.types.State; import org.openhab.core.util.BundleResolver; -import org.osgi.service.component.ComponentContext; +import org.osgi.framework.BundleContext; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Deactivate; @@ -103,12 +97,12 @@ import org.slf4j.LoggerFactory; /** * {@link ThingManagerImpl} tracks all things in the {@link ThingRegistry} and mediates the communication between the - * {@link Thing} and the {@link ThingHandler} from the binding. Therefore it tracks {@link ThingHandlerFactory}s and + * {@link Thing} and the {@link ThingHandler} from the binding. It tracks {@link ThingHandlerFactory}s and * calls {@link ThingHandlerFactory#registerHandler(Thing)} for each thing, that was added to the {@link ThingRegistry}. - * In addition the {@link ThingManagerImpl} acts as an {@link org.openhab.core.internal.events.EventHandler} + * In addition, the {@link ThingManagerImpl} acts as an {@link org.openhab.core.internal.events.EventHandler} * and subscribes to update and command events. - * Finally the {@link ThingManagerImpl} implements the {@link ThingTypeMigrationService} to offer a way to change the - * thing type of a {@link Thing}. + * Finally, the {@link ThingManagerImpl} implements the {@link ThingTypeMigrationService} to offer a way to change the + * thing-type of a {@link Thing}. * * @author Dennis Nobel - Initial contribution * @author Michael Grammling - Added dynamic configuration update @@ -123,14 +117,19 @@ import org.slf4j.LoggerFactory; * @author Christoph Weitkamp - Added preconfigured ChannelGroupBuilder * @author Yordan Zhelev - Added thing disabling mechanism * @author Björn Lange - Ignore illegal thing status transitions instead of throwing IllegalArgumentException + * @author Jan N. Klug - Add thing update mechanism */ @NonNullByDefault @Component(immediate = true, service = { ThingTypeMigrationService.class, ThingManager.class }) -public class ThingManagerImpl - implements ThingManager, ThingTracker, ThingTypeMigrationService, ReadyService.ReadyTracker { +public class ThingManagerImpl implements ReadyTracker, ThingManager, ThingTracker, ThingTypeMigrationService { + public static final String PROPERTY_THING_TYPE_VERSION = "thingTypeVersion"; - static final String XML_THING_TYPE = "openhab.xmlThingTypes"; + // interval to check if thing prerequisites are met (in s) + private static final int CHECK_INTERVAL = 2; + // time after we try to initialize a thing even if the thing-type is not registered (in s) + private static final int MAX_CHECK_PREREQUISITE_TIME = 120; + private static final ReadyMarker READY_MARKER_THINGS_LOADED = new ReadyMarker("things", "handler"); private static final String THING_STATUS_STORAGE_NAME = "thing_status_storage"; private static final String FORCE_REMOVE_THREAD_POOL_NAME = "forceRemove"; private static final String THING_MANAGER_THREAD_POOL_NAME = "thingManager"; @@ -140,244 +139,42 @@ public class ThingManagerImpl private final ScheduledExecutorService scheduler = ThreadPoolManager .getScheduledPool(THING_MANAGER_THREAD_POOL_NAME); - private final ReadyMarker marker = new ReadyMarker("things", "handler"); - private final List thingHandlerFactories = new CopyOnWriteArrayList<>(); private final Map thingHandlers = new ConcurrentHashMap<>(); - private final Map> thingHandlersByFactory = new HashMap<>(); + private final Map> thingHandlersByFactory = new ConcurrentHashMap<>(); + private final Map> updateInstructions = new ConcurrentHashMap<>(); + private final Map missingPrerequisites = new ConcurrentHashMap<>(); private final Map things = new ConcurrentHashMap<>(); private final Map thingLocks = new HashMap<>(); private final Set thingUpdatedLock = new HashSet<>(); - private final Set loadedXmlThingTypes = new CopyOnWriteArraySet<>(); - private BundleResolver bundleResolver; - - private final ChannelGroupTypeRegistry channelGroupTypeRegistry; - private final ChannelTypeRegistry channelTypeRegistry; - private final CommunicationManager communicationManager; - private final ConfigDescriptionRegistry configDescriptionRegistry; - private final ConfigDescriptionValidator configDescriptionValidator; + protected final ChannelGroupTypeRegistry channelGroupTypeRegistry; + protected final ChannelTypeRegistry channelTypeRegistry; + protected final CommunicationManager communicationManager; + protected final ConfigDescriptionRegistry configDescriptionRegistry; + protected final ConfigDescriptionValidator configDescriptionValidator; private final EventPublisher eventPublisher; - private final ThingTypeRegistry thingTypeRegistry; - private final ItemChannelLinkRegistry itemChannelLinkRegistry; + protected final ThingTypeRegistry thingTypeRegistry; + protected final ItemChannelLinkRegistry itemChannelLinkRegistry; private final ReadyService readyService; private final SafeCaller safeCaller; - private final Storage storage; - private final ThingRegistryImpl thingRegistry; + private final Storage disabledStorage; + protected final ThingRegistryImpl thingRegistry; + private final TranslationProvider translationProvider; + private final BundleContext bundleContext; + + private final ThingUpdateInstructionReader thingUpdateInstructionReader; + private final ThingStatusInfoI18nLocalizationService thingStatusInfoI18nLocalizationService; private @Nullable ScheduledFuture startLevelSetterJob = null; + private @Nullable ScheduledFuture prerequisiteCheckerJob = null; - private final ThingHandlerCallback thingHandlerCallback = new ThingHandlerCallback() { - - @Override - public void stateUpdated(ChannelUID channelUID, State state) { - communicationManager.stateUpdated(channelUID, state); - } - - @Override - public void postCommand(ChannelUID channelUID, Command command) { - communicationManager.postCommand(channelUID, command); - } - - @Override - public void channelTriggered(Thing thing, ChannelUID channelUID, String event) { - communicationManager.channelTriggered(thing, channelUID, event); - } - - @Override - public void statusUpdated(Thing thing, ThingStatusInfo statusInfo) { - // note: all provoked operations based on a status update should be executed asynchronously! - ThingStatusInfo oldStatusInfo = thing.getStatusInfo(); - ensureValidStatus(oldStatusInfo.getStatus(), statusInfo.getStatus()); - - if (ThingStatus.REMOVING.equals(oldStatusInfo.getStatus()) - && !ThingStatus.REMOVED.equals(statusInfo.getStatus())) { - // if we go to ONLINE and are still in REMOVING, notify handler about required removal - if (ThingStatus.ONLINE.equals(statusInfo.getStatus())) { - logger.debug( - "Handler is initialized now and we try to remove it, because it is in REMOVING state."); - notifyThingHandlerAboutRemoval(thing); - } - // only allow REMOVING -> REMOVED transition, all others are ignored because they are illegal - logger.debug( - "Ignoring illegal status transition for thing {} from REMOVING to {}, only REMOVED would have been allowed.", - thing.getUID(), statusInfo.getStatus()); - return; - } - - // update thing status and send event about new status - setThingStatus(thing, statusInfo); - - // if thing is a bridge - if (isBridge(thing)) { - handleBridgeStatusUpdate((Bridge) thing, statusInfo, oldStatusInfo); - } - // if thing has a bridge - if (hasBridge(thing)) { - handleBridgeChildStatusUpdate(thing, oldStatusInfo); - } - // notify thing registry about thing removal - if (ThingStatus.REMOVED.equals(thing.getStatus())) { - notifyRegistryAboutForceRemove(thing); - } - } - - private void ensureValidStatus(ThingStatus oldStatus, ThingStatus newStatus) { - if (!(ThingStatus.UNKNOWN.equals(newStatus) || ThingStatus.ONLINE.equals(newStatus) - || ThingStatus.OFFLINE.equals(newStatus) || ThingStatus.REMOVED.equals(newStatus))) { - throw new IllegalArgumentException(MessageFormat.format( - "Illegal status {0}. Bindings only may set {1}, {2}, {3} or {4}.", newStatus, - ThingStatus.UNKNOWN, ThingStatus.ONLINE, ThingStatus.OFFLINE, ThingStatus.REMOVED)); - } - if (ThingStatus.REMOVED.equals(newStatus) && !ThingStatus.REMOVING.equals(oldStatus)) { - throw new IllegalArgumentException( - MessageFormat.format("Illegal status {0}. The thing was in state {1} and not in {2}", newStatus, - oldStatus, ThingStatus.REMOVING)); - } - } - - private void handleBridgeStatusUpdate(Bridge bridge, ThingStatusInfo statusInfo, - ThingStatusInfo oldStatusInfo) { - if (ThingHandlerHelper.isHandlerInitialized(bridge) - && (ThingStatus.INITIALIZING.equals(oldStatusInfo.getStatus()))) { - // bridge has just been initialized: initialize child things as well - registerChildHandlers(bridge); - } else if (!statusInfo.equals(oldStatusInfo)) { - // bridge status has been changed: notify child things about status change - notifyThingsAboutBridgeStatusChange(bridge, statusInfo); - } - } - - private void handleBridgeChildStatusUpdate(Thing thing, ThingStatusInfo oldStatusInfo) { - if (ThingHandlerHelper.isHandlerInitialized(thing) - && ThingStatus.INITIALIZING.equals(oldStatusInfo.getStatus())) { - // child thing has just been initialized: notify bridge about it - notifyBridgeAboutChildHandlerInitialization(thing); - } - } - - @Override - public void thingUpdated(final Thing thing) { - ThingManagerImpl.this.thingUpdated(thing); - } - - @Override - public void validateConfigurationParameters(Thing thing, Map configurationParameters) { - ThingType thingType = thingTypeRegistry.getThingType(thing.getThingTypeUID()); - if (thingType != null) { - URI configDescriptionURI = thingType.getConfigDescriptionURI(); - if (configDescriptionURI != null) { - configDescriptionValidator.validate(configurationParameters, configDescriptionURI); - } - } - } - - @Override - public void validateConfigurationParameters(Channel channel, Map configurationParameters) { - ChannelType channelType = channelTypeRegistry.getChannelType(channel.getChannelTypeUID()); - if (channelType != null) { - URI configDescriptionURI = channelType.getConfigDescriptionURI(); - if (configDescriptionURI != null) { - configDescriptionValidator.validate(configurationParameters, configDescriptionURI); - } - } - } - - @Override - public @Nullable ConfigDescription getConfigDescription(ChannelTypeUID channelTypeUID) { - ChannelType channelType = channelTypeRegistry.getChannelType(channelTypeUID); - if (channelType != null) { - URI configDescriptionUri = channelType.getConfigDescriptionURI(); - if (configDescriptionUri != null) { - return configDescriptionRegistry.getConfigDescription(configDescriptionUri); - } - } - return null; - } - - @Override - public @Nullable ConfigDescription getConfigDescription(ThingTypeUID thingTypeUID) { - ThingType thingType = thingTypeRegistry.getThingType(thingTypeUID); - if (thingType != null) { - URI configDescriptionUri = thingType.getConfigDescriptionURI(); - if (configDescriptionUri != null) { - return configDescriptionRegistry.getConfigDescription(configDescriptionUri); - } - } - return null; - } - - @Override - public void configurationUpdated(Thing thing) { - if (!ThingHandlerHelper.isHandlerInitialized(thing)) { - initializeHandler(thing); - } - } - - @Override - public void migrateThingType(final Thing thing, final ThingTypeUID thingTypeUID, - final Configuration configuration) { - ThingManagerImpl.this.migrateThingType(thing, thingTypeUID, configuration); - } - - @Override - public ChannelBuilder createChannelBuilder(ChannelUID channelUID, ChannelTypeUID channelTypeUID) { - ChannelType channelType = channelTypeRegistry.getChannelType(channelTypeUID); - if (channelType == null) { - throw new IllegalArgumentException(String.format("Channel type '%s' is not known", channelTypeUID)); - } - return ThingFactoryHelper.createChannelBuilder(channelUID, channelType, configDescriptionRegistry); - } - - @Override - public ChannelBuilder editChannel(Thing thing, ChannelUID channelUID) { - Channel channel = thing.getChannel(channelUID.getId()); - if (channel == null) { - throw new IllegalArgumentException( - String.format("Channel '%s' does not exist for thing '%s'", channelUID, thing.getUID())); - } - return ChannelBuilder.create(channel); - } - - @Override - public List createChannelBuilders(ChannelGroupUID channelGroupUID, - ChannelGroupTypeUID channelGroupTypeUID) { - ChannelGroupType channelGroupType = channelGroupTypeRegistry.getChannelGroupType(channelGroupTypeUID); - if (channelGroupType == null) { - throw new IllegalArgumentException( - String.format("Channel group type '%s' is not known", channelGroupTypeUID)); - } - List channelBuilders = new ArrayList<>(); - for (ChannelDefinition channelDefinition : channelGroupType.getChannelDefinitions()) { - ChannelType channelType = channelTypeRegistry.getChannelType(channelDefinition.getChannelTypeUID()); - if (channelType != null) { - ChannelUID channelUID = new ChannelUID(channelGroupUID, channelDefinition.getId()); - ChannelBuilder channelBuilder = ThingFactoryHelper.createChannelBuilder(channelUID, - channelDefinition, configDescriptionRegistry); - if (channelBuilder != null) { - channelBuilders.add(channelBuilder); - } - } - } - return channelBuilders; - } - - @Override - public boolean isChannelLinked(ChannelUID channelUID) { - return itemChannelLinkRegistry.isLinked(channelUID); - } - - @Override - public @Nullable Bridge getBridge(ThingUID bridgeUID) { - return (Bridge) thingRegistry.get(bridgeUID); - } - }; + private final ThingHandlerCallback thingHandlerCallback = new ThingHandlerCallbackImpl(this); @Activate public ThingManagerImpl( // - final @Reference BundleResolver bundleResolver, final @Reference ChannelGroupTypeRegistry channelGroupTypeRegistry, final @Reference ChannelTypeRegistry channelTypeRegistry, final @Reference CommunicationManager communicationManager, @@ -390,8 +187,8 @@ public class ThingManagerImpl final @Reference StorageService storageService, // final @Reference ThingRegistry thingRegistry, final @Reference ThingStatusInfoI18nLocalizationService thingStatusInfoI18nLocalizationService, - final @Reference ThingTypeRegistry thingTypeRegistry) { - this.bundleResolver = bundleResolver; + final @Reference ThingTypeRegistry thingTypeRegistry, final @Reference BundleResolver bundleResolver, + final @Reference TranslationProvider translationProvider, final BundleContext bundleContext) { this.channelGroupTypeRegistry = channelGroupTypeRegistry; this.channelTypeRegistry = channelTypeRegistry; this.communicationManager = communicationManager; @@ -402,50 +199,38 @@ public class ThingManagerImpl this.readyService = readyService; this.safeCaller = safeCaller; this.thingRegistry = (ThingRegistryImpl) thingRegistry; + this.thingUpdateInstructionReader = new ThingUpdateInstructionReader(bundleResolver); this.thingStatusInfoI18nLocalizationService = thingStatusInfoI18nLocalizationService; this.thingTypeRegistry = thingTypeRegistry; + this.translationProvider = translationProvider; + this.bundleContext = bundleContext; + this.disabledStorage = storageService.getStorage(THING_STATUS_STORAGE_NAME, this.getClass().getClassLoader()); - storage = storageService.getStorage(THING_STATUS_STORAGE_NAME, this.getClass().getClassLoader()); - - readyService.registerTracker(this, new ReadyMarkerFilter().withType(XML_THING_TYPE)); this.thingRegistry.addThingTracker(this); - initializeStartLevelSetter(); - } - - private void initializeStartLevelSetter() { - readyService.registerTracker(new ReadyTracker() { - @Override - public void onReadyMarkerRemoved(ReadyMarker readyMarker) { - } - - @Override - public void onReadyMarkerAdded(ReadyMarker readyMarker) { - startLevelSetterJob = scheduler.scheduleWithFixedDelay(() -> { - if (things.values().stream() - .anyMatch(t -> !ThingHandlerHelper.isHandlerInitialized(t) && t.isEnabled())) { - return; - } - readyService.markReady(marker); - if (startLevelSetterJob != null) { - startLevelSetterJob.cancel(false); - } - readyService.unregisterTracker(this); - }, 2, 2, TimeUnit.SECONDS); - } - }, new ReadyMarkerFilter().withType(StartLevelService.STARTLEVEL_MARKER_TYPE) + readyService.registerTracker(this, new ReadyMarkerFilter().withType(StartLevelService.STARTLEVEL_MARKER_TYPE) .withIdentifier(Integer.toString(StartLevelService.STARTLEVEL_MODEL))); } @Deactivate - protected synchronized void deactivate(ComponentContext componentContext) { + protected synchronized void deactivate() { thingRegistry.removeThingTracker(this); for (ThingHandlerFactory factory : thingHandlerFactories) { removeThingHandlerFactory(factory); } readyService.unregisterTracker(this); + ScheduledFuture startLevelSetterJob = this.startLevelSetterJob; + if (startLevelSetterJob != null) { + startLevelSetterJob.cancel(true); + this.startLevelSetterJob = null; + } + ScheduledFuture prerequisiteCheckerJob = this.prerequisiteCheckerJob; + if (prerequisiteCheckerJob != null) { + prerequisiteCheckerJob.cancel(true); + this.prerequisiteCheckerJob = null; + } } - private void thingUpdated(final Thing thing) { + protected void thingUpdated(final Thing thing) { thingUpdatedLock.add(thing.getUID()); final Thing oldThing = thingRegistry.get(thing.getUID()); if (oldThing == null) { @@ -458,8 +243,7 @@ public class ThingManagerImpl "Provider for thing {0} cannot be determined because it is not known to the registry", thing.getUID().getAsString())); } - if (provider instanceof ManagedProvider) { - final ManagedProvider managedProvider = (ManagedProvider) provider; + if (provider instanceof ManagedProvider managedProvider) { managedProvider.update(thing); } else { logger.debug("Only updating thing {} in the registry because provider {} is not managed.", @@ -483,9 +267,8 @@ public class ThingManagerImpl public void run() { ThingUID thingUID = thing.getUID(); Lock lock = getLockForThing(thingUID); + lock.lock(); try { - lock.lock(); - // Remove the ThingHandler, if any final ThingHandlerFactory oldThingHandlerFactory = findThingHandlerFactory(thing.getThingTypeUID()); if (oldThingHandlerFactory != null) { @@ -561,7 +344,13 @@ public class ThingManagerImpl if (things.get(thing.getUID()) != null) { logger.error("A thing with UID '{}' is already tracked by ThingManager. This is a bug.", thing.getUID()); } - this.things.put(thing.getUID(), thing); + + things.put(thing.getUID(), thing); + ThingPrerequisites thingPrerequisites = new ThingPrerequisites(thing); + if (!thingPrerequisites.isReady()) { + missingPrerequisites.put(thing.getUID(), thingPrerequisites); + } + eventPublisher.post(ThingEventFactory.createStatusInfoEvent(thing.getUID(), thing.getStatusInfo())); logger.debug("Thing '{}' is tracked by ThingManager.", thing.getUID()); if (!isHandlerRegistered(thing)) { @@ -573,7 +362,7 @@ public class ThingManagerImpl @Override public void thingRemoving(Thing thing, ThingTrackerEvent thingTrackerEvent) { - setThingStatus(thing, ThingStatusInfoBuilder.create(ThingStatus.REMOVING).build()); + setThingStatus(thing, buildStatusInfo(ThingStatus.REMOVING, ThingStatusDetail.NONE)); notifyThingHandlerAboutRemoval(thing); } @@ -605,38 +394,51 @@ public class ThingManagerImpl thing.getUID()); } } + + missingPrerequisites.remove(thing.getUID()); } @Override @SuppressWarnings("PMD.CompareObjectsWithEquals") public void thingUpdated(Thing oldThing, Thing newThing, ThingTrackerEvent thingTrackerEvent) { ThingUID thingUID = newThing.getUID(); - normalizeThingConfiguration(oldThing); - normalizeThingConfiguration(newThing); + try { + normalizeThingConfiguration(oldThing); + } catch (ConfigValidationException e) { + logger.warn("Failed to normalize configuration for thing '{}': {}", oldThing.getUID(), + e.getValidationMessages(null)); + } + try { + normalizeThingConfiguration(newThing); + } catch (ConfigValidationException e) { + logger.warn("Failed to normalize configuration´for thing '{}': {}", newThing.getUID(), + e.getValidationMessages(null)); + } if (thingUpdatedLock.contains(thingUID)) { - // called from the thing handler itself, therefore - // it exists, is initializing/initialized and - // must not be informed (in order to prevent infinite loops) + // called from the thing handler itself or during thing structure update, therefore it exists + // and either is initializing/initialized and must not be informed (in order to prevent infinite loops) + // or will be initialized after the update is done by the thing type update method itself replaceThing(oldThing, newThing); } else { - Lock lock1 = getLockForThing(newThing.getUID()); + Lock lock = getLockForThing(newThing.getUID()); + lock.lock(); try { - lock1.lock(); ThingHandler thingHandler = replaceThing(oldThing, newThing); if (thingHandler != null) { - if (ThingHandlerHelper.isHandlerInitialized(newThing) || isInitializing(newThing)) { + if (ThingHandlerHelper.isHandlerInitialized(newThing) + || newThing.getStatus() == ThingStatus.INITIALIZING) { oldThing.setHandler(null); newThing.setHandler(thingHandler); try { - validate(newThing, getThingType(newThing)); + validate(newThing, thingTypeRegistry.getThingType(newThing.getThingTypeUID())); safeCaller.create(thingHandler, ThingHandler.class).build().thingUpdated(newThing); } catch (ConfigValidationException e) { final ThingHandlerFactory thingHandlerFactory = findThingHandlerFactory( newThing.getThingTypeUID()); if (thingHandlerFactory != null) { - if (isBridge(newThing)) { - unregisterAndDisposeChildHandlers((Bridge) newThing, thingHandlerFactory); + if (newThing instanceof Bridge bridge) { + unregisterAndDisposeChildHandlers(bridge, thingHandlerFactory); } disposeHandler(newThing, thingHandler); setThingStatus(newThing, @@ -671,7 +473,7 @@ public class ThingManagerImpl registerAndInitializeHandler(newThing, getThingHandlerFactory(newThing)); } } finally { - lock1.unlock(); + lock.unlock(); } } } @@ -680,47 +482,40 @@ public class ThingManagerImpl private @Nullable ThingHandler replaceThing(Thing oldThing, Thing newThing) { final ThingHandler thingHandler = thingHandlers.get(newThing.getUID()); if (oldThing != newThing) { - if (!oldThing.equals(this.things.remove(oldThing.getUID()))) { + if (!oldThing.equals(things.remove(oldThing.getUID()))) { logger.error("Thing '{}' is different from thing tracked by ThingManager. This is a bug.", oldThing.getUID()); } - this.things.put(newThing.getUID(), newThing); + things.put(newThing.getUID(), newThing); } return thingHandler; } - private @Nullable ThingType getThingType(Thing thing) { - return thingTypeRegistry.getThingType(thing.getThingTypeUID()); - } - private @Nullable ThingHandlerFactory findThingHandlerFactory(ThingTypeUID thingTypeUID) { - for (ThingHandlerFactory factory : thingHandlerFactories) { - if (factory.supportsThingType(thingTypeUID)) { - return factory; - } - } - return null; + return thingHandlerFactories.stream().filter(factory -> factory.supportsThingType(thingTypeUID)).findFirst() + .orElse(null); } private void registerHandler(Thing thing, ThingHandlerFactory thingHandlerFactory) { Lock lock = getLockForThing(thing.getUID()); + lock.lock(); try { - lock.lock(); - if (!isHandlerRegistered(thing)) { - if (!hasBridge(thing)) { - doRegisterHandler(thing, thingHandlerFactory); - } else { - Bridge bridge = getBridge(thing.getBridgeUID()); - if (bridge != null && ThingHandlerHelper.isHandlerInitialized(bridge)) { - doRegisterHandler(thing, thingHandlerFactory); - } else { - setThingStatus(thing, - buildStatusInfo(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED)); - } - } - } else { + if (isHandlerRegistered(thing)) { logger.debug("Attempt to register a handler twice for thing {} at the same time will be ignored.", thing.getUID()); + return; + } + + if (thing.getBridgeUID() == null) { + doRegisterHandler(thing, thingHandlerFactory); + } else { + Bridge bridge = getBridge(thing.getBridgeUID()); + if (bridge == null || !ThingHandlerHelper.isHandlerInitialized(bridge)) { + setThingStatus(thing, + buildStatusInfo(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED)); + } else { + doRegisterHandler(thing, thingHandlerFactory); + } } } finally { lock.unlock(); @@ -732,13 +527,10 @@ public class ThingManagerImpl thing.getUID()); try { ThingHandler thingHandler = thingHandlerFactory.registerHandler(thing); - thingHandler.setCallback(ThingManagerImpl.this.thingHandlerCallback); + thingHandler.setCallback(thingHandlerCallback); thing.setHandler(thingHandler); thingHandlers.put(thing.getUID(), thingHandler); - synchronized (thingHandlersByFactory) { - thingHandlersByFactory.computeIfAbsent(thingHandlerFactory, unused -> new HashSet<>()) - .add(thingHandler); - } + thingHandlersByFactory.computeIfAbsent(thingHandlerFactory, unused -> new HashSet<>()).add(thingHandler); } catch (Exception ex) { ThingStatusInfo statusInfo = buildStatusInfo(ThingStatus.UNINITIALIZED, ThingStatusDetail.HANDLER_REGISTERING_ERROR, @@ -749,10 +541,10 @@ public class ThingManagerImpl } } - private void registerChildHandlers(final Bridge bridge) { + protected void registerChildHandlers(final Bridge bridge) { for (final Thing child : bridge.getThings()) { logger.debug("Register and initialize child '{}' of bridge '{}'.", child.getUID(), bridge.getUID()); - ThreadPoolManager.getPool(THING_MANAGER_THREAD_POOL_NAME).execute(() -> { + scheduler.execute(() -> { try { registerAndInitializeHandler(child, getThingHandlerFactory(child)); } catch (Exception ex) { @@ -764,8 +556,9 @@ public class ThingManagerImpl } @SuppressWarnings("PMD.CompareObjectsWithEquals") - private void initializeHandler(Thing thing) { - if (isDisabledByStorage(thing.getUID())) { + protected void initializeHandler(Thing thing) { + ThingUID thingUID = thing.getUID(); + if (disabledStorage.containsKey(thingUID.getAsString())) { setThingStatus(thing, buildStatusInfo(ThingStatus.UNINITIALIZED, ThingStatusDetail.DISABLED)); logger.debug("Thing '{}' will not be initialized. It is marked as disabled.", thing.getUID()); return; @@ -773,15 +566,16 @@ public class ThingManagerImpl if (!isHandlerRegistered(thing)) { return; } + Lock lock = getLockForThing(thing.getUID()); + lock.lock(); try { - lock.lock(); if (ThingHandlerHelper.isHandlerInitialized(thing)) { logger.debug("Attempt to initialize the already initialized thing '{}' will be ignored.", thing.getUID()); return; } - if (isInitializing(thing)) { + if (thing.getStatus() == ThingStatus.INITIALIZING) { logger.debug("Attempt to initialize a handler twice for thing '{}' at the same time will be ignored.", thing.getUID()); return; @@ -793,7 +587,7 @@ public class ThingManagerImpl if (handler.getThing() != thing) { logger.warn("The model of {} is inconsistent [thing.getHandler().getThing() != thing]", thing.getUID()); } - ThingType thingType = getThingType(thing); + ThingType thingType = thingTypeRegistry.getThingType(thing.getThingTypeUID()); if (thingType != null) { ThingFactoryHelper.applyDefaultConfiguration(thing.getConfiguration(), thingType, configDescriptionRegistry); @@ -801,6 +595,7 @@ public class ThingManagerImpl try { validate(thing, thingType); + if (ThingStatus.REMOVING.equals(thing.getStatus())) { // preserve REMOVING state so the callback can later decide to remove the thing after it has been // initialized @@ -820,11 +615,18 @@ public class ThingManagerImpl } private void validate(Thing thing, @Nullable ThingType thingType) throws ConfigValidationException { - validate(thingType, thing.getUID(), ThingType::getConfigDescriptionURI, thing.getConfiguration()); + validate(thingType, thing.getUID(), thing.getConfiguration()); + + // validate a bridge is set when it is mandatory + if (thingType != null && thing.getBridgeUID() == null && !thingType.getSupportedBridgeTypeUIDs().isEmpty()) { + ConfigValidationMessage message = new ConfigValidationMessage("bridge", + "Configuring a bridge is mandatory.", "bridge_not_configured"); + throw new ConfigValidationException(bundleContext.getBundle(), translationProvider, List.of(message)); + } for (Channel channel : thing.getChannels()) { ChannelType channelType = channelTypeRegistry.getChannelType(channel.getChannelTypeUID()); - validate(channelType, channel.getUID(), ChannelType::getConfigDescriptionURI, channel.getConfiguration()); + validate(channelType, channel.getUID(), channel.getConfiguration()); } } @@ -833,22 +635,19 @@ public class ThingManagerImpl * * @param prototype the "prototype", i.e. thing type or channel type * @param targetUID the UID of the thing or channel entity - * @param configDescriptionURIFunction a function to determine the the config description UID for the given - * prototype * @param configuration the current configuration * @throws ConfigValidationException if validation failed */ - private > void validate(@Nullable T prototype, UID targetUID, - Function configDescriptionURIFunction, Configuration configuration) + private void validate(@Nullable AbstractDescriptionType prototype, UID targetUID, Configuration configuration) throws ConfigValidationException { if (prototype == null) { - logger.debug("Prototype for '{}' is not known, assuming it is initializable", targetUID); + logger.debug("Prototype for '{}' is not known, assuming it can be initialized", targetUID); return; } - URI configDescriptionURI = configDescriptionURIFunction.apply(prototype); + URI configDescriptionURI = prototype.getConfigDescriptionURI(); if (configDescriptionURI == null) { - logger.debug("Config description URI for '{}' not found, assuming '{}' is initializable", + logger.debug("Config description URI for '{}' not found, assuming '{}' can be initialized", prototype.getUID(), targetUID); return; } @@ -858,24 +657,32 @@ public class ThingManagerImpl private void normalizeThingConfiguration(Thing thing) throws ConfigValidationException { ThingType thingType = thingTypeRegistry.getThingType(thing.getThingTypeUID()); - normalizeConfiguration(thingType, thing.getUID(), ThingType::getConfigDescriptionURI, thing.getConfiguration()); + if (thingType == null) { + logger.warn("Could not normalize configuration for '{}' because the thing type was not found in registry.", + thing.getUID()); + return; + } + normalizeConfiguration(thingType, thing.getUID(), thing.getConfiguration()); for (Channel channel : thing.getChannels()) { - ChannelType channelType = channelTypeRegistry.getChannelType(channel.getChannelTypeUID()); - normalizeConfiguration(channelType, channel.getUID(), ChannelType::getConfigDescriptionURI, - channel.getConfiguration()); + ChannelTypeUID channelTypeUID = channel.getChannelTypeUID(); + if (channelTypeUID != null) { + ChannelType channelType = channelTypeRegistry.getChannelType(channelTypeUID); + normalizeConfiguration(channelType, channel.getUID(), channel.getConfiguration()); + } } } - private > void normalizeConfiguration(@Nullable T prototype, UID targetUID, - Function configDescriptionURIFunction, Configuration configuration) - throws ConfigValidationException { + private void normalizeConfiguration(@Nullable AbstractDescriptionType prototype, UID targetUID, + Configuration configuration) throws ConfigValidationException { if (prototype == null) { - logger.debug("Prototype for '{}' is not known, assuming it is already normalized", targetUID); - return; + ConfigValidationMessage message = new ConfigValidationMessage("thing/channel", + "Type description for '{0}' not found although we checked the presence before.", + "type_description_missing", targetUID); + throw new ConfigValidationException(bundleContext.getBundle(), translationProvider, List.of(message)); } - URI configDescriptionURI = configDescriptionURIFunction.apply(prototype); + URI configDescriptionURI = prototype.getConfigDescriptionURI(); if (configDescriptionURI == null) { logger.debug("Config description URI for '{}' not found, assuming '{}' is normalized", prototype.getUID(), targetUID); @@ -884,10 +691,10 @@ public class ThingManagerImpl ConfigDescription configDescription = configDescriptionRegistry.getConfigDescription(configDescriptionURI); if (configDescription == null) { - logger.warn( - "No config description for '{}' found when normalizing configuration for '{}'. This is probably a bug.", - configDescriptionURI, targetUID); - return; + ConfigValidationMessage message = new ConfigValidationMessage("thing/channel", + "Config description for '{0}' not found also we checked the presence before.", + "config_description_missing", targetUID); + throw new ConfigValidationException(bundleContext.getBundle(), translationProvider, List.of(message)); } Objects.requireNonNull(ConfigUtil.normalizeTypes(configuration.getProperties(), List.of(configDescription))) @@ -897,20 +704,15 @@ public class ThingManagerImpl private void doInitializeHandler(final ThingHandler thingHandler) { logger.debug("Calling initialize handler for thing '{}' at '{}'.", thingHandler.getThing().getUID(), thingHandler); - safeCaller.create(thingHandler, ThingHandler.class).onTimeout(() -> { - logger.warn("Initializing handler for thing '{}' takes more than {}ms.", thingHandler.getThing().getUID(), - SafeCaller.DEFAULT_TIMEOUT); - }).onException(e -> { - ThingStatusInfo statusInfo = buildStatusInfo(ThingStatus.UNINITIALIZED, - ThingStatusDetail.HANDLER_INITIALIZING_ERROR, e.getMessage()); - setThingStatus(thingHandler.getThing(), statusInfo); - logger.error("Exception occurred while initializing handler of thing '{}': {}", - thingHandler.getThing().getUID(), e.getMessage(), e); - }).build().initialize(); - } - - private boolean isInitializing(Thing thing) { - return thing.getStatus() == ThingStatus.INITIALIZING; + safeCaller.create(thingHandler, ThingHandler.class) + .onTimeout(() -> logger.warn("Initializing handler for thing '{}' takes more than {}ms.", + thingHandler.getThing().getUID(), SafeCaller.DEFAULT_TIMEOUT)) + .onException(e -> { + setThingStatus(thingHandler.getThing(), buildStatusInfo(ThingStatus.UNINITIALIZED, + ThingStatusDetail.HANDLER_INITIALIZING_ERROR, e.getMessage())); + logger.error("Exception occurred while initializing handler of thing '{}': {}", + thingHandler.getThing().getUID(), e.getMessage(), e); + }).build().initialize(); } @SuppressWarnings("PMD.CompareObjectsWithEquals") @@ -919,14 +721,6 @@ public class ThingManagerImpl return handler != null && handler == thing.getHandler(); } - private boolean isBridge(Thing thing) { - return thing instanceof Bridge; - } - - private boolean hasBridge(final Thing thing) { - return thing.getBridgeUID() != null; - } - private @Nullable Bridge getBridge(@Nullable ThingUID bridgeUID) { if (bridgeUID == null) { return null; @@ -937,10 +731,10 @@ public class ThingManagerImpl private void unregisterHandler(Thing thing, ThingHandlerFactory thingHandlerFactory) { Lock lock = getLockForThing(thing.getUID()); + lock.lock(); try { - lock.lock(); if (isHandlerRegistered(thing)) { - doUnregisterHandler(thing, thingHandlerFactory); + safeCaller.create(() -> doUnregisterHandler(thing, thingHandlerFactory), Runnable.class).build().run(); } } finally { lock.unlock(); @@ -949,38 +743,35 @@ public class ThingManagerImpl private void doUnregisterHandler(final Thing thing, final ThingHandlerFactory thingHandlerFactory) { logger.debug("Calling unregisterHandler handler for thing '{}' at '{}'.", thing.getUID(), thingHandlerFactory); - safeCaller.create(() -> { - ThingHandler thingHandler = thing.getHandler(); - thingHandlerFactory.unregisterHandler(thing); - if (thingHandler != null) { - thingHandler.setCallback(null); + + ThingHandler thingHandler = thing.getHandler(); + thingHandlerFactory.unregisterHandler(thing); + if (thingHandler != null) { + thingHandler.setCallback(null); + } + thing.setHandler(null); + + ThingUID thingUID = thing.getUID(); + boolean enabled = !disabledStorage.containsKey(thingUID.getAsString()); + + ThingStatusDetail detail = enabled ? ThingStatusDetail.HANDLER_MISSING_ERROR : ThingStatusDetail.DISABLED; + + setThingStatus(thing, buildStatusInfo(ThingStatus.UNINITIALIZED, detail)); + thingHandlers.remove(thing.getUID()); + synchronized (thingHandlersByFactory) { + final Set thingHandlers = thingHandlersByFactory.get(thingHandlerFactory); + if (thingHandlers != null) { + thingHandlers.remove(thingHandler); } - thing.setHandler(null); - - boolean enabled = !isDisabledByStorage(thing.getUID()); - - ThingStatusDetail detail = enabled ? ThingStatusDetail.HANDLER_MISSING_ERROR : ThingStatusDetail.DISABLED; - - setThingStatus(thing, buildStatusInfo(ThingStatus.UNINITIALIZED, detail)); - thingHandlers.remove(thing.getUID()); - synchronized (thingHandlersByFactory) { - final Set thingHandlers = thingHandlersByFactory.get(thingHandlerFactory); - if (thingHandlers != null) { - thingHandlers.remove(thingHandler); - if (thingHandlers.isEmpty()) { - thingHandlersByFactory.remove(thingHandlerFactory); - } - } - } - }, Runnable.class).build().run(); + } } private void disposeHandler(Thing thing, ThingHandler thingHandler) { Lock lock = getLockForThing(thing.getUID()); + lock.lock(); try { - lock.lock(); doDisposeHandler(thingHandler); - if (hasBridge(thing)) { + if (thing.getBridgeUID() != null) { notifyBridgeAboutChildHandlerDisposal(thing, thingHandler); } } finally { @@ -991,51 +782,38 @@ public class ThingManagerImpl private void doDisposeHandler(final ThingHandler thingHandler) { logger.debug("Calling dispose handler for thing '{}' at '{}'.", thingHandler.getThing().getUID(), thingHandler); setThingStatus(thingHandler.getThing(), buildStatusInfo(ThingStatus.UNINITIALIZED, ThingStatusDetail.NONE)); - safeCaller.create(thingHandler, ThingHandler.class).onTimeout(() -> { - logger.warn("Disposing handler for thing '{}' takes more than {}ms.", thingHandler.getThing().getUID(), - SafeCaller.DEFAULT_TIMEOUT); - }).onException(e -> { - logger.error("Exception occurred while disposing handler of thing '{}': {}", - thingHandler.getThing().getUID(), e.getMessage(), e); - }).build().dispose(); + safeCaller.create(thingHandler, ThingHandler.class) // + .onTimeout(() -> logger.warn("Disposing handler for thing '{}' takes more than {}ms.", + thingHandler.getThing().getUID(), SafeCaller.DEFAULT_TIMEOUT)) // + .onException(e -> logger.error("Exception occurred while disposing handler of thing '{}': {}", + thingHandler.getThing().getUID(), e.getMessage(), e)) // + .build().dispose(); } private void unregisterAndDisposeChildHandlers(Bridge bridge, ThingHandlerFactory thingHandlerFactory) { - addThingsToBridge(bridge); - for (Thing child : bridge.getThings()) { + ThingUID bridgeUID = bridge.getUID(); + thingRegistry.stream().filter(thing -> bridgeUID.equals(thing.getBridgeUID())).forEach(child -> { ThingHandler handler = child.getHandler(); if (handler != null) { logger.debug("Unregister and dispose child '{}' of bridge '{}'.", child.getUID(), bridge.getUID()); unregisterAndDisposeHandler(thingHandlerFactory, child, handler); } - } + }); } private void unregisterAndDisposeHandler(ThingHandlerFactory thingHandlerFactory, Thing thing, ThingHandler handler) { - if (isBridge(thing)) { - unregisterAndDisposeChildHandlers((Bridge) thing, thingHandlerFactory); + if (thing instanceof Bridge bridge) { + unregisterAndDisposeChildHandlers(bridge, thingHandlerFactory); } disposeHandler(thing, handler); unregisterHandler(thing, thingHandlerFactory); } - private void addThingsToBridge(Bridge bridge) { - Collection things = thingRegistry.getAll(); - for (Thing thing : things) { - ThingUID bridgeUID = thing.getBridgeUID(); - if (bridgeUID != null && bridgeUID.equals(bridge.getUID())) { - if (bridge instanceof BridgeImpl && !bridge.getThings().contains(thing)) { - ((BridgeImpl) bridge).addThing(thing); - } - } - } - } - - private void notifyThingsAboutBridgeStatusChange(final Bridge bridge, final ThingStatusInfo bridgeStatus) { + protected void notifyThingsAboutBridgeStatusChange(final Bridge bridge, final ThingStatusInfo bridgeStatus) { if (ThingHandlerHelper.isHandlerInitialized(bridge)) { for (final Thing child : bridge.getThings()) { - ThreadPoolManager.getPool(THING_MANAGER_THREAD_POOL_NAME).execute(() -> { + scheduler.execute(() -> { try { ThingHandler handler = child.getHandler(); if (handler != null && ThingHandlerHelper.isHandlerInitialized(child)) { @@ -1051,10 +829,10 @@ public class ThingManagerImpl } } - private void notifyBridgeAboutChildHandlerInitialization(final Thing thing) { + protected void notifyBridgeAboutChildHandlerInitialization(final Thing thing) { final Bridge bridge = getBridge(thing.getBridgeUID()); if (bridge != null) { - ThreadPoolManager.getPool(THING_MANAGER_THREAD_POOL_NAME).execute(() -> { + scheduler.execute(() -> { try { BridgeHandler bridgeHandler = bridge.getHandler(); if (bridgeHandler != null) { @@ -1090,7 +868,7 @@ public class ThingManagerImpl } } - private void notifyThingHandlerAboutRemoval(final Thing thing) { + protected void notifyThingHandlerAboutRemoval(final Thing thing) { logger.trace("Asking handler of thing '{}' to handle its removal.", thing.getUID()); ThreadPoolManager.getPool(THING_MANAGER_THREAD_POOL_NAME).execute(() -> { @@ -1108,7 +886,7 @@ public class ThingManagerImpl }); } - private void notifyRegistryAboutForceRemove(final Thing thing) { + protected void notifyRegistryAboutForceRemove(final Thing thing) { logger.debug("Removal handling of thing '{}' completed. Going to remove it now.", thing.getUID()); // call asynchronous to avoid deadlocks in thing handler @@ -1119,69 +897,36 @@ public class ThingManagerImpl logger.debug("Could not remove thing {}. Most likely because it is not managed.", thing.getUID(), ex); } catch (Exception ex) { logger.error( - "Could not remove thing {}, because an unknwon Exception occurred. Most likely because it is not managed.", + "Could not remove thing {}, because an unknown Exception occurred. Most likely because it is not managed.", thing.getUID(), ex); } }); } - @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) - protected synchronized void addThingHandlerFactory(ThingHandlerFactory thingHandlerFactory) { - logger.debug("Thing handler factory '{}' added", thingHandlerFactory.getClass().getSimpleName()); - thingHandlerFactories.add(thingHandlerFactory); - handleThingHandlerFactoryAddition(getBundleIdentifier(thingHandlerFactory)); - } - - @Override - public void onReadyMarkerAdded(ReadyMarker readyMarker) { - String identifier = readyMarker.getIdentifier(); - loadedXmlThingTypes.add(identifier); - handleThingHandlerFactoryAddition(identifier); - } - - @Override - public void onReadyMarkerRemoved(ReadyMarker readyMarker) { - String identifier = readyMarker.getIdentifier(); - loadedXmlThingTypes.remove(identifier); - } - - private void handleThingHandlerFactoryAddition(String bundleIdentifier) { - thingHandlerFactories.stream().filter(it -> bundleIdentifier.equals(getBundleIdentifier(it))) - .forEach(thingHandlerFactory -> things.values().forEach(thing -> { - if (thingHandlerFactory.supportsThingType(thing.getThingTypeUID())) { - if (!isHandlerRegistered(thing)) { - registerAndInitializeHandler(thing, thingHandlerFactory); - } else { - logger.debug("Thing handler for thing '{}' already registered", thing.getUID()); - } - } - })); - } - - private String getBundleIdentifier(ThingHandlerFactory thingHandlerFactory) { - return ReadyMarkerUtils.getIdentifier(bundleResolver.resolveBundle(thingHandlerFactory.getClass())); - } - private void registerAndInitializeHandler(final Thing thing, final @Nullable ThingHandlerFactory thingHandlerFactory) { - if (isDisabledByStorage(thing.getUID())) { + ThingUID thingUID = thing.getUID(); + if (disabledStorage.containsKey(thingUID.getAsString())) { logger.debug("Not registering a handler at this point. Thing is disabled."); - setThingStatus(thing, new ThingStatusInfo(ThingStatus.UNINITIALIZED, ThingStatusDetail.DISABLED, null)); + setThingStatus(thing, buildStatusInfo(ThingStatus.UNINITIALIZED, ThingStatusDetail.DISABLED)); } else { if (thingHandlerFactory != null) { - final String identifier = getBundleIdentifier(thingHandlerFactory); - - if (loadedXmlThingTypes.contains(identifier)) { + if (!missingPrerequisites.containsKey(thing.getUID())) { + if (thingRegistry.getProvider(thing) instanceof ManagedProvider + && checkAndPerformUpdate(thing, thingHandlerFactory)) { + return; + } normalizeThingConfiguration(thing); registerHandler(thing, thingHandlerFactory); initializeHandler(thing); } else { + setThingStatus(thing, buildStatusInfo(ThingStatus.UNINITIALIZED, ThingStatusDetail.NOT_YET_READY)); logger.debug( - "Not registering a handler at this point. The thing types of bundle '{}' are not fully loaded yet.", - identifier); + "Not registering a handler at this point. The thing type '{}' is not fully loaded yet.", + thing.getThingTypeUID()); } } else { - setThingStatus(thing, new ThingStatusInfo(ThingStatus.UNINITIALIZED, + setThingStatus(thing, buildStatusInfo(ThingStatus.UNINITIALIZED, ThingStatusDetail.HANDLER_MISSING_ERROR, "Handler factory not found")); logger.debug("Not registering a handler at this point. No handler factory for thing '{}' found.", thing.getUID()); @@ -1199,34 +944,8 @@ public class ThingManagerImpl return null; } - protected synchronized void removeThingHandlerFactory(ThingHandlerFactory thingHandlerFactory) { - logger.debug("Thing handler factory '{}' removed", thingHandlerFactory.getClass().getSimpleName()); - thingHandlerFactories.remove(thingHandlerFactory); - handleThingHandlerFactoryRemoval(thingHandlerFactory); - } - - private void handleThingHandlerFactoryRemoval(ThingHandlerFactory thingHandlerFactory) { - final Set handlers; - synchronized (thingHandlersByFactory) { - handlers = thingHandlersByFactory.remove(thingHandlerFactory); - } - if (handlers != null) { - for (ThingHandler thingHandler : handlers) { - final Thing thing = thingHandler.getThing(); - if (isHandlerRegistered(thing)) { - unregisterAndDisposeHandler(thingHandlerFactory, thing, thingHandler); - } - } - } - } - - private synchronized Lock getLockForThing(ThingUID thingUID) { - Lock lock = thingLocks.get(thingUID); - if (lock == null) { - lock = new ReentrantLock(); - thingLocks.put(thingUID, lock); - } - return lock; + private Lock getLockForThing(ThingUID thingUID) { + return Objects.requireNonNull(thingLocks.computeIfAbsent(thingUID, k -> new ReentrantLock())); } private ThingStatusInfo buildStatusInfo(ThingStatus thingStatus, ThingStatusDetail thingStatusDetail, @@ -1240,7 +959,7 @@ public class ThingManagerImpl return buildStatusInfo(thingStatus, thingStatusDetail, null); } - private void setThingStatus(Thing thing, ThingStatusInfo thingStatusInfo) { + protected void setThingStatus(Thing thing, ThingStatusInfo thingStatusInfo) { ThingStatusInfo oldStatusInfo = thingStatusInfoI18nLocalizationService.getLocalizedThingStatusInfo(thing, null); thing.setStatusInfo(thingStatusInfo); ThingStatusInfo newStatusInfo = thingStatusInfoI18nLocalizationService.getLocalizedThingStatusInfo(thing, null); @@ -1257,7 +976,7 @@ public class ThingManagerImpl @Override public void setEnabled(ThingUID thingUID, boolean enabled) { - Thing thing = this.things.get(thingUID); + Thing thing = things.get(thingUID); persistThingEnableStatus(thingUID, enabled); @@ -1282,7 +1001,7 @@ public class ThingManagerImpl // No handler registered. Try to register handler and initialize the thing. registerAndInitializeHandler(thing, findThingHandlerFactory(thing.getThingTypeUID())); // Check if registration was successful - if (!hasBridge(thing) && !isHandlerRegistered(thing)) { + if (!isHandlerRegistered(thing)) { setThingStatus(thing, buildStatusInfo(ThingStatus.UNINITIALIZED, ThingStatusDetail.HANDLER_MISSING_ERROR)); } @@ -1312,8 +1031,8 @@ public class ThingManagerImpl setThingStatus(thing, buildStatusInfo(ThingStatus.UNINITIALIZED, ThingStatusDetail.DISABLED)); } - if (isBridge(thing)) { - updateChildThingStatusForDisabledBridges((Bridge) thing); + if (thing instanceof Bridge bridge) { + updateChildThingStatusForDisabledBridges(bridge); } } } @@ -1331,31 +1050,214 @@ public class ThingManagerImpl private void persistThingEnableStatus(ThingUID thingUID, boolean enabled) { logger.debug("Thing with UID {} will be persisted as {}.", thingUID, enabled ? "enabled." : "disabled."); if (enabled) { - // Clear the disabled thing storage. Otherwise the handler will NOT be initialized later. - storage.remove(thingUID.getAsString()); + // Clear the disabled thing storage. Otherwise, the handler will NOT be initialized later. + disabledStorage.remove(thingUID.getAsString()); } else { // Mark the thing as disabled in the storage. - storage.put(thingUID.getAsString(), ""); + disabledStorage.put(thingUID.getAsString(), ""); } } @Override public boolean isEnabled(ThingUID thingUID) { - Thing thing = this.things.get(thingUID); + Thing thing = things.get(thingUID); if (thing != null) { return thing.isEnabled(); } logger.debug("Thing with UID {} is unknown. Will try to get the enabled status from the persistent storage.", thingUID); - return !isDisabledByStorage(thingUID); + return !disabledStorage.containsKey(thingUID.getAsString()); } - private boolean isDisabledByStorage(ThingUID thingUID) { - return storage.containsKey(thingUID.getAsString()); + private boolean checkAndPerformUpdate(Thing thing, ThingHandlerFactory factory) { + final int currentThingTypeVersion = Integer + .parseInt(thing.getProperties().getOrDefault(PROPERTY_THING_TYPE_VERSION, "0")); + + UpdateInstructionKey thingKey = new UpdateInstructionKey(factory, thing.getThingTypeUID()); + List instructions = updateInstructions.getOrDefault(thingKey, List.of()).stream() + .filter(ThingUpdateInstruction.applies(currentThingTypeVersion)).toList(); + + if (instructions.isEmpty()) { + return false; + } + + // create a thing builder and apply the update instructions + ThingBuilder thingBuilder = ThingBuilder.create(thing); + instructions.forEach(instruction -> instruction.perform(thing, thingBuilder)); + int newThingTypeVersion = instructions.get(instructions.size() - 1).getThingTypeVersion(); + thingBuilder.withProperty(PROPERTY_THING_TYPE_VERSION, String.valueOf(newThingTypeVersion)); + logger.info("Updating '{}' from version {} to {}", thing.getUID(), currentThingTypeVersion, + newThingTypeVersion); + + Thing newThing = thingBuilder.build(); + thingUpdated(newThing); + + ThingPrerequisites thingPrerequisites = new ThingPrerequisites(newThing); + if (!thingPrerequisites.isReady()) { + missingPrerequisites.put(newThing.getUID(), thingPrerequisites); + } + + registerAndInitializeHandler(newThing, getThingHandlerFactory(newThing)); + + return true; } - void setBundleResolver(BundleResolver bundleResolver) { - this.bundleResolver = bundleResolver; + private void checkMissingPrerequisites() { + Iterator it = missingPrerequisites.values().iterator(); + while (it.hasNext()) { + ThingPrerequisites prerequisites = it.next(); + if (prerequisites.isReady()) { + it.remove(); + Thing thing = things.get(prerequisites.thingUID); + if (thing != null) { + if (!isHandlerRegistered(thing)) { + registerAndInitializeHandler(thing, getThingHandlerFactory(thing)); + } else { + logger.warn( + "Handler of thing '{}' already registered even though not all prerequisites were met.", + thing.getUID()); + } + } else { + logger.warn("Found missing thing while checking prerequisites of thing '{}'", + prerequisites.thingUID); + } + } + } + } + + @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) + protected synchronized void addThingHandlerFactory(ThingHandlerFactory thingHandlerFactory) { + logger.debug("Thing handler factory '{}' added", thingHandlerFactory.getClass().getSimpleName()); + updateInstructions.putAll(thingUpdateInstructionReader.readForFactory(thingHandlerFactory)); + thingHandlerFactories.add(thingHandlerFactory); + things.values().stream().filter(thing -> thingHandlerFactory.supportsThingType(thing.getThingTypeUID())) + .forEach(thing -> { + if (!isHandlerRegistered(thing)) { + ThingPrerequisites thingPrerequisites = new ThingPrerequisites(thing); + if (!thingPrerequisites.isReady()) { + missingPrerequisites.put(thing.getUID(), thingPrerequisites); + } + registerAndInitializeHandler(thing, thingHandlerFactory); + } else { + logger.debug("Thing handler for thing '{}' already registered", thing.getUID()); + } + }); + } + + protected synchronized void removeThingHandlerFactory(ThingHandlerFactory thingHandlerFactory) { + logger.debug("Thing handler factory '{}' removed", thingHandlerFactory.getClass().getSimpleName()); + thingHandlerFactories.remove(thingHandlerFactory); + final Set handlers = thingHandlersByFactory.remove(thingHandlerFactory); + if (handlers != null) { + for (ThingHandler thingHandler : handlers) { + final Thing thing = thingHandler.getThing(); + if (isHandlerRegistered(thing)) { + unregisterAndDisposeHandler(thingHandlerFactory, thing, thingHandler); + } + } + } + updateInstructions.keySet().removeIf(key -> thingHandlerFactory.equals(key.factory())); + } + + @Override + public void onReadyMarkerAdded(ReadyMarker readyMarker) { + startLevelSetterJob = scheduler.scheduleWithFixedDelay(() -> { + if (things.values().stream().anyMatch(t -> !ThingHandlerHelper.isHandlerInitialized(t) && t.isEnabled())) { + return; + } + readyService.markReady(READY_MARKER_THINGS_LOADED); + if (startLevelSetterJob != null) { + startLevelSetterJob.cancel(false); + } + readyService.unregisterTracker(this); + }, CHECK_INTERVAL, CHECK_INTERVAL, TimeUnit.SECONDS); + prerequisiteCheckerJob = scheduler.scheduleWithFixedDelay(this::checkMissingPrerequisites, CHECK_INTERVAL, + CHECK_INTERVAL, TimeUnit.SECONDS); + } + + @Override + public void onReadyMarkerRemoved(ReadyMarker readyMarker) { + // do nothing + } + + /** + * The {@link ThingPrerequisites} class is used to gather and check the pre-requisites of a given thing (i.e. + * availability of the {@link ThingType} and all needed {@link ChannelType}s and {@link ConfigDescription}s). + * + */ + private class ThingPrerequisites { + private final ThingUID thingUID; + private @Nullable ThingTypeUID thingTypeUID; + private final Set channelTypeUIDs = new HashSet<>(); + private final Set configDescriptionUris = new HashSet<>(); + private int timesChecked = 0; + + public ThingPrerequisites(Thing thing) { + thingUID = thing.getUID(); + thingTypeUID = thing.getThingTypeUID(); + thing.getChannels().stream().map(Channel::getChannelTypeUID).filter(Objects::nonNull) + .map(Objects::requireNonNull).distinct().forEach(channelTypeUIDs::add); + } + + /** + * Check if all necessary information is present in the registries. + *

+ * If a {@link ThingHandlerFactory} reports that it supports {@link ThingTypeUID} but the {@link ThingType} + * can't be found in the {@link ThingTypeRegistry} this method also returns true after + * {@link #MAX_CHECK_PREREQUISITE_TIME} s. + * + * @return true if all pre-requisites are present, false otherwise + */ + public synchronized boolean isReady() { + ThingTypeUID thingTypeUID = this.thingTypeUID; + // thing-type + if (thingTypeUID != null) { + ThingType thingType = thingTypeRegistry.getThingType(thingTypeUID); + if (thingType != null) { + this.thingTypeUID = null; + URI configDescriptionUri = thingType.getConfigDescriptionURI(); + if (configDescriptionUri != null) { + configDescriptionUris.add(configDescriptionUri); + } + } else if (thingHandlerFactories.stream().anyMatch(f -> f.supportsThingType(thingTypeUID))) { + timesChecked++; + if (timesChecked > MAX_CHECK_PREREQUISITE_TIME / CHECK_INTERVAL) { + logger.warn( + "A thing handler factory claims to support '{}' for thing '{}' for more than {}s, but the thing type can't be found in the registry. This should be fixed in the binding.", + thingTypeUID, thingUID, MAX_CHECK_PREREQUISITE_TIME); + this.thingTypeUID = null; + } + } + } + + // channel types + Iterator it = channelTypeUIDs.iterator(); + while (it.hasNext()) { + ChannelType channelType = channelTypeRegistry.getChannelType(it.next()); + if (channelType != null) { + it.remove(); + URI configDescriptionUri = channelType.getConfigDescriptionURI(); + if (configDescriptionUri != null) { + configDescriptionUris.add(configDescriptionUri); + } + } + } + + // config descriptions + configDescriptionUris.removeIf(uri -> configDescriptionRegistry.getConfigDescription(uri) != null); + + boolean isReady = this.thingTypeUID == null && channelTypeUIDs.isEmpty() && configDescriptionUris.isEmpty(); + if (!isReady) { + logger.debug("Check result is 'not ready': {}", this); + } + return isReady; + } + + @Override + public String toString() { + return "ThingPrerequisites{thingUID=" + thingUID + ", thingTypeUID=" + thingTypeUID + ", channelTypeUIDs=" + + channelTypeUIDs + ", configDescriptionUris=" + configDescriptionUris + "}"; + } } } diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/update/RemoveChannelInstructionImpl.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/update/RemoveChannelInstructionImpl.java new file mode 100644 index 000000000..7c6e1fda2 --- /dev/null +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/update/RemoveChannelInstructionImpl.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.thing.internal.update; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.binding.builder.ThingBuilder; + +/** + * The {@link RemoveChannelInstructionImpl} implements a {@link ThingUpdateInstruction} that removes a channel from a + * thing. + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class RemoveChannelInstructionImpl implements ThingUpdateInstruction { + private final int thingTypeVersion; + private final String channelId; + + RemoveChannelInstructionImpl(int thingTypeVersion, String channelId) { + this.thingTypeVersion = thingTypeVersion; + this.channelId = channelId; + } + + @Override + public int getThingTypeVersion() { + return thingTypeVersion; + } + + @Override + public void perform(Thing thing, ThingBuilder thingBuilder) { + ChannelUID affectedChannelUid = new ChannelUID(thing.getUID(), channelId); + thingBuilder.withoutChannel(affectedChannelUid); + } +} diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/update/ThingUpdateInstruction.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/update/ThingUpdateInstruction.java new file mode 100644 index 000000000..a6955b704 --- /dev/null +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/update/ThingUpdateInstruction.java @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.thing.internal.update; + +import java.util.function.Predicate; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.binding.builder.ThingBuilder; + +/** + * The {@link ThingUpdateInstruction} is an interface that can be implemented to perform updates on things when the + * thing-type changes. + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public interface ThingUpdateInstruction { + + /** + * Get the (final) thing type version that the {@link Thing} will be updated to after all + * {@link ThingUpdateInstruction}s with the same version have been applied. + * + * @return the thing-type version (always > 0) + */ + int getThingTypeVersion(); + + /** + * Perform the update in this instruction for a given {@link Thing} using the given {@link ThingBuilder} + *

+ * Note: the thing type version is not updated as there may be several instructions to perform for a single version. + * + * @param thing the thing that should be updated + * @param thingBuilder the thing builder to use + */ + void perform(Thing thing, ThingBuilder thingBuilder); + + /** + * Check if this update is needed for a {@link Thing} with the given version + * + * @param currentThingTypeVersion the current thing type version of the {@link Thing} + * @return true if this instruction should be applied, false otherwise + */ + static Predicate applies(int currentThingTypeVersion) { + return i -> i.getThingTypeVersion() > currentThingTypeVersion; + } +} diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/update/ThingUpdateInstructionReader.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/update/ThingUpdateInstructionReader.java new file mode 100644 index 000000000..224895a56 --- /dev/null +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/update/ThingUpdateInstructionReader.java @@ -0,0 +1,111 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.thing.internal.update; + +import java.net.URL; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Unmarshaller; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.ThingHandlerFactory; +import org.openhab.core.thing.internal.update.dto.AddChannel; +import org.openhab.core.thing.internal.update.dto.InstructionSet; +import org.openhab.core.thing.internal.update.dto.RemoveChannel; +import org.openhab.core.thing.internal.update.dto.ThingType; +import org.openhab.core.thing.internal.update.dto.UpdateChannel; +import org.openhab.core.thing.internal.update.dto.UpdateDescriptions; +import org.openhab.core.util.BundleResolver; +import org.osgi.framework.Bundle; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link ThingUpdateInstructionReader} is used to read instructions for a given {@link ThingHandlerFactory} and + * * create a list of {@link ThingUpdateInstruction}s + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class ThingUpdateInstructionReader { + private final Logger logger = LoggerFactory.getLogger(ThingUpdateInstructionReader.class); + private final BundleResolver bundleResolver; + + public ThingUpdateInstructionReader(BundleResolver bundleResolver) { + this.bundleResolver = bundleResolver; + } + + public Map> readForFactory(ThingHandlerFactory factory) { + Bundle bundle = bundleResolver.resolveBundle(factory.getClass()); + if (bundle == null) { + logger.error( + "Could not get bundle for '{}', thing type updates will fail. If this occurs outside of tests, it is a bug.", + factory.getClass()); + return Map.of(); + } + + Map> updateInstructions = new HashMap<>(); + Enumeration entries = bundle.findEntries("OH-INF/update", "*.xml", true); + if (entries != null) { + while (entries.hasMoreElements()) { + URL url = entries.nextElement(); + try { + JAXBContext context = JAXBContext.newInstance(UpdateDescriptions.class); + Unmarshaller u = context.createUnmarshaller(); + UpdateDescriptions updateDescriptions = (UpdateDescriptions) u.unmarshal(url); + + for (ThingType thingType : updateDescriptions.getThingType()) { + ThingTypeUID thingTypeUID = new ThingTypeUID(thingType.getUid()); + UpdateInstructionKey key = new UpdateInstructionKey(factory, thingTypeUID); + List instructions = new ArrayList<>(); + List instructionSets = thingType.getInstructionSet().stream() + .sorted(Comparator.comparing(InstructionSet::getTargetVersion)).toList(); + for (InstructionSet instructionSet : instructionSets) { + int targetVersion = instructionSet.getTargetVersion(); + for (Object instruction : instructionSet.getInstructions()) { + if (instruction instanceof AddChannel addChannelType) { + instructions.add(new UpdateChannelInstructionImpl(targetVersion, addChannelType)); + } else if (instruction instanceof UpdateChannel updateChannelType) { + instructions + .add(new UpdateChannelInstructionImpl(targetVersion, updateChannelType)); + } else if (instruction instanceof RemoveChannel removeChannelType) { + instructions.add( + new RemoveChannelInstructionImpl(targetVersion, removeChannelType.getId())); + } else { + logger.warn("Instruction type '{}' is unknown.", instruction.getClass()); + } + } + } + updateInstructions.put(key, instructions); + } + logger.trace("Reading update instructions from '{}'", url.getPath()); + } catch (IllegalArgumentException | JAXBException e) { + logger.warn("Failed to parse update instructions from '{}':", url, e); + } + } + } + + return updateInstructions; + } + + public record UpdateInstructionKey(ThingHandlerFactory factory, ThingTypeUID thingTypeId) { + } +} diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/update/UpdateChannelInstructionImpl.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/update/UpdateChannelInstructionImpl.java new file mode 100644 index 000000000..fba698fb5 --- /dev/null +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/internal/update/UpdateChannelInstructionImpl.java @@ -0,0 +1,105 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.thing.internal.update; + +import java.util.List; +import java.util.Objects; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.binding.builder.ChannelBuilder; +import org.openhab.core.thing.binding.builder.ThingBuilder; +import org.openhab.core.thing.internal.update.dto.AddChannel; +import org.openhab.core.thing.internal.update.dto.UpdateChannel; +import org.openhab.core.thing.type.ChannelTypeUID; + +/** + * The {@link UpdateChannelInstructionImpl} implements a {@link ThingUpdateInstruction} that updates a channel of a + * thing. + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class UpdateChannelInstructionImpl implements ThingUpdateInstruction { + private boolean removeOldChannel; + private final int thingTypeVersion; + private final boolean preserveConfig; + private final String channelId; + private final String channelTypeUid; + private final @Nullable String label; + private final @Nullable String description; + private final @Nullable List tags; + + UpdateChannelInstructionImpl(int thingTypeVersion, UpdateChannel updateChannel) { + this.removeOldChannel = true; + this.thingTypeVersion = thingTypeVersion; + this.channelId = updateChannel.getId(); + this.channelTypeUid = updateChannel.getType(); + this.label = updateChannel.getLabel(); + this.description = updateChannel.getDescription(); + this.tags = updateChannel.getTags(); + this.preserveConfig = updateChannel.isPreserveConfiguration(); + } + + UpdateChannelInstructionImpl(int thingTypeVersion, AddChannel addChannel) { + this.removeOldChannel = false; + this.thingTypeVersion = thingTypeVersion; + this.channelId = addChannel.getId(); + this.channelTypeUid = addChannel.getType(); + this.label = addChannel.getLabel(); + this.description = addChannel.getDescription(); + this.tags = addChannel.getTags(); + this.preserveConfig = false; + } + + @Override + public int getThingTypeVersion() { + return thingTypeVersion; + } + + @Override + public void perform(Thing thing, ThingBuilder thingBuilder) { + ChannelUID affectedChannelUid = new ChannelUID(thing.getUID(), channelId); + + if (removeOldChannel) { + thingBuilder.withoutChannel(affectedChannelUid); + } + + ChannelBuilder channelBuilder = ChannelBuilder.create(affectedChannelUid) + .withType(new ChannelTypeUID(channelTypeUid)); + + if (preserveConfig) { + Channel oldChannel = thing.getChannel(affectedChannelUid); + if (oldChannel != null) { + channelBuilder.withConfiguration(oldChannel.getConfiguration()); + channelBuilder.withDefaultTags(oldChannel.getDefaultTags()); + } + } + + if (label != null) { + channelBuilder.withLabel(Objects.requireNonNull(label)); + } + if (description != null) { + channelBuilder.withDescription(Objects.requireNonNull(description)); + } + if (tags != null) { + channelBuilder.withDefaultTags(Set.copyOf(Objects.requireNonNull(tags))); + } + + thingBuilder.withChannel(channelBuilder.build()); + } +} diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/type/AbstractDescriptionType.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/type/AbstractDescriptionType.java index ee5ed3f59..9e4bdb71b 100644 --- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/type/AbstractDescriptionType.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/type/AbstractDescriptionType.java @@ -12,9 +12,12 @@ */ package org.openhab.core.thing.type; +import java.net.URI; + import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.common.registry.Identifiable; +import org.openhab.core.config.core.ConfigDescription; import org.openhab.core.thing.UID; /** @@ -29,23 +32,25 @@ import org.openhab.core.thing.UID; @NonNullByDefault public abstract class AbstractDescriptionType implements Identifiable { - private UID uid; - private String label; - private @Nullable String description; + private final UID uid; + private final String label; + private final @Nullable String description; + private final @Nullable URI configDescriptionURI; /** * Creates a new instance of this class with the specified parameters. * * @param uid the unique identifier which identifies the according type within * the overall system (must neither be null, nor empty) - * @param label the human readable label for the according type + * @param label the human-readable label for the according type * (must neither be null nor empty) - * @param description the human readable description for the according type + * @param description the human-readable description for the according type * (could be null or empty) + * @param configDescriptionURI the {@link URI} that references the {@link ConfigDescription} of this type * @throws IllegalArgumentException if the UID is null, or the label is null or empty */ - public AbstractDescriptionType(UID uid, String label, @Nullable String description) - throws IllegalArgumentException { + public AbstractDescriptionType(UID uid, String label, @Nullable String description, + @Nullable URI configDescriptionURI) throws IllegalArgumentException { if (label.isEmpty()) { throw new IllegalArgumentException("The label must neither be null nor empty!"); } @@ -53,6 +58,7 @@ public abstract class AbstractDescriptionType implements Identifiable { this.uid = uid; this.label = label; this.description = description; + this.configDescriptionURI = configDescriptionURI; } /** @@ -67,20 +73,29 @@ public abstract class AbstractDescriptionType implements Identifiable { } /** - * Returns the human readable label for the according type. + * Returns the human-readable label for the according type. * - * @return the human readable label for the according type (neither null, nor empty) + * @return the human-readable label for the according type (neither null, nor empty) */ public String getLabel() { return this.label; } /** - * Returns the human readable description for the according type. + * Returns the human-readable description for the according type. * - * @return the human readable description for the according type (could be null or empty) + * @return the human-readable description for the according type (could be null or empty) */ public @Nullable String getDescription() { return this.description; } + + /** + * Returns the link to a concrete {@link ConfigDescription}. + * + * @return the link to a concrete ConfigDescription + */ + public @Nullable URI getConfigDescriptionURI() { + return configDescriptionURI; + } } diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/type/ChannelGroupType.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/type/ChannelGroupType.java index e9f61d02c..45afe7307 100644 --- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/type/ChannelGroupType.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/type/ChannelGroupType.java @@ -38,15 +38,15 @@ public class ChannelGroupType extends AbstractDescriptionType { * Creates a new instance of this class with the specified parameters. * * @param uid the unique identifier which identifies this channel group type within the - * @param label the human readable label for the according type - * @param description the human readable description for the according type + * @param label the human-readable label for the according type + * @param description the human-readable description for the according type * @param category the category of this channel group type, e.g. Temperature * @param channelDefinitions the channel definitions this channel group forms * @throws IllegalArgumentException if the UID is null, or the label is null or empty */ ChannelGroupType(ChannelGroupTypeUID uid, String label, @Nullable String description, @Nullable String category, @Nullable List channelDefinitions) throws IllegalArgumentException { - super(uid, label, description); + super(uid, label, description, null); this.category = category; this.channelDefinitions = channelDefinitions == null ? Collections.emptyList() diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/type/ChannelType.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/type/ChannelType.java index 469c99686..11930aab2 100644 --- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/type/ChannelType.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/type/ChannelType.java @@ -17,14 +17,13 @@ import java.util.Set; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.core.config.core.ConfigDescription; import org.openhab.core.thing.Channel; import org.openhab.core.types.CommandDescription; import org.openhab.core.types.EventDescription; import org.openhab.core.types.StateDescription; /** - * The {@link ChannelType} describes a concrete type of a {@link Channel}. + * The {@link ChannelType} describes a concrete type of {@link Channel}. *

* This description is used as template definition for the creation of the according concrete {@link Channel} object. * Use the {@link ChannelTypeBuilder} for building channel types. @@ -45,7 +44,6 @@ public class ChannelType extends AbstractDescriptionType { private final @Nullable StateDescription state; private final @Nullable CommandDescription commandDescription; private final @Nullable EventDescription event; - private final @Nullable URI configDescriptionURI; private final @Nullable AutoUpdatePolicy autoUpdatePolicy; /** @@ -56,15 +54,15 @@ public class ChannelType extends AbstractDescriptionType { * @param advanced true if this channel type contains advanced features, otherwise false * @param itemType the item type of this Channel type, e.g. {@code ColorItem} * @param kind the channel kind. - * @param label the human readable label for the according type + * @param label the human-readable label for the according type * (must neither be null nor empty) - * @param description the human readable description for the according type + * @param description the human-readable description for the according type * (could be null or empty) * @param category the category of this Channel type, e.g. {@code TEMPERATURE} (could be null or empty) * @param tags all tags of this {@link ChannelType}, e.g. {@code Alarm} (could be null or empty) * @param state a {@link StateDescription} of an items state which gives information how to interpret it. * @param commandDescription a {@link CommandDescription} which should be rendered as push-buttons. The command - * values will be send to the channel from this {@link ChannelType}. + * values will be sent to the channel from this {@link ChannelType}. * @param configDescriptionURI the link to the concrete ConfigDescription (could be null) * @param autoUpdatePolicy the {@link AutoUpdatePolicy} to use. * @throws IllegalArgumentException if the UID or the item type is null or empty, @@ -75,7 +73,7 @@ public class ChannelType extends AbstractDescriptionType { @Nullable StateDescription state, @Nullable CommandDescription commandDescription, @Nullable EventDescription event, @Nullable URI configDescriptionURI, @Nullable AutoUpdatePolicy autoUpdatePolicy) throws IllegalArgumentException { - super(uid, label, description); + super(uid, label, description, configDescriptionURI); if (kind == ChannelKind.STATE && (itemType == null || itemType.isBlank())) { throw new IllegalArgumentException("If the kind is 'state', the item type must be set!"); @@ -86,7 +84,6 @@ public class ChannelType extends AbstractDescriptionType { this.itemType = itemType; this.kind = kind; - this.configDescriptionURI = configDescriptionURI; this.tags = tags == null ? Set.of() : Set.copyOf(tags); this.advanced = advanced; @@ -135,15 +132,6 @@ public class ChannelType extends AbstractDescriptionType { return super.getUID().toString(); } - /** - * Returns the link to a concrete {@link ConfigDescription}. - * - * @return the link to a concrete ConfigDescription - */ - public @Nullable URI getConfigDescriptionURI() { - return this.configDescriptionURI; - } - /** * Returns the {@link StateDescription} of an items state which gives information how to interpret it. * @@ -154,7 +142,7 @@ public class ChannelType extends AbstractDescriptionType { } /** - * Returns informations about the supported events. + * Returns information about the supported events. * * @return the event information. Can be null if the channel is a state channel. */ diff --git a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/type/ThingType.java b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/type/ThingType.java index e898a4fe2..bc26ac822 100644 --- a/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/type/ThingType.java +++ b/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/type/ThingType.java @@ -19,12 +19,11 @@ import java.util.Map; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.core.config.core.ConfigDescription; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingTypeUID; /** - * The {@link ThingType} describes a concrete type of a {@link Thing}. + * The {@link ThingType} describes a concrete type of {@link Thing}. *

* This description is used as template definition for the creation of the according concrete {@link Thing} object. *

@@ -46,7 +45,6 @@ public class ThingType extends AbstractDescriptionType { private final List supportedBridgeTypeUIDs; private final Map properties; private final @Nullable String representationProperty; - private final @Nullable URI configDescriptionURI; private final boolean listed; private final @Nullable String category; @@ -57,9 +55,9 @@ public class ThingType extends AbstractDescriptionType { * (must neither be null, nor empty) * @param supportedBridgeTypeUIDs the unique identifiers of the bridges this Thing type supports * (could be null or empty) - * @param label the human readable label for the according type + * @param label the human-readable label for the according type * (must neither be null nor empty) - * @param description the human readable description for the according type + * @param description the human-readable description for the according type * (could be null or empty) * @param listed determines whether it should be listed for manually pairing or not * @param representationProperty name of the property that uniquely identifies this Thing @@ -69,7 +67,7 @@ public class ThingType extends AbstractDescriptionType { * @param properties the properties this Thing type provides (could be null) * @param configDescriptionURI the link to the concrete ConfigDescription (could be null) * @param extensibleChannelTypeIds the channel-type ids this thing-type is extensible with (could be null or empty). - * @throws IllegalArgumentException if the UID is null or empty, or the the meta information is null + * @throws IllegalArgumentException if the UID is null or empty, or the meta information is null */ ThingType(ThingTypeUID uid, @Nullable List supportedBridgeTypeUIDs, String label, @Nullable String description, @Nullable String category, boolean listed, @@ -77,7 +75,7 @@ public class ThingType extends AbstractDescriptionType { @Nullable List channelGroupDefinitions, @Nullable Map properties, @Nullable URI configDescriptionURI, @Nullable List extensibleChannelTypeIds) throws IllegalArgumentException { - super(uid, label, description); + super(uid, label, description, configDescriptionURI); this.category = category; this.listed = listed; @@ -91,7 +89,6 @@ public class ThingType extends AbstractDescriptionType { this.extensibleChannelTypeIds = extensibleChannelTypeIds == null ? Collections.emptyList() : Collections.unmodifiableList(extensibleChannelTypeIds); this.properties = properties == null ? Collections.emptyMap() : Collections.unmodifiableMap(properties); - this.configDescriptionURI = configDescriptionURI; } /** @@ -148,15 +145,6 @@ public class ThingType extends AbstractDescriptionType { return this.channelGroupDefinitions; } - /** - * Returns the link to a concrete {@link ConfigDescription}. - * - * @return the link to a concrete ConfigDescription (could be null) - */ - public @Nullable URI getConfigDescriptionURI() { - return this.configDescriptionURI; - } - /** * Returns the properties for this {@link ThingType} * diff --git a/bundles/org.openhab.core.thing/src/main/resources/xsd/bindings.xjb b/bundles/org.openhab.core.thing/src/main/resources/xsd/bindings.xjb new file mode 100644 index 000000000..9ae191fa9 --- /dev/null +++ b/bundles/org.openhab.core.thing/src/main/resources/xsd/bindings.xjb @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/bundles/org.openhab.core.thing/src/main/resources/xsd/update-description-1.0.0.xsd b/bundles/org.openhab.core.thing/src/main/resources/xsd/update-description-1.0.0.xsd new file mode 100644 index 000000000..588b48540 --- /dev/null +++ b/bundles/org.openhab.core.thing/src/main/resources/xsd/update-description-1.0.0.xsd @@ -0,0 +1,109 @@ + + + + + + + + + The root element of an update description. It contains the update instructions for managed + things after thing-type changes. Instructions are grouped by thing-type and (internal) version. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Update a channel (e.g. change channel-type, label, description). By default, the old + configuration and tags are copied to the new channel. + + + + + + + + + If set, already existing tags are overwritten. + + + + + + + + + + Remove a channel from a thing. + + + + + + + The simple id of the channel (i.e. without the thing UID). + + + + + + + The fully qualified UID of the channel type (e.g. "system:color", + "viessmann:lastErrorMessage"). + + + + + + + + The fully qualified UID of the thing type. + + + + + diff --git a/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/events/ThingEventFactoryTest.java b/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/events/ThingEventFactoryTest.java index 09cfc02fe..4d758c9b2 100644 --- a/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/events/ThingEventFactoryTest.java +++ b/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/events/ThingEventFactoryTest.java @@ -49,7 +49,7 @@ import org.openhab.core.types.StateOption; import com.google.gson.Gson; /** - * {@link ThingEventFactoryTests} tests the {@link ThingEventFactory}. + * {@link ThingEventFactoryTest} tests the {@link ThingEventFactory}. * * @author Stefan Bußweiler - Initial contribution * @author Christoph Weitkamp - Added ChannelStateDescriptionChangedEvent diff --git a/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/internal/ThingManagerImplTest.java b/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/internal/ThingManagerImplTest.java index 2000317cc..861cd7728 100644 --- a/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/internal/ThingManagerImplTest.java +++ b/bundles/org.openhab.core.thing/src/test/java/org/openhab/core/thing/internal/ThingManagerImplTest.java @@ -28,6 +28,7 @@ import org.openhab.core.common.SafeCaller; import org.openhab.core.config.core.ConfigDescriptionRegistry; import org.openhab.core.config.core.validation.ConfigDescriptionValidator; import org.openhab.core.events.EventPublisher; +import org.openhab.core.i18n.TranslationProvider; import org.openhab.core.service.ReadyService; import org.openhab.core.storage.Storage; import org.openhab.core.storage.StorageService; @@ -43,9 +44,10 @@ import org.openhab.core.thing.internal.ThingTracker.ThingTrackerEvent; import org.openhab.core.thing.link.ItemChannelLinkRegistry; import org.openhab.core.thing.type.ChannelGroupTypeRegistry; import org.openhab.core.thing.type.ChannelTypeRegistry; +import org.openhab.core.thing.type.ThingType; import org.openhab.core.thing.type.ThingTypeRegistry; import org.openhab.core.util.BundleResolver; -import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; /** * @author Simon Kaufmann - Initial contribution @@ -55,8 +57,6 @@ import org.osgi.framework.Bundle; @NonNullByDefault public class ThingManagerImplTest extends JavaTest { - private @Mock @NonNullByDefault({}) Bundle bundleMock; - private @Mock @NonNullByDefault({}) BundleResolver bundleResolverMock; private @Mock @NonNullByDefault({}) ChannelGroupTypeRegistry channelGroupTypeRegistryMock; private @Mock @NonNullByDefault({}) ChannelTypeRegistry channelTypeRegistryMock; private @Mock @NonNullByDefault({}) CommunicationManager communicationManagerMock; @@ -71,24 +71,29 @@ public class ThingManagerImplTest extends JavaTest { private @Mock @NonNullByDefault({}) StorageService storageServiceMock; private @Mock @NonNullByDefault({}) Thing thingMock; private @Mock @NonNullByDefault({}) ThingRegistryImpl thingRegistryMock; + private @Mock @NonNullByDefault({}) BundleResolver bundleResolverMock; + private @Mock @NonNullByDefault({}) TranslationProvider translationProviderMock; + private @Mock @NonNullByDefault({}) BundleContext bundleContextMock; + private @Mock @NonNullByDefault({}) ThingType thingTypeMock; // This class is final so it cannot be mocked private final ThingStatusInfoI18nLocalizationService thingStatusInfoI18nLocalizationService = new ThingStatusInfoI18nLocalizationService(); @BeforeEach public void setup() { - when(bundleMock.getSymbolicName()).thenReturn("test"); - when(bundleResolverMock.resolveBundle(any())).thenReturn(bundleMock); when(thingMock.getUID()).thenReturn(new ThingUID("test", "thing")); when(thingMock.getStatusInfo()) .thenReturn(new ThingStatusInfo(ThingStatus.UNINITIALIZED, ThingStatusDetail.NONE, null)); + when(thingTypeMock.getConfigDescriptionURI()).thenReturn(null); + when(thingTypeRegistryMock.getThingType(any())).thenReturn(thingTypeMock); } private ThingManagerImpl createThingManager() { - return new ThingManagerImpl(bundleResolverMock, channelGroupTypeRegistryMock, channelTypeRegistryMock, - communicationManagerMock, configDescriptionRegistryMock, configDescriptionValidatorMock, - eventPublisherMock, itemChannelLinkRegistryMock, readyServiceMock, safeCallerMock, storageServiceMock, - thingRegistryMock, thingStatusInfoI18nLocalizationService, thingTypeRegistryMock); + return new ThingManagerImpl(channelGroupTypeRegistryMock, channelTypeRegistryMock, communicationManagerMock, + configDescriptionRegistryMock, configDescriptionValidatorMock, eventPublisherMock, + itemChannelLinkRegistryMock, readyServiceMock, safeCallerMock, storageServiceMock, thingRegistryMock, + thingStatusInfoI18nLocalizationService, thingTypeRegistryMock, bundleResolverMock, + translationProviderMock, bundleContextMock); } @Test diff --git a/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/binding/BindingBaseClassesOSGiTest.java b/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/binding/BindingBaseClassesOSGiTest.java index c87767aef..6a81832a9 100644 --- a/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/binding/BindingBaseClassesOSGiTest.java +++ b/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/binding/BindingBaseClassesOSGiTest.java @@ -166,7 +166,7 @@ public class BindingBaseClassesOSGiTest extends JavaOSGiTest { @Override public void handleCommand(ChannelUID channelUID, Command command) { // check getBridge works - assertThat(getBridge().getUID().toString(), is("bindingId:type1:bridgeId")); + assertThat(getBridge().getUID().toString(), is(BINDING_ID + ":type1:bridgeId")); } @Override @@ -206,12 +206,17 @@ public class BindingBaseClassesOSGiTest extends JavaOSGiTest { thingHandlerFactory.activate(componentContextMock); registerService(thingHandlerFactory, ThingHandlerFactory.class.getName()); - ThingTypeUID bridgeTypeUID = new ThingTypeUID("bindingId:type1"); - ThingUID bridgeUID = new ThingUID("bindingId:type1:bridgeId"); + ThingTypeUID bridgeTypeUID = new ThingTypeUID(BINDING_ID, "type1"); + ThingTypeUID thingTypeUID = new ThingTypeUID(BINDING_ID, "type2"); + + ThingType bridgeType = ThingTypeBuilder.instance(bridgeTypeUID, "bridge").buildBridge(); + ThingType thingType = ThingTypeBuilder.instance(thingTypeUID, "thing").build(); + registerThingTypeProvider(bridgeType, thingType); + + ThingUID bridgeUID = new ThingUID(BINDING_ID, "type1", "bridgeId"); Bridge bridge = BridgeBuilder.create(bridgeTypeUID, bridgeUID).build(); - ThingTypeUID thingTypeUID = new ThingTypeUID("bindingId:type2"); - ThingUID thingUID = new ThingUID("bindingId:type2:thingId"); + ThingUID thingUID = new ThingUID(BINDING_ID, "type2", "thingId"); Thing thing = ThingBuilder.create(thingTypeUID, thingUID).withBridge(bridge.getUID()).build(); managedThingProvider.add(bridge); @@ -234,7 +239,7 @@ public class BindingBaseClassesOSGiTest extends JavaOSGiTest { }); // the assertion is in handle command - handler.handleCommand(new ChannelUID("bindingId:type2:thingId:channel"), RefreshType.REFRESH); + handler.handleCommand(new ChannelUID(thingUID, "thingId", "channel"), RefreshType.REFRESH); unregisterService(ThingHandlerFactory.class.getName()); thingHandlerFactory.deactivate(componentContextMock); @@ -245,10 +250,12 @@ public class BindingBaseClassesOSGiTest extends JavaOSGiTest { ConfigStatusProviderThingHandlerFactory thingHandlerFactory = new ConfigStatusProviderThingHandlerFactory(); thingHandlerFactory.activate(componentContextMock); registerService(thingHandlerFactory, ThingHandlerFactory.class.getName()); + registerDefaultThingTypeAndConfigDescription(); - ThingTypeUID thingTypeUID = new ThingTypeUID("bindingId:type"); - ThingUID thingUID = new ThingUID("bindingId:type:thingId"); - Thing thing = ThingBuilder.create(thingTypeUID, thingUID).build(); + ThingTypeUID thingTypeUID = new ThingTypeUID(BINDING_ID, THING_TYPE_ID); + ThingUID thingUID = new ThingUID(thingTypeUID, "thingId"); + Thing thing = ThingBuilder.create(thingTypeUID, thingUID) + .withConfiguration(new Configuration(Map.of("parameter", ""))).build(); managedThingProvider.add(thing); @@ -301,10 +308,12 @@ public class BindingBaseClassesOSGiTest extends JavaOSGiTest { ConfigStatusProviderThingHandlerFactory thingHandlerFactory = new ConfigStatusProviderThingHandlerFactory(); thingHandlerFactory.activate(componentContextMock); registerService(thingHandlerFactory, ThingHandlerFactory.class.getName()); + registerDefaultThingTypeAndConfigDescription(); - ThingTypeUID thingTypeUID = new ThingTypeUID("bindingId:type"); - ThingUID thingUID = new ThingUID("bindingId:type:thingId"); - Thing thing = ThingBuilder.create(thingTypeUID, thingUID).build(); + ThingTypeUID thingTypeUID = new ThingTypeUID(BINDING_ID, THING_TYPE_ID); + ThingUID thingUID = new ThingUID(thingTypeUID, "thingId"); + Thing thing = ThingBuilder.create(thingTypeUID, thingUID) + .withConfiguration(new Configuration(Map.of("parameter", "ok"))).build(); managedThingProvider.add(thing); @@ -329,30 +338,30 @@ public class BindingBaseClassesOSGiTest extends JavaOSGiTest { Thread.sleep(2000); - thing.getHandler().handleConfigurationUpdate(Map.of("param", "invalid")); + thing.getHandler().handleConfigurationUpdate(Map.of("parameter", "invalid")); waitForAssert(() -> { Event event = eventSubscriber.getReceivedEvent(); assertThat(event, is(notNullValue())); - assertThat(event.getPayload(), CoreMatchers - .containsString("\"parameterName\":\"param\",\"type\":\"ERROR\",\"message\":\"param invalid\"}")); + assertThat(event.getPayload(), CoreMatchers.containsString( + "\"parameterName\":\"parameter\",\"type\":\"ERROR\",\"message\":\"param invalid\"}")); eventSubscriber.resetReceivedEvent(); }, 2500, DFL_SLEEP_TIME); - thing.getHandler().handleConfigurationUpdate(Map.of("param", "ok")); + thing.getHandler().handleConfigurationUpdate(Map.of("parameter", "ok")); waitForAssert(() -> { Event event = eventSubscriber.getReceivedEvent(); assertThat(event, is(notNullValue())); - assertThat(event.getPayload(), CoreMatchers - .containsString("\"parameterName\":\"param\",\"type\":\"INFORMATION\",\"message\":\"param ok\"}")); + assertThat(event.getPayload(), CoreMatchers.containsString( + "\"parameterName\":\"parameter\",\"type\":\"INFORMATION\",\"message\":\"param ok\"}")); }, 2500, DFL_SLEEP_TIME); } @Test public void assertBaseThingHandlerNotifiesThingManagerAboutConfigurationUpdates() { // register ThingTypeProvider & ConfigurationDescription with 'required' parameter - registerThingTypeProvider(); + registerDefaultThingTypeProvider(); registerConfigDescriptionProvider(true); // register thing handler factory @@ -396,7 +405,7 @@ public class BindingBaseClassesOSGiTest extends JavaOSGiTest { static class ConfigStatusProviderThingHandler extends ConfigStatusThingHandler { - private static final String PARAM = "param"; + private static final String PARAM = "parameter"; private static final ConfigStatusMessage ERROR = ConfigStatusMessage.Builder.error(PARAM) .withMessageKeySuffix("param.invalid").build(); private static final ConfigStatusMessage INFO = ConfigStatusMessage.Builder.information(PARAM) @@ -448,7 +457,7 @@ public class BindingBaseClassesOSGiTest extends JavaOSGiTest { public void initialize() { ThingBuilder thingBuilder = editThing(); thingBuilder.withChannel( - ChannelBuilder.create(new ChannelUID("bindingId:type:thingId:1"), CoreItemFactory.STRING).build()); + ChannelBuilder.create(new ChannelUID(thing.getUID(), "1"), CoreItemFactory.STRING).build()); updateThing(thingBuilder.build()); updateStatus(ThingStatus.ONLINE); } @@ -493,18 +502,21 @@ public class BindingBaseClassesOSGiTest extends JavaOSGiTest { @Test public void assertThingCanBeUpdatedFromThingHandler() { - registerThingTypeProvider(); + registerDefaultThingTypeProvider(); YetAnotherThingHandlerFactory thingHandlerFactory = new YetAnotherThingHandlerFactory(); thingHandlerFactory.activate(componentContextMock); registerService(thingHandlerFactory, ThingHandlerFactory.class.getName()); final ThingRegistryChangeListener listener = new ThingRegistryChangeListener(); + registerDefaultThingTypeAndConfigDescription(); + try { thingRegistry.addRegistryChangeListener(listener); - ThingTypeUID thingTypeUID = new ThingTypeUID("bindingId:type"); - ThingUID thingUID = new ThingUID("bindingId:type:thingId"); - Thing thing = ThingBuilder.create(thingTypeUID, thingUID).build(); + ThingTypeUID thingTypeUID = new ThingTypeUID(BINDING_ID, THING_TYPE_ID); + ThingUID thingUID = new ThingUID(thingTypeUID, "thingId"); + Thing thing = ThingBuilder.create(thingTypeUID, thingUID) + .withConfiguration(new Configuration(Map.of("parameter", ""))).build(); assertThat(thing.getChannels().size(), is(0)); managedThingProvider.add(thing); @@ -522,17 +534,20 @@ public class BindingBaseClassesOSGiTest extends JavaOSGiTest { @Test public void assertPropertiesCanBeUpdatedFromThingHandler() { - registerThingTypeProvider(); + registerDefaultThingTypeProvider(); YetAnotherThingHandlerFactory thingHandlerFactory = new YetAnotherThingHandlerFactory(); thingHandlerFactory.activate(componentContextMock); registerService(thingHandlerFactory, ThingHandlerFactory.class.getName()); + registerDefaultThingTypeAndConfigDescription(); final ThingRegistryChangeListener listener = new ThingRegistryChangeListener(); try { thingRegistry.addRegistryChangeListener(listener); Thing thing = ThingBuilder - .create(new ThingTypeUID("bindingId:type"), new ThingUID("bindingId:type:thingId")).build(); + .create(new ThingTypeUID(BINDING_ID, THING_TYPE_ID), + new ThingUID(BINDING_ID, THING_TYPE_ID, "thingId")) + .withConfiguration(new Configuration(Map.of("parameter", ""))).build(); managedThingProvider.add(thing); @@ -577,10 +592,12 @@ public class BindingBaseClassesOSGiTest extends JavaOSGiTest { final ThingRegistryChangeListener listener = new ThingRegistryChangeListener(); + registerDefaultThingTypeAndConfigDescription(); + try { thingRegistry.addRegistryChangeListener(listener); - ThingTypeUID thingTypeUID = new ThingTypeUID("bindingId:type"); - ThingUID thingUID = new ThingUID("bindingId:type:thingId"); + ThingTypeUID thingTypeUID = new ThingTypeUID(BINDING_ID, THING_TYPE_ID); + ThingUID thingUID = new ThingUID(thingTypeUID, "thingId"); Thing thing = ThingBuilder.create(thingTypeUID, thingUID).build(); managedThingProvider.add(thing); @@ -601,11 +618,11 @@ public class BindingBaseClassesOSGiTest extends JavaOSGiTest { thingHandlerFactory.activate(componentContextMock); registerService(thingHandlerFactory, ThingHandlerFactory.class.getName()); - registerThingTypeProvider(); registerConfigDescriptionProvider(true); + registerDefaultThingTypeProvider(); - ThingTypeUID thingTypeUID = new ThingTypeUID("bindingId:type"); - ThingUID thingUID = new ThingUID("bindingId:type:thingId"); + ThingTypeUID thingTypeUID = new ThingTypeUID(BINDING_ID, THING_TYPE_ID); + ThingUID thingUID = new ThingUID(thingTypeUID, "thingId"); Thing thing = ThingBuilder.create(thingTypeUID, thingUID) .withConfiguration(new Configuration(Map.of("parameter", "someValue"))).build(); @@ -624,11 +641,11 @@ public class BindingBaseClassesOSGiTest extends JavaOSGiTest { thingHandlerFactory.activate(componentContextMock); registerService(thingHandlerFactory, ThingHandlerFactory.class.getName()); - registerThingTypeProvider(); + registerDefaultThingTypeProvider(); registerConfigDescriptionProvider(true); - ThingTypeUID thingTypeUID = new ThingTypeUID("bindingId:type"); - ThingUID thingUID = new ThingUID("bindingId:type:thingId"); + ThingTypeUID thingTypeUID = new ThingTypeUID(BINDING_ID, THING_TYPE_ID); + ThingUID thingUID = new ThingUID(thingTypeUID, "thingId"); Thing thing = ThingBuilder.create(thingTypeUID, thingUID) .withConfiguration(new Configuration(Map.of("parameter", "someValue"))).build(); @@ -658,12 +675,12 @@ public class BindingBaseClassesOSGiTest extends JavaOSGiTest { thingHandlerFactory.activate(componentContextMock); registerService(thingHandlerFactory, ThingHandlerFactory.class.getName()); - registerThingTypeAndConfigDescription(); + registerDefaultThingTypeAndConfigDescription(); ThingRegistry thingRegistry = getService(ThingRegistry.class); - ThingTypeUID thingTypeUID = new ThingTypeUID("bindingId:type"); - ThingUID thingUID = new ThingUID("bindingId:type:thingId"); + ThingTypeUID thingTypeUID = new ThingTypeUID(BINDING_ID, THING_TYPE_ID); + ThingUID thingUID = new ThingUID(thingTypeUID, "thingId"); Thing thing = ThingBuilder.create(thingTypeUID, thingUID).build(); managedThingProvider.add(thing); @@ -694,13 +711,17 @@ public class BindingBaseClassesOSGiTest extends JavaOSGiTest { thingHandlerFactory.activate(componentContextMock); registerService(thingHandlerFactory, ThingHandlerFactory.class.getName()); - ThingTypeUID thingType1 = new ThingTypeUID("bindingId:type1"); - ThingTypeUID thingType2 = new ThingTypeUID("bindingId:type2"); + ThingTypeUID thingTypeUID1 = new ThingTypeUID(BINDING_ID, "type1"); + ThingTypeUID thingTypeUID2 = new ThingTypeUID(BINDING_ID, "type2"); - Bridge bridge = BridgeBuilder.create(thingType1, new ThingUID("bindingId:type1:bridgeId")).build(); - Thing thingA = ThingBuilder.create(thingType2, new ThingUID("bindingId:type2:thingIdA")) + ThingType thingType1 = ThingTypeBuilder.instance(thingTypeUID1, thingTypeUID1.getId()).build(); + ThingType thingType2 = ThingTypeBuilder.instance(thingTypeUID2, thingTypeUID2.getId()).build(); + registerThingTypeProvider(thingType1, thingType2); + + Bridge bridge = BridgeBuilder.create(thingTypeUID1, new ThingUID(thingTypeUID1, "bridgeId")).build(); + Thing thingA = ThingBuilder.create(thingTypeUID2, new ThingUID(thingTypeUID2, "thingIdA")) .withBridge(bridge.getUID()).build(); - Thing thingB = ThingBuilder.create(thingType2, new ThingUID("bindingId:type2:thingIdB")) + Thing thingB = ThingBuilder.create(thingTypeUID2, new ThingUID(thingTypeUID2, "thingIdB")) .withBridge(bridge.getUID()).build(); assertThat(bridge.getStatus(), is(ThingStatus.UNINITIALIZED)); @@ -751,40 +772,35 @@ public class BindingBaseClassesOSGiTest extends JavaOSGiTest { return null; } - private void registerThingTypeAndConfigDescription() { - ThingType thingType = ThingTypeBuilder.instance(new ThingTypeUID(BINDING_ID, THING_TYPE_ID), "label") - .withConfigDescriptionURI(BINDING_CONFIG_URI).build(); + private void registerDefaultThingTypeAndConfigDescription() { + registerDefaultThingTypeProvider(); ConfigDescription configDescription = ConfigDescriptionBuilder.create(BINDING_CONFIG_URI) .withParameter(ConfigDescriptionParameterBuilder .create("parameter", ConfigDescriptionParameter.Type.TEXT).withRequired(true).build()) .build(); - ThingTypeProvider thingTypeProvider = mock(ThingTypeProvider.class); - when(thingTypeProvider.getThingType(ArgumentMatchers.any(ThingTypeUID.class), - ArgumentMatchers.any(Locale.class))).thenReturn(thingType); - registerService(thingTypeProvider); - - ThingTypeRegistry thingTypeRegistry = mock(ThingTypeRegistry.class); - when(thingTypeRegistry.getThingType(ArgumentMatchers.any(ThingTypeUID.class))).thenReturn(thingType); - registerService(thingTypeRegistry); - ConfigDescriptionProvider configDescriptionProvider = mock(ConfigDescriptionProvider.class); - when(configDescriptionProvider.getConfigDescription(ArgumentMatchers.any(URI.class), - ArgumentMatchers.nullable(Locale.class))).thenReturn(configDescription); + when(configDescriptionProvider.getConfigDescription(eq(BINDING_CONFIG_URI), nullable(Locale.class))) + .thenReturn(configDescription); registerService(configDescriptionProvider); } - private void registerThingTypeProvider() { + private void registerDefaultThingTypeProvider() { ThingType thingType = ThingTypeBuilder.instance(new ThingTypeUID(BINDING_ID, THING_TYPE_ID), "label") .withConfigDescriptionURI(BINDING_CONFIG_URI).build(); + registerThingTypeProvider(thingType); + } + private void registerThingTypeProvider(ThingType... thingTypes) { ThingTypeProvider thingTypeProvider = mock(ThingTypeProvider.class); - when(thingTypeProvider.getThingType(ArgumentMatchers.any(ThingTypeUID.class), - ArgumentMatchers.nullable(Locale.class))).thenReturn(thingType); - registerService(thingTypeProvider); - ThingTypeRegistry thingTypeRegistry = mock(ThingTypeRegistry.class); - when(thingTypeRegistry.getThingType(ArgumentMatchers.any(ThingTypeUID.class))).thenReturn(thingType); + + for (ThingType thingType : thingTypes) { + when(thingTypeProvider.getThingType(eq(thingType.getUID()), nullable(Locale.class))).thenReturn(thingType); + when(thingTypeRegistry.getThingType(eq(thingType.getUID()))).thenReturn(thingType); + } + + registerService(thingTypeProvider); registerService(thingTypeRegistry); } @@ -796,8 +812,8 @@ public class BindingBaseClassesOSGiTest extends JavaOSGiTest { .build(); ConfigDescriptionProvider configDescriptionProvider = mock(ConfigDescriptionProvider.class); - when(configDescriptionProvider.getConfigDescription(ArgumentMatchers.any(URI.class), - ArgumentMatchers.nullable(Locale.class))).thenReturn(configDescription); + when(configDescriptionProvider.getConfigDescription(ArgumentMatchers.any(URI.class), nullable(Locale.class))) + .thenReturn(configDescription); registerService(configDescriptionProvider); } } diff --git a/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/binding/ChangeThingTypeOSGiTest.java b/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/binding/ChangeThingTypeOSGiTest.java index 27775a8da..7588e8e9f 100644 --- a/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/binding/ChangeThingTypeOSGiTest.java +++ b/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/binding/ChangeThingTypeOSGiTest.java @@ -437,13 +437,17 @@ public class ChangeThingTypeOSGiTest extends JavaOSGiTest { } private List getChannelDefinitions(ThingTypeUID thingTypeUID) throws URISyntaxException { + URI configDescriptionUri = new URI("scheme", "channelType:" + thingTypeUID.getId(), null); ChannelTypeUID channelTypeUID = new ChannelTypeUID("test:" + thingTypeUID.getId()); ChannelType channelType = ChannelTypeBuilder.state(channelTypeUID, "channelLabel", "itemType") .withDescription("description") // .withCategory("category") // - .withConfigDescriptionURI(new URI("scheme", "channelType:" + thingTypeUID.getId(), null)).build(); + .withConfigDescriptionURI(configDescriptionUri).build(); channelTypes.put(channelTypeUID, channelType); + ConfigDescription configDescription = ConfigDescriptionBuilder.create(configDescriptionUri).build(); + configDescriptions.put(configDescriptionUri, configDescription); + ChannelDefinition cd = new ChannelDefinitionBuilder("channel" + thingTypeUID.getId(), channelTypeUID).build(); return List.of(cd); } diff --git a/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/i18n/ThingStatusInfoI18nLocalizationServiceOSGiTest.java b/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/i18n/ThingStatusInfoI18nLocalizationServiceOSGiTest.java index 9fd4dc536..db9bf0202 100644 --- a/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/i18n/ThingStatusInfoI18nLocalizationServiceOSGiTest.java +++ b/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/i18n/ThingStatusInfoI18nLocalizationServiceOSGiTest.java @@ -39,9 +39,13 @@ import org.openhab.core.thing.binding.BaseThingHandler; import org.openhab.core.thing.binding.BaseThingHandlerFactory; import org.openhab.core.thing.binding.ThingHandler; import org.openhab.core.thing.binding.ThingHandlerFactory; +import org.openhab.core.thing.binding.ThingTypeProvider; import org.openhab.core.thing.binding.builder.ThingBuilder; import org.openhab.core.thing.binding.builder.ThingStatusInfoBuilder; import org.openhab.core.thing.testutil.i18n.DefaultLocaleSetter; +import org.openhab.core.thing.type.ThingType; +import org.openhab.core.thing.type.ThingTypeBuilder; +import org.openhab.core.thing.type.ThingTypeRegistry; import org.openhab.core.types.Command; import org.openhab.core.util.BundleResolver; import org.osgi.framework.Bundle; @@ -88,6 +92,8 @@ public class ThingStatusInfoI18nLocalizationServiceOSGiTest extends JavaOSGiTest simpleThingHandlerFactory.activate(componentContext); registerService(simpleThingHandlerFactory, ThingHandlerFactory.class.getName()); + registerThingType(); + thing = ThingBuilder.create(new ThingTypeUID("aaa:bbb"), "ccc").build(); managedThingProvider = getService(ManagedThingProvider.class); @@ -330,4 +336,17 @@ public class ThingStatusInfoI18nLocalizationServiceOSGiTest extends JavaOSGiTest } } } + + private void registerThingType() { + ThingType thingType = ThingTypeBuilder.instance(new ThingTypeUID("aaa:bbb"), "label").build(); + + ThingTypeProvider thingTypeProvider = mock(ThingTypeProvider.class); + ThingTypeRegistry thingTypeRegistry = mock(ThingTypeRegistry.class); + + when(thingTypeProvider.getThingType(eq(thingType.getUID()), nullable(Locale.class))).thenReturn(thingType); + when(thingTypeRegistry.getThingType(eq(thingType.getUID()))).thenReturn(thingType); + + registerService(thingTypeProvider); + registerService(thingTypeRegistry); + } } diff --git a/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/internal/ChannelLinkNotifierOSGiTest.java b/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/internal/ChannelLinkNotifierOSGiTest.java index d3f64018c..6ab98da08 100644 --- a/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/internal/ChannelLinkNotifierOSGiTest.java +++ b/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/internal/ChannelLinkNotifierOSGiTest.java @@ -17,12 +17,15 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -38,6 +41,8 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; import org.openhab.core.common.registry.RegistryChangeListener; import org.openhab.core.events.Event; import org.openhab.core.events.EventSubscriber; @@ -59,6 +64,7 @@ import org.openhab.core.thing.ThingTypeUID; import org.openhab.core.thing.ThingUID; import org.openhab.core.thing.binding.BaseThingHandler; import org.openhab.core.thing.binding.ThingHandlerFactory; +import org.openhab.core.thing.binding.ThingTypeProvider; import org.openhab.core.thing.binding.builder.ChannelBuilder; import org.openhab.core.thing.binding.builder.ThingBuilder; import org.openhab.core.thing.events.AbstractThingRegistryEvent; @@ -71,7 +77,14 @@ import org.openhab.core.thing.link.dto.ItemChannelLinkDTO; import org.openhab.core.thing.link.events.AbstractItemChannelLinkRegistryEvent; import org.openhab.core.thing.link.events.ItemChannelLinkRemovedEvent; import org.openhab.core.thing.type.ChannelKind; +import org.openhab.core.thing.type.ChannelType; +import org.openhab.core.thing.type.ChannelTypeBuilder; +import org.openhab.core.thing.type.ChannelTypeProvider; +import org.openhab.core.thing.type.ChannelTypeRegistry; import org.openhab.core.thing.type.ChannelTypeUID; +import org.openhab.core.thing.type.ThingType; +import org.openhab.core.thing.type.ThingTypeBuilder; +import org.openhab.core.thing.type.ThingTypeRegistry; import org.openhab.core.thing.util.ThingHandlerHelper; import org.openhab.core.types.Command; import org.openhab.core.util.BundleResolver; @@ -85,6 +98,7 @@ import org.slf4j.LoggerFactory; * @author Wouter Born - Initial contribution */ @ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) @NonNullByDefault public class ChannelLinkNotifierOSGiTest extends JavaOSGiTest { @@ -162,6 +176,7 @@ public class ChannelLinkNotifierOSGiTest extends JavaOSGiTest { @BeforeEach public void beforeEach() { registerVolatileStorageService(); + registerThingAndChannelTypeProvider(); itemChannelLinkRegistry = getService(ItemChannelLinkRegistry.class); assertThat(itemChannelLinkRegistry, is(notNullValue())); @@ -184,14 +199,8 @@ public class ChannelLinkNotifierOSGiTest extends JavaOSGiTest { when(thingHandlerFactoryMock.supportsThingType(eq(THING_TYPE_UID))).thenReturn(true); registerService(thingHandlerFactoryMock); - when(bundleMock.getSymbolicName()).thenReturn("org.openhab.core.thing"); - when(bundleResolverMock.resolveBundle(any())).thenReturn(bundleMock); - ThingManagerImpl thingManager = (ThingManagerImpl) getService(ThingManager.class); assertThat(thingManager, is(notNullValue())); - if (thingManager != null) { - thingManager.setBundleResolver(bundleResolverMock); - } } @AfterEach @@ -560,4 +569,27 @@ public class ChannelLinkNotifierOSGiTest extends JavaOSGiTest { assertNoChannelLinkEventsReceived(subjectThing); assertNoChannelLinkEventsReceived(otherThing); } + + private void registerThingAndChannelTypeProvider() { + ThingType thingType = ThingTypeBuilder.instance(THING_TYPE_UID, "label").build(); + + ThingTypeProvider thingTypeProvider = mock(ThingTypeProvider.class); + when(thingTypeProvider.getThingType(any(ThingTypeUID.class), nullable(Locale.class))).thenReturn(thingType); + registerService(thingTypeProvider); + + ThingTypeRegistry thingTypeRegistry = mock(ThingTypeRegistry.class); + when(thingTypeRegistry.getThingType(any(ThingTypeUID.class))).thenReturn(thingType); + registerService(thingTypeRegistry); + + ChannelType channelType = ChannelTypeBuilder.state(CHANNEL_TYPE_UID, "Number", "Number").build(); + + ChannelTypeProvider channelTypeProvider = mock(ChannelTypeProvider.class); + when(channelTypeProvider.getChannelType(any(ChannelTypeUID.class), nullable(Locale.class))) + .thenReturn(channelType); + registerService(channelTypeProvider); + + ChannelTypeRegistry channelTypeRegistry = mock(ChannelTypeRegistry.class); + when(channelTypeRegistry.getChannelType(any(ChannelTypeUID.class))).thenReturn(channelType); + registerService(channelTypeRegistry); + } } diff --git a/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/internal/ThingManagerOSGiJavaTest.java b/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/internal/ThingManagerOSGiJavaTest.java index 3b7706dae..7aafa59c9 100644 --- a/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/internal/ThingManagerOSGiJavaTest.java +++ b/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/internal/ThingManagerOSGiJavaTest.java @@ -22,6 +22,7 @@ import static org.mockito.Mockito.*; import java.net.URI; import java.util.Collections; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.concurrent.Semaphore; @@ -112,6 +113,8 @@ public class ThingManagerOSGiJavaTest extends JavaOSGiTest { CHANNEL_GROUP_ID); private static final ThingTypeUID THING_TYPE_UID = new ThingTypeUID("binding:thing"); + private static final ThingTypeUID THING_WITH_BRIDGE_TYPE_UID = new ThingTypeUID("binding:thingWithBridge"); + private static final ThingUID THING_UID = new ThingUID(THING_TYPE_UID, "thing"); private static final ThingTypeUID BRIDGE_TYPE_UID = new ThingTypeUID("binding:bridge"); @@ -127,6 +130,7 @@ public class ThingManagerOSGiJavaTest extends JavaOSGiTest { private @NonNullByDefault({}) ThingRegistry thingRegistry; private @NonNullByDefault({}) ReadyService readyService; private @NonNullByDefault({}) Storage storage; + private @NonNullByDefault({}) ThingManager thingManager; private @NonNullByDefault({}) URI configDescriptionChannel; @@ -137,6 +141,12 @@ public class ThingManagerOSGiJavaTest extends JavaOSGiTest { public void setUp() throws Exception { configDescriptionChannel = new URI("test:channel"); configDescriptionThing = new URI("test:test"); + + registerThingTypeProvider(); + registerChannelTypeProvider(); + registerChannelGroupTypeProvider(); + registerConfigDescriptions(); + thing = ThingBuilder.create(THING_TYPE_UID, THING_UID).withChannels(List.of( // ChannelBuilder.create(CHANNEL_UID, CoreItemFactory.SWITCH).withLabel("Test Label") .withDescription("Test Description").withType(CHANNEL_TYPE_UID) @@ -187,8 +197,56 @@ public class ThingManagerOSGiJavaTest extends JavaOSGiTest { } @Test - public void testInitializeCallsThingUpdated() throws Exception { - registerThingTypeProvider(); + public void testMissingBridgePreventsInitialization() { + registerThingHandlerFactory(THING_WITH_BRIDGE_TYPE_UID, thing -> new BaseThingHandler(thing) { + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + } + + @Override + public void initialize() { + updateStatus(ThingStatus.ONLINE); + } + }); + + Thing thing = ThingBuilder.create(THING_WITH_BRIDGE_TYPE_UID, THING_UID).build(); + managedThingProvider.add(thing); + + waitForAssert(() -> { + assertEquals(ThingStatus.UNINITIALIZED, thing.getStatus()); + assertEquals(ThingStatusDetail.HANDLER_CONFIGURATION_PENDING, thing.getStatusInfo().getStatusDetail(), + thing.getStatusInfo().toString()); + }); + + managedThingProvider.remove(thing.getUID()); + } + + @Test + public void testExistingBridgeAllowsInitialization() { + registerThingHandlerFactory(THING_WITH_BRIDGE_TYPE_UID, thing -> new BaseThingHandler(thing) { + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + } + + @Override + public void initialize() { + updateStatus(ThingStatus.ONLINE); + } + }); + + Thing thing = ThingBuilder.create(THING_WITH_BRIDGE_TYPE_UID, THING_UID).withBridge(BRIDGE_UID).build(); + managedThingProvider.add(thing); + + waitForAssert(() -> { + assertEquals(ThingStatus.UNINITIALIZED, thing.getStatus()); + assertEquals(ThingStatusDetail.BRIDGE_UNINITIALIZED, thing.getStatusInfo().getStatusDetail()); + }); + + managedThingProvider.remove(thing.getUID()); + } + + @Test + public void testInitializeCallsThingUpdated() { AtomicReference thc = new AtomicReference<>(); AtomicReference initializeRunning = new AtomicReference<>(false); registerThingHandlerFactory(THING_TYPE_UID, thing -> { @@ -325,9 +383,7 @@ public class ThingManagerOSGiJavaTest extends JavaOSGiTest { } @Test - public void testInitializeOnlyIfInitializable() throws Exception { - registerThingTypeProvider(); - registerChannelTypeProvider(); + public void testInitializeOnlyIfInitializable() { registerThingHandlerFactory(THING_TYPE_UID, thing -> new BaseThingHandler(thing) { @Override public void handleCommand(ChannelUID channelUID, Command command) { @@ -563,9 +619,7 @@ public class ThingManagerOSGiJavaTest extends JavaOSGiTest { } @Test - public void testSetEnabledWithHandler() throws Exception { - registerThingTypeProvider(); - + public void testSetEnabledWithHandler() { AtomicReference thingHandlerCallback = new AtomicReference<>(); AtomicReference initializeInvoked = new AtomicReference<>(false); AtomicReference disposeInvoked = new AtomicReference<>(false); @@ -640,8 +694,6 @@ public class ThingManagerOSGiJavaTest extends JavaOSGiTest { @Test public void testSetEnabledWithoutHandlerFactory() throws Exception { - registerThingTypeProvider(); - ThingStatusInfo thingStatusInfo = ThingStatusInfoBuilder .create(ThingStatus.UNINITIALIZED, ThingStatusDetail.NONE).build(); thing.setStatusInfo(thingStatusInfo); @@ -732,9 +784,7 @@ public class ThingManagerOSGiJavaTest extends JavaOSGiTest { } @Test - public void testDisposeNotInvokedOnAlreadyDisabledThing() throws Exception { - registerThingTypeProvider(); - + public void testDisposeNotInvokedOnAlreadyDisabledThing() { AtomicReference thingHandlerCallback = new AtomicReference<>(); AtomicReference initializeInvoked = new AtomicReference<>(false); AtomicReference disposeInvoked = new AtomicReference<>(false); @@ -809,9 +859,7 @@ public class ThingManagerOSGiJavaTest extends JavaOSGiTest { } @Test - public void testUpdateThing() throws Exception { - registerThingTypeProvider(); - + public void testUpdateThing() { AtomicReference thingHandlerCallback = new AtomicReference<>(); AtomicReference initializeInvoked = new AtomicReference<>(false); AtomicReference disposeInvoked = new AtomicReference<>(false); @@ -897,9 +945,7 @@ public class ThingManagerOSGiJavaTest extends JavaOSGiTest { } @Test - public void testStorageEntryRetainedOnThingRemoval() throws Exception { - registerThingTypeProvider(); - + public void testStorageEntryRetainedOnThingRemoval() { AtomicReference thingHandlerCallback = new AtomicReference<>(); AtomicReference initializeInvoked = new AtomicReference<>(false); AtomicReference disposeInvoked = new AtomicReference<>(false); @@ -1002,28 +1048,37 @@ public class ThingManagerOSGiJavaTest extends JavaOSGiTest { registerService(mockThingHandlerFactory, ThingHandlerFactory.class.getName()); } - private void registerThingTypeProvider() throws Exception { + private void registerThingTypeProvider() { ThingType thingType = ThingTypeBuilder.instance(THING_TYPE_UID, "label") .withConfigDescriptionURI(configDescriptionThing) .withChannelDefinitions(List.of(new ChannelDefinitionBuilder(CHANNEL_ID, CHANNEL_TYPE_UID).build())) .build(); + ThingType thingTypeWithBridge = ThingTypeBuilder.instance(THING_WITH_BRIDGE_TYPE_UID, "label") + .withConfigDescriptionURI(configDescriptionThing) + .withSupportedBridgeTypeUIDs(List.of(BRIDGE_TYPE_UID.getId())) + .withChannelDefinitions(List.of(new ChannelDefinitionBuilder(CHANNEL_ID, CHANNEL_TYPE_UID).build())) + .build(); + ThingType bridgeType = ThingTypeBuilder.instance(BRIDGE_TYPE_UID, "label").buildBridge(); ThingTypeProvider mockThingTypeProvider = mock(ThingTypeProvider.class); when(mockThingTypeProvider.getThingType(eq(THING_TYPE_UID), any())).thenReturn(thingType); + when(mockThingTypeProvider.getThingType(eq(THING_WITH_BRIDGE_TYPE_UID), any())).thenReturn(thingTypeWithBridge); + when(mockThingTypeProvider.getThingType(eq(BRIDGE_TYPE_UID), any())).thenReturn(bridgeType); + registerService(mockThingTypeProvider); } - private void registerChannelTypeProvider() throws Exception { + private void registerChannelTypeProvider() { ChannelType channelType = ChannelTypeBuilder.state(CHANNEL_TYPE_UID, "Test Label", CoreItemFactory.SWITCH) .withDescription("Test Description").withCategory("Test Category").withTag("Test Tag") - .withConfigDescriptionURI(new URI("test:channel")).build(); + .withConfigDescriptionURI(URI.create("test:channel")).build(); ChannelTypeProvider mockChannelTypeProvider = mock(ChannelTypeProvider.class); when(mockChannelTypeProvider.getChannelType(eq(CHANNEL_TYPE_UID), any())).thenReturn(channelType); registerService(mockChannelTypeProvider); } - private void registerChannelGroupTypeProvider() throws Exception { + private void registerChannelGroupTypeProvider() { ChannelGroupType channelGroupType = ChannelGroupTypeBuilder.instance(CHANNEL_GROUP_TYPE_UID, "Test Group Label") .withDescription("Test Group Description").withCategory("Test Group Category") .withChannelDefinitions(List.of(new ChannelDefinitionBuilder(CHANNEL_ID, CHANNEL_TYPE_UID).build(), @@ -1064,9 +1119,6 @@ public class ThingManagerOSGiJavaTest extends JavaOSGiTest { }; private AtomicReference initializeThingHandlerCallback() throws Exception { - registerThingTypeProvider(); - registerChannelTypeProvider(); - registerChannelGroupTypeProvider(); AtomicReference thc = new AtomicReference<>(); ThingHandlerFactory thingHandlerFactory = new BaseThingHandlerFactory() { @Override @@ -1093,4 +1145,15 @@ public class ThingManagerOSGiJavaTest extends JavaOSGiTest { }); return thc; } + + private void registerConfigDescriptions() { + ConfigDescriptionProvider configDescriptionProvider = mock(ConfigDescriptionProvider.class); + + when(configDescriptionProvider.getConfigDescription(eq(configDescriptionThing), nullable(Locale.class))) + .thenReturn(ConfigDescriptionBuilder.create(configDescriptionThing).build()); + when(configDescriptionProvider.getConfigDescription(eq(configDescriptionChannel), nullable(Locale.class))) + .thenReturn(ConfigDescriptionBuilder.create(configDescriptionChannel).build()); + + registerService(configDescriptionProvider); + } } diff --git a/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/internal/ThingManagerOSGiTest.java b/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/internal/ThingManagerOSGiTest.java index 86ba15467..3d20bd10e 100644 --- a/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/internal/ThingManagerOSGiTest.java +++ b/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/internal/ThingManagerOSGiTest.java @@ -58,8 +58,8 @@ import org.openhab.core.library.items.StringItem; import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.StringType; import org.openhab.core.service.ReadyMarker; -import org.openhab.core.service.ReadyMarkerUtils; import org.openhab.core.service.ReadyService; +import org.openhab.core.service.StartLevelService; import org.openhab.core.test.java.JavaOSGiTest; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.ChannelUID; @@ -100,9 +100,6 @@ import org.openhab.core.thing.type.ThingTypeBuilder; import org.openhab.core.thing.type.ThingTypeRegistry; import org.openhab.core.types.Command; import org.openhab.core.util.BundleResolver; -import org.osgi.framework.Bundle; -import org.osgi.framework.FrameworkUtil; -import org.osgi.framework.InvalidSyntaxException; import org.osgi.service.cm.ConfigurationAdmin; import org.osgi.service.component.ComponentContext; @@ -169,27 +166,8 @@ public class ThingManagerOSGiTest extends JavaOSGiTest { readyService = getService(ReadyService.class); assertNotNull(readyService); - waitForAssert(() -> { - try { - assertThat( - bundleContext - .getServiceReferences(ReadyMarker.class, - "(" + ThingManagerImpl.XML_THING_TYPE + "=" - + bundleContext.getBundle().getSymbolicName() + ")"), - is(notNullValue())); - } catch (InvalidSyntaxException e) { - fail("Failed to get service reference: " + e.getMessage()); - } - }); - - Bundle bundle = mock(Bundle.class); - when(bundle.getSymbolicName()).thenReturn("org.openhab.core.thing"); - - BundleResolver bundleResolver = mock(BundleResolver.class); - when(bundleResolver.resolveBundle(any())).thenReturn(bundle); - ThingManagerImpl thingManager = (ThingManagerImpl) getService(ThingTypeMigrationService.class); - thingManager.setBundleResolver(bundleResolver); + assertNotNull(thingManager); } @AfterEach @@ -199,6 +177,33 @@ public class ThingManagerOSGiTest extends JavaOSGiTest { when(componentContext.getProperties()).thenReturn(new Hashtable<>()); } + @Test + public void thingManagerHonorsMissingPrerequisite() { + // set ready marker, otherwise check job is not running + ThingManagerImpl thingManager = (ThingManagerImpl) getService(ThingManager.class); + thingManager.onReadyMarkerAdded(new ReadyMarker(StartLevelService.STARTLEVEL_MARKER_TYPE, + Integer.toString(StartLevelService.STARTLEVEL_MODEL))); + + ThingHandler thingHandler = mock(ThingHandler.class); + when(thingHandler.getThing()).thenReturn(thing); + + ThingHandlerFactory thingHandlerFactory = mock(ThingHandlerFactory.class); + when(thingHandlerFactory.supportsThingType(any(ThingTypeUID.class))).thenReturn(true); + when(thingHandlerFactory.registerHandler(any(Thing.class))).thenReturn(thingHandler); + + registerService(thingHandlerFactory); + + managedThingProvider.add(thing); + + assertThat(thing.getStatus(), is(ThingStatus.UNINITIALIZED)); + assertThat(thing.getStatusInfo().getStatusDetail(), is(ThingStatusDetail.NOT_YET_READY)); + + registerThingTypeProvider(); + registerConfigDescriptionProvider(false); + + waitForAssert(() -> assertThat(thing.getStatus(), is(ThingStatus.INITIALIZING))); + } + @Test @SuppressWarnings("null") public void thingManagerChangesTheThingType() { @@ -231,6 +236,7 @@ public class ThingManagerOSGiTest extends JavaOSGiTest { @Test public void thingManagerChangesTheThingTypeCorrectlyEvenIfInitializeTakesLongAndCalledFromThere() { registerThingTypeProvider(); + registerConfigDescriptionProvider(false); ThingTypeUID newThingTypeUID = new ThingTypeUID("binding:type2"); @@ -317,6 +323,7 @@ public class ThingManagerOSGiTest extends JavaOSGiTest { @Test public void thingManagerWaitsWithThingUpdatedUntilInitializeReturned() { registerThingTypeProvider(); + registerConfigDescriptionProvider(false); Thing thing2 = ThingBuilder.create(THING_TYPE_UID, THING_UID) .withChannels(List.of(ChannelBuilder.create(CHANNEL_UID, CoreItemFactory.SWITCH).build())).build(); @@ -379,6 +386,8 @@ public class ThingManagerOSGiTest extends JavaOSGiTest { when(thingHandlerFactory.registerHandler(any(Thing.class))).thenReturn(thingHandler); registerService(thingHandlerFactory); + registerThingTypeProvider(); + registerConfigDescriptionProvider(false); managedThingProvider.add(thing); @@ -395,6 +404,8 @@ public class ThingManagerOSGiTest extends JavaOSGiTest { when(thingHandlerFactory.registerHandler(any(Thing.class))).thenReturn(thingHandler); registerService(thingHandlerFactory); + registerThingTypeProvider(); + registerConfigDescriptionProvider(false); managedThingProvider.add(thing); managedThingProvider.remove(thing.getUID()); @@ -442,6 +453,9 @@ public class ThingManagerOSGiTest extends JavaOSGiTest { .create(ThingStatus.UNINITIALIZED, ThingStatusDetail.NONE).build(); assertThat(thing.getStatusInfo(), is(uninitializedNone)); + registerThingTypeProvider(); + registerConfigDescriptionProvider(false); + // add thing - provokes handler registration & initialization managedThingProvider.add(thing); waitForAssert(() -> verify(thingHandlerFactory, times(1)).registerHandler(thing)); @@ -521,6 +535,9 @@ public class ThingManagerOSGiTest extends JavaOSGiTest { .create(ThingStatus.UNINITIALIZED, ThingStatusDetail.NONE).build(); assertThat(testThing.getStatusInfo(), is(uninitializedNone)); + registerThingTypeProvider(); + registerConfigDescriptionProvider(false); + managedThingProvider.add(testThing); final ThingStatusInfo uninitializedError = ThingStatusInfoBuilder .create(ThingStatus.UNINITIALIZED, ThingStatusDetail.HANDLER_INITIALIZING_ERROR) @@ -684,6 +701,9 @@ public class ThingManagerOSGiTest extends JavaOSGiTest { ThingRegistry thingRegistry = getService(ThingRegistry.class); assertThat(thingRegistry, not(nullValue())); + registerThingTypeProvider(); + registerConfigDescriptionProvider(false); + // add thing - no thing initialization, because bridge is not available thingRegistry.add(thing); waitForAssert(() -> assertThat(thingState.initCalled, is(false))); @@ -767,6 +787,7 @@ public class ThingManagerOSGiTest extends JavaOSGiTest { @Test public void thingManagerDoesNotDelegateUpdateEventsToItsSource() { registerThingTypeProvider(); + registerConfigDescriptionProvider(false); class ThingHandlerState { boolean handleCommandWasCalled; @@ -829,6 +850,7 @@ public class ThingManagerOSGiTest extends JavaOSGiTest { @Test public void thingManagerHandlesStateUpdatesCorrectly() { registerThingTypeProvider(); + registerConfigDescriptionProvider(false); class ThingHandlerState { boolean thingUpdatedWasCalled; @@ -931,6 +953,9 @@ public class ThingManagerOSGiTest extends JavaOSGiTest { ThingHandlerCallback callback; } + registerThingTypeProvider(); + registerConfigDescriptionProvider(false); + final ThingHandlerState state = new ThingHandlerState(); ThingHandler thingHandler = mock(ThingHandler.class); @@ -1000,6 +1025,8 @@ public class ThingManagerOSGiTest extends JavaOSGiTest { final ThingHandlerState state = new ThingHandlerState(); ThingHandler thingHandler = mock(ThingHandler.class); + registerThingTypeProvider(); + registerConfigDescriptionProvider(false); managedThingProvider.add(thing); @@ -1048,6 +1075,7 @@ public class ThingManagerOSGiTest extends JavaOSGiTest { @Test public void thingManagerHandlesThingStatusUpdatesUninitializedAndInitializingCorrectly() { registerThingTypeProvider(); + registerConfigDescriptionProvider(false); ThingHandler thingHandler = mock(ThingHandler.class); when(thingHandler.getThing()).thenReturn(thing); @@ -1078,6 +1106,7 @@ public class ThingManagerOSGiTest extends JavaOSGiTest { @Test public void thingManagerHandlesThingStatusUpdatesRemovingAndInitializingCorrectly() { registerThingTypeProvider(); + registerConfigDescriptionProvider(false); ThingHandler thingHandler = mock(ThingHandler.class); when(thingHandler.getThing()).thenReturn(thing); @@ -1111,6 +1140,8 @@ public class ThingManagerOSGiTest extends JavaOSGiTest { when(thingHandlerFactory.registerHandler(any(Thing.class))).thenThrow(new RuntimeException(exceptionMessage)); registerService(thingHandlerFactory); + registerThingTypeProvider(); + registerConfigDescriptionProvider(false); managedThingProvider.add(thing); @@ -1124,6 +1155,8 @@ public class ThingManagerOSGiTest extends JavaOSGiTest { @SuppressWarnings({ "null", "unchecked" }) public void thingManagerHandlesThingUpdatesCorrectly() { String itemName = "name"; + registerThingTypeProvider(); + registerConfigDescriptionProvider(false); managedThingProvider.add(thing); managedItemChannelLinkProvider.add(new ItemChannelLink(itemName, CHANNEL_UID)); @@ -1196,6 +1229,7 @@ public class ThingManagerOSGiTest extends JavaOSGiTest { @Test public void thingManagerPostsThingStatusEventsIfTheStatusOfAThingIsUpdated() { registerThingTypeProvider(); + registerConfigDescriptionProvider(false); class ThingHandlerState { @Nullable @@ -1283,6 +1317,7 @@ public class ThingManagerOSGiTest extends JavaOSGiTest { @Test public void thingManagerPostsThingStatusChangedEventsIfTheStatusOfAThingIsChanged() throws Exception { registerThingTypeProvider(); + registerConfigDescriptionProvider(false); class ThingHandlerState { @Nullable @@ -1355,6 +1390,7 @@ public class ThingManagerOSGiTest extends JavaOSGiTest { @SuppressWarnings("null") public void thingManagerPostsLocalizedThingStatusInfoAndThingStatusInfoChangedEvents() throws Exception { registerThingTypeProvider(); + registerConfigDescriptionProvider(false); class ThingHandlerState { @Nullable @@ -1573,64 +1609,6 @@ public class ThingManagerOSGiTest extends JavaOSGiTest { }); } - @Test - @SuppressWarnings("null") - public void thingManagerWaitsWithInitializeUntilBundleProcessingIsFinished() throws Exception { - Thing thing = ThingBuilder.create(THING_TYPE_UID, THING_UID).build(); - - class ThingHandlerState { - @SuppressWarnings("unused") - @Nullable - ThingHandlerCallback callback; - } - final ThingHandlerState state = new ThingHandlerState(); - - ThingHandler thingHandler = mock(ThingHandler.class); - doAnswer(new Answer() { - @Override - public @Nullable Void answer(InvocationOnMock invocation) throws Throwable { - state.callback = (ThingHandlerCallback) invocation.getArgument(0); - return null; - } - }).when(thingHandler).setCallback(any(ThingHandlerCallback.class)); - when(thingHandler.getThing()).thenReturn(thing); - - ThingHandlerFactory thingHandlerFactory = mock(ThingHandlerFactory.class); - when(thingHandlerFactory.supportsThingType(any(ThingTypeUID.class))).thenReturn(true); - when(thingHandlerFactory.registerHandler(any(Thing.class))).thenReturn(thingHandler); - - registerService(thingHandlerFactory); - - final ReadyMarker marker = new ReadyMarker(ThingManagerImpl.XML_THING_TYPE, - ReadyMarkerUtils.getIdentifier(FrameworkUtil.getBundle(this.getClass()))); - waitForAssert(() -> { - // wait for the XML processing to be finished, then remove the ready marker again - assertThat(readyService.isReady(marker), is(true)); - readyService.unmarkReady(marker); - }); - - ThingStatusInfo uninitializedNone = ThingStatusInfoBuilder - .create(ThingStatus.UNINITIALIZED, ThingStatusDetail.NONE).build(); - assertThat(thing.getStatusInfo(), is(uninitializedNone)); - - managedThingProvider.add(thing); - - // just wait a little to make sure really nothing happens - Thread.sleep(1000); - verify(thingHandler, never()).initialize(); - assertThat(thing.getStatusInfo(), is(uninitializedNone)); - - readyService.markReady(marker); - - // ThingHandler.initialize() called, thing status is INITIALIZING.NONE - ThingStatusInfo initializingNone = ThingStatusInfoBuilder - .create(ThingStatus.INITIALIZING, ThingStatusDetail.NONE).build(); - waitForAssert(() -> { - verify(thingHandler, times(1)).initialize(); - assertThat(thing.getStatusInfo(), is(initializingNone)); - }); - } - @Test @SuppressWarnings("null") public void thingManagerCallsBridgeStatusChangedOnThingHandlerCorrectly() { @@ -1719,6 +1697,8 @@ public class ThingManagerOSGiTest extends JavaOSGiTest { }).when(thingHandlerFactory).registerHandler(any(Thing.class)); registerService(thingHandlerFactory); + registerThingTypeProvider(); + registerConfigDescriptionProvider(false); managedThingProvider.add(bridge); managedThingProvider.add(thing); @@ -1859,6 +1839,8 @@ public class ThingManagerOSGiTest extends JavaOSGiTest { } }).when(thingHandlerFactory).registerHandler(any(Thing.class)); + registerThingTypeProvider(); + registerConfigDescriptionProvider(false); registerService(thingHandlerFactory); managedThingProvider.add(bridge); @@ -1985,6 +1967,8 @@ public class ThingManagerOSGiTest extends JavaOSGiTest { } }).when(thingHandlerFactory).registerHandler(any(Thing.class)); + registerThingTypeProvider(); + registerConfigDescriptionProvider(false); registerService(thingHandlerFactory); managedThingProvider.add(bridge); @@ -2016,6 +2000,9 @@ public class ThingManagerOSGiTest extends JavaOSGiTest { } final ThingHandlerState state = new ThingHandlerState(); + registerThingTypeProvider(); + registerConfigDescriptionProvider(false); + managedThingProvider.add(thing); ThingHandler thingHandler = mock(ThingHandler.class); diff --git a/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/internal/ThingRegistryOSGiTest.java b/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/internal/ThingRegistryOSGiTest.java index 1380edb4d..0d9bc7789 100644 --- a/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/internal/ThingRegistryOSGiTest.java +++ b/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/internal/ThingRegistryOSGiTest.java @@ -16,8 +16,13 @@ import static java.util.Map.entry; import static org.hamcrest.CoreMatchers.*; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import java.util.Collection; +import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; @@ -44,10 +49,14 @@ import org.openhab.core.thing.binding.BaseThingHandler; import org.openhab.core.thing.binding.BaseThingHandlerFactory; import org.openhab.core.thing.binding.ThingHandler; import org.openhab.core.thing.binding.ThingHandlerFactory; +import org.openhab.core.thing.binding.ThingTypeProvider; import org.openhab.core.thing.binding.builder.ThingBuilder; import org.openhab.core.thing.events.ThingAddedEvent; import org.openhab.core.thing.events.ThingRemovedEvent; import org.openhab.core.thing.events.ThingUpdatedEvent; +import org.openhab.core.thing.type.ThingType; +import org.openhab.core.thing.type.ThingTypeBuilder; +import org.openhab.core.thing.type.ThingTypeRegistry; import org.openhab.core.types.Command; import org.osgi.framework.ServiceRegistration; @@ -76,6 +85,7 @@ public class ThingRegistryOSGiTest extends JavaOSGiTest { public void setUp() { registerVolatileStorageService(); managedThingProvider = getService(ManagedThingProvider.class); + registerThingTypeProvider(); unregisterCurrentThingHandlerFactory(); } @@ -246,4 +256,16 @@ public class ThingRegistryOSGiTest extends JavaOSGiTest { thingHandlerFactoryServiceReg = null; } } + + private void registerThingTypeProvider() { + ThingType thingType = ThingTypeBuilder.instance(THING_TYPE_UID, "label").build(); + + ThingTypeProvider thingTypeProvider = mock(ThingTypeProvider.class); + when(thingTypeProvider.getThingType(any(ThingTypeUID.class), nullable(Locale.class))).thenReturn(thingType); + registerService(thingTypeProvider); + + ThingTypeRegistry thingTypeRegistry = mock(ThingTypeRegistry.class); + when(thingTypeRegistry.getThingType(any(ThingTypeUID.class))).thenReturn(thingType); + registerService(thingTypeRegistry); + } } diff --git a/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/internal/update/ThingUpdateOSGiTest.java b/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/internal/update/ThingUpdateOSGiTest.java new file mode 100644 index 000000000..db53ab40e --- /dev/null +++ b/itests/org.openhab.core.thing.tests/src/main/java/org/openhab/core/thing/internal/update/ThingUpdateOSGiTest.java @@ -0,0 +1,384 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.thing.internal.update; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.*; +import static org.openhab.core.thing.internal.ThingManagerImpl.PROPERTY_THING_TYPE_VERSION; + +import java.util.Hashtable; +import java.util.Locale; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.service.ReadyService; +import org.openhab.core.test.SyntheticBundleInstaller; +import org.openhab.core.test.java.JavaOSGiTest; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.ManagedThingProvider; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingRegistry; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.BaseThingHandler; +import org.openhab.core.thing.binding.BaseThingHandlerFactory; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerFactory; +import org.openhab.core.thing.binding.ThingTypeProvider; +import org.openhab.core.thing.binding.builder.ChannelBuilder; +import org.openhab.core.thing.binding.builder.ThingBuilder; +import org.openhab.core.thing.type.ChannelType; +import org.openhab.core.thing.type.ChannelTypeBuilder; +import org.openhab.core.thing.type.ChannelTypeProvider; +import org.openhab.core.thing.type.ChannelTypeRegistry; +import org.openhab.core.thing.type.ChannelTypeUID; +import org.openhab.core.thing.type.ThingType; +import org.openhab.core.thing.type.ThingTypeBuilder; +import org.openhab.core.thing.type.ThingTypeRegistry; +import org.openhab.core.types.Command; +import org.openhab.core.util.BundleResolver; +import org.osgi.framework.Bundle; +import org.osgi.framework.Constants; +import org.osgi.framework.FrameworkUtil; +import org.osgi.service.component.ComponentContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Tests for {@link ThingUpdateInstructionReader} and {@link ThingUpdateInstruction} implementations. + * + * @author Jan N. Klug - Initial contribution + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@NonNullByDefault +public class ThingUpdateOSGiTest extends JavaOSGiTest { + + private static final String TEST_BUNDLE_NAME = "thingUpdateTest.bundle"; + private static final String BINDING_ID = "testBinding"; + private static final ThingTypeUID ADD_CHANNEL_THING_TYPE_UID = new ThingTypeUID(BINDING_ID, "testThingTypeAdd"); + private static final ThingTypeUID UPDATE_CHANNEL_THING_TYPE_UID = new ThingTypeUID(BINDING_ID, + "testThingTypeUpdate"); + private static final ThingTypeUID REMOVE_CHANNEL_THING_TYPE_UID = new ThingTypeUID(BINDING_ID, + "testThingTypeRemove"); + private static final ThingTypeUID MULTIPLE_CHANNEL_THING_TYPE_UID = new ThingTypeUID(BINDING_ID, + "testThingTypeMultiple"); + + private static final String[] ADDED_TAGS = { "Tag1", "Tag2" }; + + private static final String THING_ID = "thing"; + + private @NonNullByDefault({}) Bundle testBundle; + private @NonNullByDefault({}) BundleResolver bundleResolver; + private @NonNullByDefault({}) ReadyService readyService; + private @NonNullByDefault({}) ThingRegistry thingRegistry; + private @NonNullByDefault({}) ManagedThingProvider managedThingProvider; + private @NonNullByDefault({}) TestThingHandlerFactory thingHandlerFactory; + + @BeforeEach + public void beforeEach() throws Exception { + Hashtable properties = new Hashtable<>(); + properties.put(Constants.SERVICE_RANKING, Integer.MAX_VALUE); + bundleResolver = new BundleResolverImpl(); + registerService(bundleResolver, BundleResolver.class.getName(), properties); + + registerVolatileStorageService(); + + testBundle = SyntheticBundleInstaller.install(bundleContext, TEST_BUNDLE_NAME, "*.xml"); + assertThat(testBundle, is(notNullValue())); + + readyService = getService(ReadyService.class); + assertThat(readyService, is(notNullValue())); + + thingRegistry = getService(ThingRegistry.class); + assertThat(thingRegistry, is(notNullValue())); + + managedThingProvider = getService(ManagedThingProvider.class); + assertThat(managedThingProvider, is(notNullValue())); + + thingHandlerFactory = new TestThingHandlerFactory(); + registerService(thingHandlerFactory, ThingHandlerFactory.class.getName()); + } + + @AfterEach + public void afterEach() throws Exception { + testBundle.uninstall(); + managedThingProvider.getAll().forEach(t -> managedThingProvider.remove(t.getUID())); + } + + @Test + public void testSingleChannelAddition() { + registerThingType(ADD_CHANNEL_THING_TYPE_UID); + + ChannelTypeUID channelTypeUID = new ChannelTypeUID(BINDING_ID, "testChannelTypeId"); + registerChannelTypes(channelTypeUID); + + ThingUID thingUID = new ThingUID(ADD_CHANNEL_THING_TYPE_UID, THING_ID); + Thing thing = ThingBuilder.create(ADD_CHANNEL_THING_TYPE_UID, thingUID).build(); + managedThingProvider.add(thing); + + Thing updatedThing = assertThing(thing, 1); + + assertThat(updatedThing.getChannels(), hasSize(3)); + + Channel channel1 = updatedThing.getChannel("testChannel1"); + assertChannel(channel1, channelTypeUID, null, null); + + Channel channel2 = updatedThing.getChannel("testChannel2"); + assertChannel(channel2, channelTypeUID, "Test Label", null); + assertThat(channel2.getDefaultTags(), containsInAnyOrder(ADDED_TAGS)); + + Channel channel3 = updatedThing.getChannel("testChannel3"); + assertChannel(channel3, channelTypeUID, "Test Label", "Test Description"); + } + + @Test + public void testSingleChannelUpdate() { + registerThingType(UPDATE_CHANNEL_THING_TYPE_UID); + + ChannelTypeUID channelTypeOldUID = new ChannelTypeUID(BINDING_ID, "testChannelOldTypeId"); + ChannelTypeUID channelTypeNewUID = new ChannelTypeUID(BINDING_ID, "testChannelNewTypeId"); + registerChannelTypes(channelTypeOldUID, channelTypeNewUID); + + ThingUID thingUID = new ThingUID(UPDATE_CHANNEL_THING_TYPE_UID, THING_ID); + ChannelUID channelUID1 = new ChannelUID(thingUID, "testChannel1"); + ChannelUID channelUID2 = new ChannelUID(thingUID, "testChannel2"); + Configuration channelConfig = new Configuration(Map.of("foo", "bar")); + + Channel origChannel1 = ChannelBuilder.create(channelUID1).withType(channelTypeOldUID) + .withConfiguration(channelConfig).build(); + Channel origChannel2 = ChannelBuilder.create(channelUID2).withType(channelTypeOldUID) + .withConfiguration(channelConfig).build(); + + Thing thing = ThingBuilder.create(UPDATE_CHANNEL_THING_TYPE_UID, thingUID) + .withChannels(origChannel1, origChannel2).build(); + + managedThingProvider.add(thing); + + Thing updatedThing = assertThing(thing, 1); + assertThat(updatedThing.getChannels(), hasSize(2)); + + Channel channel1 = updatedThing.getChannel(channelUID1); + assertChannel(channel1, channelTypeNewUID, "New Test Label", null); + assertThat(channel1.getConfiguration(), is(channelConfig)); + + Channel channel2 = updatedThing.getChannel(channelUID2); + assertChannel(channel2, channelTypeNewUID, null, null); + assertThat(channel2.getConfiguration().getProperties(), is(anEmptyMap())); + } + + @Test + public void testSingleChannelRemoval() { + registerThingType(REMOVE_CHANNEL_THING_TYPE_UID); + + ChannelTypeUID channelTypeUID = new ChannelTypeUID(BINDING_ID, "testChannelTypeId"); + registerChannelTypes(channelTypeUID); + + ThingUID thingUID = new ThingUID(REMOVE_CHANNEL_THING_TYPE_UID, THING_ID); + ChannelUID channelUID = new ChannelUID(thingUID, "testChannel"); + + Thing thing = ThingBuilder.create(REMOVE_CHANNEL_THING_TYPE_UID, thingUID) + .withChannel(ChannelBuilder.create(channelUID).withType(channelTypeUID).build()).build(); + + managedThingProvider.add(thing); + + Thing updatedThing = assertThing(thing, 1); + assertThat(updatedThing.getChannels(), hasSize(0)); + } + + @Test + public void testMultipleChannelUpdates() { + registerThingType(MULTIPLE_CHANNEL_THING_TYPE_UID); + + ChannelTypeUID channelTypeOldUID = new ChannelTypeUID(BINDING_ID, "testChannelOldTypeId"); + ChannelTypeUID channelTypeNewUID = new ChannelTypeUID(BINDING_ID, "testChannelNewTypeId"); + registerChannelTypes(channelTypeOldUID, channelTypeNewUID); + + ThingUID thingUID = new ThingUID(MULTIPLE_CHANNEL_THING_TYPE_UID, THING_ID); + ChannelUID channelUID0 = new ChannelUID(thingUID, "testChannel0"); + ChannelUID channelUID1 = new ChannelUID(thingUID, "testChannel1"); + + Thing thing = ThingBuilder.create(MULTIPLE_CHANNEL_THING_TYPE_UID, thingUID) + .withChannel(ChannelBuilder.create(channelUID0).withType(channelTypeOldUID).build()) + .withChannel(ChannelBuilder.create(channelUID1).withType(channelTypeOldUID).build()).build(); + + managedThingProvider.add(thing); + + Thing updatedThing = assertThing(thing, 3); + assertThat(updatedThing.getChannels(), hasSize(2)); + + Channel channel1 = updatedThing.getChannel("testChannel1"); + assertChannel(channel1, channelTypeNewUID, "Test Label", null); + + Channel channel2 = updatedThing.getChannel("testChannel2"); + assertChannel(channel2, channelTypeOldUID, "TestLabel", null); + } + + @Test + public void testOnlyMatchingInstructionsUpdate() { + registerThingType(MULTIPLE_CHANNEL_THING_TYPE_UID); + + ChannelTypeUID channelTypeOldUID = new ChannelTypeUID(BINDING_ID, "testChannelOldTypeId"); + registerChannelTypes(channelTypeOldUID); + + ThingUID thingUID = new ThingUID(MULTIPLE_CHANNEL_THING_TYPE_UID, THING_ID); + ChannelUID channelUID0 = new ChannelUID(thingUID, "testChannel0"); + ChannelUID channelUID1 = new ChannelUID(thingUID, "testChannel1"); + + Thing thing = ThingBuilder.create(MULTIPLE_CHANNEL_THING_TYPE_UID, thingUID) + .withChannel(ChannelBuilder.create(channelUID0).withType(channelTypeOldUID).build()) + .withChannel(ChannelBuilder.create(channelUID1).withType(channelTypeOldUID).build()) + .withProperty(PROPERTY_THING_TYPE_VERSION, "2").build(); + + managedThingProvider.add(thing); + + Thing updatedThing = assertThing(thing, 3); + assertThat(updatedThing.getChannels(), hasSize(1)); + + Channel channel1 = updatedThing.getChannel("testChannel1"); + assertChannel(channel1, channelTypeOldUID, null, null); + } + + @Test + public void testNotModifiedIfHigherVersion() { + registerThingType(ADD_CHANNEL_THING_TYPE_UID); + + ChannelTypeUID channelTypeUID = new ChannelTypeUID(BINDING_ID, "testChannelTypeId"); + registerChannelTypes(channelTypeUID); + + ThingUID thingUID = new ThingUID(ADD_CHANNEL_THING_TYPE_UID, THING_ID); + Thing thing = ThingBuilder.create(ADD_CHANNEL_THING_TYPE_UID, thingUID) + .withProperty(PROPERTY_THING_TYPE_VERSION, "1").build(); + managedThingProvider.add(thing); + + waitForAssert(() -> assertThat(thing.getStatus(), is(ThingStatus.ONLINE))); + assertThat(thingRegistry.get(thingUID), is(sameInstance(thing))); + assertThat(thing.getChannels(), is(emptyCollectionOf(Channel.class))); + } + + private Thing assertThing(Thing oldThing, int expectedNewThingTypeVersion) { + ThingUID thingUID = oldThing.getUID(); + + waitForAssert(() -> { + @Nullable + Thing updatedThing = thingRegistry.get(thingUID); + assertThat(updatedThing, is(not(sameInstance(oldThing)))); + }); + + @Nullable + Thing updatedThing = thingRegistry.get(thingUID); + assertThat(updatedThing.getStatus(), is(ThingStatus.ONLINE)); + + // check thing type version is upgraded + String thingTypeVersion = updatedThing.getProperties().get(PROPERTY_THING_TYPE_VERSION); + assertThat(thingTypeVersion, is(Integer.toString(expectedNewThingTypeVersion))); + + return updatedThing; + } + + private void assertChannel(@Nullable Channel channel, ChannelTypeUID channelTypeUID, @Nullable String label, + @Nullable String description) { + assertThat(channel, is(notNullValue())); + assertThat(channel.getChannelTypeUID(), is(channelTypeUID)); + if (label != null) { + assertThat(channel.getLabel(), is(label)); + } else { + assertThat(channel.getLabel(), is(nullValue())); + } + if (description != null) { + assertThat(channel.getDescription(), is(description)); + } else { + assertThat(channel.getDescription(), is(nullValue())); + } + } + + private void registerThingType(ThingTypeUID thingTypeUID) { + ThingType thingType = ThingTypeBuilder.instance(thingTypeUID, "label").build(); + + ThingTypeProvider thingTypeProvider = mock(ThingTypeProvider.class); + when(thingTypeProvider.getThingType(eq(thingTypeUID), nullable(Locale.class))).thenReturn(thingType); + registerService(thingTypeProvider); + + ThingTypeRegistry thingTypeRegistry = mock(ThingTypeRegistry.class); + when(thingTypeRegistry.getThingType(eq(thingTypeUID))).thenReturn(thingType); + registerService(thingTypeRegistry); + } + + private void registerChannelTypes(ChannelTypeUID... channelTypeUIDs) { + ChannelTypeProvider channelTypeProvider = mock(ChannelTypeProvider.class); + ChannelTypeRegistry channelTypeRegistry = mock(ChannelTypeRegistry.class); + + for (ChannelTypeUID channelTypeUID : channelTypeUIDs) { + ChannelType channelType = ChannelTypeBuilder.state(channelTypeUID, "label", "Number").build(); + when(channelTypeProvider.getChannelType(eq(channelTypeUID), nullable(Locale.class))) + .thenReturn(channelType); + when(channelTypeRegistry.getChannelType(eq(channelTypeUID))).thenReturn(channelType); + } + + registerService(channelTypeProvider); + registerService(channelTypeRegistry); + } + + class TestThingHandlerFactory extends BaseThingHandlerFactory { + Logger logger = LoggerFactory.getLogger(TestThingHandlerFactory.class); + + @Override + public void activate(final ComponentContext ctx) { + super.activate(ctx); + } + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return true; + } + + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + return new BaseThingHandler(thing) { + @Override + public void initialize() { + updateStatus(ThingStatus.ONLINE); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + } + }; + } + } + + private class BundleResolverImpl implements BundleResolver { + @Override + public Bundle resolveBundle(@NonNullByDefault({}) Class clazz) { + // return the test bundle if the class is TestThingHandlerFactory + if (clazz != null && clazz.equals(TestThingHandlerFactory.class)) { + return testBundle; + } else { + return FrameworkUtil.getBundle(clazz); + } + } + } +} diff --git a/itests/org.openhab.core.thing.tests/src/main/resources/test-bundle-pool/thingUpdateTest.bundle/OH-INF/update/multiInstructions.xml b/itests/org.openhab.core.thing.tests/src/main/resources/test-bundle-pool/thingUpdateTest.bundle/OH-INF/update/multiInstructions.xml new file mode 100644 index 000000000..26d28ffd7 --- /dev/null +++ b/itests/org.openhab.core.thing.tests/src/main/resources/test-bundle-pool/thingUpdateTest.bundle/OH-INF/update/multiInstructions.xml @@ -0,0 +1,24 @@ + + + + + + + testBinding:testChannelNewTypeId + + + + + + testBinding:testChannelOldTypeId + + + + + + + + + diff --git a/itests/org.openhab.core.thing.tests/src/main/resources/test-bundle-pool/thingUpdateTest.bundle/OH-INF/update/singeInstructions.xml b/itests/org.openhab.core.thing.tests/src/main/resources/test-bundle-pool/thingUpdateTest.bundle/OH-INF/update/singeInstructions.xml new file mode 100644 index 000000000..1b764de4f --- /dev/null +++ b/itests/org.openhab.core.thing.tests/src/main/resources/test-bundle-pool/thingUpdateTest.bundle/OH-INF/update/singeInstructions.xml @@ -0,0 +1,45 @@ + + + + + + + testBinding:testChannelTypeId + + + testBinding:testChannelTypeId + + + Tag1 + Tag2 + + + + testBinding:testChannelTypeId + + Test Description + + + + + + + + + + + + + + testBinding:testChannelNewTypeId + + + + testBinding:testChannelNewTypeId + + + + +