added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.mqtt.homeassistant-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-mqtt-homeassistant" description="MQTT Binding Homeassistant" version="${project.version}">
<feature>openhab-runtime-base</feature>
<feature>openhab-transport-mqtt</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.mqtt/${project.version}</bundle>
<bundle start-level="81">mvn:org.openhab.addons.bundles/org.openhab.binding.mqtt.generic/${project.version}</bundle>
<bundle start-level="82">mvn:org.openhab.addons.bundles/org.openhab.binding.mqtt.homeassistant/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,33 @@
/**
* Copyright (c) 2010-2020 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.generic.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link MqttBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author David Graeff - Initial contribution
*/
@NonNullByDefault
public class MqttBindingConstants {
public static final String BINDING_ID = "mqtt";
// List of all Thing Type UIDs
public static final ThingTypeUID HOMEASSISTANT_MQTT_THING = new ThingTypeUID(BINDING_ID, "homeassistant");
public static final String CONFIG_HA_CHANNEL = "mqtt:ha_channel";
}

View File

@@ -0,0 +1,96 @@
/**
* Copyright (c) 2010-2020 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.generic.internal;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.MqttChannelTypeProvider;
import org.openhab.binding.mqtt.generic.TransformationServiceProvider;
import org.openhab.binding.mqtt.homeassistant.internal.handler.HomeAssistantThingHandler;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
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.transform.TransformationHelper;
import org.openhab.core.transform.TransformationService;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link MqttThingHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author David Graeff - Initial contribution
*/
@Component(service = ThingHandlerFactory.class)
@NonNullByDefault
public class MqttThingHandlerFactory extends BaseThingHandlerFactory implements TransformationServiceProvider {
private @NonNullByDefault({}) MqttChannelTypeProvider typeProvider;
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Stream
.of(MqttBindingConstants.HOMEASSISTANT_MQTT_THING).collect(Collectors.toSet());
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID) || isHomeassistantDynamicType(thingTypeUID);
}
private boolean isHomeassistantDynamicType(ThingTypeUID thingTypeUID) {
return StringUtils.equals(MqttBindingConstants.BINDING_ID, thingTypeUID.getBindingId())
&& StringUtils.startsWith(thingTypeUID.getId(), MqttBindingConstants.HOMEASSISTANT_MQTT_THING.getId());
}
@Activate
@Override
protected void activate(ComponentContext componentContext) {
super.activate(componentContext);
}
@Deactivate
@Override
protected void deactivate(ComponentContext componentContext) {
super.deactivate(componentContext);
}
@Reference
protected void setChannelProvider(MqttChannelTypeProvider provider) {
this.typeProvider = provider;
}
protected void unsetChannelProvider(MqttChannelTypeProvider provider) {
this.typeProvider = null;
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (supportsThingType(thingTypeUID)) {
return new HomeAssistantThingHandler(thing, typeProvider, this, 10000, 2000);
}
return null;
}
@Override
public @Nullable TransformationService getTransformationService(String type) {
return TransformationHelper.getTransformationService(bundleContext, type);
}
}

View File

@@ -0,0 +1,220 @@
/**
* Copyright (c) 2010-2020 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;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ScheduledExecutorService;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
import org.openhab.binding.mqtt.generic.MqttChannelTypeProvider;
import org.openhab.binding.mqtt.generic.utils.FutureCollector;
import org.openhab.binding.mqtt.generic.values.Value;
import org.openhab.binding.mqtt.homeassistant.generic.internal.MqttBindingConstants;
import org.openhab.binding.mqtt.homeassistant.internal.CFactory.ComponentConfiguration;
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
import org.openhab.core.thing.ChannelGroupUID;
import org.openhab.core.thing.type.ChannelDefinition;
import org.openhab.core.thing.type.ChannelGroupDefinition;
import org.openhab.core.thing.type.ChannelGroupType;
import org.openhab.core.thing.type.ChannelGroupTypeBuilder;
import org.openhab.core.thing.type.ChannelGroupTypeUID;
/**
* 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 BaseChannelConfiguration}
*/
@NonNullByDefault
public abstract class AbstractComponent<C extends BaseChannelConfiguration> {
// Component location fields
private final ComponentConfiguration componentConfiguration;
protected final ChannelGroupTypeUID channelGroupTypeUID;
protected final ChannelGroupUID channelGroupUID;
protected final HaID haID;
// Channels and configuration
protected final Map<String, CChannel> channels = new TreeMap<>();
// 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 channelConfigurationJson;
protected final C channelConfiguration;
protected boolean configSeen;
/**
* Provide a thingUID and HomeAssistant topic ID to determine the ESH channel group UID and type.
*
* @param thing A ThingUID
* @param haID A HomeAssistant topic ID
* @param configJson The configuration string
* @param gson A Gson instance
*/
public AbstractComponent(CFactory.ComponentConfiguration componentConfiguration, Class<C> clazz) {
this.componentConfiguration = componentConfiguration;
this.channelConfigurationJson = componentConfiguration.getConfigJSON();
this.channelConfiguration = componentConfiguration.getConfig(clazz);
this.configHash = channelConfigurationJson.hashCode();
this.haID = componentConfiguration.getHaID();
String groupId = this.haID.getGroupId(channelConfiguration.unique_id);
this.channelGroupTypeUID = new ChannelGroupTypeUID(MqttBindingConstants.BINDING_ID, groupId);
this.channelGroupUID = new ChannelGroupUID(componentConfiguration.getThingUID(), groupId);
this.configSeen = false;
String availability_topic = this.channelConfiguration.availability_topic;
if (availability_topic != null) {
componentConfiguration.getTracker().addAvailabilityTopic(availability_topic,
this.channelConfiguration.payload_available, this.channelConfiguration.payload_not_available);
}
}
protected CChannel.Builder buildChannel(String channelID, Value valueState, String label,
ChannelStateUpdateListener channelStateUpdateListener) {
return new CChannel.Builder(this, componentConfiguration, channelID, valueState, label,
channelStateUpdateListener);
}
public void setConfigSeen() {
this.configSeen = true;
}
/**
* Subscribes to all state channels of the component and adds all channels to the provided channel type provider.
*
* @param connection The connection
* @param channelStateUpdateListener A listener
* @return A future that completes as soon as all subscriptions have been performed. Completes exceptionally on
* errors.
*/
public CompletableFuture<@Nullable Void> start(MqttBrokerConnection connection, ScheduledExecutorService scheduler,
int timeout) {
return channels.values().parallelStream().map(v -> v.start(connection, scheduler, timeout))
.collect(FutureCollector.allOf());
}
/**
* Unsubscribes from all state channels of the component.
*
* @return A future that completes as soon as all subscriptions removals have been performed. Completes
* exceptionally on errors.
*/
public CompletableFuture<@Nullable Void> stop() {
return channels.values().parallelStream().map(CChannel::stop).collect(FutureCollector.allOf());
}
/**
* Add all channel types to the channel type provider.
*
* @param channelTypeProvider The channel type provider
*/
public void addChannelTypes(MqttChannelTypeProvider channelTypeProvider) {
channelTypeProvider.setChannelGroupType(groupTypeUID(), type());
channels.values().forEach(v -> v.addChannelTypes(channelTypeProvider));
}
/**
* Removes all channels from the channel type provider.
* Call this if the corresponding Thing handler gets disposed.
*
* @param channelTypeProvider The channel type provider
*/
public void removeChannelTypes(MqttChannelTypeProvider channelTypeProvider) {
channels.values().forEach(v -> v.removeChannelTypes(channelTypeProvider));
channelTypeProvider.removeChannelGroupType(groupTypeUID());
}
/**
* Each HomeAssistant component corresponds to an ESH Channel Group Type.
*/
public ChannelGroupTypeUID groupTypeUID() {
return channelGroupTypeUID;
}
/**
* The unique id of this component within the ESH framework.
*/
public ChannelGroupUID uid() {
return channelGroupUID;
}
/**
* Component (Channel Group) name.
*/
public String name() {
return channelConfiguration.name;
}
/**
* Each component consists of multiple ESH Channels.
*/
public Map<String, CChannel> channelTypes() {
return channels;
}
/**
* Return a components channel. A HomeAssistant MQTT component consists of multiple functions
* and those are mapped to one or more ESH channels. The channel IDs are constants within the
* derived Component, like the {@link ComponentSwitch#switchChannelID}.
*
* @param channelID The channel ID
* @return A components channel
*/
public @Nullable CChannel channel(String channelID) {
return channels.get(channelID);
}
/**
* @return Returns the configuration hash value for easy comparison.
*/
public int getConfigHash() {
return configHash;
}
/**
* Return the channel group type.
*/
public ChannelGroupType type() {
final List<ChannelDefinition> channelDefinitions = channels.values().stream().map(CChannel::type)
.collect(Collectors.toList());
return ChannelGroupTypeBuilder.instance(channelGroupTypeUID, name()).withChannelDefinitions(channelDefinitions)
.build();
}
/**
* Resets all channel states to state UNDEF. Call this method after the connection
* to the MQTT broker got lost.
*/
public void resetState() {
channels.values().forEach(CChannel::resetState);
}
/**
* Return the channel group definition for this component.
*/
public ChannelGroupDefinition getGroupDefinition() {
return new ChannelGroupDefinition(channelGroupUID.getId(), groupTypeUID(), name(), null);
}
}

View File

@@ -0,0 +1,161 @@
/**
* Copyright (c) 2010-2020 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;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.thing.Thing;
import org.openhab.core.util.UIDUtils;
import com.google.gson.Gson;
import com.google.gson.annotations.JsonAdapter;
import com.google.gson.annotations.SerializedName;
/**
* Base class for home assistant configurations.
*
* @author Jochen Klein - Initial contribution
*/
@NonNullByDefault
public abstract class BaseChannelConfiguration {
/**
* This class is needed, to be able to parse only the common base attributes.
* Without this, {@link BaseChannelConfiguration} cannot be instantiated, as it is abstract.
* This is needed during the discovery.
*/
private static class Config extends BaseChannelConfiguration {
public Config() {
super("private");
}
}
/**
* Parse the configJSON into a subclass of {@link BaseChannelConfiguration}
*
* @param configJSON
* @param gson
* @param clazz
* @return configuration object
*/
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 BaseChannelConfiguration}
*
* @param configJSON
* @param gson
* @return configuration object
*/
public static BaseChannelConfiguration fromString(final String configJSON, final Gson gson) {
return fromString(configJSON, gson, Config.class);
}
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;
}
public @Nullable String expand(@Nullable String value) {
return value == null ? null : value.replaceAll("~", tilde);
}
protected @Nullable Device device;
static class Device {
@JsonAdapter(ListOrStringDeserializer.class)
protected @Nullable List<String> identifiers;
protected @Nullable List<Connection> connections;
protected @Nullable String manufacturer;
protected @Nullable String model;
protected @Nullable String name;
protected @Nullable String sw_version;
@Nullable
public String getId() {
return StringUtils.join(identifiers, "_");
}
}
@JsonAdapter(ConnectionDeserializer.class)
static class Connection {
protected @Nullable String type;
protected @Nullable String identifier;
}
public String getThingName() {
@Nullable
String result = null;
if (this.device != null) {
result = this.device.name;
}
if (result == null) {
result = name;
}
return result;
}
public String getThingId(String defaultId) {
@Nullable
String result = null;
if (this.device != null) {
result = this.device.getId();
}
if (result == null) {
result = unique_id;
}
return UIDUtils.encode(result != null ? result : defaultId);
}
public Map<String, Object> appendToProperties(Map<String, Object> properties) {
final Device device_ = device;
if (device_ == null) {
return properties;
}
final String manufacturer = device_.manufacturer;
if (manufacturer != null) {
properties.put(Thing.PROPERTY_VENDOR, manufacturer);
}
final String model = device_.model;
if (model != null) {
properties.put(Thing.PROPERTY_MODEL_ID, model);
}
final String sw_version = device_.sw_version;
if (sw_version != null) {
properties.put(Thing.PROPERTY_FIRMWARE_VERSION, sw_version);
}
return properties;
}
}

View File

