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,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/test-classes" path="src/test/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.binding.mqtt.homeassistant</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

View File

@@ -0,0 +1,44 @@
This content is produced and maintained by the Eclipse SmartHome project.
* Project home: https://openhab.org/
== Declared Project Licenses
This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.
The MQTT Homie convention is made available under the MIT license, attached at the end of this file.
== Source Code
https://github.com/eclipse/smarthome
== Copyright Holders
See the NOTICE file distributed with the source code at
https://github.com/eclipse/smarthome/blob/master/NOTICE
for detailed information regarding copyright ownership.
== MIT license ==
MIT License
Copyright (c) 2017 Marvin Roger
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,18 @@
# HomeAssistant MQTT Components Binding
HomeAssistant MQTT Components are recognized as well. The base topic needs to be **homeassistant**.
The mapping is structured like this:
| HA MQTT | Framework | Example MQTT topic |
|-----------------------|---------------|------------------------------------|
| Object | Thing | homeassistant/../../object |
| Component+Node | Channel Group | homeassistant/component/node/object|
| -> Component Features | Channel | state/topic/defined/in/comp/config |
## Limitations
* The HomeAssistant Fan Components only support ON/OFF.
* The HomeAssistant Cover Components only support OPEN/CLOSE/STOP.
* The HomeAssistant Light Component only supports RGB color changes.
* The HomeAssistant Climate Components is not yet supported.

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>3.0.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.mqtt.homeassistant</artifactId>
<name>openHAB Add-ons :: Bundles :: MQTT HomeAssistant Convention</name>
<dependencies>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.mqtt</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.mqtt.generic</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

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"
]
}