[luxom] Initial contribution (#12310)

Signed-off-by: Kris Jespers <kriasoft@telenet.be>
This commit is contained in:
jesperskriasoft 2022-04-05 20:02:27 +02:00 committed by GitHub
parent 707ecaf47b
commit 873d615316
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 1919 additions and 0 deletions

View File

@ -169,6 +169,7 @@
/bundles/org.openhab.binding.loxone/ @ppieczul
/bundles/org.openhab.binding.luftdateninfo/ @weymann
/bundles/org.openhab.binding.lutron/ @actong @bobadair
/bundles/org.openhab.binding.luxom/ @jesperskriasoft
/bundles/org.openhab.binding.luxtronikheatpump/ @sgiehl
/bundles/org.openhab.binding.magentatv/ @markus7017
/bundles/org.openhab.binding.mail/ @openhab/add-ons-maintainers

View File

@ -836,6 +836,11 @@
<artifactId>org.openhab.binding.lutron</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.luxom</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.luxtronikheatpump</artifactId>

View File

@ -0,0 +1,13 @@
This content is produced and maintained by the openHAB project.
* Project home: https://www.openhab.org
== Declared Project Licenses
This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab-addons

View File

@ -0,0 +1,98 @@
# Luxom Binding
This binding integrates with a https://luxom.io/ based system through a Luxom IP interface module.
The binding has been tested with the DS65L IP interface, but it's not an official binding by Luxom.
The API implementation is based on the following documentation:
* https://old.luxom.io/uploads/ppfiles/27/LUXOM_ASCII.pdf
* https://old.luxom.io/uploads/ppfiles/28/LUXOM_ASCII_extended.pdf
## Supported Things
This binding currently supports the following thing types:
* **ipbridge** - The Lutron main repeater/processor/hub
* **dimmer** - Light dimmer
* **switch** - Switch or relay module
## Thing Configuration
### Bridge
The Bridge thing has two parameters:
- ipAddress: This is the IP address of the IP interface module
- port: The listening port (optional, defaults to 2300)
```
Bridge luxom:bridge:myhouse [ ipAddress="192.168.0.50", port="2300"] {
...
}
```
### Devices
Each device has an address on the Luxom bus, this address must be specified in the 'address' parameter.
You will have to look it up in your documentation or in the 'Luxom Plusconfig' software.
Sometimes a device does not send back a confirmation over the bus having set the correct state.
Some dimmers do the dimming, but do not send back the set brightness level.
To be able to use these devices, you can add the `doesNotReply=true` parameter so that the binding immediately sets the item's state and does not wait for confirmation.
#### Dimmers
Dimmers support the optional advanced parameters `onLevel`, `onToLast` and `stepPercentage`:
* The `onLevel` parameter specifies the level to which the dimmer will go when sent an ON command. It defaults to 100.
* The `onToLast` parameter is a boolean that defaults to false. If set to "true", the dimmer will go to its last non-zero level when sent an ON command. If the last non-zero level cannot be determined, the value of `onLevel` will be used instead.
* The `stepPercentage` specifies the in-/decrease in percentage of brightness. Default is 5.
A **dimmer** thing has a single channel *Lighting.Brightness* with type Dimmer and category DimmableLight.
Thing configuration file example:
```
Thing dimmer dimmerLightLiving1 [address="A,02", onLevel="50", onToLast="false", stepPercentage="5"]
```
#### Switches
Switches take no additional parameters.
A **switch** thing has a single channel *switch* with type Switch and category Switch.
Thing configuration file example:
```
Thing switch switchLiving1 [address="A,02"]
```
### Channels
The following is a summary of channels for all Luxom things:
| Thing | Channel | Item Type | Description |
|---------------------|----------------|---------------|-----------------------------------|
| dimmer | brightness | Dimmer | Increase/decrease the light level |
| switch | switch | Switch | Switch the device on/off |
### Full Example
demo.things:
```
Bridge luxom:bridge:myhouse [ ipAddress="192.168.0.50", port="2300"] {
Thing switch switchBedroom1 "Switch 1" @ "Bedroom" [address="1,01"]
Thing dimmer dimmerBedroom1 "dimmer 1" @ "Bedroom" [address="A,02"]
Thing dimmer dimmerKitchen1 "dimmer 1" @ "Kitchen" [address="A,04", doesNotReply=true]
}
```
demo.items:
```
Dimmer FF_Bedroom_Lights "Bedroom dimmer light" <light> (FF_Living, gLight) ["Lighting"] {channel="luxom:dimmer:myhouse:dimmerBedroom1:brightness", ga="Light", homekit="Lighting, Lighting.Brightness"}
Switch FF_Bedroom_PowerOutlet1 "Bedroom Power Outlet 1" <poweroutlet> (FF_Living, gPower) ["Switchable"] {channel="luxom:switch:myhouse:switchBedroom1:switch", ga="Outlet"}
Dimmer FF_Kitchen_Lights "Kitchen dimmer light" <light> (FF_Kitchen, gLight) ["Lighting"] {channel="luxom:dimmer:myhouse:dimmerKitchen1:brightness", ga="Light", homekit="Lighting, Lighting.Brightness"}
```

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>3.3.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.luxom</artifactId>
<name>openHAB Add-ons :: Bundles :: Luxom Binding</name>
</project>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.luxom-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-luxom" description="Luxom Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.luxom/${project.version}</bundle>
</feature>
</features>

View File

@ -0,0 +1,46 @@
/**
* Copyright (c) 2010-2022 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.luxom.internal;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link LuxomBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Kris Jespers - Initial contribution
*/
@NonNullByDefault
public class LuxomBindingConstants {
public static final String BINDING_ID = "luxom";
// List of all Thing Type UIDs
// bridge
public static final ThingTypeUID BRIDGE_THING_TYPE = new ThingTypeUID(BINDING_ID, "bridge");
// generic thing types
public static final ThingTypeUID THING_TYPE_SWITCH = new ThingTypeUID(BINDING_ID, "switch");
public static final ThingTypeUID THING_TYPE_DIMMER = new ThingTypeUID(BINDING_ID, "dimmer");
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(BRIDGE_THING_TYPE, THING_TYPE_SWITCH,
THING_TYPE_DIMMER);
// List of all Channel ids
public static final String CHANNEL_BRIGHTNESS = "brightness";
public static final String CHANNEL_SWITCH = "switch";
}

View File

@ -0,0 +1,54 @@
/**
* Copyright (c) 2010-2022 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.luxom.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.luxom.internal.handler.LuxomBridgeHandler;
import org.openhab.binding.luxom.internal.handler.LuxomDimmerHandler;
import org.openhab.binding.luxom.internal.handler.LuxomSwitchHandler;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Component;
/**
* The {@link LuxomHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Kris Jespers - Initial contribution
*/
@NonNullByDefault
@Component(configurationPid = "binding.luxom", service = ThingHandlerFactory.class)
public class LuxomHandlerFactory extends BaseThingHandlerFactory {
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return LuxomBindingConstants.SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
if (LuxomBindingConstants.BRIDGE_THING_TYPE.equals(thing.getThingTypeUID())) {
return new LuxomBridgeHandler((Bridge) thing);
} else if (LuxomBindingConstants.THING_TYPE_SWITCH.equals(thing.getThingTypeUID())) {
return new LuxomSwitchHandler(thing);
} else if (LuxomBindingConstants.THING_TYPE_DIMMER.equals(thing.getThingTypeUID())) {
return new LuxomDimmerHandler(thing);
}
return null;
}
}

