]> git.basschouten.com Git - openhab-addons.git/commitdiff
[revogi] Initial contribution - Resubmitted for OH3 (#8534)
authorAndi Bräu <andibraeu@users.noreply.github.com>
Tue, 3 Nov 2020 17:13:08 +0000 (18:13 +0100)
committerGitHub <noreply@github.com>
Tue, 3 Nov 2020 17:13:08 +0000 (09:13 -0800)
* Rename binding and resubmit to OH3

Signed-off-by: Andreas Bräu <ab@andi95.de>
30 files changed:
CODEOWNERS
bom/openhab-addons/pom.xml
bundles/org.openhab.binding.revogi/NOTICE [new file with mode: 0644]
bundles/org.openhab.binding.revogi/README.md [new file with mode: 0644]
bundles/org.openhab.binding.revogi/pom.xml [new file with mode: 0644]
bundles/org.openhab.binding.revogi/src/main/feature/feature.xml [new file with mode: 0644]
bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/RevogiSmartStripControlBindingConstants.java [new file with mode: 0644]
bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/RevogiSmartStripControlConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/RevogiSmartStripControlHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/RevogiSmartStripControlHandlerFactory.java [new file with mode: 0644]
bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/RevogiSmartStripDiscoveryService.java [new file with mode: 0644]
bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/api/DiscoveryRawResponseDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/api/DiscoveryResponseDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/api/RevogiDiscoveryService.java [new file with mode: 0644]
bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/api/StatusDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/api/StatusRawDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/api/StatusService.java [new file with mode: 0644]
bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/api/SwitchResponseDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/api/SwitchService.java [new file with mode: 0644]
bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/udp/DatagramSocketWrapper.java [new file with mode: 0644]
bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/udp/UdpResponseDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/udp/UdpSenderService.java [new file with mode: 0644]
bundles/org.openhab.binding.revogi/src/main/resources/OH-INF/binding/binding.xml [new file with mode: 0644]
bundles/org.openhab.binding.revogi/src/main/resources/OH-INF/i18n/revogi_de.properties [new file with mode: 0644]
bundles/org.openhab.binding.revogi/src/main/resources/OH-INF/thing/thing-types.xml [new file with mode: 0644]
bundles/org.openhab.binding.revogi/src/test/java/org/openhab/binding/revogi/internal/api/RevogiDiscoveryServiceTest.java [new file with mode: 0644]
bundles/org.openhab.binding.revogi/src/test/java/org/openhab/binding/revogi/internal/api/StatusServiceTest.java [new file with mode: 0644]
bundles/org.openhab.binding.revogi/src/test/java/org/openhab/binding/revogi/internal/api/SwitchServiceTest.java [new file with mode: 0644]
bundles/org.openhab.binding.revogi/src/test/java/org/openhab/binding/revogi/internal/udp/UdpSenderServiceTest.java [new file with mode: 0644]
bundles/pom.xml

index d056887e994461c8a2d4c4608e63c677b6fbc126..e249124e5b7a61fe654ec767f7888b7cd6132b2e 100644 (file)
 /bundles/org.openhab.binding.pushbullet/ @hakan42
 /bundles/org.openhab.binding.radiothermostat/ @mlobstein
 /bundles/org.openhab.binding.regoheatpump/ @crnjan
+/bundles/org.openhab.binding.revogi/ @andibraeu
 /bundles/org.openhab.binding.remoteopenhab/ @lolodomo
 /bundles/org.openhab.binding.rfxcom/ @martinvw @paulianttila
 /bundles/org.openhab.binding.rme/ @kgoderis
index fc0b0d2c432b887444915fd845f74505ced47952..73c4fd1e40512ff013d21749dbc965ab690e52be 100644 (file)
       <artifactId>org.openhab.binding.regoheatpump</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.openhab.addons.bundles</groupId>
+      <artifactId>org.openhab.binding.revogi</artifactId>
+      <version>${project.version}</version>
+    </dependency>
     <dependency>
       <groupId>org.openhab.addons.bundles</groupId>
       <artifactId>org.openhab.binding.remoteopenhab</artifactId>
diff --git a/bundles/org.openhab.binding.revogi/NOTICE b/bundles/org.openhab.binding.revogi/NOTICE
new file mode 100644 (file)
index 0000000..38d625e
--- /dev/null
@@ -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.revogi/README.md b/bundles/org.openhab.binding.revogi/README.md
new file mode 100644 (file)
index 0000000..4c1a9d6
--- /dev/null
@@ -0,0 +1,78 @@
+# Revogi Binding
+
+This binding is written to control Revogi devices. 
+The first thing implemented is the [Revogi Smart Power Strip](https://www.revogi.com/smart-power/smart-power-strip-eu/#section6). 
+The device has 6 power plugs that can be switched independently, or all together. 
+It also provides information like power consumption and electric current for each plug.
+
+It was hard to find out how to control it without internet access, but there's a way to use UDP packets. 
+See the following [support document](https://github.com/andibraeu/revogismartstripcontrol/blob/master/doc/LAN%20UDP%20Control.pdf) for details. This was the only document the Revogi support provided.
+
+## Supported Things
+
+Currently only the model `SOW019` is supported.
+
+## Discovery
+
+If your smart strip is within your network (broadcast domain), discovery can work. 
+The discovery service will send udp packets to the broadcast address and waits for a feedback.
+
+It is required to integrate your power strip into your network first, maybe with the official app.
+
+## Thing Configuration
+
+You need to know the serial number. Usually you can find it on the back. 
+The serial number will also be discovered. 
+The IP address of the device is also necessary, this address should be set static. 
+There's a fallback to broadcast status and switch requests. 
+That may be unreliable if you have more than one smart plug in your network. 
+They all react on UDP packets.
+
+## Channels
+
+| channel            | type                   | description                               |
+|--------------------|------------------------|-------------------------------------------|
+| overallPlug#switch | Switch                 | Switches all plugs                        |
+| plug1#switch       | Switch                 | Switch plug 1                             |
+| plug1#watt         | Number:Power           | Contains currently used power of plug 1   |
+| plug1#amp          | Number:ElectricCurrent | Contains currently used current of plug 1 |
+| plug2#switch       | Switch                 | Switch plug 2                             |
+| plug2#watt         | Number:Power           | Contains currently used power of plug 2   |
+| plug2#amp          | Number:ElectricCurrent | Contains currently used current of plug 2 |
+| plug3#switch       | Switch                 | Switch plug 3                             |
+| plug3#watt         | Number:Power           | Contains currently used power of plug 3   |
+| plug3#amp          | Number:ElectricCurrent | Contains currently used current of plug 3 |
+| plug4#switch       | Switch                 | Switch plug 4                             |
+| plug4#watt         | Number:Power           | Contains currently used power of plug 4   |
+| plug4#amp          | Number:ElectricCurrent | Contains currently used current of plug 4 |
+| plug5#switch       | Switch                 | Switch plug 5                             |
+| plug5#watt         | Number:Power           | Contains currently used power of plug 5   |
+| plug5#amp          | Number:ElectricCurrent | Contains currently used current of plug 5 |
+| plug6#switch       | Switch                 | Switch plug 6                             |
+| plug6#watt         | Number:Power           | Contains currently used power of plug 6   |
+| plug6#amp          | Number:ElectricCurrent | Contains currently used current of plug 6 |
+
+## Full Example
+
+Example Thing configuration:
+
+```
+Thing revogi:smartstrip:<serialNumber> "<Name>" @ "<Location>" [serialNumber="<serialNumnber>", ipAddress=<ipaddress>, pollIntervall=45]
+```
+
+Example Items configuration:
+
+```
+Group revogi (LivingRoom)
+
+Group plug1 (revogi)
+Group plug2 (revogi)
+
+Switch All_Plugs "Steckdosen komplett" <switch> (revogi) {channel="revogi:smartstrip:<serialNumnber>:overallPlug#switch"}
+
+Switch Plug_1 "Steckdose 1" <switch> (plug1) {channel="revogi:smartstrip:<serialNumnber>:plug1#switch"}
+Number Plug_1_Watt "Steckdose 1 Leistung" <chart> (plug1) {channel="revogi:smartstrip:<serialNumnber>:plug1#watt"}
+Number Plug_1_Amp "Steckdose 1 Strom" <chart> (plug1) {channel="revogi:smartstrip:<serialNumnber>:plug1#amp"}
+
+...
+```
diff --git a/bundles/org.openhab.binding.revogi/pom.xml b/bundles/org.openhab.binding.revogi/pom.xml
new file mode 100644 (file)
index 0000000..d0c6566
--- /dev/null
@@ -0,0 +1,18 @@
+<?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.0.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>org.openhab.binding.revogi</artifactId>
+
+  <name>openHAB Add-ons :: Bundles :: Revogi Binding</name>
+
+
+</project>
diff --git a/bundles/org.openhab.binding.revogi/src/main/feature/feature.xml b/bundles/org.openhab.binding.revogi/src/main/feature/feature.xml
new file mode 100644 (file)
index 0000000..c9cd827
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<features name="org.openhab.binding.revogi-${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-revogi" description="Revogi Binding" version="${project.version}">
+               <feature>openhab-runtime-base</feature>
+               <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.revogi/${project.version}</bundle>
+       </feature>
+</features>
diff --git a/bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/RevogiSmartStripControlBindingConstants.java b/bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/RevogiSmartStripControlBindingConstants.java
new file mode 100644 (file)
index 0000000..8bc3c99
--- /dev/null
@@ -0,0 +1,42 @@
+/**
+ * Copyright (c) 2010-2020 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.revogi.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * The {@link RevogiSmartStripControlBindingConstants} class defines common constants, which are
+ * used across the whole binding.
+ *
+ * @author Andi Bräu - Initial contribution
+ */
+@NonNullByDefault
+public class RevogiSmartStripControlBindingConstants {
+
+    private static final String BINDING_ID = "revogi";
+
+    // List of all Thing Type UIDs
+    public static final ThingTypeUID SMART_STRIP_THING_TYPE = new ThingTypeUID(BINDING_ID, "smartstrip");
+
+    // List of all Channel ids
+    public static final String PLUG_1_SWITCH = "plug1#switch";
+    public static final String PLUG_2_SWITCH = "plug2#switch";
+    public static final String PLUG_3_SWITCH = "plug3#switch";
+    public static final String PLUG_4_SWITCH = "plug4#switch";
+    public static final String PLUG_5_SWITCH = "plug5#switch";
+    public static final String PLUG_6_SWITCH = "plug6#switch";
+    public static final String ALL_PLUGS = "overallPlug#switch";
+
+    public static final String SERIAL_NUMBER = "serialNumber";
+}
diff --git a/bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/RevogiSmartStripControlConfiguration.java b/bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/RevogiSmartStripControlConfiguration.java
new file mode 100644 (file)
index 0000000..037ed58
--- /dev/null
@@ -0,0 +1,31 @@
+/**
+ * Copyright (c) 2010-2020 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.revogi.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link RevogiSmartStripControlConfiguration} class contains fields mapping thing configuration parameters.
+ *
+ * @author Andi Bräu - Initial contribution
+ */
+
+@NonNullByDefault
+public class RevogiSmartStripControlConfiguration {
+
+    public String serialNumber = "Serial Number";
+
+    public int pollInterval = 60;
+
+    public String ipAddress = "127.0.0.1";
+}
diff --git a/bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/RevogiSmartStripControlHandler.java b/bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/RevogiSmartStripControlHandler.java
new file mode 100644 (file)
index 0000000..dacadfb
--- /dev/null
@@ -0,0 +1,166 @@
+/**
+ * Copyright (c) 2010-2020 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.revogi.internal;
+
+import static org.openhab.core.library.unit.MetricPrefix.MILLI;
+import static org.openhab.core.library.unit.SmartHomeUnits.AMPERE;
+import static org.openhab.core.library.unit.SmartHomeUnits.WATT;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.revogi.internal.api.StatusDTO;
+import org.openhab.binding.revogi.internal.api.StatusService;
+import org.openhab.binding.revogi.internal.api.SwitchService;
+import org.openhab.binding.revogi.internal.udp.DatagramSocketWrapper;
+import org.openhab.binding.revogi.internal.udp.UdpSenderService;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.QuantityType;
+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.BaseThingHandler;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link RevogiSmartStripControlHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Andi Bräu - Initial contribution
+ */
+@NonNullByDefault
+public class RevogiSmartStripControlHandler extends BaseThingHandler {
+
+    private final Logger logger = LoggerFactory.getLogger(RevogiSmartStripControlHandler.class);
+    private final StatusService statusService;
+    private final SwitchService switchService;
+    private @Nullable ScheduledFuture<?> pollingJob;
+
+    private RevogiSmartStripControlConfiguration config;
+
+    public RevogiSmartStripControlHandler(Thing thing) {
+        super(thing);
+        config = getConfigAs(RevogiSmartStripControlConfiguration.class);
+        UdpSenderService udpSenderService = new UdpSenderService(new DatagramSocketWrapper(), scheduler);
+        this.statusService = new StatusService(udpSenderService);
+        this.switchService = new SwitchService(udpSenderService);
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        switch (channelUID.getId()) {
+            case RevogiSmartStripControlBindingConstants.PLUG_1_SWITCH:
+                switchPlug(command, 1);
+                break;
+            case RevogiSmartStripControlBindingConstants.PLUG_2_SWITCH:
+                switchPlug(command, 2);
+                break;
+            case RevogiSmartStripControlBindingConstants.PLUG_3_SWITCH:
+                switchPlug(command, 3);
+                break;
+            case RevogiSmartStripControlBindingConstants.PLUG_4_SWITCH:
+                switchPlug(command, 4);
+                break;
+            case RevogiSmartStripControlBindingConstants.PLUG_5_SWITCH:
+                switchPlug(command, 5);
+                break;
+            case RevogiSmartStripControlBindingConstants.PLUG_6_SWITCH:
+                switchPlug(command, 6);
+                break;
+            case RevogiSmartStripControlBindingConstants.ALL_PLUGS:
+                switchPlug(command, 0);
+                break;
+            default:
+                logger.debug("Something went wrong, we've got a message for {}", channelUID.getId());
+        }
+    }
+
+    private void switchPlug(Command command, int port) {
+        RevogiSmartStripControlConfiguration localConfig = this.config;
+        if (command instanceof OnOffType) {
+            int state = convertOnOffTypeToState(command);
+            switchService.switchPort(localConfig.serialNumber, localConfig.ipAddress, port, state);
+        }
+        if (command instanceof RefreshType) {
+            updateStripInformation();
+        }
+    }
+
+    private int convertOnOffTypeToState(Command command) {
+        if (command == OnOffType.ON) {
+            return 1;
+        } else {
+            return 0;
+        }
+    }
+
+    @Override
+    public void initialize() {
+        config = getConfigAs(RevogiSmartStripControlConfiguration.class);
+        updateStatus(ThingStatus.UNKNOWN);
+
+        pollingJob = scheduler.scheduleWithFixedDelay(this::updateStripInformation, 0, config.pollInterval,
+                TimeUnit.SECONDS);
+    }
+
+    @Override
+    public void dispose() {
+        super.dispose();
+        ScheduledFuture<?> localPollingJob = this.pollingJob;
+        if (localPollingJob != null) {
+            localPollingJob.cancel(true);
+            this.pollingJob = null;
+        }
+    }
+
+    private void updateStripInformation() {
+        CompletableFuture<StatusDTO> futureStatus = statusService.queryStatus(config.serialNumber, config.ipAddress);
+        futureStatus.thenAccept(this::updatePlugStatus);
+    }
+
+    private void updatePlugStatus(StatusDTO status) {
+        if (status.isOnline()) {
+            updateStatus(ThingStatus.ONLINE);
+            handleAllPlugsInformation(status);
+            handleSinglePlugInformation(status);
+        } else {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE,
+                    "Retrieved status code: " + status.getResponseCode());
+        }
+    }
+
+    private void handleSinglePlugInformation(StatusDTO status) {
+        for (int i = 0; i < status.getSwitchValue().size(); i++) {
+            int plugNumber = i + 1;
+            updateState("plug" + plugNumber + "#switch", OnOffType.from(status.getSwitchValue().get(i).toString()));
+            updateState("plug" + plugNumber + "#watt", new QuantityType<>(status.getWatt().get(i), MILLI(WATT)));
+            updateState("plug" + plugNumber + "#amp", new QuantityType<>(status.getAmp().get(i), MILLI(AMPERE)));
+        }
+    }
+
+    private void handleAllPlugsInformation(StatusDTO status) {
+        long onCount = status.getSwitchValue().stream().filter(statusValue -> statusValue == 1).count();
+        if (onCount == 6) {
+            updateState(RevogiSmartStripControlBindingConstants.ALL_PLUGS, OnOffType.ON);
+        } else {
+            updateState(RevogiSmartStripControlBindingConstants.ALL_PLUGS, OnOffType.OFF);
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/RevogiSmartStripControlHandlerFactory.java b/bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/RevogiSmartStripControlHandlerFactory.java
new file mode 100644 (file)
index 0000000..a962ea1
--- /dev/null
@@ -0,0 +1,55 @@
+/**
+ * Copyright (c) 2010-2020 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.revogi.internal;
+
+import java.util.Collections;
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+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 RevogiSmartStripControlHandlerFactory} is responsible for creating things and thing
+ * handlers.
+ *
+ * @author Andi Bräu - Initial contribution
+ */
+@NonNullByDefault
+@Component(configurationPid = "binding.revogi", service = ThingHandlerFactory.class)
+public class RevogiSmartStripControlHandlerFactory extends BaseThingHandlerFactory {
+
+    private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections
+            .singleton(RevogiSmartStripControlBindingConstants.SMART_STRIP_THING_TYPE);
+
+    @Override
+    public boolean supportsThingType(ThingTypeUID thingTypeUID) {
+        return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
+    }
+
+    @Override
+    protected @Nullable ThingHandler createHandler(Thing thing) {
+        ThingTypeUID thingTypeUID = thing.getThingTypeUID();
+
+        if (RevogiSmartStripControlBindingConstants.SMART_STRIP_THING_TYPE.equals(thingTypeUID)) {
+            return new RevogiSmartStripControlHandler(thing);
+        }
+
+        return null;
+    }
+}
diff --git a/bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/RevogiSmartStripDiscoveryService.java b/bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/RevogiSmartStripDiscoveryService.java
new file mode 100644 (file)
index 0000000..c000a8d
--- /dev/null
@@ -0,0 +1,97 @@
+/**
+ * Copyright (c) 2010-2020 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.revogi.internal;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.revogi.internal.api.DiscoveryRawResponseDTO;
+import org.openhab.binding.revogi.internal.api.DiscoveryResponseDTO;
+import org.openhab.binding.revogi.internal.api.RevogiDiscoveryService;
+import org.openhab.binding.revogi.internal.udp.DatagramSocketWrapper;
+import org.openhab.binding.revogi.internal.udp.UdpSenderService;
+import org.openhab.core.config.discovery.AbstractDiscoveryService;
+import org.openhab.core.config.discovery.DiscoveryResult;
+import org.openhab.core.config.discovery.DiscoveryResultBuilder;
+import org.openhab.core.config.discovery.DiscoveryService;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.ThingUID;
+import org.osgi.service.component.annotations.Component;
+
+/**
+ * The {@link RevogiSmartStripDiscoveryService} helps to discover new smart strips
+ *
+ * @author Andi Bräu - Initial contribution
+ */
+@Component(service = DiscoveryService.class, configurationPid = "discovery.revogi")
+@NonNullByDefault
+public class RevogiSmartStripDiscoveryService extends AbstractDiscoveryService {
+    public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections
+            .singleton(RevogiSmartStripControlBindingConstants.SMART_STRIP_THING_TYPE);
+
+    private final RevogiDiscoveryService revogiDiscoveryService;
+
+    private static final int SEARCH_TIMEOUT_SEC = 10;
+
+    public RevogiSmartStripDiscoveryService() {
+        super(SUPPORTED_THING_TYPES, SEARCH_TIMEOUT_SEC);
+        revogiDiscoveryService = new RevogiDiscoveryService(
+                new UdpSenderService(new DatagramSocketWrapper(), scheduler));
+    }
+
+    @Override
+    public Set<ThingTypeUID> getSupportedThingTypes() {
+        return SUPPORTED_THING_TYPES;
+    }
+
+    @Override
+    protected void startScan() {
+        CompletableFuture<List<DiscoveryRawResponseDTO>> discoveryResponses = revogiDiscoveryService
+                .discoverSmartStrips();
+        discoveryResponses.thenAccept(this::applyDiscoveryResults);
+    }
+
+    private void applyDiscoveryResults(final List<DiscoveryRawResponseDTO> discoveryRawResponses) {
+        discoveryRawResponses.forEach(response -> {
+            ThingUID thingUID = getThingUID(response.getData());
+            if (thingUID != null) {
+                Map<String, Object> properties = new HashMap<>();
+                properties.put(Thing.PROPERTY_MODEL_ID, response.getData().getRegId());
+                properties.put(Thing.PROPERTY_MAC_ADDRESS, response.getData().getMacAddress());
+                properties.put(Thing.PROPERTY_FIRMWARE_VERSION, response.getData().getVersion());
+                properties.put(Thing.PROPERTY_SERIAL_NUMBER, response.getData().getSerialNumber());
+                properties.put("ipAddress", response.getIpAddress());
+                DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID)
+                        .withThingType(RevogiSmartStripControlBindingConstants.SMART_STRIP_THING_TYPE)
+                        .withProperties(properties).withRepresentationProperty(Thing.PROPERTY_SERIAL_NUMBER).build();
+                thingDiscovered(discoveryResult);
+            }
+        });
+    }
+
+    private @Nullable ThingUID getThingUID(DiscoveryResponseDTO response) {
+        if (getSupportedThingTypes().contains(RevogiSmartStripControlBindingConstants.SMART_STRIP_THING_TYPE)) {
+            return new ThingUID(RevogiSmartStripControlBindingConstants.SMART_STRIP_THING_TYPE,
+                    response.getSerialNumber());
+        } else {
+            return null;
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/api/DiscoveryRawResponseDTO.java b/bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/api/DiscoveryRawResponseDTO.java
new file mode 100644 (file)
index 0000000..7b28785
--- /dev/null
@@ -0,0 +1,61 @@
+/**
+ * Copyright (c) 2010-2020 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.revogi.internal.api;
+
+import java.util.Objects;
+
+/**
+ * @author Andi Bräu - Initial contribution
+ */
+public class DiscoveryRawResponseDTO {
+
+    private final int response;
+    private final DiscoveryResponseDTO data;
+    private String ipAddress;
+
+    public DiscoveryRawResponseDTO(int response, DiscoveryResponseDTO data) {
+        this.response = response;
+        this.data = data;
+    }
+
+    public int getResponse() {
+        return response;
+    }
+
+    public DiscoveryResponseDTO getData() {
+        return data;
+    }
+
+    public String getIpAddress() {
+        return ipAddress;
+    }
+
+    public void setIpAddress(String ipAddress) {
+        this.ipAddress = ipAddress;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o)
+            return true;
+        if (o == null || getClass() != o.getClass())
+            return false;
+        DiscoveryRawResponseDTO that = (DiscoveryRawResponseDTO) o;
+        return response == that.response && data.equals(that.data) && Objects.equals(ipAddress, that.ipAddress);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(response, data, ipAddress);
+    }
+}
diff --git a/bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/api/DiscoveryResponseDTO.java b/bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/api/DiscoveryResponseDTO.java
new file mode 100644 (file)
index 0000000..9b80b1b
--- /dev/null
@@ -0,0 +1,92 @@
+/**
+ * Copyright (c) 2010-2020 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.revogi.internal.api;
+
+import java.util.Objects;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * @author Andi Bräu - Initial contribution
+ */
+public class DiscoveryResponseDTO {
+    @SerializedName("sn")
+    private final String serialNumber;
+    @SerializedName("regid")
+    private final String regId;
+    private final String sak;
+    private final String name;
+    @SerializedName("mac")
+    private final String macAddress;
+    @SerializedName("ver")
+    private final String version;
+
+    public DiscoveryResponseDTO(String serialNumber, String regId, String sak, String name, String macAddress,
+            String version) {
+        this.serialNumber = serialNumber;
+        this.regId = regId;
+        this.sak = sak;
+        this.name = name;
+        this.macAddress = macAddress;
+        this.version = version;
+    }
+
+    public DiscoveryResponseDTO() {
+        serialNumber = "";
+        regId = "";
+        sak = "";
+        name = "";
+        macAddress = "";
+        version = "";
+    }
+
+    public String getSerialNumber() {
+        return serialNumber;
+    }
+
+    public String getRegId() {
+        return regId;
+    }
+
+    public String getSak() {
+        return sak;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public String getMacAddress() {
+        return macAddress;
+    }
+
+    public String getVersion() {
+        return version;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o)
+            return true;
+        if (o == null || getClass() != o.getClass())
+            return false;
+        DiscoveryResponseDTO that = (DiscoveryResponseDTO) o;
+        return serialNumber.equals(that.serialNumber) && regId.equals(that.regId) && sak.equals(that.sak)
+                && name.equals(that.name) && macAddress.equals(that.macAddress) && version.equals(that.version);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(serialNumber, regId, sak, name, macAddress, version);
+    }
+}
diff --git a/bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/api/RevogiDiscoveryService.java b/bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/api/RevogiDiscoveryService.java
new file mode 100644 (file)
index 0000000..287de9d
--- /dev/null
@@ -0,0 +1,64 @@
+/**
+ * Copyright (c) 2010-2020 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.revogi.internal.api;
+
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.stream.Collectors;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.revogi.internal.udp.UdpResponseDTO;
+import org.openhab.binding.revogi.internal.udp.UdpSenderService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonSyntaxException;
+
+/**
+ * The {@link RevogiDiscoveryService} helps to discover smart strips within your network
+ *
+ * @author Andi Bräu - Initial contribution
+ */
+@NonNullByDefault
+public class RevogiDiscoveryService {
+    private static final String UDP_DISCOVERY_QUERY = "00sw=all,,,;";
+    private final Logger logger = LoggerFactory.getLogger(RevogiDiscoveryService.class);
+
+    private final Gson gson = new GsonBuilder().create();
+    private final UdpSenderService udpSenderService;
+
+    public RevogiDiscoveryService(UdpSenderService udpSenderService) {
+        this.udpSenderService = udpSenderService;
+    }
+
+    public CompletableFuture<List<DiscoveryRawResponseDTO>> discoverSmartStrips() {
+        CompletableFuture<List<UdpResponseDTO>> responses = udpSenderService.broadcastUdpDatagram(UDP_DISCOVERY_QUERY);
+        return responses.thenApply(futureList -> futureList.stream().filter(response -> !response.getAnswer().isEmpty())
+                .map(this::deserializeString).filter(discoveryRawResponse -> discoveryRawResponse.getResponse() == 0)
+                .collect(Collectors.toList()));
+    }
+
+    private DiscoveryRawResponseDTO deserializeString(UdpResponseDTO response) {
+        try {
+            DiscoveryRawResponseDTO discoveryRawResponse = gson.fromJson(response.getAnswer(),
+                    DiscoveryRawResponseDTO.class);
+            discoveryRawResponse.setIpAddress(response.getIpAddress());
+            return discoveryRawResponse;
+        } catch (JsonSyntaxException e) {
+            logger.warn("Could not parse string \"{}\" to DiscoveryRawResponse", response, e);
+            return new DiscoveryRawResponseDTO(503, new DiscoveryResponseDTO());
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/api/StatusDTO.java b/bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/api/StatusDTO.java
new file mode 100644 (file)
index 0000000..cc7f05c
--- /dev/null
@@ -0,0 +1,89 @@
+/**
+ * Copyright (c) 2010-2020 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.revogi.internal.api;
+
+import java.util.List;
+import java.util.Objects;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * {@link StatusDTO} is the internal data model used to control Revogi's SmartStrip
+ *
+ * @author Andi Bräu - Initial contribution
+ *
+ */
+public class StatusDTO {
+    private final boolean online;
+    private final int responseCode;
+    @SerializedName("switch")
+    private final List<Integer> switchValue;
+    private final List<Integer> watt;
+    private final List<Integer> amp;
+
+    public StatusDTO() {
+        online = false;
+        responseCode = 0;
+        switchValue = null;
+        watt = null;
+        amp = null;
+    }
+
+    public StatusDTO(boolean online, int responseCode, List<Integer> switchValue, List<Integer> watt,
+            List<Integer> amp) {
+        this.online = online;
+        this.responseCode = responseCode;
+        this.switchValue = switchValue;
+        this.watt = watt;
+        this.amp = amp;
+    }
+
+    public boolean isOnline() {
+        return online;
+    }
+
+    public int getResponseCode() {
+        return responseCode;
+    }
+
+    public List<Integer> getSwitchValue() {
+        return switchValue;
+    }
+
+    public List<Integer> getWatt() {
+        return watt;
+    }
+
+    public List<Integer> getAmp() {
+        return amp;
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        StatusDTO status = (StatusDTO) o;
+        return online == status.online && responseCode == status.responseCode
+                && Objects.equals(switchValue, status.switchValue) && Objects.equals(watt, status.watt)
+                && Objects.equals(amp, status.amp);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(online, responseCode, switchValue, watt, amp);
+    }
+}
diff --git a/bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/api/StatusRawDTO.java b/bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/api/StatusRawDTO.java
new file mode 100644 (file)
index 0000000..29e9f37
--- /dev/null
@@ -0,0 +1,42 @@
+/**
+ * Copyright (c) 2010-2020 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.revogi.internal.api;
+
+/**
+ * The class {@link StatusRawDTO} represents the raw data received from Revogi's SmartStrip
+ *
+ * @author Andi Bräu - Initial contribution
+ */
+public class StatusRawDTO {
+    private final int response;
+    private final int code;
+    private final StatusDTO data;
+
+    public StatusRawDTO(int response, int code, StatusDTO data) {
+        this.response = response;
+        this.code = code;
+        this.data = data;
+    }
+
+    public int getResponse() {
+        return response;
+    }
+
+    public int getCode() {
+        return code;
+    }
+
+    public StatusDTO getData() {
+        return data;
+    }
+}
diff --git a/bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/api/StatusService.java b/bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/api/StatusService.java
new file mode 100644 (file)
index 0000000..ee2c5c0
--- /dev/null
@@ -0,0 +1,78 @@
+/**
+ * Copyright (c) 2010-2020 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.revogi.internal.api;
+
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.jetbrains.annotations.NotNull;
+import org.openhab.binding.revogi.internal.udp.UdpResponseDTO;
+import org.openhab.binding.revogi.internal.udp.UdpSenderService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonSyntaxException;
+
+/**
+ * The {@link StatusService} contains methods to get a status of a Revogi SmartStrip
+ *
+ * @author Andi Bräu - Initial contribution
+ */
+@NonNullByDefault
+public class StatusService {
+
+    private static final String UDP_DISCOVERY_QUERY = "V3{\"sn\":\"%s\", \"cmd\": 90}";
+    public static final String VERSION_STRING = "V3";
+    private final Logger logger = LoggerFactory.getLogger(StatusService.class);
+
+    private final Gson gson = new GsonBuilder().create();
+    private final UdpSenderService udpSenderService;
+
+    public StatusService(UdpSenderService udpSenderService) {
+        this.udpSenderService = udpSenderService;
+    }
+
+    public CompletableFuture<StatusDTO> queryStatus(String serialNumber, String ipAddress) {
+        CompletableFuture<List<UdpResponseDTO>> responses;
+        if (ipAddress.trim().isEmpty()) {
+            responses = udpSenderService.broadcastUdpDatagram(String.format(UDP_DISCOVERY_QUERY, serialNumber));
+        } else {
+            responses = udpSenderService.sendMessage(String.format(UDP_DISCOVERY_QUERY, serialNumber), ipAddress);
+        }
+        return responses.thenApply(this::getStatus);
+    }
+
+    @NotNull
+    private StatusDTO getStatus(final List<UdpResponseDTO> singleResponse) {
+        return singleResponse.stream()
+                .filter(response -> !response.getAnswer().isEmpty() && response.getAnswer().contains(VERSION_STRING))
+                .map(response -> deserializeString(response.getAnswer()))
+                .filter(statusRaw -> statusRaw.getCode() == 200 && statusRaw.getResponse() == 90)
+                .map(statusRaw -> new StatusDTO(true, statusRaw.getCode(), statusRaw.getData().getSwitchValue(),
+                        statusRaw.getData().getWatt(), statusRaw.getData().getAmp()))
+                .findFirst().orElse(new StatusDTO(false, 503, null, null, null));
+    }
+
+    private StatusRawDTO deserializeString(String response) {
+        String extractedJsonResponse = response.substring(response.lastIndexOf(VERSION_STRING) + 2);
+        try {
+            return gson.fromJson(extractedJsonResponse, StatusRawDTO.class);
+        } catch (JsonSyntaxException e) {
+            logger.warn("Could not parse string \"{}\" to StatusRaw", response, e);
+            return new StatusRawDTO(503, 0, new StatusDTO(false, 503, null, null, null));
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/api/SwitchResponseDTO.java b/bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/api/SwitchResponseDTO.java
new file mode 100644 (file)
index 0000000..eb9ddfa
--- /dev/null
@@ -0,0 +1,53 @@
+/**
+ * Copyright (c) 2010-2020 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.revogi.internal.api;
+
+import java.util.Objects;
+
+/**
+ * The class {@link SwitchResponseDTO} describes the response when you switch a plug
+ *
+ * @author Andi Bräu - Initial contribution
+ */
+public class SwitchResponseDTO {
+    private final int response;
+    private final int code;
+
+    public SwitchResponseDTO(int response, int code) {
+        this.response = response;
+        this.code = code;
+    }
+
+    public int getResponse() {
+        return response;
+    }
+
+    public int getCode() {
+        return code;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o)
+            return true;
+        if (o == null || getClass() != o.getClass())
+            return false;
+        SwitchResponseDTO that = (SwitchResponseDTO) o;
+        return response == that.response && code == that.code;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(response, code);
+    }
+}
diff --git a/bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/api/SwitchService.java b/bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/api/SwitchService.java
new file mode 100644 (file)
index 0000000..4d79cc2
--- /dev/null
@@ -0,0 +1,85 @@
+/**
+ * Copyright (c) 2010-2020 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.revogi.internal.api;
+
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.jetbrains.annotations.NotNull;
+import org.openhab.binding.revogi.internal.udp.UdpResponseDTO;
+import org.openhab.binding.revogi.internal.udp.UdpSenderService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonSyntaxException;
+
+/**
+ * The {@link SwitchService} enables the binding to actually switch plugs on and of
+ *
+ * @author Andi Bräu - Initial contribution
+ */
+@NonNullByDefault
+public class SwitchService {
+
+    private static final String UDP_DISCOVERY_QUERY = "V3{\"sn\":\"%s\", \"cmd\": 20, \"port\": %d, \"state\": %d}";
+    private static final String VERSION_STRING = "V3";
+    private final Logger logger = LoggerFactory.getLogger(SwitchService.class);
+
+    private final Gson gson = new GsonBuilder().create();
+    private final UdpSenderService udpSenderService;
+
+    public SwitchService(UdpSenderService udpSenderService) {
+        this.udpSenderService = udpSenderService;
+    }
+
+    public CompletableFuture<SwitchResponseDTO> switchPort(String serialNumber, String ipAddress, int port, int state) {
+        if (state < 0 || state > 1) {
+            throw new IllegalArgumentException("state has to be 0 or 1");
+        }
+        if (port < 0) {
+            throw new IllegalArgumentException("Given port doesn't exist");
+        }
+
+        CompletableFuture<List<UdpResponseDTO>> responses;
+        if (ipAddress.trim().isEmpty()) {
+            responses = udpSenderService
+                    .broadcastUdpDatagram(String.format(UDP_DISCOVERY_QUERY, serialNumber, port, state));
+        } else {
+            responses = udpSenderService.sendMessage(String.format(UDP_DISCOVERY_QUERY, serialNumber, port, state),
+                    ipAddress);
+        }
+
+        return responses.thenApply(this::getSwitchResponse);
+    }
+
+    @NotNull
+    private SwitchResponseDTO getSwitchResponse(final List<UdpResponseDTO> singleResponse) {
+        return singleResponse.stream().filter(response -> !response.getAnswer().isEmpty())
+                .map(response -> deserializeString(response.getAnswer()))
+                .filter(switchResponse -> switchResponse.getCode() == 200 && switchResponse.getResponse() == 20)
+                .findFirst().orElse(new SwitchResponseDTO(0, 503));
+    }
+
+    private SwitchResponseDTO deserializeString(String response) {
+        String extractedJsonResponse = response.substring(response.lastIndexOf(VERSION_STRING) + 2);
+        try {
+            return gson.fromJson(extractedJsonResponse, SwitchResponseDTO.class);
+        } catch (JsonSyntaxException e) {
+            logger.warn("Could not parse string \"{}\" to SwitchResponse", response);
+            return new SwitchResponseDTO(0, 503);
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/udp/DatagramSocketWrapper.java b/bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/udp/DatagramSocketWrapper.java
new file mode 100644 (file)
index 0000000..c644ce4
--- /dev/null
@@ -0,0 +1,67 @@
+/**
+ * Copyright (c) 2010-2020 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.revogi.internal.udp;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.SocketException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * The {@link DatagramSocketWrapper} wraps Java's DatagramSocket for better testing
+ * UdpSenderService
+ *
+ * @author Andi Bräu - Initial contribution
+ */
+@NonNullByDefault
+public class DatagramSocketWrapper {
+
+    @Nullable
+    private DatagramSocket datagramSocket;
+
+    public void initSocket() throws SocketException {
+        closeSocket();
+        DatagramSocket localDatagramSocket = new DatagramSocket();
+        localDatagramSocket.setBroadcast(true);
+        localDatagramSocket.setSoTimeout(3);
+        datagramSocket = localDatagramSocket;
+    }
+
+    public void closeSocket() {
+        DatagramSocket localDatagramSocket = this.datagramSocket;
+        if (localDatagramSocket != null && !localDatagramSocket.isClosed()) {
+            localDatagramSocket.close();
+        }
+    }
+
+    public void sendPacket(DatagramPacket datagramPacket) throws IOException {
+        DatagramSocket localDatagramSocket = this.datagramSocket;
+        if (localDatagramSocket != null && !localDatagramSocket.isClosed()) {
+            localDatagramSocket.send(datagramPacket);
+        } else {
+            throw new SocketException("Datagram Socket closed or not initialized");
+        }
+    }
+
+    public void receiveAnswer(DatagramPacket datagramPacket) throws IOException {
+        DatagramSocket localDatagramSocket = this.datagramSocket;
+        if (localDatagramSocket != null && !localDatagramSocket.isClosed()) {
+            localDatagramSocket.receive(datagramPacket);
+        } else {
+            throw new SocketException("Datagram Socket closed or not initialized");
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/udp/UdpResponseDTO.java b/bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/udp/UdpResponseDTO.java
new file mode 100644 (file)
index 0000000..a5cf682
--- /dev/null
@@ -0,0 +1,53 @@
+/**
+ * Copyright (c) 2010-2020 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.revogi.internal.udp;
+
+import java.util.Objects;
+
+/**
+ * The class {@link UdpResponseDTO} represents udp reponse we expect
+ *
+ * @author Andi Bräu - Initial contribution
+ */
+public class UdpResponseDTO {
+    private final String answer;
+    private final String ipAddress;
+
+    public UdpResponseDTO(String answer, String ipAddress) {
+        this.answer = answer;
+        this.ipAddress = ipAddress;
+    }
+
+    public String getAnswer() {
+        return answer;
+    }
+
+    public String getIpAddress() {
+        return ipAddress;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o)
+            return true;
+        if (o == null || getClass() != o.getClass())
+            return false;
+        UdpResponseDTO that = (UdpResponseDTO) o;
+        return answer.equals(that.answer) && ipAddress.equals(that.ipAddress);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(answer, ipAddress);
+    }
+}
diff --git a/bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/udp/UdpSenderService.java b/bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/udp/UdpSenderService.java
new file mode 100644 (file)
index 0000000..deaafc8
--- /dev/null
@@ -0,0 +1,145 @@
+/**
+ * Copyright (c) 2010-2020 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.revogi.internal.udp;
+
+import static java.util.stream.Collectors.toList;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.InetAddress;
+import java.net.SocketException;
+import java.net.SocketTimeoutException;
+import java.net.UnknownHostException;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.common.ThreadPoolManager;
+import org.openhab.core.net.NetUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link UdpSenderService} is responsible for sending and receiving udp packets
+ *
+ * @author Andi Bräu - Initial contribution
+ */
+@NonNullByDefault
+public class UdpSenderService {
+
+    /**
+     * Limit timeout waiting time, as we have to deal with UDP
+     *
+     * How it works: for every loop, we'll wait a bit longer, so the timeout counter is multiplied with the timeout base
+     * value. Let max timeout count be 2 and timeout base value 800, then we'll have a maximum of loops of 3, waiting
+     * 800ms in the 1st loop, 1600ms in the 2nd loop and 2400ms in the third loop.
+     */
+    private static final int MAX_TIMEOUT_COUNT = 2;
+    private static final long TIMEOUT_BASE_VALUE_MS = 800L;
+    private static final int REVOGI_PORT = 8888;
+
+    private final Logger logger = LoggerFactory.getLogger(UdpSenderService.class);
+    private final DatagramSocketWrapper datagramSocketWrapper;
+    private final ScheduledExecutorService scheduler;
+    private final long timeoutBaseValue;
+
+    public UdpSenderService(DatagramSocketWrapper datagramSocketWrapper, ScheduledExecutorService scheduler) {
+        this.timeoutBaseValue = TIMEOUT_BASE_VALUE_MS;
+        this.datagramSocketWrapper = datagramSocketWrapper;
+        this.scheduler = scheduler;
+    }
+
+    public UdpSenderService(DatagramSocketWrapper datagramSocketWrapper, long timeout) {
+        this.timeoutBaseValue = timeout;
+        this.datagramSocketWrapper = datagramSocketWrapper;
+        this.scheduler = ThreadPoolManager.getScheduledPool("test pool");
+    }
+
+    public CompletableFuture<List<UdpResponseDTO>> broadcastUdpDatagram(String content) {
+        List<String> allBroadcastAddresses = NetUtil.getAllBroadcastAddresses();
+        CompletableFuture<List<UdpResponseDTO>> future = new CompletableFuture<>();
+        scheduler.submit(() -> future.complete(allBroadcastAddresses.stream().map(address -> {
+            try {
+                return sendMessage(content, InetAddress.getByName(address));
+            } catch (UnknownHostException e) {
+                logger.warn("Could not find host with IP {}", address);
+                return new ArrayList<UdpResponseDTO>();
+            }
+        }).flatMap(Collection::stream).distinct().collect(toList())));
+        return future;
+    }
+
+    public CompletableFuture<List<UdpResponseDTO>> sendMessage(String content, String ipAddress) {
+        try {
+            CompletableFuture<List<UdpResponseDTO>> future = new CompletableFuture<>();
+            InetAddress inetAddress = InetAddress.getByName(ipAddress);
+            scheduler.submit(() -> future.complete(sendMessage(content, inetAddress)));
+            return future;
+        } catch (UnknownHostException e) {
+            logger.warn("Could not find host with IP {}", ipAddress);
+            return CompletableFuture.completedFuture(Collections.emptyList());
+        }
+    }
+
+    private List<UdpResponseDTO> sendMessage(String content, InetAddress inetAddress) {
+        logger.debug("Using address {}", inetAddress);
+        byte[] buf = content.getBytes(Charset.defaultCharset());
+        DatagramPacket packet = new DatagramPacket(buf, buf.length, inetAddress, REVOGI_PORT);
+        List<UdpResponseDTO> responses = Collections.emptyList();
+        try {
+            datagramSocketWrapper.initSocket();
+            datagramSocketWrapper.sendPacket(packet);
+            responses = getUdpResponses();
+        } catch (IOException e) {
+            logger.warn("Error sending message or reading anwser {}", e.getMessage());
+        } finally {
+            datagramSocketWrapper.closeSocket();
+        }
+        return responses;
+    }
+
+    private List<UdpResponseDTO> getUdpResponses() {
+        int timeoutCounter = 0;
+        List<UdpResponseDTO> list = new ArrayList<>();
+        while (timeoutCounter < MAX_TIMEOUT_COUNT && !Thread.interrupted()) {
+            byte[] receivedBuf = new byte[512];
+            DatagramPacket answer = new DatagramPacket(receivedBuf, receivedBuf.length);
+            try {
+                datagramSocketWrapper.receiveAnswer(answer);
+            } catch (SocketTimeoutException | SocketException e) {
+                timeoutCounter++;
+                try {
+                    TimeUnit.MILLISECONDS.sleep(timeoutCounter * timeoutBaseValue);
+                } catch (InterruptedException ex) {
+                    logger.debug("Interrupted sleep");
+                    Thread.currentThread().interrupt();
+                }
+                continue;
+            } catch (IOException e) {
+                logger.warn("Error sending message or reading anwser {}", e.getMessage());
+            }
+
+            if (answer.getAddress() != null && answer.getLength() > 0) {
+                list.add(new UdpResponseDTO(new String(answer.getData(), 0, answer.getLength()),
+                        answer.getAddress().getHostAddress()));
+            }
+        }
+        return list;
+    }
+}
diff --git a/bundles/org.openhab.binding.revogi/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.revogi/src/main/resources/OH-INF/binding/binding.xml
new file mode 100644 (file)
index 0000000..619bb41
--- /dev/null
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<binding:binding id="revogi" 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>Revogi Binding</name>
+       <description>This is the binding for Revogi devices. Revogi is a vendor of several smart home devices like light bulbs,
+               power strips and sensors.</description>
+       <author>Andi Bräu</author>
+
+</binding:binding>
diff --git a/bundles/org.openhab.binding.revogi/src/main/resources/OH-INF/i18n/revogi_de.properties b/bundles/org.openhab.binding.revogi/src/main/resources/OH-INF/i18n/revogi_de.properties
new file mode 100644 (file)
index 0000000..7f929bb
--- /dev/null
@@ -0,0 +1,37 @@
+# binding
+binding.revogi.name = Revogi Smart Strip Binding
+binding.revogi.description = Mit diesem Binding kann man die Steckdosen im Smart Strip steuern und Verbrauchsinformationen erhalten
+
+# thing types
+thing-type.revogi.smartstrip.label = SmartStrip
+thing-type.revogi.smartstrip.description = Ein Binding, um Revogis Smart Strip zu steuern
+
+# thing type config description
+thing-type.config.revogi.smartstrip.serialNumber.label = Seriennummer
+thing-type.config.revogi.smartstrip.serialNumber.description = Die Seriennummer des Smart Strips
+thing-type.config.revogi.smartstrip.pollInterval.label = Aktualisierungsintervall
+thing-type.config.revogi.smartstrip.pollInterval.description = Intervall, in dem der Status aktualisiert wird
+thing-type.config.revogi.smartstrip.ipAddress.label = IP Adresse
+thing-type.config.revogi.smartstrip.ipAddress.description = IP Adresse des Smart Strips
+
+
+thing-type.revogi.smartstrip.group.plug1.label = Steckdose 1
+thing-type.revogi.smartstrip.group.plug2.label = Steckdose 2
+thing-type.revogi.smartstrip.group.plug3.label = Steckdose 3
+thing-type.revogi.smartstrip.group.plug4.label = Steckdose 4
+thing-type.revogi.smartstrip.group.plug5.label = Steckdose 5
+thing-type.revogi.smartstrip.group.plug6.label = Steckdose 6
+thing-type.revogi.smartstrip.group.overallPlug.label = Alle Steckdosen
+
+# channel types
+channel-type.revogi.single-plug.label = Schalter
+channel-type.revogi.single-plug.description = Eine einzelne Steckdose schalten
+channel-type.revogi.watts.label = Watt
+channel-type.revogi.watts.description = Enthält die aktuelle genutzte Leistung
+channel-type.revogi.amps.label = Ampere
+channel-type.revogi.amps.description = Enthält die aktuelle Stromstärke
+
+channel-group-type.revogi.plugActor.label = Einzelne Steckdose
+channel-group-type.revogi.plugActor.description = Schaltet eine einzelne Steckdose und empfängt statistische Daten
+channel-group-type.revogi.overallPlugActor.label = Alle Steckdosen
+channel-group-type.revogi.overallPlugActor.description = Schaltet alle Steckdosen
diff --git a/bundles/org.openhab.binding.revogi/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.revogi/src/main/resources/OH-INF/thing/thing-types.xml
new file mode 100644 (file)
index 0000000..b6c3ae1
--- /dev/null
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="revogi"
+       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">
+
+       <!-- SmartStrip Thing Type -->
+       <thing-type id="smartstrip">
+               <label>SmartStrip</label>
+               <description>A Thing to control Revogi SmartStrip</description>
+               <category>PowerOutlet</category>
+
+               <channel-groups>
+                       <channel-group id="overallPlug" typeId="overallPlugActuator"/>
+                       <channel-group id="plug1" typeId="plugActuator">
+                               <label>Plug 1</label>
+                       </channel-group>
+                       <channel-group id="plug2" typeId="plugActuator">
+                               <label>Plug 2</label>
+                       </channel-group>
+                       <channel-group id="plug3" typeId="plugActuator">
+                               <label>Plug 3</label>
+                       </channel-group>
+                       <channel-group id="plug4" typeId="plugActuator">
+                               <label>Plug 4</label>
+                       </channel-group>
+                       <channel-group id="plug5" typeId="plugActuator">
+                               <label>Plug 5</label>
+                       </channel-group>
+                       <channel-group id="plug6" typeId="plugActuator">
+                               <label>Plug 6</label>
+                       </channel-group>
+               </channel-groups>
+
+               <representation-property>serialNumber</representation-property>
+               <config-description>
+                       <parameter name="serialNumber" type="text" required="true">
+                               <label>Serial Number</label>
+                               <description>Serial number of your smart strip.</description>
+                       </parameter>
+                       <parameter name="pollInterval" type="integer" min="10" unit="s">
+                               <label>Poll Interval</label>
+                               <default>60</default>
+                               <description>How often (seconds) should the smart strip status be polled?</description>
+                       </parameter>
+                       <parameter name="ipAddress" type="text">
+                               <label>IP Address</label>
+                               <description>IP Address of your smart strip</description>
+                               <context>network-address</context>
+                       </parameter>
+               </config-description>
+
+       </thing-type>
+
+       <channel-group-type id="plugActuator">
+               <label>Single Plug Actuator</label>
+               <description>Switches a single plug and retrieve stats for it</description>
+               <channels>
+                       <channel id="switch" typeId="single-plug"/>
+                       <channel id="watt" typeId="watts"/>
+                       <channel id="amp" typeId="amps"/>
+               </channels>
+       </channel-group-type>
+
+       <channel-group-type id="overallPlugActuator">
+               <label>Overall Plug Actuator</label>
+               <description>Switches all plugs</description>
+               <channels>
+                       <channel id="switch" typeId="single-plug"/>
+               </channels>
+       </channel-group-type>
+
+       <channel-type id="single-plug">
+               <item-type>Switch</item-type>
+               <label>Switch</label>
+               <description>Switch a single plug</description>
+       </channel-type>
+       <channel-type id="watts" advanced="true">
+               <item-type>Number:Power</item-type>
+               <label>Power</label>
+               <description>Contains the current watt value for the given plug</description>
+               <state readOnly="true" pattern="%.1f W"/>
+       </channel-type>
+       <channel-type id="amps" advanced="true">
+               <item-type>Number:ElectricCurrent</item-type>
+               <label>Current</label>
+               <description>Contains the current Amp value for the given plug</description>
+               <state readOnly="true" pattern="%.1f A"/>
+       </channel-type>
+
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.revogi/src/test/java/org/openhab/binding/revogi/internal/api/RevogiDiscoveryServiceTest.java b/bundles/org.openhab.binding.revogi/src/test/java/org/openhab/binding/revogi/internal/api/RevogiDiscoveryServiceTest.java
new file mode 100644 (file)
index 0000000..59b1726
--- /dev/null
@@ -0,0 +1,76 @@
+/**
+ * Copyright (c) 2010-2020 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.revogi.internal.api;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.revogi.internal.udp.UdpResponseDTO;
+import org.openhab.binding.revogi.internal.udp.UdpSenderService;
+
+/**
+ * @author Andi Bräu - Initial contribution
+ */
+@NonNullByDefault
+public class RevogiDiscoveryServiceTest {
+
+    private final UdpSenderService udpSenderService = mock(UdpSenderService.class);
+    private final RevogiDiscoveryService revogiDiscoveryService = new RevogiDiscoveryService(udpSenderService);
+
+    @Test
+    public void discoverSmartStripSuccesfully() {
+        // given
+        DiscoveryResponseDTO discoveryResponse = new DiscoveryResponseDTO("1234", "reg", "sak", "Strip", "mac", "5.11");
+        List<UdpResponseDTO> discoveryString = Collections.singletonList(new UdpResponseDTO(
+                "{\"response\":0,\"data\":{\"sn\":\"1234\",\"regid\":\"reg\",\"sak\":\"sak\",\"name\":\"Strip\",\"mac\":\"mac\",\"ver\":\"5.11\"}}",
+                "127.0.0.1"));
+        when(udpSenderService.broadcastUdpDatagram("00sw=all,,,;"))
+                .thenReturn(CompletableFuture.completedFuture(discoveryString));
+
+        // when
+        CompletableFuture<List<DiscoveryRawResponseDTO>> discoverSmartStripsFutures = revogiDiscoveryService
+                .discoverSmartStrips();
+
+        // then
+        List<DiscoveryRawResponseDTO> discoverSmartStrips = discoverSmartStripsFutures.getNow(Collections.emptyList());
+        assertThat(discoverSmartStrips.size(), equalTo(1));
+        assertThat(discoverSmartStrips.get(0).getData(), equalTo(discoveryResponse));
+        assertThat(discoverSmartStrips.get(0).getIpAddress(), equalTo("127.0.0.1"));
+    }
+
+    @Test
+    public void invalidUdpResponse() throws ExecutionException, InterruptedException {
+        // given
+        List<UdpResponseDTO> discoveryString = Collections
+                .singletonList(new UdpResponseDTO("something invalid", "12345"));
+        when(udpSenderService.broadcastUdpDatagram("00sw=all,,,;"))
+                .thenReturn(CompletableFuture.completedFuture(discoveryString));
+
+        // when
+        CompletableFuture<List<DiscoveryRawResponseDTO>> futureList = revogiDiscoveryService.discoverSmartStrips();
+
+        // then
+        List<DiscoveryRawResponseDTO> discoverSmartStrips = futureList.get();
+        assertThat(discoverSmartStrips.isEmpty(), is(true));
+    }
+}
diff --git a/bundles/org.openhab.binding.revogi/src/test/java/org/openhab/binding/revogi/internal/api/StatusServiceTest.java b/bundles/org.openhab.binding.revogi/src/test/java/org/openhab/binding/revogi/internal/api/StatusServiceTest.java
new file mode 100644 (file)
index 0000000..ce520da
--- /dev/null
@@ -0,0 +1,70 @@
+/**
+ * Copyright (c) 2010-2020 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.revogi.internal.api;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.revogi.internal.udp.UdpResponseDTO;
+import org.openhab.binding.revogi.internal.udp.UdpSenderService;
+
+/**
+ * @author Andi Bräu - Initial contribution
+ */
+@NonNullByDefault
+public class StatusServiceTest {
+
+    private final UdpSenderService udpSenderService = mock(UdpSenderService.class);
+    private final StatusService statusService = new StatusService(udpSenderService);
+
+    @Test
+    public void getStatusSuccessfully() {
+        // given
+        StatusDTO status = new StatusDTO(true, 200, Arrays.asList(0, 0, 0, 0, 0, 0), Arrays.asList(0, 0, 0, 0, 0, 0),
+                Arrays.asList(0, 0, 0, 0, 0, 0));
+        List<UdpResponseDTO> statusString = Collections.singletonList(new UdpResponseDTO(
+                "V3{\"response\":90,\"code\":200,\"data\":{\"switch\":[0,0,0,0,0,0],\"watt\":[0,0,0,0,0,0],\"amp\":[0,0,0,0,0,0]}}",
+                "127.0.0.1"));
+        when(udpSenderService.sendMessage("V3{\"sn\":\"serial\", \"cmd\": 90}", "127.0.0.1"))
+                .thenReturn(CompletableFuture.completedFuture(statusString));
+
+        // when
+        CompletableFuture<StatusDTO> statusResponse = statusService.queryStatus("serial", "127.0.0.1");
+
+        // then
+        assertEquals(status, statusResponse.getNow(new StatusDTO()));
+    }
+
+    @Test
+    public void invalidUdpResponse() {
+        // given
+        List<UdpResponseDTO> statusString = Collections.singletonList(new UdpResponseDTO("something invalid", "12345"));
+        when(udpSenderService.sendMessage("V3{\"sn\":\"serial\", \"cmd\": 90}", "127.0.0.1"))
+                .thenReturn(CompletableFuture.completedFuture(statusString));
+
+        // when
+        CompletableFuture<StatusDTO> futureStatus = statusService.queryStatus("serial", "127.0.0.1");
+
+        // then
+        StatusDTO status = futureStatus.getNow(new StatusDTO());
+        assertEquals(503, status.getResponseCode());
+    }
+}
diff --git a/bundles/org.openhab.binding.revogi/src/test/java/org/openhab/binding/revogi/internal/api/SwitchServiceTest.java b/bundles/org.openhab.binding.revogi/src/test/java/org/openhab/binding/revogi/internal/api/SwitchServiceTest.java
new file mode 100644 (file)
index 0000000..b41f4b8
--- /dev/null
@@ -0,0 +1,94 @@
+/**
+ * Copyright (c) 2010-2020 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.revogi.internal.api;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.revogi.internal.udp.UdpResponseDTO;
+import org.openhab.binding.revogi.internal.udp.UdpSenderService;
+
+/**
+ * @author Andi Bräu - Initial contribution
+ */
+@NonNullByDefault
+public class SwitchServiceTest {
+
+    private UdpSenderService udpSenderService = mock(UdpSenderService.class);
+    private SwitchService switchService = new SwitchService(udpSenderService);
+
+    @Test
+    public void getStatusSuccesfully() {
+        // given
+        List<UdpResponseDTO> response = Collections
+                .singletonList(new UdpResponseDTO("V3{\"response\":20,\"code\":200}", "127.0.0.1"));
+        when(udpSenderService.sendMessage("V3{\"sn\":\"serial\", \"cmd\": 20, \"port\": 1, \"state\": 1}", "127.0.0.1"))
+                .thenReturn(CompletableFuture.completedFuture(response));
+
+        // when
+        CompletableFuture<SwitchResponseDTO> switchResponse = switchService.switchPort("serial", "127.0.0.1", 1, 1);
+
+        // then
+        assertThat(switchResponse.getNow(new SwitchResponseDTO(0, 0)), equalTo(new SwitchResponseDTO(20, 200)));
+    }
+
+    @Test
+    public void getStatusSuccesfullyWithBroadcast() {
+        // given
+        List<UdpResponseDTO> response = Collections
+                .singletonList(new UdpResponseDTO("V3{\"response\":20,\"code\":200}", "127.0.0.1"));
+        when(udpSenderService.broadcastUdpDatagram("V3{\"sn\":\"serial\", \"cmd\": 20, \"port\": 1, \"state\": 1}"))
+                .thenReturn(CompletableFuture.completedFuture(response));
+
+        // when
+        CompletableFuture<SwitchResponseDTO> switchResponse = switchService.switchPort("serial", "", 1, 1);
+
+        // then
+        assertThat(switchResponse.getNow(new SwitchResponseDTO(0, 0)), equalTo(new SwitchResponseDTO(20, 200)));
+    }
+
+    @Test
+    public void invalidUdpResponse() {
+        // given
+        List<UdpResponseDTO> response = Collections.singletonList(new UdpResponseDTO("something invalid", "12345"));
+        when(udpSenderService.sendMessage("V3{\"sn\":\"serial\", \"cmd\": 20, \"port\": 1, \"state\": 1}", "127.0.0.1"))
+                .thenReturn(CompletableFuture.completedFuture(response));
+
+        // when
+        CompletableFuture<SwitchResponseDTO> switchResponse = switchService.switchPort("serial", "127.0.0.1", 1, 1);
+
+        // then
+        assertThat(switchResponse.getNow(new SwitchResponseDTO(0, 0)), equalTo(new SwitchResponseDTO(0, 503)));
+    }
+
+    @Test
+    public void getExceptionOnWrongState() {
+        Assertions.assertThrows(IllegalArgumentException.class,
+                () -> switchService.switchPort("serial", "127.0.0.1", 1, 12));
+    }
+
+    @Test
+    public void getExceptionOnWrongPort() {
+        Assertions.assertThrows(IllegalArgumentException.class,
+                () -> switchService.switchPort("serial", "127.0.0.1", -1, 1));
+    }
+}
diff --git a/bundles/org.openhab.binding.revogi/src/test/java/org/openhab/binding/revogi/internal/udp/UdpSenderServiceTest.java b/bundles/org.openhab.binding.revogi/src/test/java/org/openhab/binding/revogi/internal/udp/UdpSenderServiceTest.java
new file mode 100644 (file)
index 0000000..0763560
--- /dev/null
@@ -0,0 +1,82 @@
+/**
+ * Copyright (c) 2010-2020 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.revogi.internal.udp;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.InetAddress;
+import java.net.SocketTimeoutException;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.core.net.NetUtil;
+
+/**
+ * @author Andi Bräu - Initial contribution
+ */
+@NonNullByDefault
+public class UdpSenderServiceTest {
+
+    private final DatagramSocketWrapper datagramSocketWrapper = mock(DatagramSocketWrapper.class);
+
+    private final UdpSenderService udpSenderService = new UdpSenderService(datagramSocketWrapper, 1L);
+
+    private final int numberOfInterfaces = NetUtil.getAllBroadcastAddresses().size();
+
+    @Test
+    public void testTimeout() throws IOException, ExecutionException, InterruptedException {
+        // given
+        doThrow(new SocketTimeoutException()).when(datagramSocketWrapper).receiveAnswer(any());
+
+        // when
+        CompletableFuture<List<UdpResponseDTO>> list = udpSenderService.broadcastUdpDatagram("send something");
+
+        // then
+        assertThat(list.get(), equalTo(Collections.emptyList()));
+        verify(datagramSocketWrapper, times(numberOfInterfaces * 2)).receiveAnswer(any());
+    }
+
+    @Test
+    public void testOneAnswer() throws IOException, ExecutionException, InterruptedException {
+        // given
+        byte[] receivedBuf = "valid answer".getBytes();
+        doAnswer(invocation -> {
+            DatagramPacket argument = invocation.getArgument(0);
+            argument.setData(receivedBuf);
+            argument.setAddress(InetAddress.getLocalHost());
+            return null;
+        }).doThrow(new SocketTimeoutException()).when(datagramSocketWrapper).receiveAnswer(any());
+
+        // when
+        CompletableFuture<List<UdpResponseDTO>> future = udpSenderService.broadcastUdpDatagram("send something");
+
+        // then
+        List<UdpResponseDTO> udpResponses = future.get();
+        assertThat(udpResponses.get(0).getAnswer(), is("valid answer"));
+        verify(datagramSocketWrapper, times(1 + 2 * numberOfInterfaces)).receiveAnswer(any());
+    }
+}
index ce230537d8ae7c8b99a7e7a4fd2422d188e8abf7..8c6c0436d5cab20dba518253ffa4a03237ba6b03 100644 (file)
     <module>org.openhab.binding.pushbullet</module>
     <module>org.openhab.binding.radiothermostat</module>
     <module>org.openhab.binding.regoheatpump</module>
+    <module>org.openhab.binding.revogi</module>
     <module>org.openhab.binding.remoteopenhab</module>
     <module>org.openhab.binding.rfxcom</module>
     <module>org.openhab.binding.rme</module>