]> git.basschouten.com Git - openhab-addons.git/commitdiff
Add time series support for forecasts (#17543)
authorJacob Laursen <jacob-github@vindvejr.dk>
Sun, 13 Oct 2024 05:44:25 +0000 (07:44 +0200)
committerGitHub <noreply@github.com>
Sun, 13 Oct 2024 05:44:25 +0000 (07:44 +0200)
Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
bundles/org.openhab.binding.fmiweather/README.md
bundles/org.openhab.binding.fmiweather/src/main/java/org/openhab/binding/fmiweather/internal/AbstractWeatherHandler.java
bundles/org.openhab.binding.fmiweather/src/main/java/org/openhab/binding/fmiweather/internal/BindingConstants.java
bundles/org.openhab.binding.fmiweather/src/main/java/org/openhab/binding/fmiweather/internal/ForecastWeatherHandler.java
bundles/org.openhab.binding.fmiweather/src/main/resources/OH-INF/i18n/fmiweather.properties
bundles/org.openhab.binding.fmiweather/src/main/resources/OH-INF/thing/thing-types.xml
bundles/org.openhab.binding.fmiweather/src/main/resources/OH-INF/update/instructions.xml [new file with mode: 0644]

index 0b604981a4bd160a05c9bf462b335b4b9f624d99..be0724a878459691ca974e2b9469503a072d3366 100644 (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.
index eb20e2cfc375a7881d50d14e83551054a40f3451..0aaf824e39972c88160367fc602be640095b4821 100644 (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);
         }
     }
 
index 4791d09a701c16c4369decded320e30870228dce..624089e7bb743db28e35f6dda6750df17e9fd47a 100644 (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";
index 3aee7ede9825d42c6e0aa93540b2d0199cd9e7ea..03e78ef778b06e56744fb2cb8d48a5d856beb6a6 100644 (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) {
index bcc28a52cf52dafcbfc67ec8af558db7646eb16d..f231e99d1a8655af4d17f665fb6a5b0842116783 100644 (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
index 5f7c64966f9e1e88a070f299d48eeb7a9b281532..7ce3e14f82b11ba5ea36fa07f2090a622a8a300d 100644 (file)
                <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>
                        </channel-group>
                </channel-groups>
 
+               <properties>
+                       <property name="thingTypeVersion">1</property>
+               </properties>
+
                <config-description>
                        <parameter name="location" type="text" required="true">
                                <label>Location</label>
diff --git a/bundles/org.openhab.binding.fmiweather/src/main/resources/OH-INF/update/instructions.xml b/bundles/org.openhab.binding.fmiweather/src/main/resources/OH-INF/update/instructions.xml
new file mode 100644 (file)
index 0000000..6c0e1a5
--- /dev/null
@@ -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>