View File

@ -0,0 +1,31 @@
/**
* Copyright (c) 2010-2022 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.luxom.internal.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* @author Kris Jespers - Initial contribution
*/
@NonNullByDefault
public class CommandExecutionSpecification {
private final String command;
public CommandExecutionSpecification(String command) {
this.command = command;
}
public String getCommand() {
return command;
}
}

View File

@ -0,0 +1,345 @@
/**
* Copyright (c) 2010-2022 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.luxom.internal.handler;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.luxom.internal.handler.config.LuxomBridgeConfig;
import org.openhab.binding.luxom.internal.protocol.LuxomAction;
import org.openhab.binding.luxom.internal.protocol.LuxomCommand;
import org.openhab.binding.luxom.internal.protocol.LuxomCommunication;
import org.openhab.binding.luxom.internal.protocol.LuxomSystemInfo;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Handler responsible for communicating with the main Luxom IP access module.
*
* @author Kris Jespers - Initial contribution
*/
@NonNullByDefault
public class LuxomBridgeHandler extends BaseBridgeHandler {
public static final int HEARTBEAT_INTERVAL_SECONDS = 50;
private final LuxomSystemInfo systemInfo;
private static final int DEFAULT_RECONNECT_INTERVAL_IN_MINUTES = 1;
private static final long HEARTBEAT_ACK_TIMEOUT_SECONDS = 20;
private final Logger logger = LoggerFactory.getLogger(LuxomBridgeHandler.class);
private @Nullable LuxomBridgeConfig config;
private final AtomicInteger nrOfSendPermits = new AtomicInteger(0);
private int reconnectInterval;
private @Nullable LuxomCommand previousCommand;
private final LuxomCommunication communication;
private final BlockingQueue<List<CommandExecutionSpecification>> sendQueue = new LinkedBlockingQueue<>();
private @Nullable Thread messageSender;
private @Nullable ScheduledFuture<?> heartBeat;
private @Nullable ScheduledFuture<?> heartBeatTimeoutTask;
private @Nullable ScheduledFuture<?> connectRetryJob;
public @Nullable LuxomBridgeConfig getIPBridgeConfig() {
return config;
}
public LuxomBridgeHandler(Bridge bridge) {
super(bridge);
logger.debug("Luxom bridge init");
systemInfo = new LuxomSystemInfo();
communication = new LuxomCommunication(this);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
logger.debug("Bridge received command {} for {}", command.toFullString(), channelUID);
}
@Override
public void initialize() {
config = getConfig().as(LuxomBridgeConfig.class);
if (validConfiguration(config)) {
reconnectInterval = (config.reconnectInterval > 0) ? config.reconnectInterval
: DEFAULT_RECONNECT_INTERVAL_IN_MINUTES;
updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, "@text/status.connecting");
scheduler.submit(this::connect); // start the async connect task
}
}
private boolean validConfiguration(@Nullable LuxomBridgeConfig config) {
if (config == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/bridge-configuration-missing");
return false;
}
if (config.ipAddress == null || config.ipAddress.trim().isEmpty()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/bridge-address-missing");
return false;
}
return true;
}
private void scheduleConnectRetry(long waitMinutes) {
logger.debug("Scheduling connection retry in {} (minutes)", waitMinutes);
connectRetryJob = scheduler.schedule(this::connect, waitMinutes, TimeUnit.MINUTES);
}
private synchronized void connect() {
if (communication.isConnected()) {
return;
}
if (config != null) {
logger.debug("Connecting to bridge at {}", config.ipAddress);
}
try {
communication.startCommunication();
} catch (Exception e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
disconnect();
scheduleConnectRetry(reconnectInterval); // Possibly a temporary problem. Try again later.
}
}
public void startProcessing() {
nrOfSendPermits.set(1);
updateStatus(ThingStatus.ONLINE);
messageSender = new Thread(this::sendCommandsThread, "Luxom sender");
messageSender.start();
logger.debug("Starting heartbeat job with interval {} (seconds)", HEARTBEAT_INTERVAL_SECONDS);
heartBeat = scheduler.scheduleWithFixedDelay(this::sendHeartBeat, 10, HEARTBEAT_INTERVAL_SECONDS,
TimeUnit.SECONDS);
}
private void sendCommandsThread() {
logger.debug("Starting send commands thread...");
try {
while (!Thread.currentThread().isInterrupted()) {
logger.debug("waiting for command to send...");
List<CommandExecutionSpecification> commands = sendQueue.take();
try {
for (CommandExecutionSpecification commandExecutionSpecification : commands) {
communication.sendMessage(commandExecutionSpecification.getCommand());
}
} catch (IOException e) {
logger.warn("Communication error while sending, will try to reconnect. Error: {}", e.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
reconnect();
// reconnect() will start a new thread; terminate this one
break;
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private synchronized void disconnect() {
logger.debug("Disconnecting from bridge");
if (connectRetryJob != null) {
connectRetryJob.cancel(true);
}
if (this.heartBeat != null) {
this.heartBeat.cancel(true);
}
cancelCheckAliveTimeoutTask();
if (messageSender != null && messageSender.isAlive()) {
messageSender.interrupt();
}
this.communication.stopCommunication();
}
public void reconnect() {
reconnect(false);
}
private synchronized void reconnect(boolean timeout) {
if (timeout) {
logger.debug("Keepalive timeout, attempting to reconnect to the bridge");
} else {
logger.debug("Connection problem, attempting to reconnect to the bridge");
}
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
disconnect();
connect();
}
public void sendCommands(List<CommandExecutionSpecification> commands) {
this.sendQueue.add(commands);
}
private @Nullable LuxomThingHandler findThingHandler(@Nullable String address) {
for (Thing thing : getThing().getThings()) {
if (thing.getHandler() instanceof LuxomThingHandler) {
LuxomThingHandler handler = (LuxomThingHandler) thing.getHandler();
try {
if (handler != null && handler.getAddress().equals(address)) {
return handler;
}
} catch (IllegalStateException e) {
logger.trace("Handler for id {} not initialized", address);
}
}
}
return null;
}
/**
* needed with fast reconnect to update status of things
*/
public void forceRefreshThings() {
for (Thing thing : getThing().getThings()) {
if (thing.getHandler() instanceof LuxomThingHandler) {
LuxomThingHandler handler = (LuxomThingHandler) thing.getHandler();
handler.ping();
}
}
}
private void sendHeartBeat() {
logger.trace("Sending heartbeat");
// Reconnect if no response is received within KEEPALIVE_TIMEOUT_SECONDS.
heartBeatTimeoutTask = scheduler.schedule(() -> reconnect(true), HEARTBEAT_ACK_TIMEOUT_SECONDS,
TimeUnit.SECONDS);
sendCommands(List.of(new CommandExecutionSpecification(LuxomAction.HEARTBEAT.getCommand())));
}
@Override
public void thingUpdated(Thing thing) {
LuxomBridgeConfig newConfig = thing.getConfiguration().as(LuxomBridgeConfig.class);
boolean validConfig = validConfiguration(newConfig);
boolean needsReconnect = validConfig && config != null && !config.sameConnectionParameters(newConfig);
if (!validConfig || needsReconnect) {
dispose();
}
this.thing = thing;
this.config = newConfig;
if (needsReconnect) {
initialize();
}
}
public void handleCommunicationError(IOException e) {
logger.debug("Communication error while reading, will try to reconnect. Error: {}", e.getMessage());
reconnect();
}
@Override
public void dispose() {
disconnect();
}
public void handleIncomingLuxomMessage(String luxomMessage) throws IOException {
cancelCheckAliveTimeoutTask(); // we got a message
logger.trace("Luxom: received {}", luxomMessage);
LuxomCommand luxomCommand = new LuxomCommand(luxomMessage);
// Now dispatch update to the proper thing handler
if (LuxomAction.PASSWORD_REQUEST == luxomCommand.getAction()) {
communication.sendMessage(LuxomAction.REQUEST_FOR_INFORMATION.getCommand()); // direct send, no queue, so
// no tcp flow constraint
} else if (LuxomAction.MODULE_INFORMATION == luxomCommand.getAction()) {
cmdSystemInfo(luxomCommand.getData());
if (ThingStatus.ONLINE != getThing().getStatus()) {
// this all happens before TCP flow controle, when startProcessing is called, TCP flow is activated...
startProcessing();
}
} else if (LuxomAction.ACKNOWLEDGE == luxomCommand.getAction()) {
logger.trace("received acknowledgement");
} else if (LuxomAction.DATA == luxomCommand.getAction()
|| LuxomAction.DATA_RESPONSE == luxomCommand.getAction()) {
previousCommand = luxomCommand;
} else if (LuxomAction.INVALID_ACTION != luxomCommand.getAction()) {
if (LuxomAction.DATA_BYTE == luxomCommand.getAction()
|| LuxomAction.DATA_BYTE_RESPONSE == luxomCommand.getAction()) {
// data for previous command if it needs it
if (previousCommand != null && previousCommand.getAction().isNeedsData()) {
previousCommand.setData(luxomCommand.getData());
luxomCommand = previousCommand;
previousCommand = null;
}
}
if (luxomCommand != null) {
LuxomThingHandler handler = findThingHandler(luxomCommand.getAddress());
if (handler != null) {
handler.handleCommandComingFromBridge(luxomCommand);
} else {
logger.warn("No handler found command {} for address : {}", luxomMessage,
luxomCommand.getAddress());
}
} else {
logger.warn("Something was wrong with the order of incoming commands, resulting command is null");
}
} else {
logger.trace("Luxom: not handled {}", luxomMessage);
}
logger.trace("nrOfPermits after receive: {}", nrOfSendPermits.get());
}
private void cancelCheckAliveTimeoutTask() {
var task = heartBeatTimeoutTask;
if (task != null) {
// This method can be called from the keepAliveReconnect thread. Make sure
// we don't interrupt ourselves, as that may prevent the reconnection attempt.
task.cancel(false);
}
}
private synchronized void cmdSystemInfo(@Nullable String info) {
systemInfo.setSwVersion(info);
}
}

View File

@ -0,0 +1,29 @@
/**
* Copyright (c) 2010-2022 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.luxom.internal.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* exception during communication with luxom IP module
*
* @author Kris Jespers - Initial contribution
*/
@NonNullByDefault
public class LuxomConnectionException extends Exception {
private static final long serialVersionUID = 654654L;
public LuxomConnectionException(Throwable cause) {
super(cause);
}
}

View File

@ -0,0 +1,173 @@
/**
* Copyright (c) 2010-2022 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.luxom.internal.handler;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.luxom.internal.LuxomBindingConstants;
import org.openhab.binding.luxom.internal.handler.config.LuxomThingDimmerConfig;
import org.openhab.binding.luxom.internal.handler.util.PercentageConverter;
import org.openhab.binding.luxom.internal.protocol.LuxomAction;
import org.openhab.binding.luxom.internal.protocol.LuxomCommand;
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.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link LuxomDimmerHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Kris Jespers - Initial contribution
*/
@NonNullByDefault
public class LuxomDimmerHandler extends LuxomThingHandler {
private final Logger logger = LoggerFactory.getLogger(LuxomDimmerHandler.class);
public LuxomDimmerHandler(Thing thing) {
super(thing);
}
private @Nullable LuxomThingDimmerConfig config;
private final AtomicReference<Integer> lastLightLevel = new AtomicReference<>(0);
@Override
public void initialize() {
super.initialize();
config = getConfig().as(LuxomThingDimmerConfig.class);
logger.debug("Initializing Switch handler for address {}", getAddress());
initDeviceState();
}
@Override
protected void initDeviceState() {
logger.debug("Initializing device state for Switch {}", getAddress());
@Nullable
Bridge bridge = getBridge();
if (bridge == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
} else if (ThingStatus.ONLINE.equals(bridge.getStatus())) {
if (config != null && config.doesNotReply) {
logger.debug("Switch {} will not reply, so always keeping it ONLINE", getAddress());
updateStatus(ThingStatus.ONLINE);
} else {
updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, "@text/status.awaiting-initial-response");
ping(); // handleUpdate() will set thing status to online when response arrives
}
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
logger.debug("dimmer at address {} received command {} for {}", getAddress(), command.toFullString(),
channelUID);
if (LuxomBindingConstants.CHANNEL_SWITCH.equals(channelUID.getId())) {
if (OnOffType.ON.equals(command)) {
set();
} else if (OnOffType.OFF.equals(command)) {
clear();
}
} else if (LuxomBindingConstants.CHANNEL_BRIGHTNESS.equals(channelUID.getId()) && config != null) {
if (command instanceof Number) {
int level = ((Number) command).intValue();
logger.trace("dimmer at address {} just setting dimmer level", getAddress());
dim(level);
} else if (command instanceof IncreaseDecreaseType) {
IncreaseDecreaseType s = (IncreaseDecreaseType) command;
int currentValue = lastLightLevel.get();
int newValue;
if (IncreaseDecreaseType.INCREASE.equals(s)) {
newValue = currentValue + config.stepPercentage;
// round down to step multiple
newValue = newValue - newValue % config.stepPercentage;
logger.trace("dimmer at address {} just increasing dimmer level", getAddress());
dim(newValue);
} else {
newValue = currentValue - config.stepPercentage;
// round up to step multiple
newValue = newValue + newValue % config.stepPercentage;
logger.trace("dimmer at address {} just increasing dimmer level", getAddress());
dim(Math.max(newValue, 0));
}
} else if (OnOffType.ON.equals(command)) {
if (config.onToLast) {
dim(lastLightLevel.get());
} else {
dim(config.onLevel.intValue());
}
} else if (OnOffType.OFF.equals(command)) {
dim(0);
}
}
}
@Override
public void handleCommandComingFromBridge(LuxomCommand command) {
updateStatus(ThingStatus.ONLINE);
if (LuxomAction.CLEAR_RESPONSE.equals(command.getAction())) {
updateState(LuxomBindingConstants.CHANNEL_SWITCH, OnOffType.OFF);
} else if (LuxomAction.SET_RESPONSE.equals(command.getAction())) {
updateState(LuxomBindingConstants.CHANNEL_SWITCH, OnOffType.ON);
} else if (LuxomAction.DATA_RESPONSE.equals(command.getAction())) {
int percentage = PercentageConverter.getPercentage(command.getData());
lastLightLevel.set(percentage);
updateState(LuxomBindingConstants.CHANNEL_BRIGHTNESS, new PercentType(percentage));
}
}
@Override
public void channelLinked(ChannelUID channelUID) {
logger.debug("dimmer at address {} linked to channel {}", getAddress(), channelUID);
if (LuxomBindingConstants.CHANNEL_SWITCH.equals(channelUID.getId())
|| LuxomBindingConstants.CHANNEL_BRIGHTNESS.equals(channelUID.getId())) {
// Refresh state when new item is linked.
if (config != null && !config.doesNotReply) {
ping();
}
}
}
/**
* example : *A,0,2,2B;*Z,057;
*/
private void dim(int percentage) {
logger.debug("dimming dimmer at address {} to {} %", getAddress(), percentage);
List<CommandExecutionSpecification> commands = new ArrayList<>(3);
if (percentage == 0) {
commands.add(new CommandExecutionSpecification(LuxomAction.CLEAR.getCommand() + ",0," + getAddress()));
} else {
commands.add(new CommandExecutionSpecification(LuxomAction.SET.getCommand() + ",0," + getAddress()));
}
commands.add(new CommandExecutionSpecification(LuxomAction.DATA.getCommand() + ",0," + getAddress()));
commands.add(new CommandExecutionSpecification(
LuxomAction.DATA_BYTE.getCommand() + ",0" + PercentageConverter.getHexRepresentation(percentage)));
sendCommands(commands);
}
}

View File

@ -0,0 +1,101 @@
/**
* Copyright (c) 2010-2022 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.luxom.internal.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.luxom.internal.LuxomBindingConstants;
import org.openhab.binding.luxom.internal.protocol.LuxomAction;
import org.openhab.binding.luxom.internal.protocol.LuxomCommand;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link LuxomSwitchHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Kris Jespers - Initial contribution
*/
@NonNullByDefault
public class LuxomSwitchHandler extends LuxomThingHandler {
private final Logger logger = LoggerFactory.getLogger(LuxomSwitchHandler.class);
public LuxomSwitchHandler(Thing thing) {
super(thing);
}
@Override
public void initialize() {
super.initialize();
logger.debug("Initializing Switch handler for address {}", getAddress());
initDeviceState();
}
@Override
protected void initDeviceState() {
logger.debug("Initializing device state for Switch {}", getAddress());
Bridge bridge = getBridge();
if (bridge == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
} else if (ThingStatus.ONLINE.equals(bridge.getStatus())) {
updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, "@text/status.awaiting-initial-response");
ping(); // handleUpdate() will set thing status to online when response arrives
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
logger.debug("switch at address {} received command {} for {}", getAddress(), command.toFullString(),
channelUID);
if (LuxomBindingConstants.CHANNEL_SWITCH.equals(channelUID.getId())) {
if (OnOffType.ON.equals(command)) {
set();
ping(); // to make sure we know the current state
} else if (OnOffType.OFF.equals(command)) {
clear();
ping(); // to make sure we know the current state
}
}
}
@Override
public void handleCommandComingFromBridge(LuxomCommand command) {
if (LuxomAction.CLEAR_RESPONSE.equals(command.getAction())) {
updateState(LuxomBindingConstants.CHANNEL_SWITCH, OnOffType.OFF);
updateStatus(ThingStatus.ONLINE);
} else if (LuxomAction.SET_RESPONSE.equals(command.getAction())) {
updateState(LuxomBindingConstants.CHANNEL_SWITCH, OnOffType.ON);
updateStatus(ThingStatus.ONLINE);
}
}
@Override
public void channelLinked(ChannelUID channelUID) {
logger.debug("switch at address {} linked to channel {}", getAddress(), channelUID);
if (LuxomBindingConstants.CHANNEL_SWITCH.equals(channelUID.getId())
|| LuxomBindingConstants.CHANNEL_BRIGHTNESS.equals(channelUID.getId())) {
// Refresh state when new item is linked.
ping();
}
}
}

View File

@ -0,0 +1,130 @@
/**
* Copyright (c) 2010-2022 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.luxom.internal.handler;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.luxom.internal.protocol.LuxomAction;
import org.openhab.binding.luxom.internal.protocol.LuxomCommand;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingStatusInfo;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Base type for all Luxom thing handlers.
*
* @author Kris Jespers - Initial contribution
*/
@NonNullByDefault
public abstract class LuxomThingHandler extends BaseThingHandler {
private final Logger logger = LoggerFactory.getLogger(LuxomThingHandler.class);
private String address = "";
@Override
public void initialize() {
String id = (String) getConfig().get("address");
if (id == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/status.thing-address-missing");
address = "noaddress";
return;
}
address = id;
}
public LuxomThingHandler(Thing thing) {
super(thing);
}
public abstract void handleCommandComingFromBridge(LuxomCommand command);
public final String getAddress() {
return address;
}
/**
* Queries for any device state needed at initialization time or after losing connectivity to the bridge, and
* updates device status. Will be called when bridge status changes to ONLINE and thing has status
* OFFLINE:BRIDGE_OFFLINE.
*/
protected abstract void initDeviceState();
/**
* Called when changing thing status to offline. Subclasses may override to take any needed actions.
*/
protected void thingOfflineNotify() {
}
protected @Nullable LuxomBridgeHandler getBridgeHandler() {
Bridge bridge = getBridge();
return bridge == null ? null : (LuxomBridgeHandler) bridge.getHandler();
}
@Override
public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
logger.debug("Bridge status changed to {} for luxom device handler {}", bridgeStatusInfo.getStatus(),
getAddress());
if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE
&& getThing().getStatusInfo().getStatusDetail() == ThingStatusDetail.BRIDGE_OFFLINE) {
initDeviceState();
} else if (bridgeStatusInfo.getStatus() == ThingStatus.OFFLINE) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
thingOfflineNotify();
}
}
protected void sendCommands(List<CommandExecutionSpecification> commands) {
@Nullable
LuxomBridgeHandler bridgeHandler = getBridgeHandler();
if (bridgeHandler == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_MISSING_ERROR,
"@text/status.bridge-handler-missing");
thingOfflineNotify();
} else {
bridgeHandler.sendCommands(commands);
}
}
/**
* example : *P,0,1,21;
*/
protected void ping() {
sendCommands(List.of(new CommandExecutionSpecification(LuxomAction.PING.getCommand() + ",0," + getAddress())));
}
/**
* example : *S,0,1,21;
*/
protected void set() {
sendCommands(List.of(new CommandExecutionSpecification(LuxomAction.SET.getCommand() + ",0," + getAddress())));
}
/**
* example : *C,0,1,21;
*/
protected void clear() {
sendCommands(List.of(new CommandExecutionSpecification(LuxomAction.CLEAR.getCommand() + ",0," + getAddress())));
}
}

