Refactor ThingManagerImpl (#3330)

* Refactor ThingManagerImpl

This

- moves config description URI to type-base class
- decouples the thing manager from bundle loading
- makes sure that all thing/channel-types and config descriptions are available when the thing is initialized
- enables thing type updates

Signed-off-by: Jan N. Klug <github@klug.nrw>
This commit is contained in:
J-N-K 2023-02-15 22:03:59 +01:00 committed by GitHub
parent 7f5694856d
commit d7fa5534bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 2070 additions and 765 deletions

View File

@ -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<String, Object> normalizeTypes(@Nullable Map<String, Object> configuration,
public static Map<String, Object> normalizeTypes(Map<String, Object> configuration,
List<ConfigDescription> configDescriptions) {
if (configDescriptions.isEmpty()) {
throw new IllegalArgumentException("Config description must not be empty.");
}
if (configuration == null) {
return null;
}
Map<String, Object> convertedConfiguration = new HashMap<>();
Map<String, ConfigDescriptionParameter> configParams = new HashMap<>();

View File

@ -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.

View File

@ -25,6 +25,11 @@
<artifactId>org.openhab.core.io.console</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
<version>2.3.3</version>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.test</artifactId>
@ -33,4 +38,48 @@
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.jvnet.jaxb2.maven2</groupId>
<artifactId>maven-jaxb2-plugin</artifactId>
<version>0.15.2</version>
<configuration>
<schemaDirectory>src/main/resources/xsd</schemaDirectory>
<noFileHeader>true</noFileHeader>
<locale>en</locale>
<episode>false</episode>
<extension>true</extension>
<args>
<arg>-Xxew</arg>
<arg>-Xxew:instantiate early</arg>
</args>
<plugins>
<plugin>
<groupId>com.github.jaxb-xew-plugin</groupId>
<artifactId>jaxb-xew-plugin</artifactId>
<version>1.10</version>
</plugin>
</plugins>
</configuration>
<dependencies>
<dependency>
<!-- Required for JDK 17 compatibility, see: https://github.com/highsource/maven-jaxb2-plugin/issues/207 -->
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>2.3.6</version>
</dependency>
</dependencies>
<executions>
<execution>
<id>generate-jaxb-sources</id>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@ -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,

View File

@ -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<String, String> oldProperties = Objects.requireNonNullElse(this.properties, Map.of());
Map<String, String> newProperties = new HashMap<>(oldProperties);
newProperties.put(key, value);
return withProperties(newProperties);
}
/**
* Set/replace the properties for this thing
*

View File

@ -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<String, Object> 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<String, Object> 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<ChannelBuilder> 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<ChannelBuilder> 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;
}
}

View File

@ -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);
}
}

View File

@ -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}
* <p />
* 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 <code>true</code> if this instruction should be applied, <code>false</code> otherwise
*/
static Predicate<ThingUpdateInstruction> applies(int currentThingTypeVersion) {
return i -> i.getThingTypeVersion() > currentThingTypeVersion;
}
}

View File

@ -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<UpdateInstructionKey, List<ThingUpdateInstruction>> 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<UpdateInstructionKey, List<ThingUpdateInstruction>> updateInstructions = new HashMap<>();
Enumeration<URL> 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<ThingUpdateInstruction> instructions = new ArrayList<>();
List<InstructionSet> 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) {
}
}

View File

@ -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<String> 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());
}
}

View File

