[revogi] Initial contribution - Resubmitted for OH3 (#8534)

* Rename binding and resubmit to OH3

Signed-off-by: Andreas Bräu <ab@andi95.de>
This commit is contained in:
Andi Bräu
2020-11-03 18:13:08 +01:00
committed by GitHub
parent c49eeb2528
commit 1c93eb9737
30 changed files with 1806 additions and 0 deletions

View File

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

View File

@@ -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";
}

View File

@@ -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";
}

View File

@@ -0,0 +1,166 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.revogi.internal;
import static org.openhab.core.library.unit.MetricPrefix.MILLI;
import static org.openhab.core.library.unit.SmartHomeUnits.AMPERE;
import static org.openhab.core.library.unit.SmartHomeUnits.WATT;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.revogi.internal.api.StatusDTO;
import org.openhab.binding.revogi.internal.api.StatusService;
import org.openhab.binding.revogi.internal.api.SwitchService;
import org.openhab.binding.revogi.internal.udp.DatagramSocketWrapper;
import org.openhab.binding.revogi.internal.udp.UdpSenderService;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link RevogiSmartStripControlHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Andi Bräu - Initial contribution
*/
@NonNullByDefault
public class RevogiSmartStripControlHandler extends BaseThingHandler {
private final Logger logger = LoggerFactory.getLogger(RevogiSmartStripControlHandler.class);
private final StatusService statusService;
private final SwitchService switchService;
private @Nullable ScheduledFuture<?> pollingJob;
private RevogiSmartStripControlConfiguration config;
public RevogiSmartStripControlHandler(Thing thing) {
super(thing);
config = getConfigAs(RevogiSmartStripControlConfiguration.class);
UdpSenderService udpSenderService = new UdpSenderService(new DatagramSocketWrapper(), scheduler);
this.statusService = new StatusService(udpSenderService);
this.switchService = new SwitchService(udpSenderService);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
switch (channelUID.getId()) {
case RevogiSmartStripControlBindingConstants.PLUG_1_SWITCH:
switchPlug(command, 1);
break;
case RevogiSmartStripControlBindingConstants.PLUG_2_SWITCH:
switchPlug(command, 2);
break;
case RevogiSmartStripControlBindingConstants.PLUG_3_SWITCH:
switchPlug(command, 3);
break;
case RevogiSmartStripControlBindingConstants.PLUG_4_SWITCH:
switchPlug(command, 4);
break;
case RevogiSmartStripControlBindingConstants.PLUG_5_SWITCH:
switchPlug(command, 5);
break;
case RevogiSmartStripControlBindingConstants.PLUG_6_SWITCH:
switchPlug(command, 6);
break;
case RevogiSmartStripControlBindingConstants.ALL_PLUGS:
switchPlug(command, 0);
break;
default:
logger.debug("Something went wrong, we've got a message for {}", channelUID.getId());
}
}
private void switchPlug(Command command, int port) {
RevogiSmartStripControlConfiguration localConfig = this.config;
if (command instanceof OnOffType) {
int state = convertOnOffTypeToState(command);
switchService.switchPort(localConfig.serialNumber, localConfig.ipAddress, port, state);
}
if (command instanceof RefreshType) {
updateStripInformation();
}
}
private int convertOnOffTypeToState(Command command) {
if (command == OnOffType.ON) {
return 1;
} else {
return 0;
}
}
@Override
public void initialize() {
config = getConfigAs(RevogiSmartStripControlConfiguration.class);
updateStatus(ThingStatus.UNKNOWN);
pollingJob = scheduler.scheduleWithFixedDelay(this::updateStripInformation, 0, config.pollInterval,
TimeUnit.SECONDS);
}
@Override
public void dispose() {
super.dispose();
ScheduledFuture<?> localPollingJob = this.pollingJob;
if (localPollingJob != null) {
localPollingJob.cancel(true);
this.pollingJob = null;
}
}
private void updateStripInformation() {
CompletableFuture<StatusDTO> futureStatus = statusService.queryStatus(config.serialNumber, config.ipAddress);
futureStatus.thenAccept(this::updatePlugStatus);
}
private void updatePlugStatus(StatusDTO status) {
if (status.isOnline()) {
updateStatus(ThingStatus.ONLINE);
handleAllPlugsInformation(status);
handleSinglePlugInformation(status);
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE,
"Retrieved status code: " + status.getResponseCode());
}
}
private void handleSinglePlugInformation(StatusDTO status) {
for (int i = 0; i < status.getSwitchValue().size(); i++) {
int plugNumber = i + 1;
updateState("plug" + plugNumber + "#switch", OnOffType.from(status.getSwitchValue().get(i).toString()));
updateState("plug" + plugNumber + "#watt", new QuantityType<>(status.getWatt().get(i), MILLI(WATT)));
updateState("plug" + plugNumber + "#amp", new QuantityType<>(status.getAmp().get(i), MILLI(AMPERE)));
}
}
private void handleAllPlugsInformation(StatusDTO status) {
long onCount = status.getSwitchValue().stream().filter(statusValue -> statusValue == 1).count();
if (onCount == 6) {
updateState(RevogiSmartStripControlBindingConstants.ALL_PLUGS, OnOffType.ON);
} else {
updateState(RevogiSmartStripControlBindingConstants.ALL_PLUGS, OnOffType.OFF);
}
}
}

