diff --git a/bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/controls/LxControl.java b/bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/controls/LxControl.java index f16c04596fa3d..ffe8fff994090 100644 --- a/bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/controls/LxControl.java +++ b/bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/controls/LxControl.java @@ -30,6 +30,7 @@ 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 @@ class LxControlDetails { Map outputs; Boolean presenceConnected; Integer connectedInputs; + Boolean hasVaporizer; + Boolean hasDoorSensor; } /** @@ -586,6 +589,24 @@ State getStateDecimalValue(String name) { 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 * diff --git a/bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/controls/LxControlFactory.java b/bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/controls/LxControlFactory.java index 8348945406f67..0b31533c01735 100644 --- a/bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/controls/LxControlFactory.java +++ b/bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/controls/LxControlFactory.java @@ -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 index 0000000000000..a8756b8e791f8 --- /dev/null +++ b/bundles/org.openhab.binding.loxone/src/main/java/org/openhab/binding/loxone/internal/controls/LxControlSauna.java @@ -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 index 0000000000000..565b7330c6752 --- /dev/null +++ b/bundles/org.openhab.binding.loxone/src/test/java/org/openhab/binding/loxone/internal/controls/LxControlSaunaDoorTest.java @@ -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 index 0000000000000..f54495536e2b8 --- /dev/null +++ b/bundles/org.openhab.binding.loxone/src/test/java/org/openhab/binding/loxone/internal/controls/LxControlSaunaDoorVaporizerTest.java @@ -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 index 0000000000000..e05711ff3004f --- /dev/null +++ b/bundles/org.openhab.binding.loxone/src/test/java/org/openhab/binding/loxone/internal/controls/LxControlSaunaTest.java @@ -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); + } + } +} diff --git a/bundles/org.openhab.binding.loxone/src/test/java/org/openhab/binding/loxone/internal/controls/LxControlTest.java b/bundles/org.openhab.binding.loxone/src/test/java/org/openhab/binding/loxone/internal/controls/LxControlTest.java index 38bde289a46e6..9af5181450a23 100644 --- a/bundles/org.openhab.binding.loxone/src/test/java/org/openhab/binding/loxone/internal/controls/LxControlTest.java +++ b/bundles/org.openhab.binding.loxone/src/test/java/org/openhab/binding/loxone/internal/controls/LxControlTest.java @@ -147,6 +147,13 @@ void testChannel(String itemType, String namePostFix) { 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 tags) { testChannel(itemType, namePostFix, null, null, null, null, null, null, tags); } @@ -249,8 +256,10 @@ void testSubControl(Type type, String name) { private Channel getChannel(String name, LxControl c) { List channels = c.getChannels(); List 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 long numberOfControls(Class c) { diff --git a/bundles/org.openhab.binding.loxone/src/test/resources/org/openhab/binding/loxone/internal/controls/LoxAPP3.json b/bundles/org.openhab.binding.loxone/src/test/resources/org/openhab/binding/loxone/internal/controls/LoxAPP3.json index a8ca82f37e219..61e37d36cbdac 100644 --- a/bundles/org.openhab.binding.loxone/src/test/resources/org/openhab/binding/loxone/internal/controls/LoxAPP3.json +++ b/bundles/org.openhab.binding.loxone/src/test/resources/org/openhab/binding/loxone/internal/controls/LoxAPP3.json @@ -851,8 +851,108 @@ } } } - } - }, + }, + "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",