View File

@ -0,0 +1,47 @@
/**
* Copyright (c) 2010-2022 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.luxom.internal.handler.config;
import java.util.Objects;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* {@link LuxomBridgeConfig} is the general config class for Luxom Bridges.
*
* @author Kris Jespers - Initial contribution
*/
@NonNullByDefault
public class LuxomBridgeConfig {
public @Nullable String ipAddress;
public int port;
/**
* reconnect after X minutes when disconnected
*/
public int reconnectInterval;
public int aliveCheckInterval;
/**
* if true, on communication error the devices will NOT go offline...
* if false, they will go offline. In both instances they will get (re)pinged after reconnect.
*
*/
public boolean useFastReconnect = false;
public boolean sameConnectionParameters(LuxomBridgeConfig config) {
return Objects.equals(ipAddress, config.ipAddress) && config.port == port
&& (reconnectInterval == config.reconnectInterval) && (aliveCheckInterval == config.aliveCheckInterval);
}
}

View File

@ -0,0 +1,25 @@
/**
* Copyright (c) 2010-2022 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.luxom.internal.handler.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* {@link LuxomThingConfig} is the general config class for luxom things.
*
* @author Kris Jespers - Initial contribution
*/
@NonNullByDefault
public class LuxomThingConfig {
public Boolean doesNotReply = Boolean.FALSE;
}

