From 5d89c9a885b29beaa8da4bc270d81121b4b41eb0 Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Sat, 7 Dec 2024 08:34:05 -0700 Subject: [PATCH] [mqtt.homeassistant] Implement WaterHeater (#17859) * [mqtt.homeassistant] Implement WaterHeater Signed-off-by: Cody Cutrer --- .../internal/MqttThingHandlerFactory.java | 7 +- .../internal/ComponentChannelType.java | 3 +- .../internal/DiscoverComponents.java | 9 +- .../internal/component/AbstractComponent.java | 30 +++ .../internal/component/Climate.java | 54 ++--- .../internal/component/ComponentFactory.java | 16 +- .../internal/component/WaterHeater.java | 190 ++++++++++++++++++ .../handler/HomeAssistantThingHandler.java | 10 +- .../resources/OH-INF/i18n/mqtt.properties | 1 + .../OH-INF/thing/homeassistant-channels.xml | 6 + ...meAssistantChannelTransformationTests.java | 4 +- .../component/AbstractComponentTests.java | 8 +- .../internal/component/WaterHeaterTests.java | 159 +++++++++++++++ .../HomeAssistantThingHandlerTests.java | 8 +- .../homeassistant/DiscoverComponentsTest.java | 4 +- .../HomeAssistantMQTTImplementationTest.java | 4 +- 16 files changed, 455 insertions(+), 58 deletions(-) create mode 100644 bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/WaterHeater.java create mode 100644 bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/WaterHeaterTests.java diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/generic/internal/MqttThingHandlerFactory.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/generic/internal/MqttThingHandlerFactory.java index 5b2c8e9fdc9..ce613e68861 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/generic/internal/MqttThingHandlerFactory.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/generic/internal/MqttThingHandlerFactory.java @@ -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 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; } diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentChannelType.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentChannelType.java index 38cad582330..2d410fe43bb 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentChannelType.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentChannelType.java @@ -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; diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/DiscoverComponents.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/DiscoverComponents.java index b88b596489a..3046847e947 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/DiscoverComponents.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/DiscoverComponents.java @@ -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) { diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/AbstractComponent.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/AbstractComponent.java index fb71bb59914..80fbb958a7d 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/AbstractComponent.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/AbstractComponent.java @@ -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 { + public enum TemperatureUnit { + @SerializedName("C") + CELSIUS(SIUnits.CELSIUS, new BigDecimal("0.1")), + @SerializedName("F") + FAHRENHEIT(ImperialUnits.FAHRENHEIT, BigDecimal.ONE); + + private final Unit unit; + private final BigDecimal defaultPrecision; + + TemperatureUnit(Unit unit, BigDecimal defaultPrecision) { + this.unit = unit; + this.defaultPrecision = defaultPrecision; + } + + public Unit getUnit() { + return unit; + } + + public BigDecimal getDefaultPrecision() { + return defaultPrecision; + } + } + public static final String JSON_ATTRIBUTES_CHANNEL_ID = "json-attributes"; // Component location fields diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Climate.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Climate.java index ec92baf08cd..6396b1e44b0 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Climate.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Climate.java @@ -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 { 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 unit; - private final BigDecimal defaultPrecision; - - TemperatureUnit(Unit unit, BigDecimal defaultPrecision) { - this.unit = unit; - this.defaultPrecision = defaultPrecision; - } - - public Unit 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 ACTION_MODES = List.of(ACTION_OFF, "heating", "cooling", "drying", "idle", "fan"); @@ -241,7 +216,7 @@ public class Climate extends AbstractComponent { @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 { 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 { 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 { 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); diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/ComponentFactory.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/ComponentFactory.java index c01fa30f815..a6d4e819487 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/ComponentFactory.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/ComponentFactory.java @@ -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; } diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/WaterHeater.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/WaterHeater.java new file mode 100644 index 00000000000..3e79c5761aa --- /dev/null +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/WaterHeater.java @@ -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 { + 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 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 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 onStates = new ArrayList<>(channelConfiguration.modes); + onStates.remove(MODE_OFF); + + List 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(); + } +} diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/handler/HomeAssistantThingHandler.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/handler/HomeAssistantThingHandler.java index 739c834d7ee..23216418496 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/handler/HomeAssistantThingHandler.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/handler/HomeAssistantThingHandler.java @@ -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 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); } diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/resources/OH-INF/i18n/mqtt.properties b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/resources/OH-INF/i18n/mqtt.properties index 774897379bd..659b0d8bc6e 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/resources/OH-INF/i18n/mqtt.properties +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/resources/OH-INF/i18n/mqtt.properties @@ -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 diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/resources/OH-INF/thing/homeassistant-channels.xml b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/resources/OH-INF/thing/homeassistant-channels.xml index 0a5bd650f7c..a949792f501 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/resources/OH-INF/thing/homeassistant-channels.xml +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/resources/OH-INF/thing/homeassistant-channels.xml @@ -66,6 +66,12 @@ + + Number:Temperature + + + + trigger diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/HomeAssistantChannelTransformationTests.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/HomeAssistantChannelTransformationTests.java index 7ef078cc534..f8341fbb1c6 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/HomeAssistantChannelTransformationTests.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/HomeAssistantChannelTransformationTests.java @@ -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"); diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/AbstractComponentTests.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/AbstractComponentTests.java index cb9c4fe5a30..febda9cf5b8 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/AbstractComponentTests.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/AbstractComponentTests.java @@ -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 diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/WaterHeaterTests.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/WaterHeaterTests.java new file mode 100644 index 00000000000..9cb1c9481e3 --- /dev/null +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/WaterHeaterTests.java @@ -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 getConfigTopics() { + return Set.of(CONFIG_TOPIC); + } +} diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/handler/HomeAssistantThingHandlerTests.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/handler/HomeAssistantThingHandlerTests.java index 3f4c066239a..964bc638796 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/handler/HomeAssistantThingHandlerTests.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/handler/HomeAssistantThingHandlerTests.java @@ -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; diff --git a/itests/org.openhab.binding.mqtt.homeassistant.tests/src/main/java/org/openhab/binding/mqtt/homeassistant/DiscoverComponentsTest.java b/itests/org.openhab.binding.mqtt.homeassistant.tests/src/main/java/org/openhab/binding/mqtt/homeassistant/DiscoverComponentsTest.java index 903c94a00d7..be1d2c73cb3 100644 --- a/itests/org.openhab.binding.mqtt.homeassistant.tests/src/main/java/org/openhab/binding/mqtt/homeassistant/DiscoverComponentsTest.java +++ b/itests/org.openhab.binding.mqtt.homeassistant.tests/src/main/java/org/openhab/binding/mqtt/homeassistant/DiscoverComponentsTest.java @@ -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")); diff --git a/itests/org.openhab.binding.mqtt.homeassistant.tests/src/main/java/org/openhab/binding/mqtt/homeassistant/HomeAssistantMQTTImplementationTest.java b/itests/org.openhab.binding.mqtt.homeassistant.tests/src/main/java/org/openhab/binding/mqtt/homeassistant/HomeAssistantMQTTImplementationTest.java index d001274eb60..6e73c11675e 100644 --- a/itests/org.openhab.binding.mqtt.homeassistant.tests/src/main/java/org/openhab/binding/mqtt/homeassistant/HomeAssistantMQTTImplementationTest.java +++ b/itests/org.openhab.binding.mqtt.homeassistant.tests/src/main/java/org/openhab/binding/mqtt/homeassistant/HomeAssistantMQTTImplementationTest.java @@ -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> 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