[mqtt-homeassistant] climate.mqtt support (#10690)

* MQTT.Homeassistant Climate support

Signed-off-by: Anton Kharuzhy <antroids@gmail.com>

* MQTT.Homeassistant synthetic config test added

Signed-off-by: Anton Kharuzhy <antroids@gmail.com>

* MQTT.Homeassistant refactoring

Signed-off-by: Anton Kharuzhy <antroids@gmail.com>

* MQTT.Homeassistant discovery test added

Signed-off-by: Anton Kharuzhy <antroids@gmail.com>

* MQTT.Homeassistant thing handler test added

Signed-off-by: Anton Kharuzhy <antroids@gmail.com>

* MQTT.Homeassistant switch test added

Signed-off-by: Anton Kharuzhy <antroids@gmail.com>

* MQTT.Homeassistant Climate test added

Signed-off-by: Anton Kharuzhy <antroids@gmail.com>

* MQTT.Homeassistant author header added

Signed-off-by: Anton Kharuzhy <antroids@gmail.com>

* MQTT.Homeassistant copyright header added

Signed-off-by: Anton Kharuzhy <antroids@gmail.com>

* MQTT.Homeassistant test fixed

Signed-off-by: Anton Kharuzhy <antroids@gmail.com>

* MQTT.Homeassistant test fixed

Signed-off-by: Anton Kharuzhy <antroids@gmail.com>

* MQTT.Homeassistant test infrastructure updated. Added tests with mqtt publishing and commands posting.

Signed-off-by: Anton Kharuzhy <antroids@gmail.com>

* MQTT.Homeassistant fixed Climate#send_if_off handling

Signed-off-by: Anton Kharuzhy <antroids@gmail.com>

* MQTT.Homeassistant do not filter the power command

Signed-off-by: Anton Kharuzhy <antroids@gmail.com>

* MQTT.Homeassistant climate unit test added

Signed-off-by: Anton Kharuzhy <antroids@gmail.com>

* Update bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/DiscoverComponents.java

Redundant annotation removed

Co-authored-by: Fabian Wolter <github@fabian-wolter.de>

* MQTT.Homeassistant Redundant @Nullable annotations removed

Signed-off-by: Anton Kharuzhy <antroids@gmail.com>

* MQTT.Homeassistant Unit tests added for all components

Signed-off-by: Anton Kharuzhy <antroids@gmail.com>

* MQTT.Homeassistant Unit tests stability fix

Signed-off-by: Anton Kharuzhy <antroids@gmail.com>

* MQTT.Homeassistant @NonNullByDefault removed from Device, config.dto package created

Signed-off-by: Anton Kharuzhy <antroids@gmail.com>

* MQTT.Homeassistant Climate author added

Signed-off-by: Anton Kharuzhy <antroids@gmail.com>

* MQTT.Homeassistant Device.sw_version renamed

Signed-off-by: Anton Kharuzhy <antroids@gmail.com>

* MQTT.Homeassistant tests wait timeout increased to 10s

Signed-off-by: Anton Kharuzhy <antroids@gmail.com>

Co-authored-by: antroids <antroids@gmail.com>
Co-authored-by: Fabian Wolter <github@fabian-wolter.de>
This commit is contained in:
antroids
2021-08-15 12:48:26 +03:00
committed by GitHub
parent 9f09db1f18
commit 3a7835e122
56 changed files with 3236 additions and 466 deletions

View File

@@ -15,6 +15,7 @@ package org.openhab.binding.mqtt.homeassistant.internal;
import java.net.URI;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ScheduledExecutorService;
import java.util.function.Predicate;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
@@ -26,7 +27,7 @@ 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.binding.mqtt.homeassistant.internal.component.AbstractComponent;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
import org.openhab.core.thing.Channel;
@@ -37,6 +38,7 @@ 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.Command;
import org.openhab.core.types.StateDescriptionFragment;
/**
@@ -55,7 +57,7 @@ import org.openhab.core.types.StateDescriptionFragment;
* @author David Graeff - Initial contribution
*/
@NonNullByDefault
public class CChannel {
public class ComponentChannel {
private static final String JINJA = "JINJA";
private final ChannelUID channelUID;
@@ -65,7 +67,7 @@ public class CChannel {
private final ChannelTypeUID channelTypeUID;
private final ChannelStateUpdateListener channelStateUpdateListener;
private CChannel(ChannelUID channelUID, ChannelState channelState, Channel channel, ChannelType type,
private ComponentChannel(ChannelUID channelUID, ChannelState channelState, Channel channel, ChannelType type,
ChannelTypeUID channelTypeUID, ChannelStateUpdateListener channelStateUpdateListener) {
super();
this.channelUID = channelUID;
@@ -117,24 +119,25 @@ public class CChannel {
}
public static class Builder {
private AbstractComponent<?> component;
private ComponentConfiguration componentConfiguration;
private String channelID;
private Value valueState;
private String label;
private final AbstractComponent<?> component;
private final String channelID;
private final Value valueState;
private final String label;
private final ChannelStateUpdateListener channelStateUpdateListener;
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 Predicate<Command> commandFilter;
private @Nullable String templateIn;
private @Nullable String templateOut;
public Builder(AbstractComponent<?> component, ComponentConfiguration componentConfiguration, String channelID,
Value valueState, String label, ChannelStateUpdateListener channelStateUpdateListener) {
public Builder(AbstractComponent<?> component, String channelID, Value valueState, String label,
ChannelStateUpdateListener channelStateUpdateListener) {
this.component = component;
this.componentConfiguration = componentConfiguration;
this.channelID = channelID;
this.valueState = valueState;
this.label = label;
@@ -161,9 +164,9 @@ public class CChannel {
/**
* @deprecated use commandTopic(String, boolean, int)
* @param command_topic
* @param retain
* @return
* @param command_topic topic
* @param retain retain
* @return this
*/
@Deprecated
public Builder commandTopic(@Nullable String command_topic, boolean retain) {
@@ -173,9 +176,17 @@ public class CChannel {
}
public Builder commandTopic(@Nullable String command_topic, boolean retain, int qos) {
return commandTopic(command_topic, retain, qos, null);
}
public Builder commandTopic(@Nullable String command_topic, boolean retain, int qos,
@Nullable String template) {
this.command_topic = command_topic;
this.retain = retain;
this.qos = qos;
if (command_topic != null && !command_topic.isBlank()) {
this.templateOut = template;
}
return this;
}
@@ -184,24 +195,29 @@ public class CChannel {
return this;
}
public CChannel build() {
public Builder commandFilter(@Nullable Predicate<Command> commandFilter) {
this.commandFilter = commandFilter;
return this;
}
public ComponentChannel build() {
return build(true);
}
public CChannel build(boolean addToComponent) {
public ComponentChannel build(boolean addToComponent) {
ChannelUID channelUID;
ChannelState channelState;
Channel channel;
ChannelType type;
ChannelTypeUID channelTypeUID;
channelUID = new ChannelUID(component.channelGroupUID, channelID);
channelUID = new ChannelUID(component.getGroupUID(), channelID);
channelTypeUID = new ChannelTypeUID(MqttBindingConstants.BINDING_ID,
channelUID.getGroupId() + "_" + channelID);
channelState = new ChannelState(
channelState = new HomeAssistantChannelState(
ChannelConfigBuilder.create().withRetain(retain).withQos(qos).withStateTopic(state_topic)
.withCommandTopic(command_topic).makeTrigger(trigger).build(),
channelUID, valueState, channelStateUpdateListener);
channelUID, valueState, channelStateUpdateListener, commandFilter);
String localStateTopic = state_topic;
if (localStateTopic == null || localStateTopic.isBlank() || this.trigger) {
@@ -215,26 +231,29 @@ public class CChannel {
}
Configuration configuration = new Configuration();
configuration.put("config", component.channelConfigurationJson);
component.haID.toConfig(configuration);
configuration.put("config", component.getChannelConfigurationJson());
component.getHaID().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,
ComponentChannel result = new ComponentChannel(channelUID, channelState, channel, type, channelTypeUID,
channelStateUpdateListener);
@Nullable
TransformationServiceProvider transformationProvider = componentConfiguration
.getTransformationServiceProvider();
TransformationServiceProvider transformationProvider = component.getTransformationServiceProvider();
final String templateIn = this.templateIn;
if (templateIn != null && transformationProvider != null) {
channelState
.addTransformation(new ChannelStateTransformation(JINJA, templateIn, transformationProvider));
}
final String templateOut = this.templateOut;
if (templateOut != null && transformationProvider != null) {
channelState.addTransformationOut(
new ChannelStateTransformation(JINJA, templateOut, transformationProvider));
}
if (addToComponent) {
component.channels.put(channelID, result);
component.getChannelMap().put(channelID, result);
}
return result;
}

View File

@@ -1,40 +0,0 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mqtt.homeassistant.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* A MQTT climate component, following the https://www.home-assistant.io/components/climate.mqtt/ specification.
*
* At the moment this only notifies the user that this feature is not yet supported.
*
* @author David Graeff - Initial contribution
*/
@NonNullByDefault
public class ComponentClimate extends AbstractComponent<ComponentClimate.ChannelConfiguration> {
/**
* Configuration class for MQTT component
*/
static class ChannelConfiguration extends BaseChannelConfiguration {
ChannelConfiguration() {
super("MQTT HVAC");
}
}
public ComponentClimate(CFactory.ComponentConfiguration componentConfiguration) {
super(componentConfiguration, ChannelConfiguration.class);
throw new UnsupportedOperationException("Component:Climate not supported yet");
}
}

View File

@@ -27,6 +27,8 @@ 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.binding.mqtt.homeassistant.internal.component.AbstractComponent;
import org.openhab.binding.mqtt.homeassistant.internal.component.ComponentFactory;
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
import org.openhab.core.io.transport.mqtt.MqttMessageSubscriber;
import org.openhab.core.thing.ThingUID;
@@ -55,7 +57,7 @@ public class DiscoverComponents implements MqttMessageSubscriber {
private @Nullable ScheduledFuture<?> stopDiscoveryFuture;
private WeakReference<@Nullable MqttBrokerConnection> connectionRef = new WeakReference<>(null);
protected @NonNullByDefault({}) ComponentDiscovered discoveredListener;
protected @Nullable ComponentDiscovered discoveredListener;
private int discoverTime;
private Set<String> topics = new HashSet<>();
@@ -92,12 +94,11 @@ public class DiscoverComponents implements MqttMessageSubscriber {
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, scheduler, gson,
transformationServiceProvider);
component = ComponentFactory.createComponent(thingUID, haID, config, updateListener, tracker, scheduler,
gson, transformationServiceProvider);
}
if (component != null) {
component.setConfigSeen();
@@ -122,9 +123,9 @@ public class DiscoverComponents implements MqttMessageSubscriber {
* @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
* You need to call {@link #stopDiscovery()} at some
* point in that case.
* @param topicDescription Contains the object-id (=device id) and potentially a node-id as well.
* @param topicDescriptions 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.
@@ -177,8 +178,6 @@ public class DiscoverComponents implements MqttMessageSubscriber {
/**
* Stops an ongoing discovery or do nothing if no discovery is running.
*
* @param connection A MQTT broker connection
*/
public void stopDiscovery() {
subscribeFail(new Throwable("Stopped"));

View File

@@ -89,7 +89,7 @@ public class HaID {
this.topic = createTopic(this);
}
private static final String createTopic(HaID id) {
private static String createTopic(HaID id) {
StringBuilder str = new StringBuilder();
str.append(id.baseTopic).append('/').append(id.component).append('/');
if (!id.nodeID.isBlank()) {
@@ -104,8 +104,8 @@ public class HaID {
* <p>
* <code>objectid</code>, <code>nodeid</code>, and <code>component</code> values are fetched from the configuration.
*
* @param baseTopic
* @param config
* @param baseTopic base topic
* @param config config
* @return newly created HaID
*/
public static HaID fromConfig(String baseTopic, Configuration config) {
@@ -120,7 +120,7 @@ public class HaID {
* <p>
* <code>objectid</code>, <code>nodeid</code>, and <code>component</code> values are added to the configuration.
*
* @param config
* @param config config
* @return the modified configuration
*/
public Configuration toConfig(Configuration config) {
@@ -139,7 +139,7 @@ public class HaID {
* 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
* @param config config
* @return newly created HaID
*/
public static Collection<HaID> fromConfig(HandlerConfiguration config) {

View File

@@ -28,6 +28,9 @@ import org.openhab.binding.mqtt.homeassistant.internal.handler.HomeAssistantThin
*/
@NonNullByDefault
public class HandlerConfiguration {
public static final String PROPERTY_BASETOPIC = "basetopic";
public static final String PROPERTY_TOPICS = "topics";
public static final String DEFAULT_BASETOPIC = "homeassistant";
/**
* hint: cannot be final, or <code>getConfigAs</code> will not work.
* The MQTT prefix topic
@@ -64,7 +67,7 @@ public class HandlerConfiguration {
public List<String> topics;
public HandlerConfiguration() {
this("homeassistant", Collections.emptyList());
this(DEFAULT_BASETOPIC, Collections.emptyList());
}
public HandlerConfiguration(String basetopic, List<String> topics) {
@@ -76,12 +79,12 @@ public class HandlerConfiguration {
/**
* Add the <code>basetopic</code> and <code>objectid</code> to the properties.
*
* @param properties
* @param properties properties
* @return the modified properties
*/
public <T extends Map<String, Object>> T appendToProperties(T properties) {
properties.put("basetopic", basetopic);
properties.put("topics", topics);
properties.put(PROPERTY_BASETOPIC, basetopic);
properties.put(PROPERTY_TOPICS, topics);
return properties;
}
}

View File

@@ -0,0 +1,65 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mqtt.homeassistant.internal;
import java.util.concurrent.CompletableFuture;
import java.util.function.Predicate;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.ChannelConfig;
import org.openhab.binding.mqtt.generic.ChannelState;
import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
import org.openhab.binding.mqtt.generic.values.Value;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Extended {@link ChannelState} with added filter for {@link #publishValue(Command)}
*
* @author Anton Kharuzhy - Initial contribution
*/
@NonNullByDefault
public class HomeAssistantChannelState extends ChannelState {
private final Logger logger = LoggerFactory.getLogger(HomeAssistantChannelState.class);
private final @Nullable Predicate<Command> commandFilter;
/**
* Creates a new channel state.
*
* @param config The channel configuration
* @param channelUID The channelUID is used for the {@link ChannelStateUpdateListener} to notify about value changes
* @param cachedValue MQTT only notifies us once about a value, during the subscribe. The channel state therefore
* needs a cache for the current value.
* @param channelStateUpdateListener A channel state update listener
* @param commandFilter A filter for commands, on <code>true</code> command will be published, on
* <code>false</code> ignored. Can be <code>null</code> to publish all commands.
*/
public HomeAssistantChannelState(ChannelConfig config, ChannelUID channelUID, Value cachedValue,
@Nullable ChannelStateUpdateListener channelStateUpdateListener,
@Nullable Predicate<Command> commandFilter) {
super(config, channelUID, cachedValue, channelStateUpdateListener);
this.commandFilter = commandFilter;
}
@Override
public CompletableFuture<Boolean> publishValue(Command command) {
if (commandFilter != null && !commandFilter.test(command)) {
logger.trace("Channel {} updates are disabled by command filter, ignoring command {}", channelUID, command);
return CompletableFuture.completedFuture(false);
}
return super.publishValue(command);
}
}

View File

@@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mqtt.homeassistant.internal;
package org.openhab.binding.mqtt.homeassistant.internal.component;
import java.util.List;
import java.util.Map;
@@ -23,10 +23,14 @@ 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.TransformationServiceProvider;
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.binding.mqtt.homeassistant.internal.ComponentChannel;
import org.openhab.binding.mqtt.homeassistant.internal.HaID;
import org.openhab.binding.mqtt.homeassistant.internal.component.ComponentFactory.ComponentConfiguration;
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
import org.openhab.core.thing.ChannelGroupUID;
import org.openhab.core.thing.type.ChannelDefinition;
@@ -40,10 +44,10 @@ import org.openhab.core.thing.type.ChannelGroupTypeUID;
* It has a name and consists of multiple channels.
*
* @author David Graeff - Initial contribution
* @param <C> Config class derived from {@link BaseChannelConfiguration}
* @param <C> Config class derived from {@link AbstractChannelConfiguration}
*/
@NonNullByDefault
public abstract class AbstractComponent<C extends BaseChannelConfiguration> {
public abstract class AbstractComponent<C extends AbstractChannelConfiguration> {
// Component location fields
private final ComponentConfiguration componentConfiguration;
protected final ChannelGroupTypeUID channelGroupTypeUID;
@@ -51,7 +55,7 @@ public abstract class AbstractComponent<C extends BaseChannelConfiguration> {
protected final HaID haID;
// Channels and configuration
protected final Map<String, CChannel> channels = new TreeMap<>();
protected final Map<String, ComponentChannel> 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;
@@ -61,14 +65,12 @@ public abstract class AbstractComponent<C extends BaseChannelConfiguration> {
protected boolean configSeen;
/**
* Provide a thingUID and HomeAssistant topic ID to determine the channel group UID and type.
* Creates component based on generic configuration and component configuration type.
*
* @param thing A ThingUID
* @param haID A HomeAssistant topic ID
* @param configJson The configuration string
* @param gson A Gson instance
* @param componentConfiguration generic componentConfiguration with not parsed JSON config
* @param clazz target configuration type
*/
public AbstractComponent(CFactory.ComponentConfiguration componentConfiguration, Class<C> clazz) {
public AbstractComponent(ComponentFactory.ComponentConfiguration componentConfiguration, Class<C> clazz) {
this.componentConfiguration = componentConfiguration;
this.channelConfigurationJson = componentConfiguration.getConfigJSON();
@@ -77,24 +79,24 @@ public abstract class AbstractComponent<C extends BaseChannelConfiguration> {
this.haID = componentConfiguration.getHaID();
String groupId = this.haID.getGroupId(channelConfiguration.unique_id);
String groupId = this.haID.getGroupId(channelConfiguration.getUniqueId());
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;
String availability_topic = this.channelConfiguration.getAvailabilityTopic();
if (availability_topic != null) {
componentConfiguration.getTracker().addAvailabilityTopic(availability_topic,
this.channelConfiguration.payload_available, this.channelConfiguration.payload_not_available);
this.channelConfiguration.getPayloadAvailable(),
this.channelConfiguration.getPayloadNotAvailable());
}
}
protected CChannel.Builder buildChannel(String channelID, Value valueState, String label,
protected ComponentChannel.Builder buildChannel(String channelID, Value valueState, String label,
ChannelStateUpdateListener channelStateUpdateListener) {
return new CChannel.Builder(this, componentConfiguration, channelID, valueState, label,
channelStateUpdateListener);
return new ComponentChannel.Builder(this, channelID, valueState, label, channelStateUpdateListener);
}
public void setConfigSeen() {
@@ -104,14 +106,15 @@ public abstract class AbstractComponent<C extends BaseChannelConfiguration> {
/**
* 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
* @param connection connection to the MQTT broker
* @param scheduler thing scheduler
* @param timeout channel subscription timeout
* @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))
return channels.values().parallelStream().map(cChannel -> cChannel.start(connection, scheduler, timeout))
.collect(FutureCollector.allOf());
}
@@ -122,7 +125,7 @@ public abstract class AbstractComponent<C extends BaseChannelConfiguration> {
* exceptionally on errors.
*/
public CompletableFuture<@Nullable Void> stop() {
return channels.values().parallelStream().map(CChannel::stop).collect(FutureCollector.allOf());
return channels.values().parallelStream().map(ComponentChannel::stop).collect(FutureCollector.allOf());
}
/**
@@ -131,7 +134,7 @@ public abstract class AbstractComponent<C extends BaseChannelConfiguration> {
* @param channelTypeProvider The channel type provider
*/
public void addChannelTypes(MqttChannelTypeProvider channelTypeProvider) {
channelTypeProvider.setChannelGroupType(groupTypeUID(), type());
channelTypeProvider.setChannelGroupType(getGroupTypeUID(), getType());
channels.values().forEach(v -> v.addChannelTypes(channelTypeProvider));
}
@@ -143,46 +146,46 @@ public abstract class AbstractComponent<C extends BaseChannelConfiguration> {
*/
public void removeChannelTypes(MqttChannelTypeProvider channelTypeProvider) {
channels.values().forEach(v -> v.removeChannelTypes(channelTypeProvider));
channelTypeProvider.removeChannelGroupType(groupTypeUID());
channelTypeProvider.removeChannelGroupType(getGroupTypeUID());
}
/**
* Each HomeAssistant component corresponds to a Channel Group Type.
*/
public ChannelGroupTypeUID groupTypeUID() {
public ChannelGroupTypeUID getGroupTypeUID() {
return channelGroupTypeUID;
}
/**
* The unique id of this component.
*/
public ChannelGroupUID uid() {
public ChannelGroupUID getGroupUID() {
return channelGroupUID;
}
/**
* Component (Channel Group) name.
*/
public String name() {
return channelConfiguration.name;
public String getName() {
return channelConfiguration.getName();
}
/**
* Each component consists of multiple Channels.
*/
public Map<String, CChannel> channelTypes() {
public Map<String, ComponentChannel> getChannelMap() {
return channels;
}
/**
* Return a components channel. A HomeAssistant MQTT component consists of multiple functions
* and those are mapped to one or more channels. The channel IDs are constants within the
* derived Component, like the {@link ComponentSwitch#switchChannelID}.
* derived Component, like the {@link Switch#switchChannelID}.
*
* @param channelID The channel ID
* @return A components channel
*/
public @Nullable CChannel channel(String channelID) {
public @Nullable ComponentChannel getChannel(String channelID) {
return channels.get(channelID);
}
@@ -196,11 +199,11 @@ public abstract class AbstractComponent<C extends BaseChannelConfiguration> {
/**
* Return the channel group type.
*/
public ChannelGroupType type() {
final List<ChannelDefinition> channelDefinitions = channels.values().stream().map(CChannel::type)
public ChannelGroupType getType() {
final List<ChannelDefinition> channelDefinitions = channels.values().stream().map(ComponentChannel::type)
.collect(Collectors.toList());
return ChannelGroupTypeBuilder.instance(channelGroupTypeUID, name()).withChannelDefinitions(channelDefinitions)
.build();
return ChannelGroupTypeBuilder.instance(channelGroupTypeUID, getName())
.withChannelDefinitions(channelDefinitions).build();
}
/**
@@ -208,13 +211,26 @@ public abstract class AbstractComponent<C extends BaseChannelConfiguration> {
* to the MQTT broker got lost.
*/
public void resetState() {
channels.values().forEach(CChannel::resetState);
channels.values().forEach(ComponentChannel::resetState);
}
/**
* Return the channel group definition for this component.
*/
public ChannelGroupDefinition getGroupDefinition() {
return new ChannelGroupDefinition(channelGroupUID.getId(), groupTypeUID(), name(), null);
return new ChannelGroupDefinition(channelGroupUID.getId(), getGroupTypeUID(), getName(), null);
}
public HaID getHaID() {
return haID;
}
public String getChannelConfigurationJson() {
return channelConfigurationJson;
}
@Nullable
public TransformationServiceProvider getTransformationServiceProvider() {
return componentConfiguration.getTransformationServiceProvider();
}
}

View File

@@ -10,11 +10,12 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mqtt.homeassistant.internal;
package org.openhab.binding.mqtt.homeassistant.internal.component;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.values.TextValue;
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
/**
* A MQTT alarm control panel, following the https://www.home-assistant.io/components/alarm_control_panel.mqtt/
@@ -26,7 +27,7 @@ import org.openhab.binding.mqtt.generic.values.TextValue;
* @author David Graeff - Initial contribution
*/
@NonNullByDefault
public class ComponentAlarmControlPanel extends AbstractComponent<ComponentAlarmControlPanel.ChannelConfiguration> {
public class AlarmControlPanel extends AbstractComponent<AlarmControlPanel.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"
@@ -35,7 +36,7 @@ public class ComponentAlarmControlPanel extends AbstractComponent<ComponentAlarm
/**
* Configuration class for MQTT component
*/
static class ChannelConfiguration extends BaseChannelConfiguration {
static class ChannelConfiguration extends AbstractChannelConfiguration {
ChannelConfiguration() {
super("MQTT Alarm");
}
@@ -55,30 +56,33 @@ public class ComponentAlarmControlPanel extends AbstractComponent<ComponentAlarm
protected String payload_arm_away = "ARM_AWAY";
}
public ComponentAlarmControlPanel(CFactory.ComponentConfiguration componentConfiguration) {
public AlarmControlPanel(ComponentFactory.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,
buildChannel(stateChannelID, new TextValue(state_enum), channelConfiguration.getName(),
componentConfiguration.getUpdateListener())
.stateTopic(channelConfiguration.state_topic, channelConfiguration.value_template)//
.stateTopic(channelConfiguration.state_topic, channelConfiguration.getValueTemplate())//
.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();
channelConfiguration.getName(), componentConfiguration.getUpdateListener())
.commandTopic(command_topic, channelConfiguration.isRetain(), channelConfiguration.getQos())
.build();
buildChannel(switchArmHomeChannelID, new TextValue(new String[] { channelConfiguration.payload_arm_home }),
channelConfiguration.name, componentConfiguration.getUpdateListener())
.commandTopic(command_topic, channelConfiguration.retain).build();
channelConfiguration.getName(), componentConfiguration.getUpdateListener())
.commandTopic(command_topic, channelConfiguration.isRetain(), channelConfiguration.getQos())
.build();
buildChannel(switchArmAwayChannelID, new TextValue(new String[] { channelConfiguration.payload_arm_away }),
channelConfiguration.name, componentConfiguration.getUpdateListener())
.commandTopic(command_topic, channelConfiguration.retain).build();
channelConfiguration.getName(), componentConfiguration.getUpdateListener())
.commandTopic(command_topic, channelConfiguration.isRetain(), channelConfiguration.getQos())
.build();
}
}
}

View File

@@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mqtt.homeassistant.internal;
package org.openhab.binding.mqtt.homeassistant.internal.component;
import java.util.List;
@@ -19,6 +19,7 @@ import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
import org.openhab.binding.mqtt.generic.values.OnOffValue;
import org.openhab.binding.mqtt.generic.values.Value;
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
import org.openhab.binding.mqtt.homeassistant.internal.listener.ExpireUpdateStateListener;
import org.openhab.binding.mqtt.homeassistant.internal.listener.OffDelayUpdateStateListener;
@@ -28,13 +29,13 @@ import org.openhab.binding.mqtt.homeassistant.internal.listener.OffDelayUpdateSt
* @author David Graeff - Initial contribution
*/
@NonNullByDefault
public class ComponentBinarySensor extends AbstractComponent<ComponentBinarySensor.ChannelConfiguration> {
public class BinarySensor extends AbstractComponent<BinarySensor.ChannelConfiguration> {
public static final String sensorChannelID = "sensor"; // Randomly chosen channel "ID"
/**
* Configuration class for MQTT component
*/
static class ChannelConfiguration extends BaseChannelConfiguration {
static class ChannelConfiguration extends AbstractChannelConfiguration {
ChannelConfiguration() {
super("MQTT Binary Sensor");
}
@@ -53,16 +54,16 @@ public class ComponentBinarySensor extends AbstractComponent<ComponentBinarySens
protected @Nullable List<String> json_attributes;
}
public ComponentBinarySensor(CFactory.ComponentConfiguration componentConfiguration) {
public BinarySensor(ComponentFactory.ComponentConfiguration componentConfiguration) {
super(componentConfiguration, ChannelConfiguration.class);
OnOffValue value = new OnOffValue(channelConfiguration.payload_on, channelConfiguration.payload_off);
buildChannel(sensorChannelID, value, "value", getListener(componentConfiguration, value))
.stateTopic(channelConfiguration.state_topic, channelConfiguration.value_template).build();
.stateTopic(channelConfiguration.state_topic, channelConfiguration.getValueTemplate()).build();
}
private ChannelStateUpdateListener getListener(CFactory.ComponentConfiguration componentConfiguration,
private ChannelStateUpdateListener getListener(ComponentFactory.ComponentConfiguration componentConfiguration,
Value value) {
ChannelStateUpdateListener updateListener = componentConfiguration.getUpdateListener();

View File

@@ -10,10 +10,11 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mqtt.homeassistant.internal;
package org.openhab.binding.mqtt.homeassistant.internal.component;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mqtt.generic.values.ImageValue;
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
/**
* A MQTT camera, following the https://www.home-assistant.io/components/camera.mqtt/ specification.
@@ -23,13 +24,13 @@ import org.openhab.binding.mqtt.generic.values.ImageValue;
* @author David Graeff - Initial contribution
*/
@NonNullByDefault
public class ComponentCamera extends AbstractComponent<ComponentCamera.ChannelConfiguration> {
public class Camera extends AbstractComponent<Camera.ChannelConfiguration> {
public static final String cameraChannelID = "camera"; // Randomly chosen channel "ID"
/**
* Configuration class for MQTT component
*/
static class ChannelConfiguration extends BaseChannelConfiguration {
static class ChannelConfiguration extends AbstractChannelConfiguration {
ChannelConfiguration() {
super("MQTT Camera");
}
@@ -37,12 +38,12 @@ public class ComponentCamera extends AbstractComponent<ComponentCamera.ChannelCo
protected String topic = "";
}
public ComponentCamera(CFactory.ComponentConfiguration componentConfiguration) {
public Camera(ComponentFactory.ComponentConfiguration componentConfiguration) {
super(componentConfiguration, ChannelConfiguration.class);
ImageValue value = new ImageValue();
buildChannel(cameraChannelID, value, channelConfiguration.name, componentConfiguration.getUpdateListener())
buildChannel(cameraChannelID, value, channelConfiguration.getName(), componentConfiguration.getUpdateListener())
.stateTopic(channelConfiguration.topic).build();
}
}

View File

@@ -0,0 +1,237 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mqtt.homeassistant.internal.component;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
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.values.NumberValue;
import org.openhab.binding.mqtt.generic.values.OnOffValue;
import org.openhab.binding.mqtt.generic.values.TextValue;
import org.openhab.binding.mqtt.generic.values.Value;
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel;
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
import org.openhab.core.library.types.StringType;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
/**
* A MQTT climate component, following the https://www.home-assistant.io/components/climate.mqtt/ specification.
*
* @author David Graeff - Initial contribution
* @author Anton Kharuzhy - Implementation
*/
@NonNullByDefault
public class Climate extends AbstractComponent<Climate.ChannelConfiguration> {
public static final String ACTION_CH_ID = "action";
public static final String AUX_CH_ID = "aux";
public static final String AWAY_MODE_CH_ID = "awayMode";
public static final String CURRENT_TEMPERATURE_CH_ID = "currentTemperature";
public static final String FAN_MODE_CH_ID = "fanMode";
public static final String HOLD_CH_ID = "hold";
public static final String MODE_CH_ID = "mode";
public static final String SWING_CH_ID = "swing";
public static final String TEMPERATURE_CH_ID = "temperature";
public static final String TEMPERATURE_HIGH_CH_ID = "temperatureHigh";
public static final String TEMPERATURE_LOW_CH_ID = "temperatureLow";
public static final String POWER_CH_ID = "power";
private static final String CELSIUM = "C";
private static final String FAHRENHEIT = "F";
private static final float DEFAULT_CELSIUM_PRECISION = 0.1f;
private static final float DEFAULT_FAHRENHEIT_PRECISION = 1f;
private static final String ACTION_OFF = "off";
private static final State ACTION_OFF_STATE = new StringType(ACTION_OFF);
private static final List<String> ACTION_MODES = List.of(ACTION_OFF, "heating", "cooling", "drying", "idle", "fan");
/**
* Configuration class for MQTT component
*/
static class ChannelConfiguration extends AbstractChannelConfiguration {
ChannelConfiguration() {
super("MQTT HVAC");
}
protected @Nullable String action_template;
protected @Nullable String action_topic;
protected @Nullable String aux_command_topic;
protected @Nullable String aux_state_template;
protected @Nullable String aux_state_topic;
protected @Nullable String away_mode_command_topic;
protected @Nullable String away_mode_state_template;
protected @Nullable String away_mode_state_topic;
protected @Nullable String current_temperature_template;
protected @Nullable String current_temperature_topic;
protected @Nullable String fan_mode_command_template;
protected @Nullable String fan_mode_command_topic;
protected @Nullable String fan_mode_state_template;
protected @Nullable String fan_mode_state_topic;
protected List<String> fan_modes = Arrays.asList("auto", "low", "medium", "high");
protected @Nullable String hold_command_template;
protected @Nullable String hold_command_topic;
protected @Nullable String hold_state_template;
protected @Nullable String hold_state_topic;
protected @Nullable List<String> hold_modes; // Are there default modes? Now the channel will be ignored without
// hold modes.
protected @Nullable String json_attributes_template; // Attributes are not supported yet
protected @Nullable String json_attributes_topic;
protected @Nullable String mode_command_template;
protected @Nullable String mode_command_topic;
protected @Nullable String mode_state_template;
protected @Nullable String mode_state_topic;
protected List<String> modes = Arrays.asList("auto", "off", "cool", "heat", "dry", "fan_only");
protected @Nullable String swing_command_template;
protected @Nullable String swing_command_topic;
protected @Nullable String swing_state_template;
protected @Nullable String swing_state_topic;
protected List<String> swing_modes = Arrays.asList("on", "off");
protected @Nullable String temperature_command_template;
protected @Nullable String temperature_command_topic;
protected @Nullable String temperature_state_template;
protected @Nullable String temperature_state_topic;
protected @Nullable String temperature_high_command_template;
protected @Nullable String temperature_high_command_topic;
protected @Nullable String temperature_high_state_template;
protected @Nullable String temperature_high_state_topic;
protected @Nullable String temperature_low_command_template;
protected @Nullable String temperature_low_command_topic;
protected @Nullable String temperature_low_state_template;
protected @Nullable String temperature_low_state_topic;
protected @Nullable String power_command_topic;
protected Integer initial = 21;
protected @Nullable Float max_temp;
protected @Nullable Float min_temp;
protected String temperature_unit = CELSIUM; // System unit by default
protected Float temp_step = 1f;
protected @Nullable Float precision;
protected Boolean send_if_off = true;
}
public Climate(ComponentFactory.ComponentConfiguration componentConfiguration) {
super(componentConfiguration, ChannelConfiguration.class);
BigDecimal minTemp = channelConfiguration.min_temp != null ? BigDecimal.valueOf(channelConfiguration.min_temp)
: null;
BigDecimal maxTemp = channelConfiguration.max_temp != null ? BigDecimal.valueOf(channelConfiguration.max_temp)
: null;
float precision = channelConfiguration.precision != null ? channelConfiguration.precision
: (FAHRENHEIT.equals(channelConfiguration.temperature_unit) ? DEFAULT_FAHRENHEIT_PRECISION
: DEFAULT_CELSIUM_PRECISION);
final ChannelStateUpdateListener updateListener = componentConfiguration.getUpdateListener();
ComponentChannel actionChannel = buildOptionalChannel(ACTION_CH_ID,
new TextValue(ACTION_MODES.toArray(new String[0])), updateListener, null, null,
channelConfiguration.action_template, channelConfiguration.action_topic, null);
final Predicate<Command> commandFilter = channelConfiguration.send_if_off ? null
: getCommandFilter(actionChannel);
buildOptionalChannel(AUX_CH_ID, new OnOffValue(), updateListener, null, channelConfiguration.aux_command_topic,
channelConfiguration.aux_state_template, channelConfiguration.aux_state_topic, commandFilter);
buildOptionalChannel(AWAY_MODE_CH_ID, new OnOffValue(), updateListener, null,
channelConfiguration.away_mode_command_topic, channelConfiguration.away_mode_state_template,
channelConfiguration.away_mode_state_topic, commandFilter);
buildOptionalChannel(CURRENT_TEMPERATURE_CH_ID,
new NumberValue(minTemp, maxTemp, BigDecimal.valueOf(precision), channelConfiguration.temperature_unit),
updateListener, null, null, channelConfiguration.current_temperature_template,
channelConfiguration.current_temperature_topic, commandFilter);
buildOptionalChannel(FAN_MODE_CH_ID, new TextValue(channelConfiguration.fan_modes.toArray(new String[0])),
updateListener, channelConfiguration.fan_mode_command_template,
channelConfiguration.fan_mode_command_topic, channelConfiguration.fan_mode_state_template,
channelConfiguration.fan_mode_state_topic, commandFilter);
if (channelConfiguration.hold_modes != null && !channelConfiguration.hold_modes.isEmpty()) {
buildOptionalChannel(HOLD_CH_ID, new TextValue(channelConfiguration.hold_modes.toArray(new String[0])),
updateListener, channelConfiguration.hold_command_template, channelConfiguration.hold_command_topic,
channelConfiguration.hold_state_template, channelConfiguration.hold_state_topic, commandFilter);
}
buildOptionalChannel(MODE_CH_ID, new TextValue(channelConfiguration.modes.toArray(new String[0])),
updateListener, channelConfiguration.mode_command_template, channelConfiguration.mode_command_topic,
channelConfiguration.mode_state_template, channelConfiguration.mode_state_topic, commandFilter);
buildOptionalChannel(SWING_CH_ID, new TextValue(channelConfiguration.swing_modes.toArray(new String[0])),
updateListener, channelConfiguration.swing_command_template, channelConfiguration.swing_command_topic,
channelConfiguration.swing_state_template, channelConfiguration.swing_state_topic, commandFilter);
buildOptionalChannel(TEMPERATURE_CH_ID,
new NumberValue(minTemp, maxTemp, BigDecimal.valueOf(channelConfiguration.temp_step),
channelConfiguration.temperature_unit),
updateListener, channelConfiguration.temperature_command_template,
channelConfiguration.temperature_command_topic, channelConfiguration.temperature_state_template,
channelConfiguration.temperature_state_topic, commandFilter);
buildOptionalChannel(TEMPERATURE_HIGH_CH_ID,
new NumberValue(minTemp, maxTemp, BigDecimal.valueOf(channelConfiguration.temp_step),
channelConfiguration.temperature_unit),
updateListener, channelConfiguration.temperature_high_command_template,
channelConfiguration.temperature_high_command_topic,
channelConfiguration.temperature_high_state_template, channelConfiguration.temperature_high_state_topic,
commandFilter);
buildOptionalChannel(TEMPERATURE_LOW_CH_ID,
new NumberValue(minTemp, maxTemp, BigDecimal.valueOf(channelConfiguration.temp_step),
channelConfiguration.temperature_unit),
updateListener, channelConfiguration.temperature_low_command_template,
channelConfiguration.temperature_low_command_topic, channelConfiguration.temperature_low_state_template,
channelConfiguration.temperature_low_state_topic, commandFilter);
buildOptionalChannel(POWER_CH_ID, new OnOffValue(), updateListener, null,
channelConfiguration.power_command_topic, null, null, null);
}
@Nullable
private ComponentChannel buildOptionalChannel(String channelId, Value valueState,
ChannelStateUpdateListener channelStateUpdateListener, @Nullable String commandTemplate,
@Nullable String commandTopic, @Nullable String stateTemplate, @Nullable String stateTopic,
@Nullable Predicate<Command> commandFilter) {
if ((commandTopic != null && !commandTopic.isBlank()) || (stateTopic != null && !stateTopic.isBlank())) {
return buildChannel(channelId, valueState, channelConfiguration.getName(), channelStateUpdateListener)
.stateTopic(stateTopic, stateTemplate, channelConfiguration.getValueTemplate())
.commandTopic(commandTopic, channelConfiguration.isRetain(), channelConfiguration.getQos(),
commandTemplate)
.commandFilter(commandFilter).build();
}
return null;
}
private @Nullable Predicate<Command> getCommandFilter(@Nullable ComponentChannel actionChannel) {
if (actionChannel == null) {
return null;
}
final var val = actionChannel.getState().getCache();
return command -> !ACTION_OFF_STATE.equals(val.getChannelState());
}
}

View File

@@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mqtt.homeassistant.internal;
package org.openhab.binding.mqtt.homeassistant.internal.component;
import java.util.concurrent.ScheduledExecutorService;
@@ -19,6 +19,8 @@ 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.homeassistant.internal.HaID;
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
import org.openhab.core.thing.ThingUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -32,8 +34,8 @@ import com.google.gson.Gson;
* @author David Graeff - Initial contribution
*/
@NonNullByDefault
public class CFactory {
private static final Logger logger = LoggerFactory.getLogger(CFactory.class);
public class ComponentFactory {
private static final Logger logger = LoggerFactory.getLogger(ComponentFactory.class);
/**
* Create a HA MQTT component. The configuration JSon string is required.
@@ -41,7 +43,7 @@ public class CFactory {
* @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
* @param channelConfigurationJSON 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
@@ -56,25 +58,25 @@ public class CFactory {
try {
switch (haID.component) {
case "alarm_control_panel":
return new ComponentAlarmControlPanel(componentConfiguration);
return new AlarmControlPanel(componentConfiguration);
case "binary_sensor":
return new ComponentBinarySensor(componentConfiguration);
return new BinarySensor(componentConfiguration);
case "camera":
return new ComponentCamera(componentConfiguration);
return new Camera(componentConfiguration);
case "cover":
return new ComponentCover(componentConfiguration);
return new Cover(componentConfiguration);
case "fan":
return new ComponentFan(componentConfiguration);
return new Fan(componentConfiguration);
case "climate":
return new ComponentClimate(componentConfiguration);
return new Climate(componentConfiguration);
case "light":
return new ComponentLight(componentConfiguration);
return new Light(componentConfiguration);
case "lock":
return new ComponentLock(componentConfiguration);
return new Lock(componentConfiguration);
case "sensor":
return new ComponentSensor(componentConfiguration);
return new Sensor(componentConfiguration);
case "switch":
return new ComponentSwitch(componentConfiguration);
return new Switch(componentConfiguration);
}
} catch (UnsupportedOperationException e) {
logger.warn("Not supported", e);
@@ -92,6 +94,14 @@ public class CFactory {
private final ScheduledExecutorService scheduler;
private @Nullable TransformationServiceProvider transformationServiceProvider;
/**
* Provide a thingUID and HomeAssistant topic ID to determine the channel group UID and type.
*
* @param thingUID A ThingUID
* @param haID A HomeAssistant topic ID
* @param configJSON The configuration string
* @param gson A Gson instance
*/
protected ComponentConfiguration(ThingUID thingUID, HaID haID, String configJSON, Gson gson,
ChannelStateUpdateListener updateListener, AvailabilityTracker tracker,
ScheduledExecutorService scheduler) {
@@ -143,8 +153,8 @@ public class CFactory {
return scheduler;
}
public <C extends BaseChannelConfiguration> C getConfig(Class<C> clazz) {
return BaseChannelConfiguration.fromString(configJSON, gson, clazz);
public <C extends AbstractChannelConfiguration> C getConfig(Class<C> clazz) {
return AbstractChannelConfiguration.fromString(configJSON, gson, clazz);
}
}
}

View File

@@ -10,11 +10,12 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mqtt.homeassistant.internal;
package org.openhab.binding.mqtt.homeassistant.internal.component;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.values.RollershutterValue;
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
/**
* A MQTT Cover component, following the https://www.home-assistant.io/components/cover.mqtt/ specification.
@@ -24,13 +25,13 @@ import org.openhab.binding.mqtt.generic.values.RollershutterValue;
* @author David Graeff - Initial contribution
*/
@NonNullByDefault
public class ComponentCover extends AbstractComponent<ComponentCover.ChannelConfiguration> {
public class Cover extends AbstractComponent<Cover.ChannelConfiguration> {
public static final String switchChannelID = "cover"; // Randomly chosen channel "ID"
/**
* Configuration class for MQTT component
*/
static class ChannelConfiguration extends BaseChannelConfiguration {
static class ChannelConfiguration extends AbstractChannelConfiguration {
ChannelConfiguration() {
super("MQTT Cover");
}
@@ -42,14 +43,16 @@ public class ComponentCover extends AbstractComponent<ComponentCover.ChannelConf
protected String payload_stop = "STOP";
}
public ComponentCover(CFactory.ComponentConfiguration componentConfiguration) {
public Cover(ComponentFactory.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();
buildChannel(switchChannelID, value, channelConfiguration.getName(), componentConfiguration.getUpdateListener())
.stateTopic(channelConfiguration.state_topic, channelConfiguration.getValueTemplate())
.commandTopic(channelConfiguration.command_topic, channelConfiguration.isRetain(),
channelConfiguration.getQos())
.build();
}
}

View File

@@ -10,11 +10,12 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mqtt.homeassistant.internal;
package org.openhab.binding.mqtt.homeassistant.internal.component;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.values.OnOffValue;
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
/**
* A MQTT Fan component, following the https://www.home-assistant.io/components/fan.mqtt/ specification.
@@ -24,13 +25,13 @@ import org.openhab.binding.mqtt.generic.values.OnOffValue;
* @author David Graeff - Initial contribution
*/
@NonNullByDefault
public class ComponentFan extends AbstractComponent<ComponentFan.ChannelConfiguration> {
public class Fan extends AbstractComponent<Fan.ChannelConfiguration> {
public static final String switchChannelID = "fan"; // Randomly chosen channel "ID"
/**
* Configuration class for MQTT component
*/
static class ChannelConfiguration extends BaseChannelConfiguration {
static class ChannelConfiguration extends AbstractChannelConfiguration {
ChannelConfiguration() {
super("MQTT Fan");
}
@@ -41,12 +42,14 @@ public class ComponentFan extends AbstractComponent<ComponentFan.ChannelConfigur
protected String payload_off = "OFF";
}
public ComponentFan(CFactory.ComponentConfiguration componentConfiguration) {
public Fan(ComponentFactory.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();
buildChannel(switchChannelID, value, channelConfiguration.getName(), componentConfiguration.getUpdateListener())
.stateTopic(channelConfiguration.state_topic, channelConfiguration.getValueTemplate())
.commandTopic(channelConfiguration.command_topic, channelConfiguration.isRetain(),
channelConfiguration.getQos())
.build();
}
}

View File

@@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mqtt.homeassistant.internal;
package org.openhab.binding.mqtt.homeassistant.internal.component;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@@ -22,6 +22,8 @@ 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.binding.mqtt.homeassistant.internal.ComponentChannel;
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.types.Command;
@@ -36,8 +38,7 @@ import org.openhab.core.types.State;
* @author David Graeff - Initial contribution
*/
@NonNullByDefault
public class ComponentLight extends AbstractComponent<ComponentLight.ChannelConfiguration>
implements ChannelStateUpdateListener {
public class Light extends AbstractComponent<Light.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"
@@ -45,7 +46,7 @@ public class ComponentLight extends AbstractComponent<ComponentLight.ChannelConf
/**
* Configuration class for MQTT component
*/
static class ChannelConfiguration extends BaseChannelConfiguration {
static class ChannelConfiguration extends AbstractChannelConfiguration {
ChannelConfiguration() {
super("MQTT Light");
}
@@ -93,30 +94,36 @@ public class ComponentLight extends AbstractComponent<ComponentLight.ChannelConf
protected String payload_off = "OFF";
}
protected CChannel colorChannel;
protected CChannel switchChannel;
protected CChannel brightnessChannel;
protected ComponentChannel colorChannel;
protected ComponentChannel switchChannel;
protected ComponentChannel brightnessChannel;
private final @Nullable ChannelStateUpdateListener channelStateUpdateListener;
public ComponentLight(CFactory.ComponentConfiguration builder) {
public Light(ComponentFactory.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)
switchChannel = buildChannel(switchChannelID, value, channelConfiguration.getName(), this)
.stateTopic(channelConfiguration.state_topic, channelConfiguration.state_value_template,
channelConfiguration.value_template)
.commandTopic(channelConfiguration.command_topic, channelConfiguration.retain).build(false);
channelConfiguration.getValueTemplate())
.commandTopic(channelConfiguration.command_topic, channelConfiguration.isRetain(),
channelConfiguration.getQos())
.build(false);
colorChannel = buildChannel(colorChannelID, value, channelConfiguration.name, this)
colorChannel = buildChannel(colorChannelID, value, channelConfiguration.getName(), this)
.stateTopic(channelConfiguration.rgb_state_topic, channelConfiguration.rgb_value_template)
.commandTopic(channelConfiguration.rgb_command_topic, channelConfiguration.retain).build(false);
.commandTopic(channelConfiguration.rgb_command_topic, channelConfiguration.isRetain(),
channelConfiguration.getQos())
.build(false);
brightnessChannel = buildChannel(brightnessChannelID, value, channelConfiguration.name, this)
brightnessChannel = buildChannel(brightnessChannelID, value, channelConfiguration.getName(), this)
.stateTopic(channelConfiguration.brightness_state_topic, channelConfiguration.brightness_value_template)
.commandTopic(channelConfiguration.brightness_command_topic, channelConfiguration.retain).build(false);
.commandTopic(channelConfiguration.brightness_command_topic, channelConfiguration.isRetain(),
channelConfiguration.getQos())
.build(false);
channels.put(colorChannelID, colorChannel);
}

View File

@@ -10,11 +10,12 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mqtt.homeassistant.internal;
package org.openhab.binding.mqtt.homeassistant.internal.component;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.values.OnOffValue;
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
/**
* A MQTT lock, following the https://www.home-assistant.io/components/lock.mqtt/ specification.
@@ -22,13 +23,13 @@ import org.openhab.binding.mqtt.generic.values.OnOffValue;
* @author David Graeff - Initial contribution
*/
@NonNullByDefault
public class ComponentLock extends AbstractComponent<ComponentLock.ChannelConfiguration> {
public class Lock extends AbstractComponent<Lock.ChannelConfiguration> {
public static final String switchChannelID = "lock"; // Randomly chosen channel "ID"
/**
* Configuration class for MQTT component
*/
static class ChannelConfiguration extends BaseChannelConfiguration {
static class ChannelConfiguration extends AbstractChannelConfiguration {
ChannelConfiguration() {
super("MQTT Lock");
}
@@ -41,7 +42,7 @@ public class ComponentLock extends AbstractComponent<ComponentLock.ChannelConfig
protected @Nullable String command_topic;
}
public ComponentLock(CFactory.ComponentConfiguration componentConfiguration) {
public Lock(ComponentFactory.ComponentConfiguration componentConfiguration) {
super(componentConfiguration, ChannelConfiguration.class);
// We do not support all HomeAssistant quirks
@@ -51,8 +52,10 @@ public class ComponentLock extends AbstractComponent<ComponentLock.ChannelConfig
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();
channelConfiguration.getName(), componentConfiguration.getUpdateListener())
.stateTopic(channelConfiguration.state_topic, channelConfiguration.getValueTemplate())
.commandTopic(channelConfiguration.command_topic, channelConfiguration.isRetain(),
channelConfiguration.getQos())
.build();
}
}

View File

@@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mqtt.homeassistant.internal;
package org.openhab.binding.mqtt.homeassistant.internal.component;
import java.util.List;
import java.util.regex.Pattern;
@@ -21,6 +21,7 @@ import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
import org.openhab.binding.mqtt.generic.values.NumberValue;
import org.openhab.binding.mqtt.generic.values.TextValue;
import org.openhab.binding.mqtt.generic.values.Value;
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
import org.openhab.binding.mqtt.homeassistant.internal.listener.ExpireUpdateStateListener;
/**
@@ -29,14 +30,14 @@ import org.openhab.binding.mqtt.homeassistant.internal.listener.ExpireUpdateStat
* @author David Graeff - Initial contribution
*/
@NonNullByDefault
public class ComponentSensor extends AbstractComponent<ComponentSensor.ChannelConfiguration> {
public class Sensor extends AbstractComponent<Sensor.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 {
static class ChannelConfiguration extends AbstractChannelConfiguration {
ChannelConfiguration() {
super("MQTT Sensor");
}
@@ -53,11 +54,10 @@ public class ComponentSensor extends AbstractComponent<ComponentSensor.ChannelCo
protected @Nullable List<String> json_attributes;
}
public ComponentSensor(CFactory.ComponentConfiguration componentConfiguration) {
public Sensor(ComponentFactory.ComponentConfiguration componentConfiguration) {
super(componentConfiguration, ChannelConfiguration.class);
Value value;
String uom = channelConfiguration.unit_of_measurement;
if (uom != null && !uom.isBlank()) {
@@ -66,16 +66,16 @@ public class ComponentSensor extends AbstractComponent<ComponentSensor.ChannelCo
value = new TextValue();
}
String icon = channelConfiguration.icon;
String icon = channelConfiguration.getIcon();
boolean trigger = triggerIcons.matcher(icon).matches();
buildChannel(sensorChannelID, value, channelConfiguration.name, getListener(componentConfiguration, value))
.stateTopic(channelConfiguration.state_topic, channelConfiguration.value_template)//
buildChannel(sensorChannelID, value, channelConfiguration.getName(), getListener(componentConfiguration, value))
.stateTopic(channelConfiguration.state_topic, channelConfiguration.getValueTemplate())//
.trigger(trigger).build();
}
private ChannelStateUpdateListener getListener(CFactory.ComponentConfiguration componentConfiguration,
private ChannelStateUpdateListener getListener(ComponentFactory.ComponentConfiguration componentConfiguration,
Value value) {
ChannelStateUpdateListener updateListener = componentConfiguration.getUpdateListener();

View File

@@ -10,11 +10,12 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mqtt.homeassistant.internal;
package org.openhab.binding.mqtt.homeassistant.internal.component;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.values.OnOffValue;
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
/**
* A MQTT switch, following the https://www.home-assistant.io/components/switch.mqtt/ specification.
@@ -22,13 +23,13 @@ import org.openhab.binding.mqtt.generic.values.OnOffValue;
* @author David Graeff - Initial contribution
*/
@NonNullByDefault
public class ComponentSwitch extends AbstractComponent<ComponentSwitch.ChannelConfiguration> {
public class Switch extends AbstractComponent<Switch.ChannelConfiguration> {
public static final String switchChannelID = "switch"; // Randomly chosen channel "ID"
/**
* Configuration class for MQTT component
*/
static class ChannelConfiguration extends BaseChannelConfiguration {
static class ChannelConfiguration extends AbstractChannelConfiguration {
ChannelConfiguration() {
super("MQTT Switch");
}
@@ -47,7 +48,7 @@ public class ComponentSwitch extends AbstractComponent<ComponentSwitch.ChannelCo
protected @Nullable String json_attributes_template;
}
public ComponentSwitch(CFactory.ComponentConfiguration componentConfiguration) {
public Switch(ComponentFactory.ComponentConfiguration componentConfiguration) {
super(componentConfiguration, ChannelConfiguration.class);
boolean optimistic = channelConfiguration.optimistic != null ? channelConfiguration.optimistic
@@ -66,8 +67,9 @@ public class ComponentSwitch extends AbstractComponent<ComponentSwitch.ChannelCo
channelConfiguration.payload_off);
buildChannel(switchChannelID, value, "state", componentConfiguration.getUpdateListener())
.stateTopic(channelConfiguration.state_topic, channelConfiguration.value_template)
.commandTopic(channelConfiguration.command_topic, channelConfiguration.retain, channelConfiguration.qos)
.stateTopic(channelConfiguration.state_topic, channelConfiguration.getValueTemplate())
.commandTopic(channelConfiguration.command_topic, channelConfiguration.isRetain(),
channelConfiguration.getQos())
.build();
}
}

View File

@@ -10,13 +10,16 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mqtt.homeassistant.internal;
package org.openhab.binding.mqtt.homeassistant.internal.config;
import java.io.IOException;
import java.lang.reflect.Field;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.homeassistant.internal.MappingJsonReader;
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.Device;
import com.google.gson.Gson;
import com.google.gson.TypeAdapter;
@@ -28,12 +31,16 @@ 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,
* <p>
* It will create a type adapter for every class derived from {@link
* AbstractChannelConfiguration} and ensures,
* that abbreviated names are replaces with their long versions during the read.
*
* <p>
* In elements, whose name end in'_topic' '~' replacement is performed.
*
* The adapters also handle {@link BaseChannelConfiguration.Device}
* <p>
* The adapters also handle {@link Device}
*
* @author Jochen Klein - Initial contribution
*/
@@ -46,21 +53,22 @@ public class ChannelConfigurationTypeAdapterFactory implements TypeAdapterFactor
if (gson == null || type == null) {
return null;
}
if (BaseChannelConfiguration.class.isAssignableFrom(type.getRawType())) {
if (AbstractChannelConfiguration.class.isAssignableFrom(type.getRawType())) {
return createHAConfig(gson, type);
}
if (BaseChannelConfiguration.Device.class.isAssignableFrom(type.getRawType())) {
if (Device.class.isAssignableFrom(type.getRawType())) {
return createHADevice(gson, type);
}
return null;
}
/**
* Handle {@link BaseChannelConfiguration}
* Handle {@link
* AbstractChannelConfiguration}
*
* @param gson
* @param type
* @return
* @param gson parser
* @param type type
* @return adapter
*/
private <T> TypeAdapter<T> createHAConfig(Gson gson, TypeToken<T> type) {
/* The delegate is the 'default' adapter */
@@ -72,7 +80,7 @@ public class ChannelConfigurationTypeAdapterFactory implements TypeAdapterFactor
/* 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));
expandTidleInTopics(AbstractChannelConfiguration.class.cast(result));
return result;
}
@@ -102,10 +110,10 @@ public class ChannelConfigurationTypeAdapterFactory implements TypeAdapterFactor
};
}
private void expandTidleInTopics(BaseChannelConfiguration config) {
private void expandTidleInTopics(AbstractChannelConfiguration config) {
Class<?> type = config.getClass();
String tilde = config.tilde;
String tilde = config.getTilde();
while (type != Object.class) {
Field[] fields = type.getDeclaredFields();
@@ -127,9 +135,7 @@ public class ChannelConfigurationTypeAdapterFactory implements TypeAdapterFactor
}
field.set(config, newValue);
} catch (IllegalArgumentException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
} catch (IllegalArgumentException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}

View File

@@ -10,10 +10,12 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mqtt.homeassistant.internal;
package org.openhab.binding.mqtt.homeassistant.internal.config;
import java.lang.reflect.Type;
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.Connection;
import com.google.gson.JsonArray;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
@@ -27,14 +29,11 @@ import com.google.gson.JsonParseException;
*
* @author Jan N. Klug - Initial contribution
*/
public class ConnectionDeserializer implements JsonDeserializer<BaseChannelConfiguration.Connection> {
public class ConnectionDeserializer implements JsonDeserializer<Connection> {
@Override
public BaseChannelConfiguration.Connection deserialize(JsonElement json, Type typeOfT,
JsonDeserializationContext context) throws JsonParseException {
public 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;
return new Connection(list.get(0).getAsString(), list.get(1).getAsString());
}
}

View File

@@ -10,11 +10,11 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mqtt.homeassistant.internal;
package org.openhab.binding.mqtt.homeassistant.internal.config;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
@@ -62,7 +62,7 @@ public class ListOrStringDeserializer extends TypeAdapter<List<String>> {
in.nextNull();
return null;
case STRING:
return Arrays.asList(in.nextString());
return Collections.singletonList(in.nextString());
case BEGIN_ARRAY:
return readList(in);
default:

View File

@@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mqtt.homeassistant.internal;
package org.openhab.binding.mqtt.homeassistant.internal.config.dto;
import java.util.List;
import java.util.Map;
@@ -22,7 +22,6 @@ 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;
/**
@@ -31,44 +30,8 @@ import com.google.gson.annotations.SerializedName;
* @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 Objects.requireNonNull(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;
public abstract class AbstractChannelConfiguration {
protected String name;
protected String icon = "";
protected int qos; // defaults to 0 according to HA specification
@@ -76,14 +39,34 @@ public abstract class BaseChannelConfiguration {
protected @Nullable String value_template;
protected @Nullable String unique_id;
protected AvailabilityMode availability_mode = AvailabilityMode.LATEST;
protected @Nullable String availability_topic;
protected String payload_available = "online";
protected String payload_not_available = "offline";
/**
* A list of MQTT topics subscribed to receive availability (online/offline) updates. Must not be used together with
* availability_topic
*/
protected @Nullable List<Availability> availability;
@SerializedName(value = "~")
protected String tilde = "";
protected BaseChannelConfiguration(String defaultName) {
protected @Nullable Device device;
/**
* Parse the base properties of the configJSON into a {@link AbstractChannelConfiguration}
*
* @param configJSON channels configuration in JSON
* @param gson parser
* @return configuration object
*/
public static AbstractChannelConfiguration fromString(final String configJSON, final Gson gson) {
return fromString(configJSON, gson, Config.class);
}
protected AbstractChannelConfiguration(String defaultName) {
this.name = defaultName;
}
@@ -91,31 +74,7 @@ public abstract class BaseChannelConfiguration {
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;
public @Nullable String getId() {
List<String> identifiers = this.identifiers;
return identifiers == null ? null : String.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) {
@@ -128,7 +87,6 @@ public abstract class BaseChannelConfiguration {
}
public String getThingId(String defaultId) {
@Nullable
String result = null;
if (this.device != null) {
result = this.device.getId();
@@ -152,10 +110,91 @@ public abstract class BaseChannelConfiguration {
if (model != null) {
properties.put(Thing.PROPERTY_MODEL_ID, model);
}
final String sw_version = device_.sw_version;
final String sw_version = device_.swVersion;
if (sw_version != null) {
properties.put(Thing.PROPERTY_FIRMWARE_VERSION, sw_version);
}
return properties;
}
public String getName() {
return name;
}
public String getIcon() {
return icon;
}
public int getQos() {
return qos;
}
public boolean isRetain() {
return retain;
}
@Nullable
public String getValueTemplate() {
return value_template;
}
@Nullable
public String getUniqueId() {
return unique_id;
}
@Nullable
public String getAvailabilityTopic() {
return availability_topic;
}
public String getPayloadAvailable() {
return payload_available;
}
public String getPayloadNotAvailable() {
return payload_not_available;
}
@Nullable
public Device getDevice() {
return device;
}
@Nullable
public List<Availability> getAvailability() {
return availability;
}
public String getTilde() {
return tilde;
}
public AvailabilityMode getAvailabilityMode() {
return availability_mode;
}
/**
* This class is needed, to be able to parse only the common base attributes.
* Without this, {@link AbstractChannelConfiguration} cannot be instantiated, as it is abstract.
* This is needed during the discovery.
*/
private static class Config extends AbstractChannelConfiguration {
public Config() {
super("private");
}
}
/**
* Parse the configJSON into a subclass of {@link AbstractChannelConfiguration}
*
* @param configJSON channels configuration in JSON
* @param gson parser
* @param clazz target configuration class
* @return configuration object
*/
public static <C extends AbstractChannelConfiguration> C fromString(final String configJSON, final Gson gson,
final Class<C> clazz) {
return Objects.requireNonNull(gson.fromJson(configJSON, clazz));
}
}

View File

@@ -0,0 +1,37 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mqtt.homeassistant.internal.config.dto;
/**
* MQTT topic subscribed to receive availability (online/offline) updates. Must not be used together with
* availability_topic
*
* @author Anton Kharuzhy - Initial contribution
*/
public class Availability {
protected String payload_available = "online";
protected String payload_not_available = "offline";
protected String topic;
public String getPayload_available() {
return payload_available;
}
public String getPayload_not_available() {
return payload_not_available;
}
public String getTopic() {
return topic;
}
}

View File

@@ -0,0 +1,42 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mqtt.homeassistant.internal.config.dto;
import com.google.gson.annotations.SerializedName;
/**
* controls the conditions needed to set the entity to available
*
* @author Anton Kharuzhy - Initial contribution
*/
public enum AvailabilityMode {
/**
* payload_available must be received on all configured availability topics before the entity is marked as online
*/
@SerializedName("all")
ALL,
/**
* payload_available must be received on at least one configured availability topic before the entity is marked as
* online
*/
@SerializedName("any")
ANY,
/**
* the last payload_available or payload_not_available received on any configured availability topic controls the
* availability
*/
@SerializedName("latest")
LATEST
}

View File

@@ -0,0 +1,47 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mqtt.homeassistant.internal.config.dto;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.homeassistant.internal.config.ConnectionDeserializer;
import com.google.gson.annotations.JsonAdapter;
/**
* Connection configuration
*
* @author Jochen Klein - Initial contribution
*/
@JsonAdapter(ConnectionDeserializer.class)
public class Connection {
protected @Nullable String type;
protected @Nullable String identifier;
public Connection() {
}
public Connection(@Nullable String type, @Nullable String identifier) {
this.type = type;
this.identifier = identifier;
}
@Nullable
public String getType() {
return type;
}
@Nullable
public String getIdentifier() {
return identifier;
}
}

View File

@@ -0,0 +1,73 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mqtt.homeassistant.internal.config.dto;
import java.util.List;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.homeassistant.internal.config.ListOrStringDeserializer;
import com.google.gson.annotations.JsonAdapter;
import com.google.gson.annotations.SerializedName;
/**
* Device configuration
*
* @author Jochen Klein - Initial contribution
*/
public 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;
@SerializedName("sw_version")
protected @Nullable String swVersion;
public @Nullable String getId() {
List<String> identifiers = this.identifiers;
return identifiers == null ? null : String.join("_", identifiers);
}
@Nullable
public List<Connection> getConnections() {
return connections;
}
@Nullable
public String getManufacturer() {
return manufacturer;
}
@Nullable
public String getModel() {
return model;
}
@Nullable
public String getName() {
return name;
}
@Nullable
public String getSwVersion() {
return swVersion;
}
@Nullable
public List<String> getIdentifiers() {
return identifiers;
}
}

View File

@@ -33,10 +33,10 @@ 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.binding.mqtt.homeassistant.internal.config.ChannelConfigurationTypeAdapterFactory;
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.DiscoveryService;
@@ -147,7 +147,7 @@ public class HomeAssistantDiscovery extends AbstractMQTTDiscovery {
}
this.future = scheduler.schedule(this::publishResults, 2, TimeUnit.SECONDS);
BaseChannelConfiguration config = BaseChannelConfiguration
AbstractChannelConfiguration config = AbstractChannelConfiguration
.fromString(new String(payload, StandardCharsets.UTF_8), gson);
// We will of course find multiple of the same unique Thing IDs, for each different component another one.

View File

@@ -32,14 +32,14 @@ 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.ComponentChannel;
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.binding.mqtt.homeassistant.internal.component.AbstractComponent;
import org.openhab.binding.mqtt.homeassistant.internal.component.ComponentFactory;
import org.openhab.binding.mqtt.homeassistant.internal.config.ChannelConfigurationTypeAdapterFactory;
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
@@ -153,12 +153,12 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
if (channelConfigurationJSON == null) {
logger.warn("Provided channel does not have a 'config' configuration key!");
} else {
component = CFactory.createComponent(thingUID, haID, channelConfigurationJSON, this, this, scheduler,
gson, transformationServiceProvider);
component = ComponentFactory.createComponent(thingUID, haID, channelConfigurationJSON, this, this,
scheduler, gson, transformationServiceProvider);
}
if (component != null) {
haComponents.put(component.uid().getId(), component);
haComponents.put(component.getGroupUID().getId(), component);
component.addChannelTypes(channelTypeProvider);
} else {
logger.warn("Could not restore component {}", thing);
@@ -235,7 +235,7 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
if (component == null) {
return null;
}
CChannel componentChannel = component.channel(channelUID.getIdWithoutGroup());
ComponentChannel componentChannel = component.getChannel(channelUID.getIdWithoutGroup());
if (componentChannel == null) {
return null;
}
@@ -264,7 +264,7 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
synchronized (haComponents) { // sync whenever discoverComponents is started
for (AbstractComponent<?> discovered : discoveredComponentsList) {
AbstractComponent<?> known = haComponents.get(discovered.uid().getId());
AbstractComponent<?> known = haComponents.get(discovered.getGroupUID().getId());
// Is component already known?
if (known != null) {
if (discovered.getConfigHash() != known.getConfigHash()) {
@@ -280,15 +280,15 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
// Add channel and group types to the types registry
discovered.addChannelTypes(channelTypeProvider);
// Add component to the component map
haComponents.put(discovered.uid().getId(), discovered);
haComponents.put(discovered.getGroupUID().getId(), discovered);
// Start component / Subscribe to channel topics
discovered.start(connection, scheduler, 0).exceptionally(e -> {
logger.warn("Failed to start component {}", discovered.uid(), e);
logger.warn("Failed to start component {}", discovered.getGroupUID(), e);
return null;
});
Collection<Channel> channels = discovered.channelTypes().values().stream().map(CChannel::getChannel)
.collect(Collectors.toList());
Collection<Channel> channels = discovered.getChannelMap().values().stream()
.map(ComponentChannel::getChannel).collect(Collectors.toList());
ThingHelper.addChannelsToThing(thing, channels);
}
}
@@ -314,7 +314,7 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
synchronized (haComponents) { // sync whenever discoverComponents is started
groupDefs = haComponents.values().stream().map(AbstractComponent::getGroupDefinition)
.collect(Collectors.toList());
channelDefs = haComponents.values().stream().map(AbstractComponent::type)
channelDefs = haComponents.values().stream().map(AbstractComponent::getType)
.map(ChannelGroupType::getChannelDefinitions).flatMap(List::stream)
.collect(Collectors.toList());
}

View File

@@ -38,7 +38,7 @@ public class ExpireUpdateStateListener extends ChannelStateUpdateListenerProxy {
private final AvailabilityTracker tracker;
private final ScheduledExecutorService scheduler;
private AtomicReference<@Nullable ScheduledFuture<?>> expire = new AtomicReference<>();
private final AtomicReference<@Nullable ScheduledFuture<?>> expire = new AtomicReference<>();
public ExpireUpdateStateListener(ChannelStateUpdateListener original, int expireAfter, Value value,
AvailabilityTracker tracker, ScheduledExecutorService scheduler) {

View File

@@ -37,7 +37,7 @@ public class OffDelayUpdateStateListener extends ChannelStateUpdateListenerProxy
private final Value value;
private final ScheduledExecutorService scheduler;
private AtomicReference<@Nullable ScheduledFuture<?>> delay = new AtomicReference<>();
private final AtomicReference<@Nullable ScheduledFuture<?>> delay = new AtomicReference<>();
public OffDelayUpdateStateListener(ChannelStateUpdateListener original, int offDelay, Value value,
ScheduledExecutorService scheduler) {