diff --git a/CODEOWNERS b/CODEOWNERS
index d056887e9..e249124e5 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -200,6 +200,7 @@
/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
diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml
index fc0b0d2c4..73c4fd1e4 100644
--- a/bom/openhab-addons/pom.xml
+++ b/bom/openhab-addons/pom.xml
@@ -991,6 +991,11 @@
org.openhab.binding.regoheatpump
${project.version}
+
+ org.openhab.addons.bundles
+ org.openhab.binding.revogi
+ ${project.version}
+
org.openhab.addons.bundles
org.openhab.binding.remoteopenhab
diff --git a/bundles/org.openhab.binding.revogi/NOTICE b/bundles/org.openhab.binding.revogi/NOTICE
new file mode 100644
index 000000000..38d625e34
--- /dev/null
+++ b/bundles/org.openhab.binding.revogi/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.revogi/README.md b/bundles/org.openhab.binding.revogi/README.md
new file mode 100644
index 000000000..4c1a9d622
--- /dev/null
+++ b/bundles/org.openhab.binding.revogi/README.md
@@ -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="", ipAddress=, pollIntervall=45]
+```
+
+Example Items configuration:
+
+```
+Group revogi (LivingRoom)
+
+Group plug1 (revogi)
+Group plug2 (revogi)
+
+Switch All_Plugs "Steckdosen komplett" (revogi) {channel="revogi:smartstrip::overallPlug#switch"}
+
+Switch Plug_1 "Steckdose 1" (plug1) {channel="revogi:smartstrip::plug1#switch"}
+Number Plug_1_Watt "Steckdose 1 Leistung" (plug1) {channel="revogi:smartstrip::plug1#watt"}
+Number Plug_1_Amp "Steckdose 1 Strom" (plug1) {channel="revogi:smartstrip::plug1#amp"}
+
+...
+```
diff --git a/bundles/org.openhab.binding.revogi/pom.xml b/bundles/org.openhab.binding.revogi/pom.xml
new file mode 100644
index 000000000..d0c656660
--- /dev/null
+++ b/bundles/org.openhab.binding.revogi/pom.xml
@@ -0,0 +1,18 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.addons.bundles
+ org.openhab.addons.reactor.bundles
+ 3.0.0-SNAPSHOT
+
+
+ org.openhab.binding.revogi
+
+ openHAB Add-ons :: Bundles :: Revogi Binding
+
+
+
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
index 000000000..c9cd82705
--- /dev/null
+++ b/bundles/org.openhab.binding.revogi/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.revogi/${project.version}
+
+
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
index 000000000..8bc3c9953
--- /dev/null
+++ b/bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/RevogiSmartStripControlBindingConstants.java
@@ -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
index 000000000..037ed581d
--- /dev/null
+++ b/bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/RevogiSmartStripControlConfiguration.java
@@ -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
index 000000000..dacadfba4
--- /dev/null
+++ b/bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/RevogiSmartStripControlHandler.java
@@ -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 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
index 000000000..a962ea1b6
--- /dev/null
+++ b/bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/RevogiSmartStripControlHandlerFactory.java
@@ -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 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
index 000000000..c000a8d83
--- /dev/null
+++ b/bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/RevogiSmartStripDiscoveryService.java
@@ -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 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 getSupportedThingTypes() {
+ return SUPPORTED_THING_TYPES;
+ }
+
+ @Override
+ protected void startScan() {
+ CompletableFuture> discoveryResponses = revogiDiscoveryService
+ .discoverSmartStrips();
+ discoveryResponses.thenAccept(this::applyDiscoveryResults);
+ }
+
+ private void applyDiscoveryResults(final List discoveryRawResponses) {
+ discoveryRawResponses.forEach(response -> {
+ ThingUID thingUID = getThingUID(response.getData());
+ if (thingUID != null) {
+ Map 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
index 000000000..7b287850c
--- /dev/null
+++ b/bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/api/DiscoveryRawResponseDTO.java
@@ -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
index 000000000..9b80b1bac
--- /dev/null
+++ b/bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/api/DiscoveryResponseDTO.java
@@ -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
index 000000000..287de9dc3
--- /dev/null
+++ b/bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/api/RevogiDiscoveryService.java
@@ -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> discoverSmartStrips() {
+ CompletableFuture> 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
index 000000000..cc7f05ce8
--- /dev/null
+++ b/bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/api/StatusDTO.java
@@ -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 switchValue;
+ private final List watt;
+ private final List amp;
+
+ public StatusDTO() {
+ online = false;
+ responseCode = 0;
+ switchValue = null;
+ watt = null;
+ amp = null;
+ }
+
+ public StatusDTO(boolean online, int responseCode, List switchValue, List watt,
+ List 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 getSwitchValue() {
+ return switchValue;
+ }
+
+ public List getWatt() {
+ return watt;
+ }
+
+ public List 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
index 000000000..29e9f37b8
--- /dev/null
+++ b/bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/api/StatusRawDTO.java
@@ -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
index 000000000..ee2c5c0ad
--- /dev/null
+++ b/bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/api/StatusService.java
@@ -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 queryStatus(String serialNumber, String ipAddress) {
+ CompletableFuture> 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 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
index 000000000..eb9ddfae9
--- /dev/null
+++ b/bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/api/SwitchResponseDTO.java
@@ -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
index 000000000..4d79cc207
--- /dev/null
+++ b/bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/api/SwitchService.java
@@ -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 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> 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 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
index 000000000..c644ce4f8
--- /dev/null
+++ b/bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/udp/DatagramSocketWrapper.java
@@ -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
index 000000000..a5cf682cf
--- /dev/null
+++ b/bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/udp/UdpResponseDTO.java
@@ -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
index 000000000..deaafc8e1
--- /dev/null
+++ b/bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/udp/UdpSenderService.java
@@ -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> broadcastUdpDatagram(String content) {
+ List allBroadcastAddresses = NetUtil.getAllBroadcastAddresses();
+ CompletableFuture> 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();
+ }
+ }).flatMap(Collection::stream).distinct().collect(toList())));
+ return future;
+ }
+
+ public CompletableFuture> sendMessage(String content, String ipAddress) {
+ try {
+ CompletableFuture> 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 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 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 getUdpResponses() {
+ int timeoutCounter = 0;
+ List 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
index 000000000..619bb4159
--- /dev/null
+++ b/bundles/org.openhab.binding.revogi/src/main/resources/OH-INF/binding/binding.xml
@@ -0,0 +1,11 @@
+
+
+
+ Revogi Binding
+ This is the binding for Revogi devices. Revogi is a vendor of several smart home devices like light bulbs,
+ power strips and sensors.
+ Andi Bräu
+
+
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
index 000000000..7f929bb9e
--- /dev/null
+++ b/bundles/org.openhab.binding.revogi/src/main/resources/OH-INF/i18n/revogi_de.properties
@@ -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
index 000000000..b6c3ae1e8
--- /dev/null
+++ b/bundles/org.openhab.binding.revogi/src/main/resources/OH-INF/thing/thing-types.xml
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+ A Thing to control Revogi SmartStrip
+ PowerOutlet
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ serialNumber
+
+
+
+ Serial number of your smart strip.
+
+
+
+ 60
+ How often (seconds) should the smart strip status be polled?
+
+
+
+ IP Address of your smart strip
+ network-address
+
+
+
+
+
+
+
+ Switches a single plug and retrieve stats for it
+
+
+
+
+
+
+
+
+
+ Switches all plugs
+
+
+
+
+
+
+ Switch
+
+ Switch a single plug
+
+
+ Number:Power
+
+ Contains the current watt value for the given plug
+
+
+
+ Number:ElectricCurrent
+
+ Contains the current Amp value for the given plug
+
+
+
+
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
index 000000000..59b17266e
--- /dev/null
+++ b/bundles/org.openhab.binding.revogi/src/test/java/org/openhab/binding/revogi/internal/api/RevogiDiscoveryServiceTest.java
@@ -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 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> discoverSmartStripsFutures = revogiDiscoveryService
+ .discoverSmartStrips();
+
+ // then
+ List 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 discoveryString = Collections
+ .singletonList(new UdpResponseDTO("something invalid", "12345"));
+ when(udpSenderService.broadcastUdpDatagram("00sw=all,,,;"))
+ .thenReturn(CompletableFuture.completedFuture(discoveryString));
+
+ // when
+ CompletableFuture> futureList = revogiDiscoveryService.discoverSmartStrips();
+
+ // then
+ List 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
index 000000000..ce520da0d
--- /dev/null
+++ b/bundles/org.openhab.binding.revogi/src/test/java/org/openhab/binding/revogi/internal/api/StatusServiceTest.java
@@ -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 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 statusResponse = statusService.queryStatus("serial", "127.0.0.1");
+
+ // then
+ assertEquals(status, statusResponse.getNow(new StatusDTO()));
+ }
+
+ @Test
+ public void invalidUdpResponse() {
+ // given
+ List 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 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
index 000000000..b41f4b894
--- /dev/null
+++ b/bundles/org.openhab.binding.revogi/src/test/java/org/openhab/binding/revogi/internal/api/SwitchServiceTest.java
@@ -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 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 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 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 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 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 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
index 000000000..076356065
--- /dev/null
+++ b/bundles/org.openhab.binding.revogi/src/test/java/org/openhab/binding/revogi/internal/udp/UdpSenderServiceTest.java
@@ -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 = 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> future = udpSenderService.broadcastUdpDatagram("send something");
+
+ // then
+ List udpResponses = future.get();
+ assertThat(udpResponses.get(0).getAnswer(), is("valid answer"));
+ verify(datagramSocketWrapper, times(1 + 2 * numberOfInterfaces)).receiveAnswer(any());
+ }
+}
diff --git a/bundles/pom.xml b/bundles/pom.xml
index ce230537d..8c6c0436d 100644
--- a/bundles/pom.xml
+++ b/bundles/pom.xml
@@ -232,6 +232,7 @@
org.openhab.binding.pushbullet
org.openhab.binding.radiothermostat
org.openhab.binding.regoheatpump
+ org.openhab.binding.revogi
org.openhab.binding.remoteopenhab
org.openhab.binding.rfxcom
org.openhab.binding.rme