View File

@@ -0,0 +1,55 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.revogi.internal;
import java.util.Collections;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Component;
/**
* The {@link RevogiSmartStripControlHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Andi Bräu - Initial contribution
*/
@NonNullByDefault
@Component(configurationPid = "binding.revogi", service = ThingHandlerFactory.class)
public class RevogiSmartStripControlHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections
.singleton(RevogiSmartStripControlBindingConstants.SMART_STRIP_THING_TYPE);
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (RevogiSmartStripControlBindingConstants.SMART_STRIP_THING_TYPE.equals(thingTypeUID)) {
return new RevogiSmartStripControlHandler(thing);
}
return null;
}
}

View File

@@ -0,0 +1,97 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.revogi.internal;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.revogi.internal.api.DiscoveryRawResponseDTO;
import org.openhab.binding.revogi.internal.api.DiscoveryResponseDTO;
import org.openhab.binding.revogi.internal.api.RevogiDiscoveryService;
import org.openhab.binding.revogi.internal.udp.DatagramSocketWrapper;
import org.openhab.binding.revogi.internal.udp.UdpSenderService;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.osgi.service.component.annotations.Component;
/**
* The {@link RevogiSmartStripDiscoveryService} helps to discover new smart strips
*
* @author Andi Bräu - Initial contribution
*/
@Component(service = DiscoveryService.class, configurationPid = "discovery.revogi")
@NonNullByDefault
public class RevogiSmartStripDiscoveryService extends AbstractDiscoveryService {
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections
.singleton(RevogiSmartStripControlBindingConstants.SMART_STRIP_THING_TYPE);
private final RevogiDiscoveryService revogiDiscoveryService;
private static final int SEARCH_TIMEOUT_SEC = 10;
public RevogiSmartStripDiscoveryService() {
super(SUPPORTED_THING_TYPES, SEARCH_TIMEOUT_SEC);
revogiDiscoveryService = new RevogiDiscoveryService(
new UdpSenderService(new DatagramSocketWrapper(), scheduler));
}
@Override
public Set<ThingTypeUID> getSupportedThingTypes() {
return SUPPORTED_THING_TYPES;
}
@Override
protected void startScan() {
CompletableFuture<List<DiscoveryRawResponseDTO>> discoveryResponses = revogiDiscoveryService
.discoverSmartStrips();
discoveryResponses.thenAccept(this::applyDiscoveryResults);
}
private void applyDiscoveryResults(final List<DiscoveryRawResponseDTO> discoveryRawResponses) {
discoveryRawResponses.forEach(response -> {
ThingUID thingUID = getThingUID(response.getData());
if (thingUID != null) {
Map<String, Object> properties = new HashMap<>();
properties.put(Thing.PROPERTY_MODEL_ID, response.getData().getRegId());
properties.put(Thing.PROPERTY_MAC_ADDRESS, response.getData().getMacAddress());
properties.put(Thing.PROPERTY_FIRMWARE_VERSION, response.getData().getVersion());
properties.put(Thing.PROPERTY_SERIAL_NUMBER, response.getData().getSerialNumber());
properties.put("ipAddress", response.getIpAddress());
DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID)
.withThingType(RevogiSmartStripControlBindingConstants.SMART_STRIP_THING_TYPE)
.withProperties(properties).withRepresentationProperty(Thing.PROPERTY_SERIAL_NUMBER).build();
thingDiscovered(discoveryResult);
}
});
}
private @Nullable ThingUID getThingUID(DiscoveryResponseDTO response) {
if (getSupportedThingTypes().contains(RevogiSmartStripControlBindingConstants.SMART_STRIP_THING_TYPE)) {
return new ThingUID(RevogiSmartStripControlBindingConstants.SMART_STRIP_THING_TYPE,
response.getSerialNumber());
} else {
return null;
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,64 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.revogi.internal.api;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.revogi.internal.udp.UdpResponseDTO;
import org.openhab.binding.revogi.internal.udp.UdpSenderService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonSyntaxException;
/**
* The {@link RevogiDiscoveryService} helps to discover smart strips within your network
*
* @author Andi Bräu - Initial contribution
*/
@NonNullByDefault
public class RevogiDiscoveryService {
private static final String UDP_DISCOVERY_QUERY = "00sw=all,,,;";
private final Logger logger = LoggerFactory.getLogger(RevogiDiscoveryService.class);
private final Gson gson = new GsonBuilder().create();
private final UdpSenderService udpSenderService;
public RevogiDiscoveryService(UdpSenderService udpSenderService) {
this.udpSenderService = udpSenderService;
}
public CompletableFuture<List<DiscoveryRawResponseDTO>> discoverSmartStrips() {
CompletableFuture<List<UdpResponseDTO>> responses = udpSenderService.broadcastUdpDatagram(UDP_DISCOVERY_QUERY);
return responses.thenApply(futureList -> futureList.stream().filter(response -> !response.getAnswer().isEmpty())
.map(this::deserializeString).filter(discoveryRawResponse -> discoveryRawResponse.getResponse() == 0)
.collect(Collectors.toList()));
}
private DiscoveryRawResponseDTO deserializeString(UdpResponseDTO response) {
try {
DiscoveryRawResponseDTO discoveryRawResponse = gson.fromJson(response.getAnswer(),
DiscoveryRawResponseDTO.class);
discoveryRawResponse.setIpAddress(response.getIpAddress());
return discoveryRawResponse;
} catch (JsonSyntaxException e) {
logger.warn("Could not parse string \"{}\" to DiscoveryRawResponse", response, e);
return new DiscoveryRawResponseDTO(503, new DiscoveryResponseDTO());
}
}
}

View File

@@ -0,0 +1,89 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.revogi.internal.api;
import java.util.List;
import java.util.Objects;
import com.google.gson.annotations.SerializedName;
/**
* {@link StatusDTO} is the internal data model used to control Revogi's SmartStrip
*
* @author Andi Bräu - Initial contribution
*
*/
public class StatusDTO {
private final boolean online;
private final int responseCode;
@SerializedName("switch")
private final List<Integer> switchValue;
private final List<Integer> watt;
private final List<Integer> amp;
public StatusDTO() {
online = false;
responseCode = 0;
switchValue = null;
watt = null;
amp = null;
}
public StatusDTO(boolean online, int responseCode, List<Integer> switchValue, List<Integer> watt,
List<Integer> amp) {
this.online = online;
this.responseCode = responseCode;
this.switchValue = switchValue;
this.watt = watt;
this.amp = amp;
}
public boolean isOnline() {
return online;
}
public int getResponseCode() {
return responseCode;
}
public List<Integer> getSwitchValue() {
return switchValue;
}
public List<Integer> getWatt() {
return watt;
}
public List<Integer> getAmp() {
return amp;
}
@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
StatusDTO status = (StatusDTO) o;
return online == status.online && responseCode == status.responseCode
&& Objects.equals(switchValue, status.switchValue) && Objects.equals(watt, status.watt)
&& Objects.equals(amp, status.amp);
}
@Override
public int hashCode() {
return Objects.hash(online, responseCode, switchValue, watt, amp);
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,78 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.revogi.internal.api;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.jetbrains.annotations.NotNull;
import org.openhab.binding.revogi.internal.udp.UdpResponseDTO;
import org.openhab.binding.revogi.internal.udp.UdpSenderService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonSyntaxException;
/**
* The {@link StatusService} contains methods to get a status of a Revogi SmartStrip
*
* @author Andi Bräu - Initial contribution
*/
@NonNullByDefault
public class StatusService {
private static final String UDP_DISCOVERY_QUERY = "V3{\"sn\":\"%s\", \"cmd\": 90}";
public static final String VERSION_STRING = "V3";
private final Logger logger = LoggerFactory.getLogger(StatusService.class);
private final Gson gson = new GsonBuilder().create();
private final UdpSenderService udpSenderService;
public StatusService(UdpSenderService udpSenderService) {
this.udpSenderService = udpSenderService;
}
public CompletableFuture<StatusDTO> queryStatus(String serialNumber, String ipAddress) {
CompletableFuture<List<UdpResponseDTO>> responses;
if (ipAddress.trim().isEmpty()) {
responses = udpSenderService.broadcastUdpDatagram(String.format(UDP_DISCOVERY_QUERY, serialNumber));
} else {
responses = udpSenderService.sendMessage(String.format(UDP_DISCOVERY_QUERY, serialNumber), ipAddress);
}
return responses.thenApply(this::getStatus);
}
@NotNull
private StatusDTO getStatus(final List<UdpResponseDTO> singleResponse) {
return singleResponse.stream()
.filter(response -> !response.getAnswer().isEmpty() && response.getAnswer().contains(VERSION_STRING))
.map(response -> deserializeString(response.getAnswer()))
.filter(statusRaw -> statusRaw.getCode() == 200 && statusRaw.getResponse() == 90)
.map(statusRaw -> new StatusDTO(true, statusRaw.getCode(), statusRaw.getData().getSwitchValue(),
statusRaw.getData().getWatt(), statusRaw.getData().getAmp()))
.findFirst().orElse(new StatusDTO(false, 503, null, null, null));
}
private StatusRawDTO deserializeString(String response) {
String extractedJsonResponse = response.substring(response.lastIndexOf(VERSION_STRING) + 2);
try {
return gson.fromJson(extractedJsonResponse, StatusRawDTO.class);
} catch (JsonSyntaxException e) {
logger.warn("Could not parse string \"{}\" to StatusRaw", response, e);
return new StatusRawDTO(503, 0, new StatusDTO(false, 503, null, null, null));
}
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,85 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.revogi.internal.api;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.jetbrains.annotations.NotNull;
import org.openhab.binding.revogi.internal.udp.UdpResponseDTO;
import org.openhab.binding.revogi.internal.udp.UdpSenderService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonSyntaxException;
/**
* The {@link SwitchService} enables the binding to actually switch plugs on and of
*
* @author Andi Bräu - Initial contribution
*/
@NonNullByDefault
public class SwitchService {
private static final String UDP_DISCOVERY_QUERY = "V3{\"sn\":\"%s\", \"cmd\": 20, \"port\": %d, \"state\": %d}";
private static final String VERSION_STRING = "V3";
private final Logger logger = LoggerFactory.getLogger(SwitchService.class);
private final Gson gson = new GsonBuilder().create();
private final UdpSenderService udpSenderService;
public SwitchService(UdpSenderService udpSenderService) {
this.udpSenderService = udpSenderService;
}
public CompletableFuture<SwitchResponseDTO> switchPort(String serialNumber, String ipAddress, int port, int state) {
if (state < 0 || state > 1) {
throw new IllegalArgumentException("state has to be 0 or 1");
}
if (port < 0) {
throw new IllegalArgumentException("Given port doesn't exist");
}
CompletableFuture<List<UdpResponseDTO>> responses;
if (ipAddress.trim().isEmpty()) {
responses = udpSenderService
.broadcastUdpDatagram(String.format(UDP_DISCOVERY_QUERY, serialNumber, port, state));
} else {
responses = udpSenderService.sendMessage(String.format(UDP_DISCOVERY_QUERY, serialNumber, port, state),
ipAddress);
}
return responses.thenApply(this::getSwitchResponse);
}
@NotNull
private SwitchResponseDTO getSwitchResponse(final List<UdpResponseDTO> singleResponse) {
return singleResponse.stream().filter(response -> !response.getAnswer().isEmpty())
.map(response -> deserializeString(response.getAnswer()))
.filter(switchResponse -> switchResponse.getCode() == 200 && switchResponse.getResponse() == 20)
.findFirst().orElse(new SwitchResponseDTO(0, 503));
}
private SwitchResponseDTO deserializeString(String response) {
String extractedJsonResponse = response.substring(response.lastIndexOf(VERSION_STRING) + 2);
try {
return gson.fromJson(extractedJsonResponse, SwitchResponseDTO.class);
} catch (JsonSyntaxException e) {
logger.warn("Could not parse string \"{}\" to SwitchResponse", response);
return new SwitchResponseDTO(0, 503);
}
}
}

View File

@@ -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");
}
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,145 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.revogi.internal.udp;
import static java.util.stream.Collectors.toList;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.common.ThreadPoolManager;
import org.openhab.core.net.NetUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link UdpSenderService} is responsible for sending and receiving udp packets
*
* @author Andi Bräu - Initial contribution
*/
@NonNullByDefault
public class UdpSenderService {
/**
* Limit timeout waiting time, as we have to deal with UDP
*
* How it works: for every loop, we'll wait a bit longer, so the timeout counter is multiplied with the timeout base
* value. Let max timeout count be 2 and timeout base value 800, then we'll have a maximum of loops of 3, waiting
* 800ms in the 1st loop, 1600ms in the 2nd loop and 2400ms in the third loop.
*/
private static final int MAX_TIMEOUT_COUNT = 2;
private static final long TIMEOUT_BASE_VALUE_MS = 800L;
private static final int REVOGI_PORT = 8888;
private final Logger logger = LoggerFactory.getLogger(UdpSenderService.class);
private final DatagramSocketWrapper datagramSocketWrapper;
private final ScheduledExecutorService scheduler;
private final long timeoutBaseValue;
public UdpSenderService(DatagramSocketWrapper datagramSocketWrapper, ScheduledExecutorService scheduler) {
this.timeoutBaseValue = TIMEOUT_BASE_VALUE_MS;
this.datagramSocketWrapper = datagramSocketWrapper;
this.scheduler = scheduler;
}
public UdpSenderService(DatagramSocketWrapper datagramSocketWrapper, long timeout) {
this.timeoutBaseValue = timeout;
this.datagramSocketWrapper = datagramSocketWrapper;
this.scheduler = ThreadPoolManager.getScheduledPool("test pool");
}
public CompletableFuture<List<UdpResponseDTO>> broadcastUdpDatagram(String content) {
List<String> allBroadcastAddresses = NetUtil.getAllBroadcastAddresses();
CompletableFuture<List<UdpResponseDTO>> future = new CompletableFuture<>();
scheduler.submit(() -> future.complete(allBroadcastAddresses.stream().map(address -> {
try {
return sendMessage(content, InetAddress.getByName(address));
} catch (UnknownHostException e) {
logger.warn("Could not find host with IP {}", address);
return new ArrayList<UdpResponseDTO>();
}
}).flatMap(Collection::stream).distinct().collect(toList())));
return future;
}
public CompletableFuture<List<UdpResponseDTO>> sendMessage(String content, String ipAddress) {
try {
CompletableFuture<List<UdpResponseDTO>> future = new CompletableFuture<>();
InetAddress inetAddress = InetAddress.getByName(ipAddress);
scheduler.submit(() -> future.complete(sendMessage(content, inetAddress)));
return future;
} catch (UnknownHostException e) {
logger.warn("Could not find host with IP {}", ipAddress);
return CompletableFuture.completedFuture(Collections.emptyList());
}
}
private List<UdpResponseDTO> sendMessage(String content, InetAddress inetAddress) {
logger.debug("Using address {}", inetAddress);
byte[] buf = content.getBytes(Charset.defaultCharset());
DatagramPacket packet = new DatagramPacket(buf, buf.length, inetAddress, REVOGI_PORT);
List<UdpResponseDTO> responses = Collections.emptyList();
try {
datagramSocketWrapper.initSocket();
datagramSocketWrapper.sendPacket(packet);
responses = getUdpResponses();
} catch (IOException e) {
logger.warn("Error sending message or reading anwser {}", e.getMessage());
} finally {
datagramSocketWrapper.closeSocket();
}
return responses;
}
private List<UdpResponseDTO> getUdpResponses() {
int timeoutCounter = 0;
List<UdpResponseDTO> list = new ArrayList<>();
while (timeoutCounter < MAX_TIMEOUT_COUNT && !Thread.interrupted()) {
byte[] receivedBuf = new byte[512];
DatagramPacket answer = new DatagramPacket(receivedBuf, receivedBuf.length);
try {
datagramSocketWrapper.receiveAnswer(answer);
} catch (SocketTimeoutException | SocketException e) {
timeoutCounter++;
try {
TimeUnit.MILLISECONDS.sleep(timeoutCounter * timeoutBaseValue);
} catch (InterruptedException ex) {
logger.debug("Interrupted sleep");
Thread.currentThread().interrupt();
}
continue;
} catch (IOException e) {
logger.warn("Error sending message or reading anwser {}", e.getMessage());
}
if (answer.getAddress() != null && answer.getLength() > 0) {
list.add(new UdpResponseDTO(new String(answer.getData(), 0, answer.getLength()),
answer.getAddress().getHostAddress()));
}
}
return list;
}
}

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="revogi" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>Revogi Binding</name>
<description>This is the binding for Revogi devices. Revogi is a vendor of several smart home devices like light bulbs,
power strips and sensors.</description>
<author>Andi Bräu</author>
</binding:binding>

View File

@@ -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

View File

@@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="revogi"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<!-- SmartStrip Thing Type -->
<thing-type id="smartstrip">
<label>SmartStrip</label>
<description>A Thing to control Revogi SmartStrip</description>
<category>PowerOutlet</category>
<channel-groups>
<channel-group id="overallPlug" typeId="overallPlugActuator"/>
<channel-group id="plug1" typeId="plugActuator">
<label>Plug 1</label>
</channel-group>
<channel-group id="plug2" typeId="plugActuator">
<label>Plug 2</label>
</channel-group>
<channel-group id="plug3" typeId="plugActuator">
<label>Plug 3</label>
</channel-group>
<channel-group id="plug4" typeId="plugActuator">
<label>Plug 4</label>
</channel-group>
<channel-group id="plug5" typeId="plugActuator">
<label>Plug 5</label>
</channel-group>
<channel-group id="plug6" typeId="plugActuator">
<label>Plug 6</label>
</channel-group>
</channel-groups>
<representation-property>serialNumber</representation-property>
<config-description>
<parameter name="serialNumber" type="text" required="true">
<label>Serial Number</label>
<description>Serial number of your smart strip.</description>
</parameter>
<parameter name="pollInterval" type="integer" min="10" unit="s">
<label>Poll Interval</label>
<default>60</default>
<description>How often (seconds) should the smart strip status be polled?</description>
</parameter>
<parameter name="ipAddress" type="text">
<label>IP Address</label>
<description>IP Address of your smart strip</description>
<context>network-address</context>
</parameter>
</config-description>
</thing-type>
<channel-group-type id="plugActuator">
<label>Single Plug Actuator</label>
<description>Switches a single plug and retrieve stats for it</description>
<channels>
<channel id="switch" typeId="single-plug"/>
<channel id="watt" typeId="watts"/>
<channel id="amp" typeId="amps"/>
</channels>
</channel-group-type>
<channel-group-type id="overallPlugActuator">
<label>Overall Plug Actuator</label>
<description>Switches all plugs</description>
<channels>
<channel id="switch" typeId="single-plug"/>
</channels>
</channel-group-type>
<channel-type id="single-plug">
<item-type>Switch</item-type>
<label>Switch</label>
<description>Switch a single plug</description>
</channel-type>
<channel-type id="watts" advanced="true">
<item-type>Number:Power</item-type>
<label>Power</label>
<description>Contains the current watt value for the given plug</description>
<state readOnly="true" pattern="%.1f W"/>
</channel-type>
<channel-type id="amps" advanced="true">
<item-type>Number:ElectricCurrent</item-type>
<label>Current</label>
<description>Contains the current Amp value for the given plug</description>
<state readOnly="true" pattern="%.1f A"/>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,76 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.revogi.internal.api;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.binding.revogi.internal.udp.UdpResponseDTO;
import org.openhab.binding.revogi.internal.udp.UdpSenderService;
/**
* @author Andi Bräu - Initial contribution
*/
@NonNullByDefault
public class RevogiDiscoveryServiceTest {
private final UdpSenderService udpSenderService = mock(UdpSenderService.class);
private final RevogiDiscoveryService revogiDiscoveryService = new RevogiDiscoveryService(udpSenderService);
@Test
public void discoverSmartStripSuccesfully() {
// given
DiscoveryResponseDTO discoveryResponse = new DiscoveryResponseDTO("1234", "reg", "sak", "Strip", "mac", "5.11");
List<UdpResponseDTO> discoveryString = Collections.singletonList(new UdpResponseDTO(
"{\"response\":0,\"data\":{\"sn\":\"1234\",\"regid\":\"reg\",\"sak\":\"sak\",\"name\":\"Strip\",\"mac\":\"mac\",\"ver\":\"5.11\"}}",
"127.0.0.1"));
when(udpSenderService.broadcastUdpDatagram("00sw=all,,,;"))
.thenReturn(CompletableFuture.completedFuture(discoveryString));
// when
CompletableFuture<List<DiscoveryRawResponseDTO>> discoverSmartStripsFutures = revogiDiscoveryService
.discoverSmartStrips();
// then
List<DiscoveryRawResponseDTO> discoverSmartStrips = discoverSmartStripsFutures.getNow(Collections.emptyList());
assertThat(discoverSmartStrips.size(), equalTo(1));
assertThat(discoverSmartStrips.get(0).getData(), equalTo(discoveryResponse));
assertThat(discoverSmartStrips.get(0).getIpAddress(), equalTo("127.0.0.1"));
}
@Test
public void invalidUdpResponse() throws ExecutionException, InterruptedException {
// given
List<UdpResponseDTO> discoveryString = Collections
.singletonList(new UdpResponseDTO("something invalid", "12345"));
when(udpSenderService.broadcastUdpDatagram("00sw=all,,,;"))
.thenReturn(CompletableFuture.completedFuture(discoveryString));
// when
CompletableFuture<List<DiscoveryRawResponseDTO>> futureList = revogiDiscoveryService.discoverSmartStrips();
// then
List<DiscoveryRawResponseDTO> discoverSmartStrips = futureList.get();
assertThat(discoverSmartStrips.isEmpty(), is(true));
}
}

View File

@@ -0,0 +1,70 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.revogi.internal.api;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.binding.revogi.internal.udp.UdpResponseDTO;
import org.openhab.binding.revogi.internal.udp.UdpSenderService;
/**
* @author Andi Bräu - Initial contribution
*/
@NonNullByDefault
public class StatusServiceTest {
private final UdpSenderService udpSenderService = mock(UdpSenderService.class);
private final StatusService statusService = new StatusService(udpSenderService);
@Test
public void getStatusSuccessfully() {
// given
StatusDTO status = new StatusDTO(true, 200, Arrays.asList(0, 0, 0, 0, 0, 0), Arrays.asList(0, 0, 0, 0, 0, 0),
Arrays.asList(0, 0, 0, 0, 0, 0));
List<UdpResponseDTO> statusString = Collections.singletonList(new UdpResponseDTO(
"V3{\"response\":90,\"code\":200,\"data\":{\"switch\":[0,0,0,0,0,0],\"watt\":[0,0,0,0,0,0],\"amp\":[0,0,0,0,0,0]}}",
"127.0.0.1"));
when(udpSenderService.sendMessage("V3{\"sn\":\"serial\", \"cmd\": 90}", "127.0.0.1"))
.thenReturn(CompletableFuture.completedFuture(statusString));
// when
CompletableFuture<StatusDTO> statusResponse = statusService.queryStatus("serial", "127.0.0.1");
// then
assertEquals(status, statusResponse.getNow(new StatusDTO()));
}
@Test
public void invalidUdpResponse() {
// given
List<UdpResponseDTO> statusString = Collections.singletonList(new UdpResponseDTO("something invalid", "12345"));
when(udpSenderService.sendMessage("V3{\"sn\":\"serial\", \"cmd\": 90}", "127.0.0.1"))
.thenReturn(CompletableFuture.completedFuture(statusString));
// when
CompletableFuture<StatusDTO> futureStatus = statusService.queryStatus("serial", "127.0.0.1");
// then
StatusDTO status = futureStatus.getNow(new StatusDTO());
assertEquals(503, status.getResponseCode());
}
}

View File

@@ -0,0 +1,94 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.revogi.internal.api;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.openhab.binding.revogi.internal.udp.UdpResponseDTO;
import org.openhab.binding.revogi.internal.udp.UdpSenderService;
/**
* @author Andi Bräu - Initial contribution
*/
@NonNullByDefault
public class SwitchServiceTest {
private UdpSenderService udpSenderService = mock(UdpSenderService.class);
private SwitchService switchService = new SwitchService(udpSenderService);
@Test
public void getStatusSuccesfully() {
// given
List<UdpResponseDTO> response = Collections
.singletonList(new UdpResponseDTO("V3{\"response\":20,\"code\":200}", "127.0.0.1"));
when(udpSenderService.sendMessage("V3{\"sn\":\"serial\", \"cmd\": 20, \"port\": 1, \"state\": 1}", "127.0.0.1"))
.thenReturn(CompletableFuture.completedFuture(response));
// when
CompletableFuture<SwitchResponseDTO> switchResponse = switchService.switchPort("serial", "127.0.0.1", 1, 1);
// then
assertThat(switchResponse.getNow(new SwitchResponseDTO(0, 0)), equalTo(new SwitchResponseDTO(20, 200)));
}
@Test
public void getStatusSuccesfullyWithBroadcast() {
// given
List<UdpResponseDTO> response = Collections
.singletonList(new UdpResponseDTO("V3{\"response\":20,\"code\":200}", "127.0.0.1"));
when(udpSenderService.broadcastUdpDatagram("V3{\"sn\":\"serial\", \"cmd\": 20, \"port\": 1, \"state\": 1}"))
.thenReturn(CompletableFuture.completedFuture(response));
// when
CompletableFuture<SwitchResponseDTO> switchResponse = switchService.switchPort("serial", "", 1, 1);
// then
assertThat(switchResponse.getNow(new SwitchResponseDTO(0, 0)), equalTo(new SwitchResponseDTO(20, 200)));
}
@Test
public void invalidUdpResponse() {
// given
List<UdpResponseDTO> response = Collections.singletonList(new UdpResponseDTO("something invalid", "12345"));
when(udpSenderService.sendMessage("V3{\"sn\":\"serial\", \"cmd\": 20, \"port\": 1, \"state\": 1}", "127.0.0.1"))
.thenReturn(CompletableFuture.completedFuture(response));
// when
CompletableFuture<SwitchResponseDTO> switchResponse = switchService.switchPort("serial", "127.0.0.1", 1, 1);
// then
assertThat(switchResponse.getNow(new SwitchResponseDTO(0, 0)), equalTo(new SwitchResponseDTO(0, 503)));
}
@Test
public void getExceptionOnWrongState() {
Assertions.assertThrows(IllegalArgumentException.class,
() -> switchService.switchPort("serial", "127.0.0.1", 1, 12));
}
@Test
public void getExceptionOnWrongPort() {
Assertions.assertThrows(IllegalArgumentException.class,
() -> switchService.switchPort("serial", "127.0.0.1", -1, 1));
}
}

View File

@@ -0,0 +1,82 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.revogi.internal.udp;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.SocketTimeoutException;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.core.net.NetUtil;
/**
* @author Andi Bräu - Initial contribution
*/
@NonNullByDefault
public class UdpSenderServiceTest {
private final DatagramSocketWrapper datagramSocketWrapper = mock(DatagramSocketWrapper.class);
private final UdpSenderService udpSenderService = new UdpSenderService(datagramSocketWrapper, 1L);
private final int numberOfInterfaces = NetUtil.getAllBroadcastAddresses().size();
@Test
public void testTimeout() throws IOException, ExecutionException, InterruptedException {
// given
doThrow(new SocketTimeoutException()).when(datagramSocketWrapper).receiveAnswer(any());
// when
CompletableFuture<List<UdpResponseDTO>> list = udpSenderService.broadcastUdpDatagram("send something");
// then
assertThat(list.get(), equalTo(Collections.emptyList()));
verify(datagramSocketWrapper, times(numberOfInterfaces * 2)).receiveAnswer(any());
}
@Test
public void testOneAnswer() throws IOException, ExecutionException, InterruptedException {
// given
byte[] receivedBuf = "valid answer".getBytes();
doAnswer(invocation -> {
DatagramPacket argument = invocation.getArgument(0);
argument.setData(receivedBuf);
argument.setAddress(InetAddress.getLocalHost());
return null;
}).doThrow(new SocketTimeoutException()).when(datagramSocketWrapper).receiveAnswer(any());
// when
CompletableFuture<List<UdpResponseDTO>> future = udpSenderService.broadcastUdpDatagram("send something");
// then
List<UdpResponseDTO> udpResponses = future.get();
assertThat(udpResponses.get(0).getAnswer(), is("valid answer"));
verify(datagramSocketWrapper, times(1 + 2 * numberOfInterfaces)).receiveAnswer(any());
}
}