diff --git a/bundles/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/MqttChannelStateDescriptionProvider.java b/bundles/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/MqttChannelStateDescriptionProvider.java index 509cff2e747..7392420a7dc 100644 --- a/bundles/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/MqttChannelStateDescriptionProvider.java +++ b/bundles/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/MqttChannelStateDescriptionProvider.java @@ -15,18 +15,29 @@ package org.openhab.binding.mqtt.generic; import java.util.Locale; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.mqtt.generic.internal.MqttThingHandlerFactory; import org.openhab.binding.mqtt.generic.internal.handler.GenericMQTTThingHandler; +import org.openhab.core.i18n.I18nUtil; +import org.openhab.core.i18n.TranslationProvider; import org.openhab.core.thing.Channel; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.type.DynamicCommandDescriptionProvider; import org.openhab.core.thing.type.DynamicStateDescriptionProvider; import org.openhab.core.types.CommandDescription; +import org.openhab.core.types.CommandDescriptionBuilder; +import org.openhab.core.types.CommandOption; import org.openhab.core.types.StateDescription; +import org.openhab.core.types.StateDescriptionFragmentBuilder; +import org.openhab.core.types.StateOption; +import org.openhab.core.util.BundleResolver; +import org.osgi.framework.Bundle; +import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -49,6 +60,16 @@ public class MqttChannelStateDescriptionProvider private final Map commandDescriptions = new ConcurrentHashMap<>(); private final Logger logger = LoggerFactory.getLogger(MqttChannelStateDescriptionProvider.class); + private final TranslationProvider i18nProvider; + private final BundleResolver bundleResolver; + + @Activate + public MqttChannelStateDescriptionProvider(@Reference TranslationProvider i18nProvider, + @Reference BundleResolver bundleResolver) { + this.i18nProvider = i18nProvider; + this.bundleResolver = bundleResolver; + } + /** * Set a state description for a channel. This description will be used when preparing the channel state by * the framework for presentation. A previous description, if existed, will be replaced. @@ -88,6 +109,13 @@ public class MqttChannelStateDescriptionProvider StateDescription description = stateDescriptions.get(channel.getUID()); if (description != null) { logger.trace("Providing state description for channel {}", channel.getUID()); + if (description.getOptions().stream().anyMatch(option -> I18nUtil.isConstant(option.getLabel()))) { + StateDescriptionFragmentBuilder builder = StateDescriptionFragmentBuilder.create(description); + builder.withOptions(description.getOptions().stream().map(option -> { + return new StateOption(option.getValue(), translateLabel(option.getLabel(), locale)); + }).collect(Collectors.toList())); + description = builder.build().toStateDescription(); + } } return description; } @@ -96,7 +124,16 @@ public class MqttChannelStateDescriptionProvider public @Nullable CommandDescription getCommandDescription(Channel channel, @Nullable CommandDescription originalCommandDescription, @Nullable Locale locale) { CommandDescription description = commandDescriptions.get(channel.getUID()); - logger.trace("Providing command description for channel {}", channel.getUID()); + if (description != null) { + logger.trace("Providing command description for channel {}", channel.getUID()); + if (description.getCommandOptions().stream().anyMatch(option -> I18nUtil.isConstant(option.getLabel()))) { + CommandDescriptionBuilder builder = CommandDescriptionBuilder.create(); + builder.withCommandOptions(description.getCommandOptions().stream().map(option -> { + return new CommandOption(option.getCommand(), translateLabel(option.getLabel(), locale)); + }).collect(Collectors.toList())); + description = builder.build(); + } + } return description; } @@ -109,4 +146,20 @@ public class MqttChannelStateDescriptionProvider stateDescriptions.remove(channel); commandDescriptions.remove(channel); } + + private @Nullable String translateLabel(@Nullable String label, @Nullable Locale locale) { + if (label == null) { + return null; + } + if (!I18nUtil.isConstant(label)) { + return label; + } + Bundle bundle = bundleResolver.resolveBundle(getClass()); + + String translatedLabel = i18nProvider.getText(bundle, I18nUtil.stripConstant(label), null, locale); + if (translatedLabel != null) { + return translatedLabel; + } + return label; + } } diff --git a/bundles/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/values/TextValue.java b/bundles/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/values/TextValue.java index 1c321b49c44..b42b0c8d6d2 100644 --- a/bundles/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/values/TextValue.java +++ b/bundles/org.openhab.binding.mqtt.generic/src/main/java/org/openhab/binding/mqtt/generic/values/TextValue.java @@ -41,9 +41,49 @@ import org.openhab.core.types.UnDefType; public class TextValue extends Value { private final @Nullable Map states; private final @Nullable Map commands; + private final @Nullable Map stateLabels; + private final @Nullable Map commandLabels; protected @Nullable String nullValue = null; + /** + * Create a string value with a limited number of allowed states and commands. + * + * @param states Allowed states. The key is the value that is received from MQTT, + * and the value is how matching values will be presented in openHAB. + * @param commands Allowed commands. The key is the value that will be received by + * openHAB, and the value is how matching commands will be sent to MQTT. + * @param stateLabels Labels for the states in the StateDescription. If a state is not found in this map, the state + * itself is used as label. + * Keys are the openHAB state, not the MQTT state. + * @param commandLabels Labels for the commands in the CommandDescription. If a command is not found in this map, + * the command itself is used as label. + */ + public TextValue(Map states, Map commands, Map stateLabels, + Map commandLabels) { + super(CoreItemFactory.STRING, List.of(StringType.class)); + if (!states.isEmpty()) { + this.states = new LinkedHashMap(states); + } else { + this.states = null; + } + if (!commands.isEmpty()) { + this.commands = new LinkedHashMap(commands); + } else { + this.commands = null; + } + if (!stateLabels.isEmpty()) { + this.stateLabels = Map.copyOf(stateLabels); + } else { + this.stateLabels = null; + } + if (!commandLabels.isEmpty()) { + this.commandLabels = Map.copyOf(commandLabels); + } else { + this.commandLabels = null; + } + } + /** * Create a string value with a limited number of allowed states and commands. * @@ -64,6 +104,8 @@ public class TextValue extends Value { } else { this.commands = null; } + this.stateLabels = null; + this.commandLabels = null; } /** @@ -90,6 +132,8 @@ public class TextValue extends Value { } else { this.commands = null; } + this.stateLabels = null; + this.commandLabels = null; } /** @@ -106,6 +150,8 @@ public class TextValue extends Value { super(CoreItemFactory.STRING, List.of(StringType.class)); this.states = null; this.commands = null; + this.stateLabels = null; + this.commandLabels = null; } public void setNullValue(@Nullable String nullValue) { @@ -159,7 +205,13 @@ public class TextValue extends Value { StateDescriptionFragmentBuilder builder = super.createStateDescription(readOnly); final Map states = this.states; if (states != null) { - states.forEach((ohState, mqttState) -> builder.withOption(new StateOption(ohState, ohState))); + states.forEach((ohState, mqttState) -> { + String label = ohState; + if (stateLabels != null) { + label = stateLabels.getOrDefault(ohState, ohState); + } + builder.withOption(new StateOption(ohState, label)); + }); } return builder; } @@ -170,7 +222,11 @@ public class TextValue extends Value { final Map commands = this.commands; if (commands != null) { for (String command : commands.keySet()) { - builder.withCommandOption(new CommandOption(command, command)); + String label = command; + if (commandLabels != null) { + label = commandLabels.getOrDefault(command, command); + } + builder.withCommandOption(new CommandOption(command, label)); } } return builder; 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 16ed2c5c351..86f6b8b316f 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 @@ -21,6 +21,7 @@ import org.eclipse.jdt.annotation.Nullable; 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.HomeAssistantStateDescriptionProvider; import org.openhab.binding.mqtt.homeassistant.internal.handler.HomeAssistantThingHandler; import org.openhab.core.i18n.UnitProvider; import org.openhab.core.thing.Thing; @@ -55,7 +56,7 @@ public class MqttThingHandlerFactory extends BaseThingHandlerFactory { @Activate public MqttThingHandlerFactory(final @Reference MqttChannelTypeProvider typeProvider, - final @Reference MqttChannelStateDescriptionProvider stateDescriptionProvider, + final @Reference HomeAssistantStateDescriptionProvider stateDescriptionProvider, final @Reference ChannelTypeRegistry channelTypeRegistry, final @Reference UnitProvider unitProvider) { this.typeProvider = typeProvider; this.stateDescriptionProvider = stateDescriptionProvider; diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/HomeAssistantStateDescriptionProvider.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/HomeAssistantStateDescriptionProvider.java new file mode 100644 index 00000000000..f69899afc5e --- /dev/null +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/HomeAssistantStateDescriptionProvider.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mqtt.homeassistant.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.mqtt.generic.MqttChannelStateDescriptionProvider; +import org.openhab.core.i18n.TranslationProvider; +import org.openhab.core.thing.type.DynamicCommandDescriptionProvider; +import org.openhab.core.thing.type.DynamicStateDescriptionProvider; +import org.openhab.core.util.BundleResolver; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * This subclass exists solely so that the I18n provider can find the correct bundle + * for translations. + * + * @author Cody Cutrer - Initial contribution + */ +@Component(service = { DynamicStateDescriptionProvider.class, DynamicCommandDescriptionProvider.class, + HomeAssistantStateDescriptionProvider.class }) +@NonNullByDefault +public class HomeAssistantStateDescriptionProvider extends MqttChannelStateDescriptionProvider { + @Activate + public HomeAssistantStateDescriptionProvider(@Reference TranslationProvider i18nProvider, + @Reference BundleResolver bundleResolver) { + super(i18nProvider, bundleResolver); + } +} diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/AlarmControlPanel.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/AlarmControlPanel.java index 297e7db44a9..eaf1a6258d1 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/AlarmControlPanel.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/AlarmControlPanel.java @@ -66,6 +66,23 @@ public class AlarmControlPanel extends AbstractComponent COMMAND_LABELS = Map.of(PAYLOAD_ARM_AWAY, + "@text/command.alarm-control-panel.arm-away", PAYLOAD_ARM_HOME, + "@text/command.alarm-control-panel.arm-home", PAYLOAD_ARM_NIGHT, + "@text/command.alarm-control-panel.arm-night", PAYLOAD_ARM_VACATION, + "@text/command.alarm-control-panel.arm-vacation", PAYLOAD_ARM_CUSTOM_BYPASS, + "@text/command.alarm-control-panel.arm-custom-bypass", PAYLOAD_DISARM, + "@text/command.alarm-control-panel.disarm", PAYLOAD_TRIGGER, "@text/command.alarm-control-panel.trigger"); + private static final Map STATE_LABELS = Map.of(STATE_ARMED_AWAY, + "@text/state.alarm-control-panel.armed-away", STATE_ARMED_CUSTOM_BYPASS, + "@text/state.alarm-control-panel.armed-custom-bypass", STATE_ARMED_HOME, + "@text/state.alarm-control-panel.armed-home", STATE_ARMED_NIGHT, + "@text/state.alarm-control-panel.armed-night", STATE_ARMED_VACATION, + "@text/state.alarm-control-panel.armed-vacation", STATE_ARMING, "@text/state.alarm-control-panel.arming", + STATE_DISARMED, "@text/state.alarm-control-panel.disarmed", STATE_DISARMING, + "@text/state.alarm-control-panel.disarming", STATE_PENDING, "@text/state.alarm-control-panel.pending", + STATE_TRIGGERED, "@text/state.alarm-control-panel.triggered"); + /** * Configuration class for MQTT component */ @@ -140,7 +157,7 @@ public class AlarmControlPanel extends AbstractComponent { public static final String PAYLOAD_PRESS = "PRESS"; + private static final Map COMMAND_LABELS = Map.of(PAYLOAD_PRESS, "@text/command.button.press"); + /** * Configuration class for MQTT component */ @@ -54,7 +56,8 @@ public class Button extends AbstractComponent { public Button(ComponentFactory.ComponentConfiguration componentConfiguration) { super(componentConfiguration, ChannelConfiguration.class); - TextValue value = new TextValue(Map.of(), Map.of(PAYLOAD_PRESS, channelConfiguration.payloadPress)); + TextValue value = new TextValue(Map.of(), Map.of(PAYLOAD_PRESS, channelConfiguration.payloadPress), Map.of(), + COMMAND_LABELS); buildChannel(BUTTON_CHANNEL_ID, ComponentChannelType.STRING, value, getName(), componentConfiguration.getUpdateListener()) 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 549d32e2352..c7fff42ba3a 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 @@ -13,9 +13,11 @@ package org.openhab.binding.mqtt.homeassistant.internal.component; import java.math.BigDecimal; -import java.util.Arrays; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.function.Predicate; +import java.util.stream.Collectors; import javax.measure.quantity.Temperature; @@ -66,6 +68,33 @@ public class Climate extends AbstractComponent { 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"); + private static final String FAN_MODE_AUTO = "auto"; + private static final String FAN_MODE_LOW = "low"; + private static final String FAN_MODE_MEDIUM = "medium"; + private static final String FAN_MODE_HIGH = "high"; + + private static final Map FAN_MODE_LABELS = Map.of(FAN_MODE_AUTO, + "@text/state.climate.fan-mode.auto", FAN_MODE_LOW, "@text/state.climate.fan-mode.low", FAN_MODE_MEDIUM, + "@text/state.climate.fan-mode.medium", FAN_MODE_HIGH, "@text/state.climate.fan-mode.high"); + + private static final String MODE_AUTO = "auto"; + private static final String MODE_OFF = "off"; + private static final String MODE_COOL = "cool"; + private static final String MODE_HEAT = "heat"; + private static final String MODE_DRY = "dry"; + private static final String MODE_FAN_ONLY = "fan_only"; + + private static final Map MODE_LABELS = Map.of(MODE_AUTO, "@text/state.climate.mode.auto", MODE_OFF, + "@text/state.climate.mode.off", MODE_COOL, "@text/state.climate.mode.cool", MODE_HEAT, + "@text/state.climate.mode.heat", MODE_DRY, "@text/state.climate.mode.dry", MODE_FAN_ONLY, + "@text/state.climate.mode.fan-only"); + + private static final String SWING_MODE_ON = "on"; + private static final String SWING_MODE_OFF = "off"; + + private static final Map SWING_MODE_LABELS = Map.of(SWING_MODE_ON, + "@text/state.climate.swing-mode.on", SWING_MODE_OFF, "@text/state.climate.swing-mode.off"); + /** * Configuration class for MQTT component */ @@ -114,7 +143,7 @@ public class Climate extends AbstractComponent { @SerializedName("fan_mode_state_topic") protected @Nullable String fanModeStateTopic; @SerializedName("fan_modes") - protected List fanModes = Arrays.asList("auto", "low", "medium", "high"); + protected List fanModes = List.of(FAN_MODE_AUTO, FAN_MODE_LOW, FAN_MODE_MEDIUM, FAN_MODE_HIGH); @SerializedName("hold_command_template") protected @Nullable String holdCommandTemplate; @@ -136,7 +165,7 @@ public class Climate extends AbstractComponent { protected @Nullable String modeStateTemplate; @SerializedName("mode_state_topic") protected @Nullable String modeStateTopic; - protected List modes = Arrays.asList("auto", "off", "cool", "heat", "dry", "fan_only"); + protected List modes = List.of(MODE_AUTO, MODE_OFF, MODE_COOL, MODE_HEAT, MODE_DRY, MODE_FAN_ONLY); @SerializedName("preset_mode_command_template") protected @Nullable String presetModeCommandTemplate; @@ -159,7 +188,7 @@ public class Climate extends AbstractComponent { @SerializedName("swing_state_topic") protected @Nullable String swingStateTopic; @SerializedName("swing_modes") - protected List swingModes = Arrays.asList("on", "off"); + protected List swingModes = List.of(SWING_MODE_ON, SWING_MODE_OFF); @SerializedName("target_humidity_command_template") protected @Nullable String targetHumidityCommandTemplate; @@ -258,8 +287,10 @@ public class Climate extends AbstractComponent { channelConfiguration.currentTemperatureTemplate, channelConfiguration.currentTemperatureTopic, commandFilter); + Map modes = channelConfiguration.fanModes.stream() + .collect(Collectors.toMap(m -> m, m -> m, (a, b) -> a, LinkedHashMap::new)); buildOptionalChannel(FAN_MODE_CH_ID, ComponentChannelType.STRING, - new TextValue(channelConfiguration.fanModes.toArray(new String[0])), updateListener, + new TextValue(modes, modes, FAN_MODE_LABELS, FAN_MODE_LABELS), updateListener, channelConfiguration.fanModeCommandTemplate, channelConfiguration.fanModeCommandTopic, channelConfiguration.fanModeStateTemplate, channelConfiguration.fanModeStateTopic, commandFilter); @@ -271,8 +302,10 @@ public class Climate extends AbstractComponent { channelConfiguration.holdStateTemplate, channelConfiguration.holdStateTopic, commandFilter); } + modes = channelConfiguration.modes.stream() + .collect(Collectors.toMap(m -> m, m -> m, (a, b) -> a, LinkedHashMap::new)); buildOptionalChannel(MODE_CH_ID, ComponentChannelType.STRING, - new TextValue(channelConfiguration.modes.toArray(new String[0])), updateListener, + new TextValue(modes, modes, MODE_LABELS, MODE_LABELS), updateListener, channelConfiguration.modeCommandTemplate, channelConfiguration.modeCommandTopic, channelConfiguration.modeStateTemplate, channelConfiguration.modeStateTopic, commandFilter); @@ -281,8 +314,10 @@ public class Climate extends AbstractComponent { channelConfiguration.presetModeCommandTemplate, channelConfiguration.presetModeCommandTopic, channelConfiguration.presetModeStateTemplate, channelConfiguration.presetModeStateTopic, commandFilter); + modes = channelConfiguration.swingModes.stream() + .collect(Collectors.toMap(m -> m, m -> m, (a, b) -> a, LinkedHashMap::new)); buildOptionalChannel(SWING_CH_ID, ComponentChannelType.STRING, - new TextValue(channelConfiguration.swingModes.toArray(new String[0])), updateListener, + new TextValue(modes, modes, SWING_MODE_LABELS, SWING_MODE_LABELS), updateListener, channelConfiguration.swingCommandTemplate, channelConfiguration.swingCommandTopic, channelConfiguration.swingStateTemplate, channelConfiguration.swingStateTopic, commandFilter); diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Cover.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Cover.java index c7cece03090..e30855e7839 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Cover.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Cover.java @@ -54,6 +54,10 @@ public class Cover extends AbstractComponent { public static final String STATE_OPENING = "opening"; public static final String STATE_STOPPED = "stopped"; + private static final Map STATE_LABELS = Map.of(STATE_CLOSED, "@text/state.cover.closed", + STATE_CLOSING, "@text/state.cover.closing", STATE_OPEN, "@text/state.cover.open", STATE_OPENING, + "@text/state.cover.opening", STATE_STOPPED, "@text/state.cover.stopped"); + /** * Configuration class for MQTT component */ @@ -121,7 +125,7 @@ public class Cover extends AbstractComponent { states.put(channelConfiguration.stateOpen, STATE_OPEN); states.put(channelConfiguration.stateOpening, STATE_OPENING); states.put(channelConfiguration.stateStopped, STATE_STOPPED); - TextValue value = new TextValue(states, Map.of()); + TextValue value = new TextValue(states, Map.of(), STATE_LABELS, Map.of()); buildChannel(STATE_CHANNEL_ID, ComponentChannelType.STRING, value, "State", componentConfiguration.getUpdateListener()).stateTopic(stateTopic).isAdvanced(true).build(); } diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Lock.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Lock.java index ec02cf4cd05..65fb2d7e56d 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Lock.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Lock.java @@ -44,12 +44,19 @@ public class Lock extends AbstractComponent { public static final String PAYLOAD_UNLOCK = "UNLOCK"; public static final String PAYLOAD_OPEN = "OPEN"; + private static final Map COMMAND_LABELS = Map.of(PAYLOAD_LOCK, "@text/command.lock.lock", + PAYLOAD_UNLOCK, "@text/command.lock.unlock", PAYLOAD_OPEN, "@text/command.lock.open"); + public static final String STATE_JAMMED = "JAMMED"; public static final String STATE_LOCKED = "LOCKED"; public static final String STATE_LOCKING = "LOCKING"; public static final String STATE_UNLOCKED = "UNLOCKED"; public static final String STATE_UNLOCKING = "UNLOCKING"; + private static final Map STATE_LABELS = Map.of(STATE_JAMMED, "@text/state.lock.jammed", + STATE_LOCKED, "@text/state.lock.locked", STATE_LOCKING, "@text/state.lock.locking", STATE_UNLOCKED, + "@text/state.lock.unlocked", STATE_UNLOCKING, "@text/state.lock.unlocking"); + /** * Configuration class for MQTT component */ @@ -121,7 +128,7 @@ public class Lock extends AbstractComponent { states.put(channelConfiguration.stateLocking, STATE_LOCKING); states.put(channelConfiguration.stateUnlocking, STATE_UNLOCKING); states.put(channelConfiguration.stateJammed, STATE_JAMMED); - stateValue = new TextValue(states, commands); + stateValue = new TextValue(states, commands, STATE_LABELS, COMMAND_LABELS); buildChannel(STATE_CHANNEL_ID, ComponentChannelType.STRING, stateValue, "State", componentConfiguration.getUpdateListener()) diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Vacuum.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Vacuum.java index b2f0e401a6b..bcaa9c99377 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Vacuum.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Vacuum.java @@ -57,6 +57,11 @@ public class Vacuum extends AbstractComponent { public static final String PAYLOAD_START = "start"; public static final String PAYLOAD_STOP = "stop"; + private static final Map COMMAND_LABELS = Map.of(PAYLOAD_CLEAN_SPOT, + "@text/command.vacuum.clean-spot", PAYLOAD_LOCATE, "@text/command.vacuum.locate", PAYLOAD_PAUSE, + "@text/command.vacuum.pause", PAYLOAD_RETURN_TO_BASE, "@text/command.vacuum.return-to-base", PAYLOAD_START, + "@text/command.vacuum.start", PAYLOAD_STOP, "@text/command.vacuum.stop"); + public static final String STATE_CLEANING = "cleaning"; public static final String STATE_DOCKED = "docked"; public static final String STATE_PAUSED = "paused"; @@ -64,6 +69,11 @@ public class Vacuum extends AbstractComponent { public static final String STATE_RETURNING = "returning"; public static final String STATE_ERROR = "error"; + private static final Map STATE_LABELS = Map.of(STATE_CLEANING, "@text/state.vacuum.cleaning", + STATE_DOCKED, "@text/state.vacuum.docked", STATE_PAUSED, "@text/state.vacuum.paused", STATE_IDLE, + "@text/state.vacuum.idle", STATE_RETURNING, "@text/state.vacuum.returning", STATE_ERROR, + "@text/state.vacuum.error"); + public static final String COMMAND_CH_ID = "command"; public static final String FAN_SPEED_CH_ID = "fan-speed"; public static final String CUSTOM_COMMAND_CH_ID = "custom-command"; @@ -143,8 +153,9 @@ public class Vacuum extends AbstractComponent { addPayloadToList(supportedFeatures, FEATURE_STOP, PAYLOAD_STOP, channelConfiguration.payloadStop, commands); addPayloadToList(supportedFeatures, FEATURE_PAUSE, PAYLOAD_PAUSE, channelConfiguration.payloadPause, commands); - buildOptionalChannel(COMMAND_CH_ID, ComponentChannelType.STRING, new TextValue(Map.of(), commands), - updateListener, null, channelConfiguration.commandTopic, null, null, "Command"); + buildOptionalChannel(COMMAND_CH_ID, ComponentChannelType.STRING, + new TextValue(Map.of(), commands, Map.of(), COMMAND_LABELS), updateListener, null, + channelConfiguration.commandTopic, null, null, "Command"); final var fanSpeedList = channelConfiguration.fanSpeedList; if (supportedFeatures.contains(FEATURE_FAN_SPEED) && fanSpeedList != null && !fanSpeedList.isEmpty()) { @@ -170,10 +181,11 @@ public class Vacuum extends AbstractComponent { if (supportedFeatures.contains(FEATURE_STATUS)) { // state key is mandatory - buildOptionalChannel(STATE_CH_ID, ComponentChannelType.STRING, - new TextValue(new String[] { STATE_CLEANING, STATE_DOCKED, STATE_PAUSED, STATE_IDLE, - STATE_RETURNING, STATE_ERROR }), - updateListener, null, null, STATE_TEMPLATE, channelConfiguration.stateTopic, "State"); + buildOptionalChannel(STATE_CH_ID, ComponentChannelType.STRING, new TextValue( + Map.of(STATE_CLEANING, STATE_CLEANING, STATE_DOCKED, STATE_DOCKED, STATE_PAUSED, STATE_PAUSED, + STATE_IDLE, STATE_IDLE, STATE_RETURNING, STATE_RETURNING, STATE_ERROR, STATE_ERROR), + Map.of(), STATE_LABELS, Map.of()), updateListener, null, null, STATE_TEMPLATE, + channelConfiguration.stateTopic, "State"); if (supportedFeatures.contains(FEATURE_BATTERY)) { buildOptionalChannel(BATTERY_LEVEL_CH_ID, ComponentChannelType.DIMMER, new PercentageValue(BigDecimal.ZERO, BigDecimal.valueOf(100), BigDecimal.ONE, null, null, null), diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Valve.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Valve.java index 005d8293123..7537fc9b4bc 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Valve.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Valve.java @@ -62,11 +62,18 @@ public class Valve extends AbstractComponent impleme public static final String PAYLOAD_CLOSE = "CLOSE"; public static final String PAYLOAD_STOP = "STOP"; + private static final Map COMMAND_LABELS = Map.of(PAYLOAD_OPEN, "@text/command.valve.open", + PAYLOAD_CLOSE, "@text/command.valve.close", PAYLOAD_STOP, "@text/command.valve.stop"); + public static final String STATE_OPEN = "open"; public static final String STATE_OPENING = "opening"; public static final String STATE_CLOSED = "closed"; public static final String STATE_CLOSING = "closing"; + private static final Map STATE_LABELS = Map.of(STATE_OPEN, "@text/state.valve.open", STATE_OPENING, + "@text/state.valve.opening", STATE_CLOSED, "@text/state.valve.closed", STATE_CLOSING, + "@text/state.valve.closing"); + private static final String FORMAT_INTEGER = "%.0f"; private final Logger logger = LoggerFactory.getLogger(Valve.class); 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 index 5ea47bcc447..a710347b8bb 100644 --- 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 @@ -14,7 +14,9 @@ package org.openhab.binding.mqtt.homeassistant.internal.component; import java.math.BigDecimal; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; import javax.measure.quantity.Temperature; @@ -55,6 +57,12 @@ public class WaterHeater extends AbstractComponent DEFAULT_MODES = List.of(MODE_OFF, MODE_ECO, MODE_ELECTRIC, MODE_GAS, MODE_HEAT_PUMP, MODE_HIGH_DEMAND, MODE_PERFORMANCE); + private static final Map MODE_LABELS = Map.of(MODE_OFF, "@text/state.water-heater.mode.off", + MODE_ECO, "@text/state.water-heater.mode.eco", MODE_ELECTRIC, "@text/state.water-heater.mode.electric", + MODE_GAS, "@text/state.water-heater.mode.gas", MODE_HEAT_PUMP, "@text/state.water-heater.mode.heat-pump", + MODE_HIGH_DEMAND, "@text/state.water-heater.mode.high-demand", MODE_PERFORMANCE, + "@text/state.water-heater.mode.performance"); + public static final String TEMPERATURE_UNIT_C = "C"; public static final String TEMPERATURE_UNIT_F = "F"; @@ -153,8 +161,10 @@ public class WaterHeater extends AbstractComponent modes = channelConfiguration.modes.stream() + .collect(Collectors.toMap(m -> m, m -> m, (a, b) -> a, LinkedHashMap::new)); buildChannel(MODE_CHANNEL_ID, ComponentChannelType.STRING, - new TextValue(channelConfiguration.modes.toArray(new String[0])), "Mode", + new TextValue(modes, modes, MODE_LABELS, MODE_LABELS), "Mode", componentConfiguration.getUpdateListener()) .stateTopic(channelConfiguration.modeStateTopic, channelConfiguration.modeStateTemplate, channelConfiguration.getValueTemplate()) 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 659b0d8bc6e..64f4886346f 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 @@ -58,3 +58,74 @@ thing-type.config.mqtt.homeassistant-updatable.doUpdate.description = Request th binding.config.mqtt.homeassistant-status.label = Publish Online Status binding.config.mqtt.homeassistant-status.description = Publish online to homeassistant/status when discovering Home Assistant things in order to trigger devices to publish up-to-date discovery information. If you also run Home Assistant and other services that depend on knowing if Home Assistant is not running, then it's possible for those services to be out-of-sync with the actual status of Home Assistant, and you may want to disable this. + +# command and state labels + +command.alarm-control-panel.arm-away = Arm Away +command.alarm-control-panel.arm-home = Arm Home +command.alarm-control-panel.arm-night = Arm Night +command.alarm-control-panel.arm-vacation = Arm Vacation +command.alarm-control-panel.custom-bypass = Custom Bypass +command.alarm-control-panel.disarm = Disarm +command.alarm-control-panel.trigger = Trigger +command.button.press = Press +command.lock.lock = Lock +command.lock.unlock = Unlock +command.lock.open = Open +command.vacuum.clean-spot = Clean Spot +command.vacuum.locate = Locate +command.vacuum.pause = Pause +command.vacuum.return-to-base = Return to Base +command.vacuum.start = Start +command.vacuum.stop = Stop +command.valve.open = Open +command.valve.close = Close +command.valve.stop = Stop +state.alarm-control-panel.armed-away = Armed Away +state.alarm-control-panel.armed-custom-bypass = Armed Custom Bypass +state.alarm-control-panel.armed-home = Armed Home +state.alarm-control-panel.armed-night = Armed Night +state.alarm-control-panel.armed-vacation = Armed Vacation +state.alarm-control-panel.arming = Arming +state.alarm-control-panel.disarmed = Disarmed +state.alarm-control-panel.pending = Pending +state.alarm-control-panel.triggered = Triggered +state.climate.fan-mode.auto = Auto +state.climate.fan-mode.low = Low +state.climate.fan-mode.medium = Medium +state.climate.fan-mode.high = High +state.climate.mode.auto = Auto +state.climate.mode.off = Off +state.climate.mode.cool = Cool +state.climate.mode.heat = Heat +state.climate.mode.dry = Dry +state.climate.mode.fan_only = Fan Only +state.climate.swing-mode.on = On +state.climate.swing-mode.off = Off +state.cover.closed = Closed +state.cover.closing = Closing +state.cover.open = Open +state.cover.opening = Opening +state.cover.stopped = Stopped +state.lock.jammed = Jammed +state.lock.locked = Locked +state.lock.locking = Locking +state.lock.unlocked = Unlocked +state.lock.unlocking = Unlocking +state.vacuum.cleaning = Cleaning +state.vacuum.docked = Docked +state.vacuum.paused = Paused +state.vacuum.idle = Idle +state.vacuum.returning = Returning +state.vacuum.error = Error +state.valve.open = Open +state.valve.opening = Opening +state.valve.closed = Closed +state.valve.closing = Closing +state.water-heater.mode.off = Off +state.water-heater.mode.eco = Eco +state.water-heater.mode.electric = Electric +state.water-heater.mode.gas = Gas +state.water-heater.mode.heat-pump = Heat Pump +state.water-heater.mode.high-demand = High Demand +state.water-heater.mode.performance = Performance diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/AbstractHomeAssistantTests.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/AbstractHomeAssistantTests.java index db716c5e126..866710b685b 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/AbstractHomeAssistantTests.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/AbstractHomeAssistantTests.java @@ -39,6 +39,7 @@ import org.openhab.binding.mqtt.generic.MqttChannelStateDescriptionProvider; import org.openhab.binding.mqtt.generic.MqttChannelTypeProvider; import org.openhab.binding.mqtt.handler.BrokerHandler; import org.openhab.binding.mqtt.homeassistant.generic.internal.MqttBindingConstants; +import org.openhab.core.i18n.TranslationProvider; import org.openhab.core.io.transport.mqtt.MqttBrokerConnection; import org.openhab.core.io.transport.mqtt.MqttMessageSubscriber; import org.openhab.core.test.java.JavaTest; @@ -58,6 +59,7 @@ import org.openhab.core.thing.type.ThingTypeBuilder; import org.openhab.core.thing.type.ThingTypeRegistry; import org.openhab.core.transform.TransformationHelper; import org.openhab.core.transform.TransformationService; +import org.openhab.core.util.BundleResolver; import org.osgi.framework.BundleContext; import org.osgi.framework.ServiceReference; @@ -100,6 +102,8 @@ public abstract class AbstractHomeAssistantTests extends JavaTest { private @Mock @NonNullByDefault({}) TransformationService transformationService1Mock; private @Mock @NonNullByDefault({}) BundleContext bundleContextMock; + private @Mock @NonNullByDefault({}) TranslationProvider translationProvider; + private @Mock @NonNullByDefault({}) BundleResolver bundleResolver; private @Mock @NonNullByDefault({}) ServiceReference serviceRefMock; private @NonNullByDefault({}) TransformationHelper transformationHelper; @@ -114,7 +118,7 @@ public abstract class AbstractHomeAssistantTests extends JavaTest { when(thingTypeRegistry.getThingType(MqttBindingConstants.HOMEASSISTANT_MQTT_THING)).thenReturn(HA_THING_TYPE); channelTypeProvider = spy(new MqttChannelTypeProvider(thingTypeRegistry, new VolatileStorageService())); - stateDescriptionProvider = spy(new MqttChannelStateDescriptionProvider()); + stateDescriptionProvider = spy(new MqttChannelStateDescriptionProvider(translationProvider, bundleResolver)); channelTypeRegistry = spy(new ChannelTypeRegistry()); setupConnection(); 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 31ba501f266..9e17da9558f 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 @@ -28,14 +28,15 @@ import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; -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.TranslationProvider; 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; +import org.openhab.core.util.BundleResolver; /** * @author Jochen Klein - Initial contribution @@ -48,12 +49,15 @@ public class HomeAssistantChannelTransformationTests { protected @Mock @NonNullByDefault({}) UnitProvider unitProvider; protected @NonNullByDefault({}) HomeAssistantChannelTransformation transformation; + private @Mock @NonNullByDefault({}) BundleResolver bundleResolver; + private @Mock @NonNullByDefault({}) TranslationProvider translationProvider; @BeforeEach public void beforeEachChannelTransformationTest() { MqttChannelTypeProvider channelTypeProvider = new MqttChannelTypeProvider(thingTypeRegistry, new VolatileStorageService()); - MqttChannelStateDescriptionProvider stateDescriptionProvider = new MqttChannelStateDescriptionProvider(); + HomeAssistantStateDescriptionProvider stateDescriptionProvider = new HomeAssistantStateDescriptionProvider( + translationProvider, bundleResolver); ChannelTypeRegistry channelTypeRegistry = new ChannelTypeRegistry(); MqttThingHandlerFactory thingHandlerFactory = new MqttThingHandlerFactory(channelTypeProvider, stateDescriptionProvider, channelTypeRegistry, unitProvider); diff --git a/bundles/org.openhab.binding.mqtt.homie/src/test/java/org/openhab/binding/mqtt/homie/internal/handler/HomieThingHandlerTests.java b/bundles/org.openhab.binding.mqtt.homie/src/test/java/org/openhab/binding/mqtt/homie/internal/handler/HomieThingHandlerTests.java index 0c4fc01042c..cf158c7b7f8 100644 --- a/bundles/org.openhab.binding.mqtt.homie/src/test/java/org/openhab/binding/mqtt/homie/internal/handler/HomieThingHandlerTests.java +++ b/bundles/org.openhab.binding.mqtt.homie/src/test/java/org/openhab/binding/mqtt/homie/internal/handler/HomieThingHandlerTests.java @@ -60,6 +60,7 @@ import org.openhab.binding.mqtt.homie.internal.homie300.Property; import org.openhab.binding.mqtt.homie.internal.homie300.PropertyAttributes; import org.openhab.binding.mqtt.homie.internal.homie300.PropertyAttributes.DataTypeEnum; import org.openhab.core.config.core.Configuration; +import org.openhab.core.i18n.TranslationProvider; import org.openhab.core.io.transport.mqtt.MqttBrokerConnection; import org.openhab.core.library.types.StringType; import org.openhab.core.test.storage.VolatileStorageService; @@ -77,6 +78,7 @@ import org.openhab.core.thing.type.ChannelTypeRegistry; import org.openhab.core.thing.type.ThingTypeBuilder; import org.openhab.core.thing.type.ThingTypeRegistry; import org.openhab.core.types.RefreshType; +import org.openhab.core.util.BundleResolver; /** * Tests cases for {@link HomieThingHandler}. @@ -96,13 +98,16 @@ public class HomieThingHandlerTests { private @Mock @NonNullByDefault({}) ThingTypeRegistry thingTypeRegistryMock; private @Mock @NonNullByDefault({}) ChannelTypeRegistry channelTypeRegistryMock; private @Mock @NonNullByDefault({}) ChannelType channelTypeMock; + private @Mock @NonNullByDefault({}) BundleResolver bundleResolver; + private @Mock @NonNullByDefault({}) TranslationProvider translationProvider; private @NonNullByDefault({}) Thing thing; private @NonNullByDefault({}) HomieThingHandler thingHandler; private final MqttChannelTypeProvider channelTypeProvider = spy( new MqttChannelTypeProvider(thingTypeRegistryMock, new VolatileStorageService())); - private final MqttChannelStateDescriptionProvider stateDescriptionProvider = new MqttChannelStateDescriptionProvider(); + private final MqttChannelStateDescriptionProvider stateDescriptionProvider = new MqttChannelStateDescriptionProvider( + translationProvider, bundleResolver); private final String deviceID = ThingChannelConstants.TEST_HOMIE_THING.getId(); private final String deviceTopic = "homie/" + deviceID;