added migrated 2.x add-ons
Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
38
bundles/org.openhab.binding.mqtt.homeassistant/.classpath
Normal file
38
bundles/org.openhab.binding.mqtt.homeassistant/.classpath
Normal 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>
|
||||
23
bundles/org.openhab.binding.mqtt.homeassistant/.project
Normal file
23
bundles/org.openhab.binding.mqtt.homeassistant/.project
Normal 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>
|
||||
44
bundles/org.openhab.binding.mqtt.homeassistant/NOTICE
Normal file
44
bundles/org.openhab.binding.mqtt.homeassistant/NOTICE
Normal 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.
|
||||
18
bundles/org.openhab.binding.mqtt.homeassistant/README.md
Normal file
18
bundles/org.openhab.binding.mqtt.homeassistant/README.md
Normal 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.
|
||||
31
bundles/org.openhab.binding.mqtt.homeassistant/pom.xml
Normal file
31
bundles/org.openhab.binding.mqtt.homeassistant/pom.xml
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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")));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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/"
|
||||
}
|
||||
@@ -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/"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"device": {
|
||||
"ids": [
|
||||
"A",
|
||||
"B",
|
||||
"C"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"device": {
|
||||
"ids": "A"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user