View File

@ -0,0 +1,31 @@
/**
* Copyright (c) 2010-2022 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.luxom.internal.handler.config;
import java.math.BigDecimal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* {@link LuxomThingDimmerConfig} is the config class for Niko Home Control Dimmer Actions.
*
* @author Kris Jespers - Initial contribution
*/
@NonNullByDefault
public class LuxomThingDimmerConfig extends LuxomThingConfig {
private static final int DEFAULT_ONLEVEL = 100;
public BigDecimal onLevel = new BigDecimal(DEFAULT_ONLEVEL);
public Boolean onToLast = Boolean.FALSE;
public Integer stepPercentage = 5;
}

View File

@ -0,0 +1,44 @@
/**
* Copyright (c) 2010-2022 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.luxom.internal.handler.util;
import java.math.BigDecimal;
import java.math.RoundingMode;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* converts the hexadecimal string representation to a integer value between 0 - 100
*
* @author Kris Jespers - Initial contribution
*/
@NonNullByDefault
public class PercentageConverter {
/**
* @param hexRepresentation
* @return if hexRepresentation == null return -1, otherwise return percentage
*/
public static int getPercentage(@Nullable String hexRepresentation) {
if (hexRepresentation == null)
return -1;
int decimal = Integer.parseInt(hexRepresentation, 16);
BigDecimal level = new BigDecimal(100 * decimal).divide(new BigDecimal(255), RoundingMode.FLOOR);
return level.intValue();
}
public static String getHexRepresentation(int percentage) {
BigDecimal decimal = new BigDecimal(255 * percentage).divide(new BigDecimal(100), RoundingMode.CEILING);
return Integer.toHexString(decimal.intValue()).toUpperCase();
}
}