@ -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<UID> {
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<UID> {
this.uid = uid;
this.label = label;
this.description = description;
this.configDescriptionURI = configDescriptionURI;
}
/**
@ -67,20 +73,29 @@ public abstract class AbstractDescriptionType implements Identifiable<UID> {
}
/**
* 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;
}
}

View File

@ -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<ChannelDefinition> channelDefinitions) throws IllegalArgumentException {
super(uid, label, description);
super(uid, label, description, null);
this.category = category;
this.channelDefinitions = channelDefinitions == null ? Collections.emptyList()

View File

@ -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}.
* <p>
* 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.
*/

View File

@ -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}.
* <p>
* This description is used as template definition for the creation of the according concrete {@link Thing} object.
* <p>
@ -46,7 +45,6 @@ public class ThingType extends AbstractDescriptionType {
private final List<String> supportedBridgeTypeUIDs;
private final Map<String, String> 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<String> supportedBridgeTypeUIDs, String label,
@Nullable String description, @Nullable String category, boolean listed,
@ -77,7 +75,7 @@ public class ThingType extends AbstractDescriptionType {
@Nullable List<ChannelGroupDefinition> channelGroupDefinitions, @Nullable Map<String, String> properties,
@Nullable URI configDescriptionURI, @Nullable List<String> 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}
*

View File

@ -0,0 +1,34 @@
<jaxb:bindings xmlns:jaxb="http://java.sun.com/xml/ns/jaxb" xmlns:xjc="http://java.sun.com/xml/ns/jaxb/xjc"
xmlns:xs="http://www.w3.org/2001/XMLSchema" jaxb:version="2.0">
<jaxb:globalBindings>
<xjc:serializable uid="1"/>
</jaxb:globalBindings>
<jaxb:bindings schemaLocation="update-description-1.0.0.xsd">
<jaxb:schemaBindings>
<jaxb:package name="org.openhab.core.thing.internal.update.dto"/>
</jaxb:schemaBindings>
</jaxb:bindings>
<jaxb:bindings schemaLocation="https://openhab.org/schemas/config-description-1.0.0.xsd">
<jaxb:bindings node="//xs:complexType[@name='parameter']">
<jaxb:bindings node=".//xs:attribute[@name='required']">
<jaxb:property name="RequiredAttribute"/>
</jaxb:bindings>
</jaxb:bindings>
<jaxb:bindings node="//xs:complexType[@name='optionsType']">
<jaxb:bindings node=".//xs:attribute[@name='value']">
<jaxb:property name="ValueAttribute"/>
</jaxb:bindings>
</jaxb:bindings>
</jaxb:bindings>
<jaxb:bindings schemaLocation="https://openhab.org/schemas/thing-description-1.0.0.xsd">
<jaxb:bindings node="//xs:complexType[@name='option']">
<jaxb:bindings node=".//xs:attribute[@name='value']">
<jaxb:property name="ValueAttribute"/>
</jaxb:bindings>
</jaxb:bindings>
</jaxb:bindings>
</jaxb:bindings>

View File

@ -0,0 +1,109 @@
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:jaxb="http://java.sun.com/xml/ns/jaxb" jaxb:version="2.0"
xmlns:update-description="https://openhab.org/schemas/update-description/v1.0.0"
xmlns:thing-description="https://openhab.org/schemas/thing-description/v1.0.0"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
targetNamespace="https://openhab.org/schemas/update-description/v1.0.0">
<xs:import namespace="https://openhab.org/schemas/config-description/v1.0.0"
schemaLocation="https://openhab.org/schemas/config-description-1.0.0.xsd"/>
<xs:import namespace="https://openhab.org/schemas/thing-description/v1.0.0"
schemaLocation="https://openhab.org/schemas/thing-description-1.0.0.xsd"/>
<xs:element name="update-descriptions">
<xs:annotation>
<xs:documentation>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.
</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:sequence>
<xs:element name="thing-type" type="update-description:thingType" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:complexType name="thingType">
<xs:sequence>
<xs:element name="instruction-set" type="update-description:instructionSet" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="uid" type="update-description:thingTypeUid" use="required"/>
</xs:complexType>
<xs:complexType name="instructionSet">
<xs:choice maxOccurs="unbounded">
<xs:annotation>
<xs:appinfo>
<jaxb:property name="instructions"/>
</xs:appinfo>
</xs:annotation>
<xs:element name="add-channel" type="update-description:addChannel" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="update-channel" type="update-description:updateChannel" minOccurs="0"
maxOccurs="unbounded"/>
<xs:element name="remove-channel" type="update-description:removeChannel" minOccurs="0"
maxOccurs="unbounded"/>
</xs:choice>
<xs:attribute name="targetVersion" type="xs:int" use="required"/>
</xs:complexType>
<xs:complexType name="addChannel">
<xs:sequence>
<xs:element name="type" type="update-description:channelTypeUid"/>
<xs:element name="label" type="xs:string" minOccurs="0"/>
<xs:element name="description" type="xs:string" minOccurs="0"/>
<xs:element name="tags" type="thing-description:tags" minOccurs="0"/>
</xs:sequence>
<xs:attribute name="id" type="update-description:channelId" use="required"/>
</xs:complexType>
<xs:complexType name="updateChannel">
<xs:annotation>
<xs:documentation>Update a channel (e.g. change channel-type, label, description). By default, the old
configuration and tags are copied to the new channel.
</xs:documentation>
</xs:annotation>
<xs:sequence>
<xs:element name="type" type="update-description:channelTypeUid"/>
<xs:element name="label" type="xs:string" minOccurs="0"/>
<xs:element name="description" type="xs:string" minOccurs="0"/>
<xs:element name="tags" type="thing-description:tags" minOccurs="0">
<xs:annotation>
<xs:documentation>If set, already existing tags are overwritten.</xs:documentation>
</xs:annotation>
</xs:element>
</xs:sequence>
<xs:attribute name="id" type="update-description:channelId" use="required"/>
<xs:attribute name="preserveConfiguration" type="xs:boolean" default="true"/>
</xs:complexType>
<xs:complexType name="removeChannel">
<xs:annotation>
<xs:documentation>Remove a channel from a thing.</xs:documentation>
</xs:annotation>
<xs:attribute name="id" type="update-description:channelId" use="required"/>
</xs:complexType>
<xs:simpleType name="channelId">
<xs:annotation>
<xs:documentation>The simple id of the channel (i.e. without the thing UID).</xs:documentation>
</xs:annotation>
<xs:restriction base="config-description:idRestrictionPattern"/>
</xs:simpleType>
<xs:simpleType name="channelTypeUid">
<xs:annotation>
<xs:documentation>The fully qualified UID of the channel type (e.g. "system:color",
"viessmann:lastErrorMessage").
</xs:documentation>
</xs:annotation>
<xs:restriction base="config-description:uriRestrictionPattern"/>
</xs:simpleType>
<xs:simpleType name="thingTypeUid">
<xs:annotation>
<xs:documentation>The fully qualified UID of the thing type.</xs:documentation>
</xs:annotation>
<xs:restriction base="config-description:uriRestrictionPattern"/>
</xs:simpleType>
</xs:schema>

View File

@ -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

View File

@ -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

View File

@ -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);
}
}

