[loxone] Implementation of EIB Dimmer (#10585)

Signed-off-by: Pawel Pieczul <pieczul@gmail.com>
This commit is contained in:
Pawel Pieczul 2021-05-09 19:32:48 +02:00 committed by GitHub
parent d674814440
commit f652e329f7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 316 additions and 126 deletions

View File

@ -88,7 +88,7 @@ The binding supports the following authentication methods, which are selected au
| Method | Miniserver Firmware | Authentication | Encryption | Requirements | | Method | Miniserver Firmware | Authentication | Encryption | Requirements |
|-------------|---------------------|--------------------------------------------------------------------------------|------------|-------------------------------------------------------| |-------------|---------------------|--------------------------------------------------------------------------------|------------|-------------------------------------------------------|
| Hash-based | 8.x | HMAC-SHA1 hash on user and password | None | None | | Hash-based | 8.x | HMAC-SHA1 hash on user and password | None | None |
| Token-based | 9.x | Token acquired on the first connection and used later instead of the password. | AES-256 | JRE must have unrestricted security policy configured | | Token-based | From 9.x | Token acquired on the first connection and used later instead of the password. | AES-256 | JRE must have unrestricted security policy configured |
For the token-based authentication, the password is required only for the first login and acquiring the token. After the token is acquired, the password is cleared in the binding configuration. For the token-based authentication, the password is required only for the first login and acquiring the token. After the token is acquired, the password is cleared in the binding configuration.
@ -121,7 +121,8 @@ Currently supported controls are presented in the table below.
| | | `String` - list of alarm sensors separated with `|` | Read-only channel | | | | `String` - list of alarm sensors separated with `|` | Read-only channel |
| | | `Switch` - acknowledge the alarm - pushbutton | `OnOffType.ON` - acknowledge alarm | | | | `Switch` - acknowledge the alarm - pushbutton | `OnOffType.ON` - acknowledge alarm |
| ColorPickerV2 | [RGBW 24v Dimmer Tree](https://www.loxone.com/enen/kb/rgbw-24v-dimmer-tree/) | `Color` | `HSBType` - sets the color of the light, `DecimalType` and `PercentType` - sets the brightness, `IncreaseDecreaseType.*` - increases/decreases the brightness, `OnOffType.*` - switches light on/off | | ColorPickerV2 | [RGBW 24v Dimmer Tree](https://www.loxone.com/enen/kb/rgbw-24v-dimmer-tree/) | `Color` | `HSBType` - sets the color of the light, `DecimalType` and `PercentType` - sets the brightness, `IncreaseDecreaseType.*` - increases/decreases the brightness, `OnOffType.*` - switches light on/off |
| Dimmer | [Dimmer](https://www.loxone.com/enen/kb/dimmer/) | `Dimmer` | `OnOffType.*`, `PercentType` | | Dimmer | [Dimmer](https://www.loxone.com/enen/kb/dimmer/) | `Dimmer` | `OnOffType.*`, `PercentType`, `IncreaseDecreaseType.*` |
| EIBDimmer | EIB Dimmer (undocumented) | `Dimmer` | `OnOffType.*`, `PercentType`, `IncreaseDecreaseType.*` |
| InfoOnlyAnalog | Analog [virtual inputs](https://www.loxone.com/enen/kb/virtual-inputs-outputs/) (virtual state) | `Number` | Read-only channel | | InfoOnlyAnalog | Analog [virtual inputs](https://www.loxone.com/enen/kb/virtual-inputs-outputs/) (virtual state) | `Number` | Read-only channel |
| InfoOnlyDigital | Digital [virtual inputs](https://www.loxone.com/enen/kb/virtual-inputs-outputs/) (virtual state) | `String` | Read-only channel | | InfoOnlyDigital | Digital [virtual inputs](https://www.loxone.com/enen/kb/virtual-inputs-outputs/) (virtual state) | `String` | Read-only channel |
| IRoomControllerV2 | [Intelligent Room Controller V2](https://www.loxone.com/enen/kb/irc-v2/) | `Number` - active mode | Read-only channel | | IRoomControllerV2 | [Intelligent Room Controller V2](https://www.loxone.com/enen/kb/irc-v2/) | `Number` - active mode | Read-only channel |

View File

@ -12,18 +12,7 @@
*/ */
package org.openhab.binding.loxone.internal.controls; 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.LxCategory;
import org.openhab.binding.loxone.internal.types.LxTags;
import org.openhab.binding.loxone.internal.types.LxUuid; import org.openhab.binding.loxone.internal.types.LxUuid;
import org.openhab.core.library.types.IncreaseDecreaseType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.openhab.core.types.Command;
/** /**
* A dimmer type of control on Loxone Miniserver. * A dimmer type of control on Loxone Miniserver.
@ -36,7 +25,7 @@ import org.openhab.core.types.Command;
* @author Stephan Brunner - initial contribution * @author Stephan Brunner - initial contribution
* *
*/ */
class LxControlDimmer extends LxControl { class LxControlDimmer extends LxControlEIBDimmer {
static class Factory extends LxControlInstance { static class Factory extends LxControlInstance {
@Override @Override
@ -51,118 +40,28 @@ class LxControlDimmer extends LxControl {
} }
/** /**
* States * States additionally to EIBDimmer
*/ */
private static final String STATE_POSITION = "position";
private static final String STATE_MIN = "min"; private static final String STATE_MIN = "min";
private static final String STATE_MAX = "max"; private static final String STATE_MAX = "max";
private static final String STATE_STEP = "step"; private static final String STATE_STEP = "step";
/**
* Command string used to set the dimmer ON
*/
private static final String CMD_ON = "On";
/**
* Command string used to set the dimmer to OFF
*/
private static final String CMD_OFF = "Off";
private LxControlDimmer(LxUuid uuid) { private LxControlDimmer(LxUuid uuid) {
super(uuid); super(uuid);
} }
@Override @Override
public void initialize(LxControlConfig config) { Double getMin() {
super.initialize(config); return getStateDoubleValue(STATE_MIN);
LxCategory category = getCategory();
if (category != null && category.getType() == LxCategory.CategoryType.LIGHTS) {
tags.addAll(LxTags.LIGHTING);
}
addChannel("Dimmer", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_DIMMER), defaultChannelLabel,
"Dimmer", tags, this::handleCommands, this::getChannelState);
} }
private void handleCommands(Command command) throws IOException { @Override
if (command instanceof OnOffType) { Double getMax() {
if (command == OnOffType.ON) { return getStateDoubleValue(STATE_MAX);
sendAction(CMD_ON);
} else {
sendAction(CMD_OFF);
}
} else if (command instanceof PercentType) {
PercentType percentCmd = (PercentType) command;
setPosition(percentCmd.doubleValue());
} else if (command instanceof IncreaseDecreaseType) {
Double value = getStateDoubleValue(STATE_POSITION);
Double min = getStateDoubleValue(STATE_MIN);
Double max = getStateDoubleValue(STATE_MAX);
Double step = getStateDoubleValue(STATE_STEP);
if (value != null && max != null && min != null && step != null && min >= 0 && max >= 0 && max > min) {
if ((IncreaseDecreaseType) command == IncreaseDecreaseType.INCREASE) {
value += step;
if (value > max) {
value = max;
}
} else {
value -= step;
if (value < min) {
value = min;
}
}
sendAction(value.toString());
}
}
} }
private PercentType getChannelState() { @Override
Double value = mapLoxoneToOH(getStateDoubleValue(STATE_POSITION)); Double getStep() {
if (value != null && value >= 0 && value <= 100) { return getStateDoubleValue(STATE_STEP);
return new PercentType(value.intValue());
}
return null;
}
/**
* Sets the current position of the dimmer
*
* @param position position to move to (0-100, 0 - full off, 100 - full on)
* @throws IOException error communicating with the Miniserver
*/
private void setPosition(Double position) throws IOException {
Double loxonePosition = mapOHToLoxone(position);
if (loxonePosition != null) {
sendAction(loxonePosition.toString());
}
}
private Double mapLoxoneToOH(Double loxoneValue) {
if (loxoneValue != null) {
// 0 means turn dimmer off, any value above zero should be mapped from min-max range
if (Double.compare(loxoneValue, 0.0) == 0) {
return 0.0;
}
Double max = getStateDoubleValue(STATE_MAX);
Double min = getStateDoubleValue(STATE_MIN);
if (max != null && min != null && max > min && min >= 0 && max >= 0) {
return 100 * (loxoneValue - min) / (max - min);
}
}
return null;
}
private Double mapOHToLoxone(Double ohValue) {
if (ohValue != null) {
// 0 means turn dimmer off, any value above zero should be mapped to min-max range
if (Double.compare(ohValue, 0.0) == 0) {
return 0.0;
}
Double max = getStateDoubleValue(STATE_MAX);
Double min = getStateDoubleValue(STATE_MIN);
if (max != null && min != null) {
double value = min + ohValue * (max - min) / 100;
return value; // no rounding to integer value is needed as loxone is accepting floating point values
}
}
return null;
} }
} }

View File

@ -0,0 +1,178 @@
/**
* 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.LxCategory;
import org.openhab.binding.loxone.internal.types.LxTags;
import org.openhab.binding.loxone.internal.types.LxUuid;
import org.openhab.core.library.types.IncreaseDecreaseType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.openhab.core.types.Command;
/**
* An EIB dimmer type of control on Loxone Miniserver.
* <p>
* This control is absent in the API documentation. It looks like it behaves like a normal Dimmer, but it is missing the
* information about min, max and step values.
*
* @author Pawel Pieczul - initial contribution
*
*/
class LxControlEIBDimmer extends LxControl {
static class Factory extends LxControlInstance {
@Override
LxControl create(LxUuid uuid) {
return new LxControlEIBDimmer(uuid);
}
@Override
String getType() {
return "eibdimmer";
}
}
/**
* States
*/
private static final String STATE_POSITION = "position";
private static final Double DEFAULT_MIN = 0.0;
private static final Double DEFAULT_MAX = 100.0;
private static final Double DEFAULT_STEP = 5.0;
/**
* Command string used to set the dimmer ON
*/
private static final String CMD_ON = "On";
/**
* Command string used to set the dimmer to OFF
*/
private static final String CMD_OFF = "Off";
LxControlEIBDimmer(LxUuid uuid) {
super(uuid);
}
@Override
public void initialize(LxControlConfig config) {
super.initialize(config);
LxCategory category = getCategory();
if (category != null && category.getType() == LxCategory.CategoryType.LIGHTS) {
tags.addAll(LxTags.LIGHTING);
}
addChannel("Dimmer", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_DIMMER), defaultChannelLabel,
"Dimmer", tags, this::handleCommands, this::getChannelState);
}
Double getMin() {
return DEFAULT_MIN;
}
Double getMax() {
return DEFAULT_MAX;
}
Double getStep() {
return DEFAULT_STEP;
}
private void handleCommands(Command command) throws IOException {
if (command instanceof OnOffType) {
if (command == OnOffType.ON) {
sendAction(CMD_ON);
} else {
sendAction(CMD_OFF);
}
} else if (command instanceof PercentType) {
PercentType percentCmd = (PercentType) command;
setPosition(percentCmd.doubleValue());
} else if (command instanceof IncreaseDecreaseType) {
Double value = getStateDoubleValue(STATE_POSITION);
Double min = getMin();
Double max = getMax();
Double step = getStep();
if (value != null && max != null && min != null && step != null && min >= 0 && max >= 0 && max > min) {
if ((IncreaseDecreaseType) command == IncreaseDecreaseType.INCREASE) {
value += step;
if (value > max) {
value = max;
}
} else {
value -= step;
if (value < min) {
value = min;
}
}
sendAction(value.toString());
}
}
}
private PercentType getChannelState() {
Double value = mapLoxoneToOH(getStateDoubleValue(STATE_POSITION));
if (value != null && value >= 0 && value <= 100) {
return new PercentType(value.intValue());
}
return null;
}
/**
* Sets the current position of the dimmer
*
* @param position position to move to (0-100, 0 - full off, 100 - full on)
* @throws IOException error communicating with the Miniserver
*/
private void setPosition(Double position) throws IOException {
Double loxonePosition = mapOHToLoxone(position);
if (loxonePosition != null) {
sendAction(loxonePosition.toString());
}
}
private Double mapLoxoneToOH(Double loxoneValue) {
if (loxoneValue != null) {
// 0 means turn dimmer off, any value above zero should be mapped from min-max range
if (Double.compare(loxoneValue, 0.0) == 0) {
return 0.0;
}
Double max = getMax();
Double min = getMin();
if (max != null && min != null && max > min && min >= 0 && max >= 0) {
return 100 * (loxoneValue - min) / (max - min);
}
}
return null;
}
private Double mapOHToLoxone(Double ohValue) {
if (ohValue != null) {
// 0 means turn dimmer off, any value above zero should be mapped to min-max range
if (Double.compare(ohValue, 0.0) == 0) {
return 0.0;
}
Double max = getMax();
Double min = getMin();
if (max != null && min != null) {
double value = min + ohValue * (max - min) / 100;
return value; // no rounding to integer value is needed as loxone is accepting floating point values
}
}
return null;
}
}

View File

@ -31,6 +31,7 @@ class LxControlFactory {
add(new LxControlAlarm.Factory()); add(new LxControlAlarm.Factory());
add(new LxControlColorPickerV2.Factory()); add(new LxControlColorPickerV2.Factory());
add(new LxControlDimmer.Factory()); add(new LxControlDimmer.Factory());
add(new LxControlEIBDimmer.Factory());
add(new LxControlInfoOnlyAnalog.Factory()); add(new LxControlInfoOnlyAnalog.Factory());
add(new LxControlInfoOnlyDigital.Factory()); add(new LxControlInfoOnlyDigital.Factory());
add(new LxControlIRoomControllerV2.Factory()); add(new LxControlIRoomControllerV2.Factory());

View File

@ -27,8 +27,6 @@ import org.openhab.core.types.Command;
import org.openhab.core.types.State; import org.openhab.core.types.State;
import org.openhab.core.types.StateDescriptionFragment; import org.openhab.core.types.StateDescriptionFragment;
import org.openhab.core.types.StateDescriptionFragmentBuilder; import org.openhab.core.types.StateDescriptionFragmentBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** /**
* An Intelligent Room Controller V2. * An Intelligent Room Controller V2.
@ -73,8 +71,6 @@ class LxControlIRoomControllerV2 extends LxControl {
private static final String CMD_SET_ABSENT_MAX_TEMPERATURE = "setAbsentMaxTemperature/"; private static final String CMD_SET_ABSENT_MAX_TEMPERATURE = "setAbsentMaxTemperature/";
private static final String CMD_SET_MANUAL_TEMPERATURE = "setManualTemperature/"; private static final String CMD_SET_MANUAL_TEMPERATURE = "setManualTemperature/";
private final Logger logger = LoggerFactory.getLogger(LxControlIRoomControllerV2.class);
private LxControlIRoomControllerV2(LxUuid uuid) { private LxControlIRoomControllerV2(LxUuid uuid) {
super(uuid); super(uuid);
} }

View File

@ -57,11 +57,11 @@ public class LxCategory extends LxContainer {
public CategoryType getType() { public CategoryType getType() {
if (catType == null && type != null) { if (catType == null && type != null) {
String tl = type.toLowerCase(); String tl = type.toLowerCase();
if (tl.equals("lights")) { if ("lights".equals(tl)) {
catType = CategoryType.LIGHTS; catType = CategoryType.LIGHTS;
} else if (tl.equals("shading")) { } else if ("shading".equals(tl)) {
catType = CategoryType.SHADING; catType = CategoryType.SHADING;
} else if (tl.equals("indoortemperature")) { } else if ("indoortemperature".equals(tl)) {
catType = CategoryType.TEMPERATURE; catType = CategoryType.TEMPERATURE;
} else { } else {
catType = CategoryType.UNDEFINED; catType = CategoryType.UNDEFINED;

View File

@ -47,7 +47,7 @@ public class LxControlAlarmNoPresenceTest extends LxControlTest {
static final String SENSORS_CHANNEL = " / Sensors"; static final String SENSORS_CHANNEL = " / Sensors";
static final String QUIT_CHANNEL = " / Acknowledge"; static final String QUIT_CHANNEL = " / Acknowledge";
private static final String numberChannels[] = { NEXT_LEVEL_CHANNEL, NEXT_LEVEL_DELAY_CHANNEL, private static final String NUMBER_CHANNELS[] = { NEXT_LEVEL_CHANNEL, NEXT_LEVEL_DELAY_CHANNEL,
NEXT_LEVEL_DELAY_TOTAL_CHANNEL, LEVEL_CHANNEL, ARMED_DELAY_CHANNEL, ARMED_TOTAL_DELAY_CHANNEL }; NEXT_LEVEL_DELAY_TOTAL_CHANNEL, LEVEL_CHANNEL, ARMED_DELAY_CHANNEL, ARMED_TOTAL_DELAY_CHANNEL };
@BeforeEach @BeforeEach
@ -175,7 +175,7 @@ public class LxControlAlarmNoPresenceTest extends LxControlTest {
private void testNumberChannel(String channel, String state) { private void testNumberChannel(String channel, String state) {
Map<String, State> states = new HashMap<>(); Map<String, State> states = new HashMap<>();
for (String s : numberChannels) { for (String s : NUMBER_CHANNELS) {
states.put(s, getChannelState(s)); states.put(s, getChannelState(s));
} }
for (Double i = -100.0; i <= 100.0; i += 2.341) { for (Double i = -100.0; i <= 100.0; i += 2.341) {

View File

@ -0,0 +1,105 @@
/**
* 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.IncreaseDecreaseType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.StopMoveType;
/**
* Test class for (@link LxControlDimmer}
*
* @author Pawel Pieczul - initial contribution
*
*/
public class LxControlEIBDimmerTest extends LxControlTest {
@BeforeEach
public void setup() {
setupControl("faa30f5c-4b4f-11e2-8928b8ba17ef51ee", "0b734138-037d-034e-ffff403fb0c34b9e",
"0fe650c2-0004-d446-ffff504f9410790f", "Kitchen Dimmer");
}
@Test
public void testControlCreation() {
testControlCreation(LxControlDimmer.class, 1, 0, 1, 1, 1);
}
@Test
public void testChannels() {
testChannel("Dimmer");
}
@Test
public void testLoxonePositionChanges() {
// filling in missing state values
testChannelState(null);
for (Double i = 0.0; i <= 100.0; i += 1.0) {
changeLoxoneState("position", i);
testChannelState(new PercentType(i.intValue()));
}
// out of range
changeLoxoneState("position", 199.9);
testChannelState(null);
changeLoxoneState("position", 400.1);
testChannelState(null);
}
@Test
public void testOnOffPercentCommands() {
executeCommand(OnOffType.ON);
testAction("On");
executeCommand(OnOffType.OFF);
testAction("Off");
for (Double i = 0.0; i <= 100.0; i += 1.0) {
executeCommand(new PercentType(i.intValue()));
testAction(i.toString());
}
executeCommand(StopMoveType.MOVE);
testAction(null);
}
@Test
public void testIncreaseDecreaseCommands() {
for (Double i = 0.0; i <= 95.0; i += 1.0) {
changeLoxoneState("position", i);
testChannelState(new PercentType(i.intValue()));
testAction(null);
executeCommand(IncreaseDecreaseType.INCREASE);
Double j = i + 5.0;
testAction(j.toString());
}
for (Double i = 100.0; i >= 5.0; i -= 1.0) {
changeLoxoneState("position", i);
testChannelState(new PercentType(i.intValue()));
testAction(null);
executeCommand(IncreaseDecreaseType.DECREASE);
Double j = i - 5.0;
testAction(j.toString());
}
// test not exceeding range
changeLoxoneState("position", 100.0);
testChannelState(PercentType.HUNDRED);
testAction(null);
executeCommand(IncreaseDecreaseType.INCREASE);
testAction("100.0");
changeLoxoneState("position", 0.0);
testChannelState(PercentType.ZERO);
testAction(null);
executeCommand(IncreaseDecreaseType.DECREASE);
testAction("0.0");
}
}

View File

@ -96,7 +96,6 @@ public class LxServerHandlerDummy implements LxServerHandlerApi {
@Override @Override
public void setChannelState(ChannelUID channelId, State state) { public void setChannelState(ChannelUID channelId, State state) {
// TODO Auto-generated method stub
} }
@Override @Override
@ -108,13 +107,11 @@ public class LxServerHandlerDummy implements LxServerHandlerApi {
@Override @Override
public String getSetting(String name) { public String getSetting(String name) {
// TODO Auto-generated method stub
return null; return null;
} }
@Override @Override
public void setSettings(Map<String, String> properties) { public void setSettings(Map<String, String> properties) {
// TODO Auto-generated method stub
} }
@Override @Override

View File

@ -313,7 +313,20 @@
"step": "131b19cd-03c0-6407-ffffd2fd15b703b6" "step": "131b19cd-03c0-6407-ffffd2fd15b703b6"
} }
}, },
"0e367c09-0161-e2c1-ffff403fb0c34b9e": { "faa30f5c-4b4f-11e2-8928b8ba17ef51ee": {
"name": "Kitchen Dimmer",
"type": "EIBDimmer",
"uuidAction": "faa30f5c-4b4f-11e2-8928b8ba17ef51ee",
"room": "0b734138-037d-034e-ffff403fb0c34b9e",
"cat": "0fe650c2-0004-d446-ffff504f9410790f",
"defaultRating": 0,
"isFavorite": false,
"isSecured": false,
"states": {
"position": "faa30f5d-4b4f-11e2-892eb8ba17ef51ee"
}
},
"0e367c09-0161-e2c1-ffff403fb0c34b9e": {
"name": "Window Blinds", "name": "Window Blinds",
"type": "Jalousie", "type": "Jalousie",
"uuidAction": "0e367c09-0161-e2c1-ffff403fb0c34b9e", "uuidAction": "0e367c09-0161-e2c1-ffff403fb0c34b9e",