@@ -0,0 +1,243 @@
/**
* Copyright (c) 2010-2020 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;
import java.net.URI;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ScheduledExecutorService;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.ChannelConfigBuilder;
import org.openhab.binding.mqtt.generic.ChannelState;
import org.openhab.binding.mqtt.generic.ChannelStateTransformation;
import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
import org.openhab.binding.mqtt.generic.MqttChannelTypeProvider;
import org.openhab.binding.mqtt.generic.TransformationServiceProvider;
import org.openhab.binding.mqtt.generic.values.Value;
import org.openhab.binding.mqtt.homeassistant.generic.internal.MqttBindingConstants;
import org.openhab.binding.mqtt.homeassistant.internal.CFactory.ComponentConfiguration;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.binding.builder.ChannelBuilder;
import org.openhab.core.thing.type.ChannelDefinition;
import org.openhab.core.thing.type.ChannelDefinitionBuilder;
import org.openhab.core.thing.type.ChannelType;
import org.openhab.core.thing.type.ChannelTypeBuilder;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.openhab.core.types.StateDescription;
/**
* An {@link AbstractComponent}s derived class consists of one or multiple channels.
* Each component channel consists of the determined ESH channel type, channel type UID and the
* ESH channel description itself as well as the the channels state.
*
* After the discovery process has completed and the tree of components and component channels
* have been built up, the channel types are registered to a custom channel type provider
* before adding the channel descriptions to the ESH Thing themselves.
* <br>
* <br>
* An object of this class creates the required {@link ChannelType} and {@link ChannelTypeUID} as well
* as keeps the {@link ChannelState} and {@link Channel} in one place.
*
* @author David Graeff - Initial contribution
*/
@NonNullByDefault
public class CChannel {
private static final String JINJA = "JINJA";
private final ChannelUID channelUID;
private final ChannelState channelState; // Channel state (value)
private final Channel channel; // ESH Channel
private final ChannelType type;
private final ChannelTypeUID channelTypeUID;
private final ChannelStateUpdateListener channelStateUpdateListener;
private CChannel(ChannelUID channelUID, ChannelState channelState, Channel channel, ChannelType type,
ChannelTypeUID channelTypeUID, ChannelStateUpdateListener channelStateUpdateListener) {
super();
this.channelUID = channelUID;
this.channelState = channelState;
this.channel = channel;
this.type = type;
this.channelTypeUID = channelTypeUID;
this.channelStateUpdateListener = channelStateUpdateListener;
}
public ChannelUID getChannelUID() {
return channelUID;
}
public Channel getChannel() {
return channel;
}
public ChannelState getState() {
return channelState;
}
public CompletableFuture<@Nullable Void> stop() {
return channelState.stop();
}
public CompletableFuture<@Nullable Void> start(MqttBrokerConnection connection, ScheduledExecutorService scheduler,
int timeout) {
// Make sure we set the callback again which might have been nulled during an stop
channelState.setChannelStateUpdateListener(this.channelStateUpdateListener);
return channelState.start(connection, scheduler, timeout);
}
public void addChannelTypes(MqttChannelTypeProvider channelTypeProvider) {
channelTypeProvider.setChannelType(channelTypeUID, type);
}
public void removeChannelTypes(MqttChannelTypeProvider channelTypeProvider) {
channelTypeProvider.removeChannelType(channelTypeUID);
}
public ChannelDefinition type() {
return new ChannelDefinitionBuilder(channelUID.getId(), channelTypeUID).build();
}
public void resetState() {
channelState.getCache().resetState();
}
public static class Builder {
private AbstractComponent<?> component;
private ComponentConfiguration componentConfiguration;
private String channelID;
private Value valueState;
private String label;
private @Nullable String state_topic;
private @Nullable String command_topic;
private boolean retain;
private boolean trigger;
private @Nullable Integer qos;
private ChannelStateUpdateListener channelStateUpdateListener;
private @Nullable String templateIn;
public Builder(AbstractComponent<?> component, ComponentConfiguration componentConfiguration, String channelID,
Value valueState, String label, ChannelStateUpdateListener channelStateUpdateListener) {
this.component = component;
this.componentConfiguration = componentConfiguration;
this.channelID = channelID;
this.valueState = valueState;
this.label = label;
this.channelStateUpdateListener = channelStateUpdateListener;
}
public Builder stateTopic(@Nullable String state_topic) {
this.state_topic = state_topic;
return this;
}
public Builder stateTopic(@Nullable String state_topic, @Nullable String... templates) {
this.state_topic = state_topic;
if (StringUtils.isNotBlank(state_topic)) {
for (String template : templates) {
if (StringUtils.isNotBlank(template)) {
this.templateIn = template;
break;
}
}
}
return this;
}
/**
* @deprecated use commandTopic(String, boolean, int)
* @param command_topic
* @param retain
* @return
*/
@Deprecated
public Builder commandTopic(@Nullable String command_topic, boolean retain) {
this.command_topic = command_topic;
this.retain = retain;
return this;
}
public Builder commandTopic(@Nullable String command_topic, boolean retain, int qos) {
this.command_topic = command_topic;
this.retain = retain;
this.qos = qos;
return this;
}
public Builder trigger(boolean trigger) {
this.trigger = trigger;
return this;
}
public CChannel build() {
return build(true);
}
public CChannel build(boolean addToComponent) {
ChannelUID channelUID;
ChannelState channelState; // Channel state (value)
Channel channel; // ESH Channel
ChannelType type;
ChannelTypeUID channelTypeUID;
channelUID = new ChannelUID(component.channelGroupUID, channelID);
channelTypeUID = new ChannelTypeUID(MqttBindingConstants.BINDING_ID,
channelUID.getGroupId() + "_" + channelID);
channelState = new ChannelState(
ChannelConfigBuilder.create().withRetain(retain).withQos(qos).withStateTopic(state_topic)
.withCommandTopic(command_topic).makeTrigger(trigger).build(),
channelUID, valueState, channelStateUpdateListener);
if (StringUtils.isBlank(state_topic) || this.trigger) {
type = ChannelTypeBuilder.trigger(channelTypeUID, label)
.withConfigDescriptionURI(URI.create(MqttBindingConstants.CONFIG_HA_CHANNEL)).build();
} else {
StateDescription description = valueState.createStateDescription(command_topic == null).build()
.toStateDescription();
type = ChannelTypeBuilder.state(channelTypeUID, label, channelState.getItemType())
.withConfigDescriptionURI(URI.create(MqttBindingConstants.CONFIG_HA_CHANNEL))
.withStateDescription(description).build();
}
Configuration configuration = new Configuration();
configuration.put("config", component.channelConfigurationJson);
component.haID.toConfig(configuration);
channel = ChannelBuilder.create(channelUID, channelState.getItemType()).withType(channelTypeUID)
.withKind(type.getKind()).withLabel(label).withConfiguration(configuration).build();
CChannel result = new CChannel(channelUID, channelState, channel, type, channelTypeUID,
channelStateUpdateListener);
@Nullable
TransformationServiceProvider transformationProvider = componentConfiguration
.getTransformationServiceProvider();
final String templateIn = this.templateIn;
if (templateIn != null && transformationProvider != null) {
channelState
.addTransformation(new ChannelStateTransformation(JINJA, templateIn, transformationProvider));
}
if (addToComponent) {
component.channels.put(channelID, result);
}
return result;
}
}
}

View File

