[mqtt.homeassistant] I18n well known commands and states (#18443)

Signed-off-by: Cody Cutrer <cody@cutrer.us>
pull/18450/head
Cody Cutrer 2025-03-25 14:39:07 -06:00 committed by GitHub
parent 45258e2ec4
commit 1608b202ab
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 355 additions and 26 deletions

View File

@ -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<ChannelUID, CommandDescription> 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());
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;
}
}

View File

@ -41,9 +41,49 @@ import org.openhab.core.types.UnDefType;
public class TextValue extends Value {
private final @Nullable Map<String, String> states;
private final @Nullable Map<String, String> commands;
private final @Nullable Map<String, String> stateLabels;
private final @Nullable Map<String, String> 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<String, String> states, Map<String, String> commands, Map<String, String> stateLabels,
Map<String, String> 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<String, String> 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<String, String> 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;

View File

@ -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;

View File

@ -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);
}
}

View File

@ -66,6 +66,23 @@ public class AlarmControlPanel extends AbstractComponent<AlarmControlPanel.Chann
public static final String STATE_PENDING = "pending";
public static final String STATE_TRIGGERED = "triggered";
private static final Map<String, String> 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<String, String> 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<AlarmControlPanel.Chann
commandEnum.put(PAYLOAD_TRIGGER, channelConfiguration.payloadTrigger);
}
TextValue value = new TextValue(stateEnum, commandEnum);
TextValue value = new TextValue(stateEnum, commandEnum, STATE_LABELS, COMMAND_LABELS);
var builder = buildChannel(STATE_CHANNEL_ID, ComponentChannelType.STRING, value, getName(),
componentConfiguration.getUpdateListener())
.stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate());

View File

@ -34,6 +34,8 @@ public class Button extends AbstractComponent<Button.ChannelConfiguration> {
public static final String PAYLOAD_PRESS = "PRESS";
private static final Map<String, String> COMMAND_LABELS = Map.of(PAYLOAD_PRESS, "@text/command.button.press");
/**
* Configuration class for MQTT component
*/
@ -54,7 +56,8 @@ public class Button extends AbstractComponent<Button.ChannelConfiguration> {
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())

View File

@ -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<Climate.ChannelConfiguration> {
private static final State ACTION_OFF_STATE = new StringType(ACTION_OFF);
private static final List<String> ACTION_MODES = List.of(ACTION_OFF, "heating", "cooling", "drying", "idle", "fan");
private static final 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<String, String> 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<String, String> 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<String, String> 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<Climate.ChannelConfiguration> {
@SerializedName("fan_mode_state_topic")
protected @Nullable String fanModeStateTopic;
@SerializedName("fan_modes")
protected List<String> fanModes = Arrays.asList("auto", "low", "medium", "high");
protected List<String> 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<Climate.ChannelConfiguration> {
protected @Nullable String modeStateTemplate;
@SerializedName("mode_state_topic")
protected @Nullable String modeStateTopic;
protected List<String> modes = Arrays.asList("auto", "off", "cool", "heat", "dry", "fan_only");
protected List<String> 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<Climate.ChannelConfiguration> {
@SerializedName("swing_state_topic")
protected @Nullable String swingStateTopic;
@SerializedName("swing_modes")
protected List<String> swingModes = Arrays.asList("on", "off");
protected List<String> 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<Climate.ChannelConfiguration> {
channelConfiguration.currentTemperatureTemplate, channelConfiguration.currentTemperatureTopic,
commandFilter);
Map<String, String> 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<Climate.ChannelConfiguration> {
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<Climate.ChannelConfiguration> {
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);

View File

@ -54,6 +54,10 @@ public class Cover extends AbstractComponent<Cover.ChannelConfiguration> {
public static final String STATE_OPENING = "opening";
public static final String STATE_STOPPED = "stopped";
private static final Map<String, String> 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<Cover.ChannelConfiguration> {
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();
}

View File

@ -44,12 +44,19 @@ public class Lock extends AbstractComponent<Lock.ChannelConfiguration> {
public static final String PAYLOAD_UNLOCK = "UNLOCK";
public static final String PAYLOAD_OPEN = "OPEN";
private static final Map<String, String> 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<String, String> 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<Lock.ChannelConfiguration> {
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())

View File

@ -57,6 +57,11 @@ public class Vacuum extends AbstractComponent<Vacuum.ChannelConfiguration> {
public static final String PAYLOAD_START = "start";
public static final String PAYLOAD_STOP = "stop";
private static final Map<String, String> 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<Vacuum.ChannelConfiguration> {
public static final String STATE_RETURNING = "returning";
public static final String STATE_ERROR = "error";
private static final Map<String, String> 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<Vacuum.ChannelConfiguration> {
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<Vacuum.ChannelConfiguration> {
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),

View File

@ -62,11 +62,18 @@ public class Valve extends AbstractComponent<Valve.ChannelConfiguration> impleme
public static final String PAYLOAD_CLOSE = "CLOSE";
public static final String PAYLOAD_STOP = "STOP";
private static final Map<String, String> 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<String, String> 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);

View File

@ -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<WaterHeater.ChannelConfigurat
public static final List<String> DEFAULT_MODES = List.of(MODE_OFF, MODE_ECO, MODE_ELECTRIC, MODE_GAS,
MODE_HEAT_PUMP, MODE_HIGH_DEMAND, MODE_PERFORMANCE);
private static final Map<String, String> 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<WaterHeater.ChannelConfigurat
}
if (channelConfiguration.modeCommandTopic != null | channelConfiguration.modeStateTopic != null) {
Map<String, String> 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())

View File

@ -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 <tt>online</tt> to <tt>homeassistant/status</tt> when discovering Home Assistant things in order to trigger devices to publish up-to-date discovery information. If you also run Home Assistant <i>and</i> 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

View File

@ -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<TransformationService> 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();

View File

@ -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);

View File

@ -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;