updateListener, channelConfiguration.fanModeCommandTemplate, channelConfiguration.fanModeCommandTopic,
channelConfiguration.fanModeStateTemplate, channelConfiguration.fanModeStateTopic, commandFilter);
- if (channelConfiguration.holdModes != null && !channelConfiguration.holdModes.isEmpty()) {
- buildOptionalChannel(HOLD_CH_ID, new TextValue(channelConfiguration.holdModes.toArray(new String[0])),
- updateListener, channelConfiguration.holdCommandTemplate, channelConfiguration.holdCommandTopic,
+ List<String> holdModes = channelConfiguration.holdModes;
+ if (holdModes != null && !holdModes.isEmpty()) {
+ buildOptionalChannel(HOLD_CH_ID, new TextValue(holdModes.toArray(new String[0])), updateListener,
+ channelConfiguration.holdCommandTemplate, channelConfiguration.holdCommandTopic,
channelConfiguration.holdStateTemplate, channelConfiguration.holdStateTopic, commandFilter);
}
final var allowedSupportedFeatures = channelConfiguration.schema == Schema.LEGACY ? LEGACY_SUPPORTED_FEATURES
: STATE_SUPPORTED_FEATURES;
- final var configSupportedFeatures = channelConfiguration.supportedFeatures == null
+ final var supportedFeatures = channelConfiguration.supportedFeatures;
+ final var configSupportedFeatures = supportedFeatures == null
? channelConfiguration.schema == Schema.LEGACY ? LEGACY_DEFAULT_FEATURES : STATE_DEFAULT_FEATURES
- : channelConfiguration.supportedFeatures;
+ : supportedFeatures;
List<String> deviceSupportedFeatures = Collections.emptyList();
if (!configSupportedFeatures.isEmpty()) {
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.Arrays;
+import java.util.Objects;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
String parentTopic = config.getParentTopic();
while (type != Object.class) {
+ Objects.requireNonNull(type, "Bug: type is null"); // Should not happen? Making compiler happy
Arrays.stream(type.getDeclaredFields()).filter(this::isMqttTopicField)
.forEach(field -> replacePlaceholderByParentTopic(config, field, parentTopic));
type = type.getSuperclass();
*/
@NonNullByDefault
public class ConnectionDeserializer implements JsonDeserializer<Connection> {
- public @Nullable Connection deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
- throws JsonParseException {
+ @Override
+ public @Nullable Connection deserialize(@Nullable JsonElement json, Type typeOfT,
+ JsonDeserializationContext context) throws JsonParseException {
JsonArray list;
if (json == null) {
throw new JsonParseException("JSON element is null, but must be connection definition.");
@Component(service = DiscoveryService.class, configurationPid = "discovery.mqttha")
@NonNullByDefault
public class HomeAssistantDiscovery extends AbstractMQTTDiscovery {
- @SuppressWarnings("unused")
private final Logger logger = LoggerFactory.getLogger(HomeAssistantDiscovery.class);
protected final Map<String, Set<HaID>> componentsPerThingID = new TreeMap<>();
protected final Map<String, ThingUID> thingIDPerTopic = new TreeMap<>();
}
if (thingIDPerTopic.containsKey(topic)) {
ThingUID thingUID = thingIDPerTopic.remove(topic);
- final String thingID = thingUID.getId();
+ if (thingUID != null) {
+ final String thingID = thingUID.getId();
- HaID haID = new HaID(topic);
+ HaID haID = new HaID(topic);
- Set<HaID> components = componentsPerThingID.getOrDefault(thingID, Collections.emptySet());
- components.remove(haID);
- if (components.isEmpty()) {
- thingRemoved(thingUID);
+ Set<HaID> components = componentsPerThingID.getOrDefault(thingID, Collections.emptySet());
+ components.remove(haID);
+ if (components.isEmpty()) {
+ thingRemoved(thingUID);
+ }
}
}
}
*/
@NonNullByDefault
public class ConfigurationException extends RuntimeException {
+ private static final long serialVersionUID = -4828651603869498942L;
+
public ConfigurationException(String message) {
super(message);
}
*/
@NonNullByDefault
public class UnsupportedComponentException extends ConfigurationException {
+ private static final long serialVersionUID = 5134690914728956232L;
+
public UnsupportedComponentException(String message) {
super(message);
}
*/
package org.openhab.binding.mqtt.homeassistant.internal.handler;
-import java.util.Collection;
+import java.util.ArrayList;
+import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.builder.ThingBuilder;
import org.openhab.core.thing.type.ChannelDefinition;
import org.openhab.core.thing.type.ChannelGroupDefinition;
import org.openhab.core.thing.type.ChannelGroupType;
public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
implements ComponentDiscovered, Consumer<List<AbstractComponent<?>>> {
public static final String AVAILABILITY_CHANNEL = "availability";
+ private static final Comparator<Channel> CHANNEL_COMPARATOR_BY_UID = Comparator
+ .comparing(channel -> channel.getUID().toString());;
private final Logger logger = LoggerFactory.getLogger(HomeAssistantThingHandler.class);
this.transformationServiceProvider);
}
- @SuppressWarnings({ "null", "unused" })
@Override
public void initialize() {
started = false;
config = getConfigAs(HandlerConfiguration.class);
- if (config.topics == null || config.topics.isEmpty()) {
+ if (config.topics.isEmpty()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Device topics unknown");
return;
}
super.stop();
}
- @SuppressWarnings({ "null", "unused" })
@Override
public @Nullable ChannelState getChannelState(ChannelUID channelUID) {
String groupID = channelUID.getGroupId();
* Callback of {@link DelayedBatchProcessing}.
* Add all newly discovered components to the Thing and start the components.
*/
- @SuppressWarnings("null")
@Override
public void accept(List<AbstractComponent<?>> discoveredComponentsList) {
MqttBrokerConnection connection = this.connection;
return null;
});
- Collection<Channel> channels = discovered.getChannelMap().values().stream()
+ List<Channel> discoveredChannels = discovered.getChannelMap().values().stream()
.map(ComponentChannel::getChannel).collect(Collectors.toList());
- ThingHelper.addChannelsToThing(thing, channels);
+ if (known != null) {
+ // We had previously known component with different config hash
+ // We remove all conflicting old channels, they will be re-added below based on the new discovery
+ logger.debug(
+ "Received component {} with slightly different config. Making sure we re-create conflicting channels...",
+ discovered.getGroupUID());
+ removeJustRediscoveredChannels(discoveredChannels);
+ }
+
+ // Add newly discovered channels. We sort the channels
+ // for (mostly) consistent jsondb serialization
+ discoveredChannels.sort(CHANNEL_COMPARATOR_BY_UID);
+ ThingHelper.addChannelsToThing(thing, discoveredChannels);
}
+ updateThingType();
+ }
+ }
+
+ private void removeJustRediscoveredChannels(List<Channel> discoveredChannels) {
+ ArrayList<Channel> mutableChannels = new ArrayList<>(getThing().getChannels());
+ Set<ChannelUID> newChannelUIDs = discoveredChannels.stream().map(Channel::getUID).collect(Collectors.toSet());
+ // Take current channels but remove those channels that were just re-discovered
+ List<Channel> existingChannelsWithNewlyDiscoveredChannelsRemoved = mutableChannels.stream()
+ .filter(existingChannel -> !newChannelUIDs.contains(existingChannel.getUID()))
+ .collect(Collectors.toList());
+ if (existingChannelsWithNewlyDiscoveredChannelsRemoved.size() < mutableChannels.size()) {
+ // We sort the channels for (mostly) consistent jsondb serialization
+ existingChannelsWithNewlyDiscoveredChannelsRemoved.sort(CHANNEL_COMPARATOR_BY_UID);
+ updateThingChannels(existingChannelsWithNewlyDiscoveredChannelsRemoved);
}
+ }
- updateThingType();
+ private void updateThingChannels(List<Channel> channelList) {
+ ThingBuilder thingBuilder = editThing();
+ thingBuilder.withChannels(channelList);
+ updateThing(thingBuilder.build());
}
@Override
*/
package org.openhab.binding.mqtt.homeassistant.internal;
-import static org.mockito.Mockito.any;
-import static org.mockito.Mockito.anyBoolean;
-import static org.mockito.Mockito.anyInt;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.when;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
+import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
*
* @author Anton Kharuzhy - Initial contribution
*/
-@SuppressWarnings({ "ConstantConditions" })
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
@NonNullByDefault
protected @Mock @NonNullByDefault({}) ThingTypeRegistry thingTypeRegistry;
protected @Mock @NonNullByDefault({}) TransformationServiceProvider transformationServiceProvider;
- @SuppressWarnings("NotNullFieldNotInitialized")
protected @NonNullByDefault({}) MqttChannelTypeProvider channelTypeProvider;
protected final Bridge bridgeThing = BridgeBuilder.create(BRIDGE_TYPE_UID, BRIDGE_UID).build();
final var subscriber = (MqttMessageSubscriber) invocation.getArgument(1);
subscriptions.putIfAbsent(topic, ConcurrentHashMap.newKeySet());
- subscriptions.get(topic).add(subscriber);
+ Set<MqttMessageSubscriber> subscribers = subscriptions.get(topic);
+ Objects.requireNonNull(subscribers); // Invariant, thanks to putIfAbsent above. To make compiler happy
+ subscribers.add(subscriber);
return CompletableFuture.completedFuture(true);
}).when(bridgeConnection).subscribe(any(), any());
* @param relativePath path from src/test/java/org/openhab/binding/mqtt/homeassistant/internal
* @return path
*/
+ @SuppressWarnings("null")
protected Path getResourcePath(String relativePath) {
try {
return Paths.get(AbstractHomeAssistantTests.class.getResource(relativePath).toURI());
*/
package org.openhab.binding.mqtt.homeassistant.internal.component;
-import static org.hamcrest.CoreMatchers.instanceOf;
-import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
+import static org.mockito.ArgumentMatchers.*;
import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.*;
import java.nio.charset.StandardCharsets;
import java.util.List;
import org.eclipse.jdt.annotation.Nullable;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
+import org.mockito.ArgumentMatchers;
import org.mockito.Mock;
import org.openhab.binding.mqtt.generic.MqttChannelTypeProvider;
import org.openhab.binding.mqtt.generic.TransformationServiceProvider;
*
* @author Anton Kharuzhy - Initial contribution
*/
-@SuppressWarnings({ "ConstantConditions" })
@NonNullByDefault
public abstract class AbstractComponentTests extends AbstractHomeAssistantTests {
private static final int SUBSCRIBE_TIMEOUT = 10000;
* @param channelId channel
* @param state expected state
*/
+ @SuppressWarnings("null")
protected static void assertState(AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> component,
String channelId, State state) {
assertThat(component.getChannel(channelId).getState().getCache().getChannelState(), is(state));
* @param payload payload
*/
protected void assertPublished(String mqttTopic, String payload) {
- verify(bridgeConnection).publish(eq(mqttTopic), eq(payload.getBytes(StandardCharsets.UTF_8)), anyInt(),
- anyBoolean());
+ verify(bridgeConnection).publish(eq(mqttTopic), ArgumentMatchers.eq(payload.getBytes(StandardCharsets.UTF_8)),
+ anyInt(), anyBoolean());
}
/**
* @param t payload must be published N times on given topic
*/
protected void assertPublished(String mqttTopic, String payload, int t) {
- verify(bridgeConnection, times(t)).publish(eq(mqttTopic), eq(payload.getBytes(StandardCharsets.UTF_8)),
- anyInt(), anyBoolean());
+ verify(bridgeConnection, times(t)).publish(eq(mqttTopic),
+ ArgumentMatchers.eq(payload.getBytes(StandardCharsets.UTF_8)), anyInt(), anyBoolean());
}
/**
* @param payload payload
*/
protected void assertNotPublished(String mqttTopic, String payload) {
- verify(bridgeConnection, never()).publish(eq(mqttTopic), eq(payload.getBytes(StandardCharsets.UTF_8)), anyInt(),
- anyBoolean());
+ verify(bridgeConnection, never()).publish(eq(mqttTopic),
+ ArgumentMatchers.eq(payload.getBytes(StandardCharsets.UTF_8)), anyInt(), anyBoolean());
}
/**
*
* @author Anton Kharuzhy - Initial contribution
*/
-@SuppressWarnings("ConstantConditions")
@NonNullByDefault
public class AlarmControlPanelTests extends AbstractComponentTests {
public static final String CONFIG_TOPIC = "alarm_control_panel/0x0000000000000000_alarm_control_panel_zigbee2mqtt";
+ @SuppressWarnings("null")
@Test
public void testAlarmControlPanel() {
// @formatter:off
*
* @author Anton Kharuzhy - Initial contribution
*/
-@SuppressWarnings("ConstantConditions")
@NonNullByDefault
public class ClimateTests extends AbstractComponentTests {
public static final String CONFIG_TOPIC = "climate/0x847127fffe11dd6a_climate_zigbee2mqtt";
+ @SuppressWarnings("null")
@Test
public void testTS0601Climate() {
var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC), "{"
assertPublished("zigbee2mqtt/th1/set/current_heating_setpoint", "25");
}
+ @SuppressWarnings("null")
@Test
public void testTS0601ClimateNotSendIfOff() {
var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC), "{"
assertPublished("zigbee2mqtt/th1/set/current_heating_setpoint", "25");
}
+ @SuppressWarnings("null")
@Test
public void testClimate() {
var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC),
*
* @author Anton Kharuzhy - Initial contribution
*/
-@SuppressWarnings("ConstantConditions")
@NonNullByDefault
public class CoverTests extends AbstractComponentTests {
public static final String CONFIG_TOPIC = "cover/0x0000000000000000_cover_zigbee2mqtt";
+ @SuppressWarnings("null")
@Test
public void test() throws InterruptedException {
// @formatter:off
*
* @author Anton Kharuzhy - Initial contribution
*/
-@SuppressWarnings("ALL")
@NonNullByDefault
public class FanTests extends AbstractComponentTests {
public static final String CONFIG_TOPIC = "fan/0x0000000000000000_fan_zigbee2mqtt";
+ @SuppressWarnings("null")
@Test
public void test() throws InterruptedException {
// @formatter:off
}
}
+ @SuppressWarnings("null")
@Test
public void testTS0601ClimateConfig() {
String json = readTestJson("configTS0601ClimateThermostat.json");
*
* @author Anton Kharuzhy - Initial contribution
*/
-@SuppressWarnings("ALL")
@NonNullByDefault
public class LockTests extends AbstractComponentTests {
public static final String CONFIG_TOPIC = "lock/0x0000000000000000_lock_zigbee2mqtt";
+ @SuppressWarnings("null")
@Test
public void test() throws InterruptedException {
// @formatter:off
*
* @author Anton Kharuzhy - Initial contribution
*/
-@SuppressWarnings("ConstantConditions")
@NonNullByDefault
public class SensorTests extends AbstractComponentTests {
public static final String CONFIG_TOPIC = "sensor/0x0000000000000000_sensor_zigbee2mqtt";
+ @SuppressWarnings("null")
@Test
public void test() throws InterruptedException {
// @formatter:off
*
* @author Anton Kharuzhy - Initial contribution
*/
-@SuppressWarnings("ConstantConditions")
@NonNullByDefault
public class SwitchTests extends AbstractComponentTests {
public static final String CONFIG_TOPIC = "switch/0x847127fffe11dd6a_auto_lock_zigbee2mqtt";
+ @SuppressWarnings("null")
@Test
public void testSwitchWithStateAndCommand() {
var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC),
assertState(component, Switch.SWITCH_CHANNEL_ID, OnOffType.ON);
}
+ @SuppressWarnings("null")
@Test
public void testSwitchWithCommand() {
var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC),
*
* @author Anton Kharuzhy - Initial contribution
*/
-@SuppressWarnings("ConstantConditions")
@NonNullByDefault
public class VacuumTests extends AbstractComponentTests {
public static final String CONFIG_TOPIC = "vacuum/rockrobo_vacuum";
+ @SuppressWarnings("null")
@Test
public void testRoborockValetudo() {
// @formatter:off
assertState(component, Vacuum.JSON_ATTRIBUTES_CH_ID, new StringType(jsonValue));
}
+ @SuppressWarnings("null")
@Test
public void testLegacySchema() {
// @formatter:off
*
* @author Anton Kharuzhy - Initial contribution
*/
-@SuppressWarnings({ "ConstantConditions", "unchecked" })
+@SuppressWarnings({ "unchecked" })
@ExtendWith(MockitoExtension.class)
@NonNullByDefault
public class HomeAssistantDiscoveryTests extends AbstractHomeAssistantTests {
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
+import java.util.Objects;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mqtt.homeassistant.internal.HaID;
import org.openhab.binding.mqtt.homeassistant.internal.HandlerConfiguration;
import org.openhab.binding.mqtt.homeassistant.internal.component.Climate;
+import org.openhab.binding.mqtt.homeassistant.internal.component.Sensor;
import org.openhab.binding.mqtt.homeassistant.internal.component.Switch;
+import org.openhab.core.thing.Channel;
import org.openhab.core.thing.binding.ThingHandlerCallback;
/**
*
* @author Anton Kharuzhy - Initial contribution
*/
-@SuppressWarnings({ "ConstantConditions" })
@ExtendWith(MockitoExtension.class)
@NonNullByDefault
public class HomeAssistantThingHandlerTests extends AbstractHomeAssistantTests {
private @Mock @NonNullByDefault({}) ThingHandlerCallback callbackMock;
private @NonNullByDefault({}) HomeAssistantThingHandler thingHandler;
+ private @NonNullByDefault({}) HomeAssistantThingHandler nonSpyThingHandler;
@BeforeEach
public void setup() {
SUBSCRIBE_TIMEOUT, ATTRIBUTE_RECEIVE_TIMEOUT);
thingHandler.setConnection(bridgeConnection);
thingHandler.setCallback(callbackMock);
+ nonSpyThingHandler = thingHandler;
thingHandler = spy(thingHandler);
}
verify(channelTypeProvider, times(2)).setChannelGroupType(any(), any());
}
+ /**
+ * Test where the same component is published twice to MQTT. The binding should handle this.
+ *
+ * @throws InterruptedException
+ */
+ @Test
+ public void testDuplicateComponentPublish() throws InterruptedException {
+ thingHandler.initialize();
+
+ verify(callbackMock).statusUpdated(eq(haThing), any());
+ // Expect a call to the bridge status changed, the start, the propertiesChanged method
+ verify(thingHandler).bridgeStatusChanged(any());
+ verify(thingHandler, timeout(SUBSCRIBE_TIMEOUT)).start(any());
+
+ // Expect subscription on each topic from config
+ MQTT_TOPICS.forEach(t -> {
+ verify(bridgeConnection, timeout(SUBSCRIBE_TIMEOUT)).subscribe(eq(t), any());
+ });
+
+ verify(thingHandler, never()).componentDiscovered(any(), any());
+ assertThat(haThing.getChannels().size(), CoreMatchers.is(0));
+
+ //
+ //
+ // Publish sensor components with identical payload except for
+ // change in "name" field. The binding should respect the latest discovery result.
+ //
+ // This simulates how multiple OpenMQTTGateway devices would publish
+ // the same discovery topics for a particular Bluetooth sensor, and thus "competing" with similar but slightly
+ // different discovery topics.
+ //
+ // In fact, only difference is actually "via_device" additional metadata field telling which OpenMQTTGateway
+ // published the discovery topic.
+ //
+ //
+
+ //
+ // 1. publish corridor temperature sensor
+ //
+ var configTopicTempCorridor = "homeassistant/sensor/tempCorridor/config";
+ thingHandler.discoverComponents.processMessage(configTopicTempCorridor, new String("{"//
+ + "\"temperature_state_topic\": \"+/+/BTtoMQTT/mysensor\","//
+ + "\"temperature_state_template\": \"{{ value_json.temperature }}\", "//
+ + "\"name\": \"CorridorTemp\", "//
+ + "\"unit_of_measurement\": \"°C\" "//
+ + "}").getBytes(StandardCharsets.UTF_8));
+ verify(thingHandler, times(1)).componentDiscovered(eq(new HaID(configTopicTempCorridor)), any(Sensor.class));
+ thingHandler.delayedProcessing.forceProcessNow();
+ waitForAssert(() -> {
+ assertThat("1 channel created", thingHandler.getThing().getChannels().size() == 1);
+ });
+
+ //
+ // 2. publish outside temperature sensor
+ //
+ var configTopicTempOutside = "homeassistant/sensor/tempOutside/config";
+ thingHandler.discoverComponents.processMessage(configTopicTempOutside, new String("{"//
+ + "\"temperature_state_topic\": \"+/+/BTtoMQTT/mysensor\","//
+ + "\"temperature_state_template\": \"{{ value_json.temperature }}\", " //
+ + "\"name\": \"OutsideTemp\", "//
+ + "\"source\": \"gateway2\" "//
+ + "}").getBytes(StandardCharsets.UTF_8));
+ thingHandler.delayedProcessing.forceProcessNow();
+ verify(thingHandler, times(1)).componentDiscovered(eq(new HaID(configTopicTempOutside)), any(Sensor.class));
+ waitForAssert(() -> {
+ assertThat("2 channel created", thingHandler.getThing().getChannels().size() == 2);
+ });
+
+ //
+ // 3. publish corridor temperature sensor, this time with different name (openHAB channel label)
+ //
+ thingHandler.discoverComponents.processMessage(configTopicTempCorridor, new String("{"//
+ + "\"temperature_state_topic\": \"+/+/BTtoMQTT/mysensor\","//
+ + "\"temperature_state_template\": \"{{ value_json.temperature }}\", "//
+ + "\"name\": \"CorridorTemp NEW\", "//
+ + "\"unit_of_measurement\": \"°C\" "//
+ + "}").getBytes(StandardCharsets.UTF_8));
+ thingHandler.delayedProcessing.forceProcessNow();
+
+ waitForAssert(() -> {
+ assertThat("2 channel created", thingHandler.getThing().getChannels().size() == 2);
+ });
+
+ //
+ // verify that both channels are there and the label corresponds to newer discovery topic payload
+ //
+ Channel corridorTempChannel = nonSpyThingHandler.getThing().getChannel("tempCorridor_5Fsensor#sensor");
+ assertThat("Corridor temperature channel is created", corridorTempChannel, CoreMatchers.notNullValue());
+ Objects.requireNonNull(corridorTempChannel); // for compiler
+ assertThat("Corridor temperature channel is having the updated label from 2nd discovery topic publish",
+ corridorTempChannel.getLabel(), CoreMatchers.is("CorridorTemp NEW"));
+
+ Channel outsideTempChannel = nonSpyThingHandler.getThing().getChannel("tempOutside_5Fsensor#sensor");
+ assertThat("Outside temperature channel is created", outsideTempChannel, CoreMatchers.notNullValue());
+
+ verify(thingHandler, times(2)).componentDiscovered(eq(new HaID(configTopicTempCorridor)), any(Sensor.class));
+
+ waitForAssert(() -> {
+ assertThat("2 channel created", thingHandler.getThing().getChannels().size() == 2);
+ });
+ }
+
@Test
public void testDispose() {
thingHandler.initialize();