From 873d615316ae8276a9f1e579ce98f3f006746a90 Mon Sep 17 00:00:00 2001 From: jesperskriasoft <80278903+jesperskriasoft@users.noreply.github.com> Date: Tue, 5 Apr 2022 20:02:27 +0200 Subject: [PATCH] [luxom] Initial contribution (#12310) Signed-off-by: Kris Jespers --- CODEOWNERS | 1 + bom/openhab-addons/pom.xml | 5 + bundles/org.openhab.binding.luxom/NOTICE | 13 + bundles/org.openhab.binding.luxom/README.md | 98 +++++ bundles/org.openhab.binding.luxom/pom.xml | 17 + .../src/main/feature/feature.xml | 9 + .../luxom/internal/LuxomBindingConstants.java | 46 +++ .../luxom/internal/LuxomHandlerFactory.java | 54 +++ .../CommandExecutionSpecification.java | 31 ++ .../internal/handler/LuxomBridgeHandler.java | 345 ++++++++++++++++++ .../handler/LuxomConnectionException.java | 29 ++ .../internal/handler/LuxomDimmerHandler.java | 173 +++++++++ .../internal/handler/LuxomSwitchHandler.java | 101 +++++ .../internal/handler/LuxomThingHandler.java | 130 +++++++ .../handler/config/LuxomBridgeConfig.java | 47 +++ .../handler/config/LuxomThingConfig.java | 25 ++ .../config/LuxomThingDimmerConfig.java | 31 ++ .../handler/util/PercentageConverter.java | 44 +++ .../luxom/internal/protocol/LuxomAction.java | 73 ++++ .../luxom/internal/protocol/LuxomCommand.java | 102 ++++++ .../internal/protocol/LuxomCommunication.java | 210 +++++++++++ .../internal/protocol/LuxomSystemInfo.java | 36 ++ .../main/resources/OH-INF/binding/binding.xml | 8 + .../resources/OH-INF/i18n/luxom.properties | 38 ++ .../resources/OH-INF/thing/thing-types.xml | 96 +++++ .../internal/protocol/LuxomCommandTest.java | 115 ++++++ .../protocol/PercentageConverterTest.java | 41 +++ bundles/pom.xml | 1 + 28 files changed, 1919 insertions(+) create mode 100644 bundles/org.openhab.binding.luxom/NOTICE create mode 100644 bundles/org.openhab.binding.luxom/README.md create mode 100644 bundles/org.openhab.binding.luxom/pom.xml create mode 100644 bundles/org.openhab.binding.luxom/src/main/feature/feature.xml create mode 100644 bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/LuxomBindingConstants.java create mode 100644 bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/LuxomHandlerFactory.java create mode 100644 bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/CommandExecutionSpecification.java create mode 100644 bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/LuxomBridgeHandler.java create mode 100644 bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/LuxomConnectionException.java create mode 100644 bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/LuxomDimmerHandler.java create mode 100644 bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/LuxomSwitchHandler.java create mode 100644 bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/LuxomThingHandler.java create mode 100644 bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/config/LuxomBridgeConfig.java create mode 100644 bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/config/LuxomThingConfig.java create mode 100644 bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/config/LuxomThingDimmerConfig.java create mode 100644 bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/util/PercentageConverter.java create mode 100644 bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/protocol/LuxomAction.java create mode 100644 bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/protocol/LuxomCommand.java create mode 100644 bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/protocol/LuxomCommunication.java create mode 100644 bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/protocol/LuxomSystemInfo.java create mode 100644 bundles/org.openhab.binding.luxom/src/main/resources/OH-INF/binding/binding.xml create mode 100644 bundles/org.openhab.binding.luxom/src/main/resources/OH-INF/i18n/luxom.properties create mode 100644 bundles/org.openhab.binding.luxom/src/main/resources/OH-INF/thing/thing-types.xml create mode 100644 bundles/org.openhab.binding.luxom/src/test/java/org/openhab/binding/luxom/internal/protocol/LuxomCommandTest.java create mode 100644 bundles/org.openhab.binding.luxom/src/test/java/org/openhab/binding/luxom/internal/protocol/PercentageConverterTest.java diff --git a/CODEOWNERS b/CODEOWNERS index b75c95be694..51771a55b96 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -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 diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index 9bd4c43c6c7..2a983e6dcb6 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -836,6 +836,11 @@ org.openhab.binding.lutron ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.luxom + ${project.version} + org.openhab.addons.bundles org.openhab.binding.luxtronikheatpump diff --git a/bundles/org.openhab.binding.luxom/NOTICE b/bundles/org.openhab.binding.luxom/NOTICE new file mode 100644 index 00000000000..38d625e3492 --- /dev/null +++ b/bundles/org.openhab.binding.luxom/NOTICE @@ -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 diff --git a/bundles/org.openhab.binding.luxom/README.md b/bundles/org.openhab.binding.luxom/README.md new file mode 100644 index 00000000000..56a1a1d7f16 --- /dev/null +++ b/bundles/org.openhab.binding.luxom/README.md @@ -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" (FF_Living, gLight) ["Lighting"] {channel="luxom:dimmer:myhouse:dimmerBedroom1:brightness", ga="Light", homekit="Lighting, Lighting.Brightness"} +Switch FF_Bedroom_PowerOutlet1 "Bedroom Power Outlet 1" (FF_Living, gPower) ["Switchable"] {channel="luxom:switch:myhouse:switchBedroom1:switch", ga="Outlet"} +Dimmer FF_Kitchen_Lights "Kitchen dimmer light" (FF_Kitchen, gLight) ["Lighting"] {channel="luxom:dimmer:myhouse:dimmerKitchen1:brightness", ga="Light", homekit="Lighting, Lighting.Brightness"} +``` diff --git a/bundles/org.openhab.binding.luxom/pom.xml b/bundles/org.openhab.binding.luxom/pom.xml new file mode 100644 index 00000000000..fe49117f573 --- /dev/null +++ b/bundles/org.openhab.binding.luxom/pom.xml @@ -0,0 +1,17 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 3.3.0-SNAPSHOT + + + org.openhab.binding.luxom + + openHAB Add-ons :: Bundles :: Luxom Binding + + diff --git a/bundles/org.openhab.binding.luxom/src/main/feature/feature.xml b/bundles/org.openhab.binding.luxom/src/main/feature/feature.xml new file mode 100644 index 00000000000..df4159c59f3 --- /dev/null +++ b/bundles/org.openhab.binding.luxom/src/main/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.luxom/${project.version} + + diff --git a/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/LuxomBindingConstants.java b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/LuxomBindingConstants.java new file mode 100644 index 00000000000..d974cd92b26 --- /dev/null +++ b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/LuxomBindingConstants.java @@ -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 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"; +} diff --git a/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/LuxomHandlerFactory.java b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/LuxomHandlerFactory.java new file mode 100644 index 00000000000..57ea4e7f8fb --- /dev/null +++ b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/LuxomHandlerFactory.java @@ -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; + } +} diff --git a/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/CommandExecutionSpecification.java b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/CommandExecutionSpecification.java new file mode 100644 index 00000000000..ff16b48eed7 --- /dev/null +++ b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/CommandExecutionSpecification.java @@ -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; + } +} diff --git a/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/LuxomBridgeHandler.java b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/LuxomBridgeHandler.java new file mode 100644 index 00000000000..c1e5e1cdb36 --- /dev/null +++ b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/LuxomBridgeHandler.java @@ -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> 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 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 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); + } +} diff --git a/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/LuxomConnectionException.java b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/LuxomConnectionException.java new file mode 100644 index 00000000000..1dc011a7b27 --- /dev/null +++ b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/LuxomConnectionException.java @@ -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); + } +} diff --git a/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/LuxomDimmerHandler.java b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/LuxomDimmerHandler.java new file mode 100644 index 00000000000..e8d6db34b2d --- /dev/null +++ b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/LuxomDimmerHandler.java @@ -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 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 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); + } +} diff --git a/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/LuxomSwitchHandler.java b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/LuxomSwitchHandler.java new file mode 100644 index 00000000000..3b27fb12ed0 --- /dev/null +++ b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/LuxomSwitchHandler.java @@ -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(); + } + } +} diff --git a/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/LuxomThingHandler.java b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/LuxomThingHandler.java new file mode 100644 index 00000000000..cf828502340 --- /dev/null +++ b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/LuxomThingHandler.java @@ -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 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()))); + } +} diff --git a/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/config/LuxomBridgeConfig.java b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/config/LuxomBridgeConfig.java new file mode 100644 index 00000000000..52bbd5c8360 --- /dev/null +++ b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/config/LuxomBridgeConfig.java @@ -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); + } +} diff --git a/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/config/LuxomThingConfig.java b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/config/LuxomThingConfig.java new file mode 100644 index 00000000000..7a7de1a1a55 --- /dev/null +++ b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/config/LuxomThingConfig.java @@ -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; +} diff --git a/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/config/LuxomThingDimmerConfig.java b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/config/LuxomThingDimmerConfig.java new file mode 100644 index 00000000000..84c25ea17b3 --- /dev/null +++ b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/config/LuxomThingDimmerConfig.java @@ -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; +} diff --git a/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/util/PercentageConverter.java b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/util/PercentageConverter.java new file mode 100644 index 00000000000..965943ee3ee --- /dev/null +++ b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/handler/util/PercentageConverter.java @@ -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(); + } +} diff --git a/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/protocol/LuxomAction.java b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/protocol/LuxomAction.java new file mode 100644 index 00000000000..bf7fba4a872 --- /dev/null +++ b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/protocol/LuxomAction.java @@ -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; + } +} diff --git a/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/protocol/LuxomCommand.java b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/protocol/LuxomCommand.java new file mode 100644 index 00000000000..ce39802447c --- /dev/null +++ b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/protocol/LuxomCommand.java @@ -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; + } +} diff --git a/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/protocol/LuxomCommunication.java b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/protocol/LuxomCommunication.java new file mode 100644 index 00000000000..9dc0a97a29c --- /dev/null +++ b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/protocol/LuxomCommunication.java @@ -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: + *
    + *
  • Start and stop TCP socket connection with Luxom IP-interface. + *
  • Read all setup and status information from the Luxom Controller. + *
  • Execute Luxom commands. + *
  • Listen to events from Luxom. + *
+ * + * @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. + *

+ * 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(); + } +} diff --git a/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/protocol/LuxomSystemInfo.java b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/protocol/LuxomSystemInfo.java new file mode 100644 index 00000000000..7e20eabf9b1 --- /dev/null +++ b/bundles/org.openhab.binding.luxom/src/main/java/org/openhab/binding/luxom/internal/protocol/LuxomSystemInfo.java @@ -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; + } +} diff --git a/bundles/org.openhab.binding.luxom/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.luxom/src/main/resources/OH-INF/binding/binding.xml new file mode 100644 index 00000000000..8f0c1d886b8 --- /dev/null +++ b/bundles/org.openhab.binding.luxom/src/main/resources/OH-INF/binding/binding.xml @@ -0,0 +1,8 @@ + + + + Luxom Binding + This is the binding for Luxom bus system (https://www.luxom.io/) + diff --git a/bundles/org.openhab.binding.luxom/src/main/resources/OH-INF/i18n/luxom.properties b/bundles/org.openhab.binding.luxom/src/main/resources/OH-INF/i18n/luxom.properties new file mode 100644 index 00000000000..0b7aa475c72 --- /dev/null +++ b/bundles/org.openhab.binding.luxom/src/main/resources/OH-INF/i18n/luxom.properties @@ -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 diff --git a/bundles/org.openhab.binding.luxom/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.luxom/src/main/resources/OH-INF/thing/thing-types.xml new file mode 100644 index 00000000000..58060d25ea5 --- /dev/null +++ b/bundles/org.openhab.binding.luxom/src/main/resources/OH-INF/thing/thing-types.xml @@ -0,0 +1,96 @@ + + + + + + This bridge represents a Luxom IP-interface (for example a DS-65L) + + + network-address + + The IP or host name of the Luxom IP-interface + + + + Port to communicate with Luxom IP-interface, default 2300 + 2300 + true + + + + The period in minutes that the handler will wait between connection attempts after disconnect + minutes + 1 + true + + + + + + + + + + Luxom Switch + + + + address + + + + Luxom bus address + false + + + + + + + + + Luxom Dimmer + + + + + + + Luxom bus address + false + + + + Output level to go to when an ON command is received. Default is 100%. + 100 + true + + + + + 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. + + false + true + + + + Step value used for increase/decrease of dimmer brightness, default 5% + 5 + true + + + + + + Switch + + Switch control for action in Luxom + Switch + veto + + diff --git a/bundles/org.openhab.binding.luxom/src/test/java/org/openhab/binding/luxom/internal/protocol/LuxomCommandTest.java b/bundles/org.openhab.binding.luxom/src/test/java/org/openhab/binding/luxom/internal/protocol/LuxomCommandTest.java new file mode 100644 index 00000000000..ddeb795271f --- /dev/null +++ b/bundles/org.openhab.binding.luxom/src/test/java/org/openhab/binding/luxom/internal/protocol/LuxomCommandTest.java @@ -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()); + } +} diff --git a/bundles/org.openhab.binding.luxom/src/test/java/org/openhab/binding/luxom/internal/protocol/PercentageConverterTest.java b/bundles/org.openhab.binding.luxom/src/test/java/org/openhab/binding/luxom/internal/protocol/PercentageConverterTest.java new file mode 100644 index 00000000000..e4048206258 --- /dev/null +++ b/bundles/org.openhab.binding.luxom/src/test/java/org/openhab/binding/luxom/internal/protocol/PercentageConverterTest.java @@ -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)); + } +} diff --git a/bundles/pom.xml b/bundles/pom.xml index 4e1d0c9a8d6..f2eb2e1bf08 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -201,6 +201,7 @@ org.openhab.binding.loxone org.openhab.binding.luftdateninfo org.openhab.binding.lutron + org.openhab.binding.luxom org.openhab.binding.luxtronikheatpump org.openhab.binding.magentatv org.openhab.binding.mail