From 1c93eb97372751c053a1e97d5306b5b3a24ef077 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andi=20Br=C3=A4u?= Date: Tue, 3 Nov 2020 18:13:08 +0100 Subject: [PATCH] [revogi] Initial contribution - Resubmitted for OH3 (#8534) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Rename binding and resubmit to OH3 Signed-off-by: Andreas Bräu --- CODEOWNERS | 1 + bom/openhab-addons/pom.xml | 5 + bundles/org.openhab.binding.revogi/NOTICE | 13 ++ bundles/org.openhab.binding.revogi/README.md | 78 ++++++++ bundles/org.openhab.binding.revogi/pom.xml | 18 ++ .../src/main/feature/feature.xml | 9 + ...vogiSmartStripControlBindingConstants.java | 42 +++++ .../RevogiSmartStripControlConfiguration.java | 31 ++++ .../RevogiSmartStripControlHandler.java | 166 ++++++++++++++++++ ...RevogiSmartStripControlHandlerFactory.java | 55 ++++++ .../RevogiSmartStripDiscoveryService.java | 97 ++++++++++ .../internal/api/DiscoveryRawResponseDTO.java | 61 +++++++ .../internal/api/DiscoveryResponseDTO.java | 92 ++++++++++ .../internal/api/RevogiDiscoveryService.java | 64 +++++++ .../revogi/internal/api/StatusDTO.java | 89 ++++++++++ .../revogi/internal/api/StatusRawDTO.java | 42 +++++ .../revogi/internal/api/StatusService.java | 78 ++++++++ .../internal/api/SwitchResponseDTO.java | 53 ++++++ .../revogi/internal/api/SwitchService.java | 85 +++++++++ .../internal/udp/DatagramSocketWrapper.java | 67 +++++++ .../revogi/internal/udp/UdpResponseDTO.java | 53 ++++++ .../revogi/internal/udp/UdpSenderService.java | 145 +++++++++++++++ .../main/resources/OH-INF/binding/binding.xml | 11 ++ .../OH-INF/i18n/revogi_de.properties | 37 ++++ .../resources/OH-INF/thing/thing-types.xml | 91 ++++++++++ .../api/RevogiDiscoveryServiceTest.java | 76 ++++++++ .../internal/api/StatusServiceTest.java | 70 ++++++++ .../internal/api/SwitchServiceTest.java | 94 ++++++++++ .../internal/udp/UdpSenderServiceTest.java | 82 +++++++++ bundles/pom.xml | 1 + 30 files changed, 1806 insertions(+) create mode 100644 bundles/org.openhab.binding.revogi/NOTICE create mode 100644 bundles/org.openhab.binding.revogi/README.md create mode 100644 bundles/org.openhab.binding.revogi/pom.xml create mode 100644 bundles/org.openhab.binding.revogi/src/main/feature/feature.xml create mode 100644 bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/RevogiSmartStripControlBindingConstants.java create mode 100644 bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/RevogiSmartStripControlConfiguration.java create mode 100644 bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/RevogiSmartStripControlHandler.java create mode 100644 bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/RevogiSmartStripControlHandlerFactory.java create mode 100644 bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/RevogiSmartStripDiscoveryService.java create mode 100644 bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/api/DiscoveryRawResponseDTO.java create mode 100644 bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/api/DiscoveryResponseDTO.java create mode 100644 bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/api/RevogiDiscoveryService.java create mode 100644 bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/api/StatusDTO.java create mode 100644 bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/api/StatusRawDTO.java create mode 100644 bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/api/StatusService.java create mode 100644 bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/api/SwitchResponseDTO.java create mode 100644 bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/api/SwitchService.java create mode 100644 bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/udp/DatagramSocketWrapper.java create mode 100644 bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/udp/UdpResponseDTO.java create mode 100644 bundles/org.openhab.binding.revogi/src/main/java/org/openhab/binding/revogi/internal/udp/UdpSenderService.java create mode 100644 bundles/org.openhab.binding.revogi/src/main/resources/OH-INF/binding/binding.xml create mode 100644 bundles/org.openhab.binding.revogi/src/main/resources/OH-INF/i18n/revogi_de.properties create mode 100644 bundles/org.openhab.binding.revogi/src/main/resources/OH-INF/thing/thing-types.xml create mode 100644 bundles/org.openhab.binding.revogi/src/test/java/org/openhab/binding/revogi/internal/api/RevogiDiscoveryServiceTest.java create mode 100644 bundles/org.openhab.binding.revogi/src/test/java/org/openhab/binding/revogi/internal/api/StatusServiceTest.java create mode 100644 bundles/org.openhab.binding.revogi/src/test/java/org/openhab/binding/revogi/internal/api/SwitchServiceTest.java create mode 100644 bundles/org.openhab.binding.revogi/src/test/java/org/openhab/binding/revogi/internal/udp/UdpSenderServiceTest.java diff --git a/CODEOWNERS b/CODEOWNERS index d056887e994..e249124e5b7 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 fc0b0d2c432..73c4fd1e405 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 00000000000..38d625e3492 --- /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 00000000000..4c1a9d622af --- /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 00000000000..d0c65666033 --- /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 00000000000..c9cd82705e5 --- /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 00000000000..8bc3c995381 --- /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 00000000000..037ed581da4 --- /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 00000000000..dacadfba4a5 --- /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 00000000000..a962ea1b666 --- /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 00000000000..c000a8d83f0 --- /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 00000000000..7b287850cd2 --- /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 00000000000..9b80b1bac2c --- /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 00000000000..287de9dc330 --- /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 00000000000..cc7f05ce8d5 --- /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 00000000000..29e9f37b862 --- /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 00000000000..ee2c5c0ad70 --- /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 00000000000..eb9ddfae9a4 --- /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 00000000000..4d79cc2078a --- /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 00000000000..c644ce4f8c4 --- /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 00000000000..a5cf682cf9c --- /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 00000000000..deaafc8e115 --- /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 00000000000..619bb415919 --- /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 00000000000..7f929bb9ed2 --- /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 00000000000..b6c3ae1e8db --- /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 00000000000..59b17266e5a --- /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 00000000000..ce520da0d5e --- /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 00000000000..b41f4b8941e --- /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 00000000000..076356065d9 --- /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 ce230537d8a..8c6c0436d5c 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