]> git.basschouten.com Git - openhab-addons.git/commitdiff
[mqtt.homeassistant] add support for Number and Select components (#14230)
authorCody Cutrer <cody@cutrer.us>
Sat, 21 Jan 2023 14:02:49 +0000 (07:02 -0700)
committerGitHub <noreply@github.com>
Sat, 21 Jan 2023 14:02:49 +0000 (15:02 +0100)
* [mqtt.homeassistant] add support for Select component

closes #13603

Signed-off-by: Cody Cutrer <cody@cutrer.us>
bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/ComponentFactory.java
bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Number.java [new file with mode: 0644]
bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Select.java [new file with mode: 0644]
bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/NumberTests.java [new file with mode: 0644]
bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/SelectTests.java [new file with mode: 0644]

index fbe97dd6c0c58d21a303ca956c0e62464f1da777..f502f45fda1fd25287d8c80a116d621e320225d9 100644 (file)
@@ -69,6 +69,10 @@ public class ComponentFactory {
                 return Light.create(componentConfiguration);
             case "lock":
                 return new Lock(componentConfiguration);
+            case "number":
+                return new Number(componentConfiguration);
+            case "select":
+                return new Select(componentConfiguration);
             case "sensor":
                 return new Sensor(componentConfiguration);
             case "switch":
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Number.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Number.java
new file mode 100644 (file)
index 0000000..a012796
--- /dev/null
@@ -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();
+    }
+}
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Select.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Select.java
new file mode 100644 (file)
index 0000000..1742789
--- /dev/null
@@ -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();
+    }
+}
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/NumberTests.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/NumberTests.java
new file mode 100644 (file)
index 0000000..4704f87
--- /dev/null
@@ -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);
+    }
+}
diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/SelectTests.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/SelectTests.java
new file mode 100644 (file)
index 0000000..64c6e23
--- /dev/null
@@ -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);
+    }
+}