Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[smhi] Add aggregated channels for daily forecast. #9387

Merged
merged 2 commits into from
Dec 22, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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(), SIUnits.METRE.divide(Units.SECOND));
pacive marked this conversation as resolved.
Show resolved Hide resolved
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(), MetricPrefix.MILLI(SIUnits.METRE).divide(Units.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