[pegelonline] Initial contribution (#16831)

* initial version

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
This commit is contained in:
Bernd Weymann 2024-06-15 19:38:55 +02:00 committed by GitHub
parent 5e157262c5
commit 7fa43ea8b1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 11055 additions and 0 deletions

View File

@ -278,6 +278,7 @@
/bundles/org.openhab.binding.orvibo/ @tavalin /bundles/org.openhab.binding.orvibo/ @tavalin
/bundles/org.openhab.binding.panasonicbdp/ @mlobstein /bundles/org.openhab.binding.panasonicbdp/ @mlobstein
/bundles/org.openhab.binding.paradoxalarm/ @theater /bundles/org.openhab.binding.paradoxalarm/ @theater
/bundles/org.openhab.binding.pegelonline/ @weymann
/bundles/org.openhab.binding.pentair/ @jsjames /bundles/org.openhab.binding.pentair/ @jsjames
/bundles/org.openhab.binding.phc/ @gnlpfjh /bundles/org.openhab.binding.phc/ @gnlpfjh
/bundles/org.openhab.binding.pilight/ @stefanroellin @niklasdoerfler /bundles/org.openhab.binding.pilight/ @stefanroellin @niklasdoerfler

View File

@ -1381,6 +1381,11 @@
<artifactId>org.openhab.binding.paradoxalarm</artifactId> <artifactId>org.openhab.binding.paradoxalarm</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.pegelonline</artifactId>
<version>${project.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.openhab.addons.bundles</groupId> <groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.pentair</artifactId> <artifactId>org.openhab.binding.pentair</artifactId>

View File

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

View File

@ -0,0 +1,122 @@
# PegelOnline Binding
Binding to observe water level from german rivers.
Data is provided by german **Water-Route and Shipping Agency** [WSV](https://www.pegelonline.wsv.de/).
Goal is to monitor actual water levels from rivers nearby your home.
In case of changing water levels the corresponding warning level is lowered or raised.
## Supported Things
| Label | Description | ID |
|---------------------|---------------------------------------------------------------------------------|---------|
| Measurement Station | Station providing water level measurements | station |
## Discovery
In case your home location coordinates are set the discovery will recognize all measurement stations within a radius of 50 km.
Found Things are added in your Inbox.
## Thing Configuration
Thing configuration contains 3 sections
* [Station selection](station_selection)
* [Warning Levels of selected station](warning_levels)
* [Refresh rate](configuration_parameters)
### Station selection
Stations can be selected with an Universally Unique Identifier (uuid).
It's automatically added by the Discovery.
Configure a station manually using [list of all available stations](https://pegelonline.wsv.de/gast/pegeltabelle) or [stations.json](https://www.pegelonline.wsv.de/webservices/rest-api/v2/stations.json) and choose the uuid of your desired measurement station.
### Warning Levels
<img align="right" src="./doc/Marburg.png" width="450" height="500"/>
Each station has specific warning levels
* Warning Levels 1 (*lowest*) to 3 (*highest*)
* Flooding Levels
Unfortunately these levels cannot be queried automatically.
Please select your [federal state](https://www.hochwasserzentralen.de/) and check if which levels they provide.
The picture shows the levels of [measurement station Marburg of federal state Hesse](https://www.hlnug.de/static/pegel/wiskiweb2/stations/25830056/station.html?v=20210802152952)
If you cannot evaluate warning or flooding levels leave the parameter empty.
### Configuration parameters
| configuration | content | unit | description | required | default |
|------------------|-----------|------|---------------------------|----------|---------|
| uuid | text | - | Unique Station Identifier | X | N/A |
| warningLevel1 | integer | cm | Warning Level 1 | | N/A |
| warningLevel2 | integer | cm | Warning Level 2 | | N/A |
| warningLevel3 | integer | cm | Warning Level 3 | | N/A |
| hq10 | integer | cm | Decade Flooding | | N/A |
| hq100 | integer | cm | Century Flooding | | N/A |
| hqExtreme | integer | cm | Extreme Flooding | | N/A |
| refreshInterval | integer | min | Refresh Interval | X | 15 |
## Channels
| channel id | type | description |
|----------------------|----------------------|--------------------------------|
| timestamp | DateTime | Last Measurement |
| level | Number:Length | Water Level |
| trend | Number | Water Level Trend |
| warning | Number | Current Warning |
### Trend
Possible values:
* 1 : Rising
* 0 : Steady
* -1 : Lowering
### Warning
Current warning according to configuration
* 0 : No Warning
* 1 : Warning level 1
* 2 : Warning Level 2
* 3 : Warning Level 3
* 4 : Decade Flooding
* 5 : Century Flooding
* 6 : Extreme Flooding
## Full Example
### Things
```java
Thing pegelonline:station:giessen "Measurement Station Giessen" [
uuid="4b386a6a-996e-4a4a-a440-15d6b40226d4",
refreshInterval=15,
warningLevel1=550,
warningLevel2=600,
warningLevel3=650,
hq10=732,
hq100=786
]
```
### Items
```java
DateTime Lahn_Giessen_Timestamp "Measurement timestamp Lahn Giessen" {channel="pegelonline:station:giessen:timestamp" }
Number:Length Lahn_Giessen_Level "Water Level Lahn Giessen]" {channel="pegelonline:station:giessen:level" }
Number Lahn_Giessen_Trend "Water Level Trend Lahn Giessen" {channel="pegelonline:station:giessen:trend"}
Number Lahn_Giessen_Warning "Warning Level Lahn Giessen" {channel="pegelonline:station:giessen:warning"}
```
## Links
[PegelOnline API Documentation](https://www.pegelonline.wsv.de/webservice/dokuRestapi#caching)

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>4.2.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.pegelonline</artifactId>
<name>openHAB Add-ons :: Bundles :: PegelOnline Binding</name>
</project>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.pegelonline-${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-pegelonline" description="PegelOnline Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.pegelonline/${project.version}</bundle>
</feature>
</features>

View File

@ -0,0 +1,63 @@
/**
* Copyright (c) 2010-2024 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.pegelonline.internal;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.library.types.PointType;
import org.openhab.core.thing.ThingTypeUID;
import com.google.gson.Gson;
/**
* The {@link PegelOnlineBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class PegelOnlineBindingConstants {
private static final String BINDING_ID = "pegelonline";
// List of all Thing Type UIDs
public static final ThingTypeUID STATION_THING = new ThingTypeUID(BINDING_ID, "station");
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(STATION_THING);
// List of all Channel ids
public static final String TIMESTAMP_CHANNEL = "timestamp";
public static final String LEVEL_CHANNEL = "level";
public static final String TREND_CHANNEL = "trend";
public static final String WARNING_CHANNEL = "warning";
public static final int NO_WARNING = 0;
public static final int WARN_LEVEL_1 = 1;
public static final int WARN_LEVEL_2 = 2;
public static final int WARN_LEVEL_3 = 3;
public static final int HQ10 = 4;
public static final int HQ100 = 5;
public static final int HQ_EXTREME = 6;
public static final Gson GSON = new Gson();
public static final String STATIONS_URI = "https://www.pegelonline.wsv.de/webservices/rest-api/v2/stations";
public static final double DISCOVERY_RADIUS = 50;
public static final PointType UNDEF_LOCATION = PointType.valueOf("-1,-1");
public static final String SPACE = " ";
public static final String UNDERLINE = "_";
public static final String HYPHEN = " - ";
public static final String EMPTY = "";
public static final String UNKNOWN = "Unknown";
}

View File

@ -0,0 +1,60 @@
/**
* Copyright (c) 2010-2024 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.pegelonline.internal;
import static org.openhab.binding.pegelonline.internal.PegelOnlineBindingConstants.STATION_THING;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.pegelonline.internal.handler.PegelOnlineHandler;
import org.openhab.core.i18n.LocationProvider;
import org.openhab.core.io.net.http.HttpClientFactory;
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.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link PegelOnlineHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
@Component(configurationPid = "binding.pegelonline", service = ThingHandlerFactory.class)
public class PegelOnlineHandlerFactory extends BaseThingHandlerFactory {
private final HttpClientFactory httpClientFactory;
@Activate
public PegelOnlineHandlerFactory(final @Reference HttpClientFactory hcf, final @Reference LocationProvider lp) {
httpClientFactory = hcf;
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return PegelOnlineBindingConstants.SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (STATION_THING.equals(thingTypeUID)) {
return new PegelOnlineHandler(thing, httpClientFactory.getCommonHttpClient());
}
return null;
}
}

View File

@ -0,0 +1,81 @@
/**
* Copyright (c) 2010-2024 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.pegelonline.internal.config;
import static org.openhab.binding.pegelonline.internal.PegelOnlineBindingConstants.*;
import java.util.Map.Entry;
import java.util.TreeMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link PegelOnlineConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class PegelOnlineConfiguration {
public String uuid = UNKNOWN;
public int warningLevel1 = Integer.MAX_VALUE;
public int warningLevel2 = Integer.MAX_VALUE;
public int warningLevel3 = Integer.MAX_VALUE;
public int hq10 = Integer.MAX_VALUE;
public int hq100 = Integer.MAX_VALUE;
public int hqExtreme = Integer.MAX_VALUE;
public int refreshInterval = 15;
public boolean uuidCheck() {
// https://stackoverflow.com/questions/20041051/how-to-judge-a-string-is-uuid-type
return uuid.matches("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$");
}
/**
* Check if configured warning levels are in ascending order
*
* @return true if ascending, false otherwise
*/
public boolean warningCheck() {
TreeMap<Integer, Integer> warnMap = this.getWarnings();
Entry<Integer, Integer> currentEntry = warnMap.firstEntry();
Entry<Integer, Integer> nextEntry = warnMap.higherEntry(currentEntry.getKey());
while (nextEntry != null) {
// ignore non configured values
if (nextEntry.getKey() != Integer.MAX_VALUE) {
if (nextEntry.getValue() < currentEntry.getValue()) {
return false;
}
}
currentEntry = nextEntry;
nextEntry = warnMap.higherEntry(currentEntry.getKey());
}
return true;
}
/**
* Calculate sorted map with level height and warning level based on configuration
*
* @return TreeMap with keys containing level height and values containing warning level
*/
public TreeMap<Integer, Integer> getWarnings() {
TreeMap<Integer, Integer> warnMap = new TreeMap<>();
warnMap.put(0, NO_WARNING);
warnMap.put(warningLevel1, WARN_LEVEL_1);
warnMap.put(warningLevel2, WARN_LEVEL_2);
warnMap.put(warningLevel3, WARN_LEVEL_3);
warnMap.put(hq10, HQ10);
warnMap.put(hq100, HQ100);
warnMap.put(hqExtreme, HQ_EXTREME);
return warnMap;
}
}

View File

@ -0,0 +1,122 @@
/**
* Copyright (c) 2010-2024 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.pegelonline.internal.discovery;
import static org.openhab.binding.pegelonline.internal.PegelOnlineBindingConstants.*;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.api.ContentResponse;
import org.openhab.binding.pegelonline.internal.dto.Station;
import org.openhab.binding.pegelonline.internal.handler.PegelOnlineHandler;
import org.openhab.binding.pegelonline.internal.utils.Utils;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.i18n.LocationProvider;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.library.types.PointType;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@link PegelDiscovery} Discovery of measurement stations
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
@Component(service = DiscoveryService.class, immediate = true, configurationPid = "discovery.pegelonline")
public class PegelDiscovery extends AbstractDiscoveryService implements ThingHandlerService {
private final Logger logger = LoggerFactory.getLogger(PegelDiscovery.class);
private Optional<PegelOnlineHandler> handler = Optional.empty();
private PointType homeLocation = UNDEF_LOCATION;
private HttpClientFactory httpClientFactory;
@Activate
public PegelDiscovery(final @Reference HttpClientFactory hcf, final @Reference LocationProvider lp) {
super(SUPPORTED_THING_TYPES_UIDS, 10, false);
httpClientFactory = hcf;
PointType location = lp.getLocation();
if (location != null) {
homeLocation = location;
} else {
logger.debug("No home location found");
}
}
@Override
protected void startScan() {
double homeLat = homeLocation.getLatitude().doubleValue();
double homeLon = homeLocation.getLongitude().doubleValue();
try {
ContentResponse cr = httpClientFactory.getCommonHttpClient().GET(STATIONS_URI);
Station[] stationArray = GSON.fromJson(cr.getContentAsString(), Station[].class);
if (stationArray != null) {
for (Station station : stationArray) {
double distance = Utils.calculateDistance(homeLat, homeLon, station.latitude, station.longitude);
if (distance < DISCOVERY_RADIUS) {
logger.trace("Station in range {},{}", station.longname, station.water.shortname);
reportResult(station);
}
}
} else {
logger.trace("No stations found in discovery");
}
} catch (ExecutionException | TimeoutException | InterruptedException e) {
logger.trace("Exception during station discovery: {}", e.getMessage());
}
}
public void reportResult(Station s) {
String label = "Pegel Station " + Utils.toTitleCase(s.shortname) + " / " + Utils.toTitleCase(s.water.shortname);
Map<String, Object> properties = new HashMap<String, Object>();
properties.put("agency", s.agency);
properties.put("km", s.km);
properties.put("river", s.water.longname);
properties.put("station", s.longname);
properties.put("uuid", s.uuid);
properties.put("location", s.latitude + "," + s.longitude);
ThingUID uid = new ThingUID(STATION_THING, s.uuid);
thingDiscovered(DiscoveryResultBuilder.create(uid).withRepresentationProperty("uuid").withLabel(label)
.withProperties(properties).build());
}
@Override
public void deactivate() {
super.deactivate();
}
@Override
public void setThingHandler(ThingHandler thingHandler) {
if (thingHandler instanceof PegelOnlineHandler pegelOnlineHandler) {
handler = Optional.of(pegelOnlineHandler);
}
}
@Override
public @Nullable ThingHandler getThingHandler() {
return handler.orElse(null);
}
}

View File

@ -0,0 +1,26 @@
/**
* Copyright (c) 2010-2024 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.pegelonline.internal.dto;
/**
* {@link Measure} DTO for water level measurements
*
* @author Bernd Weymann - Initial contribution
*/
public class Measure {
public String timestamp; // "2021-07-31T19:00:00+02:00",
public double value; // ":238.0,
public int trend; // -1,
public String stateMnwMhw; // "normal",
public String stateNswHsw; // "unknown"
}

View File

@ -0,0 +1,30 @@
/**
* Copyright (c) 2010-2024 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.pegelonline.internal.dto;
/**
* {@link Station} DTO for measurement Station
*
* @author Bernd Weymann - Initial contribution
*/
public class Station {
public String uuid; // "47174d8f-1b8e-4599-8a59-b580dd55bc87",
public long number; // "48900237",
public String shortname; // "EITZE",
public String longname; // "EITZE",
public double km; // 9.56,
public String agency; // : "WSA VERDEN",
public double longitude; // 9.27676943537587,
public double latitude; // 52.90406541008721,
public Water water;
}

View File

@ -0,0 +1,23 @@
/**
* Copyright (c) 2010-2024 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.pegelonline.internal.dto;
/**
* {@link Station} DTO for river naming
*
* @author Bernd Weymann - Initial contribution
*/
public class Water {
public String shortname; // "ALLER",
public String longname; // "ALLER"
}

View File

@ -0,0 +1,178 @@
/**
* Copyright (c) 2010-2024 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.pegelonline.internal.handler;
import static org.openhab.binding.pegelonline.internal.PegelOnlineBindingConstants.*;
import java.util.Optional;
import java.util.TreeMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.openhab.binding.pegelonline.internal.config.PegelOnlineConfiguration;
import org.openhab.binding.pegelonline.internal.dto.Measure;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.MetricPrefix;
import org.openhab.core.library.unit.SIUnits;
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.openhab.core.types.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link PegelOnlineHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class PegelOnlineHandler extends BaseThingHandler {
private static final String STATIONS_URI = "https://www.pegelonline.wsv.de/webservices/rest-api/v2/stations";
private final Logger logger = LoggerFactory.getLogger(PegelOnlineHandler.class);
private Optional<PegelOnlineConfiguration> configuration = Optional.empty();
private Optional<ScheduledFuture<?>> schedule = Optional.empty();
private Optional<Measure> cache = Optional.empty();
private TreeMap<Integer, Integer> warnMap = new TreeMap<>();
private String stationUUID = UNKNOWN;
private HttpClient httpClient;
public PegelOnlineHandler(Thing thing, HttpClient hc) {
super(thing);
httpClient = hc;
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (command instanceof RefreshType) {
if (cache.isPresent()) {
Measure m = cache.get();
if (LEVEL_CHANNEL.equals(channelUID.getId())) {
updateChannelState(LEVEL_CHANNEL, QuantityType.valueOf(m.value, MetricPrefix.CENTI(SIUnits.METRE)));
} else if (TREND_CHANNEL.equals(channelUID.getId())) {
updateChannelState(TREND_CHANNEL, DecimalType.valueOf(Integer.toString(m.trend)));
} else if (TIMESTAMP_CHANNEL.equals(channelUID.getId())) {
updateChannelState(TIMESTAMP_CHANNEL, DateTimeType.valueOf(m.timestamp));
} else if (WARNING_CHANNEL.equals(channelUID.getId())) {
updateChannelState(WARNING_CHANNEL,
DecimalType.valueOf(Integer.toString(warnMap.floorEntry((int) m.value).getValue())));
}
}
}
}
@Override
public void initialize() {
PegelOnlineConfiguration config = getConfigAs(PegelOnlineConfiguration.class);
stationUUID = config.uuid;
if (!config.uuidCheck()) {
String description = "@text/pegelonline.handler.status.uuid [\"" + stationUUID + "\"]";
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, description);
return;
}
if (!config.warningCheck()) {
String description = "@text/pegelonline.handler.status.warning";
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, description);
return;
}
warnMap = config.getWarnings();
configuration = Optional.of(config);
String description = "@text/pegelonline.handler.status.wait-feedback";
updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, description);
schedule = Optional.of(scheduler.scheduleWithFixedDelay(this::performMeasurement, 0,
configuration.get().refreshInterval, TimeUnit.MINUTES));
}
@Override
public void dispose() {
warnMap.clear();
if (schedule.isPresent()) {
schedule.get().cancel(true);
}
schedule = Optional.empty();
}
@Override
public void updateConfiguration(Configuration configuration) {
super.updateConfiguration(configuration);
}
void performMeasurement() {
try {
ContentResponse cr = httpClient.GET(STATIONS_URI + "/" + stationUUID + "/W/currentmeasurement.json");
int responseStatus = cr.getStatus();
if (responseStatus == 200) {
String content = cr.getContentAsString();
Measure measureDto = GSON.fromJson(content, Measure.class);
if (isValid(measureDto) && measureDto != null) {
updateStatus(ThingStatus.ONLINE);
updateChannels(measureDto);
} else {
String description = "@text/pegelonline.handler.status.json-error [\"" + content + "\"]";
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, description);
}
} else if (responseStatus == 404) {
// 404 respoonse shows station isn't found
String description = "@text/pegelonline.handler.status.uuid-not-found [\"" + stationUUID + "\"]";
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, description);
} else {
String description = "@text/pegelonline.handler.status.http-status [\"" + responseStatus + "\"]";
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, description);
}
} catch (InterruptedException | ExecutionException | TimeoutException e) {
String description = "@text/pegelonline.handler.status.http-exception [\"" + e.getMessage() + "\"]";
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, description);
}
}
private boolean isValid(@Nullable Measure measureDto) {
if (measureDto != null) {
if (measureDto.timestamp != null) {
try {
DateTimeType.valueOf(measureDto.timestamp);
return true;
} catch (Exception e) {
logger.trace("Error converting {} into DateTime: {}", measureDto.timestamp, e.getMessage());
}
}
}
return false;
}
private void updateChannels(Measure measureDto) {
cache = Optional.of(measureDto);
updateChannelState(TIMESTAMP_CHANNEL, DateTimeType.valueOf(measureDto.timestamp));
updateChannelState(LEVEL_CHANNEL, QuantityType.valueOf(measureDto.value, MetricPrefix.CENTI(SIUnits.METRE)));
updateChannelState(TREND_CHANNEL, DecimalType.valueOf(Integer.toString(measureDto.trend)));
updateChannelState(WARNING_CHANNEL,
DecimalType.valueOf(Integer.toString(warnMap.floorEntry((int) measureDto.value).getValue())));
}
private void updateChannelState(String channel, State st) {
updateState(new ChannelUID(thing.getUID(), channel), st);
}
}

View File

@ -0,0 +1,73 @@
/**
* Copyright (c) 2010-2024 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.pegelonline.internal.utils;
import static org.openhab.binding.pegelonline.internal.PegelOnlineBindingConstants.UNKNOWN;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.util.StringUtils;
/**
* {@link Utils} Utilities for binding
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class Utils {
public static final int EARTH_RADIUS = 6371;
/**
* Calculate the Distance Using Equirectangular Distance Approximation
*
* @param lat1 - Latitude of coordinate 1
* @param lon1 - Longitude of coordinate 1
* @param lat2 - Latitude of coordinate 2
* @param lon2 - Longitude of coordinate 2
* @return distance in km
*
* @see https://www.baeldung.com/java-find-distance-between-points#equirectangular-distance-approximation
*
*/
public static double calculateDistance(double lat1, double lon1, double lat2, double lon2) {
double lat1Rad = Math.toRadians(lat1);
double lat2Rad = Math.toRadians(lat2);
double lon1Rad = Math.toRadians(lon1);
double lon2Rad = Math.toRadians(lon2);
double x = (lon2Rad - lon1Rad) * Math.cos((lat1Rad + lat2Rad) / 2);
double y = (lat2Rad - lat1Rad);
double distance = Math.sqrt(x * x + y * y) * EARTH_RADIUS;
return distance;
}
/**
* Converts String from "all upper case" into "title case" after space and hyphen
*
* @param input - string to convert
* @return title case string
*/
public static String toTitleCase(@Nullable String input) {
if (input == null) {
return toTitleCase(UNKNOWN);
} else {
StringBuffer titleCaseString = new StringBuffer();
for (String string : StringUtils.splitByCharacterType(input)) {
String converted = StringUtils.capitalize(string.toLowerCase());
titleCaseString.append(converted);
}
return titleCaseString.toString();
}
}
}

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<addon:addon id="pegelonline" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:addon="https://openhab.org/schemas/addon/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/addon/v1.0.0 https://openhab.org/schemas/addon-1.0.0.xsd">
<type>binding</type>
<name>PegelOnline Binding</name>
<description>This is the binding for PegelOnline.</description>
<connection>cloud</connection>
<countries>de</countries>
</addon:addon>

View File

@ -0,0 +1,58 @@
# add-on
addon.pegelonline.name = PegelOnline Binding
addon.pegelonline.description = This is the binding for PegelOnline.
# thing types
thing-type.pegelonline.station.label = Measurement Station
thing-type.pegelonline.station.description = Station providing water level measurements
# thing types config
thing-type.config.pegelonline.station.hq10.label = Decade Flooding
thing-type.config.pegelonline.station.hq10.description = Water level of decade flooding 10-20 years
thing-type.config.pegelonline.station.hq100.label = Century Flooding
thing-type.config.pegelonline.station.hq100.description = Water level of century flooding in ~ 100 years
thing-type.config.pegelonline.station.hqExtreme.label = Extreme Flooding
thing-type.config.pegelonline.station.hqExtreme.description = Water level of extra ordinary flooding > 200 years
thing-type.config.pegelonline.station.refreshInterval.label = Refresh Interval
thing-type.config.pegelonline.station.refreshInterval.description = Interval measurement polling in minutes.
thing-type.config.pegelonline.station.uuid.label = Station Identifier
thing-type.config.pegelonline.station.uuid.description = Unique Station Identifier
thing-type.config.pegelonline.station.warningLevel1.label = Warning Level 1
thing-type.config.pegelonline.station.warningLevel1.description = Water level triggering level 1 warning
thing-type.config.pegelonline.station.warningLevel2.label = Warning Level 2
thing-type.config.pegelonline.station.warningLevel2.description = Water level triggering level 2 warning
thing-type.config.pegelonline.station.warningLevel3.label = Warning Level 3
thing-type.config.pegelonline.station.warningLevel3.description = Water level triggering level 3 warning
# channel types
channel-type.pegelonline.level.label = Water Level
channel-type.pegelonline.timestamp.label = Last Measurement
channel-type.pegelonline.timestamp.state.pattern = %1$tA, %1$td.%1$tm. %1$tH:%1$tM
channel-type.pegelonline.trend.label = Water Level Trend
channel-type.pegelonline.trend.state.option.-1 = Lowering
channel-type.pegelonline.trend.state.option.0 = Steady
channel-type.pegelonline.trend.state.option.1 = Rising
channel-type.pegelonline.warning.label = Warning Level
channel-type.pegelonline.warning.state.option.0 = No warning
channel-type.pegelonline.warning.state.option.1 = Warning Level 1
channel-type.pegelonline.warning.state.option.2 = Warning Level 2
channel-type.pegelonline.warning.state.option.3 = Warning Level 3
channel-type.pegelonline.warning.state.option.4 = Decade Flooding
channel-type.pegelonline.warning.state.option.5 = Century Flooding
channel-type.pegelonline.warning.state.option.6 = Extreme Flooding
# channel types
pegelonline.handler.status.uuid = Unique Identifier {0} not valid
pegelonline.handler.status.warning = Warnings shall be entered in increasing order
pegelonline.handler.status.flooding = Flooding Levels shall be entered in increasing order
pegelonline.handler.status.wait-feedback = Wait for first feedback
pegelonline.handler.status.uuid-not-found = No station found for uuid {0}
pegelonline.handler.status.uuid-verification = Verification for uuid {0} ongoing. Next try in 1 minute.
pegelonline.handler.status.http-status = HTTP status {0} received
pegelonline.handler.status.http-exception = Exception {0}
pegelonline.handler.status.json-error = Error parsing {0}

View File

@ -0,0 +1,94 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="pegelonline"
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">
<thing-type id="station">
<label>Measurement Station</label>
<description>Station providing water level measurements</description>
<channels>
<channel id="timestamp" typeId="timestamp"/>
<channel id="level" typeId="level"/>
<channel id="trend" typeId="trend"/>
<channel id="warning" typeId="warning"/>
</channels>
<representation-property>uuid</representation-property>
<config-description>
<parameter name="uuid" type="text" required="true">
<label>Station Identifier</label>
<description>Unique Station Identifier</description>
</parameter>
<parameter name="warningLevel1" type="integer">
<label>Warning Level 1</label>
<description>Water level triggering level 1 warning</description>
</parameter>
<parameter name="warningLevel2" type="integer">
<label>Warning Level 2</label>
<description>Water level triggering level 2 warning</description>
</parameter>
<parameter name="warningLevel3" type="integer">
<label>Warning Level 3</label>
<description>Water level triggering level 3 warning</description>
</parameter>
<parameter name="hq10" type="integer">
<label>Decade Flooding</label>
<description>Water level of decade flooding 10-20 years</description>
</parameter>
<parameter name="hq100" type="integer">
<label>Century Flooding</label>
<description>Water level of century flooding in ~ 100 years</description>
</parameter>
<parameter name="hqExtreme" type="integer">
<label>Extreme Flooding</label>
<description>Water level of extra ordinary flooding > 200 years</description>
</parameter>
<parameter name="refreshInterval" type="integer" unit="m" min="1" required="true">
<label>Refresh Interval</label>
<default>15</default>
<description>Interval measurement polling in minutes.</description>
</parameter>
</config-description>
</thing-type>
<channel-type id="timestamp">
<item-type>DateTime</item-type>
<label>Last Measurement</label>
<state pattern="%1$tA, %1$td.%1$tm. %1$tH:%1$tM" readOnly="true"/>
</channel-type>
<channel-type id="level">
<item-type>Number:Length</item-type>
<label>Water Level</label>
<state pattern="%d %unit%" readOnly="true"/>
</channel-type>
<channel-type id="trend">
<item-type>Number</item-type>
<label>Water Level Trend</label>
<state readOnly="true">
<options>
<option value="-1">Lowering</option>
<option value="0">Steady</option>
<option value="1">Rising</option>
</options>
</state>
</channel-type>
<channel-type id="warning">
<item-type>Number</item-type>
<label>Warning Level</label>
<state readOnly="true">
<options>
<option value="0">No warning</option>
<option value="1">Warning Level 1</option>
<option value="2">Warning Level 2</option>
<option value="3">Warning Level 3</option>
<option value="4">Decade Flooding</option>
<option value="5">Century Flooding</option>
<option value="6">Extreme Flooding</option>
</options>
</state>
</channel-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,156 @@
/**
* Copyright (c) 2010-2024 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.pegelonline.internal.handler;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.config.core.ConfigDescription;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelGroupUID;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatusInfo;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.ThingHandlerCallback;
import org.openhab.core.thing.binding.builder.ChannelBuilder;
import org.openhab.core.thing.type.ChannelGroupTypeUID;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.openhab.core.types.TimeSeries;
import org.openhab.core.types.TimeSeries.Policy;
import org.openhab.core.types.UnDefType;
/**
* The {@link CallbackMock} is a helper for unit tests to receive callbacks
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class CallbackMock implements ThingHandlerCallback {
private Map<String, State> stateMap = new HashMap<>();
private @Nullable ThingStatusInfo thingStatus;
public @Nullable ThingStatusInfo getThingStatus() {
synchronized (this) {
while (thingStatus == null) {
try {
wait();
} catch (InterruptedException e) {
}
}
}
return thingStatus;
}
@Override
public void stateUpdated(ChannelUID channelUID, State state) {
stateMap.put(channelUID.getAsString(), state);
}
public State getState(String channelUID) {
State val = stateMap.get(channelUID);
if (val == null) {
return UnDefType.UNDEF;
} else {
return val;
}
}
@Override
public void postCommand(ChannelUID channelUID, Command command) {
}
@Override
public void sendTimeSeries(ChannelUID channelUID, TimeSeries timeSeries) {
}
public TimeSeries getTimeSeries(String cuid) {
return new TimeSeries(Policy.REPLACE);
}
@Override
public void statusUpdated(Thing thing, ThingStatusInfo thingStatus) {
synchronized (this) {
this.thingStatus = thingStatus;
notifyAll();
}
}
@Override
public void thingUpdated(Thing thing) {
}
@Override
public void validateConfigurationParameters(Thing thing, Map<String, Object> configurationParameters) {
}
@Override
public void validateConfigurationParameters(Channel channel, Map<String, Object> configurationParameters) {
}
@Override
public @Nullable ConfigDescription getConfigDescription(ChannelTypeUID channelTypeUID) {
return null;
}
@Override
public @Nullable ConfigDescription getConfigDescription(ThingTypeUID thingTypeUID) {
return null;
}
@Override
public void configurationUpdated(Thing thing) {
}
@Override
public void migrateThingType(Thing thing, ThingTypeUID thingTypeUID, Configuration configuration) {
}
@Override
public void channelTriggered(Thing thing, ChannelUID channelUID, String event) {
}
@Override
public ChannelBuilder createChannelBuilder(ChannelUID channelUID, ChannelTypeUID channelTypeUID) {
return ChannelBuilder.create(channelUID);
}
@Override
public ChannelBuilder editChannel(Thing thing, ChannelUID channelUID) {
return ChannelBuilder.create(channelUID);
}
@Override
public List<ChannelBuilder> createChannelBuilders(ChannelGroupUID channelGroupUID,
ChannelGroupTypeUID channelGroupTypeUID) {
return List.of();
}
@Override
public boolean isChannelLinked(ChannelUID channelUID) {
return false;
}
@Override
public @Nullable Bridge getBridge(ThingUID bridgeUID) {
return null;
}
}

View File

@ -0,0 +1,375 @@
/**
* Copyright (c) 2010-2024 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.pegelonline.internal.handler;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import static org.openhab.binding.pegelonline.internal.PegelOnlineBindingConstants.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.junit.jupiter.api.Test;
import org.openhab.binding.pegelonline.internal.config.PegelOnlineConfiguration;
import org.openhab.binding.pegelonline.internal.dto.Measure;
import org.openhab.binding.pegelonline.internal.dto.Station;
import org.openhab.binding.pegelonline.internal.util.FileReader;
import org.openhab.binding.pegelonline.internal.utils.Utils;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingStatusInfo;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.internal.ThingImpl;
import org.openhab.core.types.State;
/**
* The {@link PegelTest} Test helper utils
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
class PegelTest {
public static final String TEST_STATION_UUID = "1ebd0f94-cc06-445c-8e73-43fe2b8c72dc";
@Test
void testConfigurationValidations() {
PegelOnlineConfiguration config = new PegelOnlineConfiguration();
assertFalse(config.uuidCheck(), config.uuid);
config.uuid = "abc@";
assertFalse(config.uuidCheck(), config.uuid);
config.uuid = "abc d";
assertFalse(config.uuidCheck(), config.uuid);
config.uuid = "1234567a-abc1-efd9-cdf3-0123456789ab";
assertTrue(config.uuidCheck(), config.uuid);
assertTrue(config.warningCheck(), "Warnings");
String content = FileReader.readFileInString("src/test/resources/stations.json");
Station[] stationArray = GSON.fromJson(content, Station[].class);
assertNotNull(stationArray);
for (Station station : stationArray) {
config.uuid = station.uuid;
assertTrue(config.uuidCheck(), config.uuid);
}
}
@Test
void testNameConversion() {
String stationName = "EIDER-SPERRWERK BP";
String conversion = Utils.toTitleCase(stationName);
assertEquals("Eider-Sperrwerk Bp", conversion, "Station Name");
String content = FileReader.readFileInString("src/test/resources/stations.json");
Station[] stationArray = GSON.fromJson(content, Station[].class);
assertNotNull(stationArray);
for (Station station : stationArray) {
assertTrue(Character.isUpperCase(Utils.toTitleCase(station.shortname).charAt(0)),
"First Character Upper Case");
assertTrue(Character.isUpperCase(Utils.toTitleCase(station.water.shortname).charAt(0)),
"First Character Upper Case");
}
}
@Test
void testDistance() {
// Frankfurt Main: 50.117461111005, 8.639069127891485
String content = FileReader.readFileInString("src/test/resources/stations.json");
Station[] stationArray = GSON.fromJson(content, Station[].class);
assertNotNull(stationArray);
int hitCounter = 0;
for (Station station : stationArray) {
double distance = Utils.calculateDistance(50.117461111005, 8.639069127891485, station.latitude,
station.longitude);
if (distance < 50) {
hitCounter++;
assertTrue(station.water.shortname.equals("RHEIN") || station.water.shortname.equals("MAIN"),
"RHEIN or MAIN");
}
}
assertEquals(11, hitCounter, "Meassurement Stations around FRA");
}
@Test
void testMeasureObject() {
String content = FileReader.readFileInString("src/test/resources/measure.json");
Measure measure = GSON.fromJson(content, Measure.class);
if (measure != null) {
assertEquals("2021-08-01T16:00:00+02:00", measure.timestamp, "Timestamp");
assertEquals(238, measure.value, "Level");
assertEquals(-1, measure.trend, "Trend");
} else {
fail();
}
}
@Test
void test404Status() {
String stationContent = FileReader.readFileInString("src/test/resources/stations.json");
ContentResponse stationResponse = mock(ContentResponse.class);
when(stationResponse.getStatus()).thenReturn(200);
when(stationResponse.getContentAsString()).thenReturn(stationContent);
String content = "{}";
ContentResponse measureResponse = mock(ContentResponse.class);
when(measureResponse.getStatus()).thenReturn(404);
when(measureResponse.getContentAsString()).thenReturn(content);
HttpClient httpClientMock = mock(HttpClient.class);
try {
when(httpClientMock.GET(STATIONS_URI + "/" + TEST_STATION_UUID + "/W/currentmeasurement.json"))
.thenReturn(measureResponse);
when(httpClientMock.GET(STATIONS_URI)).thenReturn(stationResponse);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
fail();
}
CallbackMock callback = new CallbackMock();
ThingImpl ti = new ThingImpl(new ThingTypeUID("pegelonline:station"), "test");
PegelOnlineHandler handler = new PegelOnlineHandler(ti, httpClientMock);
Configuration config = new Configuration();
config.put("uuid", TEST_STATION_UUID);
handler.setCallback(callback);
handler.updateConfiguration(config);
handler.initialize();
handler.performMeasurement();
ThingStatusInfo tsi = callback.getThingStatus();
assertNotNull(tsi);
assertEquals(ThingStatus.OFFLINE, tsi.getStatus(), "Status");
assertEquals(ThingStatusDetail.CONFIGURATION_ERROR, tsi.getStatusDetail(), "Detail");
String description = tsi.getDescription();
assertNotNull(description);
assertEquals("@text/pegelonline.handler.status.uuid-not-found [\"" + TEST_STATION_UUID + "\"]", description,
"Description");
}
@Test
void testWrongContent() {
String stationContent = FileReader.readFileInString("src/test/resources/stations.json");
ContentResponse stationResponse = mock(ContentResponse.class);
when(stationResponse.getStatus()).thenReturn(200);
when(stationResponse.getContentAsString()).thenReturn(stationContent);
String content = "{}";
ContentResponse measureResponse = mock(ContentResponse.class);
when(measureResponse.getStatus()).thenReturn(200);
when(measureResponse.getContentAsString()).thenReturn(content);
HttpClient httpClientMock = mock(HttpClient.class);
try {
when(httpClientMock.GET(STATIONS_URI + "/" + TEST_STATION_UUID + "/W/currentmeasurement.json"))
.thenReturn(measureResponse);
when(httpClientMock.GET(STATIONS_URI)).thenReturn(stationResponse);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
fail();
}
CallbackMock callback = new CallbackMock();
ThingImpl ti = new ThingImpl(new ThingTypeUID("pegelonline:station"), "test");
PegelOnlineHandler handler = new PegelOnlineHandler(ti, httpClientMock);
Configuration config = new Configuration();
config.put("uuid", TEST_STATION_UUID);
handler.setCallback(callback);
handler.updateConfiguration(config);
handler.initialize();
handler.performMeasurement();
ThingStatusInfo tsi = callback.getThingStatus();
assertNotNull(tsi);
assertEquals(ThingStatus.OFFLINE, tsi.getStatus(), "Status");
assertEquals(ThingStatusDetail.COMMUNICATION_ERROR, tsi.getStatusDetail(), "Detail");
String description = tsi.getDescription();
assertNotNull(description);
assertEquals("@text/pegelonline.handler.status.json-error [\"{}\"]", description, "Description");
}
@Test
public void testWrongConfiguration() {
CallbackMock callback = new CallbackMock();
PegelOnlineHandler handler = getConfiguredHandler(callback, 99);
Configuration config = new Configuration();
config.put("uuid", " ");
handler.updateConfiguration(new Configuration(config));
handler.initialize();
ThingStatusInfo tsi = callback.getThingStatus();
assertNotNull(tsi);
assertEquals(ThingStatus.OFFLINE, tsi.getStatus(), "Status");
assertEquals(ThingStatusDetail.CONFIGURATION_ERROR, tsi.getStatusDetail(), "Detail");
String description = tsi.getDescription();
assertNotNull(description);
assertEquals("@text/pegelonline.handler.status.uuid [\" \"]", description, "Description");
}
@Test
public void testInconsistentLevels() {
CallbackMock callback = new CallbackMock();
PegelOnlineHandler handler = getConfiguredHandler(callback, 99);
Configuration config = new Configuration();
config.put("uuid", TEST_STATION_UUID);
config.put("warningLevel1", 100);
config.put("warningLevel2", 200);
config.put("warningLevel3", 150);
handler.updateConfiguration(config);
handler.initialize();
ThingStatusInfo tsi = callback.getThingStatus();
assertNotNull(tsi);
assertEquals(ThingStatus.OFFLINE, tsi.getStatus(), "Status");
assertEquals(ThingStatusDetail.CONFIGURATION_ERROR, tsi.getStatusDetail(), "Detail");
String description = tsi.getDescription();
assertNotNull(description);
assertEquals("@text/pegelonline.handler.status.warning", description, "Description");
handler.dispose();
config = new Configuration();
config.put("uuid", TEST_STATION_UUID);
config.put("warningLevel1", 100);
config.put("warningLevel2", 200);
config.put("warningLevel3", 300);
config.put("hqExtreme", 600);
handler.updateConfiguration(new Configuration(config));
handler.initialize();
tsi = callback.getThingStatus();
assertNotNull(tsi);
assertEquals(ThingStatus.UNKNOWN, tsi.getStatus(), "Status");
assertEquals(ThingStatusDetail.NONE, tsi.getStatusDetail(), "Detail");
description = tsi.getDescription();
assertNotNull(description);
assertEquals("@text/pegelonline.handler.status.wait-feedback", description, "Description");
handler.dispose();
config = new Configuration();
config.put("uuid", TEST_STATION_UUID);
config.put("warningLevel1", 100);
config.put("warningLevel2", 200);
config.put("warningLevel3", 300);
config.put("hq10", 100);
config.put("hq100", 200);
config.put("hqExtreme", 150);
handler.updateConfiguration(new Configuration(config));
handler.initialize();
tsi = callback.getThingStatus();
assertNotNull(tsi);
assertEquals(ThingStatus.OFFLINE, tsi.getStatus(), "Status");
assertEquals(ThingStatusDetail.CONFIGURATION_ERROR, tsi.getStatusDetail(), "Detail");
description = tsi.getDescription();
assertNotNull(description);
assertEquals("@text/pegelonline.handler.status.warning", description, "Description");
}
@Test
public void testWrongResponse() {
String measureContent = "{}";
ContentResponse measureResponse = mock(ContentResponse.class);
when(measureResponse.getStatus()).thenReturn(500);
when(measureResponse.getContentAsString()).thenReturn(measureContent);
HttpClient httpClientMock = mock(HttpClient.class);
try {
when(httpClientMock.GET(STATIONS_URI + "/" + TEST_STATION_UUID + "/W/currentmeasurement.json"))
.thenReturn(measureResponse);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
fail();
}
CallbackMock callback = new CallbackMock();
ThingImpl ti = new ThingImpl(new ThingTypeUID("pegelonline:station"), "test");
PegelOnlineHandler handler = new PegelOnlineHandler(ti, httpClientMock);
Configuration config = new Configuration();
config.put("uuid", TEST_STATION_UUID);
handler.setCallback(callback);
handler.updateConfiguration(config);
handler.initialize();
handler.performMeasurement();
ThingStatusInfo tsi = callback.getThingStatus();
assertNotNull(tsi);
assertEquals(ThingStatus.OFFLINE, tsi.getStatus(), "Status");
assertEquals(ThingStatusDetail.COMMUNICATION_ERROR, tsi.getStatusDetail(), "Detail");
String description = tsi.getDescription();
assertNotNull(description);
assertEquals("@text/pegelonline.handler.status.http-status [\"500\"]", description, "Description");
}
@Test
public void testWarnings() {
CallbackMock callback = new CallbackMock();
PegelOnlineHandler handler = getConfiguredHandler(callback, 99);
handler.initialize();
handler.performMeasurement();
State state = callback.getState("pegelonline:station:test:warning");
assertTrue(state instanceof DecimalType);
assertEquals(NO_WARNING, ((DecimalType) state).intValue(), "No warning");
handler = getConfiguredHandler(callback, 100);
handler.initialize();
handler.performMeasurement();
state = callback.getState("pegelonline:station:test:warning");
assertTrue(state instanceof DecimalType);
assertEquals(WARN_LEVEL_1, ((DecimalType) state).intValue(), "Warn Level 1");
handler = getConfiguredHandler(callback, 299);
handler.initialize();
handler.performMeasurement();
state = callback.getState("pegelonline:station:test:warning");
assertTrue(state instanceof DecimalType);
assertEquals(WARN_LEVEL_2, ((DecimalType) state).intValue(), "Warn Level 2");
handler = getConfiguredHandler(callback, 1000);
handler.initialize();
handler.performMeasurement();
state = callback.getState("pegelonline:station:test:warning");
assertTrue(state instanceof DecimalType);
assertEquals(HQ_EXTREME, ((DecimalType) state).intValue(), "HQ extreme");
}
private PegelOnlineHandler getConfiguredHandler(CallbackMock callback, int levelSimulation) {
String stationContent = FileReader.readFileInString("src/test/resources/stations.json");
ContentResponse stationResponse = mock(ContentResponse.class);
when(stationResponse.getStatus()).thenReturn(200);
when(stationResponse.getContentAsString()).thenReturn(stationContent);
String measureContent = "{ \"timestamp\": \"2021-08-01T16:00:00+02:00\", \"value\": " + levelSimulation
+ ", \"trend\": -1}";
ContentResponse measureResponse = mock(ContentResponse.class);
when(measureResponse.getStatus()).thenReturn(200);
when(measureResponse.getContentAsString()).thenReturn(measureContent);
HttpClient httpClientMock = mock(HttpClient.class);
try {
when(httpClientMock.GET(STATIONS_URI + "/" + TEST_STATION_UUID + "/W/currentmeasurement.json"))
.thenReturn(measureResponse);
when(httpClientMock.GET(STATIONS_URI)).thenReturn(stationResponse);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
fail();
}
ThingImpl ti = new ThingImpl(new ThingTypeUID("pegelonline:station"), "test");
PegelOnlineHandler handler = new PegelOnlineHandler(ti, httpClientMock);
Configuration config = new Configuration();
config.put("uuid", TEST_STATION_UUID);
config.put("warningLevel1", 100);
config.put("warningLevel2", 200);
config.put("warningLevel3", 300);
config.put("hq10", 400);
config.put("hq100", 500);
config.put("hqExtreme", 600);
handler.setCallback(callback);
handler.updateConfiguration(config);
return handler;
}
}

View File

@ -0,0 +1,48 @@
/**
* Copyright (c) 2010-2024 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.pegelonline.internal.util;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.pegelonline.internal.PegelOnlineBindingConstants;
/**
* The {@link FileReader} Helper Util to read test resource files
*
* @author Bernd Weymann - Initial contribution
*/
@NonNullByDefault
public class FileReader {
public static String readFileInString(String filename) {
try (BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(filename), "UTF-8"));) {
StringBuilder buf = new StringBuilder();
String sCurrentLine;
while ((sCurrentLine = br.readLine()) != null) {
buf.append(sCurrentLine);
}
return buf.toString();
} catch (IOException e) {
// fail if file cannot be read
assertTrue(false, e.getMessage());
}
return PegelOnlineBindingConstants.UNKNOWN;
}
}

View File

@ -0,0 +1,7 @@
{
"timestamp": "2021-08-01T16:00:00+02:00",
"value": 238.0,
"trend": -1,
"stateMnwMhw": "normal",
"stateNswHsw": "unknown"
}

View File

@ -0,0 +1,100 @@
[
{
"uuid": "4e7a6cfa-7548-4f7f-a97a-eb0694881003",
"number": "25830056",
"shortname": "Marburg",
"longname": "MARBURG",
"km": -38.7,
"agency": "REGIERUNGSPRÄSIDIUM GIESSEN ABTEILUNG STAATLICHES UMWELTAMT MARBURG",
"longitude": 8.764488839485487,
"latitude": 50.798715477809225,
"water": {
"shortname": "LAHN",
"longname": "LAHN"
}
},
{
"uuid": "4b386a6a-996e-4a4a-a440-15d6b40226d4",
"number": "25800100",
"shortname": "GIESSEN KLÄRWERK",
"longname": "GIESSEN KLÄRWERK",
"km": -3.21,
"agency": "WSA MOSEL-SAAR-LAHN",
"longitude": 8.64860169166119,
"latitude": 50.575037651225514,
"water": {
"shortname": "LAHN",
"longname": "LAHN"
}
},
{
"uuid": "32807065-b887-49f0-935a-80033e5f3cb0",
"number": "25800200",
"shortname": "LEUN NEU",
"longname": "LEUN NEU",
"km": 25.1,
"agency": "WSA MOSEL-SAAR-LAHN",
"longitude": 8.355230130810975,
"latitude": 50.545120232764674,
"water": {
"shortname": "LAHN",
"longname": "LAHN"
}
},
{
"uuid": "89038b42-8181-48df-a0cd-2ca3913f2d68",
"number": "25800440",
"shortname": "LIMBURG SCHLEUSE UP",
"longname": "LIMBURG SCHLEUSE UP",
"km": 76.611,
"agency": "WSA MOSEL-SAAR-LAHN",
"longitude": 8.065188851061134,
"latitude": 50.39151276997554,
"water": {
"shortname": "LAHN",
"longname": "LAHN"
}
},
{
"uuid": "eadedeb6-c31e-483f-b6c4-ca0153359ad7",
"number": "25800500",
"shortname": "DIEZ HAFEN",
"longname": "DIEZ HAFEN",
"km": 83.7,
"agency": "WSA MOSEL-SAAR-LAHN",
"longitude": 8.005066992072132,
"latitude": 50.3723880903084,
"water": {
"shortname": "LAHN",
"longname": "LAHN"
}
},
{
"uuid": "64f735fd-88b6-42ea-9cdd-dc18d3806c34",
"number": "25800600",
"shortname": "KALKOFEN NEU",
"longname": "KALKOFEN NEU",
"km": 106.4,
"agency": "WSA MOSEL-SAAR-LAHN",
"longitude": 7.8898156192725235,
"latitude": 50.31783177830708,
"water": {
"shortname": "LAHN",
"longname": "LAHN"
}
},
{
"uuid": "6b6b31e2-e5c7-4c85-8405-b8d0b6e158c4",
"number": "25800800",
"shortname": "LAHNSTEIN SCHLEUSE UP",
"longname": "LAHNSTEIN SCHLEUSE UP",
"km": 135.986,
"agency": "WSA MOSEL-SAAR-LAHN",
"longitude": 7.612956624441373,
"latitude": 50.30803174924558,
"water": {
"shortname": "LAHN",
"longname": "LAHN"
}
}
]

File diff suppressed because it is too large Load Diff

View File

@ -312,6 +312,7 @@
<module>org.openhab.binding.orvibo</module> <module>org.openhab.binding.orvibo</module>
<module>org.openhab.binding.panasonicbdp</module> <module>org.openhab.binding.panasonicbdp</module>
<module>org.openhab.binding.paradoxalarm</module> <module>org.openhab.binding.paradoxalarm</module>
<module>org.openhab.binding.pegelonline</module>
<module>org.openhab.binding.pentair</module> <module>org.openhab.binding.pentair</module>
<module>org.openhab.binding.phc</module> <module>org.openhab.binding.phc</module>
<module>org.openhab.binding.pilight</module> <module>org.openhab.binding.pilight</module>