added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/test-classes" path="src/test/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.binding.dwdunwetter</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

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,116 @@
# DwdUnwetter Binding
Binding to retrieve the Weather Warnings of the "Deutscher Wetterdienstes" from the [DWD Geoserver](https://maps.dwd.de/geoserver/web/).
The DWD provides weather warnings for Germany.
Regions outside of Germany are not supported.
## Supported Things
This binding supports one thing, the Weather Warning.
Each Thing provides one or more warnings for a city.
## Thing Configuration
| Property | Default | Required | Description |
|--------------|---------|----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| cellId | - | Yes | ID of the area to retrieve weather warnings. See [this list](https://www.dwd.de/DE/leistungen/opendata/help/warnungen/cap_warncellids_csv.csv) of valid IDs. Using the percent sign (%) as a wildcard, it is possible to query multiple cells. For example, the value `8111%` retrieves all cell IDs that start with `8111`. |
| refresh | 30 | No | Time between API requests in minutes. Minimum 15 minutes. |
| warningCount | 1 | No | Number of warnings to provide. |
The binding will deliver no warnings if the number of retrieved warnings for one Thing is too big.
This can only happen if you select too many cell IDs (more than about 300) with the percent wildcard.
Example:
```
dwdunwetter:dwdwarnings:cologne "Warnings Cologne" [ cellId="105315000", refresh=15, warningCount=1 ]
```
## Channels
The are multiple channels for every weather warning.
The channels are numbered, with channel names ending in 1 for the first warning, 2 for the second warning, and so on.
The warnings will be sorted first by `Severity` and then by the `Valid From` date.
This ensures that the channels for the first warning will always be the highest Severity.
If the API returns more warnings than the configured number in the Thing, the warnings with the lowest Severity will be dropped.
| Channel | Type | Description |
|--------------|-----------------|----------------------------------------------------------------------------------|
| warningN | Switch | ON if a warning is present |
| UpdatedN | Trigger Channel | Triggers NEW when a warning is sent the first time |
| severityN | String | Severity of the warning. Possible values are Minor, Moderate, Severe, and Extreme|
| headlineN | String | Headline of the warning (e.g. "Amtliche Warnung vor FROST") |
| descriptionN | String | Textual description of the warning |
| eventN | String | Type of the warning (e.g. FROST) |
| effectiveN | DateTime | Issued Date and Time |
| onsetN | DateTime | Start Date and Time for which the warning is valid |
| expiresN | DateTime | End Date and Time for which the warning is valid |
| altitudeN | Number:Length | Lower Height above sea level for which the warning is valid |
| ceilingN | Number:Length | Upper Height above sea level for which the warning is valid |
| urgencyN | String | Urgency of the warning. Possible values are Future and Immediate |
| instructionN | String | Additional instructions and safety information |
All channels are readonly.
The main purpose of the channel `warningN` is to be used for controlling visibility in sitemaps.
The channel can also be used in rules to check if there is a warning present.
It should not be used for rules that need to fire if a new warning shows up.
If a warning is replaced by another warning, that channel stays at ON, and there will be no state change.
For rules that need to fire if a new warning occurs, there is the trigger channel `updatedN`.
That trigger channel fires an event whenever a warning is sent for the first time.
It also triggers if a warning is replaced by another.
More explanations about the specific values of the channels can be found in the documentation of the DWD at: [CAP DWD Profile 1.2](https://www.dwd.de/DE/leistungen/opendata/help/warnungen/cap_dwd_profile_de_pdf.pdf?__blob=publicationFile&v=7)
## Full Example
demo.things:
```
dwdunwetter:dwdwarnings:cologne "Warnings Cologne" [ cellId="105315000", refresh=15, warningCount=1 ]
```
demo.items:
```
Switch WarningCologne "Weather warning" { channel="dwdunwetter:dwdwarnings:cologne:warning1" }
String WarningCologneServerity "Severity[%s]" { channel="dwdunwetter:dwdwarnings:cologne:severity1" }
String WarningCologneBeschreibung "[%s]" { channel="dwdunwetter:dwdwarnings:cologne:description1" }
String WarningCologneAusgabedatum "Issued at [%s]" { channel="dwdunwetter:dwdwarnings:cologne:effective1" }
String WarningCologneGueltigAb "Valid from [%s]" { channel="dwdunwetter:dwdwarnings:cologne:onset1" }
String WarningCologneGueltigBis "Valid to [%s]" { channel="dwdunwetter:dwdwarnings:cologne:expires1" }
String WarningCologneTyp "Type [%s]" { channel="dwdunwetter:dwdwarnings:cologne:event1" }
String WarningCologneTitel "[%s]" { channel="dwdunwetter:dwdwarnings:cologne:headline1" }
String WarningCologneHoeheAb "Height from [%d m]" { channel="dwdunwetter:dwdwarnings:cologne:altitude1" }
String WarningCologneHoeheBis "Height to [%d m]" { channel="dwdunwetter:dwdwarnings:cologne:ceiling1" }
String WarningCologneUrgency "[%s]" { channel="dwdunwetter:dwdwarnings:cologne:urgency1" }
String WarningCologneInstruction "Additional information: [%s]" { channel="dwdunwetter:dwdwarnings:cologne:instruction1" }
```
demo.sitemap:
```
sitemap demo label="Main Menu"
{
Frame {
Text item=WarningCologneTitel visibility=[WarningCologne==ON]
Text item=WarningCologneBeschreibung visibility=[WarningCologne==ON]
}
}
```
demo.rules
```
rule "New Warnung"
when
Channel 'dwdunwetter:dwdwarnings:cologne:updated1' triggered NEW
then
// New Warning send a push notification to everyone
end
```

View File

@@ -0,0 +1,105 @@
# DwdUnwetter Binding
Binding zur Abfrage von aktuellen Unwetterwarnungen des Deutschen Wetterdienstes via [DWD Geoserver](https://maps.dwd.de/geoserver/web/)
## Unterstütztes Thing
Das Binding unterstützt genau ein Thing - Unwetterwarnungen.
Ein Thing stellt dabei eine oder mehrere Warnungen für eine Gemeinde bereit.
## Thing Konfiguration
| Property | Default | Required | Description |
|--------------|---------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| cellId | - | Yes | ID der abzufragenden Zelle. Siehe [cap_warncellids_csv.csv](https://www.dwd.de/DE/leistungen/opendata/help/warnungen/cap_warncellids_csv.csv "cap_warncellids_csv.csv") Es kann auch mittels % eine Gesamtmenge abgefragt werden, z.B. 8111% alle Gemeinden die mit 8111 anfangen |
| refresh | 30 | No | Abfrageintervall in Minuten. Minimum 15 Minuten. |
| warningCount | 1 | No | Anzahl der Warnungen, die als Channels bereitgestellt werden sollen |
Wählt man die Cell-ID mittels des %-Operators zu groß, so kann es passieren, das gar keine Warnungen kommen.
Das ist immer Fall, wenn die zurückgelieferte XML-Datei des DWD zu groß ist, dass sie nicht intern gebuffered werden kann.
Dies ist bei ca. 300+ Warnungen der Fall.
## Channels
Für jede bereitgestellte Warnung werden mehrere Channels bereitgestellt.
Die Channels sind jeweils durchnummeriert, Channels die mit 1 enden sind für die erste Warnung, Channels die mit 2 enden für die zweite Warnung usw.
Die vom DWD gelieferten Warnungen werden dabei nach Severity (Warnstufe) sortiert und innerhalb der Warnstufe nach Beginndatum.
Dadurch ist sichergestellt, dass in den Channels für die erste Warnung (...1) immer die Warnung mit der höchsten Warnstufe steht.
Werden mehr Warnungen vom DWD geliefert, als an Channels konfiguriert ist, werden dadurch die Warnungen mit der niedrigsten Warnstufe verworfen.
| Channel | Type | Beschreibung |
|--------------|-----------------|--------------------------------------------------------------------------------------------------------|
| warningN | Switch | Schalter, der auf ON steht, wenn eine Warnung vorliegt, OFF sonst. |
| UpdatedN | Trigger Channel | Sendet das Event "NEW", wenn diese Warnung das erste mal gesendet wird. |
| severityN | String | Warnstufe, von niedrig nach hoch: Minor, Moderate, Severe, Extreme. |
| headlineN | String | Überschrift der Warnung, z.B. Amtliche WARNUNG vor STURMBÖEN |
| descriptionN | String | Klartext Beschreibung der Warnung. |
| eventN | String | Art der Warnung, z.B. STURMBÖEN |
| effectiveN | DateTime | Zeitpunkt, an dem die Warnung ausgegeben wurde. |
| onsetN | DateTime | Zeitpunkt, von dem an die Warnung gilt. |
| effectiveN | DateTime | Zeitpunkt, bis zu dem die Warnung gilt. |
| altitudeN | Number:Length | Höhe über dem Meerespiegel, ab dem die Warnung gilt. |
| ceilingN | Number:Length | Höhe über dem Meerespiegel, bis zu dem die Warnung gilt. |
| urgencyN | String | Zeitrahmen der Meldung, Mögliche Werte sind Future (Vorabinformation) und Immediate (Konkrete Warnung) |
| instructionN | String | Zusatztext zur Warnung (Instruktionen und Sicherheitshinweise) |
Sämtliche Channels sind ReadOnly!
Der Channel _warningN_ dient hauptsächlich dazu, um z.B. in Sitemaps dynamisch Warnungen ein- oder auszublenden, bzw. um in Regeln zu prüfen, ob überhaupt eine Warnung vorliegt.
Er ist nicht geeignet um auf das Erscheinen einer Warnung zu prüfen.
Denn wenn eine Warnung durch eine neue Warnung ersetzt wird, bleibt der Zustand ON, es gibt keinen Zustandswechsel.
Um auf das erscheinen einer Warnung zu prüfen, sollte der Trigger-Channel _updatedN_ genutzt werden.
Der feuert immer dann, wenn eine Warnung das erste mal gesendet wird.
Das heißt, der feuert auch dann, wenn eine Warnung durch eine neue Warnung ersetzt wird.
Weitere Erläuterungen der Bedeutungen finden sich in der Dokumentation des DWDs unter [CAP DWD Profile 1.2](https://www.dwd.de/DE/leistungen/opendata/help/warnungen/cap_dwd_profile_de_pdf.pdf?__blob=publicationFile&v=7)
## Vollständiges Beispiel
demo.things:
```
dwdunwetter:dwdwarnings:koeln "Warnungen Köln" [ cellId="105315000", refresh=15, warningCount=1 ]
```
demo.items:
```
Switch WarnungKoeln "Warnung vorhanden" { channel="dwdunwetter:dwdwarnings:koeln:warning1" }
String WarnungKoelnServerity "Warnstufe [%s]" { channel="dwdunwetter:dwdwarnings:koeln:severity1" }
String WarnungKoelnBeschreibung "[%s]" { channel="dwdunwetter:dwdwarnings:koeln:description1" }
String WarnungKoelnAusgabedatum "Ausgeben am [%s]" { channel="dwdunwetter:dwdwarnings:koeln:effective1" }
String WarnungKoelnGueltigAb "Warnung gültig ab [%s]" { channel="dwdunwetter:dwdwarnings:koeln:onset1" }
String WarnungKoelnGueltigBis "Warnung gültig bis [%s]" { channel="dwdunwetter:dwdwarnings:koeln:expires1" }
String WarnungKoelnTyp "Warnungstyp [%s]" { channel="dwdunwetter:dwdwarnings:koeln:event1" }
String WarnungKoelnTitel "[%s]" { channel="dwdunwetter:dwdwarnings:koeln:headline1" }
String WarnungKoelnHoeheAb "Höhe ab [%d m]" { channel="dwdunwetter:dwdwarnings:koeln:altitude1" }
String WarnungKoelnHoeheBis "Höhe bis [%d m]" { channel="dwdunwetter:dwdwarnings:koeln:ceiling1" }
String WarningCologneUrgency "[%s]" { channel="dwdunwetter:dwdwarnings:cologne:urgency1" }
String WarningCologneInstruction "Zusatzinformationen: [%s]" { channel="dwdunwetter:dwdwarnings:cologne:instruction1" }
```
demo.sitemap:
```
sitemap demo label="Main Menu"
{
Frame {
Text item=WarnungKoelnTitel visibility=[WarnungKoeln==ON]
Text item=WarnungKoelnBeschreibung visibility=[WarnungKoeln==ON]
}
}
```
demo.rules
```
rule "Neue Warnung"
when
Channel 'dwdunwetter:dwdwarnings:koeln:updated1' triggered NEW
then
// Neue Warnung, Informiere alle Bewohner
end
```

View File

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

View File

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

View File

@@ -0,0 +1,49 @@
/**
* 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.dwdunwetter.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link DwdUnwetterBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Martin Koehler - Initial contribution
*/
@NonNullByDefault
public class DwdUnwetterBindingConstants {
public static final String BINDING_ID = "dwdunwetter";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_WARNINGS = new ThingTypeUID(BINDING_ID, "dwdwarnings");
// Channels
public static final String CHANNEL_LAST_UPDATED = "lastUpdated";
// Channels per Warning
public static final String CHANNEL_WARNING = "warning";
public static final String CHANNEL_UPDATED = "updated";
public static final String CHANNEL_SEVERITY = "severity";
public static final String CHANNEL_DESCRIPTION = "description";
public static final String CHANNEL_EFFECTIVE = "effective";
public static final String CHANNEL_ONSET = "onset";
public static final String CHANNEL_EXPIRES = "expires";
public static final String CHANNEL_EVENT = "event";
public static final String CHANNEL_HEADLINE = "headline";
public static final String CHANNEL_ALTITUDE = "altitude";
public static final String CHANNEL_CEILING = "ceiling";
public static final String CHANNEL_INSTRUCTION = "instruction";
public static final String CHANNEL_URGENCY = "urgency";
}

View File

@@ -0,0 +1,28 @@
/**
* 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.dwdunwetter.internal.config;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link DwdUnwetterConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Martin Koehler - Initial contribution
*/
@NonNullByDefault
public class DwdUnwetterConfiguration {
public int refresh;
public int warningCount;
public String cellId = StringUtils.EMPTY;
}

View File

@@ -0,0 +1,63 @@
/**
* 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.dwdunwetter.internal.data;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.stream.Collectors;
/**
* Cache of Warnings to update the {@link DwdUnwetterBindingConstants#CHANNEL_UPDATED} if a new warning is sent to a
* channel.
*
* @author Martin Koehler - Initial contribution
*/
public class DwdWarningCache {
// Remove Entries 30 Minutes after they expired
private static final long WAIT_TIME_IN_MINUTES = 30;
private final Map<String, Instant> idExpiresMap;
public DwdWarningCache() {
idExpiresMap = new HashMap<>();
}
private boolean isExpired(Entry<String, Instant> entry) {
Instant expireTime = entry.getValue().plus(WAIT_TIME_IN_MINUTES, ChronoUnit.MINUTES);
return Instant.now().isAfter(expireTime);
}
/**
* Adds a Warning
*
* @param data The warning data
* @return <code>true</code> if it is a new warning, <code>false</code> if the warning is not new.
*/
public boolean addEntry(DwdWarningData data) {
return idExpiresMap.put(data.getId(), data.getExpires()) == null;
}
/**
* Removes the expired Entries
*/
public void deleteOldEntries() {
List<String> oldEntries = idExpiresMap.entrySet().stream().filter(this::isExpired).map(Entry::getKey)
.collect(Collectors.toList());
oldEntries.forEach(idExpiresMap::remove);
}
}

View File

@@ -0,0 +1,184 @@
/**
* 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.dwdunwetter.internal.data;
import java.math.BigDecimal;
import java.time.Instant;
import org.apache.commons.lang.StringUtils;
/**
* Data for one warning.
*
* @author Martin Koehler - Initial contribution
*/
public class DwdWarningData {
private String id;
private Severity severity;
private String description;
private Instant effective;
private Instant expires;
private Instant onset;
private String event;
private String status;
private String msgType;
private String headline;
private BigDecimal altitude;
private BigDecimal ceiling;
private String instruction;
private Urgency urgency;
public void setId(String id) {
this.id = id;
}
public String getId() {
return id;
}
public void setSeverity(Severity severity) {
this.severity = severity;
}
public Severity getSeverity() {
return severity == null ? Severity.UNKNOWN : severity;
}
public void setDescription(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
public void setEffective(Instant effective) {
this.effective = effective;
}
public Instant getEffective() {
return effective;
}
public void setExpires(Instant expires) {
this.expires = expires;
}
public Instant getExpires() {
return expires;
}
public void setEvent(String event) {
this.event = event;
}
public String getEvent() {
return event;
}
public void setStatus(String status) {
this.status = status;
}
public boolean isTest() {
return StringUtils.equalsIgnoreCase(status, "Test");
}
public void setMsgType(String msgType) {
this.msgType = msgType;
}
public boolean isCancel() {
return StringUtils.equalsIgnoreCase(msgType, "Cancel");
}
public void setHeadline(String headline) {
this.headline = headline;
}
public String getHeadline() {
return headline;
}
public Instant getOnset() {
return onset;
}
public void setOnset(Instant onset) {
this.onset = onset;
}
public void setAltitude(BigDecimal altitude) {
this.altitude = altitude;
}
public BigDecimal getAltitude() {
return altitude;
}
public void setCeiling(BigDecimal ceiling) {
this.ceiling = ceiling;
}
public BigDecimal getCeiling() {
return ceiling;
}
public void setInstruction(String instruction) {
this.instruction = instruction;
}
public String getInstruction() {
return instruction;
}
public void setUrgency(Urgency urgency) {
this.urgency = urgency;
}
public Urgency getUrgency() {
return urgency;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((id == null) ? 0 : id.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
DwdWarningData other = (DwdWarningData) obj;
if (id == null) {
if (other.id != null) {
return false;
}
} else if (!id.equals(other.id)) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,68 @@
/**
* 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.dwdunwetter.internal.data;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import org.apache.commons.lang.StringUtils;
import org.openhab.core.io.net.http.HttpUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Provides the access to the API Endpoint
*
* @author Martin Koehler - Initial contribution
*/
public class DwdWarningDataAccess {
private final Logger logger = LoggerFactory.getLogger(DwdWarningDataAccess.class);
// URL of the Service
private static final String DWD_URL = "https://maps.dwd.de/geoserver/dwd/ows?service=WFS&version=2.0.0&request=GetFeature&typeName=dwd:Warnungen_Gemeinden";
/**
* Returns the raw Data from the Endpoint.
* In case of errors or empty cellId value, returns an {@link StringUtils#EMPTY Empty String}.
*
* @param cellId The warnCell-Id for which the warnings should be returned
* @return The raw data received or an empty string.
*/
public String getDataFromEndpoint(String cellId) {
try {
if (StringUtils.isBlank(cellId)) {
logger.warn("No cellId provided");
return StringUtils.EMPTY;
}
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(DWD_URL);
stringBuilder.append("&CQL_FILTER=");
stringBuilder
.append(URLEncoder.encode("WARNCELLID LIKE '" + cellId + "'", StandardCharsets.UTF_8.toString()));
logger.debug("Refreshing Data for cell {}", cellId);
String rawData = HttpUtil.executeUrl("GET", stringBuilder.toString(), 5000);
logger.trace("Raw request: {}", stringBuilder);
logger.trace("Raw response: {}", rawData);
return rawData;
} catch (IOException e) {
logger.warn("Communication error occurred while getting data: {}", e.getMessage());
logger.debug("Communication error trace", e);
}
return StringUtils.EMPTY;
}
}

View File

@@ -0,0 +1,312 @@
/**
* 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.dwdunwetter.internal.data;
import java.io.StringReader;
import java.math.BigDecimal;
import java.time.Duration;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.DateTimeParseException;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.stream.events.XMLEvent;
import org.apache.commons.lang.StringUtils;
import org.openhab.core.cache.ExpiringCache;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.unit.ImperialUnits;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Contains the Data for all retrieved warnings for one thing.
*
* @author Martin Koehler - Initial contribution
*/
public class DwdWarningsData {
private static final int MIN_REFRESH_WAIT_MINUTES = 5;
private final Logger logger = LoggerFactory.getLogger(DwdWarningsData.class);
private List<DwdWarningData> cityData = new LinkedList<>();
private DwdWarningCache cache = new DwdWarningCache();
private ExpiringCache<String> dataAccessCached;
private DateTimeFormatter formatter = new DateTimeFormatterBuilder()
// date/time
.append(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
// offset (hh:mm - "+00:00" when it's zero)
.optionalStart().appendOffset("+HH:MM", "+00:00").optionalEnd()
// offset (hhmm - "+0000" when it's zero)
.optionalStart().appendOffset("+HHMM", "+0000").optionalEnd()
// offset (hh - "Z" when it's zero)
.optionalStart().appendOffset("+HH", "Z").optionalEnd()
// create formatter
.toFormatter();
public DwdWarningsData(String cellId) {
DwdWarningDataAccess dataAccess = new DwdWarningDataAccess();
this.dataAccessCached = new ExpiringCache<>(Duration.ofMinutes(MIN_REFRESH_WAIT_MINUTES),
() -> dataAccess.getDataFromEndpoint(cellId));
}
private String getValue(XMLEventReader eventReader) throws XMLStreamException {
XMLEvent event = eventReader.nextEvent();
return event.asCharacters().getData();
}
private BigDecimal getBigDecimalValue(XMLEventReader eventReader) throws XMLStreamException {
XMLEvent event = eventReader.nextEvent();
try {
return new BigDecimal(event.asCharacters().getData());
} catch (NumberFormatException e) {
logger.debug("Exception while parsing a BigDecimal", e);
return BigDecimal.ZERO;
}
}
private Instant getTimestampValue(XMLEventReader eventReader) throws XMLStreamException {
XMLEvent event = eventReader.nextEvent();
String dateTimeString = event.asCharacters().getData();
try {
OffsetDateTime dateTime = OffsetDateTime.parse(dateTimeString, formatter);
return dateTime.toInstant();
} catch (DateTimeParseException e) {
logger.debug("Exception while parsing a DateTime", e);
return Instant.MIN;
}
}
/**
* Refreshes the Warnings Data
*/
public boolean refresh() {
String rawData = dataAccessCached.getValue();
if (StringUtils.isEmpty(rawData)) {
logger.debug("No Data from Endpoint");
return false;
}
cityData.clear();
try {
XMLInputFactory inputFactory = XMLInputFactory.newInstance();
XMLStreamReader reader = inputFactory.createXMLStreamReader(new StringReader(rawData));
XMLEventReader eventReader = inputFactory.createXMLEventReader(reader);
DwdWarningData gemeindeData = new DwdWarningData();
boolean insideGemeinde = false;
while (eventReader.hasNext()) {
XMLEvent event = eventReader.nextEvent();
if (!insideGemeinde && event.isStartElement()) {
DwdXmlTag xmlTag = DwdXmlTag.getDwdXmlTag(event.asStartElement().getName().getLocalPart());
switch (xmlTag) {
case WARNUNGEN_GEMEINDEN:
gemeindeData = new DwdWarningData();
insideGemeinde = true;
break;
default:
break;
}
} else if (insideGemeinde && event.isStartElement()) {
DwdXmlTag xmlTag = DwdXmlTag.getDwdXmlTag(event.asStartElement().getName().getLocalPart());
switch (xmlTag) {
case SEVERITY:
gemeindeData.setSeverity(Severity.getSeverity(getValue(eventReader)));
break;
case DESCRIPTION:
gemeindeData.setDescription(getValue(eventReader));
break;
case EFFECTIVE:
gemeindeData.setEffective(getTimestampValue(eventReader));
break;
case EXPIRES:
gemeindeData.setExpires(getTimestampValue(eventReader));
break;
case EVENT:
gemeindeData.setEvent(getValue(eventReader));
break;
case STATUS:
gemeindeData.setStatus(getValue(eventReader));
break;
case MSGTYPE:
gemeindeData.setMsgType(getValue(eventReader));
break;
case HEADLINE:
gemeindeData.setHeadline(getValue(eventReader));
break;
case ONSET:
gemeindeData.setOnset(getTimestampValue(eventReader));
break;
case ALTITUDE:
gemeindeData.setAltitude(getBigDecimalValue(eventReader));
break;
case CEILING:
gemeindeData.setCeiling(getBigDecimalValue(eventReader));
break;
case IDENTIFIER:
gemeindeData.setId(getValue(eventReader));
break;
case INSTRUCTION:
gemeindeData.setInstruction(getValue(eventReader));
break;
case URGENCY:
gemeindeData.setUrgency(Urgency.getUrgency(getValue(eventReader)));
break;
default:
break;
}
} else if (insideGemeinde && event.isEndElement()) {
DwdXmlTag xmlTag = DwdXmlTag.getDwdXmlTag(event.asEndElement().getName().getLocalPart());
switch (xmlTag) {
case WARNUNGEN_GEMEINDEN:
if (!gemeindeData.isTest() && !gemeindeData.isCancel()) {
cityData.add(gemeindeData);
}
insideGemeinde = false;
break;
default:
break;
}
}
}
} catch (XMLStreamException e) {
logger.warn("Exception occurred while parsing the XML response: {}", e.getMessage());
logger.debug("Exception trace", e);
return false;
}
Collections.sort(cityData, new SeverityComparator());
return true;
}
private DwdWarningData getGemeindeData(int number) {
return cityData.size() <= number ? null : cityData.get(number);
}
public State getWarning(int number) {
DwdWarningData data = getGemeindeData(number);
return data == null ? OnOffType.OFF : OnOffType.ON;
}
public State getSeverity(int number) {
DwdWarningData data = getGemeindeData(number);
return data == null ? UnDefType.NULL : StringType.valueOf(data.getSeverity().getText());
}
public State getDescription(int number) {
DwdWarningData data = getGemeindeData(number);
return data == null ? UnDefType.NULL : StringType.valueOf(data.getDescription());
}
public State getEffective(int number) {
DwdWarningData data = getGemeindeData(number);
if (data == null) {
return UnDefType.NULL;
}
ZonedDateTime zoned = ZonedDateTime.ofInstant(data.getEffective(), ZoneId.systemDefault());
return new DateTimeType(zoned);
}
public State getExpires(int number) {
DwdWarningData data = getGemeindeData(number);
if (data == null) {
return UnDefType.NULL;
}
ZonedDateTime zoned = ZonedDateTime.ofInstant(data.getExpires(), ZoneId.systemDefault());
return new DateTimeType(zoned);
}
public State getOnset(int number) {
DwdWarningData data = getGemeindeData(number);
if (data == null) {
return UnDefType.NULL;
}
ZonedDateTime zoned = ZonedDateTime.ofInstant(data.getOnset(), ZoneId.systemDefault());
return new DateTimeType(zoned);
}
public State getEvent(int number) {
DwdWarningData data = getGemeindeData(number);
return data == null ? UnDefType.NULL : StringType.valueOf(data.getEvent());
}
public State getHeadline(int number) {
DwdWarningData data = getGemeindeData(number);
return data == null ? UnDefType.NULL : StringType.valueOf(data.getHeadline());
}
public State getAltitude(int number) {
DwdWarningData data = getGemeindeData(number);
if (data == null) {
return UnDefType.NULL;
}
return new QuantityType<>(data.getAltitude(), ImperialUnits.FOOT);
}
public State getCeiling(int number) {
DwdWarningData data = getGemeindeData(number);
if (data == null) {
return UnDefType.NULL;
}
return new QuantityType<>(data.getCeiling(), ImperialUnits.FOOT);
}
public State getInstruction(int number) {
DwdWarningData data = getGemeindeData(number);
return data == null ? UnDefType.NULL : StringType.valueOf(data.getInstruction());
}
public State getUrgency(int number) {
DwdWarningData data = getGemeindeData(number);
return data == null ? UnDefType.NULL : StringType.valueOf(data.getUrgency().getText());
}
public boolean isNew(int number) {
DwdWarningData data = getGemeindeData(number);
if (data == null) {
return false;
}
return cache.addEntry(data);
}
public void updateCache() {
cache.deleteOldEntries();
}
/**
* Only for Tests
*/
protected void setDataAccess(DwdWarningDataAccess dataAccess) {
dataAccessCached = new ExpiringCache<>(Duration.ofMinutes(MIN_REFRESH_WAIT_MINUTES),
() -> dataAccess.getDataFromEndpoint(""));
}
}

View File

@@ -0,0 +1,58 @@
/**
* 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.dwdunwetter.internal.data;
import java.util.Arrays;
import org.apache.commons.lang.StringUtils;
/**
* The XML Tags to extract the relevant parts from the API response.
* The names map directly to the XML Tags of the API Response.
*
* @author Martin Koehler - Initial contribution
*/
public enum DwdXmlTag {
UNKNOWN(""),
SEVERITY("SEVERITY"),
DESCRIPTION("DESCRIPTION"),
EFFECTIVE("EFFECTIVE"),
EXPIRES("EXPIRES"),
ONSET("ONSET"),
EVENT("EVENT"),
STATUS("STATUS"),
MSGTYPE("MSGTYPE"),
HEADLINE("HEADLINE"),
ALTITUDE("ALTITUDE"),
CEILING("CEILING"),
INSTRUCTION("INSTRUCTION"),
URGENCY("URGENCY"),
IDENTIFIER("IDENTIFIER"),
WARNUNGEN_GEMEINDEN("Warnungen_Gemeinden");
private String tag;
private DwdXmlTag(String tag) {
this.tag = tag;
}
String getTag() {
return tag;
}
public static DwdXmlTag getDwdXmlTag(String tag) {
return Arrays.asList(DwdXmlTag.values()).stream().filter(t -> StringUtils.equals(t.getTag(), tag)).findFirst()
.orElse(UNKNOWN);
}
}

View File

@@ -0,0 +1,52 @@
/**
* 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.dwdunwetter.internal.data;
import java.util.Arrays;
import org.apache.commons.lang.StringUtils;
/**
* Severity enum to make the severity comparable
*
* @author Martin Koehler - Initial contribution
*/
public enum Severity {
EXTREME(1, "Extreme"),
SEVERE(2, "Severe"),
MODERATE(3, "Moderate"),
MINOR(4, "Minor"),
UNKNOWN(5, "Unknown");
private int order;
private String text;
private Severity(int order, String text) {
this.order = order;
this.text = text;
}
public int getOrder() {
return order;
}
public String getText() {
return text;
}
public static Severity getSeverity(String input) {
return Arrays.asList(Severity.values()).stream()
.filter(sev -> StringUtils.equalsIgnoreCase(input, sev.getText())).findAny().orElse(UNKNOWN);
}
}

View File

@@ -0,0 +1,37 @@
/**
* 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.dwdunwetter.internal.data;
import java.util.Comparator;
import org.apache.commons.lang.ObjectUtils;
/**
* Comperator to sort a Warning first by Severity, second by the onSet date.
*
* @author Martin Koehler - Initial contribution
*/
public class SeverityComparator implements Comparator<DwdWarningData> {
@Override
public int compare(DwdWarningData o1, DwdWarningData o2) {
Comparator.comparingInt(d -> ((DwdWarningData) d).getSeverity().getOrder());
Comparator.comparing(DwdWarningData::getOnset);
int result = Integer.compare(o1.getSeverity().getOrder(), o2.getSeverity().getOrder());
if (result == 0) {
result = ObjectUtils.compare(o1.getOnset(), o2.getOnset());
}
return result;
}
}

View File

@@ -0,0 +1,44 @@
/**
* 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.dwdunwetter.internal.data;
import java.util.Arrays;
import org.apache.commons.lang.StringUtils;
/**
* Enum for the urgency of the warning.
*
* @author Martin Koehler - Initial contribution
*/
public enum Urgency {
IMMEDIATE("Immediate"),
FUTURE("Future"),
UNKNOWN("Unknown");
private final String text;
private Urgency(String text) {
this.text = text;
}
public String getText() {
return text;
}
public static Urgency getUrgency(String input) {
return Arrays.asList(Urgency.values()).stream()
.filter(urg -> StringUtils.equalsIgnoreCase(input, urg.getText())).findAny().orElse(UNKNOWN);
}
}

View File

@@ -0,0 +1,56 @@
/**
* 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.dwdunwetter.internal.factory;
import static org.openhab.binding.dwdunwetter.internal.DwdUnwetterBindingConstants.THING_TYPE_WARNINGS;
import java.util.Collections;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.dwdunwetter.internal.handler.DwdUnwetterHandler;
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 DwdUnwetterHandlerFactory} is responsible for creating things and thing handlers.
*
* @author Martin Koehler - Initial contribution
*/
@NonNullByDefault
@Component(configurationPid = "binding.dwdunwetter", service = ThingHandlerFactory.class)
public class DwdUnwetterHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_WARNINGS);
@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 (THING_TYPE_WARNINGS.equals(thingTypeUID)) {
return new DwdUnwetterHandler(thing);
}
return null;
}
}

View File

@@ -0,0 +1,237 @@
/**
* 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.dwdunwetter.internal.handler;
import static org.openhab.binding.dwdunwetter.internal.DwdUnwetterBindingConstants.*;
import java.util.ArrayList;
import java.util.List;
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.dwdunwetter.internal.config.DwdUnwetterConfiguration;
import org.openhab.binding.dwdunwetter.internal.data.DwdWarningsData;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.builder.ChannelBuilder;
import org.openhab.core.thing.type.ChannelKind;
import org.openhab.core.thing.type.ChannelTypeUID;
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 DwdUnwetterHandler} is responsible for handling commands, which are sent to one of the channels.
*
* @author Martin Koehler - Initial contribution
*/
@NonNullByDefault
public class DwdUnwetterHandler extends BaseThingHandler {
private final Logger logger = LoggerFactory.getLogger(DwdUnwetterHandler.class);
private @Nullable ScheduledFuture<?> refreshJob;
private int warningCount;
private @Nullable DwdWarningsData data;
private boolean inRefresh;
private boolean initializing;
public DwdUnwetterHandler(Thing thing) {
super(thing);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (command instanceof RefreshType) {
scheduler.submit(this::refresh);
}
}
/**
* Refreshes the Warning Data.
*
* The Switch Channel is switched to ON only after all other Channels are updated.
* The Switch Channel is switched to OFF before all other Channels are updated.
*/
private void refresh() {
if (inRefresh) {
logger.trace("Already refreshing. Ignoring refresh request.");
return;
}
if (initializing) {
logger.trace("Still initializing. Ignoring refresh request.");
return;
}
ThingStatus status = getThing().getStatus();
if (status != ThingStatus.ONLINE && status != ThingStatus.UNKNOWN) {
logger.debug("Unable to refresh. Thing status is {}", status);
return;
}
final DwdWarningsData warningsData = data;
if (warningsData == null) {
logger.debug("Unable to refresh. No data to use.");
return;
}
inRefresh = true;
boolean refreshSucceeded = warningsData.refresh();
if (!refreshSucceeded) {
logger.debug("Failed to retrieve new data from the server.");
inRefresh = false;
return;
}
if (status == ThingStatus.UNKNOWN) {
updateStatus(ThingStatus.ONLINE);
}
updateState(getChannelUuid(CHANNEL_LAST_UPDATED), new DateTimeType());
for (int i = 0; i < warningCount; i++) {
State warning = warningsData.getWarning(i);
if (warning == OnOffType.OFF) {
updateState(getChannelUuid(CHANNEL_WARNING, i), warning);
}
updateState(getChannelUuid(CHANNEL_SEVERITY, i), warningsData.getSeverity(i));
updateState(getChannelUuid(CHANNEL_DESCRIPTION, i), warningsData.getDescription(i));
updateState(getChannelUuid(CHANNEL_EFFECTIVE, i), warningsData.getEffective(i));
updateState(getChannelUuid(CHANNEL_EXPIRES, i), warningsData.getExpires(i));
updateState(getChannelUuid(CHANNEL_ONSET, i), warningsData.getOnset(i));
updateState(getChannelUuid(CHANNEL_EVENT, i), warningsData.getEvent(i));
updateState(getChannelUuid(CHANNEL_HEADLINE, i), warningsData.getHeadline(i));
updateState(getChannelUuid(CHANNEL_ALTITUDE, i), warningsData.getAltitude(i));
updateState(getChannelUuid(CHANNEL_CEILING, i), warningsData.getCeiling(i));
updateState(getChannelUuid(CHANNEL_INSTRUCTION, i), warningsData.getInstruction(i));
updateState(getChannelUuid(CHANNEL_URGENCY, i), warningsData.getUrgency(i));
if (warning == OnOffType.ON) {
updateState(getChannelUuid(CHANNEL_WARNING, i), warning);
}
if (warningsData.isNew(i)) {
triggerChannel(getChannelUuid(CHANNEL_UPDATED, i), "NEW");
}
}
warningsData.updateCache();
inRefresh = false;
}
@Override
public void initialize() {
logger.debug("Start initializing!");
initializing = true;
updateStatus(ThingStatus.UNKNOWN);
DwdUnwetterConfiguration config = getConfigAs(DwdUnwetterConfiguration.class);
warningCount = config.warningCount;
data = new DwdWarningsData(config.cellId);
updateThing(editThing().withChannels(createChannels()).build());
refreshJob = scheduler.scheduleWithFixedDelay(this::refresh, 0, config.refresh, TimeUnit.MINUTES);
initializing = false;
logger.debug("Finished initializing!");
}
private ChannelUID getChannelUuid(String typeId, int warningNumber) {
return new ChannelUID(getThing().getUID(), typeId + (warningNumber + 1));
}
private ChannelUID getChannelUuid(String typeId) {
return new ChannelUID(getThing().getUID(), typeId);
}
/**
* Creates a trigger Channel.
*/
private Channel createTriggerChannel(String typeId, String label, int warningNumber) {
ChannelUID channelUID = getChannelUuid(typeId, warningNumber);
return ChannelBuilder.create(channelUID, "String") //
.withType(new ChannelTypeUID(BINDING_ID, typeId)) //
.withLabel(label + " (" + (warningNumber + 1) + ")")//
.withKind(ChannelKind.TRIGGER) //
.build();
}
/**
* Creates a normal, state based, channel associated with a warning.
*/
private Channel createChannel(String typeId, String itemType, String label, int warningNumber) {
ChannelUID channelUID = getChannelUuid(typeId, warningNumber);
return ChannelBuilder.create(channelUID, itemType) //
.withType(new ChannelTypeUID(BINDING_ID, typeId)) //
.withLabel(label + " (" + (warningNumber + 1) + ")")//
.build();
}
/**
* Creates a normal, state based, channel not associated with a warning.
*/
private Channel createChannel(String typeId, String itemType, String label) {
ChannelUID channelUID = getChannelUuid(typeId);
return ChannelBuilder.create(channelUID, itemType) //
.withType(new ChannelTypeUID(BINDING_ID, typeId)) //
.withLabel(label)//
.build();
}
/**
* Creates the ChannelsT for each warning.
*
* @return The List of Channels
*/
private List<Channel> createChannels() {
List<Channel> channels = new ArrayList<>(warningCount * 11 + 1);
channels.add(createChannel(CHANNEL_LAST_UPDATED, "DateTime", "Last Updated"));
for (int i = 0; i < warningCount; i++) {
channels.add(createChannel(CHANNEL_WARNING, "Switch", "Warning", i));
channels.add(createTriggerChannel(CHANNEL_UPDATED, "Updated", i));
channels.add(createChannel(CHANNEL_SEVERITY, "String", "Severity", i));
channels.add(createChannel(CHANNEL_DESCRIPTION, "String", "Description", i));
channels.add(createChannel(CHANNEL_EFFECTIVE, "DateTime", "Issued", i));
channels.add(createChannel(CHANNEL_ONSET, "DateTime", "Valid From", i));
channels.add(createChannel(CHANNEL_EXPIRES, "DateTime", "Valid To", i));
channels.add(createChannel(CHANNEL_EVENT, "String", "Type", i));
channels.add(createChannel(CHANNEL_HEADLINE, "String", "Headline", i));
channels.add(createChannel(CHANNEL_ALTITUDE, "Number:Length", "Height (from)", i));
channels.add(createChannel(CHANNEL_CEILING, "Number:Length", "Height (to)", i));
channels.add(createChannel(CHANNEL_INSTRUCTION, "String", "Instruction", i));
channels.add(createChannel(CHANNEL_URGENCY, "String", "Urgency", i));
}
return channels;
}
@Override
public void dispose() {
final ScheduledFuture<?> job = refreshJob;
if (job != null) {
job.cancel(true);
}
super.dispose();
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="dwdunwetter" 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>DWD Unwetter Binding</name>
<description>This is the binding for DWD Unwetter.</description>
<author>Martin Koehler</author>
</binding:binding>

View File

@@ -0,0 +1,48 @@
# binding
binding.dwdunwetter.name = DWD Unwetter Binding
binding.dwdunwetter.description = Das DWD Unwetter Binding ermöglicht es über die API des DWD aktuelle Unwetterwarnungen abzurufen
# thing types
thing-type.dwdunwetter.dwdwarnings.label = DWD Unwetter Warnungen
thing-type.dwdunwetter.dwdwarnings.description = DWD Unwetterwarnungen für ein Gebiet
thing-type.config.dwdunwetter.dwdwarnings.cellId.label=Cell-ID
thing-type.config.dwdunwetter.dwdwarnings.cellId.description=ID der abzufragenden Zelle.\
Siehe https://www.dwd.de/DE/leistungen/opendata/help/warnungen/cap_warncellids_csv.csv.\
Es kann auch mittels % eine Gesamtmenge abgefragt werden, z.B. 8111% alle Gemeinden die mit 8111 anfangen.
thing-type.config.dwdunwetter.dwdwarnings.refresh.label=Refresh in Minuten
thing-type.config.dwdunwetter.dwdwarnings.refresh.description=Abfrageintervall in Minuten. Minimal 15 Minuten.
thing-type.config.dwdunwetter.dwdwarnings.warningCount.label=Anzahl bereitgestellter Warnungen
thing-type.config.dwdunwetter.dwdwarnings.warningCount.description=Die Anzahl der Warnungen, die als Channels bereitgestellt werden sollen.\
Die Warnungen werden dabei nach Severity und dann nach Beginn sortiert. Die erste Warnung ist also die Warnung mit der höchsten Warnstufe
# channel types
channel-type.dwdunwetter.warning.label = Warnung
channel-type.dwdunwetter.warning.description = Steht auf ON, wenn eine Warning vorliegt, OFF wenn keine vorliegt.\
Es ist garantiert, dass wenn der Channel von OFF auf ON umspringt, die anderen Channels bereits gefüllt sind, mit Ausnahme des Trigger Channels.\
Es ist garantiert, dass wenn der Channel von ON auf OFF umspringt, die anderen Channels erst danach auf NULL gesetzt werden.
channel-type.dwdunwetter.updated.label = Aktualisiert
channel-type.dwdunwetter.updated.description = Triggered, wenn eine Warnung das erste mal gesendet wird.\
Dies passiert als letztes, nachdem alle anderen Channels bereits gesetzt sind.
channel-type.dwdunwetter.severity.label = Schwere-Grad
channel-type.dwdunwetter.severity.description = Schwere-Grad der Warnung. Mögliche Werte sind Minor, Moderate, Severe und Extreme.
channel-type.dwdunwetter.description.label = Beschreibung
channel-type.dwdunwetter.description.description = Klartext Beschreibung der Warnung
channel-type.dwdunwetter.effective.label = Ausgegeben
channel-type.dwdunwetter.effective.description = Datum und Uhrzeit, wann die Warnung ausgegeben wurde
channel-type.dwdunwetter.onset.label = Gültig ab
channel-type.dwdunwetter.onset.description = Datum und Uhrzeit, ab dem die Warnung gültig ist
channel-type.dwdunwetter.expires.label = Gültig bis
channel-type.dwdunwetter.expires.description = Datum und Uhrzeit, bis zu dem die Warnung gültig ist
channel-type.dwdunwetter.headline.label = Titel
channel-type.dwdunwetter.headline.description = Titel der Warnung z.B. "Amtliche Warnung vor FROST"
channel-type.dwdunwetter.event.label = Art
channel-type.dwdunwetter.event.description = Art der Warnung, z.B. FROST
channel-type.dwdunwetter.altitude.label = Höhe von
channel-type.dwdunwetter.altitude.description = Höhe über dem Meeresspiegel, ab dem die Warnung gilt
channel-type.dwdunwetter.ceiling.label = Höhe bis
channel-type.dwdunwetter.ceiling.description = Höhe über dem Meeresspiegel, bis zu dem die Warnung gilt
channel-type.dwdunwetter.urgency.label=Zeitrahmen
channel-type.dwdunwetter.urgency.description=Zeitrahmen der Meldung - Vorabinformation oder Warnung
channel-type.dwdunwetter.instruction.label=Zusatztext
channel-type.dwdunwetter.instruction.description=Zusatztext zur Warnung (Instruktionen und Sicherheitshinweise)

View File

@@ -0,0 +1,135 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="dwdunwetter"
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="dwdwarnings">
<label>Weather Warnings</label>
<description>Weather Warnings for an area</description>
<config-description>
<parameter name="cellId" type="text" required="true">
<label>Cell-ID</label>
<description><![CDATA[ID of the area to retrieve weather warnings.
For a list of valid IDs look at https://www.dwd.de/DE/leistungen/opendata/help/warnungen/cap_warncellids_csv.csv.
With the % sign at the end it is possible to query multiple cells at once. For example with 8111% are cells retrieved that starts with 8111.]]></description>
</parameter>
<parameter name="refresh" type="integer" unit="min" min="15">
<default>30</default>
<label>Refresh in Minutes</label>
<description>Time between to API requests in minutes. Minimum 15 minutes.</description>
</parameter>
<parameter name="warningCount" type="integer" min="1" max="15">
<default>1</default>
<label>Number of Provided Warnings</label>
<description><![CDATA[Number of warnings to provide.
For each warning there will multiple channels.
The warnings are sorted by severity first and begin second so the first warning is always the one with the highest severity.]]></description>
</parameter>
</config-description>
</thing-type>
<channel-type id="lastUpdated">
<item-type>DateTime</item-type>
<label>Last Updated</label>
<description>Timestamp of the last update from the endpoint</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="warning">
<item-type>Switch</item-type>
<label>Warning</label>
<description><![CDATA[ON if a warning is present, OFF else.
While be switched to ON only after all other channels - except the trigger channel - are updated.
Will be switched to OFF before all other channels are updated to NULL]]></description>
<state readOnly="true"/>
</channel-type>
<channel-type id="updated">
<kind>trigger</kind>
<label>Updated</label>
<description><![CDATA[Triggers NEW if a warning is send the first time.
This happens after all other channels are populated]]></description>
<event>
<options>
<option value="NEW">New</option>
</options>
</event>
</channel-type>
<channel-type id="severity">
<item-type>String</item-type>
<label>Severity</label>
<description>Severity of the warning. Possible values are Minor, Moderate, Severe and Extreme.</description>
<state readOnly="true">
<options>
<option value="Minor">Minor</option>
<option value="Moderate">Moderate</option>
<option value="Severe">Severe</option>
<option value="Extreme">Extreme</option>
</options>
</state>
</channel-type>
<channel-type id="description">
<item-type>String</item-type>
<label>Description</label>
<description>Textual description of the warning.</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="effective">
<item-type>DateTime</item-type>
<label>Issued</label>
<description>Issued Date and Time</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="onset">
<item-type>DateTime</item-type>
<label>Valid From</label>
<description>Start Date and Time for which the warning is valid</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="expires">
<item-type>DateTime</item-type>
<label>Valid To</label>
<description>End Date and Time for which the warning is valid</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="headline">
<item-type>String</item-type>
<label>Headline</label>
<description>Headline of the warning like "Amtliche Warnung vor FROST"</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="event">
<item-type>String</item-type>
<label>Type</label>
<description>Type of the warning, e.g. FROST</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="altitude">
<item-type>Number:Length</item-type>
<label>Height (from)</label>
<description>Lower Height above sea level for which the warning is valid</description>
<state readOnly="true" pattern="%d m"/>
</channel-type>
<channel-type id="ceiling">
<item-type>Number:Length</item-type>
<label>Height (to)</label>
<description>Upper Height above sea level for which the warning is valid</description>
<state readOnly="true" pattern="%d m"/>
</channel-type>
<channel-type id="instruction">
<item-type>String</item-type>
<label>Instruction</label>
<description>Instructions and safety information</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="urgency">
<item-type>String</item-type>
<label>Urgency</label>
<description>Urgency of the warning. Possible values are Immediate and Future.</description>
<state readOnly="true">
<options>
<option value="Immediate">Immediate</option>
<option value="Future">Future</option>
</options>
</state>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,140 @@
/**
* 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.dwdunwetter;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
import static org.mockito.MockitoAnnotations.initMocks;
import java.io.InputStream;
import java.util.List;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.apache.commons.lang.StringUtils;
import org.hamcrest.CoreMatchers;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.openhab.binding.dwdunwetter.internal.DwdUnwetterBindingConstants;
import org.openhab.binding.dwdunwetter.internal.handler.DwdUnwetterHandler;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.test.java.JavaTest;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusInfo;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerCallback;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
/**
* Test cases for {@link DwdUnwetterHandler}. The tests provide mocks for supporting entities using Mockito.
*
* @author Martin Koehler - Initial contribution
*/
public class DwdUnwetterHandlerTest extends JavaTest {
private ThingHandler handler;
@Mock
private ThingHandlerCallback callback;
@Mock
private Thing thing;
@Before
public void setUp() {
initMocks(this);
handler = new DwdUnwetterHandler(thing);
handler.setCallback(callback);
// mock getConfiguration to prevent NPEs
when(thing.getUID()).thenReturn(new ThingUID(DwdUnwetterBindingConstants.BINDING_ID, "test"));
Configuration configuration = new Configuration();
configuration.put("refresh", Integer.valueOf("1"));
configuration.put("warningCount", Integer.valueOf("1"));
when(thing.getConfiguration()).thenReturn(configuration);
}
@Test
public void testInitializeShouldCallTheCallback() {
// we expect the handler#initialize method to call the callback during execution and
// pass it the thing and a ThingStatusInfo object containing the ThingStatus of the thing.
handler.initialize();
// the argument captor will capture the argument of type ThingStatusInfo given to the
// callback#statusUpdated method.
ArgumentCaptor<ThingStatusInfo> statusInfoCaptor = ArgumentCaptor.forClass(ThingStatusInfo.class);
// verify the interaction with the callback and capture the ThingStatusInfo argument:
waitForAssert(() -> {
verify(callback, times(1)).statusUpdated(eq(thing), statusInfoCaptor.capture());
});
// assert that the (temporary) UNKNOWN status was to the mocked thing first:
assertThat(statusInfoCaptor.getAllValues().get(0).getStatus(), is(ThingStatus.UNKNOWN));
}
/**
* Tests that the labels of the channels are equal to the ChannelType Definition
*/
@Test
public void testLabels() throws Exception {
handler.initialize();
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
InputStream stream = getClass().getResourceAsStream("/OH-INF/thing/thing-types.xml");
Document document = builder.parse(stream);
NodeList nodeList = document.getElementsByTagName("channel-type");
thing = handler.getThing();
List<Channel> channels = thing.getChannels();
for (Channel channel : channels) {
String label = getLabel(nodeList, channel.getChannelTypeUID());
assertThat(channel.getLabel(), CoreMatchers.startsWith(label));
}
}
private String getLabel(NodeList nodeList, ChannelTypeUID uuid) {
for (int i = 0; i < nodeList.getLength(); i++) {
Node node = nodeList.item(i);
Node nodeId = node.getAttributes().getNamedItem("id");
if (nodeId == null) {
continue;
}
if (StringUtils.equals(nodeId.getTextContent(), uuid.getId())) {
return getLabel(node.getChildNodes());
}
}
return null;
}
private String getLabel(NodeList nodeList) {
for (int i = 0; i < nodeList.getLength(); i++) {
Node node = nodeList.item(i);
if (node.getNodeName().equals("label")) {
return node.getTextContent();
}
}
return null;
}
}

View File

@@ -0,0 +1,65 @@
/**
* 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.dwdunwetter.internal.data;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import java.time.Instant;
import org.junit.Before;
import org.junit.Test;
/**
* Tests for {@link DwdWarningCache}
*
* @author Martin Koehler - Initial contribution
*/
public class DwdWarningCacheTest {
private DwdWarningCache cache;
@Before
public void setUp() {
cache = new DwdWarningCache();
}
@Test
public void testAddEntry() {
DwdWarningData data = createData("ID", 0);
assertThat(cache.addEntry(data), is(true));
assertThat(cache.addEntry(data), is(false));
}
@Test
public void testDeleteOldEntries() {
DwdWarningData data = createData("ID", 0);
cache.addEntry(data);
cache.deleteOldEntries();
assertThat(cache.addEntry(data), is(false));
data = createData("ID", 60 * 60);
assertThat(cache.addEntry(data), is(false));
cache.deleteOldEntries();
assertThat(cache.addEntry(data), is(true));
}
private DwdWarningData createData(String id, long secondsBeforeNow) {
DwdWarningData data = new DwdWarningData();
data.setId(id);
data.setExpires(Instant.now().minusSeconds(secondsBeforeNow));
return data;
}
}

View File

@@ -0,0 +1,173 @@
/**
* 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.dwdunwetter.internal.data;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.time.ZoneId;
import org.junit.Before;
import org.junit.Test;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.types.UnDefType;
/**
* Tests for {@link DwdWarningsData}
*
* Uses the warnings.xml from the resources directory instead of accessing the api endpoint.
*
* The XML has 2 Entries:
* <ol>
* <li>Amtliche WARNUNG vor WINDBÖEN, MINOR</li>
* <li>Amtliche WARNUNG vor STURMBÖEN, MODERATE</li>
* </ol>
*
* @author Martin Koehler - Initial contribution
*/
public class DwdWarningsDataTest {
private TestDataProvider testDataProvider;
private DwdWarningsData warningsData;
@Before
public void setUp() throws IOException {
this.testDataProvider = new TestDataProvider();
loadXmlFromFile();
warningsData = new DwdWarningsData("");
warningsData.setDataAccess(testDataProvider);
warningsData.refresh();
}
@Test
public void testGetHeadline() {
assertThat(warningsData.getHeadline(0), is("Amtliche WARNUNG vor STURMBÖEN"));
assertThat(warningsData.getHeadline(1), is("Amtliche WARNUNG vor WINDBÖEN"));
assertThat(warningsData.getHeadline(2), is(UnDefType.NULL));
}
@Test
public void testGetSeverity() {
assertThat(warningsData.getSeverity(0), is("Moderate"));
assertThat(warningsData.getSeverity(1), is("Minor"));
assertThat(warningsData.getSeverity(2), is(UnDefType.NULL));
}
@Test
public void testGetEvent() {
assertThat(warningsData.getEvent(0), is("STURMBÖEN"));
assertThat(warningsData.getEvent(1), is("WINDBÖEN"));
assertThat(warningsData.getEvent(2), is(UnDefType.NULL));
}
@Test
public void testGetDescription() {
assertThat(warningsData.getDescription(0), is(
"Es treten Sturmböen mit Geschwindigkeiten zwischen 60 km/h (17m/s, 33kn, Bft 7) und 80 km/h (22m/s, 44kn, Bft 9) anfangs aus südwestlicher, später aus westlicher Richtung auf. In Schauernähe sowie in exponierten Lagen muss mit schweren Sturmböen um 90 km/h (25m/s, 48kn, Bft 10) gerechnet werden."));
assertThat(warningsData.getDescription(1), is(
"Es treten Windböen mit Geschwindigkeiten bis 60 km/h (17m/s, 33kn, Bft 7) aus westlicher Richtung auf. In Schauernähe sowie in exponierten Lagen muss mit Sturmböen bis 80 km/h (22m/s, 44kn, Bft 9) gerechnet werden."));
assertThat(warningsData.getDescription(2), is(UnDefType.NULL));
}
@Test
public void testGetAltitude() {
assertThat(warningsData.getAltitude(0).format("%.0f ft"), is("0 ft"));
assertThat(warningsData.getAltitude(1).format("%.0f ft"), is("0 ft"));
assertThat(warningsData.getAltitude(2), is(UnDefType.NULL));
}
@Test
public void testGetCeiling() {
assertThat(warningsData.getCeiling(0).format("%.0f ft"), is("9843 ft"));
assertThat(warningsData.getCeiling(1).format("%.0f ft"), is("9843 ft"));
assertThat(warningsData.getCeiling(2), is(UnDefType.NULL));
}
@Test
public void testGetExpires() {
// Conversion is needed as getExpires returns the Date with the System Default Zone
assertThat(((DateTimeType) warningsData.getExpires(0)).getZonedDateTime().withZoneSameInstant(ZoneId.of("UTC"))
.toString(), is("2018-12-22T18:00Z[UTC]"));
assertThat(((DateTimeType) warningsData.getExpires(1)).getZonedDateTime().withZoneSameInstant(ZoneId.of("UTC"))
.toString(), is("2018-12-23T01:00Z[UTC]"));
assertThat(warningsData.getExpires(2), is(UnDefType.NULL));
}
@Test
public void testGetOnset() {
// Conversion is needed as getOnset returns the Date with the System Default Zone
assertThat(((DateTimeType) warningsData.getOnset(0)).getZonedDateTime().withZoneSameInstant(ZoneId.of("UTC"))
.toString(), is("2018-12-21T10:00Z[UTC]"));
assertThat(((DateTimeType) warningsData.getOnset(1)).getZonedDateTime().withZoneSameInstant(ZoneId.of("UTC"))
.toString(), is("2018-12-22T18:00Z[UTC]"));
assertThat(warningsData.getOnset(2), is(UnDefType.NULL));
}
@Test
public void testGetEffective() {
// Conversion is needed as getEffective returns the Date with the System Default Zone
assertThat(((DateTimeType) warningsData.getEffective(0)).getZonedDateTime()
.withZoneSameInstant(ZoneId.of("UTC")).toString(), is("2018-12-22T03:02Z[UTC]"));
assertThat(((DateTimeType) warningsData.getEffective(1)).getZonedDateTime()
.withZoneSameInstant(ZoneId.of("UTC")).toString(), is("2018-12-22T10:15Z[UTC]"));
assertThat(warningsData.getEffective(2), is(UnDefType.NULL));
}
@Test
public void testGetWarning() {
assertThat(warningsData.getWarning(0), is(OnOffType.ON));
assertThat(warningsData.getWarning(1), is(OnOffType.ON));
assertThat(warningsData.getWarning(2), is(OnOffType.OFF));
}
@Test
public void testisNew() {
assertThat(warningsData.isNew(0), is(true));
assertThat(warningsData.isNew(1), is(true));
assertThat(warningsData.isNew(2), is(false));
// No longer new
assertThat(warningsData.isNew(0), is(false));
assertThat(warningsData.isNew(1), is(false));
assertThat(warningsData.isNew(2), is(false));
}
private void loadXmlFromFile() throws IOException {
InputStream stream = getClass().getResourceAsStream("warnings.xml");
BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8));
String line = null;
StringWriter stringWriter = new StringWriter();
while ((line = reader.readLine()) != null) {
stringWriter.write(line);
}
reader.close();
testDataProvider.rawData = stringWriter.toString();
}
private class TestDataProvider extends DwdWarningDataAccess {
private String rawData = "";
@Override
public String getDataFromEndpoint(String cellId) {
return rawData;
}
}
}

View File

@@ -0,0 +1,126 @@
<?xml version="1.0" encoding="UTF-8"?>
<wfs:FeatureCollection xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:dwd="http://www.dwd.de" xmlns:wfs="http://www.opengis.net/wfs/2.0" xmlns:gml="http://www.opengis.net/gml/3.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" numberMatched="2" numberReturned="2" timeStamp="2018-12-22T10:34:44.134Z" xsi:schemaLocation="http://www.dwd.de https://maps.dwd.de:443/geoserver/dwd/wfs?service=WFS&amp;version=2.0.0&amp;request=DescribeFeatureType&amp;typeName=dwd%3AWarnungen_Gemeinden http://www.opengis.net/wfs/2.0 https://maps.dwd.de:443/geoserver/schemas/wfs/2.0/wfs.xsd http://www.opengis.net/gml/3.2 https://maps.dwd.de:443/geoserver/schemas/gml/3.2.1/gml.xsd">
<wfs:boundedBy>
<gml:Envelope>
<gml:lowerCorner>47.7798 12.874</gml:lowerCorner>
<gml:upperCorner>47.8513 12.9787</gml:upperCorner>
</gml:Envelope>
</wfs:boundedBy>
<wfs:member>
<dwd:Warnungen_Gemeinden gml:id="Warnungen_Gemeinden.809172111.2.49.0.1.276.0.DWD.PVW.1545473700000.f6482b21-04d4-4811-b379-652602cb9703.DEU">
<gml:boundedBy>
<gml:Envelope srsName="urn:ogc:def:crs:EPSG::4326" srsDimension="2">
<gml:lowerCorner>47.7798 12.874</gml:lowerCorner>
<gml:upperCorner>47.8513 12.9787</gml:upperCorner>
</gml:Envelope>
</gml:boundedBy>
<dwd:AREADESC>Ainring</dwd:AREADESC>
<dwd:NAME>Gemeinde Ainring</dwd:NAME>
<dwd:WARNCELLID>809172111</dwd:WARNCELLID>
<dwd:IDENTIFIER>2.49.0.1.276.0.DWD.PVW.1545473700000.f6482b21-04d4-4811-b379-652602cb9703.DEU</dwd:IDENTIFIER>
<dwd:SENDER>CAP@dwd.de</dwd:SENDER>
<dwd:SENT>2018-12-22T10:15:00Z</dwd:SENT>
<dwd:STATUS>Actual</dwd:STATUS>
<dwd:MSGTYPE>Alert</dwd:MSGTYPE>
<dwd:SOURCE>PVW</dwd:SOURCE>
<dwd:SCOPE>Public</dwd:SCOPE>
<dwd:CODE>id:2.49.0.1.276.0.DWD.PVW.1545473700000.f6482b21-04d4-4811-b379-652602cb9703</dwd:CODE>
<dwd:LANGUAGE>de-DE</dwd:LANGUAGE>
<dwd:CATEGORY>Met</dwd:CATEGORY>
<dwd:EVENT>WINDBÖEN</dwd:EVENT>
<dwd:RESPONSETYPE>None</dwd:RESPONSETYPE>
<dwd:URGENCY>Immediate</dwd:URGENCY>
<dwd:SEVERITY>Minor</dwd:SEVERITY>
<dwd:CERTAINTY>Likely</dwd:CERTAINTY>
<dwd:EC_PROFILE>2.1</dwd:EC_PROFILE>
<dwd:EC_LICENSE>Geobasisdaten: Copyright Bundesamt für Kartographie und Geodäsie, Frankfurt am Main, 2013</dwd:EC_LICENSE>
<dwd:EC_II>51</dwd:EC_II>
<dwd:EC_GROUP>WIND</dwd:EC_GROUP>
<dwd:EC_AREA_COLOR>255 255 0</dwd:EC_AREA_COLOR>
<dwd:EFFECTIVE>2018-12-22T10:15:00Z</dwd:EFFECTIVE>
<dwd:ONSET>2018-12-22T18:00:00Z</dwd:ONSET>
<dwd:EXPIRES>2018-12-23T01:00:00Z</dwd:EXPIRES>
<dwd:SENDERNAME>DWD / Nationales Warnzentrum Offenbach</dwd:SENDERNAME>
<dwd:HEADLINE>Amtliche WARNUNG vor WINDBÖEN</dwd:HEADLINE>
<dwd:DESCRIPTION>Es treten Windböen mit Geschwindigkeiten bis 60 km/h (17m/s, 33kn, Bft 7) aus westlicher Richtung auf. In Schauernähe sowie in exponierten Lagen muss mit Sturmböen bis 80 km/h (22m/s, 44kn, Bft 9) gerechnet werden.</dwd:DESCRIPTION>
<dwd:WEB>http://www.wettergefahren.de</dwd:WEB>
<dwd:CONTACT>Deutscher Wetterdienst</dwd:CONTACT>
<dwd:PARAMETERNAME>Windrichtung;Böen;Exponierte Böen</dwd:PARAMETERNAME>
<dwd:PARAMATERVALUE>W;&lt;60 [km/h];&lt;80 [km/h]</dwd:PARAMATERVALUE>
<dwd:ALTITUDE>0</dwd:ALTITUDE>
<dwd:CEILING>9842.5197</dwd:CEILING>
<dwd:THE_GEOM>
<gml:MultiSurface srsName="urn:ogc:def:crs:EPSG::4326" srsDimension="2" gml:id="Warnungen_Gemeinden.809172111.2.49.0.1.276.0.DWD.PVW.1545473700000.f6482b21-04d4-4811-b379-652602cb9703.DEU.THE_GEOM">
<gml:surfaceMember>
<gml:Polygon gml:id="Warnungen_Gemeinden.809172111.2.49.0.1.276.0.DWD.PVW.1545473700000.f6482b21-04d4-4811-b379-652602cb9703.DEU.THE_GEOM.1">
<gml:exterior>
<gml:LinearRing>
<gml:posList>47.8513 12.9113 47.8327 12.8749 47.8241 12.874 47.8066 12.9062 47.8039 12.9186 47.7798 12.9396 47.8191 12.9787 47.8262 12.9556 47.8441 12.9405 47.8513 12.9113</gml:posList>
</gml:LinearRing>
</gml:exterior>
</gml:Polygon>
</gml:surfaceMember>
</gml:MultiSurface>
</dwd:THE_GEOM>
</dwd:Warnungen_Gemeinden>
</wfs:member>
<wfs:member>
<dwd:Warnungen_Gemeinden gml:id="Warnungen_Gemeinden.809172111.2.49.0.1.276.0.DWD.PVW.1545447720000.90650875-4c27-4ccd-8a11-ecfbdff8e3cf.DEU">
<gml:boundedBy>
<gml:Envelope srsName="urn:ogc:def:crs:EPSG::4326" srsDimension="2">
<gml:lowerCorner>47.7798 12.874</gml:lowerCorner>
<gml:upperCorner>47.8513 12.9787</gml:upperCorner>
</gml:Envelope>
</gml:boundedBy>
<dwd:AREADESC>Ainring</dwd:AREADESC>
<dwd:NAME>Gemeinde Ainring</dwd:NAME>
<dwd:WARNCELLID>809172111</dwd:WARNCELLID>
<dwd:IDENTIFIER>2.49.0.1.276.0.DWD.PVW.1545447720000.90650875-4c27-4ccd-8a11-ecfbdff8e3cf.DEU</dwd:IDENTIFIER>
<dwd:SENDER>CAP@dwd.de</dwd:SENDER>
<dwd:SENT>2018-12-22T03:02:00Z</dwd:SENT>
<dwd:STATUS>Actual</dwd:STATUS>
<dwd:MSGTYPE>Update</dwd:MSGTYPE>
<dwd:SOURCE>PVW</dwd:SOURCE>
<dwd:SCOPE>Public</dwd:SCOPE>
<dwd:CODE>id:2.49.0.1.276.0.DWD.PVW.1545447720000.90650875-4c27-4ccd-8a11-ecfbdff8e3cf</dwd:CODE>
<dwd:LANGUAGE>de-DE</dwd:LANGUAGE>
<dwd:CATEGORY>Met</dwd:CATEGORY>
<dwd:EVENT>STURMBÖEN</dwd:EVENT>
<dwd:RESPONSETYPE>Prepare</dwd:RESPONSETYPE>
<dwd:URGENCY>Immediate</dwd:URGENCY>
<dwd:SEVERITY>Moderate</dwd:SEVERITY>
<dwd:CERTAINTY>Likely</dwd:CERTAINTY>
<dwd:EC_PROFILE>2.1</dwd:EC_PROFILE>
<dwd:EC_LICENSE>Geobasisdaten: Copyright Bundesamt für Kartographie und Geodäsie, Frankfurt am Main, 2013</dwd:EC_LICENSE>
<dwd:EC_II>52</dwd:EC_II>
<dwd:EC_GROUP>WIND</dwd:EC_GROUP>
<dwd:EC_AREA_COLOR>255 153 0</dwd:EC_AREA_COLOR>
<dwd:EFFECTIVE>2018-12-22T03:02:00Z</dwd:EFFECTIVE>
<dwd:ONSET>2018-12-21T10:00:00Z</dwd:ONSET>
<dwd:EXPIRES>2018-12-22T18:00:00Z</dwd:EXPIRES>
<dwd:SENDERNAME>DWD / Nationales Warnzentrum Offenbach</dwd:SENDERNAME>
<dwd:HEADLINE>Amtliche WARNUNG vor STURMBÖEN</dwd:HEADLINE>
<dwd:DESCRIPTION>Es treten Sturmböen mit Geschwindigkeiten zwischen 60 km/h (17m/s, 33kn, Bft 7) und 80 km/h (22m/s, 44kn, Bft 9) anfangs aus südwestlicher, später aus westlicher Richtung auf. In Schauernähe sowie in exponierten Lagen muss mit schweren Sturmböen um 90 km/h (25m/s, 48kn, Bft 10) gerechnet werden.</dwd:DESCRIPTION>
<dwd:INSTRUCTION>ACHTUNG! Hinweis auf mögliche Gefahren: Es können zum Beispiel einzelne Äste herabstürzen. Achten Sie besonders auf herabfallende Gegenstände.</dwd:INSTRUCTION>
<dwd:WEB>http://www.wettergefahren.de</dwd:WEB>
<dwd:CONTACT>Deutscher Wetterdienst</dwd:CONTACT>
<dwd:PARAMETERNAME>Böen;Exponierte Böen;Windrichtung;Windrichtung</dwd:PARAMETERNAME>
<dwd:PARAMATERVALUE>60 bis 80 [km/h];~90 [km/h];SW;W</dwd:PARAMATERVALUE>
<dwd:ALTITUDE>0</dwd:ALTITUDE>
<dwd:CEILING>9842.5197</dwd:CEILING>
<dwd:THE_GEOM>
<gml:MultiSurface srsName="urn:ogc:def:crs:EPSG::4326" srsDimension="2" gml:id="Warnungen_Gemeinden.809172111.2.49.0.1.276.0.DWD.PVW.1545447720000.90650875-4c27-4ccd-8a11-ecfbdff8e3cf.DEU.THE_GEOM">
<gml:surfaceMember>
<gml:Polygon gml:id="Warnungen_Gemeinden.809172111.2.49.0.1.276.0.DWD.PVW.1545447720000.90650875-4c27-4ccd-8a11-ecfbdff8e3cf.DEU.THE_GEOM.1">
<gml:exterior>
<gml:LinearRing>
<gml:posList>47.8513 12.9113 47.8327 12.8749 47.8241 12.874 47.8066 12.9062 47.8039 12.9186 47.7798 12.9396 47.8191 12.9787 47.8262 12.9556 47.8441 12.9405 47.8513 12.9113</gml:posList>
</gml:LinearRing>
</gml:exterior>
</gml:Polygon>
</gml:surfaceMember>
</gml:MultiSurface>
</dwd:THE_GEOM>
</dwd:Warnungen_Gemeinden>
</wfs:member>
</wfs:FeatureCollection>