View File

@ -0,0 +1,73 @@
/**
* Copyright (c) 2010-2022 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.luxom.internal.protocol;
import java.util.Arrays;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* luxom action
*
* @author Kris Jespers - Initial contribution
*/
@NonNullByDefault
public enum LuxomAction {
HEARTBEAT("*U", false),
ACKNOWLEDGE("@1*V", false),
TOGGLE("*T", true),
PING("*P", true),
MODULE_INFORMATION("*!", false),
PASSWORD_REQUEST("@1*PW-", false),
CLEAR_RESPONSE("@1*C", true),
SET_RESPONSE("@1*S", true),
DATA_RESPONSE("@1*A", true, true),
DATA_BYTE_RESPONSE("@1*Z", false),
DATA("*A", true, true),
DATA_BYTE("*Z", false),
SET("*S", true),
CLEAR("*C", true),
REQUEST_FOR_INFORMATION("*?", false),
INVALID_ACTION("-INVALID-", false); // this is not part of the luxom api, it's for internal use.;
private final String command;
private final boolean hasAddress;
private final boolean needsData;
LuxomAction(String command, boolean hasAddress) {
this(command, hasAddress, false);
}
LuxomAction(String command, boolean hasAddress, boolean needsData) {
this.command = command;
this.hasAddress = hasAddress;
this.needsData = needsData;
}
public static LuxomAction of(String command) {
return Arrays.stream(LuxomAction.values()).filter(a -> a.getCommand().equals(command)).findFirst()
.orElse(INVALID_ACTION);
}
public String getCommand() {
return command;
}
public boolean isHasAddress() {
return hasAddress;
}
public boolean isNeedsData() {
return needsData;
}
}

