Add time series support for forecasts (#17543)

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-13 07:44:25 +02:00 committed by Ciprian Pascu
parent 2a1fb95946
commit 04b9c33706
7 changed files with 150 additions and 35 deletions

View File

@ -28,13 +28,13 @@ The binding automatically discovers weather stations and forecasts for nearby pl
## Thing Configuration
### `observation` thing configuration
### `observation` Thing Configuration
| Parameter | Type | Required | Description | Example |
| --------- | ---- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
| `fmisid` | text | ✓ | FMI Station ID. You can FMISID of see all weathers stations at [FMI web site](https://en.ilmatieteenlaitos.fi/observation-stations?p_p_id=stationlistingportlet_WAR_fmiwwwweatherportlets&p_p_lifecycle=0&p_p_state=normal&p_p_mode=view&p_p_col_id=column-4&p_p_col_count=1&_stationlistingportlet_WAR_fmiwwwweatherportlets_stationGroup=WEATHER#station-listing) | `"852678"` for Espoo Nuuksio station |
### `forecast` thing configuration
### `forecast` Thing Configuration
| Parameter | Type | Required | Description | Example |
| ---------- | ---- | -------- | ---------------------------------------------------------------------------------------------------- | --------------------------------- |
@ -44,7 +44,7 @@ The binding automatically discovers weather stations and forecasts for nearby pl
Observation and forecast things provide slightly different details on weather.
### `observation` thing channels
### `observation` Thing Channels
Observation channels are grouped in single group, `current`.
@ -67,11 +67,12 @@ You can check the exact observation time by using the `time` channel.
To refer to certain channel, use the normal convention `THING_ID:GROUP_ID#CHANNEL_ID`, e.g. `fmiweather:observation:station_874863_Espoo_Tapiola:current#temperature`.
### `forecast` thing channels
### `forecast` Thing Channels
Forecast has multiple channel groups, one for each forecasted time. The groups are named as follows:
- `forecastNow`: Forecasted weather for the current time
- `forecast`: Forecasted weather (with time series support)
- `forecastNow`: Forecasted weather for the current time (deprecated, please use `forecast` instead)
- `forecastHours01`: Forecasted weather for 1 hours from now
- `forecastHours02`: Forecasted weather for 2 hours from now
- etc.

View File

@ -44,6 +44,7 @@ import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -210,13 +211,24 @@ public abstract class AbstractWeatherHandler extends BaseThingHandler {
*/
protected void updateStateIfLinked(ChannelUID channelUID, @Nullable BigDecimal value, @Nullable Unit<?> unit) {
if (isLinked(channelUID)) {
if (value == null) {
updateState(channelUID, UnDefType.UNDEF);
} else if (unit == null) {
updateState(channelUID, new DecimalType(value));
} else {
updateState(channelUID, new QuantityType<>(value, unit));
}
updateState(channelUID, getState(value, unit));
}
}
/**
* Return QuantityType or DecimalType channel state
*
* @param value value to update
* @param unit unit associated with the value
* @return UNDEF state when value is null, otherwise QuantityType or DecimalType
*/
protected State getState(@Nullable BigDecimal value, @Nullable Unit<?> unit) {
if (value == null) {
return UnDefType.UNDEF;
} else if (unit == null) {
return new DecimalType(value);
} else {
return new QuantityType<>(value, unit);
}
}

View File

@ -35,6 +35,9 @@ public class BindingConstants {
public static final ThingTypeUID THING_TYPE_FORECAST = new ThingTypeUID(BINDING_ID, "forecast");
public static final ThingUID UID_LOCAL_FORECAST = new ThingUID(BINDING_ID, "forecast", "local");
// List of all static Channel Group IDs
public static final String CHANNEL_GROUP_FORECAST = "forecast";
// List of all Channel ids
public static final String CHANNEL_TIME = "time";
public static final String CHANNEL_TEMPERATURE = "temperature";

View File

@ -16,6 +16,7 @@ import static org.openhab.binding.fmiweather.internal.BindingConstants.*;
import static org.openhab.binding.fmiweather.internal.client.ForecastRequest.*;
import static org.openhab.core.library.unit.SIUnits.CELSIUS;
import static org.openhab.core.library.unit.Units.*;
import static org.openhab.core.types.TimeSeries.Policy.REPLACE;
import java.math.BigDecimal;
import java.time.Instant;
@ -41,6 +42,7 @@ import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.types.TimeSeries;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -140,29 +142,8 @@ public class ForecastWeatherHandler extends AbstractWeatherHandler {
properties.put(PROP_LATITUDE, location.latitude.toPlainString());
properties.put(PROP_LONGITUDE, location.longitude.toPlainString());
updateProperties(properties);
for (Channel channel : getThing().getChannels()) {
ChannelUID channelUID = channel.getUID();
int hours = getHours(channelUID);
int timeIndex = getTimeIndex(hours);
if (channelUID.getIdWithoutGroup().equals(CHANNEL_TIME)) {
// All parameters and locations should share the same timestamps. We use temperature to figure out
// timestamp for the group of channels
String field = ForecastRequest.PARAM_TEMPERATURE;
Data data = unwrap(response.getData(location, field),
"Field %s not present for location %s in response. Bug?", field, location);
updateEpochSecondStateIfLinked(channelUID, data.timestampsEpochSecs[timeIndex]);
} else {
String field = getDataField(channelUID);
Unit<?> unit = getUnit(channelUID);
if (field == null) {
logger.error("Channel {} not handled. Bug?", channelUID.getId());
continue;
}
Data data = unwrap(response.getData(location, field),
"Field %s not present for location %s in response. Bug?", field, location);
updateStateIfLinked(channelUID, data.values[timeIndex], unit);
}
}
updateHourlyChannels(response, location);
updateTimeSeriesChannels(response, location);
updateStatus(ThingStatus.ONLINE);
} catch (FMIUnexpectedResponseException e) {
// Unexpected (possibly bug) issue with response
@ -172,6 +153,71 @@ public class ForecastWeatherHandler extends AbstractWeatherHandler {
}
}
private void updateHourlyChannels(FMIResponse response, Location location) throws FMIUnexpectedResponseException {
for (Channel channel : getThing().getChannels()) {
ChannelUID channelUID = channel.getUID();
if (CHANNEL_GROUP_FORECAST.equals(channelUID.getGroupId())) {
// Skip time series group
continue;
}
int hours = getHours(channelUID);
int timeIndex = getTimeIndex(hours);
if (channelUID.getIdWithoutGroup().equals(CHANNEL_TIME)) {
// All parameters and locations should share the same timestamps. We use temperature to figure out
// timestamp for the group of channels
final String field = ForecastRequest.PARAM_TEMPERATURE;
Data data = unwrap(response.getData(location, field),
"Field %s not present for location %s in response. Bug?", field, location);
updateEpochSecondStateIfLinked(channelUID, data.timestampsEpochSecs[timeIndex]);
} else {
String field = getDataField(channelUID);
Unit<?> unit = getUnit(channelUID);
if (field == null) {
logger.error("Channel {} not handled. Bug?", channelUID.getId());
continue;
}
Data data = unwrap(response.getData(location, field),
"Field %s not present for location %s in response. Bug?", field, location);
updateStateIfLinked(channelUID, data.values[timeIndex], unit);
}
}
}
private void updateTimeSeriesChannels(FMIResponse response, Location location)
throws FMIUnexpectedResponseException {
for (Channel channel : getThing().getChannelsOfGroup(CHANNEL_GROUP_FORECAST)) {
ChannelUID channelUID = channel.getUID();
if (CHANNEL_TIME.equals(channelUID.getIdWithoutGroup())) {
// All parameters and locations should share the same timestamps. We use temperature to figure out
// timestamp for the group of channels
final String field = ForecastRequest.PARAM_TEMPERATURE;
Data data = unwrap(response.getData(location, field),
"Field %s not present for location %s in response. Bug?", field, location);
updateEpochSecondStateIfLinked(channelUID, data.timestampsEpochSecs[0]);
continue;
}
String field = getDataField(channelUID);
Unit<?> unit = getUnit(channelUID);
if (field == null) {
logger.error("Channel {} not handled. Bug?", channelUID.getId());
continue;
}
Data data = unwrap(response.getData(location, field),
"Field %s not present for location %s in response. Bug?", field, location);
if (data.values.length != data.timestampsEpochSecs.length) {
logger.warn("Number of values ({}) doesn't match number of timestamps ({})", data.values.length,
data.timestampsEpochSecs.length);
continue;
}
updateStateIfLinked(channelUID, data.values[0], unit);
TimeSeries timeSeries = new TimeSeries(REPLACE);
for (int i = 0; i < data.values.length; i++) {
timeSeries.add(Instant.ofEpochSecond(data.timestampsEpochSecs[i]), getState(data.values[i], unit));
}
sendTimeSeries(channelUID, timeSeries);
}
}
private static int getHours(ChannelUID uid) {
String groupId = uid.getGroupId();
if (groupId == null) {

View File

@ -7,6 +7,8 @@ addon.fmiweather.description = This is the binding for Finnish Meteorological In
thing-type.fmiweather.forecast.label = FMI Weather Forecast
thing-type.fmiweather.forecast.description = Finnish Meteorological Institute (FMI) weather forecast
thing-type.fmiweather.forecast.group.forecast.label = Forecast
thing-type.fmiweather.forecast.group.forecast.description = This is the weather forecast
thing-type.fmiweather.forecast.group.forecastHours01.label = 1 Hours Forecast
thing-type.fmiweather.forecast.group.forecastHours01.description = This is the weather forecast in 1 hours.
thing-type.fmiweather.forecast.group.forecastHours02.label = 2 Hours Forecast

View File

@ -28,6 +28,10 @@
<description>Finnish Meteorological Institute (FMI) weather forecast</description>
<channel-groups>
<channel-group id="forecast" typeId="group-forecast">
<label>Forecast</label>
<description>This is the weather forecast</description>
</channel-group>
<channel-group id="forecastNow" typeId="group-forecast">
<label>Forecast for the Current Time</label>
<description>This is the weather forecast for the current time</description>
@ -234,6 +238,10 @@
</channel-group>
</channel-groups>
<properties>
<property name="thingTypeVersion">1</property>
</properties>
<config-description>
<parameter name="location" type="text" required="true">
<label>Location</label>

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<update:update-descriptions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:update="https://openhab.org/schemas/update-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/update-description/v1.0.0 https://openhab.org/schemas/update-description-1.0.0.xsd">
<thing-type uid="fmiweather:forecast">
<instruction-set targetVersion="1">
<add-channel id="time" groupIds="forecast">
<type>fmiweather:forecast-time-channel</type>
</add-channel>
<add-channel id="temperature" groupIds="forecast">
<type>fmiweather:temperature-channel</type>
</add-channel>
<add-channel id="humidity" groupIds="forecast">
<type>fmiweather:humidity-channel</type>
</add-channel>
<add-channel id="wind-direction" groupIds="forecast">
<type>fmiweather:wind-direction-channel</type>
</add-channel>
<add-channel id="wind-speed" groupIds="forecast">
<type>fmiweather:wind-speed-channel</type>
</add-channel>
<add-channel id="wind-gust" groupIds="forecast">
<type>fmiweather:wind-gust-channel</type>
</add-channel>
<add-channel id="pressure" groupIds="forecast">
<type>fmiweather:pressure-channel</type>
</add-channel>
<add-channel id="precipitation-intensity" groupIds="forecast">
<type>fmiweather:precipitation-intensity-channel</type>
</add-channel>
<add-channel id="total-cloud-cover" groupIds="forecast">
<type>fmiweather:total-cloud-cover-channel</type>
</add-channel>
<add-channel id="weather-id" groupIds="forecast">
<type>fmiweather:weather-id-channel</type>
</add-channel>
</instruction-set>
</thing-type>
</update:update-descriptions>