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 ## Thing Configuration
### `observation` thing configuration ### `observation` Thing Configuration
| Parameter | Type | Required | Description | Example | | 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 | | `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 | | 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 and forecast things provide slightly different details on weather.
### `observation` thing channels ### `observation` Thing Channels
Observation channels are grouped in single group, `current`. 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`. 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: 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 - `forecastHours01`: Forecasted weather for 1 hours from now
- `forecastHours02`: Forecasted weather for 2 hours from now - `forecastHours02`: Forecasted weather for 2 hours from now
- etc. - etc.

View File

@ -44,6 +44,7 @@ import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseThingHandler; import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.types.Command; import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType; import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType; import org.openhab.core.types.UnDefType;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; 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) { protected void updateStateIfLinked(ChannelUID channelUID, @Nullable BigDecimal value, @Nullable Unit<?> unit) {
if (isLinked(channelUID)) { if (isLinked(channelUID)) {
if (value == null) { updateState(channelUID, getState(value, unit));
updateState(channelUID, UnDefType.UNDEF); }
} else if (unit == null) { }
updateState(channelUID, new DecimalType(value));
} else { /**
updateState(channelUID, new QuantityType<>(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 ThingTypeUID THING_TYPE_FORECAST = new ThingTypeUID(BINDING_ID, "forecast");
public static final ThingUID UID_LOCAL_FORECAST = new ThingUID(BINDING_ID, "forecast", "local"); 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 // List of all Channel ids
public static final String CHANNEL_TIME = "time"; public static final String CHANNEL_TIME = "time";
public static final String CHANNEL_TEMPERATURE = "temperature"; 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.binding.fmiweather.internal.client.ForecastRequest.*;
import static org.openhab.core.library.unit.SIUnits.CELSIUS; import static org.openhab.core.library.unit.SIUnits.CELSIUS;
import static org.openhab.core.library.unit.Units.*; import static org.openhab.core.library.unit.Units.*;
import static org.openhab.core.types.TimeSeries.Policy.REPLACE;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.Instant; 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.Thing;
import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.types.TimeSeries;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -140,29 +142,8 @@ public class ForecastWeatherHandler extends AbstractWeatherHandler {
properties.put(PROP_LATITUDE, location.latitude.toPlainString()); properties.put(PROP_LATITUDE, location.latitude.toPlainString());
properties.put(PROP_LONGITUDE, location.longitude.toPlainString()); properties.put(PROP_LONGITUDE, location.longitude.toPlainString());
updateProperties(properties); updateProperties(properties);
for (Channel channel : getThing().getChannels()) { updateHourlyChannels(response, location);
ChannelUID channelUID = channel.getUID(); updateTimeSeriesChannels(response, location);
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);
}
}
updateStatus(ThingStatus.ONLINE); updateStatus(ThingStatus.ONLINE);
} catch (FMIUnexpectedResponseException e) { } catch (FMIUnexpectedResponseException e) {
// Unexpected (possibly bug) issue with response // 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) { private static int getHours(ChannelUID uid) {
String groupId = uid.getGroupId(); String groupId = uid.getGroupId();
if (groupId == null) { 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.label = FMI Weather Forecast
thing-type.fmiweather.forecast.description = Finnish Meteorological Institute (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.label = 1 Hours Forecast
thing-type.fmiweather.forecast.group.forecastHours01.description = This is the weather forecast in 1 hours. 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 thing-type.fmiweather.forecast.group.forecastHours02.label = 2 Hours Forecast

View File

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