[loxone] Implementation of EIB Dimmer (#10585)

Signed-off-by: Pawel Pieczul <pieczul@gmail.com>
pull/10665/head
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 |
|-------------|---------------------|--------------------------------------------------------------------------------|------------|-------------------------------------------------------|
| 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.
@ -121,7 +121,8 @@ Currently supported controls are presented in the table below.
| | | `String` - list of alarm sensors separated with `|` | Read-only channel |
| | | `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 |
| 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 |
| 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 |

View File

@ -12,18 +12,7 @@
*/
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;
/**
* A dimmer type of control on Loxone Miniserver.
@ -36,7 +25,7 @@ import org.openhab.core.types.Command;
* @author Stephan Brunner - initial contribution
*
*/
class LxControlDimmer extends LxControl {
class LxControlDimmer extends LxControlEIBDimmer {
static class Factory extends LxControlInstance {
@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_MAX = "max";
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) {
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 getStateDoubleValue(STATE_MIN);
}
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 = 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());
}
}
@Override
Double getMax() {
return getStateDoubleValue(STATE_MAX);
}
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 = 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;
@Override
Double getStep() {
return getStateDoubleValue(STATE_STEP);
}
}

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 LxControlColorPickerV2.Factory());
add(new LxControlDimmer.Factory());
add(new LxControlEIBDimmer.Factory());
add(new LxControlInfoOnlyAnalog.Factory());
add(new LxControlInfoOnlyDigital.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.StateDescriptionFragment;
import org.openhab.core.types.StateDescriptionFragmentBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 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_MANUAL_TEMPERATURE = "setManualTemperature/";
private final Logger logger = LoggerFactory.getLogger(LxControlIRoomControllerV2.class);
private LxControlIRoomControllerV2(LxUuid uuid) {
super(uuid);
}

View File

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

View File

@ -47,7 +47,7 @@ public class LxControlAlarmNoPresenceTest extends LxControlTest {
static final String SENSORS_CHANNEL = " / Sensors";
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 };
@BeforeEach
@ -175,7 +175,7 @@ public class LxControlAlarmNoPresenceTest extends LxControlTest {
private void testNumberChannel(String channel, String state) {
Map<String, State> states = new HashMap<>();
for (String s : numberChannels) {
for (String s : NUMBER_CHANNELS) {
states.put(s, getChannelState(s));
}
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
public void setChannelState(ChannelUID channelId, State state) {
// TODO Auto-generated method stub
}
@Override
@ -108,13 +107,11 @@ public class LxServerHandlerDummy implements LxServerHandlerApi {
@Override
public String getSetting(String name) {
// TODO Auto-generated method stub
return null;
}
@Override
public void setSettings(Map<String, String> properties) {
// TODO Auto-generated method stub
}
@Override

View File

@ -313,7 +313,20 @@
"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",
"type": "Jalousie",
"uuidAction": "0e367c09-0161-e2c1-ffff403fb0c34b9e",