Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[loxone] Sauna controller implementation #11270

Merged
merged 1 commit into from
Nov 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -130,6 +131,8 @@ class LxControlDetails {
Map<String, String> outputs;
Boolean presenceConnected;
Integer connectedInputs;
Boolean hasVaporizer;
Boolean hasDoorSensor;
}

/**
Expand Down Expand Up @@ -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
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Loading