From b5cc85373d2c18a118e88fc7e3f2bda759d2d03c Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Mon, 9 Sep 2024 08:14:12 +0200 Subject: [PATCH] [solarforecast] Add manual update feature (#17335) Signed-off-by: Bernd Weymann Signed-off-by: Ciprian Pascu --- .../README.md | 45 ++++++++++-- .../internal/actions/SolarForecast.java | 5 ++ .../actions/SolarForecastActions.java | 16 +++++ .../forecastsolar/ForecastSolarObject.java | 5 ++ .../internal/solcast/SolcastObject.java | 17 +++-- .../solcast/handler/SolcastPlaneHandler.java | 9 ++- .../OH-INF/config/sc-plane-config.xml | 4 +- .../OH-INF/i18n/solarforecast.properties | 10 +-- .../binding/solarforecast/SolcastTest.java | 70 ++++++++++++++++++- 9 files changed, 163 insertions(+), 18 deletions(-) diff --git a/bundles/org.openhab.binding.solarforecast/README.md b/bundles/org.openhab.binding.solarforecast/README.md index be93564f39f..72953200712 100644 --- a/bundles/org.openhab.binding.solarforecast/README.md +++ b/bundles/org.openhab.binding.solarforecast/README.md @@ -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 +``` + diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/actions/SolarForecast.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/actions/SolarForecast.java index b6d37bb2697..7597802a074 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/actions/SolarForecast.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/actions/SolarForecast.java @@ -85,6 +85,11 @@ public interface SolarForecast { */ Instant getForecastEnd(); + /** + * Forces update in the next scheduling cycle + */ + void triggerUpdate(); + /** * Get TimeSeries for Power forecast * diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/actions/SolarForecastActions.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/actions/SolarForecastActions.java index c794ebb1c3d..43ca9be321a 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/actions/SolarForecastActions.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/actions/SolarForecastActions.java @@ -160,6 +160,18 @@ public class SolarForecastActions implements ThingActions { } } + @RuleAction(label = "@text/actionTriggerUpdateLabel", description = "@text/actionTriggerUpdateDesc") + public void triggerUpdate() { + if (thingHandler.isPresent()) { + List 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); diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarObject.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarObject.java index fa0f79cca56..025cd636af8 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarObject.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/forecastsolar/ForecastSolarObject.java @@ -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"); diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastObject.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastObject.java index 1055e6e6b1a..4f6f91ff8bc 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastObject.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/SolcastObject.java @@ -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"; + } } } diff --git a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/handler/SolcastPlaneHandler.java b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/handler/SolcastPlaneHandler.java index 89c46564cfa..e160d16a6d1 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/handler/SolcastPlaneHandler.java +++ b/bundles/org.openhab.binding.solarforecast/src/main/java/org/openhab/binding/solarforecast/internal/solcast/handler/SolcastPlaneHandler.java @@ -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); diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/sc-plane-config.xml b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/sc-plane-config.xml index d17426c82e4..cc0fc6dee04 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/sc-plane-config.xml +++ b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/config/sc-plane-config.xml @@ -9,9 +9,9 @@ Resource Id of Solcast rooftop site - + - Data refresh rate of forecast data in minutes + Data refresh rate of forecast data in minutes, zero for manual updates. 120 diff --git a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/i18n/solarforecast.properties b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/i18n/solarforecast.properties index 36ef6e7ac14..94f6e1aa979 100644 --- a/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/i18n/solarforecast.properties +++ b/bundles/org.openhab.binding.solarforecast/src/main/resources/OH-INF/i18n/solarforecast.properties @@ -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 diff --git a/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/SolcastTest.java b/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/SolcastTest.java index 01763ae3e96..16d9a88c069 100644 --- a/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/SolcastTest.java +++ b/bundles/org.openhab.binding.solarforecast/src/test/java/org/openhab/binding/solarforecast/SolcastTest.java @@ -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 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();