Skip to content

Commit

Permalink
[mqtt] Add support for abbreviations and '~' expansion in home assist…
Browse files Browse the repository at this point in the history
…ant auto discovery (openhab#5065)

* Gson TypeAdapterFactory for config
* introduce HAConfiguation.FACTORY
* added unit tests for abbreviations
* rename builder to componentConfiguration
* rename HAConfig to ChannelConfiguration

Signed-off-by: Jochen Klein <[email protected]>
  • Loading branch information
jochen314 authored and cweitkamp committed Mar 14, 2019
1 parent 83ac31c commit a3ddca5
Show file tree
Hide file tree
Showing 23 changed files with 750 additions and 216 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
import org.openhab.binding.mqtt.generic.internal.convention.homeassistant.ComponentSwitch;
import org.openhab.binding.mqtt.generic.internal.convention.homeassistant.DiscoverComponents;
import org.openhab.binding.mqtt.generic.internal.convention.homeassistant.DiscoverComponents.ComponentDiscovered;
import org.openhab.binding.mqtt.generic.internal.convention.homeassistant.ChannelConfigurationTypeAdapterFactory;
import org.openhab.binding.mqtt.generic.internal.convention.homeassistant.HaID;
import org.openhab.binding.mqtt.generic.internal.generic.ChannelStateUpdateListener;
import org.openhab.binding.mqtt.generic.internal.generic.MqttChannelTypeProvider;
Expand All @@ -57,6 +58,7 @@
import org.slf4j.LoggerFactory;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

/**
* A full implementation test, that starts the embedded MQTT broker and publishes a homeassistant MQTT discovery device
Expand Down Expand Up @@ -156,10 +158,11 @@ public void parseHATree() throws InterruptedException, ExecutionException, Timeo
MqttChannelTypeProvider channelTypeProvider = mock(MqttChannelTypeProvider.class);

final Map<String, AbstractComponent<?>> haComponents = new HashMap<String, AbstractComponent<?>>();
Gson gson = new GsonBuilder().registerTypeAdapterFactory(new ChannelConfigurationTypeAdapterFactory()).create();

ScheduledExecutorService scheduler = new ScheduledThreadPoolExecutor(4);
DiscoverComponents discover = spy(new DiscoverComponents(ThingChannelConstants.testHomeAssistantThing,
scheduler, channelStateUpdateListener, new Gson()));
scheduler, channelStateUpdateListener, gson));

// The DiscoverComponents object calls ComponentDiscovered callbacks.
// In the following implementation we add the found component to the `haComponents` map
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,11 @@
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.openhab.binding.mqtt.generic.internal.convention.homeassistant.DiscoverComponents;
import org.openhab.binding.mqtt.generic.internal.convention.homeassistant.HaID;
import org.openhab.binding.mqtt.generic.internal.convention.homeassistant.DiscoverComponents.ComponentDiscovered;
import org.openhab.binding.mqtt.generic.internal.handler.ThingChannelConstants;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

/**
* Tests the {@link DiscoverComponents} class.
Expand Down Expand Up @@ -65,8 +64,10 @@ public void discoveryTimeTest() throws InterruptedException, ExecutionException,
// Create a scheduler
ScheduledExecutorService scheduler = new ScheduledThreadPoolExecutor(1);

Gson gson = new GsonBuilder().registerTypeAdapterFactory(new ChannelConfigurationTypeAdapterFactory()).create();

DiscoverComponents discover = spy(
new DiscoverComponents(ThingChannelConstants.testHomeAssistantThing, scheduler, null, new Gson()));
new DiscoverComponents(ThingChannelConstants.testHomeAssistantThing, scheduler, null, gson));

discover.startDiscovery(connection, 50, new HaID("homeassistant", "object", "node", "component"), discovered)
.get(100, TimeUnit.MILLISECONDS);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,38 +19,96 @@
import org.junit.Test;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class HAConfigurationTests {

private Gson gson = new GsonBuilder().registerTypeAdapterFactory(new ChannelConfigurationTypeAdapterFactory()).create();

@Test
public void testTasmotaSwitch() {
public void testAbbreviations() {
String json = "{\n"//
+ " \"name\":\"Licht Dachterasse\",\n" //
+ " \"cmd_t\":\"~cmnd/POWER\",\n" //
+ " \"stat_t\":\"~tele/STATE\",\n" //
+ " \"val_tpl\":\"{{value_json.POWER}}\",\n" //
+ " \"pl_off\":\"OFF\",\n" //
+ " \"pl_on\":\"ON\",\n" //
+ " \"avty_t\":\"~tele/LWT\",\n" //
+ " \"pl_avail\":\"Online\",\n" //
+ " \"pl_not_avail\":\"Offline\",\n" //
+ " \"uniq_id\":\"86C9AC_RL_1\",\n" //
+ " \"name\":\"A\",\n" //
+ " \"icon\":\"2\",\n" //
+ " \"qos\":1,\n" //
+ " \"retain\":true,\n" //
+ " \"val_tpl\":\"B\",\n" //
+ " \"uniq_id\":\"C\",\n" //
+ " \"avty_t\":\"~E\",\n" //
+ " \"pl_avail\":\"F\",\n" //
+ " \"pl_not_avail\":\"G\",\n" //
+ " \"device\":{\n" //
+ " \"identifiers\":[\"86C9AC\"],\n" //
+ " \"name\":\"Licht Dachterasse\",\n" //
+ " \"model\":\"Sonoff TH\",\n" //
+ " \"sw_version\":\"6.4.1(release-sensors)\",\n" //
+ " \"manufacturer\":\"Tasmota\"\n" //
+ " \"ids\":[\"H\"],\n" //
+ " \"cns\":[{\n" //
+ " \"type\": \"I1\",\n" //
+ " \"identifier\": \"I2\"\n" //
+ " }],\n" //
+ " \"name\":\"J\",\n" //
+ " \"mdl\":\"K\",\n" //
+ " \"sw\":\"L\",\n" //
+ " \"mf\":\"M\"\n" //
+ " },\n" //
+ " \"~\":\"sonoff-2476/\"\n" //
+ " \"~\":\"D/\"\n" //
+ "}";

HAConfiguration config = HAConfiguration.fromString(json, new Gson());
BaseChannelConfiguration config = BaseChannelConfiguration.fromString(json, gson);

assertThat(config.name, is("A"));
assertThat(config.icon, is("2"));
assertThat(config.qos, is(1));
assertThat(config.retain, is(true));
assertThat(config.value_template, is("B"));
assertThat(config.unique_id, is("C"));
assertThat(config.availability_topic, is("D/E"));
assertThat(config.payload_available, is("F"));
assertThat(config.payload_not_available, is("G"));

assertThat(config.name, is("Licht Dachterasse"));
assertThat(config.device, is(notNullValue()));
assertThat(config.device.identifiers, contains("86C9AC"));
assertThat(config.device.name, is("Licht Dachterasse"));
assertThat(config.device.identifiers, contains("H"));
assertThat(config.device.connections, is(notNullValue()));
assertThat(config.device.connections.get(0).type, is("I1"));
assertThat(config.device.connections.get(0).identifier, is("I2"));
assertThat(config.device.name, is("J"));
assertThat(config.device.model, is("K"));
assertThat(config.device.sw_version, is("L"));
assertThat(config.device.manufacturer, is("M"));
}

@Test
public void testTildeSubstritution() {
String json = "{\n"//
+ " \"name\":\"A\",\n" //
+ " \"icon\":\"2\",\n" //
+ " \"qos\":1,\n" //
+ " \"retain\":true,\n" //
+ " \"val_tpl\":\"B\",\n" //
+ " \"uniq_id\":\"C\",\n" //
+ " \"avty_t\":\"~E\",\n" //
+ " \"pl_avail\":\"F\",\n" //
+ " \"pl_not_avail\":\"G\",\n" //
+ " \"optimistic\":true,\n" //
+ " \"state_topic\":\"O/~\",\n" //
+ " \"command_topic\":\"P~Q\",\n" //
+ " \"device\":{\n" //
+ " \"ids\":[\"H\"],\n" //
+ " \"cns\":[{\n" //
+ " \"type\": \"I1\",\n" //
+ " \"identifier\": \"I2\"\n" //
+ " }],\n" //
+ " \"name\":\"J\",\n" //
+ " \"mdl\":\"K\",\n" //
+ " \"sw\":\"L\",\n" //
+ " \"mf\":\"M\"\n" //
+ " },\n" //
+ " \"~\":\"D/\"\n" //
+ "}";

ComponentSwitch.ChannelConfiguration config = BaseChannelConfiguration.fromString(json, gson, ComponentSwitch.ChannelConfiguration.class);

assertThat(config.availability_topic, is("D/E"));
assertThat(config.state_topic, is("O/D/"));
assertThat(config.command_topic, is("P~Q"));

}

@Test
Expand All @@ -76,7 +134,7 @@ public void testSampleFanConfig() {
+ " ]\n" //
+ "}";

HAConfiguration config = HAConfiguration.fromString(json, new Gson(), ComponentFan.Config.class);
ComponentFan.ChannelConfiguration config = BaseChannelConfiguration.fromString(json, gson, ComponentFan.ChannelConfiguration.class);
assertThat(config.name, is("Bedroom Fan"));

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ Bundle-ClassPath: .
Import-Package:
com.google.gson,
com.google.gson.annotations,
com.google.gson.reflect,
com.google.gson.stream,
org.apache.commons.lang,
org.apache.commons.net.util,
org.eclipse.jdt.annotation;resolution:=optional,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.smarthome.core.thing.ChannelGroupUID;
import org.eclipse.smarthome.core.thing.ThingUID;
import org.eclipse.smarthome.core.thing.type.ChannelDefinition;
import org.eclipse.smarthome.core.thing.type.ChannelDefinitionBuilder;
import org.eclipse.smarthome.core.thing.type.ChannelGroupType;
Expand All @@ -32,17 +31,15 @@
import org.openhab.binding.mqtt.generic.internal.MqttBindingConstants;
import org.openhab.binding.mqtt.generic.internal.generic.MqttChannelTypeProvider;

import com.google.gson.Gson;

/**
* A HomeAssistant component is comparable to an ESH channel group.
* It has a name and consists of multiple channels.
*
* @author David Graeff - Initial contribution
* @param <C> Config class derived from {@link HAConfiguration}
* @param <C> Config class derived from {@link BaseChannelConfiguration}
*/
@NonNullByDefault
public abstract class AbstractComponent<C extends HAConfiguration> {
public abstract class AbstractComponent<C extends BaseChannelConfiguration> {
// Component location fields
protected final ChannelGroupTypeUID channelGroupTypeUID;
protected final ChannelGroupUID channelGroupUID;
Expand All @@ -53,8 +50,8 @@ public abstract class AbstractComponent<C extends HAConfiguration> {
// The hash code ({@link String#hashCode()}) of the configuration string
// Used to determine if a component has changed.
protected final int configHash;
protected final String configJson;
protected final C config;
protected final String channelConfigurationJson;
protected final C channelConfiguration;

/**
* Provide a thingUID and HomeAssistant topic ID to determine the ESH channel group UID and type.
Expand All @@ -64,15 +61,15 @@ public abstract class AbstractComponent<C extends HAConfiguration> {
* @param configJson The configuration string
* @param gson A Gson instance
*/
public AbstractComponent(ThingUID thing, HaID haID, String configJson, Gson gson, Class<C> clazz) {
public AbstractComponent(CFactory.ComponentConfiguration componentConfiguration, Class<C> clazz) {
this.haID = componentConfiguration.getHaID();
this.channelGroupTypeUID = new ChannelGroupTypeUID(MqttBindingConstants.BINDING_ID,
haID.getChannelGroupTypeID());
this.channelGroupUID = new ChannelGroupUID(thing, haID.getChannelGroupID());
this.haID = haID;
this.channelGroupUID = new ChannelGroupUID(componentConfiguration.getThingUID(), haID.getChannelGroupID());

this.configJson = configJson;
this.config = HAConfiguration.fromString(configJson, gson, clazz);
this.configHash = configJson.hashCode();
this.channelConfigurationJson = componentConfiguration.getConfigJSON();
this.channelConfiguration = componentConfiguration.getConfig(clazz);
this.configHash = channelConfigurationJson.hashCode();
}

/**
Expand Down Expand Up @@ -137,7 +134,7 @@ public ChannelGroupUID uid() {
* Component (Channel Group) name.
*/
public String name() {
return config.name;
return channelConfiguration.name;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,59 +29,59 @@
* @author Jochen Klein - Initial contribution
*/
@NonNullByDefault
public abstract class HAConfiguration {

public String name;

protected String icon = "";
protected int qos = 0;
protected boolean retain = false;
protected @Nullable String value_template;
protected @Nullable String unique_id;

protected @Nullable String availability_topic;
protected String payload_available = "online";
protected String payload_not_available = "offline";

@SerializedName(value = "~")
protected String tilde = "";
public abstract class BaseChannelConfiguration {

/**
* This class is needed, to be able to parse only the common base attributes.
* Without this, {@link HAConfiguration} cannot be instantiated, as it is abstract.
* Without this, {@link BaseChannelConfiguration} cannot be instantiated, as it is abstract.
* This is needed during the discovery.
*/
private static class Config extends HAConfiguration {
private static class Config extends BaseChannelConfiguration {
public Config() {
super("private");
}
}

/**
* Parse the configJSON into a subclass of {@link HAConfiguration}
* Parse the configJSON into a subclass of {@link BaseChannelConfiguration}
*
* @param configJSON
* @param gson
* @param clazz
* @return configuration object
*/
public static <C extends HAConfiguration> C fromString(final String configJSON, final Gson gson,
public static <C extends BaseChannelConfiguration> C fromString(final String configJSON, final Gson gson,
final Class<C> clazz) {
return gson.fromJson(configJSON, clazz);
}

/**
* Parse the base properties of the configJSON into a {@link HAConfiguration}
* Parse the base properties of the configJSON into a {@link BaseChannelConfiguration}
*
* @param configJSON
* @param gson
* @return configuration object
*/
public static HAConfiguration fromString(final String configJSON, final Gson gson) {
public static BaseChannelConfiguration fromString(final String configJSON, final Gson gson) {
return fromString(configJSON, gson, Config.class);
}

protected HAConfiguration(String defaultName) {
public String name;

protected String icon = "";
protected int qos; // defaults to 0 according to HA specification
protected boolean retain; // defaults to false according to HA specification
protected @Nullable String value_template;
protected @Nullable String unique_id;

protected @Nullable String availability_topic;
protected String payload_available = "online";
protected String payload_not_available = "offline";

@SerializedName(value = "~")
protected String tilde = "";

protected BaseChannelConfiguration(String defaultName) {
this.name = defaultName;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ public CChannel(AbstractComponent<?> component, String channelID, Value valueSta
}

Configuration configuration = new Configuration();
configuration.put("config", component.configJson);
configuration.put("config", component.channelConfigurationJson);
channel = ChannelBuilder.create(channelUID, channelState.getItemType()).withType(channelTypeUID)
.withKind(type.getKind()).withLabel(label).withConfiguration(configuration).build();
}
Expand Down
Loading

0 comments on commit a3ddca5

Please sign in to comment.