[velux] fix bugs with Somfy devices, and switch devices (#9245)

* [velux] add data type to config params in readme
* [velux] eliminate mvn warning
* [velux] fix Somfy bugs: uniqueindex = serial number or name
* [velux] fix inconsistent switch state logic
* [velux] cosmetics on BRIDGE_THING_TYPE_UID

Signed-off-by: Andrew Fiddian-Green <software@whitebear.ch>
This commit is contained in:
Andrew Fiddian-Green 2020-12-08 17:20:14 +00:00 committed by GitHub
parent d2e5c3e7dd
commit ce5d5ca61d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 129 additions and 97 deletions

View File

@ -82,17 +82,17 @@ These types of Thing only supported in the Velux Bridge in API version two or hi
These types of Thing are configured by means of their serial number in the hub.
In addition there are some optional Configuration Parameters.
| Configuration Parameter | Default | Required | Description |
|-------------------------|------------------------|:--------:|-------------------------------------------------------------------|
| serial | | Yes | Serial number of the io-homecontrol device in the hub. |
| name | | No | (Optional) name of the io-homecontrol device in the hub. |
| inverted | false | No | (Optional) the position is inverted (i.e. 0% translates to 100%). |
| Configuration Parameter | Default | Type | Required | Description |
|-------------------------|---------|---------|:--------:|----------------------------------------------------------------------------------------|
| serial | | custom | Yes | Serial number of the device in the hub (custom format 00:00:00:00:00:00:00:00) |
| name | | text | No | Name of the device in the hub. |
| inverted | false | boolean | No | The `position` and `state` (if available) are inverted (i.e. 0% <-> 100%, OFF <-> ON). |
Notes:
1. To enable a complete inversion of all parameter values (i.e. for Velux windows), use the property `inverted` or add a trailing star to the eight-byte serial number. For an example, see below at item `Velux DG Window Bathroom`.
2. Somfy devices do not provide a valid serial number to the Velux KLF200 gateway. The bridge reports a registration of the serial number 00:00:00:00:00:00:00:00. Therefore the binding implements a fallback to allow an item specification with a actuator `name` instead of actuator serial number whenever such an invalid serial number occurs. For an example, see below at item `Velux OG Somfy Shutter`.
2. Somfy devices do not provide a valid serial number to the Velux KLF200 gateway. In this case you should enter the default `serial` number 00:00:00:00:00:00:00:00, and in addition enter the `name` parameter; this is the name that you gave to the actuator when you first registered it in the KLF200 Bridge. For an example, see below at item `Velux OG Somfy Shutter`.
### Thing Configuration for "scene"
@ -100,10 +100,10 @@ The Velux Bridge in API version one (firmware version 0.1.1.*) allows activating
So besides the bridge, only one real Thing type exists, namely "scene".
This type of Thing is configured by means of its scene name in the hub.
| Configuration Parameter | Default | Required | Description |
|-------------------------|------------------------|:--------:|-----------------------------------------------------------------------|
| sceneName | | Yes | Name of the scene in the hub. |
| velocity | | No | The speed at which the scene will be executed (deafult, silent, fast) |
| Configuration Parameter | Default | Type | Required | Description |
|-------------------------|-----------|------|:--------:|-----------------------------------------------------------------------------|
| sceneName | | text | Yes | Name of the scene in the hub. |
| velocity | 'default' | text | No | The speed at which the scene will be executed ('default', 'silent', 'fast') |
### Thing Configuration for "vshutter"
@ -112,10 +112,10 @@ So besides the bridge, this binding provides a virtual rollershutter Thing consi
Therefore the respective Item definition contains multiple pairs of rollershutter levels each followed by a scene name.
The virtual shutter Thing must be configured with pairs of level (0..10%) combined with the appropriate scene names (text) as follows.
| Configuration Parameter | Default | Required | Description |
|-------------------------|------------------------|:--------:|-----------------------------------------------------------|
| sceneLevels | | Yes | <Level1>,<Scene1>,<Level2>,<Scene2>,.... |
| currentLevel | 0 | No | Inverts any device values. |
| Configuration Parameter | Default | Type | Required | Description |
|-------------------------|---------|---------|:--------:|-----------------------------------------|
| sceneLevels | | text | Yes | {Level1},{Scene1},{Level2},{Scene2},.. |
| currentLevel | 0 | integer | No | Inverts any device values (0..100). |
## Supported Channels for Thing Types

View File

@ -153,4 +153,6 @@ public class VeluxBindingConstants {
public static final String UNKNOWN_THING_TYPE_ID = "FAILED";
public static final String UNKNOWN_IP_ADDRESS = "xxx.xxx.xxx.xxx";
public static final String BRIDGE_THING_TYPE_UID = THING_TYPE_BRIDGE.toString();
}

View File

@ -12,7 +12,7 @@
*/
package org.openhab.binding.velux.internal.handler;
import static org.openhab.binding.velux.internal.VeluxBindingConstants.CHANNEL_ACTUATOR_POSITION;
import static org.openhab.binding.velux.internal.VeluxBindingConstants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
@ -82,6 +82,10 @@ final class ChannelActuatorPosition extends ChannelHandlerTemplate {
LOGGER.trace("handleRefresh(): there are some existing products.");
}
Thing2VeluxActuator veluxActuator = thisBridgeHandler.channel2VeluxActuator.get(channelUID);
if (veluxActuator == null || !veluxActuator.isKnown()) {
LOGGER.warn("handleRefresh(): unknown actuator.");
break;
}
GetProduct bcp = thisBridgeHandler.thisBridge.bridgeAPI().getProduct();
if (bcp == null) {
LOGGER.trace("handleRefresh(): aborting processing as handler is null.");
@ -93,10 +97,16 @@ final class ChannelActuatorPosition extends ChannelHandlerTemplate {
VeluxProduct product = bcp.getProduct();
VeluxProductPosition position = new VeluxProductPosition(product.getDisplayPosition());
if (position.isValid()) {
PercentType posPercent = position.getPositionAsPercentType(veluxActuator.isInverted());
LOGGER.trace("handleRefresh(): position of actuator is {}%.", posPercent);
newState = posPercent;
break;
if (CHANNEL_ACTUATOR_POSITION.equals(channelId)) {
newState = position.getPositionAsPercentType(veluxActuator.isInverted());
LOGGER.trace("handleRefresh(): position of actuator is {}%.", newState);
break;
} else if (CHANNEL_ACTUATOR_STATE.equals(channelId)) {
newState = OnOffType.from(
position.getPositionAsPercentType(veluxActuator.isInverted()).intValue() > 50);
LOGGER.trace("handleRefresh(): state of actuator is {}.", newState);
break;
}
}
LOGGER.trace("handleRefresh(): position of actuator is 'UNDEFINED'.");
newState = UnDefType.UNDEF;
@ -124,55 +134,49 @@ final class ChannelActuatorPosition extends ChannelHandlerTemplate {
LOGGER.debug("handleCommand({},{},{},{}) called.", channelUID, channelId, command, thisBridgeHandler);
Command newValue = null;
do { // just for common exit
assert thisBridgeHandler.bridgeParameters.actuators != null : "VeluxBridgeHandler.bridgeParameters.actuators not initialized.";
if (thisBridgeHandler.bridgeParameters.actuators.autoRefresh(thisBridgeHandler.thisBridge)) {
LOGGER.trace("handleCommand(): there are some existing products.");
}
Thing2VeluxActuator veluxActuator = thisBridgeHandler.channel2VeluxActuator.get(channelUID);
if (veluxActuator == null || !veluxActuator.isKnown()) {
LOGGER.warn("handleRefresh(): unknown actuator.");
break;
}
VeluxProductPosition targetLevel = VeluxProductPosition.UNKNOWN;
if (channelId.equals(CHANNEL_ACTUATOR_POSITION)) {
if ((command instanceof UpDownType) && (command == UpDownType.UP)) {
LOGGER.trace("handleCommand(): found UP command.");
targetLevel = veluxActuator.isInverted() ? new VeluxProductPosition(PercentType.HUNDRED)
: new VeluxProductPosition(PercentType.ZERO);
} else if ((command instanceof UpDownType) && (command == UpDownType.DOWN)) {
LOGGER.trace("handleCommand(): found DOWN command.");
targetLevel = veluxActuator.isInverted() ? new VeluxProductPosition(PercentType.ZERO)
if (CHANNEL_ACTUATOR_POSITION.equals(channelId)) {
if (command instanceof UpDownType) {
LOGGER.trace("handleCommand(): found UpDownType.{} command.", command);
targetLevel = UpDownType.UP.equals(command) ^ veluxActuator.isInverted()
? new VeluxProductPosition(PercentType.ZERO)
: new VeluxProductPosition(PercentType.HUNDRED);
} else if ((command instanceof StopMoveType) && (command == StopMoveType.STOP)) {
LOGGER.trace("handleCommand(): found STOP command.");
targetLevel = new VeluxProductPosition();
} else if (command instanceof StopMoveType) {
LOGGER.trace("handleCommand(): found StopMoveType.{} command.", command);
targetLevel = StopMoveType.STOP.equals(command) ? new VeluxProductPosition() : targetLevel;
} else if (command instanceof PercentType) {
LOGGER.trace("handleCommand(): found command of type PercentType.");
LOGGER.trace("handleCommand(): found PercentType.{} command", command);
PercentType ptCommand = (PercentType) command;
if (veluxActuator.isInverted()) {
ptCommand = new PercentType(PercentType.HUNDRED.intValue() - ptCommand.intValue());
}
LOGGER.trace("handleCommand(): found command to set level to {}.", ptCommand);
targetLevel = new VeluxProductPosition(ptCommand);
} else {
LOGGER.info("handleCommand({},{}): ignoring command.", channelUID.getAsString(), command);
break;
}
} else {
if ((command instanceof OnOffType) && (command == OnOffType.ON)) {
LOGGER.trace("handleCommand(): found ON command.");
targetLevel = veluxActuator.isInverted() ? new VeluxProductPosition(PercentType.HUNDRED)
: new VeluxProductPosition(PercentType.ZERO);
} else if ((command instanceof OnOffType) && (command == OnOffType.OFF)) {
LOGGER.trace("handleCommand(): found OFF command.");
targetLevel = veluxActuator.isInverted() ? new VeluxProductPosition(PercentType.ZERO)
} else if (CHANNEL_ACTUATOR_STATE.equals(channelId)) {
if (command instanceof OnOffType) {
LOGGER.trace("handleCommand(): found OnOffType.{} command.", command);
targetLevel = OnOffType.OFF.equals(command) ^ veluxActuator.isInverted()
? new VeluxProductPosition(PercentType.ZERO)
: new VeluxProductPosition(PercentType.HUNDRED);
} else {
LOGGER.info("handleCommand({},{}): ignoring command.", channelUID.getAsString(), command);
break;
}
}
if (targetLevel == VeluxProductPosition.UNKNOWN) {
LOGGER.info("handleCommand({},{}): ignoring command.", channelUID.getAsString(), command);
break;
}
LOGGER.debug("handleCommand(): sending command with target level {}.", targetLevel);
new VeluxBridgeRunProductCommand().sendCommand(thisBridgeHandler.thisBridge,
veluxActuator.getProductBridgeIndex().toInt(), targetLevel);
LOGGER.trace("handleCommand(): The new shutter level will be send through the home monitoring events.");
if (thisBridgeHandler.bridgeParameters.actuators.autoRefresh(thisBridgeHandler.thisBridge)) {
LOGGER.trace("handleCommand(): position of actuators are updated.");
}

View File

@ -466,7 +466,7 @@ public class VeluxBridgeHandler extends ExtendedBaseBridgeHandler implements Vel
continue;
}
Thing2VeluxActuator actuator = channel2VeluxActuator.get(channelUID);
if (!actuator.isKnown()) {
if (actuator == null || !actuator.isKnown()) {
logger.trace("syncChannelsWithProducts(): channel {} not registered on bridge.", channelUID);
continue;
}

View File

@ -13,8 +13,10 @@
package org.openhab.binding.velux.internal.handler.utils;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.velux.internal.VeluxBindingConstants;
import org.openhab.binding.velux.internal.VeluxBindingProperties;
import org.openhab.binding.velux.internal.handler.VeluxBridgeHandler;
import org.openhab.binding.velux.internal.things.VeluxExistingProducts;
import org.openhab.binding.velux.internal.things.VeluxProduct;
import org.openhab.binding.velux.internal.things.VeluxProduct.ProductBridgeIndex;
import org.openhab.binding.velux.internal.things.VeluxProductSerialNo;
@ -50,32 +52,60 @@ public class Thing2VeluxActuator {
// Private
private void mapThing2Velux() {
if (!ThingConfiguration.exists(bridgeHandler, channelUID,
VeluxBindingProperties.CONFIG_ACTUATOR_SERIALNUMBER)) {
logger.trace("mapThing2Velux(): aborting processing as {} is not set within {}.",
VeluxBindingProperties.CONFIG_ACTUATOR_SERIALNUMBER, channelUID);
if (channelUID.toString().startsWith(VeluxBindingConstants.BRIDGE_THING_TYPE_UID)) {
logger.trace("mapThing2Velux(): channel {} is on a Bridge Thing, exiting.", channelUID);
return;
}
String actuatorSerial = (String) ThingConfiguration.getValue(bridgeHandler, channelUID,
VeluxBindingProperties.CONFIG_ACTUATOR_SERIALNUMBER);
logger.trace("mapThing2Velux(): found actuatorSerial={}.", actuatorSerial);
// Handle value inversion
boolean propertyInverted = false;
if (ThingConfiguration.exists(bridgeHandler, channelUID, VeluxBindingProperties.PROPERTY_ACTUATOR_INVERTED)) {
propertyInverted = (boolean) ThingConfiguration.getValue(bridgeHandler, channelUID,
VeluxBindingProperties.PROPERTY_ACTUATOR_INVERTED);
// the uniqueIndex is the serial number (if valid)
String uniqueIndex = null;
boolean invert = false;
if (ThingConfiguration.exists(bridgeHandler, channelUID, VeluxBindingProperties.CONFIG_ACTUATOR_SERIALNUMBER)) {
String serial = (String) ThingConfiguration.getValue(bridgeHandler, channelUID,
VeluxBindingProperties.CONFIG_ACTUATOR_SERIALNUMBER);
invert = VeluxProductSerialNo.indicatesRevertedValues(serial);
serial = VeluxProductSerialNo.cleaned(serial);
uniqueIndex = ("".equals(serial) || serial.equals(VeluxProductSerialNo.UNKNOWN)) ? null : serial;
}
isInverted = propertyInverted || VeluxProductSerialNo.indicatesRevertedValues(actuatorSerial);
logger.trace("mapThing2Velux(): found isInverted={}.", isInverted);
actuatorSerial = VeluxProductSerialNo.cleaned(actuatorSerial);
logger.trace("mapThing2Velux(): in {} serialNumber={}.", channelUID, uniqueIndex);
if (!bridgeHandler.bridgeParameters.actuators.getChannel().existingProducts.isRegistered(actuatorSerial)) {
logger.warn("mapThing2Velux(): cannot work on unknown actuator with serial {}.", actuatorSerial);
// if serial number not valid, the uniqueIndex is name (if valid)
if (uniqueIndex == null) {
if (ThingConfiguration.exists(bridgeHandler, channelUID, VeluxBindingProperties.PROPERTY_ACTUATOR_NAME)) {
String name = (String) ThingConfiguration.getValue(bridgeHandler, channelUID,
VeluxBindingProperties.PROPERTY_ACTUATOR_NAME);
uniqueIndex = ("".equals(name) || name.equals(VeluxBindingConstants.UNKNOWN)) ? null : name;
}
logger.trace("mapThing2Velux(): in {} name={}.", channelUID, uniqueIndex);
}
if (uniqueIndex == null) {
logger.warn("mapThing2Velux(): in {} cannot find a uniqueIndex, aborting.", channelUID);
return;
} else {
logger.trace("mapThing2Velux(): in {} uniqueIndex={}, proceeding.", channelUID, uniqueIndex);
}
// handle value inversion
if (!invert) {
if (ThingConfiguration.exists(bridgeHandler, channelUID,
VeluxBindingProperties.PROPERTY_ACTUATOR_INVERTED)) {
invert = (boolean) ThingConfiguration.getValue(bridgeHandler, channelUID,
VeluxBindingProperties.PROPERTY_ACTUATOR_INVERTED);
}
}
isInverted = invert;
logger.trace("mapThing2Velux(): in {} isInverted={}.", channelUID, isInverted);
VeluxExistingProducts existing = bridgeHandler.bridgeParameters.actuators.getChannel().existingProducts;
if (!existing.isRegistered(uniqueIndex)) {
logger.warn("mapThing2Velux(): actuator with uniqueIndex={} is not registered", uniqueIndex);
return;
}
logger.trace("mapThing2Velux(): fetching actuator for {}.", actuatorSerial);
thisProduct = bridgeHandler.bridgeParameters.actuators.getChannel().existingProducts.get(actuatorSerial);
logger.trace("mapThing2Velux(): fetching actuator for {}.", uniqueIndex);
thisProduct = existing.get(uniqueIndex);
logger.debug("mapThing2Velux(): found actuator {}.", thisProduct);
return;
}

View File

@ -47,7 +47,7 @@ public class VeluxExistingProducts {
// Type definitions, class-internal variables
private Map<String, VeluxProduct> existingProductsByUniqueIndex;
private Map<Integer, String> bridgeIndexToSerialNumber;
private Map<Integer, String> bridgeIndexToUniqueIndex;
private Map<String, VeluxProduct> modifiedProductsByUniqueIndex;
private int memberCount;
@ -61,7 +61,7 @@ public class VeluxExistingProducts {
public VeluxExistingProducts() {
logger.trace("VeluxExistingProducts(constructor) called.");
existingProductsByUniqueIndex = new ConcurrentHashMap<>();
bridgeIndexToSerialNumber = new ConcurrentHashMap<>();
bridgeIndexToUniqueIndex = new ConcurrentHashMap<>();
modifiedProductsByUniqueIndex = new ConcurrentHashMap<>();
memberCount = 0;
dirty = true;
@ -70,24 +70,22 @@ public class VeluxExistingProducts {
// Class access methods
public boolean isRegistered(String productUniqueIndexOrSerialNumber) {
logger.trace("isRegistered(String {}) returns {}.", productUniqueIndexOrSerialNumber,
existingProductsByUniqueIndex.containsKey(productUniqueIndexOrSerialNumber) ? "true" : "false");
return existingProductsByUniqueIndex.containsKey(productUniqueIndexOrSerialNumber);
public boolean isRegistered(String productUniqueIndex) {
boolean result = existingProductsByUniqueIndex.containsKey(productUniqueIndex);
logger.trace("isRegistered(String {}) returns {}.", productUniqueIndex, result);
return result;
}
public boolean isRegistered(VeluxProduct product) {
logger.trace("isRegistered(VeluxProduct {}) called.", product.toString());
if (product.isV2()) {
return isRegistered(product.getSerialNumber());
}
return isRegistered(product.getProductUniqueIndex());
boolean result = existingProductsByUniqueIndex.containsKey(product.getProductUniqueIndex());
logger.trace("isRegistered(VeluxProduct {}) returns {}.", product, result);
return result;
}
public boolean isRegistered(ProductBridgeIndex bridgeProductIndex) {
logger.trace("isRegisteredProductBridgeIndex {}) called.", bridgeProductIndex.toString());
String serialNumber = bridgeIndexToSerialNumber.get(bridgeProductIndex.toInt());
return serialNumber != null && isRegistered(serialNumber);
boolean result = bridgeIndexToUniqueIndex.containsKey(bridgeProductIndex.toInt());
logger.trace("isRegistered(ProductBridgeIndex {}) returns {}.", bridgeProductIndex, result);
return result;
}
public boolean register(VeluxProduct newProduct) {
@ -97,12 +95,12 @@ public class VeluxExistingProducts {
}
logger.trace("register() registering new product {}.", newProduct);
String uniqueIndex = newProduct.isV2() ? newProduct.getSerialNumber() : newProduct.getProductUniqueIndex();
String uniqueIndex = newProduct.getProductUniqueIndex();
logger.trace("register() registering by UniqueIndex {}", uniqueIndex);
existingProductsByUniqueIndex.put(uniqueIndex, newProduct);
logger.trace("register() registering by ProductBridgeIndex {}", newProduct.getBridgeProductIndex().toInt());
bridgeIndexToSerialNumber.put(newProduct.getBridgeProductIndex().toInt(), newProduct.getSerialNumber());
bridgeIndexToUniqueIndex.put(newProduct.getBridgeProductIndex().toInt(), uniqueIndex);
logger.trace("register() registering set of modifications by UniqueIndex {}", uniqueIndex);
modifiedProductsByUniqueIndex.put(uniqueIndex, newProduct);
@ -125,8 +123,7 @@ public class VeluxExistingProducts {
dirty |= thisProduct.setCurrentPosition(productPosition);
dirty |= thisProduct.setTarget(productTarget);
if (dirty) {
String uniqueIndex = thisProduct.isV2() ? thisProduct.getSerialNumber()
: thisProduct.getProductUniqueIndex();
String uniqueIndex = thisProduct.getProductUniqueIndex();
logger.trace("update(): updating by UniqueIndex {}.", uniqueIndex);
existingProductsByUniqueIndex.replace(uniqueIndex, thisProduct);
modifiedProductsByUniqueIndex.put(uniqueIndex, thisProduct);
@ -141,21 +138,16 @@ public class VeluxExistingProducts {
currentProduct.getCurrentPosition(), currentProduct.getTarget());
}
public VeluxProduct get(String productUniqueIndexOrSerialNumber) {
logger.trace("get({}) called.", productUniqueIndexOrSerialNumber);
if (!isRegistered(productUniqueIndexOrSerialNumber)) {
return VeluxProduct.UNKNOWN;
}
return existingProductsByUniqueIndex.getOrDefault(productUniqueIndexOrSerialNumber, VeluxProduct.UNKNOWN);
public VeluxProduct get(String productUniqueIndex) {
logger.trace("get({}) called.", productUniqueIndex);
return existingProductsByUniqueIndex.getOrDefault(productUniqueIndex, VeluxProduct.UNKNOWN);
}
public VeluxProduct get(ProductBridgeIndex bridgeProductIndex) {
logger.trace("get({}) called.", bridgeProductIndex);
String serialNumber = bridgeIndexToSerialNumber.get(bridgeProductIndex.toInt());
if (!isRegistered(bridgeProductIndex) || serialNumber == null) {
return VeluxProduct.UNKNOWN;
}
return existingProductsByUniqueIndex.getOrDefault(serialNumber, VeluxProduct.UNKNOWN);
String unique = bridgeIndexToUniqueIndex.get(bridgeProductIndex.toInt());
return unique != null ? existingProductsByUniqueIndex.getOrDefault(unique, VeluxProduct.UNKNOWN)
: VeluxProduct.UNKNOWN;
}
public VeluxProduct[] values() {

View File

@ -217,7 +217,10 @@ public class VeluxProduct {
// Class helper methods
public String getProductUniqueIndex() {
return this.name.toString().concat("#").concat(this.typeId.toString());
if (!v2 || serialNumber.startsWith(VeluxProductSerialNo.UNKNOWN)) {
return name.toString();
}
return VeluxProductSerialNo.cleaned(serialNumber);
}
// Getter and Setter methods

View File

@ -138,6 +138,7 @@
<label>@text/config.velux.thing.rollershutter.serial.label</label>
<description>@text/config.velux.thing.rollershutter.serial.description</description>
<required>true</required>
<default>00:00:00:00:00:00:00:00</default>
<advanced>false</advanced>
</parameter>
<parameter name="name" type="text">