View File

@ -0,0 +1,102 @@
/**
* Copyright (c) 2010-2022 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.luxom.internal.protocol;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* luxom command
*
* @author Kris Jespers - Initial contribution
*/
@NonNullByDefault
public class LuxomCommand {
private final LuxomAction action;
private final @Nullable String address; // must for data byte commands be set after construction
private @Nullable String data;
public LuxomCommand(String command) {
if (command.length() == 0) {
action = LuxomAction.INVALID_ACTION;
data = command;
address = null;
return;
}
String[] parts = command.split(",");
if (parts.length == 1) {
if (command.startsWith(LuxomAction.MODULE_INFORMATION.getCommand())) {
action = LuxomAction.MODULE_INFORMATION;
data = command.substring(2);
} else if (command.equals(LuxomAction.PASSWORD_REQUEST.getCommand())) {
action = LuxomAction.PASSWORD_REQUEST;
data = null;
} else if (command.equals(LuxomAction.ACKNOWLEDGE.getCommand())) {
action = LuxomAction.ACKNOWLEDGE;
data = null;
} else {
action = LuxomAction.INVALID_ACTION;
data = command;
}
address = null;
} else {
action = LuxomAction.of(parts[0]);
StringBuilder stringBuilder = new StringBuilder();
if (action.isHasAddress()) {
// first 0 not needed ?
for (int i = 2; i < parts.length; i++) {
stringBuilder.append(parts[i]);
if (i != (parts.length - 1)) {
stringBuilder.append(",");
}
}
address = stringBuilder.toString();
data = null;
} else {
for (int i = 1; i < parts.length; i++) {
stringBuilder.append(parts[i]);
if (i != (parts.length - 1)) {
stringBuilder.append(",");
}
}
address = null;
data = stringBuilder.toString();
}
}
}
@Override
public String toString() {
return "LuxomCommand{" + "action=" + action + ", address='" + address + '\'' + ", data='" + data + '\'' + '}';
}
public LuxomAction getAction() {
return action;
}
@Nullable
public String getData() {
return data;
}
@Nullable
public String getAddress() {
return address;
}
public void setData(@Nullable String data) {
this.data = data;
}
}

View File

