mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 15:11:59 +01:00
[smhi] Add aggregated channels for daily forecast. (#9387)
* Add aggregated channels for daily forecast. Also updates to use Optionals instead of @Nullables, and add unit tests * Revert unsing explicit unit definition Signed-off-by: Anders Alfredsson <andersb86@gmail.com>
This commit is contained in:
parent
f88225113b
commit
393ae49dc4
@ -27,18 +27,24 @@ You can also choose for which hours and which days you would like to get forecas
|
||||
|
||||
## Channels
|
||||
|
||||
The channels are the same for all forecasts:
|
||||
The channels are the same for all forecasts, but the daily forecast provides some additional aggregated values.
|
||||
For the other daily forecast channels, the values are for 12:00 UTC.
|
||||
|
||||
#### Basic channels
|
||||
|
||||
| channel | type | description |
|
||||
|----------|--------|------------------------------|
|
||||
| Temperature | Number:Temperature | Temperature in Celsius |
|
||||
| Max Temperature | Number:Temperature | Highest temperature of the day (daily forecast only) |
|
||||
| Min Temperature | Number:Temperature | Lowest temperature of the day (daily forecast only) |
|
||||
| Wind direction | Number:Angle | Wind direction in degrees |
|
||||
| Wind Speed | Number:Speed | Wind speed in m/s |
|
||||
| Max Wind Speed | Number:Speed | Highest wind speed of the day (daily forecast only) |
|
||||
| Min Wind Speed | Number:Speed | Lowest wind speed of the day (daily forecast only) |
|
||||
| Wind gust speed | Number:Speed | Wind gust speed in m/s |
|
||||
| Minimum precipitation | Number:Speed | Minimum precipitation intensity in mm/h |
|
||||
| Maximum precipitation | Number:Speed | Maximum precipitation intensity in mm/h |
|
||||
| Total precipitation | Number:Length | Total amount of precipitation during the day, in mm (daily forecast only) |
|
||||
| Precipitation category* | Number | Type of precipitation |
|
||||
| Air pressure | Number:Pressure | Air pressure in hPa |
|
||||
| Relative humidity | Number:Dimensionless | Relative humidity in percent |
|
||||
|
@ -16,9 +16,9 @@ package org.openhab.binding.smhi.internal;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* A class containing a forecast for a specific point in time.
|
||||
@ -43,8 +43,8 @@ public class Forecast implements Comparable<Forecast> {
|
||||
return parameters;
|
||||
}
|
||||
|
||||
public @Nullable BigDecimal getParameter(String parameter) {
|
||||
return parameters.get(parameter);
|
||||
public Optional<BigDecimal> getParameter(String parameter) {
|
||||
return Optional.ofNullable(parameters.get(parameter));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -0,0 +1,47 @@
|
||||
/**
|
||||
* 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.smhi.internal;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* @author Anders Alfredsson - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ForecastAggregator {
|
||||
public static Optional<BigDecimal> max(TimeSeries timeSeries, int dayOffset, String parameter) {
|
||||
List<Forecast> dayForecasts = timeSeries.getDay(dayOffset);
|
||||
return dayForecasts.stream().map(forecast -> forecast.getParameter(parameter)).filter(Optional::isPresent)
|
||||
.map(Optional::get).max(BigDecimal::compareTo);
|
||||
}
|
||||
|
||||
public static Optional<BigDecimal> min(TimeSeries timeSeries, int dayOffset, String parameter) {
|
||||
List<Forecast> dayForecasts = timeSeries.getDay(dayOffset);
|
||||
return dayForecasts.stream().map(forecast -> forecast.getParameter(parameter)).filter(Optional::isPresent)
|
||||
.map(Optional::get).min(BigDecimal::compareTo);
|
||||
}
|
||||
|
||||
public static Optional<BigDecimal> total(TimeSeries timeSeries, int dayOffset, String parameter) {
|
||||
List<Forecast> dayForecasts = timeSeries.getDay(dayOffset);
|
||||
BigDecimal sum = dayForecasts.stream().map(forecast -> forecast.getParameter(parameter))
|
||||
.filter(Optional::isPresent).map(Optional::get).reduce(BigDecimal::add).orElse(BigDecimal.ZERO);
|
||||
BigDecimal mean = sum.divide(BigDecimal.valueOf(dayForecasts.size()), RoundingMode.HALF_UP);
|
||||
return Optional.of(mean.multiply(BigDecimal.valueOf(24)));
|
||||
}
|
||||
}
|
@ -13,10 +13,7 @@
|
||||
package org.openhab.binding.smhi.internal;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
@ -56,13 +53,22 @@ public class SmhiBindingConstants {
|
||||
public static final String PRECIPITATION_CATEGORY = "pcat";
|
||||
public static final String WEATHER_SYMBOL = "wsymb2";
|
||||
|
||||
public static final List<String> CHANNEL_IDS = Collections
|
||||
.unmodifiableList(Stream
|
||||
.of(PRESSURE, TEMPERATURE, VISIBILITY, WIND_DIRECTION, WIND_SPEED, RELATIVE_HUMIDITY,
|
||||
THUNDER_PROBABILITY, TOTAL_CLOUD_COVER, LOW_CLOUD_COVER, MEDIUM_CLOUD_COVER,
|
||||
HIGH_CLOUD_COVER, GUST, PRECIPITATION_MIN, PRECIPITATION_MAX, PRECIPITATION_MEAN,
|
||||
PRECIPITATION_MEDIAN, PERCENT_FROZEN, PRECIPITATION_CATEGORY, WEATHER_SYMBOL)
|
||||
.collect(Collectors.toList()));
|
||||
public static final String TEMPERATURE_MAX = "tmax";
|
||||
public static final String TEMPERATURE_MIN = "tmin";
|
||||
public static final String WIND_MAX = "wsmax";
|
||||
public static final String WIND_MIN = "wsmin";
|
||||
public static final String PRECIPITATION_TOTAL = "ptotal";
|
||||
|
||||
public static final List<String> HOURLY_CHANNELS = List.of(PRESSURE, TEMPERATURE, VISIBILITY, WIND_DIRECTION,
|
||||
WIND_SPEED, RELATIVE_HUMIDITY, THUNDER_PROBABILITY, TOTAL_CLOUD_COVER, LOW_CLOUD_COVER, MEDIUM_CLOUD_COVER,
|
||||
HIGH_CLOUD_COVER, GUST, PRECIPITATION_MIN, PRECIPITATION_MAX, PRECIPITATION_MEAN, PRECIPITATION_MEDIAN,
|
||||
PERCENT_FROZEN, PRECIPITATION_CATEGORY, WEATHER_SYMBOL);
|
||||
|
||||
public static final List<String> DAILY_CHANNELS = List.of(PRESSURE, TEMPERATURE, TEMPERATURE_MAX, TEMPERATURE_MIN,
|
||||
VISIBILITY, WIND_DIRECTION, WIND_SPEED, WIND_MAX, WIND_MIN, RELATIVE_HUMIDITY, THUNDER_PROBABILITY,
|
||||
TOTAL_CLOUD_COVER, LOW_CLOUD_COVER, MEDIUM_CLOUD_COVER, HIGH_CLOUD_COVER, GUST, PRECIPITATION_MIN,
|
||||
PRECIPITATION_MAX, PRECIPITATION_TOTAL, PRECIPITATION_MEAN, PRECIPITATION_MEDIAN, PERCENT_FROZEN,
|
||||
PRECIPITATION_CATEGORY, WEATHER_SYMBOL);
|
||||
|
||||
public static final String BASE_URL = "https://opendata-download-metfcst.smhi.se/api/category/pmp3g/version/2/";
|
||||
public static final String APPROVED_TIME_URL = BASE_URL + "approvedtime.json";
|
||||
|
@ -17,10 +17,7 @@ import static org.openhab.binding.smhi.internal.SmhiBindingConstants.*;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@ -143,11 +140,11 @@ public class SmhiHandler extends BaseThingHandler {
|
||||
if (channels.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
Forecast forecast = timeSeries.getForecast(i);
|
||||
if (forecast != null) {
|
||||
Optional<Forecast> forecast = timeSeries.getForecast(i);
|
||||
if (forecast.isPresent()) {
|
||||
channels.forEach(c -> {
|
||||
String id = c.getUID().getIdWithoutGroup();
|
||||
BigDecimal value = forecast.getParameter(id);
|
||||
Optional<BigDecimal> value = forecast.get().getParameter(id);
|
||||
updateChannel(c, value);
|
||||
});
|
||||
}
|
||||
@ -159,74 +156,87 @@ public class SmhiHandler extends BaseThingHandler {
|
||||
continue;
|
||||
}
|
||||
|
||||
int offset = 24 * i + 12;
|
||||
Forecast forecast = timeSeries.getForecast(currentDay, offset);
|
||||
int dayOffset = i;
|
||||
int hourOffset = 24 * dayOffset + 12;
|
||||
Optional<Forecast> forecast = timeSeries.getForecast(currentDay, hourOffset);
|
||||
|
||||
if (forecast == null) {
|
||||
if (forecast.isEmpty()) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("No forecast yet for {}", currentDay.plusHours(offset));
|
||||
logger.debug("No forecast yet for {}", currentDay.plusHours(hourOffset));
|
||||
}
|
||||
channels.forEach(c -> {
|
||||
updateState(c.getUID(), UnDefType.NULL);
|
||||
updateState(c.getUID(), UnDefType.UNDEF);
|
||||
});
|
||||
} else {
|
||||
channels.forEach(c -> {
|
||||
String id = c.getUID().getIdWithoutGroup();
|
||||
BigDecimal value = forecast.getParameter(id);
|
||||
Optional<BigDecimal> value;
|
||||
if (isAggregatedChannel(id)) {
|
||||
value = getAggregatedValue(id, timeSeries, dayOffset);
|
||||
} else {
|
||||
value = forecast.get().getParameter(id);
|
||||
}
|
||||
updateChannel(c, value);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateChannel(Channel channel, @Nullable BigDecimal value) {
|
||||
private void updateChannel(Channel channel, Optional<BigDecimal> value) {
|
||||
String id = channel.getUID().getIdWithoutGroup();
|
||||
State newState = UnDefType.NULL;
|
||||
State newState = UnDefType.UNDEF;
|
||||
|
||||
if (value != null) {
|
||||
if (value.isPresent()) {
|
||||
switch (id) {
|
||||
case PRESSURE:
|
||||
newState = new QuantityType<>(value, MetricPrefix.HECTO(SIUnits.PASCAL));
|
||||
newState = new QuantityType<>(value.get(), MetricPrefix.HECTO(SIUnits.PASCAL));
|
||||
break;
|
||||
case TEMPERATURE:
|
||||
newState = new QuantityType<>(value, SIUnits.CELSIUS);
|
||||
case TEMPERATURE_MAX:
|
||||
case TEMPERATURE_MIN:
|
||||
newState = new QuantityType<>(value.get(), SIUnits.CELSIUS);
|
||||
break;
|
||||
case VISIBILITY:
|
||||
newState = new QuantityType<>(value, MetricPrefix.KILO(SIUnits.METRE));
|
||||
newState = new QuantityType<>(value.get(), MetricPrefix.KILO(SIUnits.METRE));
|
||||
break;
|
||||
case WIND_DIRECTION:
|
||||
newState = new QuantityType<>(value, Units.DEGREE_ANGLE);
|
||||
newState = new QuantityType<>(value.get(), Units.DEGREE_ANGLE);
|
||||
break;
|
||||
case WIND_SPEED:
|
||||
case WIND_MAX:
|
||||
case WIND_MIN:
|
||||
case GUST:
|
||||
newState = new QuantityType<>(value, Units.METRE_PER_SECOND);
|
||||
newState = new QuantityType<>(value.get(), Units.METRE_PER_SECOND);
|
||||
break;
|
||||
case RELATIVE_HUMIDITY:
|
||||
case THUNDER_PROBABILITY:
|
||||
newState = new QuantityType<>(value, Units.PERCENT);
|
||||
newState = new QuantityType<>(value.get(), Units.PERCENT);
|
||||
break;
|
||||
case PERCENT_FROZEN:
|
||||
// Smhi returns -9 for spp if there's no precipitation, convert to UNDEF
|
||||
if (value.intValue() == -9) {
|
||||
if (value.get().intValue() == -9) {
|
||||
newState = UnDefType.UNDEF;
|
||||
} else {
|
||||
newState = new QuantityType<>(value, Units.PERCENT);
|
||||
newState = new QuantityType<>(value.get(), Units.PERCENT);
|
||||
}
|
||||
break;
|
||||
case HIGH_CLOUD_COVER:
|
||||
case MEDIUM_CLOUD_COVER:
|
||||
case LOW_CLOUD_COVER:
|
||||
case TOTAL_CLOUD_COVER:
|
||||
newState = new QuantityType<>(value.multiply(OCTAS_TO_PERCENT), Units.PERCENT);
|
||||
newState = new QuantityType<>(value.get().multiply(OCTAS_TO_PERCENT), Units.PERCENT);
|
||||
break;
|
||||
case PRECIPITATION_MAX:
|
||||
case PRECIPITATION_MEAN:
|
||||
case PRECIPITATION_MEDIAN:
|
||||
case PRECIPITATION_MIN:
|
||||
newState = new QuantityType<>(value, Units.MILLIMETRE_PER_HOUR);
|
||||
newState = new QuantityType<>(value.get(), Units.MILLIMETRE_PER_HOUR);
|
||||
break;
|
||||
case PRECIPITATION_TOTAL:
|
||||
newState = new QuantityType<>(value.get(), MetricPrefix.MILLI(SIUnits.METRE));
|
||||
break;
|
||||
default:
|
||||
newState = new DecimalType(value);
|
||||
newState = new DecimalType(value.get());
|
||||
}
|
||||
}
|
||||
|
||||
@ -367,32 +377,27 @@ public class SmhiHandler extends BaseThingHandler {
|
||||
private List<Channel> createChannels() {
|
||||
List<Channel> channels = new ArrayList<>();
|
||||
|
||||
// There's currently a bug in PaperUI that can cause options to be added more than one time
|
||||
// to the list. Convert to a sorted set to work around this.
|
||||
// See https://github.com/openhab/openhab-webui/issues/212
|
||||
Set<Integer> hours = new TreeSet<>();
|
||||
Set<Integer> days = new TreeSet<>();
|
||||
@Nullable
|
||||
List<Integer> hourlyForecasts = config.hourlyForecasts;
|
||||
if (hourlyForecasts != null) {
|
||||
hours.addAll(hourlyForecasts);
|
||||
}
|
||||
@Nullable
|
||||
List<Integer> dailyForecasts = config.dailyForecasts;
|
||||
|
||||
if (hourlyForecasts != null) {
|
||||
for (int i : hourlyForecasts) {
|
||||
ChannelGroupUID groupUID = new ChannelGroupUID(thing.getUID(), "hour_" + i);
|
||||
HOURLY_CHANNELS.forEach(id -> {
|
||||
channels.add(createChannel(groupUID, id));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (dailyForecasts != null) {
|
||||
days.addAll(dailyForecasts);
|
||||
}
|
||||
|
||||
for (int i : hours) {
|
||||
ChannelGroupUID groupUID = new ChannelGroupUID(thing.getUID(), "hour_" + i);
|
||||
CHANNEL_IDS.forEach(id -> {
|
||||
channels.add(createChannel(groupUID, id));
|
||||
});
|
||||
}
|
||||
|
||||
for (int i : days) {
|
||||
ChannelGroupUID groupUID = new ChannelGroupUID(thing.getUID(), "day_" + i);
|
||||
CHANNEL_IDS.forEach(id -> {
|
||||
channels.add(createChannel(groupUID, id));
|
||||
});
|
||||
for (int i : dailyForecasts) {
|
||||
ChannelGroupUID groupUID = new ChannelGroupUID(thing.getUID(), "day_" + i);
|
||||
DAILY_CHANNELS.forEach(id -> {
|
||||
channels.add(createChannel(groupUID, id));
|
||||
});
|
||||
}
|
||||
}
|
||||
return channels;
|
||||
}
|
||||
@ -409,17 +414,22 @@ public class SmhiHandler extends BaseThingHandler {
|
||||
String itemType = "Number";
|
||||
switch (channelID) {
|
||||
case TEMPERATURE:
|
||||
case TEMPERATURE_MAX:
|
||||
case TEMPERATURE_MIN:
|
||||
itemType += ":Temperature";
|
||||
break;
|
||||
case PRESSURE:
|
||||
itemType += ":Pressure";
|
||||
break;
|
||||
case VISIBILITY:
|
||||
case PRECIPITATION_TOTAL:
|
||||
itemType += ":Length";
|
||||
break;
|
||||
case WIND_DIRECTION:
|
||||
itemType += ":Angle";
|
||||
case WIND_SPEED:
|
||||
case WIND_MAX:
|
||||
case WIND_MIN:
|
||||
case GUST:
|
||||
case PRECIPITATION_MAX:
|
||||
case PRECIPITATION_MEAN:
|
||||
@ -442,4 +452,34 @@ public class SmhiHandler extends BaseThingHandler {
|
||||
.withType(new ChannelTypeUID(BINDING_ID, channelID)).build();
|
||||
return channel;
|
||||
}
|
||||
|
||||
private boolean isAggregatedChannel(String channelId) {
|
||||
switch (channelId) {
|
||||
case TEMPERATURE_MAX:
|
||||
case TEMPERATURE_MIN:
|
||||
case WIND_MAX:
|
||||
case WIND_MIN:
|
||||
case PRECIPITATION_TOTAL:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<BigDecimal> getAggregatedValue(String channelId, TimeSeries timeSeries, int dayOffset) {
|
||||
switch (channelId) {
|
||||
case TEMPERATURE_MAX:
|
||||
return ForecastAggregator.max(timeSeries, dayOffset, TEMPERATURE);
|
||||
case TEMPERATURE_MIN:
|
||||
return ForecastAggregator.min(timeSeries, dayOffset, TEMPERATURE);
|
||||
case WIND_MAX:
|
||||
return ForecastAggregator.max(timeSeries, dayOffset, WIND_SPEED);
|
||||
case WIND_MIN:
|
||||
return ForecastAggregator.min(timeSeries, dayOffset, WIND_SPEED);
|
||||
case PRECIPITATION_TOTAL:
|
||||
return ForecastAggregator.total(timeSeries, dayOffset, PRECIPITATION_MEAN);
|
||||
default:
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,8 +16,10 @@ package org.openhab.binding.smhi.internal;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Spliterator;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
@ -48,8 +50,8 @@ public class TimeSeries implements Iterable<Forecast> {
|
||||
* @param hourOffset number of hours after now.
|
||||
* @return
|
||||
*/
|
||||
public @Nullable Forecast getForecast(int hourOffset) {
|
||||
return getForecast(ZonedDateTime.now(), hourOffset);
|
||||
public Optional<Forecast> getForecast(int hourOffset) {
|
||||
return getForecast(referenceTime, hourOffset);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -58,17 +60,23 @@ public class TimeSeries implements Iterable<Forecast> {
|
||||
* @param hourOffset number of hours after now.
|
||||
* @return
|
||||
*/
|
||||
public @Nullable Forecast getForecast(ZonedDateTime startTime, int hourOffset) {
|
||||
public Optional<Forecast> getForecast(ZonedDateTime startTime, int hourOffset) {
|
||||
if (hourOffset < 0) {
|
||||
throw new IllegalArgumentException("Offset must be at least 0");
|
||||
}
|
||||
|
||||
for (Forecast forecast : forecasts) {
|
||||
if (forecast.getValidTime().compareTo(startTime.plusHours(hourOffset)) >= 0) {
|
||||
return forecast;
|
||||
if (forecast.getValidTime().compareTo(startTime.plusHours(hourOffset)) > 0) {
|
||||
return Optional.of(forecast);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
public List<Forecast> getDay(int dayOffset) {
|
||||
ZonedDateTime day = referenceTime.plusDays(dayOffset);
|
||||
return forecasts.stream().filter(forecast -> forecast.getValidTime().getDayOfMonth() == day.getDayOfMonth())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -6,11 +6,11 @@
|
||||
https://openhab.org/schemas/config-description-1.0.0.xsd">
|
||||
|
||||
<config-description uri="thing-type:smhi:forecast">
|
||||
<parameter name="latitude" type="decimal" required="true">
|
||||
<parameter name="latitude" type="decimal" step="0.000001" required="true">
|
||||
<label>Latitude</label>
|
||||
<description>Latitude for the forecast</description>
|
||||
</parameter>
|
||||
<parameter name="longitude" type="decimal" required="true">
|
||||
<parameter name="longitude" type="decimal" step="0.000001" required="true">
|
||||
<label>Longitude</label>
|
||||
<description>Longitude for the forecast</description>
|
||||
</parameter>
|
||||
|
@ -8,97 +8,127 @@
|
||||
<item-type>Number:Pressure</item-type>
|
||||
<label>Air Pressure</label>
|
||||
<description>Air pressure in hPa</description>
|
||||
<state readOnly="true" pattern="%.1f %unit%"/>
|
||||
<state readOnly="true" pattern="%.1f hPa"/>
|
||||
</channel-type>
|
||||
<channel-type id="t">
|
||||
<item-type>Number:Temperature</item-type>
|
||||
<label>Temperature</label>
|
||||
<description>Temperature</description>
|
||||
<state readOnly="true" pattern="%.1f %unit%"/>
|
||||
<state readOnly="true" pattern="%.1f °C"/>
|
||||
</channel-type>
|
||||
<channel-type id="tmax">
|
||||
<item-type>Number:Temperature</item-type>
|
||||
<label>Max Temperature</label>
|
||||
<description>Highest temperature of the day</description>
|
||||
<state readOnly="true" pattern="%.1f °C"/>
|
||||
</channel-type>
|
||||
<channel-type id="tmin">
|
||||
<item-type>Number:Temperature</item-type>
|
||||
<label>Min Temperature</label>
|
||||
<description>Lowest temperature of the day</description>
|
||||
<state readOnly="true" pattern="%.1f °C"/>
|
||||
</channel-type>
|
||||
<channel-type id="vis" advanced="true">
|
||||
<item-type>Number:Length</item-type>
|
||||
<label>Visibility</label>
|
||||
<description>Horizontal visibility</description>
|
||||
<state readOnly="true" pattern="%.1f %unit%"/>
|
||||
<state readOnly="true" pattern="%.1f km"/>
|
||||
</channel-type>
|
||||
<channel-type id="wd">
|
||||
<item-type>Number:Angle</item-type>
|
||||
<label>Wind Direction</label>
|
||||
<description>Wind direction</description>
|
||||
<state readOnly="true" pattern="%d %unit%"/>
|
||||
<state readOnly="true" pattern="%d °"/>
|
||||
</channel-type>
|
||||
<channel-type id="ws">
|
||||
<item-type>Number:Speed</item-type>
|
||||
<label>Wind Speed</label>
|
||||
<description>Wind speed</description>
|
||||
<state readOnly="true" pattern="%.1f %unit%"/>
|
||||
<state readOnly="true" pattern="%.1f m/s"/>
|
||||
</channel-type>
|
||||
<channel-type id="wsmax">
|
||||
<item-type>Number:Speed</item-type>
|
||||
<label>Max Wind Speed</label>
|
||||
<description>Highest wind speed of the day</description>
|
||||
<state readOnly="true" pattern="%.1f m/s"/>
|
||||
</channel-type>
|
||||
<channel-type id="wsmin">
|
||||
<item-type>Number:Speed</item-type>
|
||||
<label>Min Wind Speed</label>
|
||||
<description>Lowest wind speed of the day</description>
|
||||
<state readOnly="true" pattern="%.1f m/s"/>
|
||||
</channel-type>
|
||||
<channel-type id="r">
|
||||
<item-type>Number:Dimensionless</item-type>
|
||||
<label>Relative Humidity</label>
|
||||
<description>Relative humidity in percent</description>
|
||||
<state readOnly="true" pattern="%d %unit%"/>
|
||||
<state readOnly="true" pattern="%d %%"/>
|
||||
</channel-type>
|
||||
<channel-type id="tstm" advanced="true">
|
||||
<item-type>Number:Dimensionless</item-type>
|
||||
<label>Thunder Probability</label>
|
||||
<description>Probability of thunder in percent</description>
|
||||
<state readOnly="true" pattern="%d %unit%"/>
|
||||
<state readOnly="true" pattern="%d %%"/>
|
||||
</channel-type>
|
||||
<channel-type id="tcc_mean">
|
||||
<item-type>Number:Dimensionless</item-type>
|
||||
<label>Total Cloud Cover</label>
|
||||
<description>Mean value of total cloud cover in percent</description>
|
||||
<state readOnly="true" pattern="%.1f %unit%"/>
|
||||
<state readOnly="true" pattern="%.1f %%"/>
|
||||
</channel-type>
|
||||
<channel-type id="lcc_mean" advanced="true">
|
||||
<item-type>Number:Dimensionless</item-type>
|
||||
<label>Low Level Cloud Cover</label>
|
||||
<description>Mean value of low level cloud cover (0-2500 m) in percent</description>
|
||||
<state readOnly="true" pattern="%.1f %unit%"/>
|
||||
<state readOnly="true" pattern="%.1f %%"/>
|
||||
</channel-type>
|
||||
<channel-type id="mcc_mean" advanced="true">
|
||||
<item-type>Number:Dimensionless</item-type>
|
||||
<label>Medium Level Cloud Cover</label>
|
||||
<description>Mean value of medium level cloud cover (2500-6000 m) in percent</description>
|
||||
<state readOnly="true" pattern="%.1f %unit%"/>
|
||||
<state readOnly="true" pattern="%.1f %%"/>
|
||||
</channel-type>
|
||||
<channel-type id="hcc_mean" advanced="true">
|
||||
<item-type>Number:Dimensionless</item-type>
|
||||
<label>High Level Cloud Cover</label>
|
||||
<description>Mean value of high level cloud cover (> 6000 m) in percent</description>
|
||||
<state readOnly="true" pattern="%.1f %unit%"/>
|
||||
<state readOnly="true" pattern="%.1f %%"/>
|
||||
</channel-type>
|
||||
<channel-type id="gust">
|
||||
<item-type>Number:Speed</item-type>
|
||||
<label>Wind Gust Speed</label>
|
||||
<description>Wind gust speed</description>
|
||||
<state readOnly="true" pattern="%.1f %unit%"/>
|
||||
<state readOnly="true" pattern="%.1f m/s"/>
|
||||
</channel-type>
|
||||
<channel-type id="pmin">
|
||||
<item-type>Number:Speed</item-type>
|
||||
<label>Minimum Precipitation</label>
|
||||
<description>Minimum precipitation intensity</description>
|
||||
<state readOnly="true" pattern="%.1f %unit%"/>
|
||||
<state readOnly="true" pattern="%.1f mm/h"/>
|
||||
</channel-type>
|
||||
<channel-type id="pmax">
|
||||
<item-type>Number:Speed</item-type>
|
||||
<label>Maximum Precipitation</label>
|
||||
<description>Maximum precipitation intensity</description>
|
||||
<state readOnly="true" pattern="%.1f %unit%"/>
|
||||
<state readOnly="true" pattern="%.1f mm/h"/>
|
||||
</channel-type>
|
||||
<channel-type id="ptotal">
|
||||
<item-type>Number:Length</item-type>
|
||||
<label>Total Precipitation</label>
|
||||
<description>Total amount of precipitation during the day</description>
|
||||
<state readOnly="true" pattern="%.1f mm"/>
|
||||
</channel-type>
|
||||
<channel-type id="pmean" advanced="true">
|
||||
<item-type>Number:Speed</item-type>
|
||||
<label>Mean Precipitation</label>
|
||||
<description>Mean precipitation intensity</description>
|
||||
<state readOnly="true" pattern="%.1f %unit%"/>
|
||||
<state readOnly="true" pattern="%.1f mm/h"/>
|
||||
</channel-type>
|
||||
<channel-type id="pmedian" advanced="true">
|
||||
<item-type>Number:Speed</item-type>
|
||||
<label>Median Precipitation</label>
|
||||
<description>Median precipitation intensity</description>
|
||||
<state readOnly="true" pattern="%.1f %unit%"/>
|
||||
<state readOnly="true" pattern="%.1f mm/h"/>
|
||||
</channel-type>
|
||||
<channel-type id="pcat">
|
||||
<item-type>Number</item-type>
|
||||
@ -120,7 +150,7 @@
|
||||
<item-type>Number:Dimensionless</item-type>
|
||||
<label>Frozen Precipitation</label>
|
||||
<description>Percent of precipitation in frozen form</description>
|
||||
<state readOnly="true" pattern="%d %unit%"/>
|
||||
<state readOnly="true" pattern="%d %%"/>
|
||||
</channel-type>
|
||||
<channel-type id="wsymb2">
|
||||
<item-type>Number</item-type>
|
||||
@ -190,11 +220,16 @@
|
||||
<description>Forecast at noon for the specified offset</description>
|
||||
<channels>
|
||||
<channel id="t" typeId="t"/>
|
||||
<channel id="tmax" typeId="tmax"/>
|
||||
<channel id="tmin" typeId="tmin"/>
|
||||
<channel id="wd" typeId="wd"/>
|
||||
<channel id="ws" typeId="ws"/>
|
||||
<channel id="wsmax" typeId="wsmax"/>
|
||||
<channel id="wsmin" typeId="wsmin"/>
|
||||
<channel id="gust" typeId="gust"/>
|
||||
<channel id="pmin" typeId="pmin"/>
|
||||
<channel id="pmax" typeId="pmax"/>
|
||||
<channel id="ptotal" typeId="ptotal"/>
|
||||
<channel id="pcat" typeId="pcat"/>
|
||||
<channel id="msl" typeId="msl"/>
|
||||
<channel id="r" typeId="r"/>
|
||||
|
@ -0,0 +1,112 @@
|
||||
/**
|
||||
* 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.smhi.internal;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.openhab.binding.smhi.internal.SmhiBindingConstants.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.ZonedDateTime;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* @author Anders Alfredsson - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SmhiTest {
|
||||
private static final ZonedDateTime TIME = ZonedDateTime.parse("2020-12-13T08:15:00Z");
|
||||
private @NonNullByDefault({}) TimeSeries timeSeries;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
try {
|
||||
InputStream is = SmhiTest.class.getResourceAsStream("forecast.json");
|
||||
if (is == null) {
|
||||
throw new AssertionError("Couldn't read forecast example");
|
||||
}
|
||||
String jsonString = new String(is.readAllBytes());
|
||||
timeSeries = Parser.parseTimeSeries(jsonString);
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("Couldn't read forecast example");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parameterTest() {
|
||||
assertNotNull(timeSeries);
|
||||
Forecast forecast = timeSeries.getForecast(TIME, 0).orElseThrow(AssertionError::new);
|
||||
|
||||
BigDecimal msl = forecast.getParameter(PRESSURE).orElseThrow(AssertionError::new);
|
||||
BigDecimal t = forecast.getParameter(TEMPERATURE).orElseThrow(AssertionError::new);
|
||||
BigDecimal vis = forecast.getParameter(VISIBILITY).orElseThrow(AssertionError::new);
|
||||
BigDecimal wd = forecast.getParameter(WIND_DIRECTION).orElseThrow(AssertionError::new);
|
||||
BigDecimal ws = forecast.getParameter(WIND_SPEED).orElseThrow(AssertionError::new);
|
||||
BigDecimal r = forecast.getParameter(RELATIVE_HUMIDITY).orElseThrow(AssertionError::new);
|
||||
BigDecimal tstm = forecast.getParameter(THUNDER_PROBABILITY).orElseThrow(AssertionError::new);
|
||||
BigDecimal tcc = forecast.getParameter(TOTAL_CLOUD_COVER).orElseThrow(AssertionError::new);
|
||||
BigDecimal lcc = forecast.getParameter(LOW_CLOUD_COVER).orElseThrow(AssertionError::new);
|
||||
BigDecimal mcc = forecast.getParameter(MEDIUM_CLOUD_COVER).orElseThrow(AssertionError::new);
|
||||
BigDecimal hcc = forecast.getParameter(HIGH_CLOUD_COVER).orElseThrow(AssertionError::new);
|
||||
BigDecimal gust = forecast.getParameter(GUST).orElseThrow(AssertionError::new);
|
||||
BigDecimal pmin = forecast.getParameter(PRECIPITATION_MIN).orElseThrow(AssertionError::new);
|
||||
BigDecimal pmax = forecast.getParameter(PRECIPITATION_MAX).orElseThrow(AssertionError::new);
|
||||
BigDecimal spp = forecast.getParameter(PERCENT_FROZEN).orElseThrow(AssertionError::new);
|
||||
BigDecimal pcat = forecast.getParameter(PRECIPITATION_CATEGORY).orElseThrow(AssertionError::new);
|
||||
BigDecimal pmean = forecast.getParameter(PRECIPITATION_MEAN).orElseThrow(AssertionError::new);
|
||||
BigDecimal pmedian = forecast.getParameter(PRECIPITATION_MEDIAN).orElseThrow(AssertionError::new);
|
||||
BigDecimal wsymb = forecast.getParameter(WEATHER_SYMBOL).orElseThrow(AssertionError::new);
|
||||
|
||||
assertEquals(0, msl.compareTo(BigDecimal.valueOf(1013.7)));
|
||||
assertEquals(0, t.compareTo(BigDecimal.valueOf(3.0)));
|
||||
assertEquals(0, vis.compareTo(BigDecimal.valueOf(24.3)));
|
||||
assertEquals(0, wd.compareTo(BigDecimal.valueOf(110)));
|
||||
assertEquals(0, ws.compareTo(BigDecimal.valueOf(1.5)));
|
||||
assertEquals(0, r.compareTo(BigDecimal.valueOf(96)));
|
||||
assertEquals(0, tstm.compareTo(BigDecimal.valueOf(0)));
|
||||
assertEquals(0, tcc.compareTo(BigDecimal.valueOf(8)));
|
||||
assertEquals(0, lcc.compareTo(BigDecimal.valueOf(8)));
|
||||
assertEquals(0, mcc.compareTo(BigDecimal.valueOf(4)));
|
||||
assertEquals(0, hcc.compareTo(BigDecimal.valueOf(0)));
|
||||
assertEquals(0, gust.compareTo(BigDecimal.valueOf(3.0)));
|
||||
assertEquals(0, pmin.compareTo(BigDecimal.valueOf(0.0)));
|
||||
assertEquals(0, pmax.compareTo(BigDecimal.valueOf(0.0)));
|
||||
assertEquals(0, spp.compareTo(BigDecimal.valueOf(-9)));
|
||||
assertEquals(0, pcat.compareTo(BigDecimal.valueOf(0)));
|
||||
assertEquals(0, pmean.compareTo(BigDecimal.valueOf(0.0)));
|
||||
assertEquals(0, pmedian.compareTo(BigDecimal.valueOf(0.0)));
|
||||
assertEquals(0, wsymb.compareTo(BigDecimal.valueOf(6)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aggregationsTest() {
|
||||
assertNotNull(timeSeries);
|
||||
BigDecimal maxTemp = ForecastAggregator.max(timeSeries, 5, TEMPERATURE).orElseThrow(AssertionError::new);
|
||||
BigDecimal minTemp = ForecastAggregator.min(timeSeries, 5, TEMPERATURE).orElseThrow(AssertionError::new);
|
||||
BigDecimal maxWind = ForecastAggregator.max(timeSeries, 5, WIND_SPEED).orElseThrow(AssertionError::new);
|
||||
BigDecimal minWind = ForecastAggregator.min(timeSeries, 5, WIND_SPEED).orElseThrow(AssertionError::new);
|
||||
BigDecimal totalPrecip = ForecastAggregator.total(timeSeries, 5, PRECIPITATION_MEAN)
|
||||
.orElseThrow(AssertionError::new);
|
||||
|
||||
assertEquals(0, maxTemp.compareTo(BigDecimal.valueOf(7.5)));
|
||||
assertEquals(0, minTemp.compareTo(BigDecimal.valueOf(4.2)));
|
||||
assertEquals(0, maxWind.compareTo(BigDecimal.valueOf(4.4)));
|
||||
assertEquals(0, minWind.compareTo(BigDecimal.valueOf(3.7)));
|
||||
assertEquals(0, totalPrecip.compareTo(BigDecimal.valueOf(2.4)));
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user