[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:
antroids 2021-10-24 11:51:48 +02:00 committed by GitHub
parent 196e4e2210
commit ce61044329
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 199 additions and 81 deletions

View File

@ -29,6 +29,8 @@ import org.openhab.binding.mqtt.generic.TransformationServiceProvider;
import org.openhab.binding.mqtt.generic.utils.FutureCollector; 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.AbstractComponent;
import org.openhab.binding.mqtt.homeassistant.internal.component.ComponentFactory; 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.MqttBrokerConnection;
import org.openhab.core.io.transport.mqtt.MqttMessageSubscriber; import org.openhab.core.io.transport.mqtt.MqttMessageSubscriber;
import org.openhab.core.thing.ThingUID; import org.openhab.core.thing.ThingUID;
@ -97,18 +99,27 @@ public class DiscoverComponents implements MqttMessageSubscriber {
AbstractComponent<?> component = null; AbstractComponent<?> component = null;
if (config.length() > 0) { if (config.length() > 0) {
try {
component = ComponentFactory.createComponent(thingUID, haID, config, updateListener, tracker, scheduler, component = ComponentFactory.createComponent(thingUID, haID, config, updateListener, tracker, scheduler,
gson, transformationServiceProvider); gson, transformationServiceProvider);
}
if (component != null) {
component.setConfigSeen(); component.setConfigSeen();
logger.trace("Found HomeAssistant thing {} component {}", haID.objectID, haID.component); logger.trace("Found HomeAssistant thing {} component {}", haID.objectID, haID.component);
if (discoveredListener != null) { if (discoveredListener != null) {
discoveredListener.componentDiscovered(haID, component); 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 { } else {
logger.debug("Configuration of HomeAssistant thing {} invalid: {}", haID.objectID, config); logger.warn("Configuration of HomeAssistant thing {} is empty", haID.objectID);
} }
} }

View File

@ -21,6 +21,8 @@ import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
import org.openhab.binding.mqtt.generic.TransformationServiceProvider; import org.openhab.binding.mqtt.generic.TransformationServiceProvider;
import org.openhab.binding.mqtt.homeassistant.internal.HaID; 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.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.openhab.core.thing.ThingUID;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -48,14 +50,12 @@ public class ComponentFactory {
* @param updateListener A channel state update listener * @param updateListener A channel state update listener
* @return A HA MQTT Component * @return A HA MQTT Component
*/ */
public static @Nullable AbstractComponent<?> createComponent(ThingUID thingUID, HaID haID, public static AbstractComponent<?> createComponent(ThingUID thingUID, HaID haID, String channelConfigurationJSON,
String channelConfigurationJSON, ChannelStateUpdateListener updateListener, AvailabilityTracker tracker, ChannelStateUpdateListener updateListener, AvailabilityTracker tracker, ScheduledExecutorService scheduler,
ScheduledExecutorService scheduler, Gson gson, Gson gson, TransformationServiceProvider transformationServiceProvider) throws ConfigurationException {
TransformationServiceProvider transformationServiceProvider) {
ComponentConfiguration componentConfiguration = new ComponentConfiguration(thingUID, haID, ComponentConfiguration componentConfiguration = new ComponentConfiguration(thingUID, haID,
channelConfigurationJSON, gson, updateListener, tracker, scheduler) channelConfigurationJSON, gson, updateListener, tracker, scheduler)
.transformationProvider(transformationServiceProvider); .transformationProvider(transformationServiceProvider);
try {
switch (haID.component) { switch (haID.component) {
case "alarm_control_panel": case "alarm_control_panel":
return new AlarmControlPanel(componentConfiguration); return new AlarmControlPanel(componentConfiguration);
@ -77,11 +77,9 @@ public class ComponentFactory {
return new Sensor(componentConfiguration); return new Sensor(componentConfiguration);
case "switch": case "switch":
return new Switch(componentConfiguration); return new Switch(componentConfiguration);
default:
throw new UnsupportedComponentException("Component '" + haID + "' is unsupported!");
} }
} catch (UnsupportedOperationException e) {
LOGGER.warn("Not supported", e);
}
return null;
} }
protected static class ComponentConfiguration { protected static class ComponentConfiguration {

View File

@ -16,6 +16,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.values.OnOffValue; 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.config.dto.AbstractChannelConfiguration;
import org.openhab.binding.mqtt.homeassistant.internal.exception.ConfigurationException;
import com.google.gson.annotations.SerializedName; import com.google.gson.annotations.SerializedName;
@ -53,7 +54,7 @@ public class Lock extends AbstractComponent<Lock.ChannelConfiguration> {
// We do not support all HomeAssistant quirks // We do not support all HomeAssistant quirks
if (channelConfiguration.optimistic && !channelConfiguration.stateTopic.isBlank()) { 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, buildChannel(SWITCH_CHANNEL_ID,

View File

@ -16,6 +16,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.values.OnOffValue; 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.config.dto.AbstractChannelConfiguration;
import org.openhab.binding.mqtt.homeassistant.internal.exception.ConfigurationException;
import com.google.gson.annotations.SerializedName; import com.google.gson.annotations.SerializedName;
@ -65,7 +66,7 @@ public class Switch extends AbstractComponent<Switch.ChannelConfiguration> {
: channelConfiguration.stateTopic.isBlank(); : channelConfiguration.stateTopic.isBlank();
if (optimistic && !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 String stateOn = channelConfiguration.stateOn != null ? channelConfiguration.stateOn

View File

@ -37,16 +37,18 @@ public class ConnectionDeserializer implements JsonDeserializer<Connection> {
throws JsonParseException { throws JsonParseException {
JsonArray list; JsonArray list;
if (json == null) { if (json == null) {
throw new JsonParseException("JSON element is null"); throw new JsonParseException("JSON element is null, but must be connection definition.");
} }
try { try {
list = json.getAsJsonArray(); list = json.getAsJsonArray();
} catch (IllegalStateException e) { } 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) { if (list.size() != 2) {
throw new JsonParseException( throw new JsonParseException("Connection information must be a tuple, but has " + list.size()
"Connection information must be a tuple, but has " + list.size() + " elements!"); + " elements! For example: " + "\"connections\": [[\"mac\", \"02:5b:26:a8:dc:12\"]]");
} }
return new Connection(list.get(0).getAsString(), list.get(1).getAsString()); return new Connection(list.get(0).getAsString(), list.get(1).getAsString());
} }

View File

@ -14,14 +14,15 @@ package org.openhab.binding.mqtt.homeassistant.internal.config.dto;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; 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.thing.Thing;
import org.openhab.core.util.UIDUtils; import org.openhab.core.util.UIDUtils;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
import com.google.gson.annotations.SerializedName; 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, public static <C extends AbstractChannelConfiguration> C fromString(final String configJSON, final Gson gson,
final Class<C> clazz) { 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);
}
} }
} }

View File

@ -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.HandlerConfiguration;
import org.openhab.binding.mqtt.homeassistant.internal.config.ChannelConfigurationTypeAdapterFactory; 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.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.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder; import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.DiscoveryService; import org.openhab.core.config.discovery.DiscoveryService;
@ -146,14 +147,15 @@ public class HomeAssistantDiscovery extends AbstractMQTTDiscovery {
} }
this.future = scheduler.schedule(this::publishResults, 2, TimeUnit.SECONDS); 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. // 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 // Therefore the components are assembled into a list and given to the DiscoveryResult label for the user to
// easily recognize object capabilities. // easily recognize object capabilities.
HaID haID = new HaID(topic); HaID haID = new HaID(topic);
try {
AbstractChannelConfiguration config = AbstractChannelConfiguration
.fromString(new String(payload, StandardCharsets.UTF_8), gson);
final String thingID = config.getThingId(haID.objectID); final String thingID = config.getThingId(haID.objectID);
final ThingTypeUID typeID = new ThingTypeUID(MqttBindingConstants.BINDING_ID, final ThingTypeUID typeID = new ThingTypeUID(MqttBindingConstants.BINDING_ID,
@ -183,6 +185,12 @@ public class HomeAssistantDiscovery extends AbstractMQTTDiscovery {
DiscoveryResultBuilder.create(thingUID).withProperties(properties) DiscoveryResultBuilder.create(thingUID).withProperties(properties)
.withRepresentationProperty("deviceId").withBridge(connectionBridge) .withRepresentationProperty("deviceId").withBridge(connectionBridge)
.withLabel(config.getThingName() + " (" + componentNames + ")").build()); .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() { protected void publishResults() {

View File

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

View File

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

View File

@ -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.AbstractComponent;
import org.openhab.binding.mqtt.homeassistant.internal.component.ComponentFactory; 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.config.ChannelConfigurationTypeAdapterFactory;
import org.openhab.binding.mqtt.homeassistant.internal.exception.ConfigurationException;
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection; import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
import org.openhab.core.thing.Channel; import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.ChannelUID;
@ -153,15 +154,14 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
if (channelConfigurationJSON == null) { if (channelConfigurationJSON == null) {
logger.warn("Provided channel does not have a 'config' configuration key!"); logger.warn("Provided channel does not have a 'config' configuration key!");
} else { } else {
try {
component = ComponentFactory.createComponent(thingUID, haID, channelConfigurationJSON, this, this, component = ComponentFactory.createComponent(thingUID, haID, channelConfigurationJSON, this, this,
scheduler, gson, transformationServiceProvider); scheduler, gson, transformationServiceProvider);
}
if (component != null) {
haComponents.put(component.getGroupUID().getId(), component); haComponents.put(component.getGroupUID().getId(), component);
component.addChannelTypes(channelTypeProvider); component.addChannelTypes(channelTypeProvider);
} else { } catch (ConfigurationException e) {
logger.warn("Could not restore component {}", thing); logger.error("Cannot not restore component {}: {}", thing, e.getMessage());
}
} }
} }
updateThingType(); updateThingType();

View File

@ -22,6 +22,7 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import java.nio.charset.StandardCharsets;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -152,4 +153,34 @@ public class HomeAssistantThingHandlerTests extends AbstractHomeAssistantTests {
// Expect channel group types removed, 1 for each component // Expect channel group types removed, 1 for each component
verify(channelTypeProvider, times(2)).removeChannelGroupType(any()); 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));
}
} }