@ -0,0 +1,210 @@
/**
* Copyright (c) 2010-2022 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.luxom.internal.protocol;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.Socket;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.luxom.internal.handler.LuxomBridgeHandler;
import org.openhab.binding.luxom.internal.handler.LuxomConnectionException;
import org.openhab.binding.luxom.internal.handler.config.LuxomBridgeConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link LuxomCommunication} class is able to do the following tasks with Luxom IP
* systems:
* <ul>
* <li>Start and stop TCP socket connection with Luxom IP-interface.
* <li>Read all setup and status information from the Luxom Controller.
* <li>Execute Luxom commands.
* <li>Listen to events from Luxom.
* </ul>
*
* @author Kris Jespers - Initial contribution
*/
@NonNullByDefault
public class LuxomCommunication {
private final Logger logger = LoggerFactory.getLogger(LuxomCommunication.class);
private final LuxomBridgeHandler bridgeHandler;
private @Nullable Socket luxomSocket;
private @Nullable PrintWriter luxomOut;
private @Nullable BufferedReader luxomIn;
private volatile boolean listenerStopped;
private volatile boolean stillListeningToEvents;
public LuxomCommunication(LuxomBridgeHandler luxomBridgeHandler) {
super();
bridgeHandler = luxomBridgeHandler;
}
public synchronized void startCommunication() throws LuxomConnectionException {
try {
waitForEventListenerThreadToStop();
initializeSocket();
// Start Luxom event listener. This listener will act on all messages coming from
// IP-interface.
(new Thread(this::runLuxomEvents,
"OH-binding-" + bridgeHandler.getThing().getBridgeUID() + "-listen-for-events")).start();
} catch (IOException | InterruptedException e) {
throw new LuxomConnectionException(e);
}
}
private void waitForEventListenerThreadToStop() throws InterruptedException, IOException {
for (int i = 1; stillListeningToEvents && (i <= 5); i++) {
// the events listener thread did not finish yet, so wait max 5000ms before restarting
// noinspection BusyWait
Thread.sleep(1000);
}
if (stillListeningToEvents) {
throw new IOException("starting but previous connection still active after 5000ms");
}
}
private void initializeSocket() throws IOException {
LuxomBridgeConfig luxomBridgeConfig = bridgeHandler.getIPBridgeConfig();
if (luxomBridgeConfig != null) {
InetAddress addr = InetAddress.getByName(luxomBridgeConfig.ipAddress);
int port = luxomBridgeConfig.port;
luxomSocket = new Socket(addr, port);
luxomSocket.setReuseAddress(true);
luxomSocket.setKeepAlive(true);
luxomOut = new PrintWriter(luxomSocket.getOutputStream());
luxomIn = new BufferedReader(new InputStreamReader(luxomSocket.getInputStream()));
logger.debug("Luxom: connected via local port {}", luxomSocket.getLocalPort());
} else {
logger.warn("Luxom: ip bridge not initialized");
}
}
/**
* Cleanup socket when the communication with Luxom IP-interface is closed.
*/
public synchronized void stopCommunication() {
listenerStopped = true;
closeSocket();
}
private void closeSocket() {
if (luxomSocket != null) {
try {
luxomSocket.close();
} catch (IOException ignore) {
// ignore IO Error when trying to close the socket if the intention is to close it anyway
}
}
luxomSocket = null;
logger.debug("Luxom: communication stopped");
}
/**
* Method that handles inbound communication from Luxom, to be called on a separate thread.
* <p>
* The thread listens to the TCP socket opened at instantiation of the {@link LuxomCommunication} class
* and interprets all inbound json messages. It triggers state updates for active channels linked to the Niko Home
* Control actions. It is started after initialization of the communication.
*/
private void runLuxomEvents() {
StringBuilder luxomMessage = new StringBuilder();
logger.debug("Luxom: listening for events");
listenerStopped = false;
stillListeningToEvents = true;
try {
boolean mayUseFastReconnect = false;
boolean mustDoFullReconnect = false;
while (!listenerStopped && (luxomIn != null)) {
int nextChar = luxomIn.read();
if (nextChar == -1) {
logger.trace("Luxom: stream ends unexpectedly...");
LuxomBridgeConfig luxomBridgeConfig = bridgeHandler.getIPBridgeConfig();
if (mayUseFastReconnect && luxomBridgeConfig != null && luxomBridgeConfig.useFastReconnect) {
// we stay in the loop and just reinitialize socket
mayUseFastReconnect = false; // just once use fast reconnect
this.closeSocket();
this.initializeSocket();
// followed by forced update of status
bridgeHandler.forceRefreshThings();
} else {
listenerStopped = true;
mustDoFullReconnect = true;
}
} else {
mayUseFastReconnect = true; // reset
char c = (char) nextChar;
logger.trace("Luxom: read char {}", c);
luxomMessage.append(c);
if (';' == c) {
String message = luxomMessage.toString();
bridgeHandler.handleIncomingLuxomMessage(message.substring(0, message.length() - 1));
luxomMessage = new StringBuilder();
}
}
}
if (mustDoFullReconnect) {
// I want to do this out of the loop
bridgeHandler.reconnect();
}
logger.trace("Luxom: stopped listening to events");
} catch (IOException e) {
logger.warn("Luxom: listening to events - IO exception", e);
if (!listenerStopped) {
stillListeningToEvents = false;
// this is a socket error, not a communication stop triggered from outside this runnable
// the IO has stopped working, so we need to close cleanly and try to restart
bridgeHandler.handleCommunicationError(e);
return;
}
} finally {
stillListeningToEvents = false;
}
// this is a stop from outside the runnable, so just log it and stop
logger.debug("Luxom: event listener thread stopped");
}
public synchronized void sendMessage(String message) throws IOException {
logger.debug("Luxom: send {}", message);
if (luxomOut != null) {
luxomOut.print(message + ";");
luxomOut.flush();
if (luxomOut.checkError()) {
throw new IOException(String.format("luxom communication error when sending message: %s", message));
}
}
}
public boolean isConnected() {
return luxomSocket != null && luxomSocket.isConnected();
}
}

View File

