[MQTT.Homeassistant] process errors in MQTT message handlers during components discovery (#11315)
Signed-off-by: Anton Kharuzhy <publicantroids@gmail.com>
This commit is contained in:
parent
196e4e2210
commit
ce61044329
|
@ -29,6 +29,8 @@ import org.openhab.binding.mqtt.generic.TransformationServiceProvider;
|
|||
import org.openhab.binding.mqtt.generic.utils.FutureCollector;
|
||||
import org.openhab.binding.mqtt.homeassistant.internal.component.AbstractComponent;
|
||||
import org.openhab.binding.mqtt.homeassistant.internal.component.ComponentFactory;
|
||||
import org.openhab.binding.mqtt.homeassistant.internal.exception.ConfigurationException;
|
||||
import org.openhab.binding.mqtt.homeassistant.internal.exception.UnsupportedComponentException;
|
||||
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
|
||||
import org.openhab.core.io.transport.mqtt.MqttMessageSubscriber;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
|
@ -97,18 +99,27 @@ public class DiscoverComponents implements MqttMessageSubscriber {
|
|||
AbstractComponent<?> component = null;
|
||||
|
||||
if (config.length() > 0) {
|
||||
component = ComponentFactory.createComponent(thingUID, haID, config, updateListener, tracker, scheduler,
|
||||
gson, transformationServiceProvider);
|
||||
}
|
||||
if (component != null) {
|
||||
component.setConfigSeen();
|
||||
try {
|
||||
component = ComponentFactory.createComponent(thingUID, haID, config, updateListener, tracker, scheduler,
|
||||
gson, transformationServiceProvider);
|
||||
component.setConfigSeen();
|
||||
|
||||
logger.trace("Found HomeAssistant thing {} component {}", haID.objectID, haID.component);
|
||||
if (discoveredListener != null) {
|
||||
discoveredListener.componentDiscovered(haID, component);
|
||||
logger.trace("Found HomeAssistant thing {} component {}", haID.objectID, haID.component);
|
||||
|
||||
if (discoveredListener != null) {
|
||||
discoveredListener.componentDiscovered(haID, component);
|
||||
}
|
||||
} catch (UnsupportedComponentException e) {
|
||||
logger.warn("HomeAssistant discover error: thing {} component type is unsupported: {}", haID.objectID,
|
||||
haID.component);
|
||||
} catch (ConfigurationException e) {
|
||||
logger.warn("HomeAssistant discover error: invalid configuration of thing {} component {}: {}",
|
||||
haID.objectID, haID.component, e.getMessage());
|
||||
} catch (Exception e) {
|
||||
logger.warn("HomeAssistant discover error: {}", e.getMessage());
|
||||
}
|
||||
} else {
|
||||
logger.debug("Configuration of HomeAssistant thing {} invalid: {}", haID.objectID, config);
|
||||
logger.warn("Configuration of HomeAssistant thing {} is empty", haID.objectID);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -21,6 +21,8 @@ import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
|
|||
import org.openhab.binding.mqtt.generic.TransformationServiceProvider;
|
||||
import org.openhab.binding.mqtt.homeassistant.internal.HaID;
|
||||
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
|
||||
import org.openhab.binding.mqtt.homeassistant.internal.exception.ConfigurationException;
|
||||
import org.openhab.binding.mqtt.homeassistant.internal.exception.UnsupportedComponentException;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
@ -48,40 +50,36 @@ public class ComponentFactory {
|
|||
* @param updateListener A channel state update listener
|
||||
* @return A HA MQTT Component
|
||||
*/
|
||||
public static @Nullable AbstractComponent<?> createComponent(ThingUID thingUID, HaID haID,
|
||||
String channelConfigurationJSON, ChannelStateUpdateListener updateListener, AvailabilityTracker tracker,
|
||||
ScheduledExecutorService scheduler, Gson gson,
|
||||
TransformationServiceProvider transformationServiceProvider) {
|
||||
public static AbstractComponent<?> createComponent(ThingUID thingUID, HaID haID, String channelConfigurationJSON,
|
||||
ChannelStateUpdateListener updateListener, AvailabilityTracker tracker, ScheduledExecutorService scheduler,
|
||||
Gson gson, TransformationServiceProvider transformationServiceProvider) throws ConfigurationException {
|
||||
ComponentConfiguration componentConfiguration = new ComponentConfiguration(thingUID, haID,
|
||||
channelConfigurationJSON, gson, updateListener, tracker, scheduler)
|
||||
.transformationProvider(transformationServiceProvider);
|
||||
try {
|
||||
switch (haID.component) {
|
||||
case "alarm_control_panel":
|
||||
return new AlarmControlPanel(componentConfiguration);
|
||||
case "binary_sensor":
|
||||
return new BinarySensor(componentConfiguration);
|
||||
case "camera":
|
||||
return new Camera(componentConfiguration);
|
||||
case "cover":
|
||||
return new Cover(componentConfiguration);
|
||||
case "fan":
|
||||
return new Fan(componentConfiguration);
|
||||
case "climate":
|
||||
return new Climate(componentConfiguration);
|
||||
case "light":
|
||||
return new Light(componentConfiguration);
|
||||
case "lock":
|
||||
return new Lock(componentConfiguration);
|
||||
case "sensor":
|
||||
return new Sensor(componentConfiguration);
|
||||
case "switch":
|
||||
return new Switch(componentConfiguration);
|
||||
}
|
||||
} catch (UnsupportedOperationException e) {
|
||||
LOGGER.warn("Not supported", e);
|
||||
switch (haID.component) {
|
||||
case "alarm_control_panel":
|
||||
return new AlarmControlPanel(componentConfiguration);
|
||||
case "binary_sensor":
|
||||
return new BinarySensor(componentConfiguration);
|
||||
case "camera":
|
||||
return new Camera(componentConfiguration);
|
||||
case "cover":
|
||||
return new Cover(componentConfiguration);
|
||||
case "fan":
|
||||
return new Fan(componentConfiguration);
|
||||
case "climate":
|
||||
return new Climate(componentConfiguration);
|
||||
case "light":
|
||||
return new Light(componentConfiguration);
|
||||
case "lock":
|
||||
return new Lock(componentConfiguration);
|
||||
case "sensor":
|
||||
return new Sensor(componentConfiguration);
|
||||
case "switch":
|
||||
return new Switch(componentConfiguration);
|
||||
default:
|
||||
throw new UnsupportedComponentException("Component '" + haID + "' is unsupported!");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected static class ComponentConfiguration {
|
||||
|
|
|
@ -16,6 +16,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
|
|||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.mqtt.generic.values.OnOffValue;
|
||||
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
|
||||
import org.openhab.binding.mqtt.homeassistant.internal.exception.ConfigurationException;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
|
@ -53,7 +54,7 @@ public class Lock extends AbstractComponent<Lock.ChannelConfiguration> {
|
|||
|
||||
// We do not support all HomeAssistant quirks
|
||||
if (channelConfiguration.optimistic && !channelConfiguration.stateTopic.isBlank()) {
|
||||
throw new UnsupportedOperationException("Component:Lock does not support forced optimistic mode");
|
||||
throw new ConfigurationException("Component:Lock does not support forced optimistic mode");
|
||||
}
|
||||
|
||||
buildChannel(SWITCH_CHANNEL_ID,
|
||||
|
|
|
@ -16,6 +16,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
|
|||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.mqtt.generic.values.OnOffValue;
|
||||
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
|
||||
import org.openhab.binding.mqtt.homeassistant.internal.exception.ConfigurationException;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
|
@ -65,7 +66,7 @@ public class Switch extends AbstractComponent<Switch.ChannelConfiguration> {
|
|||
: channelConfiguration.stateTopic.isBlank();
|
||||
|
||||
if (optimistic && !channelConfiguration.stateTopic.isBlank()) {
|
||||
throw new UnsupportedOperationException("Component:Switch does not support forced optimistic mode");
|
||||
throw new ConfigurationException("Component:Switch does not support forced optimistic mode");
|
||||
}
|
||||
|
||||
String stateOn = channelConfiguration.stateOn != null ? channelConfiguration.stateOn
|
||||
|
|
|
@ -37,16 +37,18 @@ public class ConnectionDeserializer implements JsonDeserializer<Connection> {
|
|||
throws JsonParseException {
|
||||
JsonArray list;
|
||||
if (json == null) {
|
||||
throw new JsonParseException("JSON element is null");
|
||||
throw new JsonParseException("JSON element is null, but must be connection definition.");
|
||||
}
|
||||
try {
|
||||
list = json.getAsJsonArray();
|
||||
} catch (IllegalStateException e) {
|
||||
throw new JsonParseException("Cannot parse JSON array", e);
|
||||
throw new JsonParseException("Cannot parse JSON array. Each connection must be defined as array with two "
|
||||
+ "elements: connection_type, connection identifier. For example: \"connections\": [[\"mac\", "
|
||||
+ "\"02:5b:26:a8:dc:12\"]]", e);
|
||||
}
|
||||
if (list.size() != 2) {
|
||||
throw new JsonParseException(
|
||||
"Connection information must be a tuple, but has " + list.size() + " elements!");
|
||||
throw new JsonParseException("Connection information must be a tuple, but has " + list.size()
|
||||
+ " elements! For example: " + "\"connections\": [[\"mac\", \"02:5b:26:a8:dc:12\"]]");
|
||||
}
|
||||
return new Connection(list.get(0).getAsString(), list.get(1).getAsString());
|
||||
}
|
||||
|
|
|
@ -14,14 +14,15 @@ package org.openhab.binding.mqtt.homeassistant.internal.config.dto;
|
|||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.mqtt.homeassistant.internal.exception.ConfigurationException;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.util.UIDUtils;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
|
@ -199,6 +200,15 @@ public abstract class AbstractChannelConfiguration {
|
|||
*/
|
||||
public static <C extends AbstractChannelConfiguration> C fromString(final String configJSON, final Gson gson,
|
||||
final Class<C> clazz) {
|
||||
return Objects.requireNonNull(gson.fromJson(configJSON, clazz));
|
||||
try {
|
||||
@Nullable
|
||||
final C config = gson.fromJson(configJSON, clazz);
|
||||
if (config == null) {
|
||||
throw new ConfigurationException("Channel configuration is empty");
|
||||
}
|
||||
return config;
|
||||
} catch (JsonSyntaxException e) {
|
||||
throw new ConfigurationException("Cannot parse channel configuration JSON", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,6 +36,7 @@ import org.openhab.binding.mqtt.homeassistant.internal.HaID;
|
|||
import org.openhab.binding.mqtt.homeassistant.internal.HandlerConfiguration;
|
||||
import org.openhab.binding.mqtt.homeassistant.internal.config.ChannelConfigurationTypeAdapterFactory;
|
||||
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
|
||||
import org.openhab.binding.mqtt.homeassistant.internal.exception.ConfigurationException;
|
||||
import org.openhab.core.config.discovery.DiscoveryResult;
|
||||
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
|
||||
import org.openhab.core.config.discovery.DiscoveryService;
|
||||
|
@ -146,43 +147,50 @@ public class HomeAssistantDiscovery extends AbstractMQTTDiscovery {
|
|||
}
|
||||
this.future = scheduler.schedule(this::publishResults, 2, TimeUnit.SECONDS);
|
||||
|
||||
AbstractChannelConfiguration config = AbstractChannelConfiguration
|
||||
.fromString(new String(payload, StandardCharsets.UTF_8), gson);
|
||||
|
||||
// We will of course find multiple of the same unique Thing IDs, for each different component another one.
|
||||
// Therefore the components are assembled into a list and given to the DiscoveryResult label for the user to
|
||||
// easily recognize object capabilities.
|
||||
|
||||
HaID haID = new HaID(topic);
|
||||
final String thingID = config.getThingId(haID.objectID);
|
||||
|
||||
final ThingTypeUID typeID = new ThingTypeUID(MqttBindingConstants.BINDING_ID,
|
||||
MqttBindingConstants.HOMEASSISTANT_MQTT_THING.getId() + "_" + thingID);
|
||||
try {
|
||||
AbstractChannelConfiguration config = AbstractChannelConfiguration
|
||||
.fromString(new String(payload, StandardCharsets.UTF_8), gson);
|
||||
|
||||
final ThingUID thingUID = new ThingUID(typeID, connectionBridge, thingID);
|
||||
final String thingID = config.getThingId(haID.objectID);
|
||||
|
||||
thingIDPerTopic.put(topic, thingUID);
|
||||
final ThingTypeUID typeID = new ThingTypeUID(MqttBindingConstants.BINDING_ID,
|
||||
MqttBindingConstants.HOMEASSISTANT_MQTT_THING.getId() + "_" + thingID);
|
||||
|
||||
// We need to keep track of already found component topics for a specific thing
|
||||
Set<HaID> components = componentsPerThingID.computeIfAbsent(thingID, key -> ConcurrentHashMap.newKeySet());
|
||||
components.add(haID);
|
||||
final ThingUID thingUID = new ThingUID(typeID, connectionBridge, thingID);
|
||||
|
||||
final String componentNames = components.stream().map(id -> id.component)
|
||||
.map(c -> HA_COMP_TO_NAME.getOrDefault(c, c)).collect(Collectors.joining(", "));
|
||||
thingIDPerTopic.put(topic, thingUID);
|
||||
|
||||
final List<String> topics = components.stream().map(HaID::toShortTopic).collect(Collectors.toList());
|
||||
// We need to keep track of already found component topics for a specific thing
|
||||
Set<HaID> components = componentsPerThingID.computeIfAbsent(thingID, key -> ConcurrentHashMap.newKeySet());
|
||||
components.add(haID);
|
||||
|
||||
Map<String, Object> properties = new HashMap<>();
|
||||
HandlerConfiguration handlerConfig = new HandlerConfiguration(haID.baseTopic, topics);
|
||||
properties = handlerConfig.appendToProperties(properties);
|
||||
properties = config.appendToProperties(properties);
|
||||
properties.put("deviceId", thingID);
|
||||
final String componentNames = components.stream().map(id -> id.component)
|
||||
.map(c -> HA_COMP_TO_NAME.getOrDefault(c, c)).collect(Collectors.joining(", "));
|
||||
|
||||
// Because we need the new properties map with the updated "components" list
|
||||
results.put(thingUID.getAsString(),
|
||||
DiscoveryResultBuilder.create(thingUID).withProperties(properties)
|
||||
.withRepresentationProperty("deviceId").withBridge(connectionBridge)
|
||||
.withLabel(config.getThingName() + " (" + componentNames + ")").build());
|
||||
final List<String> topics = components.stream().map(HaID::toShortTopic).collect(Collectors.toList());
|
||||
|
||||
Map<String, Object> properties = new HashMap<>();
|
||||
HandlerConfiguration handlerConfig = new HandlerConfiguration(haID.baseTopic, topics);
|
||||
properties = handlerConfig.appendToProperties(properties);
|
||||
properties = config.appendToProperties(properties);
|
||||
properties.put("deviceId", thingID);
|
||||
|
||||
// Because we need the new properties map with the updated "components" list
|
||||
results.put(thingUID.getAsString(),
|
||||
DiscoveryResultBuilder.create(thingUID).withProperties(properties)
|
||||
.withRepresentationProperty("deviceId").withBridge(connectionBridge)
|
||||
.withLabel(config.getThingName() + " (" + componentNames + ")").build());
|
||||
} catch (ConfigurationException e) {
|
||||
logger.warn("HomeAssistant discover error: invalid configuration of thing {} component {}: {}",
|
||||
haID.objectID, haID.component, e.getMessage());
|
||||
} catch (Exception e) {
|
||||
logger.warn("HomeAssistant discover error: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
protected void publishResults() {
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
/**
|
||||
* Copyright (c) 2010-2021 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.binding.mqtt.homeassistant.internal.exception;
|
||||
|
||||
/**
|
||||
* Exception class for errors in HomeAssistant components configurations
|
||||
*
|
||||
* @author Anton Kharuzhy - Initial contribution
|
||||
*/
|
||||
public class ConfigurationException extends RuntimeException {
|
||||
public ConfigurationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public ConfigurationException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
/**
|
||||
* Copyright (c) 2010-2021 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.binding.mqtt.homeassistant.internal.exception;
|
||||
|
||||
/**
|
||||
* Exception class for unsupported components
|
||||
*
|
||||
* @author Anton Kharuzhy - Initial contribution
|
||||
*/
|
||||
public class UnsupportedComponentException extends ConfigurationException {
|
||||
public UnsupportedComponentException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public UnsupportedComponentException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
|
@ -40,6 +40,7 @@ import org.openhab.binding.mqtt.homeassistant.internal.HandlerConfiguration;
|
|||
import org.openhab.binding.mqtt.homeassistant.internal.component.AbstractComponent;
|
||||
import org.openhab.binding.mqtt.homeassistant.internal.component.ComponentFactory;
|
||||
import org.openhab.binding.mqtt.homeassistant.internal.config.ChannelConfigurationTypeAdapterFactory;
|
||||
import org.openhab.binding.mqtt.homeassistant.internal.exception.ConfigurationException;
|
||||
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
|
||||
import org.openhab.core.thing.Channel;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
|
@ -153,15 +154,14 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
|
|||
if (channelConfigurationJSON == null) {
|
||||
logger.warn("Provided channel does not have a 'config' configuration key!");
|
||||
} else {
|
||||
component = ComponentFactory.createComponent(thingUID, haID, channelConfigurationJSON, this, this,
|
||||
scheduler, gson, transformationServiceProvider);
|
||||
}
|
||||
|
||||
if (component != null) {
|
||||
haComponents.put(component.getGroupUID().getId(), component);
|
||||
component.addChannelTypes(channelTypeProvider);
|
||||
} else {
|
||||
logger.warn("Could not restore component {}", thing);
|
||||
try {
|
||||
component = ComponentFactory.createComponent(thingUID, haID, channelConfigurationJSON, this, this,
|
||||
scheduler, gson, transformationServiceProvider);
|
||||
haComponents.put(component.getGroupUID().getId(), component);
|
||||
component.addChannelTypes(channelTypeProvider);
|
||||
} catch (ConfigurationException e) {
|
||||
logger.error("Cannot not restore component {}: {}", thing, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
updateThingType();
|
||||
|
|
|
@ -22,6 +22,7 @@ import static org.mockito.Mockito.times;
|
|||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
@ -152,4 +153,34 @@ public class HomeAssistantThingHandlerTests extends AbstractHomeAssistantTests {
|
|||
// Expect channel group types removed, 1 for each component
|
||||
verify(channelTypeProvider, times(2)).removeChannelGroupType(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProcessMessageFromUnsupportedComponent() {
|
||||
thingHandler.initialize();
|
||||
thingHandler.discoverComponents.processMessage("homeassistant/unsupportedType/id_zigbee2mqtt/config",
|
||||
"{}".getBytes(StandardCharsets.UTF_8));
|
||||
// Ignore unsupported component
|
||||
thingHandler.delayedProcessing.forceProcessNow();
|
||||
assertThat(haThing.getChannels().size(), CoreMatchers.is(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProcessMessageWithEmptyConfig() {
|
||||
thingHandler.initialize();
|
||||
thingHandler.discoverComponents.processMessage("homeassistant/sensor/id_zigbee2mqtt/config",
|
||||
"".getBytes(StandardCharsets.UTF_8));
|
||||
// Ignore component with empty config
|
||||
thingHandler.delayedProcessing.forceProcessNow();
|
||||
assertThat(haThing.getChannels().size(), CoreMatchers.is(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProcessMessageWithBadFormatConfig() {
|
||||
thingHandler.initialize();
|
||||
thingHandler.discoverComponents.processMessage("homeassistant/sensor/id_zigbee2mqtt/config",
|
||||
"{bad format}}".getBytes(StandardCharsets.UTF_8));
|
||||
// Ignore component with bad format config
|
||||
thingHandler.delayedProcessing.forceProcessNow();
|
||||
assertThat(haThing.getChannels().size(), CoreMatchers.is(0));
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue