[mqtt.homeassistant] Implement WaterHeater (#17859)

* [mqtt.homeassistant] Implement WaterHeater

Signed-off-by: Cody Cutrer <cody@cutrer.us>
Signed-off-by: Ciprian Pascu <contact@ciprianpascu.ro>
This commit is contained in:
Cody Cutrer 2024-12-07 08:34:05 -07:00 committed by Ciprian Pascu
parent 56c55a9604
commit 74329618b5
16 changed files with 455 additions and 58 deletions

View File

@ -22,6 +22,7 @@ import org.openhab.binding.mqtt.generic.MqttChannelStateDescriptionProvider;
import org.openhab.binding.mqtt.generic.MqttChannelTypeProvider;
import org.openhab.binding.mqtt.homeassistant.internal.HomeAssistantJinjaFunctionLibrary;
import org.openhab.binding.mqtt.homeassistant.internal.handler.HomeAssistantThingHandler;
import org.openhab.core.i18n.UnitProvider;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
@ -47,6 +48,7 @@ public class MqttThingHandlerFactory extends BaseThingHandlerFactory {
private final MqttChannelStateDescriptionProvider stateDescriptionProvider;
private final ChannelTypeRegistry channelTypeRegistry;
private final Jinjava jinjava = new Jinjava();
private final UnitProvider unitProvider;
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Stream
.of(MqttBindingConstants.HOMEASSISTANT_MQTT_THING).collect(Collectors.toSet());
@ -54,10 +56,11 @@ public class MqttThingHandlerFactory extends BaseThingHandlerFactory {
@Activate
public MqttThingHandlerFactory(final @Reference MqttChannelTypeProvider typeProvider,
final @Reference MqttChannelStateDescriptionProvider stateDescriptionProvider,
final @Reference ChannelTypeRegistry channelTypeRegistry) {
final @Reference ChannelTypeRegistry channelTypeRegistry, final @Reference UnitProvider unitProvider) {
this.typeProvider = typeProvider;
this.stateDescriptionProvider = stateDescriptionProvider;
this.channelTypeRegistry = channelTypeRegistry;
this.unitProvider = unitProvider;
HomeAssistantJinjaFunctionLibrary.register(jinjava.getGlobalContext());
}
@ -78,7 +81,7 @@ public class MqttThingHandlerFactory extends BaseThingHandlerFactory {
if (supportsThingType(thingTypeUID)) {
return new HomeAssistantThingHandler(thing, typeProvider, stateDescriptionProvider, channelTypeRegistry,
jinjava, 10000, 2000);
jinjava, unitProvider, 10000, 2000);
}
return null;
}

View File

@ -33,7 +33,8 @@ public enum ComponentChannelType {
SWITCH("ha-switch"),
TRIGGER("ha-trigger"),
HUMIDITY("ha-humidity"),
GPS_ACCURACY("ha-gps-accuracy");
GPS_ACCURACY("ha-gps-accuracy"),
TEMPERATURE("ha-temperature");
final ChannelTypeUID channelTypeUID;

View File

@ -30,6 +30,7 @@ import org.openhab.binding.mqtt.homeassistant.internal.component.AbstractCompone
import org.openhab.binding.mqtt.homeassistant.internal.component.ComponentFactory;
import org.openhab.binding.mqtt.homeassistant.internal.exception.ConfigurationException;
import org.openhab.binding.mqtt.homeassistant.internal.exception.UnsupportedComponentException;
import org.openhab.core.i18n.UnitProvider;
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
import org.openhab.core.io.transport.mqtt.MqttMessageSubscriber;
import org.openhab.core.thing.ThingUID;
@ -57,6 +58,7 @@ public class DiscoverComponents implements MqttMessageSubscriber {
protected final CompletableFuture<@Nullable Void> discoverFinishedFuture = new CompletableFuture<>();
private final Gson gson;
private final Jinjava jinjava;
private final UnitProvider unitProvider;
private @Nullable ScheduledFuture<?> stopDiscoveryFuture;
private WeakReference<@Nullable MqttBrokerConnection> connectionRef = new WeakReference<>(null);
@ -82,12 +84,13 @@ public class DiscoverComponents implements MqttMessageSubscriber {
*/
public DiscoverComponents(ThingUID thingUID, ScheduledExecutorService scheduler,
ChannelStateUpdateListener channelStateUpdateListener, AvailabilityTracker tracker, Gson gson,
Jinjava jinjava, boolean newStyleChannels) {
Jinjava jinjava, UnitProvider unitProvider, boolean newStyleChannels) {
this.thingUID = thingUID;
this.scheduler = scheduler;
this.updateListener = channelStateUpdateListener;
this.gson = gson;
this.jinjava = jinjava;
this.unitProvider = unitProvider;
this.tracker = tracker;
this.newStyleChannels = newStyleChannels;
}
@ -105,7 +108,7 @@ public class DiscoverComponents implements MqttMessageSubscriber {
if (config.length() > 0) {
try {
component = ComponentFactory.createComponent(thingUID, haID, config, updateListener, tracker, scheduler,
gson, jinjava, newStyleChannels);
gson, jinjava, unitProvider, newStyleChannels);
component.setConfigSeen();
logger.trace("Found HomeAssistant component {}", haID);
@ -119,8 +122,6 @@ public class DiscoverComponents implements MqttMessageSubscriber {
} catch (ConfigurationException e) {
logger.warn("HomeAssistant discover error: invalid configuration of thing {} component {}: {}",
haID.objectID, haID.component, e.getMessage());
} catch (Exception e) {
logger.warn("HomeAssistant discover error: {}", e.getMessage());
}
} else {
if (discoveredListener != null) {

View File

@ -12,6 +12,7 @@
*/
package org.openhab.binding.mqtt.homeassistant.internal.component;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@ -21,6 +22,9 @@ import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ScheduledExecutorService;
import java.util.stream.Stream;
import javax.measure.Unit;
import javax.measure.quantity.Temperature;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.AvailabilityTracker;
@ -40,6 +44,8 @@ import org.openhab.binding.mqtt.homeassistant.internal.config.dto.Availability;
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AvailabilityMode;
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.Device;
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
import org.openhab.core.library.unit.ImperialUnits;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.binding.generic.ChannelTransformation;
@ -53,6 +59,7 @@ import org.openhab.core.types.CommandDescription;
import org.openhab.core.types.StateDescription;
import com.google.gson.Gson;
import com.google.gson.annotations.SerializedName;
import com.hubspot.jinjava.Jinjava;
/**
@ -64,6 +71,29 @@ import com.hubspot.jinjava.Jinjava;
*/
@NonNullByDefault
public abstract class AbstractComponent<C extends AbstractChannelConfiguration> {
public enum TemperatureUnit {
@SerializedName("C")
CELSIUS(SIUnits.CELSIUS, new BigDecimal("0.1")),
@SerializedName("F")
FAHRENHEIT(ImperialUnits.FAHRENHEIT, BigDecimal.ONE);
private final Unit<Temperature> unit;
private final BigDecimal defaultPrecision;
TemperatureUnit(Unit<Temperature> unit, BigDecimal defaultPrecision) {
this.unit = unit;
this.defaultPrecision = defaultPrecision;
}
public Unit<Temperature> getUnit() {
return unit;
}
public BigDecimal getDefaultPrecision() {
return defaultPrecision;
}
}
public static final String JSON_ATTRIBUTES_CHANNEL_ID = "json-attributes";
// Component location fields

View File

@ -17,7 +17,6 @@ import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
import javax.measure.Unit;
import javax.measure.quantity.Temperature;
import org.eclipse.jdt.annotation.NonNullByDefault;
@ -32,7 +31,6 @@ import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannelType;
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.unit.ImperialUnits;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.library.unit.Units;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
@ -69,29 +67,6 @@ public class Climate extends AbstractComponent<Climate.ChannelConfiguration> {
public static final String TEMPERATURE_LOW_CH_ID_DEPRECATED = "temperatureLow";
public static final String POWER_CH_ID = "power";
public enum TemperatureUnit {
@SerializedName("C")
CELSIUS(SIUnits.CELSIUS, new BigDecimal("0.1")),
@SerializedName("F")
FAHRENHEIT(ImperialUnits.FAHRENHEIT, BigDecimal.ONE);
private final Unit<Temperature> unit;
private final BigDecimal defaultPrecision;
TemperatureUnit(Unit<Temperature> unit, BigDecimal defaultPrecision) {
this.unit = unit;
this.defaultPrecision = defaultPrecision;
}
public Unit<Temperature> getUnit() {
return unit;
}
public BigDecimal getDefaultPrecision() {
return defaultPrecision;
}
}
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");
@ -241,7 +216,7 @@ public class Climate extends AbstractComponent<Climate.ChannelConfiguration> {
@SerializedName("min_temp")
protected @Nullable BigDecimal minTemp;
@SerializedName("temperature_unit")
protected TemperatureUnit temperatureUnit = TemperatureUnit.CELSIUS; // System unit by default
protected @Nullable TemperatureUnit temperatureUnit;
@SerializedName("temp_step")
protected BigDecimal tempStep = BigDecimal.ONE;
protected @Nullable BigDecimal precision;
@ -252,8 +227,16 @@ public class Climate extends AbstractComponent<Climate.ChannelConfiguration> {
public Climate(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
super(componentConfiguration, ChannelConfiguration.class, newStyleChannels);
TemperatureUnit temperatureUnit = channelConfiguration.temperatureUnit;
if (channelConfiguration.temperatureUnit == null) {
if (ImperialUnits.FAHRENHEIT.equals(componentConfiguration.getUnitProvider().getUnit(Temperature.class))) {
temperatureUnit = TemperatureUnit.FAHRENHEIT;
} else {
temperatureUnit = TemperatureUnit.CELSIUS;
}
}
BigDecimal precision = channelConfiguration.precision != null ? channelConfiguration.precision
: channelConfiguration.temperatureUnit.getDefaultPrecision();
: temperatureUnit.getDefaultPrecision();
final ChannelStateUpdateListener updateListener = componentConfiguration.getUpdateListener();
ComponentChannel actionChannel = buildOptionalChannel(ACTION_CH_ID, ComponentChannelType.STRING,
@ -277,9 +260,8 @@ public class Climate extends AbstractComponent<Climate.ChannelConfiguration> {
null, channelConfiguration.currentHumidityTemplate, channelConfiguration.currentHumidityTopic, null);
buildOptionalChannel(newStyleChannels ? CURRENT_TEMPERATURE_CH_ID : CURRENT_TEMPERATURE_CH_ID_DEPRECATED,
ComponentChannelType.NUMBER,
new NumberValue(null, null, precision, channelConfiguration.temperatureUnit.getUnit()), updateListener,
null, null, channelConfiguration.currentTemperatureTemplate,
ComponentChannelType.TEMPERATURE, new NumberValue(null, null, precision, temperatureUnit.getUnit()),
updateListener, null, null, channelConfiguration.currentTemperatureTemplate,
channelConfiguration.currentTemperatureTopic, commandFilter);
buildOptionalChannel(newStyleChannels ? FAN_MODE_CH_ID : FAN_MODE_CH_ID_DEPRECATED, ComponentChannelType.STRING,
@ -317,25 +299,25 @@ public class Climate extends AbstractComponent<Climate.ChannelConfiguration> {
channelConfiguration.targetHumidityCommandTopic, channelConfiguration.targetHumidityStateTemplate,
channelConfiguration.targetHumidityStateTopic, commandFilter);
buildOptionalChannel(TEMPERATURE_CH_ID, ComponentChannelType.NUMBER,
buildOptionalChannel(TEMPERATURE_CH_ID, ComponentChannelType.TEMPERATURE,
new NumberValue(channelConfiguration.minTemp, channelConfiguration.maxTemp,
channelConfiguration.tempStep, channelConfiguration.temperatureUnit.getUnit()),
channelConfiguration.tempStep, temperatureUnit.getUnit()),
updateListener, channelConfiguration.temperatureCommandTemplate,
channelConfiguration.temperatureCommandTopic, channelConfiguration.temperatureStateTemplate,
channelConfiguration.temperatureStateTopic, commandFilter);
buildOptionalChannel(newStyleChannels ? TEMPERATURE_HIGH_CH_ID : TEMPERATURE_HIGH_CH_ID_DEPRECATED,
ComponentChannelType.NUMBER,
ComponentChannelType.TEMPERATURE,
new NumberValue(channelConfiguration.minTemp, channelConfiguration.maxTemp,
channelConfiguration.tempStep, channelConfiguration.temperatureUnit.getUnit()),
channelConfiguration.tempStep, temperatureUnit.getUnit()),
updateListener, channelConfiguration.temperatureHighCommandTemplate,
channelConfiguration.temperatureHighCommandTopic, channelConfiguration.temperatureHighStateTemplate,
channelConfiguration.temperatureHighStateTopic, commandFilter);
buildOptionalChannel(newStyleChannels ? TEMPERATURE_LOW_CH_ID : TEMPERATURE_LOW_CH_ID_DEPRECATED,
ComponentChannelType.NUMBER,
ComponentChannelType.TEMPERATURE,
new NumberValue(channelConfiguration.minTemp, channelConfiguration.maxTemp,
channelConfiguration.tempStep, channelConfiguration.temperatureUnit.getUnit()),
channelConfiguration.tempStep, temperatureUnit.getUnit()),
updateListener, channelConfiguration.temperatureLowCommandTemplate,
channelConfiguration.temperatureLowCommandTopic, channelConfiguration.temperatureLowStateTemplate,
channelConfiguration.temperatureLowStateTopic, commandFilter);

View File

@ -21,6 +21,7 @@ import org.openhab.binding.mqtt.homeassistant.internal.HaID;
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
import org.openhab.binding.mqtt.homeassistant.internal.exception.ConfigurationException;
import org.openhab.binding.mqtt.homeassistant.internal.exception.UnsupportedComponentException;
import org.openhab.core.i18n.UnitProvider;
import org.openhab.core.thing.ThingUID;
import com.google.gson.Gson;
@ -47,9 +48,10 @@ public class ComponentFactory {
*/
public static AbstractComponent<?> createComponent(ThingUID thingUID, HaID haID, String channelConfigurationJSON,
ChannelStateUpdateListener updateListener, AvailabilityTracker tracker, ScheduledExecutorService scheduler,
Gson gson, Jinjava jinjava, boolean newStyleChannels) throws ConfigurationException {
Gson gson, Jinjava jinjava, UnitProvider unitProvider, boolean newStyleChannels)
throws ConfigurationException {
ComponentConfiguration componentConfiguration = new ComponentConfiguration(thingUID, haID,
channelConfigurationJSON, gson, jinjava, updateListener, tracker, scheduler);
channelConfigurationJSON, gson, jinjava, updateListener, tracker, scheduler, unitProvider);
switch (haID.component) {
case "alarm_control_panel":
return new AlarmControlPanel(componentConfiguration, newStyleChannels);
@ -97,6 +99,8 @@ public class ComponentFactory {
return new Vacuum(componentConfiguration, newStyleChannels);
case "valve":
return new Valve(componentConfiguration, newStyleChannels);
case "water_heater":
return new WaterHeater(componentConfiguration, newStyleChannels);
default:
throw new UnsupportedComponentException("Component '" + haID + "' is unsupported!");
}
@ -111,6 +115,7 @@ public class ComponentFactory {
private final Gson gson;
private final Jinjava jinjava;
private final ScheduledExecutorService scheduler;
private final UnitProvider unitProvider;
/**
* Provide a thingUID and HomeAssistant topic ID to determine the channel group UID and type.
@ -122,7 +127,7 @@ public class ComponentFactory {
*/
protected ComponentConfiguration(ThingUID thingUID, HaID haID, String configJSON, Gson gson, Jinjava jinjava,
ChannelStateUpdateListener updateListener, AvailabilityTracker tracker,
ScheduledExecutorService scheduler) {
ScheduledExecutorService scheduler, UnitProvider unitProvider) {
this.thingUID = thingUID;
this.haID = haID;
this.configJSON = configJSON;
@ -131,6 +136,7 @@ public class ComponentFactory {
this.updateListener = updateListener;
this.tracker = tracker;
this.scheduler = scheduler;
this.unitProvider = unitProvider;
}
public ThingUID getThingUID() {
@ -157,6 +163,10 @@ public class ComponentFactory {
return jinjava;
}
public UnitProvider getUnitProvider() {
return unitProvider;
}
public AvailabilityTracker getTracker() {
return tracker;
}

View File

@ -0,0 +1,190 @@
/**
* Copyright (c) 2010-2024 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.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import javax.measure.quantity.Temperature;
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.OnOffValue;
import org.openhab.binding.mqtt.generic.values.TextValue;
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannelType;
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
import org.openhab.binding.mqtt.homeassistant.internal.exception.ConfigurationException;
import org.openhab.core.library.unit.ImperialUnits;
import com.google.gson.annotations.SerializedName;
/**
* A MQTT Humidifier, following the https://www.home-assistant.io/integrations/water_heater.mqtt/ specification.
*
* @author Cody Cutrer - Initial contribution
*/
@NonNullByDefault
public class WaterHeater extends AbstractComponent<WaterHeater.ChannelConfiguration> {
public static final String CURRENT_TEMPERATURE_CHANNEL_ID = "current-temperature";
public static final String MODE_CHANNEL_ID = "mode";
public static final String STATE_CHANNEL_ID = "state";
public static final String TARGET_TEMPERATURE_CHANNEL_ID = "target-temperature";
public static final String PLATFORM_WATER_HEATER = "water_heater";
public static final String MODE_OFF = "off";
public static final String MODE_ECO = "eco";
public static final String MODE_ELECTRIC = "electric";
public static final String MODE_GAS = "gas";
public static final String MODE_HEAT_PUMP = "heat_pump";
public static final String MODE_HIGH_DEMAND = "high_demand";
public static final String MODE_PERFORMANCE = "performance";
public static final List<String> DEFAULT_MODES = List.of(MODE_OFF, MODE_ECO, MODE_ELECTRIC, MODE_GAS,
MODE_HEAT_PUMP, MODE_HIGH_DEMAND, MODE_PERFORMANCE);
public static final String TEMPERATURE_UNIT_C = "C";
public static final String TEMPERATURE_UNIT_F = "F";
/**
* Configuration class for MQTT component
*/
static class ChannelConfiguration extends AbstractChannelConfiguration {
ChannelConfiguration() {
super("MQTT Humidifier");
}
protected @Nullable Boolean optimistic;
@SerializedName("power_command_topic")
protected @Nullable String powerCommandTopic;
@SerializedName("power_command_template")
protected @Nullable String powerCommandTemplate;
@SerializedName("current_temperature_topic")
protected @Nullable String currentTemperatureTopic;
@SerializedName("current_temperature_template")
protected @Nullable String currentTemperatureTemplate;
@SerializedName("temperature_command_topic")
protected @Nullable String temperatureCommandTopic;
@SerializedName("temperature_command_template")
protected @Nullable String temperatureCommandTemplate;
@SerializedName("temperature_state_topic")
protected @Nullable String temperatureStateTopic;
@SerializedName("temperature_state_template")
protected @Nullable String temperatureStateTemplate;
@SerializedName("mode_command_topic")
protected @Nullable String modeCommandTopic;
@SerializedName("mode_command_template")
protected @Nullable String modeCommandTemplate;
@SerializedName("mode_state_topic")
protected @Nullable String modeStateTopic;
@SerializedName("mode_state_template")
protected @Nullable String modeStateTemplate;
@SerializedName("device_class")
protected @Nullable String deviceClass;
protected String platform = "";
protected @Nullable Integer initial;
@SerializedName("min_temp")
protected @Nullable BigDecimal minTemp;
@SerializedName("max_temp")
protected @Nullable BigDecimal maxTemp;
protected @Nullable BigDecimal precision;
@SerializedName("temperature_unit")
protected @Nullable TemperatureUnit temperatureUnit;
@SerializedName("payload_on")
protected String payloadOn = "ON";
@SerializedName("payload_off")
protected String payloadOff = "OFF";
protected List<String> modes = DEFAULT_MODES;
}
public WaterHeater(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
super(componentConfiguration, ChannelConfiguration.class, newStyleChannels);
if (!PLATFORM_WATER_HEATER.equals(channelConfiguration.platform)) {
throw new ConfigurationException("platform must be " + PLATFORM_WATER_HEATER);
}
TemperatureUnit temperatureUnit = channelConfiguration.temperatureUnit;
if (channelConfiguration.temperatureUnit == null) {
if (ImperialUnits.FAHRENHEIT.equals(componentConfiguration.getUnitProvider().getUnit(Temperature.class))) {
temperatureUnit = TemperatureUnit.FAHRENHEIT;
} else {
temperatureUnit = TemperatureUnit.CELSIUS;
}
}
BigDecimal precision = channelConfiguration.precision != null ? channelConfiguration.precision
: temperatureUnit.getDefaultPrecision();
List<String> onStates = new ArrayList<>(channelConfiguration.modes);
onStates.remove(MODE_OFF);
List<String> unsupportedModes = onStates.stream().filter(mode -> !DEFAULT_MODES.contains(mode))
.collect(Collectors.toList());
if (!unsupportedModes.isEmpty()) {
throw new ConfigurationException("unsupported modes: " + unsupportedModes.toString());
}
if (channelConfiguration.powerCommandTopic != null) {
buildChannel(STATE_CHANNEL_ID, ComponentChannelType.SWITCH,
new OnOffValue(onStates.toArray(new String[0]), new String[] { MODE_OFF },
channelConfiguration.payloadOn, channelConfiguration.payloadOff),
"State", componentConfiguration.getUpdateListener())
.stateTopic(channelConfiguration.modeStateTopic, channelConfiguration.modeStateTemplate,
channelConfiguration.getValueTemplate())
.commandTopic(channelConfiguration.powerCommandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos(), channelConfiguration.powerCommandTemplate)
.inferOptimistic(channelConfiguration.optimistic).build();
}
if (channelConfiguration.modeCommandTopic != null | channelConfiguration.modeStateTopic != null) {
buildChannel(MODE_CHANNEL_ID, ComponentChannelType.STRING,
new TextValue(channelConfiguration.modes.toArray(new String[0])), "Mode",
componentConfiguration.getUpdateListener())
.stateTopic(channelConfiguration.modeStateTopic, channelConfiguration.modeStateTemplate,
channelConfiguration.getValueTemplate())
.commandTopic(channelConfiguration.modeCommandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos(), channelConfiguration.modeCommandTemplate)
.inferOptimistic(channelConfiguration.optimistic).build();
}
if (channelConfiguration.currentTemperatureTopic != null) {
buildChannel(CURRENT_TEMPERATURE_CHANNEL_ID, ComponentChannelType.TEMPERATURE,
new NumberValue(null, null, null, temperatureUnit.getUnit()), "Current Temperature",
componentConfiguration.getUpdateListener())
.stateTopic(channelConfiguration.currentTemperatureTopic,
channelConfiguration.currentTemperatureTemplate, channelConfiguration.getValueTemplate())
.build();
}
if (channelConfiguration.temperatureStateTopic != null
|| channelConfiguration.temperatureCommandTopic != null) {
buildChannel(TARGET_TEMPERATURE_CHANNEL_ID, ComponentChannelType.TEMPERATURE,
new NumberValue(channelConfiguration.minTemp, channelConfiguration.maxTemp, precision,
temperatureUnit.getUnit()),
"Target Temperature", componentConfiguration.getUpdateListener())
.stateTopic(channelConfiguration.temperatureStateTopic,
channelConfiguration.temperatureStateTemplate, channelConfiguration.getValueTemplate())
.commandTopic(channelConfiguration.temperatureCommandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos(), channelConfiguration.temperatureCommandTemplate)
.inferOptimistic(channelConfiguration.optimistic).build();
}
finalizeChannels();
}
}

View File

@ -47,6 +47,7 @@ import org.openhab.binding.mqtt.homeassistant.internal.config.ChannelConfigurati
import org.openhab.binding.mqtt.homeassistant.internal.exception.ConfigurationException;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.config.core.validation.ConfigValidationException;
import org.openhab.core.i18n.UnitProvider;
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
@ -95,6 +96,7 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
protected final MqttChannelStateDescriptionProvider stateDescriptionProvider;
protected final ChannelTypeRegistry channelTypeRegistry;
protected final Jinjava jinjava;
protected final UnitProvider unitProvider;
public final int attributeReceiveTimeout;
protected final DelayedBatchProcessing<Object> delayedProcessing;
protected final DiscoverComponents discoverComponents;
@ -123,20 +125,21 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
*/
public HomeAssistantThingHandler(Thing thing, MqttChannelTypeProvider channelTypeProvider,
MqttChannelStateDescriptionProvider stateDescriptionProvider, ChannelTypeRegistry channelTypeRegistry,
Jinjava jinjava, int subscribeTimeout, int attributeReceiveTimeout) {
Jinjava jinjava, UnitProvider unitProvider, int subscribeTimeout, int attributeReceiveTimeout) {
super(thing, subscribeTimeout);
this.gson = new GsonBuilder().registerTypeAdapterFactory(new ChannelConfigurationTypeAdapterFactory()).create();
this.channelTypeProvider = channelTypeProvider;
this.stateDescriptionProvider = stateDescriptionProvider;
this.channelTypeRegistry = channelTypeRegistry;
this.jinjava = jinjava;
this.unitProvider = unitProvider;
this.attributeReceiveTimeout = attributeReceiveTimeout;
this.delayedProcessing = new DelayedBatchProcessing<>(attributeReceiveTimeout, this, scheduler);
newStyleChannels = "true".equals(thing.getProperties().get("newStyleChannels"));
this.discoverComponents = new DiscoverComponents(thing.getUID(), scheduler, this, this, gson, jinjava,
newStyleChannels);
unitProvider, newStyleChannels);
}
@Override
@ -184,7 +187,8 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
String channelConfigurationJSON = (String) channelConfig.get("config");
try {
AbstractComponent<?> component = ComponentFactory.createComponent(thingUID, haID,
channelConfigurationJSON, this, this, scheduler, gson, jinjava, newStyleChannels);
channelConfigurationJSON, this, this, scheduler, gson, jinjava, unitProvider,
newStyleChannels);
if (typeID.equals(MqttBindingConstants.HOMEASSISTANT_MQTT_THING)) {
typeID = calculateThingTypeUID(component);
}

View File

@ -30,6 +30,7 @@ channel-type.mqtt.ha-string-advanced.label = String
channel-type.mqtt.ha-string.label = String
channel-type.mqtt.ha-switch-advanced.label = Switch
channel-type.mqtt.ha-switch.label = Switch
channel-type.mqtt.ha-temperature.label = Temperature
channel-type.mqtt.ha-trigger-advanced.label = Trigger
channel-type.mqtt.ha-trigger.label = Trigger

View File

@ -66,6 +66,12 @@
<config-description-ref uri="channel-type:mqtt:ha-channel"/>
</channel-type>
<channel-type id="ha-temperature">
<item-type>Number:Temperature</item-type>
<label>Temperature</label>
<config-description-ref uri="channel-type:mqtt:ha-channel"/>
</channel-type>
<channel-type id="ha-trigger">
<kind>trigger</kind>
<label>Trigger</label>

View File

@ -32,6 +32,7 @@ import org.openhab.binding.mqtt.generic.MqttChannelStateDescriptionProvider;
import org.openhab.binding.mqtt.generic.MqttChannelTypeProvider;
import org.openhab.binding.mqtt.homeassistant.generic.internal.MqttThingHandlerFactory;
import org.openhab.binding.mqtt.homeassistant.internal.component.AbstractComponent;
import org.openhab.core.i18n.UnitProvider;
import org.openhab.core.test.storage.VolatileStorageService;
import org.openhab.core.thing.type.ChannelTypeRegistry;
import org.openhab.core.thing.type.ThingTypeRegistry;
@ -44,6 +45,7 @@ import org.openhab.core.thing.type.ThingTypeRegistry;
@NonNullByDefault
public class HomeAssistantChannelTransformationTests {
protected @Mock @NonNullByDefault({}) ThingTypeRegistry thingTypeRegistry;
protected @Mock @NonNullByDefault({}) UnitProvider unitProvider;
protected @NonNullByDefault({}) HomeAssistantChannelTransformation transformation;
@ -54,7 +56,7 @@ public class HomeAssistantChannelTransformationTests {
MqttChannelStateDescriptionProvider stateDescriptionProvider = new MqttChannelStateDescriptionProvider();
ChannelTypeRegistry channelTypeRegistry = new ChannelTypeRegistry();
MqttThingHandlerFactory thingHandlerFactory = new MqttThingHandlerFactory(channelTypeProvider,
stateDescriptionProvider, channelTypeRegistry);
stateDescriptionProvider, channelTypeRegistry, unitProvider);
AbstractComponent component = Mockito.mock(AbstractComponent.class);
HaID haID = new HaID("homeassistant/light/pool/light/config");

View File

@ -41,6 +41,7 @@ import org.openhab.binding.mqtt.homeassistant.internal.HaID;
import org.openhab.binding.mqtt.homeassistant.internal.HandlerConfiguration;
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
import org.openhab.binding.mqtt.homeassistant.internal.handler.HomeAssistantThingHandler;
import org.openhab.core.i18n.UnitProvider;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatusInfo;
@ -64,6 +65,7 @@ public abstract class AbstractComponentTests extends AbstractHomeAssistantTests
private @Mock @NonNullByDefault({}) ThingHandlerCallback callbackMock;
private @NonNullByDefault({}) LatchThingHandler thingHandler;
protected @Mock @NonNullByDefault({}) UnitProvider unitProvider;
@BeforeEach
public void setupThingHandler() {
@ -84,7 +86,7 @@ public abstract class AbstractComponentTests extends AbstractHomeAssistantTests
haThing.setProperty("newStyleChannels", "true");
}
thingHandler = new LatchThingHandler(haThing, channelTypeProvider, stateDescriptionProvider,
channelTypeRegistry, SUBSCRIBE_TIMEOUT, ATTRIBUTE_RECEIVE_TIMEOUT);
channelTypeRegistry, unitProvider, SUBSCRIBE_TIMEOUT, ATTRIBUTE_RECEIVE_TIMEOUT);
thingHandler.setConnection(bridgeConnection);
thingHandler.setCallback(callbackMock);
thingHandler = spy(thingHandler);
@ -341,9 +343,9 @@ public abstract class AbstractComponentTests extends AbstractHomeAssistantTests
public LatchThingHandler(Thing thing, MqttChannelTypeProvider channelTypeProvider,
MqttChannelStateDescriptionProvider stateDescriptionProvider, ChannelTypeRegistry channelTypeRegistry,
int subscribeTimeout, int attributeReceiveTimeout) {
UnitProvider unitProvider, int subscribeTimeout, int attributeReceiveTimeout) {
super(thing, channelTypeProvider, stateDescriptionProvider, channelTypeRegistry, new Jinjava(),
subscribeTimeout, attributeReceiveTimeout);
unitProvider, subscribeTimeout, attributeReceiveTimeout);
}
@Override

View File

@ -0,0 +1,159 @@
/**
* Copyright (c) 2010-2024 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 static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.mockito.Mockito.*;
import java.util.Set;
import javax.measure.quantity.Temperature;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
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.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.unit.ImperialUnits;
/**
* Tests for {@link WaterHeater}
*
* @author Cody Cutrer - Initial contribution
*/
@NonNullByDefault
public class WaterHeaterTests extends AbstractComponentTests {
public static final String CONFIG_TOPIC = "water_heater/boiler";
@SuppressWarnings("null")
@Test
public void test() {
when(unitProvider.getUnit(Temperature.class)).thenReturn(ImperialUnits.FAHRENHEIT);
var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC), """
{
"platform": "water_heater",
"name": "Boiler",
"modes": [
"off",
"eco",
"performance"
],
"mode_state_topic": "basement/boiler/mode",
"mode_command_topic": "basement/boiler/mode/set",
"mode_command_template": "{{ value if value==\\"off\\" else \\"on\\" }}",
"temperature_state_topic": "basement/boiler/temperature",
"temperature_command_topic": "basement/boiler/temperature/set",
"current_temperature_topic": "basement/boiler/current_temperature",
"precision": 1.0
}
""");
assertThat(component.channels.size(), is(3));
assertThat(component.getName(), is("Boiler"));
assertChannel(component, WaterHeater.MODE_CHANNEL_ID, "basement/boiler/mode", "basement/boiler/mode/set",
"Mode", TextValue.class);
assertChannel(component, WaterHeater.CURRENT_TEMPERATURE_CHANNEL_ID, "basement/boiler/current_temperature", "",
"Current Temperature", NumberValue.class);
assertChannel(component, WaterHeater.TARGET_TEMPERATURE_CHANNEL_ID, "basement/boiler/temperature",
"basement/boiler/temperature/set", "Target Temperature", NumberValue.class);
publishMessage("basement/boiler/mode", "eco");
assertState(component, WaterHeater.MODE_CHANNEL_ID, new StringType("eco"));
publishMessage("basement/boiler/mode", "invalid");
assertState(component, WaterHeater.MODE_CHANNEL_ID, new StringType("eco"));
publishMessage("basement/boiler/current_temperature", "120");
assertState(component, WaterHeater.CURRENT_TEMPERATURE_CHANNEL_ID,
new QuantityType<>(120, ImperialUnits.FAHRENHEIT));
publishMessage("basement/boiler/temperature", "125");
assertState(component, WaterHeater.TARGET_TEMPERATURE_CHANNEL_ID,
new QuantityType<>(125, ImperialUnits.FAHRENHEIT));
component.getChannel(WaterHeater.MODE_CHANNEL_ID).getState().publishValue(new StringType("eco"));
assertPublished("basement/boiler/mode/set", "on");
component.getChannel(WaterHeater.MODE_CHANNEL_ID).getState().publishValue(new StringType("off"));
assertPublished("basement/boiler/mode/set", "off");
component.getChannel(WaterHeater.TARGET_TEMPERATURE_CHANNEL_ID).getState().publishValue(new DecimalType(130));
assertPublished("basement/boiler/temperature/set", "130");
}
@SuppressWarnings("null")
@Test
public void testSynthesizedPowerState() {
when(unitProvider.getUnit(Temperature.class)).thenReturn(ImperialUnits.FAHRENHEIT);
var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC), """
{
"platform": "water_heater",
"name": "Boiler",
"modes": [
"off",
"eco",
"performance"
],
"mode_state_topic": "basement/boiler/mode",
"mode_command_topic": "basement/boiler/mode/set",
"temperature_state_topic": "basement/boiler/temperature",
"temperature_command_topic": "basement/boiler/temperature/set",
"current_temperature_topic": "basement/boiler/current_temperature",
"precision": 1.0,
"power_command_topic": "basement/boiler/power/set"
}
""");
assertThat(component.channels.size(), is(4));
assertThat(component.getName(), is("Boiler"));
assertChannel(component, WaterHeater.STATE_CHANNEL_ID, "basement/boiler/mode", "basement/boiler/power/set",
"State", OnOffValue.class);
assertChannel(component, WaterHeater.MODE_CHANNEL_ID, "basement/boiler/mode", "basement/boiler/mode/set",
"Mode", TextValue.class);
assertChannel(component, WaterHeater.CURRENT_TEMPERATURE_CHANNEL_ID, "basement/boiler/current_temperature", "",
"Current Temperature", NumberValue.class);
assertChannel(component, WaterHeater.TARGET_TEMPERATURE_CHANNEL_ID, "basement/boiler/temperature",
"basement/boiler/temperature/set", "Target Temperature", NumberValue.class);
publishMessage("basement/boiler/mode", "eco");
assertState(component, WaterHeater.MODE_CHANNEL_ID, new StringType("eco"));
assertState(component, WaterHeater.STATE_CHANNEL_ID, OnOffType.ON);
publishMessage("basement/boiler/mode", "invalid");
assertState(component, WaterHeater.MODE_CHANNEL_ID, new StringType("eco"));
assertState(component, WaterHeater.STATE_CHANNEL_ID, OnOffType.ON);
publishMessage("basement/boiler/mode", "off");
assertState(component, WaterHeater.MODE_CHANNEL_ID, new StringType("off"));
assertState(component, WaterHeater.STATE_CHANNEL_ID, OnOffType.OFF);
component.getChannel(WaterHeater.MODE_CHANNEL_ID).getState().publishValue(new StringType("eco"));
assertPublished("basement/boiler/mode/set", "eco");
component.getChannel(WaterHeater.MODE_CHANNEL_ID).getState().publishValue(new StringType("off"));
assertPublished("basement/boiler/mode/set", "off");
component.getChannel(WaterHeater.STATE_CHANNEL_ID).getState().publishValue(OnOffType.ON);
assertPublished("basement/boiler/power/set", "ON");
component.getChannel(WaterHeater.STATE_CHANNEL_ID).getState().publishValue(OnOffType.OFF);
assertPublished("basement/boiler/power/set", "OFF");
}
@Override
protected Set<String> getConfigTopics() {
return Set.of(CONFIG_TOPIC);
}
}

View File

@ -38,6 +38,7 @@ import org.openhab.binding.mqtt.homeassistant.internal.component.Climate;
import org.openhab.binding.mqtt.homeassistant.internal.component.Sensor;
import org.openhab.binding.mqtt.homeassistant.internal.component.Switch;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.i18n.UnitProvider;
import org.openhab.core.library.CoreItemFactory;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
@ -72,6 +73,7 @@ public class HomeAssistantThingHandlerTests extends AbstractHomeAssistantTests {
private @Mock @NonNullByDefault({}) ThingHandlerCallback callbackMock;
private @NonNullByDefault({}) HomeAssistantThingHandler thingHandler;
private @NonNullByDefault({}) HomeAssistantThingHandler nonSpyThingHandler;
private @Mock @NonNullByDefault({}) UnitProvider unitProvider;
@BeforeEach
public void setup() {
@ -87,7 +89,7 @@ public class HomeAssistantThingHandlerTests extends AbstractHomeAssistantTests {
protected void setupThingHandler() {
thingHandler = new HomeAssistantThingHandler(haThing, channelTypeProvider, stateDescriptionProvider,
channelTypeRegistry, new Jinjava(), SUBSCRIBE_TIMEOUT, ATTRIBUTE_RECEIVE_TIMEOUT);
channelTypeRegistry, new Jinjava(), unitProvider, SUBSCRIBE_TIMEOUT, ATTRIBUTE_RECEIVE_TIMEOUT);
thingHandler.setConnection(bridgeConnection);
thingHandler.setCallback(callbackMock);
nonSpyThingHandler = thingHandler;
@ -409,7 +411,7 @@ public class HomeAssistantThingHandlerTests extends AbstractHomeAssistantTests {
public void testDuplicateChannelIdNewStyleChannels() {
haThing.setProperty("newStyleChannels", "true");
thingHandler = new HomeAssistantThingHandler(haThing, channelTypeProvider, stateDescriptionProvider,
channelTypeRegistry, new Jinjava(), SUBSCRIBE_TIMEOUT, ATTRIBUTE_RECEIVE_TIMEOUT);
channelTypeRegistry, new Jinjava(), unitProvider, SUBSCRIBE_TIMEOUT, ATTRIBUTE_RECEIVE_TIMEOUT);
thingHandler.setConnection(bridgeConnection);
thingHandler.setCallback(callbackMock);
nonSpyThingHandler = thingHandler;
@ -466,7 +468,7 @@ public class HomeAssistantThingHandlerTests extends AbstractHomeAssistantTests {
public void testDuplicateChannelIdNewStyleChannelsComplex() {
haThing.setProperty("newStyleChannels", "true");
thingHandler = new HomeAssistantThingHandler(haThing, channelTypeProvider, stateDescriptionProvider,
channelTypeRegistry, new Jinjava(), SUBSCRIBE_TIMEOUT, ATTRIBUTE_RECEIVE_TIMEOUT);
channelTypeRegistry, new Jinjava(), unitProvider, SUBSCRIBE_TIMEOUT, ATTRIBUTE_RECEIVE_TIMEOUT);
thingHandler.setConnection(bridgeConnection);
thingHandler.setCallback(callbackMock);
nonSpyThingHandler = thingHandler;

View File

@ -41,6 +41,7 @@ import org.openhab.binding.mqtt.homeassistant.internal.DiscoverComponents.Compon
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.core.i18n.UnitProvider;
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
import org.openhab.core.test.java.JavaOSGiTest;
@ -81,9 +82,10 @@ public class DiscoverComponentsTest extends JavaOSGiTest {
Gson gson = new GsonBuilder().registerTypeAdapterFactory(new ChannelConfigurationTypeAdapterFactory()).create();
Jinjava jinjava = new Jinjava();
UnitProvider unitProvider = mock(UnitProvider.class);
DiscoverComponents discover = spy(new DiscoverComponents(ThingChannelConstants.TEST_HOME_ASSISTANT_THING,
scheduler, channelStateUpdateListener, availabilityTracker, gson, jinjava, true));
scheduler, channelStateUpdateListener, availabilityTracker, gson, jinjava, unitProvider, true));
HandlerConfiguration config = new HandlerConfiguration("homeassistant", List.of("switch/object"));

View File

@ -48,6 +48,7 @@ import org.openhab.binding.mqtt.homeassistant.internal.HaID;
import org.openhab.binding.mqtt.homeassistant.internal.component.AbstractComponent;
import org.openhab.binding.mqtt.homeassistant.internal.component.Switch;
import org.openhab.binding.mqtt.homeassistant.internal.config.ChannelConfigurationTypeAdapterFactory;
import org.openhab.core.i18n.UnitProvider;
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
import org.openhab.core.io.transport.mqtt.MqttConnectionObserver;
import org.openhab.core.io.transport.mqtt.MqttConnectionState;
@ -161,6 +162,7 @@ public class HomeAssistantMQTTImplementationTest extends MqttOSGiTest {
@Test
public void parseHATree() throws Exception {
MqttChannelTypeProvider channelTypeProvider = mock(MqttChannelTypeProvider.class);
UnitProvider unitProvider = mock(UnitProvider.class);
final Map<String, AbstractComponent<?>> haComponents = new HashMap<>();
Gson gson = new GsonBuilder().registerTypeAdapterFactory(new ChannelConfigurationTypeAdapterFactory()).create();
@ -168,7 +170,7 @@ public class HomeAssistantMQTTImplementationTest extends MqttOSGiTest {
ScheduledExecutorService scheduler = new ScheduledThreadPoolExecutor(4);
DiscoverComponents discover = spy(new DiscoverComponents(ThingChannelConstants.TEST_HOME_ASSISTANT_THING,
scheduler, channelStateUpdateListener, availabilityTracker, gson, jinjava, true));
scheduler, channelStateUpdateListener, availabilityTracker, gson, jinjava, unitProvider, true));
// The DiscoverComponents object calls ComponentDiscovered callbacks.
// In the following implementation we add the found component to the `haComponents` map