@ -0,0 +1,36 @@
/**
* Copyright (c) 2010-2022 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.luxom.internal.protocol;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link LuxomSystemInfo} class represents the systeminfo Luxom communication object. It contains all
* Luxom system data received from the Luxom IP controller when initializing the connection.
*
* @author Kris Jespers - Initial contribution
*/
@NonNullByDefault
public final class LuxomSystemInfo {
private @Nullable String swVersion = "";
public @Nullable String getSwVersion() {
return swVersion;
}
public void setSwVersion(@Nullable String swVersion) {
this.swVersion = swVersion;
}
}

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="luxom" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>Luxom Binding</name>
<description>This is the binding for Luxom bus system (https://www.luxom.io/)</description>
</binding:binding>

View File

@ -0,0 +1,38 @@
# binding
binding.luxom.name = Luxom Binding
binding.luxom.description = This is the binding for Luxom bus system (https://www.luxom.io/)
# thing types
thing-type.luxom.switch.label = Switch
thing-type.luxom.switch.description = Switch type action in Luxom
thing-type.luxom.dimmer.label = Dimmer
thing-type.luxom.dimmer.description = Dimmer type action in Luxom
# thing types config
thing-type.config.luxom.switch.address.label = Address
thing-type.config.luxom.switch.address.description = Luxom bus address
thing-type.config.luxom.dimmer.address.label = Address
thing-type.config.luxom.dimmer.address.description = Luxom bus address
thing-type.config.luxom.dimmer.onLevel.label = On Level
thing-type.config.luxom.dimmer.onLevel.description = Output level to go to when an ON command is received. Default is 100%.
thing-type.config.luxom.dimmer.onToLast.label = Turn On To Last Level
thing-type.config.luxom.dimmer.onToLast.description = If set to true, dimmer will go to the last non-zero level set when an ON command is received. If the last level cannot be determined, the value of onLevel will be used instead.
thing-type.config.luxom.dimmer.stepPercentage.label = Step Value
thing-type.config.luxom.dimmer.stepPercentage.description = Step value used for increase/decrease of dimmer brightness, default 5%
# channel types
channel-type.luxom.button.label = Button
channel-type.luxom.button.description = Pushbutton control for action in Luxom
# messages
status.awaiting-initial-response = Awaiting initial response
status.bridge-configuration-missing = Bridge configuration missing
status.connecting = Connecting
status.bridge-address-missing = Bridge address not specified
status.bridge-initializing = Luxom bridge is initializing
status.bridge-handler-missing = No bridge associated
status.thing-address-missing = Address is missing

View File

@ -0,0 +1,96 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="luxom"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<bridge-type id="bridge">
<label>Luxom Bridge</label>
<description>This bridge represents a Luxom IP-interface (for example a DS-65L)</description>
<config-description>
<parameter name="ipAddress" type="text" required="true">
<context>network-address</context>
<label>IP or Host Name</label>
<description>The IP or host name of the Luxom IP-interface</description>
</parameter>
<parameter name="port" type="integer" required="true">
<label>Bridge Port</label>
<description>Port to communicate with Luxom IP-interface, default 2300</description>
<default>2300</default>
<advanced>true</advanced>
</parameter>
<parameter name="reconnectInterval" type="integer" min="1" max="60" unit="min">
<label>Reconnect Interval</label>
<description>The period in minutes that the handler will wait between connection attempts after disconnect</description>
<unitLabel>minutes</unitLabel>
<default>1</default>
<advanced>true</advanced>
</parameter>
</config-description>
</bridge-type>
<thing-type id="switch">
<supported-bridge-type-refs>
<bridge-type-ref id="bridge"/>
</supported-bridge-type-refs>
<label>switch</label>
<description>Luxom Switch</description>
<channels>
<channel id="switch" typeId="switchState"/>
</channels>
<representation-property>address</representation-property>
<config-description>
<parameter name="address" type="text" required="true">
<label>Address</label>
<description>Luxom bus address</description>
<advanced>false</advanced>
</parameter>
</config-description>
</thing-type>
<thing-type id="dimmer">
<supported-bridge-type-refs>
<bridge-type-ref id="bridge"/>
</supported-bridge-type-refs>
<label>Dimmer</label>
<description>Luxom Dimmer</description>
<channels>
<channel id="brightness" typeId="system.brightness"/>
</channels>
<config-description>
<parameter name="address" type="text" required="true">
<label>Address</label>
<description>Luxom bus address</description>
<advanced>false</advanced>
</parameter>
<parameter name="onLevel" type="decimal" min="0.01" max="100.00">
<label>On Level</label>
<description>Output level to go to when an ON command is received. Default is 100%.</description>
<default>100</default>
<advanced>true</advanced>
</parameter>
<parameter name="onToLast" type="boolean">
<label>Turn On To Last Level</label>
<description>
If set to true, dimmer will go to the last non-zero level set when an ON command is received. If the
last level cannot be determined, the value of onLevel will be used instead.
</description>
<default>false</default>
<advanced>true</advanced>
</parameter>
<parameter name="stepPercentage" type="integer" required="false">
<label>Step Value</label>
<description>Step value used for increase/decrease of dimmer brightness, default 5%</description>
<default>5</default>
<advanced>true</advanced>
</parameter>
</config-description>
</thing-type>
<channel-type id="switchState">
<item-type>Switch</item-type>
<label>Switch</label>
<description>Switch control for action in Luxom</description>
<category>Switch</category>
<autoUpdatePolicy>veto</autoUpdatePolicy>
</channel-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,115 @@
/**
* Copyright (c) 2010-2022 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.luxom.internal.protocol;
import static org.junit.jupiter.api.Assertions.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
/**
*
* @author Kris Jespers - Initial contribution
*/
@NonNullByDefault
class LuxomCommandTest {
@Test
void parsePulseCommand() {
LuxomCommand command = new LuxomCommand("*P,0,1,04");
assertEquals(LuxomAction.PING, command.getAction());
assertEquals("1,04", command.getAddress());
assertNull(command.getData());
}
@Test
void parsePasswordRequest() {
LuxomCommand command = new LuxomCommand(LuxomAction.PASSWORD_REQUEST.getCommand());
assertEquals(LuxomAction.PASSWORD_REQUEST, command.getAction());
assertNull(command.getData());
}
@Test
void parseClearCommand() {
LuxomCommand command = new LuxomCommand("*C,0,1,04");
assertEquals(LuxomAction.CLEAR, command.getAction());
assertEquals("1,04", command.getAddress());
assertNull(command.getData());
}
@Test
void parseClearResponse() {
LuxomCommand command = new LuxomCommand("@1*C,0,1,04");
assertEquals(LuxomAction.CLEAR_RESPONSE, command.getAction());
assertEquals("1,04", command.getAddress());
assertNull(command.getData());
}
@Test
void parseClearResponse2() {
LuxomCommand command = new LuxomCommand("@1*C,0,1,04");
assertEquals(LuxomAction.CLEAR_RESPONSE, command.getAction());
assertEquals("1,04", command.getAddress());
assertNull(command.getData());
}
@Test
void parseSetCommand() {
LuxomCommand command = new LuxomCommand("*S,0,1,04");
assertEquals(LuxomAction.SET, command.getAction());
assertEquals("1,04", command.getAddress());
assertNull(command.getData());
}
@Test
void parseSetResponse() {
LuxomCommand command = new LuxomCommand("@1*S,0,1,04");
assertEquals(LuxomAction.SET_RESPONSE, command.getAction());
assertEquals("1,04", command.getAddress());
assertNull(command.getData());
}
@Test
void parseDimCommand() {
LuxomCommand command = new LuxomCommand("*A,0,1,04");
assertEquals(LuxomAction.DATA, command.getAction());
assertEquals("1,04", command.getAddress());
assertNull(command.getData());
}
@Test
void parseDataCommand() {
LuxomCommand command = new LuxomCommand("*Z,048");
assertEquals(LuxomAction.DATA_BYTE, command.getAction());
assertEquals("048", command.getData());
assertNull(command.getAddress());
}
@Test
void parseDataResponseCommand() {
LuxomCommand command = new LuxomCommand("@1*Z,048");
assertEquals(LuxomAction.DATA_BYTE_RESPONSE, command.getAction());
assertEquals("048", command.getData());
assertNull(command.getAddress());
}
}

View File

@ -0,0 +1,41 @@
/**
* Copyright (c) 2010-2022 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.luxom.internal.protocol;
import static org.junit.jupiter.api.Assertions.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.binding.luxom.internal.handler.util.PercentageConverter;
/**
*
* @author Kris Jespers - Initial contribution
*/
@NonNullByDefault
class PercentageConverterTest {
@Test
void hexToPercentage() {
assertEquals(34, PercentageConverter.getPercentage("057"));
}
@Test
void hexToPercentage100() {
assertEquals(100, PercentageConverter.getPercentage("0FF"));
}
@Test
void percentageToHex() {
assertEquals("57", PercentageConverter.getHexRepresentation(34));
}
}

View File

@ -201,6 +201,7 @@
<module>org.openhab.binding.loxone</module>
<module>org.openhab.binding.luftdateninfo</module>
<module>org.openhab.binding.lutron</module>
<module>org.openhab.binding.luxom</module>
<module>org.openhab.binding.luxtronikheatpump</module>
<module>org.openhab.binding.magentatv</module>
<module>org.openhab.binding.mail</module>