]> git.basschouten.com Git - openhab-addons.git/commitdiff
[solarforecast] Add manual update feature (#17335)
authorBernd Weymann <bernd.weymann@gmail.com>
Mon, 9 Sep 2024 06:14:12 +0000 (08:14 +0200)
committerGitHub <noreply@github.com>
Mon, 9 Sep 2024 06:14:12 +0000 (08:14 +0200)
Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
bundles/org.openhab.binding.solarforecast/README.md
bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/actions/SolarForecast.java
bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/actions/SolarForecastActions.java
bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarObject.java
bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastObject.java
bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/handler/SolcastPlaneHandler.java
bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/sc-plane-config.xml
bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/i18n/solarforecast.properties
bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/SolcastTest.java

index be93564f39f164e6ee1fbac7212b46cd42e99486..72953200712a561edd0712057b03ffad3b4235e8 100644 (file)
@@ -55,16 +55,23 @@ See [DateTime](#date-time) section for more information.
 
 ### Solcast Plane Configuration
 
-| Name            | Type    | Description                                            | Default         | Required | Advanced |
-|-----------------|---------|--------------------------------------------------------|-----------------|----------|----------|
-| resourceId      | text    | Resource Id of Solcast rooftop site                    | N/A             | yes      | no       |
-| refreshInterval | integer | Forecast Refresh Interval in minutes                   | 120             | yes      | no       |
+| Name            | Type    | Description                                                              | Default         | Required | Advanced |
+|-----------------|---------|--------------------------------------------------------------------------|-----------------|----------|----------|
+| resourceId      | text    | Resource Id of Solcast rooftop site                                      | N/A             | yes      | no       |
+| refreshInterval | integer | Forecast Refresh Interval in minutes (0 = disable automatic refresh)     | 120             | yes      | no       |
 
 `resourceId` for each plane can be obtained in your [Rooftop Sites](https://toolkit.solcast.com.au/rooftop-sites)
 
 `refreshInterval` of forecast data needs to respect the throttling of the Solcast service. 
 If you have 25 free calls per day, each plane needs 2 calls per update a refresh interval of 120 minutes will result in 24 calls per day.
 
+With `refreshInterval = 0` the forecast data will not be updated by binding.
+This gives the user the possibility to define an own update strategy in rules.
+See [manual update rule example](#solcast-manual-update) to update Solcast forecast data 
+
+- after startup
+- every 2 hours only during daytime using [Astro Binding](https://www.openhab.org/addons/bindings/astro/)  
+
 ## Solcast Channels
 
 Each `sc-plane` reports its own values including a `json` channel holding JSON content.
@@ -354,3 +361,33 @@ rule "Solcast Actions"
         logInfo("SF Tests","Optimist energy {}",energyOptimistic)
 end
 ```
+
+### Solcast manual update
+
+```java
+rule "Daylight End"
+    when
+        Channel "astro:sun:local:daylight#event" triggered END
+    then
+        PV_Daytime.postUpdate(OFF) // switch item holding daytime state        
+end
+
+rule "Daylight Start"
+    when
+        Channel "astro:sun:local:daylight#event" triggered START
+    then
+        PV_Daytime.postUpdate(ON)           
+end
+
+rule "Solacast Updates"
+    when 
+        Thing "solarforecast:sc-plane:homeSouthWest" changed to INITIALIZING or // Thing status changed to INITIALIZING
+        Time cron "0 30 0/2 ? * * *" // every 2 hours at minute 30 
+    then
+        if(PV_Daytime.state == ON) {
+            val solarforecastActions = getActions("solarforecast","solarforecast:sc-plane:homeSouthWest")
+            solarforecastActions.triggerUpdate
+        } // reject updates during night
+end
+```
+
index b6d37bb26971c12b3cd11fa69b2b81fba451ea7f..7597802a07429c4c8a3af009e77a09d5ac199a17 100644 (file)
@@ -85,6 +85,11 @@ public interface SolarForecast {
      */
     Instant getForecastEnd();
 
+    /**
+     * Forces update in the next scheduling cycle
+     */
+    void triggerUpdate();
+
     /**
      * Get TimeSeries for Power forecast
      *
index c794ebb1c3d035a2ba51e7e41e7c48c2b6d394a9..43ca9be321a6880cced0016bd10c6f50cfde36f9 100644 (file)
@@ -160,6 +160,18 @@ public class SolarForecastActions implements ThingActions {
         }
     }
 
+    @RuleAction(label = "@text/actionTriggerUpdateLabel", description = "@text/actionTriggerUpdateDesc")
+    public void triggerUpdate() {
+        if (thingHandler.isPresent()) {
+            List<SolarForecast> forecastObjectList = ((SolarForecastProvider) thingHandler.get()).getSolarForecasts();
+            forecastObjectList.forEach(forecast -> {
+                forecast.triggerUpdate();
+            });
+        } else {
+            logger.trace("Handler missing");
+        }
+    }
+
     public static State getDay(ThingActions actions, LocalDate ld, String... args) {
         return ((SolarForecastActions) actions).getDay(ld, args);
     }
@@ -180,6 +192,10 @@ public class SolarForecastActions implements ThingActions {
         return ((SolarForecastActions) actions).getForecastEnd();
     }
 
+    public static void triggerUpdate(ThingActions actions) {
+        ((SolarForecastActions) actions).triggerUpdate();
+    }
+
     @Override
     public void setThingHandler(ThingHandler handler) {
         thingHandler = Optional.of(handler);
index fa0f79cca56fea4990c2fc7b6381a4ea3a7f01aa..025cd636af8ad55320c4b0f84282759fdcb424fe 100644 (file)
@@ -327,6 +327,11 @@ public class ForecastSolarObject implements SolarForecast {
         return zdt.toInstant();
     }
 
+    @Override
+    public void triggerUpdate() {
+        expirationDateTime = Instant.MIN;
+    }
+
     private void throwOutOfRangeException(Instant query) {
         if (getForecastBegin().equals(Instant.MAX) || getForecastEnd().equals(Instant.MIN)) {
             throw new SolarForecastException(this, "Forecast invalid time range");
index 1055e6e6b1ad7a8ab2039a4556c07624da53c474..4f6f91ff8bc64d1932b4f558c481352ab1fb09ff 100644 (file)
@@ -82,13 +82,13 @@ public class SolcastObject implements SolarForecast {
         }
     }
 
-    public SolcastObject(String id, TimeZoneProvider tzp) {
+    public SolcastObject(String id, Instant expiration, TimeZoneProvider tzp) {
         // invalid forecast object
         identifier = id;
         timeZoneProvider = tzp;
         dateOutputFormatter = DateTimeFormatter.ofPattern(SolarForecastBindingConstants.PATTERN_FORMAT)
                 .withZone(tzp.getTimeZone());
-        expirationDateTime = Instant.now().minusSeconds(1);
+        expirationDateTime = expiration;
     }
 
     public SolcastObject(String id, String content, Instant expiration, TimeZoneProvider tzp) {
@@ -458,6 +458,11 @@ public class SolcastObject implements SolarForecast {
         return Instant.MIN;
     }
 
+    @Override
+    public void triggerUpdate() {
+        expirationDateTime = Instant.MIN;
+    }
+
     private QueryMode evalArguments(String[] args) {
         if (args.length > 0) {
             if (args.length > 1) {
@@ -501,7 +506,11 @@ public class SolcastObject implements SolarForecast {
     }
 
     private String getTimeRange() {
-        return "Valid range: " + dateOutputFormatter.format(getForecastBegin()) + " - "
-                + dateOutputFormatter.format(getForecastEnd());
+        if (getForecastBegin().isBefore(Instant.MAX) && getForecastEnd().isAfter(Instant.MIN)) {
+            return "Valid range: " + dateOutputFormatter.format(getForecastBegin()) + " - "
+                    + dateOutputFormatter.format(getForecastEnd());
+        } else {
+            return "Invalid time range";
+        }
     }
 }
index 89c46564cfacf5e83466fe36a30a3cceb4973ee8..e160d16a6d1c2b584547b7c893d50c174bc646a0 100644 (file)
@@ -84,7 +84,9 @@ public class SolcastPlaneHandler extends BaseThingHandler implements SolarForeca
             if (handler != null) {
                 if (handler instanceof SolcastBridgeHandler sbh) {
                     bridgeHandler = Optional.of(sbh);
-                    forecast = Optional.of(new SolcastObject(thing.getUID().getAsString(), sbh));
+                    Instant expiration = (configuration.refreshInterval == 0) ? Instant.MAX
+                            : Instant.now().minusSeconds(1);
+                    forecast = Optional.of(new SolcastObject(thing.getUID().getAsString(), expiration, sbh));
                     sbh.addPlane(this);
                 } else {
                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
@@ -159,9 +161,10 @@ public class SolcastPlaneHandler extends BaseThingHandler implements SolarForeca
                         estimateRequest.header(HttpHeader.AUTHORIZATION, BEARER + bridge.getApiKey());
                         ContentResponse crEstimate = estimateRequest.send();
                         if (crEstimate.getStatus() == 200) {
+                            Instant expiration = (configuration.refreshInterval == 0) ? Instant.MAX
+                                    : Instant.now().plus(configuration.refreshInterval, ChronoUnit.MINUTES);
                             SolcastObject localForecast = new SolcastObject(thing.getUID().getAsString(),
-                                    crEstimate.getContentAsString(),
-                                    Instant.now().plus(configuration.refreshInterval, ChronoUnit.MINUTES), bridge);
+                                    crEstimate.getContentAsString(), expiration, bridge);
 
                             // get forecast
                             Request forecastRequest = httpClient.newRequest(forecastUrl);
index d17426c82e4bea976c37e4a4d75092e2220472fb..cc0fc6dee046c2cce267cac1eb37aafbeb5a2b7c 100644 (file)
@@ -9,9 +9,9 @@
                        <label>Rooftop Resource Id</label>
                        <description>Resource Id of Solcast rooftop site</description>
                </parameter>
-               <parameter name="refreshInterval" type="integer" min="1" unit="min" required="true">
+               <parameter name="refreshInterval" type="integer" min="0" unit="min" required="true">
                        <label>Forecast Refresh Interval</label>
-                       <description>Data refresh rate of forecast data in minutes</description>
+                       <description>Data refresh rate of forecast data in minutes, zero for manual updates.</description>
                        <default>120</default>
                </parameter>
        </config-description>
index 36ef6e7ac14b1a362a5b2bfe90dbe5be19833943..94f6e1aa979e5e9bed6c5ce817e1d7e584ddf6b6 100644 (file)
@@ -6,11 +6,11 @@ addon.solarforecast.description = Solar Forecast for your location
 # thing types
 
 thing-type.solarforecast.fs-plane.label = ForecastSolar PV Plane
-thing-type.solarforecast.fs-plane.description = PV Plane as part of Multi Plane Bridge
+thing-type.solarforecast.fs-plane.description = One PV Plane of Multi Plane Bridge
 thing-type.solarforecast.fs-site.label = ForecastSolar Site
 thing-type.solarforecast.fs-site.description = Site location for Forecast Solar
 thing-type.solarforecast.sc-plane.label = Solcast PV Plane
-thing-type.solarforecast.sc-plane.description = PV Plane as part of Multi Plane Bridge
+thing-type.solarforecast.sc-plane.description = One PV Plane of Multi Plane Bridge
 thing-type.solarforecast.sc-site.label = Solcast Site
 thing-type.solarforecast.sc-site.description = Solcast service site definition
 
@@ -35,9 +35,9 @@ thing-type.config.solarforecast.fs-site.apiKey.description = If you have a paid
 thing-type.config.solarforecast.fs-site.inverterKwp.label = Inverter Kilowatt Peak
 thing-type.config.solarforecast.fs-site.inverterKwp.description = Inverter maximum kilowatt peak capability
 thing-type.config.solarforecast.fs-site.location.label = PV Location
-thing-type.config.solarforecast.fs-site.location.description = Location of photovoltaic system
+thing-type.config.solarforecast.fs-site.location.description = Location of photovoltaic system. Location from openHAB settings is used in case of empty value.
 thing-type.config.solarforecast.sc-plane.refreshInterval.label = Forecast Refresh Interval
-thing-type.config.solarforecast.sc-plane.refreshInterval.description = Data refresh rate of forecast data in minutes
+thing-type.config.solarforecast.sc-plane.refreshInterval.description = Data refresh rate of forecast data in minutes, zero for manual updates.
 thing-type.config.solarforecast.sc-plane.resourceId.label = Rooftop Resource Id
 thing-type.config.solarforecast.sc-plane.resourceId.description = Resource Id of Solcast rooftop site
 thing-type.config.solarforecast.sc-site.apiKey.label = API Key
@@ -107,3 +107,5 @@ actionForecastBeginLabel = Forecast Startpoint
 actionForecastBeginDesc = Returns earliest timestamp of forecast data
 actionForecastEndLabel = Forecast End
 actionForecastEndDesc = Returns latest timestamp of forecast data
+actionTriggerUpdateLabel = Trigger Forecast Update
+actionTriggerUpdateDesc = Triggers manual update of forecast data
index 01763ae3e967b7e13714cc00662ce8befde4e4c8..16d9a88c069558dc3b0977222160b0b7bc2f4d7b 100644 (file)
@@ -22,8 +22,10 @@ import java.time.ZoneId;
 import java.time.ZonedDateTime;
 import java.time.temporal.ChronoUnit;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Map;
 
 import javax.measure.quantity.Energy;
 
@@ -503,7 +505,7 @@ class SolcastTest {
     @Test
     void testTimes() {
         String utcTimeString = "2022-07-17T19:30:00.0000000Z";
-        SolcastObject so = new SolcastObject("sc-test", TIMEZONEPROVIDER);
+        SolcastObject so = new SolcastObject("sc-test", Instant.now(), TIMEZONEPROVIDER);
         ZonedDateTime zdt = so.getZdtFromUTC(utcTimeString);
         assertNotNull(zdt);
         assertEquals("2022-07-17T21:30+02:00[Europe/Berlin]", zdt.toString(), "ZonedDateTime");
@@ -676,6 +678,72 @@ class SolcastTest {
         scph2.dispose();
     }
 
+    @Test
+    void testRefreshManual() {
+        Map<String, Object> manualConfiguration = new HashMap<>();
+        manualConfiguration.put("refreshInterval", 0);
+
+        BridgeImpl bi = new BridgeImpl(SolarForecastBindingConstants.SOLCAST_SITE, "bridge");
+        SolcastBridgeHandler scbh = new SolcastBridgeHandler(bi, new TimeZP());
+        bi.setHandler(scbh);
+        CallbackMock cm = new CallbackMock();
+        scbh.setCallback(cm);
+        SolcastPlaneHandler scph1 = new SolcastPlaneMock(bi);
+        CallbackMock cm1 = new CallbackMock();
+        scph1.setCallback(cm1);
+        scph1.handleConfigurationUpdate(manualConfiguration);
+        scph1.initialize();
+        scbh.getData();
+        // no update shall happen
+        assertEquals(Instant.MAX, scbh.getSolarForecasts().get(0).getForecastBegin(), "Bridge forecast begin");
+        assertEquals(Instant.MIN, scbh.getSolarForecasts().get(0).getForecastEnd(), "Bridge forecast begin");
+        assertEquals(Instant.MAX, scph1.getSolarForecasts().get(0).getForecastBegin(), "Plane 1 forecast begin");
+        assertEquals(Instant.MIN, scph1.getSolarForecasts().get(0).getForecastEnd(), "Plane 1 forecast begin");
+
+        SolcastPlaneHandler scph2 = new SolcastPlaneMock(bi);
+        CallbackMock cm2 = new CallbackMock();
+        scph2.setCallback(cm2);
+        scph2.handleConfigurationUpdate(manualConfiguration);
+        scph2.initialize();
+        scbh.getData();
+        assertEquals(Instant.MAX, scbh.getSolarForecasts().get(0).getForecastBegin(), "Bridge forecast begin");
+        assertEquals(Instant.MIN, scbh.getSolarForecasts().get(0).getForecastEnd(), "Bridge forecast begin");
+        assertEquals(Instant.MAX, scbh.getSolarForecasts().get(1).getForecastBegin(), "Bridge forecast begin");
+        assertEquals(Instant.MIN, scbh.getSolarForecasts().get(1).getForecastEnd(), "Bridge forecast begin");
+        assertEquals(Instant.MAX, scph1.getSolarForecasts().get(0).getForecastBegin(), "Plane 1 forecast begin");
+        assertEquals(Instant.MIN, scph1.getSolarForecasts().get(0).getForecastEnd(), "Plane 1 forecast begin");
+        assertEquals(Instant.MAX, scph2.getSolarForecasts().get(0).getForecastBegin(), "Plane 2 forecast begin");
+        assertEquals(Instant.MIN, scph2.getSolarForecasts().get(0).getForecastEnd(), "Plane 2 forecast begin");
+
+        manualConfiguration.put("refreshInterval", 5);
+        scph1.handleConfigurationUpdate(manualConfiguration);
+        scph1.initialize();
+        scph2.handleConfigurationUpdate(manualConfiguration);
+        scph2.initialize();
+        scbh.getData();
+
+        assertEquals(Instant.parse("2022-07-17T21:30:00Z"), scbh.getSolarForecasts().get(0).getForecastBegin(),
+                "Bridge forecast begin");
+        assertEquals(Instant.parse("2022-07-24T21:00:00Z"), scbh.getSolarForecasts().get(0).getForecastEnd(),
+                "Bridge forecast begin");
+        assertEquals(Instant.parse("2022-07-17T21:30:00Z"), scbh.getSolarForecasts().get(1).getForecastBegin(),
+                "Bridge forecast begin");
+        assertEquals(Instant.parse("2022-07-24T21:00:00Z"), scbh.getSolarForecasts().get(1).getForecastEnd(),
+                "Bridge forecast begin");
+        assertEquals(Instant.parse("2022-07-17T21:30:00Z"), scph1.getSolarForecasts().get(0).getForecastBegin(),
+                "Plane 1 forecast begin");
+        assertEquals(Instant.parse("2022-07-24T21:00:00Z"), scph1.getSolarForecasts().get(0).getForecastEnd(),
+                "Plane 1 forecast begin");
+        assertEquals(Instant.parse("2022-07-17T21:30:00Z"), scph2.getSolarForecasts().get(0).getForecastBegin(),
+                "Plane 2 forecast begin");
+        assertEquals(Instant.parse("2022-07-24T21:00:00Z"), scph2.getSolarForecasts().get(0).getForecastEnd(),
+                "Plane 2 forecast begin");
+
+        scbh.dispose();
+        scph1.dispose();
+        scph2.dispose();
+    }
+
     @Test
     void testCombinedEnergyTimeSeries() {
         setFixedTimeJul18();