View File

@ -437,13 +437,17 @@ public class ChangeThingTypeOSGiTest extends JavaOSGiTest {
}
private List<ChannelDefinition> 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);
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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<Boolean> 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<ThingHandlerCallback> thc = new AtomicReference<>();
AtomicReference<Boolean> 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> thingHandlerCallback = new AtomicReference<>();
AtomicReference<Boolean> initializeInvoked = new AtomicReference<>(false);
AtomicReference<Boolean> 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> thingHandlerCallback = new AtomicReference<>();
AtomicReference<Boolean> initializeInvoked = new AtomicReference<>(false);
AtomicReference<Boolean> 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> thingHandlerCallback = new AtomicReference<>();
AtomicReference<Boolean> initializeInvoked = new AtomicReference<>(false);
AtomicReference<Boolean> 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> thingHandlerCallback = new AtomicReference<>();
AtomicReference<Boolean> initializeInvoked = new AtomicReference<>(false);
AtomicReference<Boolean> 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<ThingHandlerCallback> initializeThingHandlerCallback() throws Exception {
registerThingTypeProvider();
registerChannelTypeProvider();
registerChannelGroupTypeProvider();
AtomicReference<ThingHandlerCallback> 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);
}
}

View File

@ -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<Void>() {
@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);

View File

@ -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);
}
}

View File

@ -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<String, Object> 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);
}
}
}
}

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<update:update-descriptions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:update="https://openhab.org/schemas/update-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/update-description/v1.0.0 https://openhab.org/schemas/update-description-1.0.0.xsd">
<thing-type uid="testBinding:testThingTypeMultiple">
<instruction-set targetVersion="1">
<update-channel id="testChannel1">
<type>testBinding:testChannelNewTypeId</type>
<label>Test Label</label>
</update-channel>
</instruction-set>
<instruction-set targetVersion="2">
<add-channel id="testChannel2">
<type>testBinding:testChannelOldTypeId</type>
<label>TestLabel</label>
</add-channel>
</instruction-set>
<instruction-set targetVersion="3">
<remove-channel id="testChannel0"/>
</instruction-set>
</thing-type>
</update:update-descriptions>

View File

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<update:update-descriptions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:update="https://openhab.org/schemas/update-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/update-description/v1.0.0 https://openhab.org/schemas/update-description-1.0.0.xsd">
<thing-type uid="testBinding:testThingTypeAdd">
<instruction-set targetVersion="1">
<add-channel id="testChannel1">
<type>testBinding:testChannelTypeId</type>
</add-channel>
<add-channel id="testChannel2">
<type>testBinding:testChannelTypeId</type>
<label>Test Label</label>
<tags>
<tag>Tag1</tag>
<tag>Tag2</tag>
</tags>
</add-channel>
<add-channel id="testChannel3">
<type>testBinding:testChannelTypeId</type>
<label>Test Label</label>
<description>Test Description</description>
</add-channel>
</instruction-set>
</thing-type>
<thing-type uid="testBinding:testThingTypeRemove">
<instruction-set targetVersion="1">
<remove-channel id="testChannel"/>
</instruction-set>
</thing-type>
<thing-type uid="testBinding:testThingTypeUpdate">
<instruction-set targetVersion="1">
<update-channel id="testChannel1">
<type>testBinding:testChannelNewTypeId</type>
<label>New Test Label</label>
</update-channel>
<update-channel id="testChannel2" preserveConfiguration="false">
<type>testBinding:testChannelNewTypeId</type>
</update-channel>
</instruction-set>
</thing-type>
</update:update-descriptions>