Skip to content

Commit

Permalink
[smhi] Add aggregated channels for daily forecast. (openhab#9387)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
pacive authored and seaside1 committed Dec 28, 2020
1 parent fff8a7c commit 23ac9e0
Show file tree
Hide file tree
Showing 10 changed files with 342 additions and 87 deletions.
8 changes: 7 additions & 1 deletion bundles/org.openhab.binding.smhi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@
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.
Expand All @@ -43,8 +43,8 @@ public Map<String, BigDecimal> getParameters() {
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
Expand Down
Original file line number Diff line number Diff line change
@@ -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)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,7 @@
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;

Expand Down Expand Up @@ -143,11 +140,11 @@ private void updateChannels(TimeSeries timeSeries) {
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);
});
}
Expand All @@ -159,74 +156,87 @@ private void updateChannels(TimeSeries timeSeries) {
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());
}
}

Expand Down Expand Up @@ -367,32 +377,27 @@ private ZonedDateTime calculateCurrentDay() {
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 (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));
});
if (hourlyForecasts != null) {
for (int i : hourlyForecasts) {
ChannelGroupUID groupUID = new ChannelGroupUID(thing.getUID(), "hour_" + i);
HOURLY_CHANNELS.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));
});
if (dailyForecasts != null) {
for (int i : dailyForecasts) {
ChannelGroupUID groupUID = new ChannelGroupUID(thing.getUID(), "day_" + i);
DAILY_CHANNELS.forEach(id -> {
channels.add(createChannel(groupUID, id));
});
}
}
return channels;
}
Expand All @@ -409,17 +414,22 @@ private Channel createChannel(ChannelGroupUID channelGroupUID, String channelID)
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:
Expand All @@ -442,4 +452,34 @@ private Channel createChannel(ChannelGroupUID channelGroupUID, String channelID)
.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();
}
}
}
Loading

0 comments on commit 23ac9e0

Please sign in to comment.