[mqtt.homeassistant] add support for Number and Select components (#14230)
* [mqtt.homeassistant] add support for Select component closes #13603 Signed-off-by: Cody Cutrer <cody@cutrer.us>
This commit is contained in:
parent
89d0689e36
commit
00c10e1015
|
@ -69,6 +69,10 @@ public class ComponentFactory {
|
||||||
return Light.create(componentConfiguration);
|
return Light.create(componentConfiguration);
|
||||||
case "lock":
|
case "lock":
|
||||||
return new Lock(componentConfiguration);
|
return new Lock(componentConfiguration);
|
||||||
|
case "number":
|
||||||
|
return new Number(componentConfiguration);
|
||||||
|
case "select":
|
||||||
|
return new Select(componentConfiguration);
|
||||||
case "sensor":
|
case "sensor":
|
||||||
return new Sensor(componentConfiguration);
|
return new Sensor(componentConfiguration);
|
||||||
case "switch":
|
case "switch":
|
||||||
|
|
|
@ -0,0 +1,92 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2023 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 org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
|
import org.openhab.binding.mqtt.generic.values.NumberValue;
|
||||||
|
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
|
||||||
|
import org.openhab.binding.mqtt.homeassistant.internal.exception.ConfigurationException;
|
||||||
|
import org.openhab.core.types.util.UnitUtils;
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A MQTT Number, following the https://www.home-assistant.io/components/number.mqtt/ specification.
|
||||||
|
*
|
||||||
|
* @author Cody Cutrer - Initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class Number extends AbstractComponent<Number.ChannelConfiguration> {
|
||||||
|
public static final String NUMBER_CHANNEL_ID = "number"; // Randomly chosen channel "ID"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration class for MQTT component
|
||||||
|
*/
|
||||||
|
static class ChannelConfiguration extends AbstractChannelConfiguration {
|
||||||
|
ChannelConfiguration() {
|
||||||
|
super("MQTT Number");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected @Nullable Boolean optimistic;
|
||||||
|
|
||||||
|
@SerializedName("unit_of_measurement")
|
||||||
|
protected @Nullable String unitOfMeasurement;
|
||||||
|
@SerializedName("device_class")
|
||||||
|
protected @Nullable String deviceClass;
|
||||||
|
|
||||||
|
@SerializedName("command_template")
|
||||||
|
protected @Nullable String commandTemplate;
|
||||||
|
@SerializedName("command_topic")
|
||||||
|
protected @Nullable String commandTopic;
|
||||||
|
@SerializedName("state_topic")
|
||||||
|
protected String stateTopic = "";
|
||||||
|
|
||||||
|
protected BigDecimal min = new BigDecimal(1);
|
||||||
|
protected BigDecimal max = new BigDecimal(100);
|
||||||
|
protected BigDecimal step = new BigDecimal(1);
|
||||||
|
|
||||||
|
@SerializedName("payload_reset")
|
||||||
|
protected String payloadReset = "None";
|
||||||
|
|
||||||
|
protected String mode = "auto";
|
||||||
|
|
||||||
|
@SerializedName("json_attributes_topic")
|
||||||
|
protected @Nullable String jsonAttributesTopic;
|
||||||
|
@SerializedName("json_attributes_template")
|
||||||
|
protected @Nullable String jsonAttributesTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Number(ComponentFactory.ComponentConfiguration componentConfiguration) {
|
||||||
|
super(componentConfiguration, ChannelConfiguration.class);
|
||||||
|
|
||||||
|
boolean optimistic = channelConfiguration.optimistic != null ? channelConfiguration.optimistic
|
||||||
|
: channelConfiguration.stateTopic.isBlank();
|
||||||
|
|
||||||
|
if (optimistic && !channelConfiguration.stateTopic.isBlank()) {
|
||||||
|
throw new ConfigurationException("Component:Number does not support forced optimistic mode");
|
||||||
|
}
|
||||||
|
|
||||||
|
NumberValue value = new NumberValue(channelConfiguration.min, channelConfiguration.max,
|
||||||
|
channelConfiguration.step, UnitUtils.parseUnit(channelConfiguration.unitOfMeasurement));
|
||||||
|
|
||||||
|
buildChannel(NUMBER_CHANNEL_ID, value, channelConfiguration.getName(),
|
||||||
|
componentConfiguration.getUpdateListener())
|
||||||
|
.stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate())
|
||||||
|
.commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
|
||||||
|
channelConfiguration.getQos(), channelConfiguration.commandTemplate)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2023 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 org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
|
import org.openhab.binding.mqtt.generic.values.TextValue;
|
||||||
|
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
|
||||||
|
import org.openhab.binding.mqtt.homeassistant.internal.exception.ConfigurationException;
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A MQTT select, following the https://www.home-assistant.io/components/select.mqtt/ specification.
|
||||||
|
*
|
||||||
|
* @author Cody Cutrer - Initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class Select extends AbstractComponent<Select.ChannelConfiguration> {
|
||||||
|
public static final String SELECT_CHANNEL_ID = "select"; // Randomly chosen channel "ID"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration class for MQTT component
|
||||||
|
*/
|
||||||
|
static class ChannelConfiguration extends AbstractChannelConfiguration {
|
||||||
|
ChannelConfiguration() {
|
||||||
|
super("MQTT Select");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected @Nullable Boolean optimistic;
|
||||||
|
|
||||||
|
@SerializedName("command_template")
|
||||||
|
protected @Nullable String commandTemplate;
|
||||||
|
@SerializedName("command_topic")
|
||||||
|
protected @Nullable String commandTopic;
|
||||||
|
@SerializedName("state_topic")
|
||||||
|
protected String stateTopic = "";
|
||||||
|
|
||||||
|
protected String[] options = new String[0];
|
||||||
|
|
||||||
|
@SerializedName("json_attributes_topic")
|
||||||
|
protected @Nullable String jsonAttributesTopic;
|
||||||
|
@SerializedName("json_attributes_template")
|
||||||
|
protected @Nullable String jsonAttributesTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Select(ComponentFactory.ComponentConfiguration componentConfiguration) {
|
||||||
|
super(componentConfiguration, ChannelConfiguration.class);
|
||||||
|
|
||||||
|
boolean optimistic = channelConfiguration.optimistic != null ? channelConfiguration.optimistic
|
||||||
|
: channelConfiguration.stateTopic.isBlank();
|
||||||
|
|
||||||
|
if (optimistic && !channelConfiguration.stateTopic.isBlank()) {
|
||||||
|
throw new ConfigurationException("Component:Select does not support forced optimistic mode");
|
||||||
|
}
|
||||||
|
|
||||||
|
TextValue value = new TextValue(channelConfiguration.options);
|
||||||
|
|
||||||
|
buildChannel(SELECT_CHANNEL_ID, value, channelConfiguration.getName(),
|
||||||
|
componentConfiguration.getUpdateListener())
|
||||||
|
.stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate())
|
||||||
|
.commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
|
||||||
|
channelConfiguration.getQos(), channelConfiguration.commandTemplate)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,80 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2023 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 java.util.Set;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.openhab.binding.mqtt.generic.values.NumberValue;
|
||||||
|
import org.openhab.core.library.types.DecimalType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link Number}
|
||||||
|
*
|
||||||
|
* @author Cody Cutrer - Initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class NumberTests extends AbstractComponentTests {
|
||||||
|
public static final String CONFIG_TOPIC = "number/0x0000000000000000_number_zigbee2mqtt";
|
||||||
|
|
||||||
|
@SuppressWarnings("null")
|
||||||
|
@Test
|
||||||
|
public void test() throws InterruptedException {
|
||||||
|
var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC), """
|
||||||
|
{
|
||||||
|
"name": "BWA Link Hot Tub Pump 1",
|
||||||
|
"availability_topic": "homie/bwa/$state",
|
||||||
|
"payload_available": "ready",
|
||||||
|
"payload_not_available": "lost",
|
||||||
|
"qos": 1,
|
||||||
|
"icon": "mdi:chart-bubble",
|
||||||
|
"device": {
|
||||||
|
"manufacturer": "Balboa Water Group",
|
||||||
|
"sw_version": "2.1.3",
|
||||||
|
"model": "BFBP20",
|
||||||
|
"name": "BWA Link",
|
||||||
|
"identifiers": "bwa"
|
||||||
|
},
|
||||||
|
"state_topic": "homie/bwa/spa/pump1",
|
||||||
|
"command_topic": "homie/bwa/spa/pump1/set",
|
||||||
|
"command_template": "{{ value | round(0) }}",
|
||||||
|
"min": 0,
|
||||||
|
"max": 2,
|
||||||
|
"unique_id": "bwa_spa_pump1"
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
assertThat(component.channels.size(), is(1));
|
||||||
|
assertThat(component.getName(), is("BWA Link Hot Tub Pump 1"));
|
||||||
|
|
||||||
|
assertChannel(component, Number.NUMBER_CHANNEL_ID, "homie/bwa/spa/pump1", "homie/bwa/spa/pump1/set",
|
||||||
|
"BWA Link Hot Tub Pump 1", NumberValue.class);
|
||||||
|
|
||||||
|
publishMessage("homie/bwa/spa/pump1", "1");
|
||||||
|
assertState(component, Number.NUMBER_CHANNEL_ID, new DecimalType(1));
|
||||||
|
publishMessage("homie/bwa/spa/pump1", "2");
|
||||||
|
assertState(component, Number.NUMBER_CHANNEL_ID, new DecimalType(2));
|
||||||
|
|
||||||
|
component.getChannel(Number.NUMBER_CHANNEL_ID).getState().publishValue(new DecimalType(1.1));
|
||||||
|
assertPublished("homie/bwa/spa/pump1/set", "1");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Set<String> getConfigTopics() {
|
||||||
|
return Set.of(CONFIG_TOPIC);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,130 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2023 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.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.openhab.binding.mqtt.generic.values.TextValue;
|
||||||
|
import org.openhab.core.library.types.StringType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link Select}
|
||||||
|
*
|
||||||
|
* @author Cody Cutrer - Initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class SelectTests extends AbstractComponentTests {
|
||||||
|
public static final String CONFIG_TOPIC = "select/0x54ef44100064b266";
|
||||||
|
|
||||||
|
@SuppressWarnings("null")
|
||||||
|
@Test
|
||||||
|
public void testSelectWithStateAndCommand() {
|
||||||
|
var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC), """
|
||||||
|
{
|
||||||
|
"availability": [
|
||||||
|
{"topic": "zigbee2mqtt/bridge/state"},
|
||||||
|
{"topic": "zigbee2mqtt/gbos/availability"}
|
||||||
|
],
|
||||||
|
"availability_mode": "all",
|
||||||
|
"command_topic": "zigbee2mqtt/gbos/set/approach_distance",
|
||||||
|
"device": {
|
||||||
|
"configuration_url": "#/device/0x54ef44100064b266/info",
|
||||||
|
"identifiers": [
|
||||||
|
"zigbee2mqtt_0x54ef44100064b266"
|
||||||
|
],
|
||||||
|
"manufacturer": "Xiaomi",
|
||||||
|
"model": "Aqara presence detector FP1 (RTCZCGQ11LM)",
|
||||||
|
"name": "Guest Bathroom Occupancy Sensor",
|
||||||
|
"sw_version": ""
|
||||||
|
},
|
||||||
|
"name": "Guest Bathroom Occupancy Sensor approach distance",
|
||||||
|
"options": [
|
||||||
|
"far",
|
||||||
|
"medium",
|
||||||
|
"near"
|
||||||
|
],
|
||||||
|
"state_topic": "zigbee2mqtt/gbos",
|
||||||
|
"unique_id": "0x54ef44100064b266_approach_distance_zigbee2mqtt",
|
||||||
|
"value_template":"{{ value_json.approach_distance }}"
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
assertThat(component.channels.size(), is(1));
|
||||||
|
assertThat(component.getName(), is("Guest Bathroom Occupancy Sensor approach distance"));
|
||||||
|
|
||||||
|
assertChannel(component, Select.SELECT_CHANNEL_ID, "zigbee2mqtt/gbos", "zigbee2mqtt/gbos/set/approach_distance",
|
||||||
|
"Guest Bathroom Occupancy Sensor approach distance", TextValue.class);
|
||||||
|
|
||||||
|
publishMessage("zigbee2mqtt/gbos", "{\"approach_distance\": \"far\"}");
|
||||||
|
assertState(component, Select.SELECT_CHANNEL_ID, new StringType("far"));
|
||||||
|
publishMessage("zigbee2mqtt/gbos", "{\"approach_distance\": \"medium\"}");
|
||||||
|
assertState(component, Select.SELECT_CHANNEL_ID, new StringType("medium"));
|
||||||
|
|
||||||
|
component.getChannel(Select.SELECT_CHANNEL_ID).getState().publishValue(new StringType("near"));
|
||||||
|
assertPublished("zigbee2mqtt/gbos/set/approach_distance", "near");
|
||||||
|
component.getChannel(Select.SELECT_CHANNEL_ID).getState().publishValue(new StringType("medium"));
|
||||||
|
assertPublished("zigbee2mqtt/gbos/set/approach_distance", "medium");
|
||||||
|
assertThrows(IllegalArgumentException.class,
|
||||||
|
() -> component.getChannel(Select.SELECT_CHANNEL_ID).getState().publishValue(new StringType("bogus")));
|
||||||
|
assertNotPublished("zigbee2mqtt/gbos/set/approach_distance", "bogus");
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("null")
|
||||||
|
@Test
|
||||||
|
public void testSelectWithCommandTemplate() {
|
||||||
|
var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC), """
|
||||||
|
{
|
||||||
|
"availability": [
|
||||||
|
{"topic": "zigbee2mqtt/bridge/state"},
|
||||||
|
{"topic": "zigbee2mqtt/gbos/availability"}
|
||||||
|
],
|
||||||
|
"availability_mode": "all",
|
||||||
|
"command_topic": "zigbee2mqtt/gbos/set/approach_distance",
|
||||||
|
"command_template": "set to {{ value }}",
|
||||||
|
"device": {
|
||||||
|
"configuration_url": "#/device/0x54ef44100064b266/info",
|
||||||
|
"identifiers": [
|
||||||
|
"zigbee2mqtt_0x54ef44100064b266"
|
||||||
|
],
|
||||||
|
"manufacturer": "Xiaomi",
|
||||||
|
"model": "Aqara presence detector FP1 (RTCZCGQ11LM)",
|
||||||
|
"name": "Guest Bathroom Occupancy Sensor",
|
||||||
|
"sw_version": ""
|
||||||
|
},
|
||||||
|
"name": "Guest Bathroom Occupancy Sensor approach distance",
|
||||||
|
"options": [
|
||||||
|
"far",
|
||||||
|
"medium",
|
||||||
|
"near"
|
||||||
|
],
|
||||||
|
"state_topic": "zigbee2mqtt/gbos",
|
||||||
|
"unique_id": "0x54ef44100064b266_approach_distance_zigbee2mqtt",
|
||||||
|
"value_template":"{{ value_json.approach_distance }}"
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
component.getChannel(Select.SELECT_CHANNEL_ID).getState().publishValue(new StringType("near"));
|
||||||
|
assertPublished("zigbee2mqtt/gbos/set/approach_distance", "set to near");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Set<String> getConfigTopics() {
|
||||||
|
return Set.of(CONFIG_TOPIC);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue