]> git.basschouten.com Git - openhab-addons.git/commitdiff
[loxone] Sauna controller implementation (#11270)
authorPawel Pieczul <pieczul@gmail.com>
Wed, 17 Nov 2021 23:40:26 +0000 (00:40 +0100)
committerGitHub <noreply@github.com>
Wed, 17 Nov 2021 23:40:26 +0000 (00:40 +0100)
Signed-off-by: Pawel Pieczul <pieczul@gmail.com>
bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/controls/LxControl.java
bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/controls/LxControlFactory.java
bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/controls/LxControlSauna.java [new file with mode: 0644]
bundles/org.openhab.binding.loxone/src/test/java/org/openhab/binding/loxone/internal/controls/LxControlSaunaDoorTest.java [new file with mode: 0644]
bundles/org.openhab.binding.loxone/src/test/java/org/openhab/binding/loxone/internal/controls/LxControlSaunaDoorVaporizerTest.java [new file with mode: 0644]
bundles/org.openhab.binding.loxone/src/test/java/org/openhab/binding/loxone/internal/controls/LxControlSaunaTest.java [new file with mode: 0644]
bundles/org.openhab.binding.loxone/src/test/java/org/openhab/binding/loxone/internal/controls/LxControlTest.java
bundles/org.openhab.binding.loxone/src/test/resources/org/openhab/binding/loxone/internal/controls/LoxAPP3.json

index f16c04596fa3d8a8852311feec60c1fe1b851141..ffe8fff994090f4b6bf38363f27d695e4b10bc91 100644 (file)
@@ -30,6 +30,7 @@ import org.openhab.binding.loxone.internal.types.LxState;
 import org.openhab.binding.loxone.internal.types.LxUuid;
 import org.openhab.core.library.types.DecimalType;
 import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.PercentType;
 import org.openhab.core.library.types.StringType;
 import org.openhab.core.thing.Channel;
 import org.openhab.core.thing.ChannelUID;
@@ -130,6 +131,8 @@ public class LxControl {
         Map<String, String> outputs;
         Boolean presenceConnected;
         Integer connectedInputs;
+        Boolean hasVaporizer;
+        Boolean hasDoorSensor;
     }
 
     /**
@@ -586,6 +589,24 @@ public class LxControl {
         return null;
     }
 
+    /**
+     * Gets value of a state object of given name, if exists, and converts it to percent type value.
+     * Assumes the state value is between 0.0-100.0 which corresponds directly to 0-100 percent.
+     *
+     * @param name state name
+     * @return state value
+     */
+    State getStatePercentValue(String name) {
+        Double value = getStateDoubleValue(name);
+        if (value == null) {
+            return null;
+        }
+        if (value >= 0.0 && value <= 100.0) {
+            return new PercentType(value.intValue());
+        }
+        return UnDefType.UNDEF;
+    }
+
     /**
      * Gets text value of a state object of given name, if exists
      *
index 8348945406f67a50274fafe5acca1ba51597bb3a..0b31533c01735501a764536a9966c14026fc6ab0 100644 (file)
@@ -43,6 +43,7 @@ class LxControlFactory {
         add(new LxControlMeter.Factory());
         add(new LxControlPushbutton.Factory());
         add(new LxControlRadio.Factory());
+        add(new LxControlSauna.Factory());
         add(new LxControlSlider.Factory());
         add(new LxControlSwitch.Factory());
         add(new LxControlTextState.Factory());
diff --git a/bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/controls/LxControlSauna.java b/bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/controls/LxControlSauna.java
new file mode 100644 (file)
index 0000000..a8756b8
--- /dev/null
@@ -0,0 +1,191 @@
+/**
+ * Copyright (c) 2010-2021 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.loxone.internal.controls;
+
+import static org.openhab.binding.loxone.internal.LxBindingConstants.*;
+
+import java.io.IOException;
+
+import org.openhab.binding.loxone.internal.types.LxUuid;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.thing.type.ChannelTypeUID;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+
+/**
+ * Loxone Miniserver's Sauna
+ *
+ * @author Pawel Pieczul - initial contribution
+ *
+ */
+class LxControlSauna extends LxControl {
+
+    static class Factory extends LxControlInstance {
+        @Override
+        LxControl create(LxUuid uuid) {
+            return new LxControlSauna(uuid);
+        }
+
+        @Override
+        String getType() {
+            return "sauna";
+        }
+    }
+
+    private static final String STATE_ACTIVE = "active";
+    private static final String STATE_POWER_LEVEL = "power";
+    private static final String STATE_TEMP_ACTUAL = "tempactual";
+    private static final String STATE_TEMP_BENCH = "tempbench";
+    private static final String STATE_TEMP_TARGET = "temptarget";
+    private static final String STATE_FAN = "fan";
+    private static final String STATE_DRYING = "drying";
+    private static final String STATE_DOOR_CLOSED = "doorclosed";
+    private static final String STATE_ERROR = "error";
+    private static final String STATE_VAPOR_POWER_LEVEL = "vaporpower";
+    private static final String STATE_SAUNA_ERROR = "saunaerror";
+    private static final String STATE_TIMER = "timer";
+    private static final String STATE_TIMER_TOTAL = "timertotal";
+    private static final String STATE_OUT_OF_WATER = "lesswater";
+    private static final String STATE_HUMIDITY_ACTUAL = "humidityactual";
+    private static final String STATE_HUMIDITY_TARGET = "humiditytarget";
+    private static final String STATE_EVAPORATOR_MODE = "mode";
+
+    private static final String CMD_ON = "on";
+    private static final String CMD_OFF = "off";
+    private static final String CMD_FAN_ON = "fanon";
+    private static final String CMD_FAN_OFF = "fanoff";
+    private static final String CMD_SET_TEMP_TARGET = "temp/";
+    private static final String CMD_SET_HUMIDITY_TARGET = "humidity/";
+    private static final String CMD_SET_EVAPORATOR_MODE = "mode/";
+    private static final String CMD_NEXT_STATE = "pulse";
+    private static final String CMD_START_TIMER = "starttimer";
+
+    LxControlSauna(LxUuid uuid) {
+        super(uuid);
+    }
+
+    @Override
+    public void initialize(LxControlConfig config) {
+        super.initialize(config);
+        addChannel("Switch", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_SWITCH),
+                defaultChannelLabel + " / Active", "Sauna Active", tags, this::handleSaunaActivateCommands,
+                () -> getStateOnOffValue(STATE_ACTIVE));
+        addChannel("Number", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_RO_NUMBER),
+                defaultChannelLabel + " / Power", "Sauna Power Level", tags, null,
+                () -> getStatePercentValue(STATE_POWER_LEVEL));
+        addChannel("Number", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_RO_NUMBER),
+                defaultChannelLabel + " / Temperature / Actual", "Actual Temperature", tags, null,
+                () -> getStateDecimalValue(STATE_TEMP_ACTUAL));
+        addChannel("Number", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_RO_NUMBER),
+                defaultChannelLabel + " / Temperature / Bench", "Bench Temperature", tags, null,
+                () -> getStateDecimalValue(STATE_TEMP_BENCH));
+        addChannel("Number", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_NUMBER),
+                defaultChannelLabel + " / Temperature / Target", "Target Temperature", tags,
+                (cmd) -> handleSetNumberCommands(cmd, CMD_SET_TEMP_TARGET),
+                () -> getStateDecimalValue(STATE_TEMP_TARGET));
+        addChannel("Switch", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_SWITCH),
+                defaultChannelLabel + " / Fan", "Fan", tags, this::handleFanCommands,
+                () -> getStateOnOffValue(STATE_FAN));
+        addChannel("Switch", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_RO_SWITCH),
+                defaultChannelLabel + " / Drying", "Drying", tags, null, () -> getStateOnOffValue(STATE_DRYING));
+        if (details != null && details.hasDoorSensor != null && details.hasDoorSensor) {
+            addChannel("Switch", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_RO_SWITCH),
+                    defaultChannelLabel + " / Door Closed", "Door Closed", tags, null,
+                    () -> getStateOnOffValue(STATE_DOOR_CLOSED));
+        }
+        addChannel("Number", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_RO_NUMBER),
+                defaultChannelLabel + " / Error Code", "Error Code", tags, null, () -> getStateErrorValue());
+        addChannel("Number", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_RO_NUMBER),
+                defaultChannelLabel + " / Timer / Current", "Current Timer Value", tags, null,
+                () -> getStateDecimalValue(STATE_TIMER));
+        addChannel("Switch", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_SWITCH),
+                defaultChannelLabel + " / Timer / Trigger", "Start Timer", tags,
+                (cmd) -> handleTriggerCommands(cmd, CMD_START_TIMER), () -> OnOffType.OFF);
+        addChannel("Number", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_RO_NUMBER),
+                defaultChannelLabel + " / Timer / Total", "Total Timer Value", tags, null,
+                () -> getStateDecimalValue(STATE_TIMER_TOTAL));
+        if (details != null && details.hasVaporizer != null && details.hasVaporizer) {
+            addChannel("Number", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_RO_NUMBER),
+                    defaultChannelLabel + " / Evaporator / Power", "Evaporator Power Level", tags, null,
+                    () -> getStatePercentValue(STATE_VAPOR_POWER_LEVEL));
+            addChannel("Switch", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_RO_SWITCH),
+                    defaultChannelLabel + " / Evaporator / Out Of Water", "Evaporator Out Of Water", tags, null,
+                    () -> getStateOnOffValue(STATE_OUT_OF_WATER));
+            addChannel("Number", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_RO_NUMBER),
+                    defaultChannelLabel + " / Evaporator / Humidity / Actual", "Actual Humidity", tags, null,
+                    () -> getStateDecimalValue(STATE_HUMIDITY_ACTUAL));
+            addChannel("Number", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_NUMBER),
+                    defaultChannelLabel + " / Evaporator / Humidity / Target", "Target Humidity", tags,
+                    (cmd) -> handleSetNumberCommands(cmd, CMD_SET_HUMIDITY_TARGET),
+                    () -> getStateDecimalValue(STATE_HUMIDITY_TARGET));
+            addChannel("Number", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_NUMBER),
+                    defaultChannelLabel + " / Evaporator / Mode", "Evaporator Mode", tags, this::handleModeCommands,
+                    () -> getStateDecimalValue(STATE_EVAPORATOR_MODE));
+        }
+        addChannel("Switch", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_SWITCH),
+                defaultChannelLabel + " / Next State", "Trigger Next State", tags,
+                (cmd) -> handleTriggerCommands(cmd, CMD_NEXT_STATE), () -> OnOffType.OFF);
+    }
+
+    private void handleSaunaActivateCommands(Command command) throws IOException {
+        if (command instanceof OnOffType) {
+            if ((OnOffType) command == OnOffType.ON) {
+                sendAction(CMD_ON);
+            } else {
+                sendAction(CMD_OFF);
+            }
+        }
+    }
+
+    private void handleSetNumberCommands(Command command, String prefix) throws IOException {
+        if (command instanceof DecimalType) {
+            Double value = ((DecimalType) command).doubleValue();
+            sendAction(prefix + value.toString());
+        }
+    }
+
+    private void handleFanCommands(Command command) throws IOException {
+        if (command instanceof OnOffType) {
+            if ((OnOffType) command == OnOffType.ON) {
+                sendAction(CMD_FAN_ON);
+            } else {
+                sendAction(CMD_FAN_OFF);
+            }
+        }
+    }
+
+    private void handleTriggerCommands(Command command, String prefix) throws IOException {
+        if (command instanceof OnOffType && (OnOffType) command == OnOffType.ON) {
+            sendAction(prefix);
+        }
+    }
+
+    private void handleModeCommands(Command command) throws IOException {
+        if (command instanceof DecimalType) {
+            Double value = ((DecimalType) command).doubleValue();
+            // per API there are 7 evaporator modes selected with number 0-6
+            if (value % 1 == 0 && value >= 0.0 && value <= 6.0) {
+                sendAction(CMD_SET_EVAPORATOR_MODE + value.toString());
+            }
+        }
+    }
+
+    private State getStateErrorValue() {
+        Double val = getStateDoubleValue(STATE_ERROR);
+        if (val != null && val != 0.0) {
+            return getStateDecimalValue(STATE_SAUNA_ERROR);
+        }
+        return DecimalType.ZERO;
+    }
+}
diff --git a/bundles/org.openhab.binding.loxone/src/test/java/org/openhab/binding/loxone/internal/controls/LxControlSaunaDoorTest.java b/bundles/org.openhab.binding.loxone/src/test/java/org/openhab/binding/loxone/internal/controls/LxControlSaunaDoorTest.java
new file mode 100644 (file)
index 0000000..565b733
--- /dev/null
@@ -0,0 +1,56 @@
+/**
+ * Copyright (c) 2010-2021 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.loxone.internal.controls;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.openhab.core.library.types.OnOffType;
+
+/**
+ * Test class for (@link LxControlSauna} - version with door sensor no vaporizer
+ *
+ * @author Pawel Pieczul - initial contribution
+ *
+ */
+public class LxControlSaunaDoorTest extends LxControlSaunaTest {
+    @Override
+    @BeforeEach
+    public void setup() {
+        setupControl("17452951-02ae-1b6e-ffff266cf17271dc", "0b734138-037d-034e-ffff403fb0c34b9e",
+                "0fe650c2-0004-d446-ffff504f9410790f", "Sauna Controller No Vaporizer With Door Sensor");
+    }
+
+    @Override
+    @Test
+    public void testControlCreation() {
+        testControlCreation(LxControlSauna.class, 3, 0, 13, 13, 14);
+    }
+
+    @Override
+    @Test
+    public void testChannels() {
+        super.testChannels();
+        testChannel("Switch", DOOR_CLOSED_CHANNEL);
+    }
+
+    @Override
+    @Test
+    public void testDoorClosedChannel() {
+        for (int i = 0; i < 5; i++) {
+            changeLoxoneState("doorclosed", 0.0);
+            testChannelState(DOOR_CLOSED_CHANNEL, OnOffType.OFF);
+            changeLoxoneState("doorclosed", 1.0);
+            testChannelState(DOOR_CLOSED_CHANNEL, OnOffType.ON);
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.loxone/src/test/java/org/openhab/binding/loxone/internal/controls/LxControlSaunaDoorVaporizerTest.java b/bundles/org.openhab.binding.loxone/src/test/java/org/openhab/binding/loxone/internal/controls/LxControlSaunaDoorVaporizerTest.java
new file mode 100644 (file)
index 0000000..f544955
--- /dev/null
@@ -0,0 +1,123 @@
+/**
+ * Copyright (c) 2010-2021 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.loxone.internal.controls;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.PercentType;
+import org.openhab.core.types.UnDefType;
+
+/**
+ * Test class for (@link LxControlSauna} - version with vaporizer and door sensor
+ *
+ * @author Pawel Pieczul - initial contribution
+ *
+ */
+public class LxControlSaunaDoorVaporizerTest extends LxControlSaunaDoorTest {
+    @Override
+    @BeforeEach
+    public void setup() {
+        setupControl("17452951-02ae-1b6e-ffff266cf17271dd", "0b734138-037d-034e-ffff403fb0c34b9e",
+                "0fe650c2-0004-d446-ffff504f9410790f", "Sauna Controller With Vaporizer With Door Sensor");
+    }
+
+    @Override
+    @Test
+    public void testControlCreation() {
+        testControlCreation(LxControlSauna.class, 3, 0, 18, 18, 21);
+    }
+
+    @Override
+    @Test
+    public void testChannels() {
+        super.testChannels();
+        testChannel("Number", VAPOR_POWER_CHANNEL);
+        testChannel("Switch", OUT_OF_WATER_CHANNEL);
+        testChannel("Number", ACTUAL_HUMIDITY_CHANNEL);
+        testChannel("Number", TARGET_HUMIDITY_CHANNEL);
+        testChannel("Number", EVAPORATOR_MODE_CHANNEL);
+    }
+
+    @Override
+    @Test
+    public void vaporPowerChannel() {
+        for (Double i = 0.0; i <= 100.0; i += 1.0) {
+            changeLoxoneState("vaporpower", i);
+            testChannelState(VAPOR_POWER_CHANNEL, new PercentType(i.intValue()));
+        }
+        changeLoxoneState("vaporpower", -1.0);
+        testChannelState(VAPOR_POWER_CHANNEL, UnDefType.UNDEF);
+        changeLoxoneState("vaporpower", 100.1);
+        testChannelState(VAPOR_POWER_CHANNEL, UnDefType.UNDEF);
+    }
+
+    @Override
+    @Test
+    public void testOutOfWaterChannel() {
+        for (int i = 0; i < 5; i++) {
+            changeLoxoneState("lesswater", 0.0);
+            testChannelState(OUT_OF_WATER_CHANNEL, OnOffType.OFF);
+            changeLoxoneState("lesswater", 1.0);
+            testChannelState(OUT_OF_WATER_CHANNEL, OnOffType.ON);
+        }
+    }
+
+    @Override
+    @Test
+    public void testActualHumidityChannel() {
+        for (Double i = 0.0; i <= 100.0; i += 0.17) {
+            changeLoxoneState("humidityactual", i);
+            testChannelState(ACTUAL_HUMIDITY_CHANNEL, new DecimalType(i));
+        }
+    }
+
+    @Override
+    @Test
+    public void testTargetHumidityChannel() {
+        for (Double i = 0.0; i <= 100.0; i += 0.17) {
+            changeLoxoneState("humiditytarget", i);
+            testChannelState(TARGET_HUMIDITY_CHANNEL, new DecimalType(i));
+        }
+        for (Double i = 0.0; i <= 100.0; i += 0.13) {
+            executeCommand(TARGET_HUMIDITY_CHANNEL, new DecimalType(i));
+            testAction("humidity/" + i.toString());
+        }
+    }
+
+    @Override
+    @Test
+    public void testEvaporatorModelChannel() {
+        for (Double i = 0.0; i <= 6.0; i += 1.0) {
+            changeLoxoneState("mode", i);
+            testChannelState(EVAPORATOR_MODE_CHANNEL, new DecimalType(i));
+        }
+        for (Double i = -10.0; i < 0.0; i += 0.4) {
+            executeCommand(EVAPORATOR_MODE_CHANNEL, new DecimalType(i));
+            testAction(null);
+        }
+        for (Double i = 0.0; i < 6.0; i += 1.0) {
+            executeCommand(EVAPORATOR_MODE_CHANNEL, new DecimalType(i));
+            testAction("mode/" + i.toString());
+        }
+        for (Double i = 6.1; i < 15.0; i += 0.1) {
+            executeCommand(EVAPORATOR_MODE_CHANNEL, new DecimalType(i));
+            testAction(null);
+        }
+        for (Double i = 0.3; i < 6.0; i += 1.0) {
+            executeCommand(EVAPORATOR_MODE_CHANNEL, new DecimalType(i));
+            testAction(null);
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.loxone/src/test/java/org/openhab/binding/loxone/internal/controls/LxControlSaunaTest.java b/bundles/org.openhab.binding.loxone/src/test/java/org/openhab/binding/loxone/internal/controls/LxControlSaunaTest.java
new file mode 100644 (file)
index 0000000..e05711f
--- /dev/null
@@ -0,0 +1,238 @@
+/**
+ * Copyright (c) 2010-2021 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.loxone.internal.controls;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.PercentType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.types.UnDefType;
+
+/**
+ * Test class for (@link LxControlSauna} - version with no door sensor and no vaporizer
+ *
+ * @author Pawel Pieczul - initial contribution
+ *
+ */
+public class LxControlSaunaTest extends LxControlTest {
+    private static final String ACTIVE_CHANNEL = " / Active";
+    private static final String POWER_CHANNEL = " / Power";
+    private static final String TEMP_ACTUAL_CHANNEL = " / Temperature / Actual";
+    private static final String TEMP_BENCH_CHANNEL = " / Temperature / Bench";
+    private static final String TEMP_TARGET_CHANNEL = " / Temperature / Target";
+    private static final String FAN_CHANNEL = " / Fan";
+    private static final String DRYING_CHANNEL = " / Drying";
+    static final String DOOR_CLOSED_CHANNEL = " / Door Closed";
+    private static final String ERROR_CODE_CHANNEL = " / Error Code";
+    static final String VAPOR_POWER_CHANNEL = " / Evaporator / Power";
+    private static final String TIMER_CURRENT_CHANNEL = " / Timer / Current";
+    private static final String TIMER_TRIGGER_CHANNEL = " / Timer / Trigger";
+    private static final String TIMER_TOTAL_CHANNEL = " / Timer / Total";
+    static final String OUT_OF_WATER_CHANNEL = " / Evaporator / Out Of Water";
+    static final String ACTUAL_HUMIDITY_CHANNEL = " / Evaporator / Humidity / Actual";
+    static final String TARGET_HUMIDITY_CHANNEL = " / Evaporator / Humidity / Target";
+    static final String EVAPORATOR_MODE_CHANNEL = " / Evaporator / Mode";
+    private static final String NEXT_STATE_CHANNEL = " / Next State";
+
+    @BeforeEach
+    public void setup() {
+        setupControl("17452951-02ae-1b6e-ffff266cf17271db", "0b734138-037d-034e-ffff403fb0c34b9e",
+                "0fe650c2-0004-d446-ffff504f9410790f", "Sauna Controller No Vaporizer No Door Sensor");
+    }
+
+    @Test
+    public void testControlCreation() {
+        testControlCreation(LxControlSauna.class, 3, 0, 12, 12, 14);
+    }
+
+    @Test
+    public void testChannels() {
+        testChannel("Switch", ACTIVE_CHANNEL);
+        testChannel("Number", POWER_CHANNEL);
+        testChannel("Number", TEMP_ACTUAL_CHANNEL);
+        testChannel("Number", TEMP_BENCH_CHANNEL);
+        testChannel("Number", TEMP_TARGET_CHANNEL);
+        testChannel("Switch", FAN_CHANNEL);
+        testChannel("Switch", DRYING_CHANNEL);
+        testChannel("Number", ERROR_CODE_CHANNEL);
+        testChannel("Number", TIMER_CURRENT_CHANNEL);
+        testChannel("Switch", TIMER_TRIGGER_CHANNEL);
+        testChannel("Number", TIMER_TOTAL_CHANNEL);
+        testChannel("Switch", NEXT_STATE_CHANNEL);
+    }
+
+    @Test
+    public void testActiveChannel() {
+        for (int i = 0; i < 5; i++) {
+            changeLoxoneState("active", 0.0);
+            testChannelState(ACTIVE_CHANNEL, OnOffType.OFF);
+            changeLoxoneState("active", 1.0);
+            testChannelState(ACTIVE_CHANNEL, OnOffType.ON);
+        }
+        for (int i = 0; i < 5; i++) {
+            executeCommand(ACTIVE_CHANNEL, OnOffType.ON);
+            testAction("on");
+            executeCommand(ACTIVE_CHANNEL, DecimalType.ZERO);
+            testAction(null);
+            executeCommand(ACTIVE_CHANNEL, OnOffType.OFF);
+            testAction("off");
+            executeCommand(ACTIVE_CHANNEL, StringType.EMPTY);
+            testAction(null);
+        }
+    }
+
+    @Test
+    public void testPowerChannel() {
+        for (Double i = 0.0; i <= 100.0; i += 1.0) {
+            changeLoxoneState("power", i);
+            testChannelState(POWER_CHANNEL, new PercentType(i.intValue()));
+        }
+        changeLoxoneState("power", -1.0);
+        testChannelState(POWER_CHANNEL, UnDefType.UNDEF);
+        changeLoxoneState("power", 100.1);
+        testChannelState(POWER_CHANNEL, UnDefType.UNDEF);
+    }
+
+    @Test
+    public void testTempActualBenchChannels() {
+        for (Double i = -20.0; i <= 150.0; i += 0.37) {
+            changeLoxoneState("tempactual", i);
+            testChannelState(TEMP_ACTUAL_CHANNEL, new DecimalType(i));
+            changeLoxoneState("tempbench", i * 1.1);
+            testChannelState(TEMP_BENCH_CHANNEL, new DecimalType(i * 1.1));
+            changeLoxoneState("temptarget", i * 1.2);
+            testChannelState(TEMP_TARGET_CHANNEL, new DecimalType(i * 1.2));
+        }
+    }
+
+    @Test
+    public void testTempTargetSetCommand() {
+        for (Double i = 0.0; i <= 150.0; i += 0.37) {
+            executeCommand(TEMP_TARGET_CHANNEL, new DecimalType(i));
+            testAction("temp/" + i.toString());
+        }
+    }
+
+    @Test
+    public void testFanChannel() {
+        for (int i = 0; i < 5; i++) {
+            changeLoxoneState("fan", 0.0);
+            testChannelState(FAN_CHANNEL, OnOffType.OFF);
+            changeLoxoneState("fan", 1.0);
+            testChannelState(FAN_CHANNEL, OnOffType.ON);
+        }
+        for (int i = 0; i < 5; i++) {
+            executeCommand(FAN_CHANNEL, OnOffType.ON);
+            testAction("fanon");
+            executeCommand(FAN_CHANNEL, DecimalType.ZERO);
+            testAction(null);
+            executeCommand(FAN_CHANNEL, OnOffType.OFF);
+            testAction("fanoff");
+            executeCommand(FAN_CHANNEL, StringType.EMPTY);
+            testAction(null);
+        }
+    }
+
+    @Test
+    public void testDryingChannel() {
+        for (int i = 0; i < 5; i++) {
+            changeLoxoneState("drying", 0.0);
+            testChannelState(DRYING_CHANNEL, OnOffType.OFF);
+            changeLoxoneState("drying", 1.0);
+            testChannelState(DRYING_CHANNEL, OnOffType.ON);
+        }
+    }
+
+    @Test
+    public void testDoorClosedChannel() {
+        testNoChannel(DOOR_CLOSED_CHANNEL);
+    }
+
+    @Test
+    public void testErrorCodeChannel() {
+        for (Double i = 0.0; i < 10.0; i += 1.0) {
+            changeLoxoneState("saunaerror", i);
+            changeLoxoneState("error", 0.0);
+            testChannelState(ERROR_CODE_CHANNEL, DecimalType.ZERO);
+            changeLoxoneState("error", 1.0);
+            testChannelState(ERROR_CODE_CHANNEL, new DecimalType(i));
+        }
+    }
+
+    @Test
+    public void testTimerCurrentTotalChannels() {
+        for (Double i = 0.0; i <= 150.0; i += 0.21) {
+            changeLoxoneState("timer", i);
+            testChannelState(TIMER_CURRENT_CHANNEL, new DecimalType(i));
+            changeLoxoneState("timertotal", i * 1.3);
+            testChannelState(TIMER_TOTAL_CHANNEL, new DecimalType(i * 1.3));
+        }
+    }
+
+    @Test
+    public void testTimerTriggerChannel() {
+        for (int i = 0; i <= 10; i++) {
+            executeCommand(TIMER_TRIGGER_CHANNEL, DecimalType.ZERO);
+            testAction(null);
+            testChannelState(TIMER_TRIGGER_CHANNEL, OnOffType.OFF);
+            executeCommand(TIMER_TRIGGER_CHANNEL, OnOffType.ON);
+            testAction("starttimer");
+            testChannelState(TIMER_TRIGGER_CHANNEL, OnOffType.OFF);
+            executeCommand(TIMER_TRIGGER_CHANNEL, OnOffType.OFF);
+            testAction(null);
+            testChannelState(TIMER_TRIGGER_CHANNEL, OnOffType.OFF);
+        }
+    }
+
+    @Test
+    public void vaporPowerChannel() {
+        testNoChannel(VAPOR_POWER_CHANNEL);
+    }
+
+    @Test
+    public void testOutOfWaterChannel() {
+        testNoChannel(OUT_OF_WATER_CHANNEL);
+    }
+
+    @Test
+    public void testActualHumidityChannel() {
+        testNoChannel(ACTUAL_HUMIDITY_CHANNEL);
+    }
+
+    @Test
+    public void testTargetHumidityChannel() {
+        testNoChannel(TARGET_HUMIDITY_CHANNEL);
+    }
+
+    @Test
+    public void testEvaporatorModelChannel() {
+        testNoChannel(EVAPORATOR_MODE_CHANNEL);
+    }
+
+    @Test
+    public void testNextStateTriggerChannel() {
+        for (int i = 0; i <= 10; i++) {
+            executeCommand(NEXT_STATE_CHANNEL, DecimalType.ZERO);
+            testAction(null);
+            testChannelState(NEXT_STATE_CHANNEL, OnOffType.OFF);
+            executeCommand(NEXT_STATE_CHANNEL, OnOffType.ON);
+            testAction("pulse");
+            testChannelState(NEXT_STATE_CHANNEL, OnOffType.OFF);
+            executeCommand(NEXT_STATE_CHANNEL, OnOffType.OFF);
+            testAction(null);
+            testChannelState(NEXT_STATE_CHANNEL, OnOffType.OFF);
+        }
+    }
+}
index 38bde289a46e6bca3806ff54f9702d89c58155ec..9af5181450a233bb8de553e7945575eacfac053a 100644 (file)
@@ -147,6 +147,13 @@ class LxControlTest {
         testChannel(itemType, namePostFix, null, null, null, null, null, null, null);
     }
 
+    void testNoChannel(String namePostFix) {
+        LxControl ctrl = getControl(controlUuid);
+        assertNotNull(ctrl);
+        Channel c = getChannel(getExpectedName(ctrl.getLabel(), ctrl.getRoom().getName(), namePostFix), ctrl);
+        assertNull(c);
+    }
+
     void testChannel(String itemType, String namePostFix, Set<String> tags) {
         testChannel(itemType, namePostFix, null, null, null, null, null, null, tags);
     }
@@ -249,8 +256,10 @@ class LxControlTest {
     private Channel getChannel(String name, LxControl c) {
         List<Channel> channels = c.getChannels();
         List<Channel> filtered = channels.stream().filter(a -> name.equals(a.getLabel())).collect(Collectors.toList());
-        assertEquals(1, filtered.size());
-        return filtered.get(0);
+        if (filtered.size() == 1) {
+            return filtered.get(0);
+        }
+        return null;
     }
 
     private <T> long numberOfControls(Class<T> c) {
index a8ca82f37e219091712c3f826773498bbf3acf92..61e37d36cbdac565bf71221b6b12768d0e579c71 100644 (file)
                                        }
                                }
                        }
-               }
-       },
+               },
+        "17452951-02ae-1b6e-ffff266cf17271db": {
+            "name": "Sauna Controller No Vaporizer No Door Sensor",
+            "type": "Sauna",
+            "uuidAction": "17452951-02ae-1b6e-ffff266cf17271db",
+            "room": "0b734138-037d-034e-ffff403fb0c34b9e",
+            "cat": "0fe650c2-0004-d446-ffff504f9410790f",
+            "defaultRating": 0,
+            "isFavorite": false,
+            "isSecured": false,
+            "details": {
+                "jLockable": true,
+                "hasVaporizer": false,
+                "hasDoorSensor": false
+            },
+            "states": {
+                "jLocked": "97452951-02ae-1b57-ffffe29abab51e83",
+                "power": "17452951-02ae-1b62-ffffe29abab51e83",
+                "tempActual": "17452951-02ae-1b51-ffffe29abab51e83",
+                "tempBench": "17452951-02ae-1b55-ffffe29abab51e83",
+                "tempTarget": "17452951-02ae-1b6b-ffffe29abab51e83",
+                "fan": "17452951-02ae-1b67-ffffe29abab51e83",
+                "drying": "17452951-02ae-1b69-ffffe29abab51e83",
+                "doorClosed": "17452951-02ae-1b54-ffffe29abab51e83",
+                "presence": "17452951-02ae-1b56-ffffe29abab51e83",
+                "error": "17452951-02ae-1b6a-ffffe29abab51e83",
+                "saunaError": "17452951-02ae-1b4e-ffffe29abab51e83",
+                "timer": "17452951-02ae-1b68-ffffe29abab51e83",
+                "active": "17452951-02ae-1b66-ffffe29abab51e83",
+                "timerTotal": "17452951-02ae-1b5c-ffffe29abab51e83"
+            }
+        },
+        "17452951-02ae-1b6e-ffff266cf17271dc": {
+            "name": "Sauna Controller No Vaporizer With Door Sensor",
+            "type": "Sauna",
+            "uuidAction": "17452951-02ae-1b6e-ffff266cf17271dc",
+            "room": "0b734138-037d-034e-ffff403fb0c34b9e",
+            "cat": "0fe650c2-0004-d446-ffff504f9410790f",
+            "defaultRating": 0,
+            "isFavorite": false,
+            "isSecured": false,
+            "details": {
+                "jLockable": true,
+                "hasVaporizer": false,
+                "hasDoorSensor": true
+            },
+            "states": {
+                "jLocked": "97452951-02ae-1b57-ffffe29abab51e84",
+                "power": "17452951-02ae-1b62-ffffe29abab51e84",
+                "tempActual": "17452951-02ae-1b51-ffffe29abab51e84",
+                "tempBench": "17452951-02ae-1b55-ffffe29abab51e84",
+                "tempTarget": "17452951-02ae-1b6b-ffffe29abab51e84",
+                "fan": "17452951-02ae-1b67-ffffe29abab51e84",
+                "drying": "17452951-02ae-1b69-ffffe29abab51e84",
+                "doorClosed": "17452951-02ae-1b54-ffffe29abab51e84",
+                "presence": "17452951-02ae-1b56-ffffe29abab51e84",
+                "error": "17452951-02ae-1b6a-ffffe29abab51e84",
+                "saunaError": "17452951-02ae-1b4e-ffffe29abab51e84",
+                "timer": "17452951-02ae-1b68-ffffe29abab51e84",
+                "active": "17452951-02ae-1b66-ffffe29abab51e84",
+                "timerTotal": "17452951-02ae-1b5c-ffffe29abab51e84"
+            }
+        },
+        "17452951-02ae-1b6e-ffff266cf17271dd": {
+            "name": "Sauna Controller With Vaporizer With Door Sensor",
+            "type": "Sauna",
+            "uuidAction": "17452951-02ae-1b6e-ffff266cf17271dd",
+            "room": "0b734138-037d-034e-ffff403fb0c34b9e",
+            "cat": "0fe650c2-0004-d446-ffff504f9410790f",
+            "defaultRating": 0,
+            "isFavorite": false,
+            "isSecured": false,
+            "details": {
+                "jLockable": true,
+                "hasVaporizer": true,
+                "hasDoorSensor": true
+            },
+            "states": {
+                "jLocked": "97452951-02ae-1b57-ffffe29abab51e85",
+                "power": "17452951-02ae-1b62-ffffe29abab51e85",
+                "tempActual": "17452951-02ae-1b51-ffffe29abab51e85",
+                "tempBench": "17452951-02ae-1b55-ffffe29abab51e85",
+                "tempTarget": "17452951-02ae-1b6b-ffffe29abab51e85",
+                "fan": "17452951-02ae-1b67-ffffe29abab51e85",
+                "drying": "17452951-02ae-1b69-ffffe29abab51e85",
+                "doorClosed": "17452951-02ae-1b54-ffffe29abab51e85",
+                "presence": "17452951-02ae-1b56-ffffe29abab51e85",
+                "error": "17452951-02ae-1b6a-ffffe29abab51e85",
+                "vaporPower": "17eb161f-0350-6d90-ffff292cf0ed07b9",
+                "saunaError": "17452951-02ae-1b4e-ffffe29abab51e85",
+                "tempAndHumidity": "17eb161f-0350-6d6d-ffff292cf0ed07b9",
+                "ready": "17eb161f-0350-6d6e-ffff292cf0ed07b9",
+                "timer": "17452951-02ae-1b68-ffffe29abab51e85",
+                "active": "17452951-02ae-1b66-ffffe29abab51e85",
+                "lessWater": "17eb161f-0350-6d80-ffff292cf0ed07b9",
+                "humidityActual": "17eb161f-0350-6d7a-ffff292cf0ed07b9",
+                "humidityTarget": "17eb161f-0350-6d99-ffff292cf0ed07b9",
+                "mode": "17eb161f-0350-6d96-ffff292cf0ed07b9",
+                "timerTotal": "17452951-02ae-1b5c-ffffe29abab51e85"
+            }
+        }        
+    },
        "weatherServer": {
                "states": {
                        "actual": "0b734139-0012-04f4-ffff403fb0c34b9e",