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