[mqtt.homeassistant] Implement WaterHeater (#17859)

* [mqtt.homeassistant] Implement WaterHeater

Signed-off-by: Cody Cutrer <cody@cutrer.us>
This commit is contained in:
Cody Cutrer 2024-12-07 08:34:05 -07:00 committed by GitHub
parent f733c85343
commit 5d89c9a885
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
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.generic.MqttChannelTypeProvider;
import org.openhab.binding.mqtt.homeassistant.internal.HomeAssistantJinjaFunctionLibrary; import org.openhab.binding.mqtt.homeassistant.internal.HomeAssistantJinjaFunctionLibrary;
import org.openhab.binding.mqtt.homeassistant.internal.handler.HomeAssistantThingHandler; 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.Thing;
import org.openhab.core.thing.ThingTypeUID; import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory; import org.openhab.core.thing.binding.BaseThingHandlerFactory;
@ -47,6 +48,7 @@ public class MqttThingHandlerFactory extends BaseThingHandlerFactory {
private final MqttChannelStateDescriptionProvider stateDescriptionProvider; private final MqttChannelStateDescriptionProvider stateDescriptionProvider;
private final ChannelTypeRegistry channelTypeRegistry; private final ChannelTypeRegistry channelTypeRegistry;
private final Jinjava jinjava = new Jinjava(); private final Jinjava jinjava = new Jinjava();
private final UnitProvider unitProvider;
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Stream private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Stream
.of(MqttBindingConstants.HOMEASSISTANT_MQTT_THING).collect(Collectors.toSet()); .of(MqttBindingConstants.HOMEASSISTANT_MQTT_THING).collect(Collectors.toSet());
@ -54,10 +56,11 @@ public class MqttThingHandlerFactory extends BaseThingHandlerFactory {
@Activate @Activate
public MqttThingHandlerFactory(final @Reference MqttChannelTypeProvider typeProvider, public MqttThingHandlerFactory(final @Reference MqttChannelTypeProvider typeProvider,
final @Reference MqttChannelStateDescriptionProvider stateDescriptionProvider, final @Reference MqttChannelStateDescriptionProvider stateDescriptionProvider,
final @Reference ChannelTypeRegistry channelTypeRegistry) { final @Reference ChannelTypeRegistry channelTypeRegistry, final @Reference UnitProvider unitProvider) {
this.typeProvider = typeProvider; this.typeProvider = typeProvider;
this.stateDescriptionProvider = stateDescriptionProvider; this.stateDescriptionProvider = stateDescriptionProvider;
this.channelTypeRegistry = channelTypeRegistry; this.channelTypeRegistry = channelTypeRegistry;
this.unitProvider = unitProvider;
HomeAssistantJinjaFunctionLibrary.register(jinjava.getGlobalContext()); HomeAssistantJinjaFunctionLibrary.register(jinjava.getGlobalContext());
} }
@ -78,7 +81,7 @@ public class MqttThingHandlerFactory extends BaseThingHandlerFactory {
if (supportsThingType(thingTypeUID)) { if (supportsThingType(thingTypeUID)) {
return new HomeAssistantThingHandler(thing, typeProvider, stateDescriptionProvider, channelTypeRegistry, return new HomeAssistantThingHandler(thing, typeProvider, stateDescriptionProvider, channelTypeRegistry,
jinjava, 10000, 2000); jinjava, unitProvider, 10000, 2000);
} }
return null; return null;
} }

View File

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

View File

@ -12,6 +12,7 @@
*/ */
package org.openhab.binding.mqtt.homeassistant.internal.component; package org.openhab.binding.mqtt.homeassistant.internal.component;
import java.math.BigDecimal;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -21,6 +22,9 @@ import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.stream.Stream; 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.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.AvailabilityTracker; 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.AvailabilityMode;
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.Device; import org.openhab.binding.mqtt.homeassistant.internal.config.dto.Device;
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection; 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.Channel;
import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.binding.generic.ChannelTransformation; 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 org.openhab.core.types.StateDescription;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.annotations.SerializedName;
import com.hubspot.jinjava.Jinjava; import com.hubspot.jinjava.Jinjava;
/** /**
@ -64,6 +71,29 @@ import com.hubspot.jinjava.Jinjava;
*/ */
@NonNullByDefault @NonNullByDefault
public abstract class AbstractComponent<C extends AbstractChannelConfiguration> { 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"; public static final String JSON_ATTRIBUTES_CHANNEL_ID = "json-attributes";
// Component location fields // Component location fields

View File

@ -17,7 +17,6 @@ import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.function.Predicate; import java.util.function.Predicate;
import javax.measure.Unit;
import javax.measure.quantity.Temperature; import javax.measure.quantity.Temperature;
import org.eclipse.jdt.annotation.NonNullByDefault; 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.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
import org.openhab.core.library.types.StringType; import org.openhab.core.library.types.StringType;
import org.openhab.core.library.unit.ImperialUnits; 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.library.unit.Units;
import org.openhab.core.types.Command; import org.openhab.core.types.Command;
import org.openhab.core.types.State; 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 TEMPERATURE_LOW_CH_ID_DEPRECATED = "temperatureLow";
public static final String POWER_CH_ID = "power"; 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 String ACTION_OFF = "off";
private static final State ACTION_OFF_STATE = new StringType(ACTION_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"); 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") @SerializedName("min_temp")
protected @Nullable BigDecimal minTemp; protected @Nullable BigDecimal minTemp;
@SerializedName("temperature_unit") @SerializedName("temperature_unit")
protected TemperatureUnit temperatureUnit = TemperatureUnit.CELSIUS; // System unit by default protected @Nullable TemperatureUnit temperatureUnit;
@SerializedName("temp_step") @SerializedName("temp_step")
protected BigDecimal tempStep = BigDecimal.ONE; protected BigDecimal tempStep = BigDecimal.ONE;
protected @Nullable BigDecimal precision; protected @Nullable BigDecimal precision;
@ -252,8 +227,16 @@ public class Climate extends AbstractComponent<Climate.ChannelConfiguration> {
public Climate(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) { public Climate(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
super(componentConfiguration, ChannelConfiguration.class, 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 BigDecimal precision = channelConfiguration.precision != null ? channelConfiguration.precision
: channelConfiguration.temperatureUnit.getDefaultPrecision(); : temperatureUnit.getDefaultPrecision();
final ChannelStateUpdateListener updateListener = componentConfiguration.getUpdateListener(); final ChannelStateUpdateListener updateListener = componentConfiguration.getUpdateListener();
ComponentChannel actionChannel = buildOptionalChannel(ACTION_CH_ID, ComponentChannelType.STRING, 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); null, channelConfiguration.currentHumidityTemplate, channelConfiguration.currentHumidityTopic, null);
buildOptionalChannel(newStyleChannels ? CURRENT_TEMPERATURE_CH_ID : CURRENT_TEMPERATURE_CH_ID_DEPRECATED, buildOptionalChannel(newStyleChannels ? CURRENT_TEMPERATURE_CH_ID : CURRENT_TEMPERATURE_CH_ID_DEPRECATED,
ComponentChannelType.NUMBER, ComponentChannelType.TEMPERATURE, new NumberValue(null, null, precision, temperatureUnit.getUnit()),
new NumberValue(null, null, precision, channelConfiguration.temperatureUnit.getUnit()), updateListener, updateListener, null, null, channelConfiguration.currentTemperatureTemplate,
null, null, channelConfiguration.currentTemperatureTemplate,
channelConfiguration.currentTemperatureTopic, commandFilter); channelConfiguration.currentTemperatureTopic, commandFilter);
buildOptionalChannel(newStyleChannels ? FAN_MODE_CH_ID : FAN_MODE_CH_ID_DEPRECATED, ComponentChannelType.STRING, 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.targetHumidityCommandTopic, channelConfiguration.targetHumidityStateTemplate,
channelConfiguration.targetHumidityStateTopic, commandFilter); channelConfiguration.targetHumidityStateTopic, commandFilter);
buildOptionalChannel(TEMPERATURE_CH_ID, ComponentChannelType.NUMBER, buildOptionalChannel(TEMPERATURE_CH_ID, ComponentChannelType.TEMPERATURE,
new NumberValue(channelConfiguration.minTemp, channelConfiguration.maxTemp, new NumberValue(channelConfiguration.minTemp, channelConfiguration.maxTemp,
channelConfiguration.tempStep, channelConfiguration.temperatureUnit.getUnit()), channelConfiguration.tempStep, temperatureUnit.getUnit()),
updateListener, channelConfiguration.temperatureCommandTemplate, updateListener, channelConfiguration.temperatureCommandTemplate,
channelConfiguration.temperatureCommandTopic, channelConfiguration.temperatureStateTemplate, channelConfiguration.temperatureCommandTopic, channelConfiguration.temperatureStateTemplate,
channelConfiguration.temperatureStateTopic, commandFilter); channelConfiguration.temperatureStateTopic, commandFilter);
buildOptionalChannel(newStyleChannels ? TEMPERATURE_HIGH_CH_ID : TEMPERATURE_HIGH_CH_ID_DEPRECATED, buildOptionalChannel(newStyleChannels ? TEMPERATURE_HIGH_CH_ID : TEMPERATURE_HIGH_CH_ID_DEPRECATED,
ComponentChannelType.NUMBER, ComponentChannelType.TEMPERATURE,
new NumberValue(channelConfiguration.minTemp, channelConfiguration.maxTemp, new NumberValue(channelConfiguration.minTemp, channelConfiguration.maxTemp,
channelConfiguration.tempStep, channelConfiguration.temperatureUnit.getUnit()), channelConfiguration.tempStep, temperatureUnit.getUnit()),
updateListener, channelConfiguration.temperatureHighCommandTemplate, updateListener, channelConfiguration.temperatureHighCommandTemplate,
channelConfiguration.temperatureHighCommandTopic, channelConfiguration.temperatureHighStateTemplate, channelConfiguration.temperatureHighCommandTopic, channelConfiguration.temperatureHighStateTemplate,
channelConfiguration.temperatureHighStateTopic, commandFilter); channelConfiguration.temperatureHighStateTopic, commandFilter);
buildOptionalChannel(newStyleChannels ? TEMPERATURE_LOW_CH_ID : TEMPERATURE_LOW_CH_ID_DEPRECATED, buildOptionalChannel(newStyleChannels ? TEMPERATURE_LOW_CH_ID : TEMPERATURE_LOW_CH_ID_DEPRECATED,
ComponentChannelType.NUMBER, ComponentChannelType.TEMPERATURE,
new NumberValue(channelConfiguration.minTemp, channelConfiguration.maxTemp, new NumberValue(channelConfiguration.minTemp, channelConfiguration.maxTemp,
channelConfiguration.tempStep, channelConfiguration.temperatureUnit.getUnit()), channelConfiguration.tempStep, temperatureUnit.getUnit()),
updateListener, channelConfiguration.temperatureLowCommandTemplate, updateListener, channelConfiguration.temperatureLowCommandTemplate,
channelConfiguration.temperatureLowCommandTopic, channelConfiguration.temperatureLowStateTemplate, channelConfiguration.temperatureLowCommandTopic, channelConfiguration.temperatureLowStateTemplate,
channelConfiguration.temperatureLowStateTopic, commandFilter); 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.config.dto.AbstractChannelConfiguration;
import org.openhab.binding.mqtt.homeassistant.internal.exception.ConfigurationException; import org.openhab.binding.mqtt.homeassistant.internal.exception.ConfigurationException;
import org.openhab.binding.mqtt.homeassistant.internal.exception.UnsupportedComponentException; import org.openhab.binding.mqtt.homeassistant.internal.exception.UnsupportedComponentException;
import org.openhab.core.i18n.UnitProvider;
import org.openhab.core.thing.ThingUID; import org.openhab.core.thing.ThingUID;
import com.google.gson.Gson; import com.google.gson.Gson;
@ -47,9 +48,10 @@ public class ComponentFactory {
*/ */
public static AbstractComponent<?> createComponent(ThingUID thingUID, HaID haID, String channelConfigurationJSON, public static AbstractComponent<?> createComponent(ThingUID thingUID, HaID haID, String channelConfigurationJSON,
ChannelStateUpdateListener updateListener, AvailabilityTracker tracker, ScheduledExecutorService scheduler, 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, ComponentConfiguration componentConfiguration = new ComponentConfiguration(thingUID, haID,
channelConfigurationJSON, gson, jinjava, updateListener, tracker, scheduler); channelConfigurationJSON, gson, jinjava, updateListener, tracker, scheduler, unitProvider);
switch (haID.component) { switch (haID.component) {
case "alarm_control_panel": case "alarm_control_panel":
return new AlarmControlPanel(componentConfiguration, newStyleChannels); return new AlarmControlPanel(componentConfiguration, newStyleChannels);
@ -97,6 +99,8 @@ public class ComponentFactory {
return new Vacuum(componentConfiguration, newStyleChannels); return new Vacuum(componentConfiguration, newStyleChannels);
case "valve": case "valve":
return new Valve(componentConfiguration, newStyleChannels); return new Valve(componentConfiguration, newStyleChannels);
case "water_heater":
return new WaterHeater(componentConfiguration, newStyleChannels);
default: default:
throw new UnsupportedComponentException("Component '" + haID + "' is unsupported!"); throw new UnsupportedComponentException("Component '" + haID + "' is unsupported!");
} }
@ -111,6 +115,7 @@ public class ComponentFactory {
private final Gson gson; private final Gson gson;
private final Jinjava jinjava; private final Jinjava jinjava;
private final ScheduledExecutorService scheduler; private final ScheduledExecutorService scheduler;
private final UnitProvider unitProvider;
/** /**
* Provide a thingUID and HomeAssistant topic ID to determine the channel group UID and type. * 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, protected ComponentConfiguration(ThingUID thingUID, HaID haID, String configJSON, Gson gson, Jinjava jinjava,
ChannelStateUpdateListener updateListener, AvailabilityTracker tracker, ChannelStateUpdateListener updateListener, AvailabilityTracker tracker,
ScheduledExecutorService scheduler) { ScheduledExecutorService scheduler, UnitProvider unitProvider) {
this.thingUID = thingUID; this.thingUID = thingUID;
this.haID = haID; this.haID = haID;
this.configJSON = configJSON; this.configJSON = configJSON;
@ -131,6 +136,7 @@ public class ComponentFactory {
this.updateListener = updateListener; this.updateListener = updateListener;
this.tracker = tracker; this.tracker = tracker;
this.scheduler = scheduler; this.scheduler = scheduler;
this.unitProvider = unitProvider;
} }
public ThingUID getThingUID() { public ThingUID getThingUID() {
@ -157,6 +163,10 @@ public class ComponentFactory {
return jinjava; return jinjava;
} }
public UnitProvider getUnitProvider() {
return unitProvider;
}
public AvailabilityTracker getTracker() { public AvailabilityTracker getTracker() {
return tracker; 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.binding.mqtt.homeassistant.internal.exception.ConfigurationException;
import org.openhab.core.config.core.Configuration; import org.openhab.core.config.core.Configuration;
import org.openhab.core.config.core.validation.ConfigValidationException; 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.io.transport.mqtt.MqttBrokerConnection;
import org.openhab.core.thing.Channel; import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.ChannelUID;
@ -95,6 +96,7 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
protected final MqttChannelStateDescriptionProvider stateDescriptionProvider; protected final MqttChannelStateDescriptionProvider stateDescriptionProvider;
protected final ChannelTypeRegistry channelTypeRegistry; protected final ChannelTypeRegistry channelTypeRegistry;
protected final Jinjava jinjava; protected final Jinjava jinjava;
protected final UnitProvider unitProvider;
public final int attributeReceiveTimeout; public final int attributeReceiveTimeout;
protected final DelayedBatchProcessing<Object> delayedProcessing; protected final DelayedBatchProcessing<Object> delayedProcessing;
protected final DiscoverComponents discoverComponents; protected final DiscoverComponents discoverComponents;
@ -123,20 +125,21 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
*/ */
public HomeAssistantThingHandler(Thing thing, MqttChannelTypeProvider channelTypeProvider, public HomeAssistantThingHandler(Thing thing, MqttChannelTypeProvider channelTypeProvider,
MqttChannelStateDescriptionProvider stateDescriptionProvider, ChannelTypeRegistry channelTypeRegistry, MqttChannelStateDescriptionProvider stateDescriptionProvider, ChannelTypeRegistry channelTypeRegistry,
Jinjava jinjava, int subscribeTimeout, int attributeReceiveTimeout) { Jinjava jinjava, UnitProvider unitProvider, int subscribeTimeout, int attributeReceiveTimeout) {
super(thing, subscribeTimeout); super(thing, subscribeTimeout);
this.gson = new GsonBuilder().registerTypeAdapterFactory(new ChannelConfigurationTypeAdapterFactory()).create(); this.gson = new GsonBuilder().registerTypeAdapterFactory(new ChannelConfigurationTypeAdapterFactory()).create();
this.channelTypeProvider = channelTypeProvider; this.channelTypeProvider = channelTypeProvider;
this.stateDescriptionProvider = stateDescriptionProvider; this.stateDescriptionProvider = stateDescriptionProvider;
this.channelTypeRegistry = channelTypeRegistry; this.channelTypeRegistry = channelTypeRegistry;
this.jinjava = jinjava; this.jinjava = jinjava;
this.unitProvider = unitProvider;
this.attributeReceiveTimeout = attributeReceiveTimeout; this.attributeReceiveTimeout = attributeReceiveTimeout;
this.delayedProcessing = new DelayedBatchProcessing<>(attributeReceiveTimeout, this, scheduler); this.delayedProcessing = new DelayedBatchProcessing<>(attributeReceiveTimeout, this, scheduler);
newStyleChannels = "true".equals(thing.getProperties().get("newStyleChannels")); newStyleChannels = "true".equals(thing.getProperties().get("newStyleChannels"));
this.discoverComponents = new DiscoverComponents(thing.getUID(), scheduler, this, this, gson, jinjava, this.discoverComponents = new DiscoverComponents(thing.getUID(), scheduler, this, this, gson, jinjava,
newStyleChannels); unitProvider, newStyleChannels);
} }
@Override @Override
@ -184,7 +187,8 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
String channelConfigurationJSON = (String) channelConfig.get("config"); String channelConfigurationJSON = (String) channelConfig.get("config");
try { try {
AbstractComponent<?> component = ComponentFactory.createComponent(thingUID, haID, 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)) { if (typeID.equals(MqttBindingConstants.HOMEASSISTANT_MQTT_THING)) {
typeID = calculateThingTypeUID(component); 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-string.label = String
channel-type.mqtt.ha-switch-advanced.label = Switch channel-type.mqtt.ha-switch-advanced.label = Switch
channel-type.mqtt.ha-switch.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-advanced.label = Trigger
channel-type.mqtt.ha-trigger.label = Trigger channel-type.mqtt.ha-trigger.label = Trigger

View File

@ -66,6 +66,12 @@
<config-description-ref uri="channel-type:mqtt:ha-channel"/> <config-description-ref uri="channel-type:mqtt:ha-channel"/>
</channel-type> </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"> <channel-type id="ha-trigger">
<kind>trigger</kind> <kind>trigger</kind>
<label>Trigger</label> <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.generic.MqttChannelTypeProvider;
import org.openhab.binding.mqtt.homeassistant.generic.internal.MqttThingHandlerFactory; import org.openhab.binding.mqtt.homeassistant.generic.internal.MqttThingHandlerFactory;
import org.openhab.binding.mqtt.homeassistant.internal.component.AbstractComponent; 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.test.storage.VolatileStorageService;
import org.openhab.core.thing.type.ChannelTypeRegistry; import org.openhab.core.thing.type.ChannelTypeRegistry;
import org.openhab.core.thing.type.ThingTypeRegistry; import org.openhab.core.thing.type.ThingTypeRegistry;
@ -44,6 +45,7 @@ import org.openhab.core.thing.type.ThingTypeRegistry;
@NonNullByDefault @NonNullByDefault
public class HomeAssistantChannelTransformationTests { public class HomeAssistantChannelTransformationTests {
protected @Mock @NonNullByDefault({}) ThingTypeRegistry thingTypeRegistry; protected @Mock @NonNullByDefault({}) ThingTypeRegistry thingTypeRegistry;
protected @Mock @NonNullByDefault({}) UnitProvider unitProvider;
protected @NonNullByDefault({}) HomeAssistantChannelTransformation transformation; protected @NonNullByDefault({}) HomeAssistantChannelTransformation transformation;
@ -54,7 +56,7 @@ public class HomeAssistantChannelTransformationTests {
MqttChannelStateDescriptionProvider stateDescriptionProvider = new MqttChannelStateDescriptionProvider(); MqttChannelStateDescriptionProvider stateDescriptionProvider = new MqttChannelStateDescriptionProvider();
ChannelTypeRegistry channelTypeRegistry = new ChannelTypeRegistry(); ChannelTypeRegistry channelTypeRegistry = new ChannelTypeRegistry();
MqttThingHandlerFactory thingHandlerFactory = new MqttThingHandlerFactory(channelTypeProvider, MqttThingHandlerFactory thingHandlerFactory = new MqttThingHandlerFactory(channelTypeProvider,
stateDescriptionProvider, channelTypeRegistry); stateDescriptionProvider, channelTypeRegistry, unitProvider);
AbstractComponent component = Mockito.mock(AbstractComponent.class); AbstractComponent component = Mockito.mock(AbstractComponent.class);
HaID haID = new HaID("homeassistant/light/pool/light/config"); 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.HandlerConfiguration;
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration; import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
import org.openhab.binding.mqtt.homeassistant.internal.handler.HomeAssistantThingHandler; 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.library.types.HSBType;
import org.openhab.core.thing.Thing; import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatusInfo; import org.openhab.core.thing.ThingStatusInfo;
@ -64,6 +65,7 @@ public abstract class AbstractComponentTests extends AbstractHomeAssistantTests
private @Mock @NonNullByDefault({}) ThingHandlerCallback callbackMock; private @Mock @NonNullByDefault({}) ThingHandlerCallback callbackMock;
private @NonNullByDefault({}) LatchThingHandler thingHandler; private @NonNullByDefault({}) LatchThingHandler thingHandler;
protected @Mock @NonNullByDefault({}) UnitProvider unitProvider;
@BeforeEach @BeforeEach
public void setupThingHandler() { public void setupThingHandler() {
@ -84,7 +86,7 @@ public abstract class AbstractComponentTests extends AbstractHomeAssistantTests
haThing.setProperty("newStyleChannels", "true"); haThing.setProperty("newStyleChannels", "true");
} }
thingHandler = new LatchThingHandler(haThing, channelTypeProvider, stateDescriptionProvider, thingHandler = new LatchThingHandler(haThing, channelTypeProvider, stateDescriptionProvider,
channelTypeRegistry, SUBSCRIBE_TIMEOUT, ATTRIBUTE_RECEIVE_TIMEOUT); channelTypeRegistry, unitProvider, SUBSCRIBE_TIMEOUT, ATTRIBUTE_RECEIVE_TIMEOUT);
thingHandler.setConnection(bridgeConnection); thingHandler.setConnection(bridgeConnection);
thingHandler.setCallback(callbackMock); thingHandler.setCallback(callbackMock);
thingHandler = spy(thingHandler); thingHandler = spy(thingHandler);
@ -341,9 +343,9 @@ public abstract class AbstractComponentTests extends AbstractHomeAssistantTests
public LatchThingHandler(Thing thing, MqttChannelTypeProvider channelTypeProvider, public LatchThingHandler(Thing thing, MqttChannelTypeProvider channelTypeProvider,
MqttChannelStateDescriptionProvider stateDescriptionProvider, ChannelTypeRegistry channelTypeRegistry, MqttChannelStateDescriptionProvider stateDescriptionProvider, ChannelTypeRegistry channelTypeRegistry,
int subscribeTimeout, int attributeReceiveTimeout) { UnitProvider unitProvider, int subscribeTimeout, int attributeReceiveTimeout) {
super(thing, channelTypeProvider, stateDescriptionProvider, channelTypeRegistry, new Jinjava(), super(thing, channelTypeProvider, stateDescriptionProvider, channelTypeRegistry, new Jinjava(),
subscribeTimeout, attributeReceiveTimeout); unitProvider, subscribeTimeout, attributeReceiveTimeout);
} }
@Override @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.Sensor;
import org.openhab.binding.mqtt.homeassistant.internal.component.Switch; import org.openhab.binding.mqtt.homeassistant.internal.component.Switch;
import org.openhab.core.config.core.Configuration; import org.openhab.core.config.core.Configuration;
import org.openhab.core.i18n.UnitProvider;
import org.openhab.core.library.CoreItemFactory; import org.openhab.core.library.CoreItemFactory;
import org.openhab.core.thing.Channel; import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.ChannelUID;
@ -72,6 +73,7 @@ public class HomeAssistantThingHandlerTests extends AbstractHomeAssistantTests {
private @Mock @NonNullByDefault({}) ThingHandlerCallback callbackMock; private @Mock @NonNullByDefault({}) ThingHandlerCallback callbackMock;
private @NonNullByDefault({}) HomeAssistantThingHandler thingHandler; private @NonNullByDefault({}) HomeAssistantThingHandler thingHandler;
private @NonNullByDefault({}) HomeAssistantThingHandler nonSpyThingHandler; private @NonNullByDefault({}) HomeAssistantThingHandler nonSpyThingHandler;
private @Mock @NonNullByDefault({}) UnitProvider unitProvider;
@BeforeEach @BeforeEach
public void setup() { public void setup() {
@ -87,7 +89,7 @@ public class HomeAssistantThingHandlerTests extends AbstractHomeAssistantTests {
protected void setupThingHandler() { protected void setupThingHandler() {
thingHandler = new HomeAssistantThingHandler(haThing, channelTypeProvider, stateDescriptionProvider, 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.setConnection(bridgeConnection);
thingHandler.setCallback(callbackMock); thingHandler.setCallback(callbackMock);
nonSpyThingHandler = thingHandler; nonSpyThingHandler = thingHandler;
@ -409,7 +411,7 @@ public class HomeAssistantThingHandlerTests extends AbstractHomeAssistantTests {
public void testDuplicateChannelIdNewStyleChannels() { public void testDuplicateChannelIdNewStyleChannels() {
haThing.setProperty("newStyleChannels", "true"); haThing.setProperty("newStyleChannels", "true");
thingHandler = new HomeAssistantThingHandler(haThing, channelTypeProvider, stateDescriptionProvider, 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.setConnection(bridgeConnection);
thingHandler.setCallback(callbackMock); thingHandler.setCallback(callbackMock);
nonSpyThingHandler = thingHandler; nonSpyThingHandler = thingHandler;
@ -466,7 +468,7 @@ public class HomeAssistantThingHandlerTests extends AbstractHomeAssistantTests {
public void testDuplicateChannelIdNewStyleChannelsComplex() { public void testDuplicateChannelIdNewStyleChannelsComplex() {
haThing.setProperty("newStyleChannels", "true"); haThing.setProperty("newStyleChannels", "true");
thingHandler = new HomeAssistantThingHandler(haThing, channelTypeProvider, stateDescriptionProvider, 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.setConnection(bridgeConnection);
thingHandler.setCallback(callbackMock); thingHandler.setCallback(callbackMock);
nonSpyThingHandler = thingHandler; 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.HaID;
import org.openhab.binding.mqtt.homeassistant.internal.HandlerConfiguration; 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.ChannelConfigurationTypeAdapterFactory;
import org.openhab.core.i18n.UnitProvider;
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection; import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
import org.openhab.core.test.java.JavaOSGiTest; import org.openhab.core.test.java.JavaOSGiTest;
@ -81,9 +82,10 @@ public class DiscoverComponentsTest extends JavaOSGiTest {
Gson gson = new GsonBuilder().registerTypeAdapterFactory(new ChannelConfigurationTypeAdapterFactory()).create(); Gson gson = new GsonBuilder().registerTypeAdapterFactory(new ChannelConfigurationTypeAdapterFactory()).create();
Jinjava jinjava = new Jinjava(); Jinjava jinjava = new Jinjava();
UnitProvider unitProvider = mock(UnitProvider.class);
DiscoverComponents discover = spy(new DiscoverComponents(ThingChannelConstants.TEST_HOME_ASSISTANT_THING, 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")); 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.AbstractComponent;
import org.openhab.binding.mqtt.homeassistant.internal.component.Switch; import org.openhab.binding.mqtt.homeassistant.internal.component.Switch;
import org.openhab.binding.mqtt.homeassistant.internal.config.ChannelConfigurationTypeAdapterFactory; 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.MqttBrokerConnection;
import org.openhab.core.io.transport.mqtt.MqttConnectionObserver; import org.openhab.core.io.transport.mqtt.MqttConnectionObserver;
import org.openhab.core.io.transport.mqtt.MqttConnectionState; import org.openhab.core.io.transport.mqtt.MqttConnectionState;
@ -161,6 +162,7 @@ public class HomeAssistantMQTTImplementationTest extends MqttOSGiTest {
@Test @Test
public void parseHATree() throws Exception { public void parseHATree() throws Exception {
MqttChannelTypeProvider channelTypeProvider = mock(MqttChannelTypeProvider.class); MqttChannelTypeProvider channelTypeProvider = mock(MqttChannelTypeProvider.class);
UnitProvider unitProvider = mock(UnitProvider.class);
final Map<String, AbstractComponent<?>> haComponents = new HashMap<>(); final Map<String, AbstractComponent<?>> haComponents = new HashMap<>();
Gson gson = new GsonBuilder().registerTypeAdapterFactory(new ChannelConfigurationTypeAdapterFactory()).create(); Gson gson = new GsonBuilder().registerTypeAdapterFactory(new ChannelConfigurationTypeAdapterFactory()).create();
@ -168,7 +170,7 @@ public class HomeAssistantMQTTImplementationTest extends MqttOSGiTest {
ScheduledExecutorService scheduler = new ScheduledThreadPoolExecutor(4); ScheduledExecutorService scheduler = new ScheduledThreadPoolExecutor(4);
DiscoverComponents discover = spy(new DiscoverComponents(ThingChannelConstants.TEST_HOME_ASSISTANT_THING, 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. // The DiscoverComponents object calls ComponentDiscovered callbacks.
// In the following implementation we add the found component to the `haComponents` map // In the following implementation we add the found component to the `haComponents` map