[fmiweather] Add support for edited Scandinavia forecast (#17555)

* Add support for edited Scandinavia forecast

Resolves #17548

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
Signed-off-by: Ciprian Pascu <contact@ciprianpascu.ro>
This commit is contained in:
Jacob Laursen 2024-10-19 22:37:22 +02:00 committed by Ciprian Pascu
parent 077f7bfd84
commit 5a749d03fe
9 changed files with 96 additions and 33 deletions

View File

@ -2,8 +2,12 @@
This binding integrates to [the Finnish Meteorological Institute (FMI) Open Data API](https://en.ilmatieteenlaitos.fi/open-data).
Binding provides access to weather observations from FMI weather stations and [HARMONIE weather forecast model](https://en.ilmatieteenlaitos.fi/weather-forecast-models) forecasts.
The binding provides access to weather observations from FMI weather stations and FMI weather forecasts.
Forecast covers "northern Europe" (Finland, Baltics, Scandinavia, some parts of surrounding countries), see [coverage map in the documentation](https://en.ilmatieteenlaitos.fi/weather-forecast-models).
The binding supports two different forecast queries:
- [HARMONIE weather forecast model](https://en.ilmatieteenlaitos.fi/weather-forecast-models), which is one of the weather models that meteorologists use in their work.
- An edited query providing the official FMI forecast, which is often more accurate since it's edited by meteorologists who combine several different weather models and their experience to produce the official forecast.
![example of things](doc/images/fmi-example-things.png)
@ -36,9 +40,10 @@ The binding automatically discovers weather stations and forecasts for nearby pl
### `forecast` Thing Configuration
| Parameter | Type | Required | Description | Example |
| ---------- | ---- | -------- | ---------------------------------------------------------------------------------------------------- | --------------------------------- |
| Parameter | Type | Required | Description | Example |
| ---------- | ---- | -------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------- |
| `location` | text | ✓ | Latitude longitude location for the forecast. The parameter is given in format `LATITUDE,LONGITUDE`. | `"60.192059, 24.945831"` for Helsinki |
| `query` | text | | Stored query for official FMI forecast, either `harmonie` or `edited`. | |
## Channels

View File

@ -138,9 +138,6 @@ public abstract class AbstractWeatherHandler extends BaseThingHandler {
if (retry < RETRIES) {
try {
response = client.query(getRequest(), TIMEOUT_MILLIS);
} catch (FMIUnexpectedResponseException e) {
handleError(e, retry);
return;
} catch (FMIResponseException e) {
handleError(e, retry);
return;

View File

@ -37,6 +37,7 @@ import org.openhab.binding.fmiweather.internal.client.LatLon;
import org.openhab.binding.fmiweather.internal.client.Location;
import org.openhab.binding.fmiweather.internal.client.Request;
import org.openhab.binding.fmiweather.internal.client.exception.FMIUnexpectedResponseException;
import org.openhab.binding.fmiweather.internal.config.ForecastConfiguration;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
@ -82,6 +83,7 @@ public class ForecastWeatherHandler extends AbstractWeatherHandler {
}
private @NonNullByDefault({}) LatLon location;
private String query = "";
public ForecastWeatherHandler(Thing thing) {
super(thing);
@ -91,27 +93,23 @@ public class ForecastWeatherHandler extends AbstractWeatherHandler {
@Override
public void initialize() {
try {
Object location = getConfig().get(BindingConstants.LOCATION);
if (location == null) {
logger.debug("Location not set for thing {} -- aborting initialization.", getThing().getUID());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
String.format("location parameter not set"));
return;
}
String latlon = location.toString();
String[] split = latlon.split(",");
if (split.length != 2) {
throw new NumberFormatException(String.format(
"Expecting location parameter to have latitude and longitude separated by comma (LATITUDE,LONGITUDE). Found %d values instead.",
split.length));
}
this.location = new LatLon(new BigDecimal(split[0].trim()), new BigDecimal(split[1].trim()));
super.initialize();
} catch (NumberFormatException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, String.format(
"location parameter should be in format LATITUDE,LONGITUDE. Error details: %s", e.getMessage()));
ForecastConfiguration config = getConfigAs(ForecastConfiguration.class);
String location = config.location;
if (location.isBlank()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "location parameter not set");
return;
}
String[] split = location.split(",");
if (split.length != 2) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, String.format(
"location parameter should have latitude and longitude separated by comma (LATITUDE,LONGITUDE). Found %d values instead",
split.length));
return;
}
this.location = new LatLon(new BigDecimal(split[0].trim()), new BigDecimal(split[1].trim()));
query = config.query;
super.initialize();
}
@Override
@ -123,7 +121,7 @@ public class ForecastWeatherHandler extends AbstractWeatherHandler {
@Override
protected Request getRequest() {
long now = Instant.now().getEpochSecond();
return new ForecastRequest(location, floorToEvenMinutes(now, QUERY_RESOLUTION_MINUTES),
return new ForecastRequest(location, query, floorToEvenMinutes(now, QUERY_RESOLUTION_MINUTES),
ceilToEvenMinutes(now + TimeUnit.HOURS.toSeconds(FORECAST_HORIZON_HOURS), QUERY_RESOLUTION_MINUTES),
QUERY_RESOLUTION_MINUTES);
}

View File

@ -133,10 +133,12 @@ public class Client {
throws FMIExceptionReportException, FMIUnexpectedResponseException, FMIIOException {
try {
String url = request.toUrl();
logger.trace("GET request for {}", url);
String responseText = HttpUtil.executeUrl("GET", url, timeoutMillis);
if (responseText == null) {
throw new FMIIOException(String.format("HTTP error with %s", request.toUrl()));
}
logger.trace("Response content: '{}'", responseText);
FMIResponse response = parseMultiPointCoverageXml(responseText);
logger.debug("Request {} translated to url {}. Response: {}", request, url, response);
return response;

View File

@ -13,6 +13,7 @@
package org.openhab.binding.fmiweather.internal.client;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.fmiweather.internal.config.ForecastConfiguration;
/**
* Request for weather forecasts
@ -23,7 +24,8 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
@NonNullByDefault
public class ForecastRequest extends Request {
public static final String STORED_QUERY_ID = "fmi::forecast::harmonie::surface::point::multipointcoverage";
public static final String STORED_QUERY_ID_HARMONIE = "fmi::forecast::harmonie::surface::point::multipointcoverage";
public static final String STORED_QUERY_ID_EDITED = "fmi::forecast::edited::weather::scandinavia::point::multipointcoverage";
// For description of variables: http://opendata.fmi.fi/meta?observableProperty=forecast
public static final String PARAM_TEMPERATURE = "Temperature";
@ -39,7 +41,12 @@ public class ForecastRequest extends Request {
PARAM_WIND_SPEED, PARAM_WIND_GUST, PARAM_PRESSURE, PARAM_PRECIPITATION_1H, PARAM_TOTAL_CLOUD_COVER,
PARAM_WEATHER_SYMBOL };
public ForecastRequest(QueryParameter location, long startEpoch, long endEpoch, long timestepMinutes) {
super(STORED_QUERY_ID, location, startEpoch, endEpoch, timestepMinutes, PARAMETERS);
public ForecastRequest(QueryParameter location, String query, long startEpoch, long endEpoch,
long timestepMinutes) {
super(switch (query) {
case ForecastConfiguration.QUERY_HARMONIE -> STORED_QUERY_ID_HARMONIE;
case ForecastConfiguration.QUERY_EDITED -> STORED_QUERY_ID_EDITED;
default -> throw new IllegalArgumentException("Invalid query parameter '%s'".formatted(query));
}, location, startEpoch, endEpoch, timestepMinutes, PARAMETERS);
}
}

View File

@ -0,0 +1,29 @@
/**
* Copyright (c) 2010-2024 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.fmiweather.internal.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link ForecastConfiguration} class contains fields mapping Thing configuration parameters.
*
* @author Jacob Laursen - Initial contribution
*/
@NonNullByDefault
public class ForecastConfiguration {
public static final String QUERY_HARMONIE = "harmonie";
public static final String QUERY_EDITED = "edited";
public String location = "";
public String query = QUERY_HARMONIE;
}

View File

@ -118,6 +118,10 @@ thing-type.fmiweather.observation.description = Finnish Meteorological Institute
thing-type.config.fmiweather.forecast.location.label = Location
thing-type.config.fmiweather.forecast.location.description = Location of weather in geographical coordinates (latitude,longitude).
thing-type.config.fmiweather.forecast.query.label = Stored Query
thing-type.config.fmiweather.forecast.query.description = Stored query for official FMI forecast
thing-type.config.fmiweather.forecast.query.option.harmonie = Harmonie Surface Point Weather Forecast
thing-type.config.fmiweather.forecast.query.option.edited = Forecast for Scandinavia (edited by a forecaster)
thing-type.config.fmiweather.observation.fmisid.label = FMISID of the Weather Station
thing-type.config.fmiweather.observation.fmisid.description = Station ID (FMISID) of the weather observation station <br /> <br />See https://en.ilmatieteenlaitos.fi/observation-stations for a list of observation stations. Select 'Weather' station for widest set of observations.

View File

@ -247,6 +247,15 @@
<label>Location</label>
<description>Location of weather in geographical coordinates (latitude,longitude).</description>
</parameter>
<parameter name="query" type="text" required="false">
<label>Stored Query</label>
<description>Stored query for official FMI forecast</description>
<default>harmonie</default>
<options>
<option value="harmonie">Harmonie Surface Point Weather Forecast</option>
<option value="edited">Forecast for Scandinavia (edited by a forecaster)</option>
</options>
</parameter>
</config-description>
</thing-type>

View File

@ -49,9 +49,9 @@ public class FMIRequestTest {
}
@Test
public void testForecastRequestToUrl() {
ForecastRequest request = new ForecastRequest(new LatLon(new BigDecimal("9"), new BigDecimal("8")), 1552215664L,
1552215665L, 61);
public void testForecastRequestToUrlHarmonie() {
ForecastRequest request = new ForecastRequest(new LatLon(new BigDecimal("9"), new BigDecimal("8")), "harmonie",
1552215664L, 1552215665L, 61);
assertThat(request.toUrl(),
is("""
https://opendata.fmi.fi/wfs?service=WFS&version=2.0.0&request=getFeature&storedquery_id=fmi::forecast::harmonie::surface::point::multipointcoverage\
@ -60,6 +60,18 @@ public class FMIRequestTest {
"""));
}
@Test
public void testForecastRequestToUrlEdited() {
ForecastRequest request = new ForecastRequest(new LatLon(new BigDecimal("9"), new BigDecimal("8")), "edited",
1552215664L, 1552215665L, 61);
assertThat(request.toUrl(),
is("""
https://opendata.fmi.fi/wfs?service=WFS&version=2.0.0&request=getFeature&storedquery_id=fmi::forecast::edited::weather::scandinavia::point::multipointcoverage\
&starttime=2019-03-10T11:01:04Z&endtime=2019-03-10T11:01:05Z&timestep=61&latlon=9,8\
&parameters=Temperature,Humidity,WindDirection,WindSpeedMS,WindGust,Pressure,Precipitation1h,TotalCloudCover,WeatherSymbol3\
"""));
}
@Test
public void testCustomLocation() {
QueryParameter location = new QueryParameter() {