[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:
Anders Alfredsson 2020-12-22 22:50:27 +01:00 committed by GitHub
parent f88225113b
commit 393ae49dc4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 344 additions and 89 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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