@@ -0,0 +1,140 @@
/**
* Copyright (c) 2010-2020 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;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.AvailabilityTracker;
import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
import org.openhab.binding.mqtt.generic.TransformationServiceProvider;
import org.openhab.core.thing.ThingUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
/**
* A factory to create HomeAssistant MQTT components. Those components are specified at:
* https://www.home-assistant.io/docs/mqtt/discovery/
*
* @author David Graeff - Initial contribution
*/
@NonNullByDefault
public class CFactory {
private static final Logger logger = LoggerFactory.getLogger(CFactory.class);
/**
* Create a HA MQTT component. The configuration JSon string is required.
*
* @param thingUID The Thing UID that this component will belong to.
* @param haID The location of this component. The HomeAssistant ID contains the object-id, node-id and
* component-id.
* @param configJSON Most components expect a "name", a "state_topic" and "command_topic" like with
* "{name:'Name',state_topic:'homeassistant/switch/0/object/state',command_topic:'homeassistant/switch/0/object/set'".
* @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,
Gson gson, TransformationServiceProvider transformationServiceProvider) {
ComponentConfiguration componentConfiguration = new ComponentConfiguration(thingUID, haID,
channelConfigurationJSON, gson, updateListener, tracker)
.transformationProvider(transformationServiceProvider);
try {
switch (haID.component) {
case "alarm_control_panel":
return new ComponentAlarmControlPanel(componentConfiguration);
case "binary_sensor":
return new ComponentBinarySensor(componentConfiguration);
case "camera":
return new ComponentCamera(componentConfiguration);
case "cover":
return new ComponentCover(componentConfiguration);
case "fan":
return new ComponentFan(componentConfiguration);
case "climate":
return new ComponentClimate(componentConfiguration);
case "light":
return new ComponentLight(componentConfiguration);
case "lock":
return new ComponentLock(componentConfiguration);
case "sensor":
return new ComponentSensor(componentConfiguration);
case "switch":
return new ComponentSwitch(componentConfiguration);
}
} catch (UnsupportedOperationException e) {
logger.warn("Not supported", e);
}
return null;
}
protected static class ComponentConfiguration {
private final ThingUID thingUID;
private final HaID haID;
private final String configJSON;
private final ChannelStateUpdateListener updateListener;
private final AvailabilityTracker tracker;
private final Gson gson;
private @Nullable TransformationServiceProvider transformationServiceProvider;
protected ComponentConfiguration(ThingUID thingUID, HaID haID, String configJSON, Gson gson,
ChannelStateUpdateListener updateListener, AvailabilityTracker tracker) {
this.thingUID = thingUID;
this.haID = haID;
this.configJSON = configJSON;
this.gson = gson;
this.updateListener = updateListener;
this.tracker = tracker;
}
public ComponentConfiguration transformationProvider(
TransformationServiceProvider transformationServiceProvider) {
this.transformationServiceProvider = transformationServiceProvider;
return this;
}
public ThingUID getThingUID() {
return thingUID;
}
public HaID getHaID() {
return haID;
}
public String getConfigJSON() {
return configJSON;
}
public ChannelStateUpdateListener getUpdateListener() {
return updateListener;
}
@Nullable
public TransformationServiceProvider getTransformationServiceProvider() {
return transformationServiceProvider;
}
public Gson getGson() {
return gson;
}
public AvailabilityTracker getTracker() {
return tracker;
}
public <C extends BaseChannelConfiguration> C getConfig(Class<C> clazz) {
return BaseChannelConfiguration.fromString(configJSON, gson, clazz);
}
}
}

View File

@@ -0,0 +1,148 @@
/**
* Copyright (c) 2010-2020 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;
import java.io.IOException;
import java.lang.reflect.Field;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.Gson;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
/**
* This a Gson type adapter factory.
*
* It will create a type adapter for every class derived from {@link BaseChannelConfiguration} and ensures,
* that abbreviated names are replaces with their long versions during the read.
*
* In elements, whose name end in'_topic' '~' replacement is performed.
*
* The adapters also handle {@link BaseChannelConfiguration.Device}
*
* @author Jochen Klein - Initial contribution
*/
@NonNullByDefault
public class ChannelConfigurationTypeAdapterFactory implements TypeAdapterFactory {
@Override
@Nullable
public <T> TypeAdapter<T> create(@Nullable Gson gson, @Nullable TypeToken<T> type) {
if (gson == null || type == null) {
return null;
}
if (BaseChannelConfiguration.class.isAssignableFrom(type.getRawType())) {
return createHAConfig(gson, type);
}
if (BaseChannelConfiguration.Device.class.isAssignableFrom(type.getRawType())) {
return createHADevice(gson, type);
}
return null;
}
/**
* Handle {@link BaseChannelConfiguration}
*
* @param gson
* @param type
* @return
*/
private <T> TypeAdapter<T> createHAConfig(Gson gson, TypeToken<T> type) {
/* The delegate is the 'default' adapter */
final TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);
return new TypeAdapter<T>() {
@Override
public T read(@Nullable JsonReader in) throws IOException {
if (in == null) {
return null;
}
/* read the object using the default adapter, but translate the names in the reader */
T result = delegate.read(MappingJsonReader.getConfigMapper(in));
/* do the '~' expansion afterwards */
expandTidleInTopics(BaseChannelConfiguration.class.cast(result));
return result;
}
@Override
public void write(@Nullable JsonWriter out, T value) throws IOException {
delegate.write(out, value);
}
};
}
private <T> TypeAdapter<T> createHADevice(Gson gson, TypeToken<T> type) {
/* The delegate is the 'default' adapter */
final TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);
return new TypeAdapter<T>() {
@Override
public T read(@Nullable JsonReader in) throws IOException {
if (in == null) {
return null;
}
/* read the object using the default adapter, but translate the names in the reader */
T result = delegate.read(MappingJsonReader.getDeviceMapper(in));
return result;
}
@Override
public void write(@Nullable JsonWriter out, T value) throws IOException {
delegate.write(out, value);
}
};
}
private void expandTidleInTopics(BaseChannelConfiguration config) {
Class<?> type = config.getClass();
String tilde = config.tilde;
while (type != Object.class) {
Field[] fields = type.getDeclaredFields();
for (Field field : fields) {
if (String.class.isAssignableFrom(field.getType()) && field.getName().endsWith("_topic")) {
field.setAccessible(true);
try {
final String oldValue = (String) field.get(config);
String newValue = oldValue;
if (StringUtils.isNotBlank(oldValue)) {
if (oldValue.charAt(0) == '~') {
newValue = tilde + oldValue.substring(1);
} else if (oldValue.charAt(oldValue.length() - 1) == '~') {
newValue = oldValue.substring(0, oldValue.length() - 1) + tilde;
}
}
field.set(config, newValue);
} catch (IllegalArgumentException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
type = type.getSuperclass();
}
}
}

View File

@@ -0,0 +1,87 @@
/**
* Copyright (c) 2010-2020 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;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.values.TextValue;
/**
* A MQTT alarm control panel, following the https://www.home-assistant.io/components/alarm_control_panel.mqtt/
* specification.
*
* The implemented provides three state-less switches (For disarming, arming@home, arming@away) and one alarm state
* text.
*
* @author David Graeff - Initial contribution
*/
@NonNullByDefault
public class ComponentAlarmControlPanel extends AbstractComponent<ComponentAlarmControlPanel.ChannelConfiguration> {
public static final String stateChannelID = "alarm"; // Randomly chosen channel "ID"
public static final String switchDisarmChannelID = "disarm"; // Randomly chosen channel "ID"
public static final String switchArmHomeChannelID = "armhome"; // Randomly chosen channel "ID"
public static final String switchArmAwayChannelID = "armaway"; // Randomly chosen channel "ID"
/**
* Configuration class for MQTT component
*/
static class ChannelConfiguration extends BaseChannelConfiguration {
ChannelConfiguration() {
super("MQTT Alarm");
}
protected @Nullable String code;
protected String state_topic = "";
protected String state_disarmed = "disarmed";
protected String state_armed_home = "armed_home";
protected String state_armed_away = "armed_away";
protected String state_pending = "pending";
protected String state_triggered = "triggered";
protected @Nullable String command_topic;
protected String payload_disarm = "DISARM";
protected String payload_arm_home = "ARM_HOME";
protected String payload_arm_away = "ARM_AWAY";
}
public ComponentAlarmControlPanel(CFactory.ComponentConfiguration componentConfiguration) {
super(componentConfiguration, ChannelConfiguration.class);
final String[] state_enum = { channelConfiguration.state_disarmed, channelConfiguration.state_armed_home,
channelConfiguration.state_armed_away, channelConfiguration.state_pending,
channelConfiguration.state_triggered };
buildChannel(stateChannelID, new TextValue(state_enum), channelConfiguration.name,
componentConfiguration.getUpdateListener())
.stateTopic(channelConfiguration.state_topic, channelConfiguration.value_template)//
.build();
String command_topic = channelConfiguration.command_topic;
if (command_topic != null) {
buildChannel(switchDisarmChannelID, new TextValue(new String[] { channelConfiguration.payload_disarm }),
channelConfiguration.name, componentConfiguration.getUpdateListener())//
.commandTopic(command_topic, channelConfiguration.retain)//
.build();
buildChannel(switchArmHomeChannelID, new TextValue(new String[] { channelConfiguration.payload_arm_home }),
channelConfiguration.name, componentConfiguration.getUpdateListener())//
.commandTopic(command_topic, channelConfiguration.retain)//
.build();
buildChannel(switchArmAwayChannelID, new TextValue(new String[] { channelConfiguration.payload_arm_away }),
channelConfiguration.name, componentConfiguration.getUpdateListener())//
.commandTopic(command_topic, channelConfiguration.retain)//
.build();
}
}
}

View File

@@ -0,0 +1,57 @@
/**
* Copyright (c) 2010-2020 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;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.values.OnOffValue;
/**
* A MQTT BinarySensor, following the https://www.home-assistant.io/components/binary_sensor.mqtt/ specification.
*
* @author David Graeff - Initial contribution
*/
@NonNullByDefault
public class ComponentBinarySensor extends AbstractComponent<ComponentBinarySensor.ChannelConfiguration> {
public static final String sensorChannelID = "sensor"; // Randomly chosen channel "ID"
/**
* Configuration class for MQTT component
*/
static class ChannelConfiguration extends BaseChannelConfiguration {
ChannelConfiguration() {
super("MQTT Binary Sensor");
}
protected @Nullable String device_class;
protected boolean force_update = false;
protected int expire_after = 0;
protected String state_topic = "";
protected String payload_on = "ON";
protected String payload_off = "OFF";
}
public ComponentBinarySensor(CFactory.ComponentConfiguration componentConfiguration) {
super(componentConfiguration, ChannelConfiguration.class);
if (channelConfiguration.force_update) {
throw new UnsupportedOperationException("Component:Sensor does not support forced updates");
}
buildChannel(sensorChannelID, new OnOffValue(channelConfiguration.payload_on, channelConfiguration.payload_off),
channelConfiguration.name, componentConfiguration.getUpdateListener())//
.stateTopic(channelConfiguration.state_topic, channelConfiguration.value_template)//
.build();
}
}

View File

@@ -0,0 +1,49 @@
/**
* Copyright (c) 2010-2020 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;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mqtt.generic.values.ImageValue;
/**
* A MQTT camera, following the https://www.home-assistant.io/components/camera.mqtt/ specification.
*
* At the moment this only notifies the user that this feature is not yet supported.
*
* @author David Graeff - Initial contribution
*/
@NonNullByDefault
public class ComponentCamera extends AbstractComponent<ComponentCamera.ChannelConfiguration> {
public static final String cameraChannelID = "camera"; // Randomly chosen channel "ID"
/**
* Configuration class for MQTT component
*/
static class ChannelConfiguration extends BaseChannelConfiguration {
ChannelConfiguration() {
super("MQTT Camera");
}
protected String topic = "";
}
public ComponentCamera(CFactory.ComponentConfiguration componentConfiguration) {
super(componentConfiguration, ChannelConfiguration.class);
ImageValue value = new ImageValue();
buildChannel(cameraChannelID, value, channelConfiguration.name, componentConfiguration.getUpdateListener())//
.stateTopic(channelConfiguration.topic)//
.build();
}
}

View File

@@ -0,0 +1,40 @@
/**
* Copyright (c) 2010-2020 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;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* A MQTT climate component, following the https://www.home-assistant.io/components/climate.mqtt/ specification.
*
* At the moment this only notifies the user that this feature is not yet supported.
*
* @author David Graeff - Initial contribution
*/
@NonNullByDefault
public class ComponentClimate extends AbstractComponent<ComponentClimate.ChannelConfiguration> {
/**
* Configuration class for MQTT component
*/
static class ChannelConfiguration extends BaseChannelConfiguration {
ChannelConfiguration() {
super("MQTT HVAC");
}
}
public ComponentClimate(CFactory.ComponentConfiguration componentConfiguration) {
super(componentConfiguration, ChannelConfiguration.class);
throw new UnsupportedOperationException("Component:Climate not supported yet");
}
}

View File

@@ -0,0 +1,56 @@
/**
* Copyright (c) 2010-2020 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;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.values.RollershutterValue;
/**
* A MQTT Cover component, following the https://www.home-assistant.io/components/cover.mqtt/ specification.
*
* Only Open/Close/Stop works so far.
*
* @author David Graeff - Initial contribution
*/
@NonNullByDefault
public class ComponentCover extends AbstractComponent<ComponentCover.ChannelConfiguration> {
public static final String switchChannelID = "cover"; // Randomly chosen channel "ID"
/**
* Configuration class for MQTT component
*/
static class ChannelConfiguration extends BaseChannelConfiguration {
ChannelConfiguration() {
super("MQTT Cover");
}
protected @Nullable String state_topic;
protected @Nullable String command_topic;
protected String payload_open = "OPEN";
protected String payload_close = "CLOSE";
protected String payload_stop = "STOP";
}
public ComponentCover(CFactory.ComponentConfiguration componentConfiguration) {
super(componentConfiguration, ChannelConfiguration.class);
RollershutterValue value = new RollershutterValue(channelConfiguration.payload_open,
channelConfiguration.payload_close, channelConfiguration.payload_stop);
buildChannel(switchChannelID, value, channelConfiguration.name, componentConfiguration.getUpdateListener())//
.stateTopic(channelConfiguration.state_topic, channelConfiguration.value_template)//
.commandTopic(channelConfiguration.command_topic, channelConfiguration.retain)//
.build();
}
}

View File

@@ -0,0 +1,53 @@
/**
* Copyright (c) 2010-2020 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;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.values.OnOffValue;
/**
* A MQTT Fan component, following the https://www.home-assistant.io/components/fan.mqtt/ specification.
*
* Only ON/OFF is supported so far.
*
* @author David Graeff - Initial contribution
*/
@NonNullByDefault
public class ComponentFan extends AbstractComponent<ComponentFan.ChannelConfiguration> {
public static final String switchChannelID = "fan"; // Randomly chosen channel "ID"
/**
* Configuration class for MQTT component
*/
static class ChannelConfiguration extends BaseChannelConfiguration {
ChannelConfiguration() {
super("MQTT Fan");
}
protected @Nullable String state_topic;
protected String command_topic = "";
protected String payload_on = "ON";
protected String payload_off = "OFF";
}
public ComponentFan(CFactory.ComponentConfiguration componentConfiguration) {
super(componentConfiguration, ChannelConfiguration.class);
OnOffValue value = new OnOffValue(channelConfiguration.payload_on, channelConfiguration.payload_off);
buildChannel(switchChannelID, value, channelConfiguration.name, componentConfiguration.getUpdateListener())//
.stateTopic(channelConfiguration.state_topic, channelConfiguration.value_template)//
.commandTopic(channelConfiguration.command_topic, channelConfiguration.retain)//
.build();
}
}

View File

@@ -0,0 +1,175 @@
/**
* Copyright (c) 2010-2020 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;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ScheduledExecutorService;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
import org.openhab.binding.mqtt.generic.mapping.ColorMode;
import org.openhab.binding.mqtt.generic.values.ColorValue;
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
/**
* A MQTT light, following the https://www.home-assistant.io/components/light.mqtt/ specification.
*
* This class condenses the three state/command topics (for ON/OFF, Brightness, Color) to one
* color channel.
*
* @author David Graeff - Initial contribution
*/
@NonNullByDefault
public class ComponentLight extends AbstractComponent<ComponentLight.ChannelConfiguration>
implements ChannelStateUpdateListener {
public static final String switchChannelID = "light"; // Randomly chosen channel "ID"
public static final String brightnessChannelID = "brightness"; // Randomly chosen channel "ID"
public static final String colorChannelID = "color"; // Randomly chosen channel "ID"
/**
* Configuration class for MQTT component
*/
static class ChannelConfiguration extends BaseChannelConfiguration {
ChannelConfiguration() {
super("MQTT Light");
}
protected int brightness_scale = 255;
protected boolean optimistic = false;
protected @Nullable List<String> effect_list;
// Defines when on the payload_on is sent. Using last (the default) will send any style (brightness, color, etc)
// topics first and then a payload_on to the command_topic. Using first will send the payload_on and then any
// style topics. Using brightness will only send brightness commands instead of the payload_on to turn the light
// on.
protected String on_command_type = "last";
protected @Nullable String state_topic;
protected @Nullable String command_topic;
protected @Nullable String state_value_template;
protected @Nullable String brightness_state_topic;
protected @Nullable String brightness_command_topic;
protected @Nullable String brightness_value_template;
protected @Nullable String color_temp_state_topic;
protected @Nullable String color_temp_command_topic;
protected @Nullable String color_temp_value_template;
protected @Nullable String effect_command_topic;
protected @Nullable String effect_state_topic;
protected @Nullable String effect_value_template;
protected @Nullable String rgb_command_topic;
protected @Nullable String rgb_state_topic;
protected @Nullable String rgb_value_template;
protected @Nullable String rgb_command_template;
protected @Nullable String white_value_command_topic;
protected @Nullable String white_value_state_topic;
protected @Nullable String white_value_template;
protected @Nullable String xy_command_topic;
protected @Nullable String xy_state_topic;
protected @Nullable String xy_value_template;
protected String payload_on = "ON";
protected String payload_off = "OFF";
}
protected CChannel colorChannel;
protected CChannel switchChannel;
protected CChannel brightnessChannel;
private final @Nullable ChannelStateUpdateListener channelStateUpdateListener;
public ComponentLight(CFactory.ComponentConfiguration builder) {
super(builder, ChannelConfiguration.class);
this.channelStateUpdateListener = builder.getUpdateListener();
ColorValue value = new ColorValue(ColorMode.RGB, channelConfiguration.payload_on,
channelConfiguration.payload_off, 100);
// Create three MQTT subscriptions and use this class object as update listener
switchChannel = buildChannel(switchChannelID, value, channelConfiguration.name, this)//
// Some lights use the value_template field for the template, most use state_value_template
.stateTopic(channelConfiguration.state_topic, channelConfiguration.state_value_template,
channelConfiguration.value_template)//
.commandTopic(channelConfiguration.command_topic, channelConfiguration.retain)//
.build(false);
colorChannel = buildChannel(colorChannelID, value, channelConfiguration.name, this)//
.stateTopic(channelConfiguration.rgb_state_topic, channelConfiguration.rgb_value_template)//
.commandTopic(channelConfiguration.rgb_command_topic, channelConfiguration.retain)//
.build(false);
brightnessChannel = buildChannel(brightnessChannelID, value, channelConfiguration.name, this)//
.stateTopic(channelConfiguration.brightness_state_topic, channelConfiguration.brightness_value_template)//
.commandTopic(channelConfiguration.brightness_command_topic, channelConfiguration.retain)//
.build(false);
channels.put(colorChannelID, colorChannel);
}
@Override
public CompletableFuture<@Nullable Void> start(MqttBrokerConnection connection, ScheduledExecutorService scheduler,
int timeout) {
return Stream.of(switchChannel, brightnessChannel, colorChannel) //
.map(v -> v.start(connection, scheduler, timeout)) //
.reduce(CompletableFuture.completedFuture(null), (f, v) -> f.thenCompose(b -> v));
}
@Override
public CompletableFuture<@Nullable Void> stop() {
return Stream.of(switchChannel, brightnessChannel, colorChannel) //
.map(v -> v.stop()) //
.reduce(CompletableFuture.completedFuture(null), (f, v) -> f.thenCompose(b -> v));
}
/**
* Proxy method to condense all three MQTT subscriptions to one channel
*/
@Override
public void updateChannelState(ChannelUID channelUID, State value) {
ChannelStateUpdateListener listener = channelStateUpdateListener;
if (listener != null) {
listener.updateChannelState(colorChannel.getChannelUID(), value);
}
}
/**
* Proxy method to condense all three MQTT subscriptions to one channel
*/
@Override
public void postChannelCommand(ChannelUID channelUID, Command value) {
ChannelStateUpdateListener listener = channelStateUpdateListener;
if (listener != null) {
listener.postChannelCommand(colorChannel.getChannelUID(), value);
}
}
/**
* Proxy method to condense all three MQTT subscriptions to one channel
*/
@Override
public void triggerChannel(ChannelUID channelUID, String eventPayload) {
ChannelStateUpdateListener listener = channelStateUpdateListener;
if (listener != null) {
listener.triggerChannel(colorChannel.getChannelUID(), eventPayload);
}
}
}

View File

@@ -0,0 +1,60 @@
/**
* Copyright (c) 2010-2020 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;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.values.OnOffValue;
/**
* A MQTT lock, following the https://www.home-assistant.io/components/lock.mqtt/ specification.
*
* @author David Graeff - Initial contribution
*/
@NonNullByDefault
public class ComponentLock extends AbstractComponent<ComponentLock.ChannelConfiguration> {
public static final String switchChannelID = "lock"; // Randomly chosen channel "ID"
/**
* Configuration class for MQTT component
*/
static class ChannelConfiguration extends BaseChannelConfiguration {
ChannelConfiguration() {
super("MQTT Lock");
}
protected boolean optimistic = false;
protected String state_topic = "";
protected String payload_lock = "LOCK";
protected String payload_unlock = "UNLOCK";
protected @Nullable String command_topic;
}
public ComponentLock(CFactory.ComponentConfiguration componentConfiguration) {
super(componentConfiguration, ChannelConfiguration.class);
// We do not support all HomeAssistant quirks
if (channelConfiguration.optimistic && StringUtils.isNotBlank(channelConfiguration.state_topic)) {
throw new UnsupportedOperationException("Component:Lock does not support forced optimistic mode");
}
buildChannel(switchChannelID,
new OnOffValue(channelConfiguration.payload_lock, channelConfiguration.payload_unlock),
channelConfiguration.name, componentConfiguration.getUpdateListener())//
.stateTopic(channelConfiguration.state_topic, channelConfiguration.value_template)//
.commandTopic(channelConfiguration.command_topic, channelConfiguration.retain)//
.build();
}
}

View File

@@ -0,0 +1,75 @@
/**
* Copyright (c) 2010-2020 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;
import java.util.regex.Pattern;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.values.NumberValue;
import org.openhab.binding.mqtt.generic.values.TextValue;
import org.openhab.binding.mqtt.generic.values.Value;
/**
* A MQTT sensor, following the https://www.home-assistant.io/components/sensor.mqtt/ specification.
*
* @author David Graeff - Initial contribution
*/
@NonNullByDefault
public class ComponentSensor extends AbstractComponent<ComponentSensor.ChannelConfiguration> {
public static final String sensorChannelID = "sensor"; // Randomly chosen channel "ID"
private static final Pattern triggerIcons = Pattern.compile("^mdi:(toggle|gesture).*$");
/**
* Configuration class for MQTT component
*/
static class ChannelConfiguration extends BaseChannelConfiguration {
ChannelConfiguration() {
super("MQTT Sensor");
}
protected @Nullable String unit_of_measurement;
protected @Nullable String device_class;
protected boolean force_update = false;
protected int expire_after = 0;
protected String state_topic = "";
}
public ComponentSensor(CFactory.ComponentConfiguration componentConfiguration) {
super(componentConfiguration, ChannelConfiguration.class);
if (channelConfiguration.force_update) {
throw new UnsupportedOperationException("Component:Sensor does not support forced updates");
}
Value value;
String uom = channelConfiguration.unit_of_measurement;
if (uom != null && StringUtils.isNotBlank(uom)) {
value = new NumberValue(null, null, null, uom);
} else {
value = new TextValue();
}
String icon = channelConfiguration.icon;
boolean trigger = triggerIcons.matcher(icon).matches();
buildChannel(sensorChannelID, value, channelConfiguration.name, componentConfiguration.getUpdateListener())//
.stateTopic(channelConfiguration.state_topic, channelConfiguration.value_template)//
.trigger(trigger).build();
}
}

View File

@@ -0,0 +1,74 @@
/**
* Copyright (c) 2010-2020 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;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.values.OnOffValue;
/**
* A MQTT switch, following the https://www.home-assistant.io/components/switch.mqtt/ specification.
*
* @author David Graeff - Initial contribution
*/
@NonNullByDefault
public class ComponentSwitch extends AbstractComponent<ComponentSwitch.ChannelConfiguration> {
public static final String switchChannelID = "switch"; // Randomly chosen channel "ID"
/**
* Configuration class for MQTT component
*/
static class ChannelConfiguration extends BaseChannelConfiguration {
ChannelConfiguration() {
super("MQTT Switch");
}
protected @Nullable Boolean optimistic;
protected @Nullable String command_topic;
protected String state_topic = "";
protected @Nullable String state_on;
protected @Nullable String state_off;
protected String payload_on = "ON";
protected String payload_off = "OFF";
protected @Nullable String json_attributes_topic;
protected @Nullable String json_attributes_template;
}
public ComponentSwitch(CFactory.ComponentConfiguration componentConfiguration) {
super(componentConfiguration, ChannelConfiguration.class);
boolean optimistic = channelConfiguration.optimistic != null ? channelConfiguration.optimistic
: StringUtils.isBlank(channelConfiguration.state_topic);
if (optimistic && StringUtils.isNotBlank(channelConfiguration.state_topic)) {
throw new UnsupportedOperationException("Component:Switch does not support forced optimistic mode");
}
String state_on = channelConfiguration.state_on != null ? channelConfiguration.state_on
: channelConfiguration.payload_on;
String state_off = channelConfiguration.state_off != null ? channelConfiguration.state_off
: channelConfiguration.payload_off;
OnOffValue value = new OnOffValue(state_on, state_off, channelConfiguration.payload_on,
channelConfiguration.payload_off);
buildChannel(switchChannelID, value, "state", componentConfiguration.getUpdateListener())//
.stateTopic(channelConfiguration.state_topic, channelConfiguration.value_template)//
.commandTopic(channelConfiguration.command_topic, channelConfiguration.retain, channelConfiguration.qos)//
.build();
}
}

View File

@@ -0,0 +1,36 @@
/**
* Copyright (c) 2010-2020 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;
import java.lang.reflect.Type;
import com.google.gson.*;
/**
* The {@link ConnectionDeserializer} will de-serialize a connection-list
*
* see: https://www.home-assistant.io/integrations/sensor.mqtt/#connections
*
* @author Jan N. Klug - Initial contribution
*/
public class ConnectionDeserializer implements JsonDeserializer<BaseChannelConfiguration.Connection> {
@Override
public BaseChannelConfiguration.Connection deserialize(JsonElement json, Type typeOfT,
JsonDeserializationContext context) throws JsonParseException {
JsonArray list = json.getAsJsonArray();
BaseChannelConfiguration.Connection conn = new BaseChannelConfiguration.Connection();
conn.type = list.get(0).getAsString();
conn.identifier = list.get(1).getAsString();
return conn;
}
}

View File

@@ -0,0 +1,186 @@
/**
* Copyright (c) 2010-2020 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;
import java.lang.ref.WeakReference;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.AvailabilityTracker;
import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
import org.openhab.binding.mqtt.generic.TransformationServiceProvider;
import org.openhab.binding.mqtt.generic.utils.FutureCollector;
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
import org.openhab.core.io.transport.mqtt.MqttMessageSubscriber;
import org.openhab.core.thing.ThingUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
/**
* Responsible for subscribing to the HomeAssistant MQTT components wildcard topic, either
* in a time limited discovery mode or as a background discovery.
*
* @author David Graeff - Initial contribution
*/
@NonNullByDefault
public class DiscoverComponents implements MqttMessageSubscriber {
private final Logger logger = LoggerFactory.getLogger(DiscoverComponents.class);
private final ThingUID thingUID;
private final ScheduledExecutorService scheduler;
private final ChannelStateUpdateListener updateListener;
private final AvailabilityTracker tracker;
private final TransformationServiceProvider transformationServiceProvider;
protected final CompletableFuture<@Nullable Void> discoverFinishedFuture = new CompletableFuture<>();
private final Gson gson;
private @Nullable ScheduledFuture<?> stopDiscoveryFuture;
private WeakReference<@Nullable MqttBrokerConnection> connectionRef = new WeakReference<>(null);
protected @NonNullByDefault({}) ComponentDiscovered discoveredListener;
private int discoverTime;
private Set<String> topics = new HashSet<>();
/**
* Implement this to get notified of new components
*/
public static interface ComponentDiscovered {
void componentDiscovered(HaID homeAssistantTopicID, AbstractComponent<?> component);
}
/**
* Create a new discovery object.
*
* @param thingUID The Thing UID to perform the discovery for.
* @param scheduler A scheduler for timeouts
* @param channelStateUpdateListener Channel update listener. Usually the handler.
*/
public DiscoverComponents(ThingUID thingUID, ScheduledExecutorService scheduler,
ChannelStateUpdateListener channelStateUpdateListener, AvailabilityTracker tracker, Gson gson,
TransformationServiceProvider transformationServiceProvider) {
this.thingUID = thingUID;
this.scheduler = scheduler;
this.updateListener = channelStateUpdateListener;
this.gson = gson;
this.tracker = tracker;
this.transformationServiceProvider = transformationServiceProvider;
}
@Override
public void processMessage(String topic, byte[] payload) {
if (!topic.endsWith("/config")) {
return;
}
HaID haID = new HaID(topic);
String config = new String(payload);
AbstractComponent<?> component = null;
if (config.length() > 0) {
component = CFactory.createComponent(thingUID, haID, config, updateListener, tracker, gson,
transformationServiceProvider);
}
if (component != null) {
component.setConfigSeen();
logger.trace("Found HomeAssistant thing {} component {}", haID.objectID, haID.component);
if (discoveredListener != null) {
discoveredListener.componentDiscovered(haID, component);
}
} else {
logger.debug("Configuration of HomeAssistant thing {} invalid: {}", haID.objectID, config);
}
}
/**
* Start a components discovery.
*
* <p>
* We need to consider the case that the remote client is using node IDs
* and also the case that no node IDs are used.
* </p>
*
* @param connection A MQTT broker connection
* @param discoverTime The time in milliseconds for the discovery to run. Can be 0 to disable the
* timeout.
* You need to call {@link #stopDiscovery(MqttBrokerConnection)} at some
* point in that case.
* @param topicDescription Contains the object-id (=device id) and potentially a node-id as well.
* @param componentsDiscoveredListener Listener for results
* @return A future that completes normally after the given time in milliseconds or exceptionally on any error.
* Completes immediately if the timeout is disabled.
*/
public CompletableFuture<@Nullable Void> startDiscovery(MqttBrokerConnection connection, int discoverTime,
Set<HaID> topicDescriptions, ComponentDiscovered componentsDiscoveredListener) {
this.topics = topicDescriptions.stream().map(id -> id.getTopic("config")).collect(Collectors.toSet());
this.discoverTime = discoverTime;
this.discoveredListener = componentsDiscoveredListener;
this.connectionRef = new WeakReference<>(connection);
// Subscribe to the wildcard topic and start receive MQTT retained topics
this.topics.parallelStream().map(t -> connection.subscribe(t, this)).collect(FutureCollector.allOf())
.thenRun(this::subscribeSuccess).exceptionally(this::subscribeFail);
return discoverFinishedFuture;
}
private void subscribeSuccess() {
final MqttBrokerConnection connection = connectionRef.get();
// Set up a scheduled future that will stop the discovery after the given time
if (connection != null && discoverTime > 0) {
this.stopDiscoveryFuture = scheduler.schedule(() -> {
this.stopDiscoveryFuture = null;
this.topics.parallelStream().forEach(t -> connection.unsubscribe(t, this));
this.discoveredListener = null;
discoverFinishedFuture.complete(null);
}, discoverTime, TimeUnit.MILLISECONDS);
} else {
// No timeout -> complete immediately
discoverFinishedFuture.complete(null);
}
}
private @Nullable Void subscribeFail(Throwable e) {
final ScheduledFuture<?> scheduledFuture = this.stopDiscoveryFuture;
if (scheduledFuture != null) { // Cancel timeout
scheduledFuture.cancel(false);
this.stopDiscoveryFuture = null;
}
this.discoveredListener = null;
final MqttBrokerConnection connection = connectionRef.get();
if (connection != null) {
this.topics.parallelStream().forEach(t -> connection.unsubscribe(t, this));
connectionRef.clear();
}
discoverFinishedFuture.completeExceptionally(e);
return null;
}
/**
* Stops an ongoing discovery or do nothing if no discovery is running.
*
* @param connection A MQTT broker connection
*/
public void stopDiscovery() {
subscribeFail(new Throwable("Stopped"));
}
}

View File

@@ -0,0 +1,264 @@
/**
* Copyright (c) 2010-2020 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;
import java.util.ArrayList;
import java.util.Collection;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.util.UIDUtils;
/**
* HomeAssistant MQTT components use a specific MQTT topic layout,
* starting with a base prefix (usually "homeassistant"),
* followed by the component id, an optional node id and the object id.
*
* This helper class can split up an MQTT topic into such parts.
* <p>
* Implementation note: This is an immutable class.
*
* @author David Graeff - Initial contribution
*/
@NonNullByDefault
public class HaID {
public final String baseTopic;
public final String component;
public final String nodeID;
public final String objectID;
private final String topic;
/**
* Creates a {@link HaID} object for a given HomeAssistant MQTT topic.
*
* @param mqttTopic A topic like "homeassistant/binary_sensor/garden/config" or
* "homeassistant/binary_sensor/0/garden/config"
*/
public HaID(String mqttTopic) {
String[] strings = mqttTopic.split("/");
if (strings.length < 4 || strings.length > 5) {
throw new IllegalArgumentException("MQTT topic not a HomeAssistant topic (wrong length)!");
}
if (!"config".equals(strings[strings.length - 1])) {
throw new IllegalArgumentException("MQTT topic not a HomeAssistant topic ('config' missing)!");
}
baseTopic = strings[0];
component = strings[1];
if (strings.length == 5) {
nodeID = strings[2];
objectID = strings[3];
} else {
nodeID = "";
objectID = strings[2];
}
this.topic = createTopic(this);
}
public HaID() {
this("", "", "", "");
}
/**
* Creates a {@link HaID} by providing all components separately.
*
* @param baseTopic The base topic. Usually "homeassistant".
* @param objectID The object ID
* @param nodeID The node ID (can be the empty string)
* @param component The component ID
*/
private HaID(String baseTopic, String objectID, String nodeID, String component) {
this.baseTopic = baseTopic;
this.objectID = objectID;
this.nodeID = nodeID;
this.component = component;
this.topic = createTopic(this);
}
private static final String createTopic(HaID id) {
StringBuilder str = new StringBuilder();
str.append(id.baseTopic).append('/').append(id.component).append('/');
if (StringUtils.isNotBlank(id.nodeID)) {
str.append(id.nodeID).append('/');
}
str.append(id.objectID).append('/');
return str.toString();
}
/**
* Extract the HaID information from a channel configuration.
* <p>
* <code>objectid</code>, <code>nodeid</code>, and <code>component</code> values are fetched from the configuration.
*
* @param baseTopic
* @param config
* @return newly created HaID
*/
public static HaID fromConfig(String baseTopic, Configuration config) {
String component = (String) config.get("component");
String nodeID = (String) config.getProperties().getOrDefault("nodeid", "");
String objectID = (String) config.get("objectid");
return new HaID(baseTopic, objectID, nodeID, component);
}
/**
* Add the HaID information to a channel configuration.
* <p>
* <code>objectid</code>, <code>nodeid</code>, and <code>component</code> values are added to the configuration.
*
* @param config
* @return the modified configuration
*/
public Configuration toConfig(Configuration config) {
config.put("objectid", objectID);
config.put("nodeid", nodeID);
config.put("component", component);
return config;
}
/**
* Extract the HaID information from a thing configuration.
* <p>
* <code>basetpoic</code> and <code>objectid</code> are taken from the configuration.
* The <code>objectid</code> string may be in the form <code>nodeid/objectid</code>.
* <p>
* The <code>component</code> component in the resulting HaID will be set to <code>+</code>.
* This enables the HaID to be used as an mqtt subscription topic.
*
* @param config
* @return newly created HaID
*/
public static Collection<HaID> fromConfig(HandlerConfiguration config) {
Collection<HaID> result = new ArrayList<>();
for (String topic : config.topics) {
String[] parts = topic.split("/");
switch (parts.length) {
case 2:
result.add(new HaID(config.basetopic, parts[1], "", parts[0]));
break;
case 3:
result.add(new HaID(config.basetopic, parts[2], parts[1], parts[0]));
break;
default:
throw new IllegalArgumentException(
"Bad configuration. topic must be <component>/<objectId> or <component>/<nodeId>/<objectId>!");
}
}
return result;
}
/**
* Return the topic to put into the HandlerConfiguration for this component.
* <p>
* <code>objectid</code> in the thing configuration will be
* <code>nodeID/objectID<code> from the HaID, if <code>nodeID</code> is not empty.
* <p>
*
* @return the short topic.
*/
public String toShortTopic() {
String objectID = this.objectID;
if (StringUtils.isNotBlank(nodeID)) {
objectID = nodeID + "/" + objectID;
}
return component + "/" + objectID;
}
/**
* The default group id is the unique_id of the component, given in the config-json.
* If the unique id is not set, then a fallback is constructed from the HaID information.
*
* @return group id
*/
public String getGroupId(@Nullable final String uniqueId) {
String result = uniqueId;
// the null test is only here so the compile knows, result is not null afterwards
if (result == null || StringUtils.isBlank(result)) {
StringBuilder str = new StringBuilder();
if (StringUtils.isNotBlank(nodeID)) {
str.append(nodeID).append('_');
}
str.append(objectID).append('_').append(component);
result = str.toString();
}
return UIDUtils.encode(result);
}
/**
* Return a topic, which can be used for a mqtt subscription.
* Defined values for suffix are:
* <ul>
* <li>config</li>
* <li>state</li>
* </ul>
*
* @return fallback group id
*/
public String getTopic(String suffix) {
return topic + suffix;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + baseTopic.hashCode();
result = prime * result + component.hashCode();
result = prime * result + nodeID.hashCode();
result = prime * result + objectID.hashCode();
return result;
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
HaID other = (HaID) obj;
if (!baseTopic.equals(other.baseTopic)) {
return false;
}
if (!component.equals(other.component)) {
return false;
}
if (!nodeID.equals(other.nodeID)) {
return false;
}
if (!objectID.equals(other.objectID)) {
return false;
}
return true;
}
@Override
public String toString() {
return topic;
}
}

View File

@@ -0,0 +1,87 @@
/**
* Copyright (c) 2010-2020 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;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mqtt.homeassistant.internal.handler.HomeAssistantThingHandler;
/**
* The {@link HomeAssistantThingHandler} manages Things that are responsible for
* HomeAssistant MQTT components.
* This class contains the necessary configuration for such a Thing handler.
*
* @author David Graeff - Initial contribution
*/
@NonNullByDefault
public class HandlerConfiguration {
/**
* hint: cannot be final, or <code>getConfigAs</code> will not work.
* The MQTT prefix topic
*/
public String basetopic;
/**
* hint: cannot be final, or <code>getConfigAs</code> will not work.
* List of configuration topics.
* <ul>
* <li>
* Each topic is gets the base topic prepended.
* </li>
* <li>
* each topic has:
* <ol>
* <li>
* <code>component</code> (e.g. "switch", "light", ...)
* </li>
* <li>
* <code>node_id</code> (optional)
* </li>
* <li>
* <code>object_id</code> This is only to allow for separate topics for each device
* </li>
* <li>
* "config"
* </li>
* </ol>
* </li>
* </ul>
*
*/
public List<String> topics;
public HandlerConfiguration() {
this("homeassistant", Collections.emptyList());
}
public HandlerConfiguration(String basetopic, List<String> topics) {
super();
this.basetopic = basetopic;
this.topics = topics;
}
/**
* Add the <code>basetopic</code> and <code>objectid</code> to the properties.
*
* @param properties
* @return the modified properties
*/
public <T extends Map<String, Object>> T appendToProperties(T properties) {
properties.put("basetopic", basetopic);
properties.put("topics", topics);
return properties;
}
}

View File

@@ -0,0 +1,92 @@
/**
* Copyright (c) 2010-2020 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;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
/**
* JsonTypeAdapter which will read a single string or a string list
*
* see: https://www.home-assistant.io/components/binary_sensor.mqtt/ -> device / identifiers
*
* @author Jochen Klein - Initial contribution
*/
public class ListOrStringDeserializer extends TypeAdapter<List<String>> {
@Override
public void write(@Nullable JsonWriter out, @Nullable List<String> value) throws IOException {
Objects.requireNonNull(out);
if (value == null) {
out.nullValue();
return;
}
out.beginArray();
for (String str : value) {
out.jsonValue(str);
}
out.endArray();
}
@Override
public @Nullable List<String> read(@Nullable JsonReader in) throws IOException {
Objects.requireNonNull(in);
JsonToken peek = in.peek();
switch (peek) {
case NULL:
in.nextNull();
return null;
case STRING:
return Arrays.asList(in.nextString());
case BEGIN_ARRAY:
return readList(in);
default:
throw new IOException("unexpected token " + peek + ". Array of string or string expected");
}
}
private @NonNull List<String> readList(@NonNull JsonReader in) throws IOException {
in.beginArray();
List<String> result = new ArrayList<>();
JsonToken peek = in.peek();
while (peek != JsonToken.END_ARRAY) {
if (peek == JsonToken.STRING) {
result.add(in.nextString());
} else {
throw new IOException("unexpected token " + peek + ". Array of string or string expected");
}
peek = in.peek();
}
in.endArray();
return result;
}
}

View File

@@ -0,0 +1,257 @@
/**
* Copyright (c) 2010-2020 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;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mqtt.generic.tools.JsonReaderDelegate;
import com.google.gson.stream.JsonReader;
/**
* JsonReader which will replace specific names.
*
* @author Jochen Klein - Initial contribution
*/
@NonNullByDefault
public class MappingJsonReader extends JsonReaderDelegate {
private static final Map<String, String> ABBREVIATIONS = new HashMap<>();
private static final Map<String, String> DEVICE_ABBREVIATIONS = new HashMap<>();
static {
ABBREVIATIONS.put("act_t", "action_topic");
ABBREVIATIONS.put("act_tpl", "action_template");
ABBREVIATIONS.put("atype", "automation_type");
ABBREVIATIONS.put("aux_cmd_t", "aux_command_topic");
ABBREVIATIONS.put("aux_stat_tpl", "aux_state_template");
ABBREVIATIONS.put("aux_stat_t", "aux_state_topic");
ABBREVIATIONS.put("avty", "availability");
ABBREVIATIONS.put("avty_t", "availability_topic");
ABBREVIATIONS.put("away_mode_cmd_t", "away_mode_command_topic");
ABBREVIATIONS.put("away_mode_stat_tpl", "away_mode_state_template");
ABBREVIATIONS.put("away_mode_stat_t", "away_mode_state_topic");
ABBREVIATIONS.put("b_tpl", "blue_template");
ABBREVIATIONS.put("bri_cmd_t", "brightness_command_topic");
ABBREVIATIONS.put("bri_scl", "brightness_scale");
ABBREVIATIONS.put("bri_stat_t", "brightness_state_topic");
ABBREVIATIONS.put("bri_tpl", "brightness_template");
ABBREVIATIONS.put("bri_val_tpl", "brightness_value_template");
ABBREVIATIONS.put("clr_temp_cmd_tpl", "color_temp_command_template");
ABBREVIATIONS.put("bat_lev_t", "battery_level_topic");
ABBREVIATIONS.put("bat_lev_tpl", "battery_level_template");
ABBREVIATIONS.put("chrg_t", "charging_topic");
ABBREVIATIONS.put("chrg_tpl", "charging_template");
ABBREVIATIONS.put("clr_temp_cmd_t", "color_temp_command_topic");
ABBREVIATIONS.put("clr_temp_stat_t", "color_temp_state_topic");
ABBREVIATIONS.put("clr_temp_tpl", "color_temp_template");
ABBREVIATIONS.put("clr_temp_val_tpl", "color_temp_value_template");
ABBREVIATIONS.put("cln_t", "cleaning_topic");
ABBREVIATIONS.put("cln_tpl", "cleaning_template");
ABBREVIATIONS.put("cmd_off_tpl", "command_off_template");
ABBREVIATIONS.put("cmd_on_tpl", "command_on_template");
ABBREVIATIONS.put("cmd_t", "command_topic");
ABBREVIATIONS.put("cmd_tpl", "command_template");
ABBREVIATIONS.put("cod_arm_req", "code_arm_required");
ABBREVIATIONS.put("cod_dis_req", "code_disarm_required");
ABBREVIATIONS.put("curr_temp_t", "current_temperature_topic");
ABBREVIATIONS.put("curr_temp_tpl", "current_temperature_template");
ABBREVIATIONS.put("dev", "device");
ABBREVIATIONS.put("dev_cla", "device_class");
ABBREVIATIONS.put("dock_t", "docked_topic");
ABBREVIATIONS.put("dock_tpl", "docked_template");
ABBREVIATIONS.put("err_t", "error_topic");
ABBREVIATIONS.put("err_tpl", "error_template");
ABBREVIATIONS.put("fanspd_t", "fan_speed_topic");
ABBREVIATIONS.put("fanspd_tpl", "fan_speed_template");
ABBREVIATIONS.put("fanspd_lst", "fan_speed_list");
ABBREVIATIONS.put("flsh_tlng", "flash_time_long");
ABBREVIATIONS.put("flsh_tsht", "flash_time_short");
ABBREVIATIONS.put("fx_cmd_t", "effect_command_topic");
ABBREVIATIONS.put("fx_list", "effect_list");
ABBREVIATIONS.put("fx_stat_t", "effect_state_topic");
ABBREVIATIONS.put("fx_tpl", "effect_template");
ABBREVIATIONS.put("fx_val_tpl", "effect_value_template");
ABBREVIATIONS.put("exp_aft", "expire_after");
ABBREVIATIONS.put("fan_mode_cmd_t", "fan_mode_command_topic");
ABBREVIATIONS.put("fan_mode_stat_tpl", "fan_mode_state_template");
ABBREVIATIONS.put("fan_mode_stat_t", "fan_mode_state_topic");
ABBREVIATIONS.put("frc_upd", "force_update");
ABBREVIATIONS.put("g_tpl", "green_template");
ABBREVIATIONS.put("hold_cmd_t", "hold_command_topic");
ABBREVIATIONS.put("hold_stat_tpl", "hold_state_template");
ABBREVIATIONS.put("hold_stat_t", "hold_state_topic");
ABBREVIATIONS.put("hs_cmd_t", "hs_command_topic");
ABBREVIATIONS.put("hs_stat_t", "hs_state_topic");
ABBREVIATIONS.put("hs_val_tpl", "hs_value_template");
ABBREVIATIONS.put("ic", "icon");
ABBREVIATIONS.put("init", "initial");
ABBREVIATIONS.put("json_attr", "json_attributes");
ABBREVIATIONS.put("json_attr_t", "json_attributes_topic");
ABBREVIATIONS.put("json_attr_tpl", "json_attributes_template");
ABBREVIATIONS.put("max_mirs", "max_mireds");
ABBREVIATIONS.put("min_mirs", "min_mireds");
ABBREVIATIONS.put("max_temp", "max_temp");
ABBREVIATIONS.put("min_temp", "min_temp");
ABBREVIATIONS.put("mode_cmd_t", "mode_command_topic");
ABBREVIATIONS.put("mode_stat_tpl", "mode_state_template");
ABBREVIATIONS.put("mode_stat_t", "mode_state_topic");
ABBREVIATIONS.put("name", "name");
ABBREVIATIONS.put("off_dly", "off_delay");
ABBREVIATIONS.put("on_cmd_type", "on_command_type");
ABBREVIATIONS.put("opt", "optimistic");
ABBREVIATIONS.put("osc_cmd_t", "oscillation_command_topic");
ABBREVIATIONS.put("osc_stat_t", "oscillation_state_topic");
ABBREVIATIONS.put("osc_val_tpl", "oscillation_value_template");
ABBREVIATIONS.put("pl", "payload");
ABBREVIATIONS.put("pl_arm_away", "payload_arm_away");
ABBREVIATIONS.put("pl_arm_home", "payload_arm_home");
ABBREVIATIONS.put("pl_arm_custom_b", "payload_arm_custom_bypass");
ABBREVIATIONS.put("pl_arm_nite", "payload_arm_night");
ABBREVIATIONS.put("pl_avail", "payload_available");
ABBREVIATIONS.put("pl_cln_sp", "payload_clean_spot");
ABBREVIATIONS.put("pl_cls", "payload_close");
ABBREVIATIONS.put("pl_disarm", "payload_disarm");
ABBREVIATIONS.put("pl_hi_spd", "payload_high_speed");
ABBREVIATIONS.put("pl_home", "payload_home");
ABBREVIATIONS.put("pl_lock", "payload_lock");
ABBREVIATIONS.put("pl_loc", "payload_locate");
ABBREVIATIONS.put("pl_lo_spd", "payload_low_speed");
ABBREVIATIONS.put("pl_med_spd", "payload_medium_speed");
ABBREVIATIONS.put("pl_not_avail", "payload_not_available");
ABBREVIATIONS.put("pl_not_home", "payload_not_home");
ABBREVIATIONS.put("pl_off", "payload_off");
ABBREVIATIONS.put("pl_off_spd", "payload_off_speed");
ABBREVIATIONS.put("pl_on", "payload_on");
ABBREVIATIONS.put("pl_open", "payload_open");
ABBREVIATIONS.put("pl_osc_off", "payload_oscillation_off");
ABBREVIATIONS.put("pl_osc_on", "payload_oscillation_on");
ABBREVIATIONS.put("pl_paus", "payload_pause");
ABBREVIATIONS.put("pl_stop", "payload_stop");
ABBREVIATIONS.put("pl_strt", "payload_start");
ABBREVIATIONS.put("pl_stpa", "payload_start_pause");
ABBREVIATIONS.put("pl_ret", "payload_return_to_base");
ABBREVIATIONS.put("pl_toff", "payload_turn_off");
ABBREVIATIONS.put("pl_ton", "payload_turn_on");
ABBREVIATIONS.put("pl_unlk", "payload_unlock");
ABBREVIATIONS.put("pos_clsd", "position_closed");
ABBREVIATIONS.put("pos_open", "position_open");
ABBREVIATIONS.put("pow_cmd_t", "power_command_topic");
ABBREVIATIONS.put("pow_stat_t", "power_state_topic");
ABBREVIATIONS.put("pow_stat_tpl", "power_state_template");
ABBREVIATIONS.put("r_tpl", "red_template");
ABBREVIATIONS.put("ret", "retain");
ABBREVIATIONS.put("rgb_cmd_tpl", "rgb_command_template");
ABBREVIATIONS.put("rgb_cmd_t", "rgb_command_topic");
ABBREVIATIONS.put("rgb_stat_t", "rgb_state_topic");
ABBREVIATIONS.put("rgb_val_tpl", "rgb_value_template");
ABBREVIATIONS.put("send_cmd_t", "send_command_topic");
ABBREVIATIONS.put("send_if_off", "send_if_off");
ABBREVIATIONS.put("set_fan_spd_t", "set_fan_speed_topic");
ABBREVIATIONS.put("set_pos_tpl", "set_position_template");
ABBREVIATIONS.put("set_pos_t", "set_position_topic");
ABBREVIATIONS.put("pos_t", "position_topic");
ABBREVIATIONS.put("spd_cmd_t", "speed_command_topic");
ABBREVIATIONS.put("spd_stat_t", "speed_state_topic");
ABBREVIATIONS.put("spd_val_tpl", "speed_value_template");
ABBREVIATIONS.put("spds", "speeds");
ABBREVIATIONS.put("src_type", "source_type");
ABBREVIATIONS.put("stat_clsd", "state_closed");
ABBREVIATIONS.put("stat_closing", "state_closing");
ABBREVIATIONS.put("stat_off", "state_off");
ABBREVIATIONS.put("stat_on", "state_on");
ABBREVIATIONS.put("stat_open", "state_open");
ABBREVIATIONS.put("stat_opening", "state_opening");
ABBREVIATIONS.put("stat_locked", "state_locked");
ABBREVIATIONS.put("stat_unlocked", "state_unlocked");
ABBREVIATIONS.put("stat_t", "state_topic");
ABBREVIATIONS.put("stat_tpl", "state_template");
ABBREVIATIONS.put("stat_val_tpl", "state_value_template");
ABBREVIATIONS.put("stype", "subtype");
ABBREVIATIONS.put("sup_feat", "supported_features");
ABBREVIATIONS.put("swing_mode_cmd_t", "swing_mode_command_topic");
ABBREVIATIONS.put("swing_mode_stat_tpl", "swing_mode_state_template");
ABBREVIATIONS.put("swing_mode_stat_t", "swing_mode_state_topic");
ABBREVIATIONS.put("temp_cmd_t", "temperature_command_topic");
ABBREVIATIONS.put("temp_hi_cmd_t", "temperature_high_command_topic");
ABBREVIATIONS.put("temp_hi_stat_tpl", "temperature_high_state_template");
ABBREVIATIONS.put("temp_hi_stat_t", "temperature_high_state_topic");
ABBREVIATIONS.put("temp_lo_cmd_t", "temperature_low_command_topic");
ABBREVIATIONS.put("temp_lo_stat_tpl", "temperature_low_state_template");
ABBREVIATIONS.put("temp_lo_stat_t", "temp_lo_stat_t");
ABBREVIATIONS.put("temp_stat_tpl", "temperature_state_template");
ABBREVIATIONS.put("temp_stat_t", "temperature_state_topic");
ABBREVIATIONS.put("temp_unit", "temperature_unit");
ABBREVIATIONS.put("tilt_clsd_val", "tilt_closed_value");
ABBREVIATIONS.put("tilt_cmd_t", "tilt_command_topic");
ABBREVIATIONS.put("tilt_inv_stat", "tilt_invert_state");
ABBREVIATIONS.put("tilt_max", "tilt_max");
ABBREVIATIONS.put("tilt_min", "tilt_min");
ABBREVIATIONS.put("tilt_opnd_val", "tilt_opened_value");
ABBREVIATIONS.put("tilt_opt", "tilt_optimistic");
ABBREVIATIONS.put("tilt_status_t", "tilt_status_topic");
ABBREVIATIONS.put("tilt_status_tpl", "tilt_status_template");
ABBREVIATIONS.put("t", "topic");
ABBREVIATIONS.put("uniq_id", "unique_id");
ABBREVIATIONS.put("unit_of_meas", "unit_of_measurement");
ABBREVIATIONS.put("val_tpl", "value_template");
ABBREVIATIONS.put("whit_val_cmd_t", "white_value_command_topic");
ABBREVIATIONS.put("whit_val_scl", "white_value_scale");
ABBREVIATIONS.put("whit_val_stat_t", "white_value_state_topic");
ABBREVIATIONS.put("whit_val_tpl", "white_value_template");
ABBREVIATIONS.put("xy_cmd_t", "xy_command_topic");
ABBREVIATIONS.put("xy_stat_t", "xy_state_topic");
ABBREVIATIONS.put("xy_val_tpl", "xy_value_template");
DEVICE_ABBREVIATIONS.put("cns", "connections");
DEVICE_ABBREVIATIONS.put("ids", "identifiers");
DEVICE_ABBREVIATIONS.put("name", "name");
DEVICE_ABBREVIATIONS.put("mf", "manufacturer");
DEVICE_ABBREVIATIONS.put("mdl", "model");
DEVICE_ABBREVIATIONS.put("sw", "sw_version");
}
private final Map<String, String> mapping;
/**
*
* @param delegate
* @return return a JsonReader which replaces all config abbreviations
*/
public static MappingJsonReader getConfigMapper(JsonReader delegate) {
return new MappingJsonReader(JsonReaderDelegate.getDelegate(delegate), ABBREVIATIONS);
}
/**
*
* @param delegate
* @return return a JsonReader which replaces all config.device abbreviations
*/
public static MappingJsonReader getDeviceMapper(JsonReader delegate) {
return new MappingJsonReader(JsonReaderDelegate.getDelegate(delegate), DEVICE_ABBREVIATIONS);
}
private MappingJsonReader(JsonReader delegate, Map<String, String> mapping) {
super(delegate);
this.mapping = mapping;
}
@Override
public String nextName() throws IOException {
String name = super.nextName();
return mapping.getOrDefault(name, name);
}
}

View File

@@ -0,0 +1,221 @@
/**
* Copyright (c) 2010-2020 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.discovery;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.discovery.AbstractMQTTDiscovery;
import org.openhab.binding.mqtt.discovery.MQTTTopicDiscoveryService;
import org.openhab.binding.mqtt.generic.MqttChannelTypeProvider;
import org.openhab.binding.mqtt.homeassistant.generic.internal.MqttBindingConstants;
import org.openhab.binding.mqtt.homeassistant.internal.BaseChannelConfiguration;
import org.openhab.binding.mqtt.homeassistant.internal.ChannelConfigurationTypeAdapterFactory;
import org.openhab.binding.mqtt.homeassistant.internal.HaID;
import org.openhab.binding.mqtt.homeassistant.internal.HandlerConfiguration;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.type.ThingType;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
/**
* The {@link HomeAssistantDiscovery} is responsible for discovering device nodes that follow the
* Home Assistant MQTT discovery convention (https://www.home-assistant.io/docs/mqtt/discovery/).
*
* @author David Graeff - Initial contribution
*/
@Component(immediate = true, 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<>();
protected final Map<String, DiscoveryResult> results = new ConcurrentHashMap<>();
private @Nullable ScheduledFuture<?> future;
private final Gson gson;
public static final Map<String, String> HA_COMP_TO_NAME = new TreeMap<>();
{
HA_COMP_TO_NAME.put("alarm_control_panel", "Alarm Control Panel");
HA_COMP_TO_NAME.put("binary_sensor", "Sensor");
HA_COMP_TO_NAME.put("camera", "Camera");
HA_COMP_TO_NAME.put("cover", "Blind");
HA_COMP_TO_NAME.put("fan", "Fan");
HA_COMP_TO_NAME.put("climate", "Climate Control");
HA_COMP_TO_NAME.put("light", "Light");
HA_COMP_TO_NAME.put("lock", "Lock");
HA_COMP_TO_NAME.put("sensor", "Sensor");
HA_COMP_TO_NAME.put("switch", "Switch");
}
static final String BASE_TOPIC = "homeassistant";
@NonNullByDefault({})
protected MqttChannelTypeProvider typeProvider;
@NonNullByDefault({})
protected MQTTTopicDiscoveryService mqttTopicDiscovery;
public HomeAssistantDiscovery() {
super(null, 3, true, BASE_TOPIC + "/#");
this.gson = new GsonBuilder().registerTypeAdapterFactory(new ChannelConfigurationTypeAdapterFactory()).create();
}
@Reference
public void setMQTTTopicDiscoveryService(MQTTTopicDiscoveryService service) {
mqttTopicDiscovery = service;
}
public void unsetMQTTTopicDiscoveryService(@Nullable MQTTTopicDiscoveryService service) {
mqttTopicDiscovery.unsubscribe(this);
this.mqttTopicDiscovery = null;
}
@Override
protected MQTTTopicDiscoveryService getDiscoveryService() {
return mqttTopicDiscovery;
}
@Reference
protected void setTypeProvider(MqttChannelTypeProvider provider) {
this.typeProvider = provider;
}
protected void unsetTypeProvider(MqttChannelTypeProvider provider) {
this.typeProvider = null;
}
@Override
public Set<@NonNull ThingTypeUID> getSupportedThingTypes() {
return typeProvider.getThingTypeUIDs();
}
@Override
public void receivedMessage(ThingUID connectionBridge, MqttBrokerConnection connection, String topic,
byte[] payload) {
resetTimeout();
// For HomeAssistant we need to subscribe to a wildcard topic, because topics can either be:
// homeassistant/<component>/<node_id>/<object_id>/config OR
// homeassistant/<component>/<object_id>/config.
// We check for the last part to filter all non-config topics out.
if (!topic.endsWith("/config")) {
return;
}
// Reset the found-component timer.
// We will collect components for the thing label description for another 2 seconds.
final ScheduledFuture<?> future = this.future;
if (future != null) {
future.cancel(false);
}
this.future = scheduler.schedule(this::publishResults, 2, TimeUnit.SECONDS);
BaseChannelConfiguration config = BaseChannelConfiguration
.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);
final ThingUID thingUID = new ThingUID(typeID, connectionBridge, thingID);
thingIDPerTopic.put(topic, thingUID);
// 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 String componentNames = components.stream().map(id -> id.component)
.map(c -> HA_COMP_TO_NAME.getOrDefault(c, c)).collect(Collectors.joining(", "));
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);
// Because we need the new properties map with the updated "components" list
results.put(thingUID.getAsString(),
DiscoveryResultBuilder.create(thingUID).withProperties(properties).withRepresentationProperty(thingID)
.withBridge(connectionBridge).withLabel(config.getThingName() + " (" + componentNames + ")")
.build());
}
protected void publishResults() {
Collection<DiscoveryResult> localResults;
localResults = new ArrayList<>(results.values());
results.clear();
componentsPerThingID.clear();
for (DiscoveryResult result : localResults) {
final ThingTypeUID typeID = result.getThingTypeUID();
ThingType type = typeProvider.derive(typeID, MqttBindingConstants.HOMEASSISTANT_MQTT_THING).build();
typeProvider.setThingTypeIfAbsent(typeID, type);
thingDiscovered(result);
}
}
@Override
public void topicVanished(ThingUID connectionBridge, MqttBrokerConnection connection, String topic) {
if (!topic.endsWith("/config")) {
return;
}
if (thingIDPerTopic.containsKey(topic)) {
ThingUID thingUID = thingIDPerTopic.remove(topic);
final String thingID = thingUID.getId();
HaID haID = new HaID(topic);
Set<HaID> components = componentsPerThingID.getOrDefault(thingID, Collections.emptySet());
components.remove(haID);
if (components.isEmpty()) {
thingRemoved(thingUID);
}
}
}
}

View File

@@ -0,0 +1,326 @@
/**
* Copyright (c) 2010-2020 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.handler;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.AbstractMQTTThingHandler;
import org.openhab.binding.mqtt.generic.ChannelState;
import org.openhab.binding.mqtt.generic.MqttChannelTypeProvider;
import org.openhab.binding.mqtt.generic.TransformationServiceProvider;
import org.openhab.binding.mqtt.generic.tools.DelayedBatchProcessing;
import org.openhab.binding.mqtt.generic.utils.FutureCollector;
import org.openhab.binding.mqtt.homeassistant.generic.internal.MqttBindingConstants;
import org.openhab.binding.mqtt.homeassistant.internal.AbstractComponent;
import org.openhab.binding.mqtt.homeassistant.internal.CChannel;
import org.openhab.binding.mqtt.homeassistant.internal.CFactory;
import org.openhab.binding.mqtt.homeassistant.internal.ChannelConfigurationTypeAdapterFactory;
import org.openhab.binding.mqtt.homeassistant.internal.DiscoverComponents;
import org.openhab.binding.mqtt.homeassistant.internal.DiscoverComponents.ComponentDiscovered;
import org.openhab.binding.mqtt.homeassistant.internal.HaID;
import org.openhab.binding.mqtt.homeassistant.internal.HandlerConfiguration;
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.type.ChannelDefinition;
import org.openhab.core.thing.type.ChannelGroupDefinition;
import org.openhab.core.thing.type.ChannelGroupType;
import org.openhab.core.thing.type.ThingType;
import org.openhab.core.thing.util.ThingHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
/**
* Handles HomeAssistant MQTT object things. Such an HA Object can have multiple HA Components with different instances
* of those Components. This handler auto-discovers all available Components and Component Instances and
* adds any new appearing components over time.<br>
* <br>
*
* The specification does not cover the case of disappearing Components. This handler doesn't as well therefore.<br>
* <br>
*
* A Component Instance equals an ESH Channel Group and the Component parts equal ESH Channels.<br>
* <br>
*
* If a Components configuration changes, the known ChannelGroupType and ChannelTypes are replaced with the new ones.
*
* @author David Graeff - Initial contribution
*/
@NonNullByDefault
public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
implements ComponentDiscovered, Consumer<List<AbstractComponent<?>>> {
public static final String AVAILABILITY_CHANNEL = "availability";
private final Logger logger = LoggerFactory.getLogger(HomeAssistantThingHandler.class);
protected final MqttChannelTypeProvider channelTypeProvider;
public final int attributeReceiveTimeout;
protected final DelayedBatchProcessing<AbstractComponent<?>> delayedProcessing;
protected final DiscoverComponents discoverComponents;
private final Gson gson;
protected final Map<String, AbstractComponent<?>> haComponents = new HashMap<>();
protected HandlerConfiguration config = new HandlerConfiguration();
private Set<HaID> discoveryHomeAssistantIDs = new HashSet<>();
protected final TransformationServiceProvider transformationServiceProvider;
private boolean started;
/**
* Create a new thing handler for HomeAssistant MQTT components.
* A channel type provider and a topic value receive timeout must be provided.
*
* @param thing The thing of this handler
* @param channelTypeProvider A channel type provider
* @param subscribeTimeout Timeout for the entire tree parsing and subscription. In milliseconds.
* @param attributeReceiveTimeout The timeout per attribute field subscription. In milliseconds.
*/
public HomeAssistantThingHandler(Thing thing, MqttChannelTypeProvider channelTypeProvider,
TransformationServiceProvider transformationServiceProvider, int subscribeTimeout,
int attributeReceiveTimeout) {
super(thing, subscribeTimeout);
this.gson = new GsonBuilder().registerTypeAdapterFactory(new ChannelConfigurationTypeAdapterFactory()).create();
this.channelTypeProvider = channelTypeProvider;
this.transformationServiceProvider = transformationServiceProvider;
this.attributeReceiveTimeout = attributeReceiveTimeout;
this.delayedProcessing = new DelayedBatchProcessing<>(attributeReceiveTimeout, this, scheduler);
this.discoverComponents = new DiscoverComponents(thing.getUID(), scheduler, this, this, gson,
this.transformationServiceProvider);
}
@SuppressWarnings({ "null", "unused" })
@Override
public void initialize() {
started = false;
config = getConfigAs(HandlerConfiguration.class);
if (config.topics == null || config.topics.isEmpty()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Device topics unknown");
return;
}
discoveryHomeAssistantIDs.addAll(HaID.fromConfig(config));
for (Channel channel : thing.getChannels()) {
final String groupID = channel.getUID().getGroupId();
if (groupID == null) {
logger.warn("Channel {} has no groupd ID", channel.getLabel());
continue;
}
// Already restored component?
@Nullable
AbstractComponent<?> component = haComponents.get(groupID);
if (component != null) {
// the types may have been removed in dispose() so we need to add them again
component.addChannelTypes(channelTypeProvider);
continue;
}
HaID haID = HaID.fromConfig(config.basetopic, channel.getConfiguration());
discoveryHomeAssistantIDs.add(haID);
ThingUID thingUID = channel.getUID().getThingUID();
String channelConfigurationJSON = (String) channel.getConfiguration().get("config");
if (channelConfigurationJSON == null) {
logger.warn("Provided channel does not have a 'config' configuration key!");
} else {
component = CFactory.createComponent(thingUID, haID, channelConfigurationJSON, this, this, gson,
transformationServiceProvider);
}
if (component != null) {
haComponents.put(component.uid().getId(), component);
component.addChannelTypes(channelTypeProvider);
} else {
logger.warn("Could not restore component {}", thing);
}
}
updateThingType();
super.initialize();
}
@Override
public void dispose() {
// super.dispose() calls stop()
super.dispose();
haComponents.values().forEach(c -> c.removeChannelTypes(channelTypeProvider));
}
@Override
public CompletableFuture<Void> unsubscribeAll() {
// already unsubscribed everything by calling stop()
return CompletableFuture.allOf();
}
/**
* Start a background discovery for the configured HA MQTT object-id.
*/
@Override
protected CompletableFuture<@Nullable Void> start(MqttBrokerConnection connection) {
started = true;
connection.setQos(1);
updateStatus(ThingStatus.UNKNOWN);
// Start all known components and channels within the components and put the Thing offline
// if any subscribing failed ( == broker connection lost)
CompletableFuture<@Nullable Void> future = haComponents.values().parallelStream()
.map(e -> e.start(connection, scheduler, attributeReceiveTimeout))
.reduce(CompletableFuture.completedFuture(null), (a, v) -> a.thenCompose(b -> v)) // reduce to one
.exceptionally(e -> {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
return null;
});
return future
.thenCompose(b -> discoverComponents.startDiscovery(connection, 0, discoveryHomeAssistantIDs, this));
}
@Override
protected void stop() {
if (started) {
discoverComponents.stopDiscovery();
delayedProcessing.join();
// haComponents does not need to be synchronised -> the discovery thread is disabled
haComponents.values().parallelStream().map(AbstractComponent::stop) //
// we need to join all the stops, otherwise they might not be done when start is called
.collect(FutureCollector.allOf()).join();
started = false;
}
super.stop();
}
@SuppressWarnings({ "null", "unused" })
@Override
public @Nullable ChannelState getChannelState(ChannelUID channelUID) {
String groupID = channelUID.getGroupId();
if (groupID == null) {
return null;
}
AbstractComponent<?> component;
synchronized (haComponents) { // sync whenever discoverComponents is started
component = haComponents.get(groupID);
}
if (component == null) {
return null;
}
CChannel componentChannel = component.channel(channelUID.getIdWithoutGroup());
if (componentChannel == null) {
return null;
}
return componentChannel.getState();
}
/**
* Callback of {@link DiscoverComponents}. Add to a delayed batch processor.
*/
@Override
public void componentDiscovered(HaID homeAssistantTopicID, AbstractComponent<?> component) {
delayedProcessing.accept(component);
}
/**
* 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;
if (connection == null) {
return;
}
synchronized (haComponents) { // sync whenever discoverComponents is started
for (AbstractComponent<?> discovered : discoveredComponentsList) {
AbstractComponent<?> known = haComponents.get(discovered.uid().getId());
// Is component already known?
if (known != null) {
if (discovered.getConfigHash() != known.getConfigHash()) {
// Don't wait for the future to complete. We are also not interested in failures.
// The component will be replaced in a moment.
known.stop();
} else {
known.setConfigSeen();
continue;
}
}
// Add channel and group types to the types registry
discovered.addChannelTypes(channelTypeProvider);
// Add component to the component map
haComponents.put(discovered.uid().getId(), discovered);
// Start component / Subscribe to channel topics
discovered.start(connection, scheduler, 0).exceptionally(e -> {
logger.warn("Failed to start component {}", discovered.uid(), e);
return null;
});
Collection<Channel> channels = discovered.channelTypes().values().stream().map(CChannel::getChannel)
.collect(Collectors.toList());
ThingHelper.addChannelsToThing(thing, channels);
}
}
updateThingType();
}
@Override
protected void updateThingStatus(boolean messageReceived, boolean availabilityTopicsSeen) {
if (!messageReceived || availabilityTopicsSeen) {
updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE);
}
}
private void updateThingType() {
// if this is a dynamic type, then we update the type
ThingTypeUID typeID = thing.getThingTypeUID();
if (!MqttBindingConstants.HOMEASSISTANT_MQTT_THING.equals(typeID)) {
List<ChannelGroupDefinition> groupDefs;
List<ChannelDefinition> channelDefs;
synchronized (haComponents) { // sync whenever discoverComponents is started
groupDefs = haComponents.values().stream().map(AbstractComponent::getGroupDefinition)
.collect(Collectors.toList());
channelDefs = haComponents.values().stream().map(AbstractComponent::type)
.map(ChannelGroupType::getChannelDefinitions).flatMap(List::stream)
.collect(Collectors.toList());
}
ThingType thingType = channelTypeProvider.derive(typeID, MqttBindingConstants.HOMEASSISTANT_MQTT_THING)
.withChannelDefinitions(channelDefs).withChannelGroupDefinitions(groupDefs).build();
channelTypeProvider.setThingType(typeID, thingType);
}
}
}

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="mqtt:ha_channel">
<parameter name="component" type="text" readOnly="true" required="true">
<label>Component</label>
<description>HomeAssistant component type (e.g. binary_sensor, switch, light)</description>
<default></default>
</parameter>
<parameter name="nodeid" type="text" readOnly="true">
<label>Node ID</label>
<description>Optional node name of the component</description>
<default></default>
</parameter>
<parameter name="objectid" type="text" readOnly="true" required="true">
<label>Object ID</label>
<description>Object id of the component</description>
<default></default>
</parameter>
<parameter name="config" type="text" readOnly="true" required="true">
<label>Json Configuration</label>
<description>The json configuration string received by the component via MQTT.</description>
<default></default>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="mqtt"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="homeassistant">
<supported-bridge-type-refs>
<bridge-type-ref id="broker"/>
<bridge-type-ref id="systemBroker"/>
</supported-bridge-type-refs>
<label>HomeAssistant MQTT Component</label>
<description>You need a configured Broker first. This Thing represents a device, that follows the "HomeAssistant MQTT
Component" specification.</description>
<config-description>
<parameter name="topics" type="text" required="true" multiple="true">
<label>MQTT Config Topic</label>
<description>List of HomeAssistant configuration topics (e.g. /homeassistant/switch/4711/config)</description>
</parameter>
<parameter name="basetopic" type="text" required="true">
<label>MQTT Base Prefix</label>
<description>MQTT base prefix</description>
<default>homeassistant</default>
</parameter>
</config-description>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,144 @@
/**
* Copyright (c) 2010-2020 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;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.collection.IsIterableContainingInOrder.contains;
import static org.junit.Assert.assertThat;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.List;
import org.eclipse.jdt.annotation.NonNull;
import org.junit.Test;
import org.openhab.binding.mqtt.homeassistant.internal.BaseChannelConfiguration.Connection;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
/**
* @author Jochen Klein - Initial contribution
*/
public class HAConfigurationTests {
private Gson gson = new GsonBuilder().registerTypeAdapterFactory(new ChannelConfigurationTypeAdapterFactory())
.create();
private static String readTestJson(final String name) {
StringBuilder result = new StringBuilder();
try (BufferedReader in = new BufferedReader(
new InputStreamReader(HAConfigurationTests.class.getResourceAsStream(name), "UTF-8"))) {
String line;
while ((line = in.readLine()) != null) {
result.append(line).append('\n');
}
return result.toString();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Test
public void testAbbreviations() {
String json = readTestJson("configA.json");
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.device, is(notNullValue()));
BaseChannelConfiguration.Device device = config.device;
if (device != null) {
assertThat(device.identifiers, contains("H"));
assertThat(device.connections, is(notNullValue()));
List<@NonNull Connection> connections = device.connections;
if (connections != null) {
assertThat(connections.get(0).type, is("I1"));
assertThat(connections.get(0).identifier, is("I2"));
}
assertThat(device.name, is("J"));
assertThat(device.model, is("K"));
assertThat(device.sw_version, is("L"));
assertThat(device.manufacturer, is("M"));
}
}
@Test
public void testTildeSubstritution() {
String json = readTestJson("configB.json");
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"));
assertThat(config.device, is(notNullValue()));
BaseChannelConfiguration.Device device = config.device;
if (device != null) {
assertThat(device.identifiers, contains("H"));
}
}
@Test
public void testSampleFanConfig() {
String json = readTestJson("configFan.json");
ComponentFan.ChannelConfiguration config = BaseChannelConfiguration.fromString(json, gson,
ComponentFan.ChannelConfiguration.class);
assertThat(config.name, is("Bedroom Fan"));
}
@Test
public void testDeviceListConfig() {
String json = readTestJson("configDeviceList.json");
ComponentFan.ChannelConfiguration config = BaseChannelConfiguration.fromString(json, gson,
ComponentFan.ChannelConfiguration.class);
assertThat(config.device, is(notNullValue()));
BaseChannelConfiguration.Device device = config.device;
if (device != null) {
assertThat(device.identifiers, is(Arrays.asList("A", "B", "C")));
}
}
@Test
public void testDeviceSingleStringConfig() {
String json = readTestJson("configDeviceSingleString.json");
ComponentFan.ChannelConfiguration config = BaseChannelConfiguration.fromString(json, gson,
ComponentFan.ChannelConfiguration.class);
assertThat(config.device, is(notNullValue()));
BaseChannelConfiguration.Device device = config.device;
if (device != null) {
assertThat(device.identifiers, is(Arrays.asList("A")));
}
}
}

View File

@@ -0,0 +1,75 @@
/**
* Copyright (c) 2010-2020 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;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.core.IsCollectionContaining.hasItem;
import static org.junit.Assert.assertThat;
import java.util.Collection;
import java.util.Collections;
import org.junit.Test;
import org.openhab.core.config.core.Configuration;
/**
* @author Jochen Klein - Initial contribution
*/
public class HaIDTests {
@Test
public void testWithoutNode() {
HaID subject = new HaID("homeassistant/switch/name/config");
assertThat(subject.objectID, is("name"));
assertThat(subject.component, is("switch"));
assertThat(subject.getTopic("suffix"), is("homeassistant/switch/name/suffix"));
Configuration config = new Configuration();
subject.toConfig(config);
HaID restore = HaID.fromConfig("homeassistant", config);
assertThat(restore, is(subject));
HandlerConfiguration haConfig = new HandlerConfiguration(subject.baseTopic,
Collections.singletonList(subject.toShortTopic()));
Collection<HaID> restoreList = HaID.fromConfig(haConfig);
assertThat(restoreList, hasItem(new HaID("homeassistant/switch/name/config")));
}
@Test
public void testWithNode() {
HaID subject = new HaID("homeassistant/switch/node/name/config");
assertThat(subject.objectID, is("name"));
assertThat(subject.component, is("switch"));
assertThat(subject.getTopic("suffix"), is("homeassistant/switch/node/name/suffix"));
Configuration config = new Configuration();
subject.toConfig(config);
HaID restore = HaID.fromConfig("homeassistant", config);
assertThat(restore, is(subject));
HandlerConfiguration haConfig = new HandlerConfiguration(subject.baseTopic,
Collections.singletonList(subject.toShortTopic()));
Collection<HaID> restoreList = HaID.fromConfig(haConfig);
assertThat(restoreList, hasItem(new HaID("homeassistant/switch/node/name/config")));
}
}

View File

@@ -0,0 +1,60 @@
/**
* Copyright (c) 2010-2020 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.handler;
import static org.openhab.binding.mqtt.homeassistant.generic.internal.MqttBindingConstants.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.type.ChannelTypeUID;
/**
* Static test definitions, like thing, bridge and channel definitions
*
* @author David Graeff - Initial contribution
*/
@NonNullByDefault
public class ThingChannelConstants {
// Common ThingUID and ChannelUIDs
public static final ThingUID testHomeAssistantThing = new ThingUID(HOMEASSISTANT_MQTT_THING, "device234");
public static final ChannelTypeUID unknownChannel = new ChannelTypeUID(BINDING_ID, "unknown");
public static final String jsonPathJSON = "{ \"device\": { \"status\": { \"temperature\": 23.2 }}}";
public static final String jsonPathPattern = "$.device.status.temperature";
public static final List<Channel> thingChannelList = new ArrayList<>();
public static final List<Channel> thingChannelListWithJson = new ArrayList<>();
static Configuration textConfiguration() {
Map<String, Object> data = new HashMap<>();
data.put("stateTopic", "test/state");
data.put("commandTopic", "test/command");
return new Configuration(data);
}
static Configuration textConfigurationWithJson() {
Map<String, Object> data = new HashMap<>();
data.put("stateTopic", "test/state");
data.put("commandTopic", "test/command");
data.put("transformationPattern", "JSONPATH:" + jsonPathPattern);
return new Configuration(data);
}
}

View File

@@ -0,0 +1,27 @@
{
"name": "A",
"icon": "2",
"qos": 1,
"retain": true,
"val_tpl": "B",
"uniq_id": "C",
"avty_t": "~E",
"pl_avail": "F",
"pl_not_avail": "G",
"device": {
"ids": [
"H"
],
"cns": [
[
"I1",
"I2"
]
],
"name": "J",
"mdl": "K",
"sw": "L",
"mf": "M"
},
"~": "D/"
}

View File

@@ -0,0 +1,28 @@
{
"name": "A",
"icon": "2",
"qos": 1,
"retain": true,
"val_tpl": "B",
"uniq_id": "C",
"avty_t": "~E",
"pl_avail": "F",
"pl_not_avail": "G",
"optimistic": true,
"state_topic": "O/~",
"command_topic": "P~Q",
"device": {
"ids": "H",
"cns": [
[
"I1",
"I2"
]
],
"name": "J",
"mdl": "K",
"sw": "L",
"mf": "M"
},
"~": "D/"
}

View File

@@ -0,0 +1,22 @@
{
"name": "Bedroom Fan",
"state_topic": "bedroom_fan/on/state",
"command_topic": "bedroom_fan/on/set",
"oscillation_state_topic": "bedroom_fan/oscillation/state",
"oscillation_command_topic": "bedroom_fan/oscillation/set",
"speed_state_topic": "bedroom_fan/speed/state",
"speed_command_topic": "bedroom_fan/speed/set",
"qos": 0,
"payload_on": "true",
"payload_off": "false",
"payload_oscillation_on": "true",
"payload_oscillation_off": "false",
"payload_low_speed": "low",
"payload_medium_speed": "medium",
"payload_high_speed": "high",
"speeds": [
"low",
"medium",
"high"
]
}