[solarforecast] Add manual update feature (#17335)

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
pull/17403/head
Bernd Weymann 2024-09-09 08:14:12 +02:00 committed by GitHub
parent 4f5cb48e13
commit 8baf9f1efb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 163 additions and 18 deletions

View File

@ -55,16 +55,23 @@ See [DateTime](#date-time) section for more information.
### Solcast Plane Configuration ### Solcast Plane Configuration
| Name | Type | Description | Default | Required | Advanced | | Name | Type | Description | Default | Required | Advanced |
|-----------------|---------|--------------------------------------------------------|-----------------|----------|----------| |-----------------|---------|--------------------------------------------------------------------------|-----------------|----------|----------|
| resourceId | text | Resource Id of Solcast rooftop site | N/A | yes | no | | resourceId | text | Resource Id of Solcast rooftop site | N/A | yes | no |
| refreshInterval | integer | Forecast Refresh Interval in minutes | 120 | 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) `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. `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. 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 ## Solcast Channels
Each `sc-plane` reports its own values including a `json` channel holding JSON content. 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) logInfo("SF Tests","Optimist energy {}",energyOptimistic)
end 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
```

View File

@ -85,6 +85,11 @@ public interface SolarForecast {
*/ */
Instant getForecastEnd(); Instant getForecastEnd();
/**
* Forces update in the next scheduling cycle
*/
void triggerUpdate();
/** /**
* Get TimeSeries for Power forecast * Get TimeSeries for Power forecast
* *

View 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) { public static State getDay(ThingActions actions, LocalDate ld, String... args) {
return ((SolarForecastActions) actions).getDay(ld, args); return ((SolarForecastActions) actions).getDay(ld, args);
} }
@ -180,6 +192,10 @@ public class SolarForecastActions implements ThingActions {
return ((SolarForecastActions) actions).getForecastEnd(); return ((SolarForecastActions) actions).getForecastEnd();
} }
public static void triggerUpdate(ThingActions actions) {
((SolarForecastActions) actions).triggerUpdate();
}
@Override @Override
public void setThingHandler(ThingHandler handler) { public void setThingHandler(ThingHandler handler) {
thingHandler = Optional.of(handler); thingHandler = Optional.of(handler);

View File

@ -327,6 +327,11 @@ public class ForecastSolarObject implements SolarForecast {
return zdt.toInstant(); return zdt.toInstant();
} }
@Override
public void triggerUpdate() {
expirationDateTime = Instant.MIN;
}
private void throwOutOfRangeException(Instant query) { private void throwOutOfRangeException(Instant query) {
if (getForecastBegin().equals(Instant.MAX) || getForecastEnd().equals(Instant.MIN)) { if (getForecastBegin().equals(Instant.MAX) || getForecastEnd().equals(Instant.MIN)) {
throw new SolarForecastException(this, "Forecast invalid time range"); throw new SolarForecastException(this, "Forecast invalid time range");

View 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 // invalid forecast object
identifier = id; identifier = id;
timeZoneProvider = tzp; timeZoneProvider = tzp;
dateOutputFormatter = DateTimeFormatter.ofPattern(SolarForecastBindingConstants.PATTERN_FORMAT) dateOutputFormatter = DateTimeFormatter.ofPattern(SolarForecastBindingConstants.PATTERN_FORMAT)
.withZone(tzp.getTimeZone()); .withZone(tzp.getTimeZone());
expirationDateTime = Instant.now().minusSeconds(1); expirationDateTime = expiration;
} }
public SolcastObject(String id, String content, Instant expiration, TimeZoneProvider tzp) { public SolcastObject(String id, String content, Instant expiration, TimeZoneProvider tzp) {
@ -458,6 +458,11 @@ public class SolcastObject implements SolarForecast {
return Instant.MIN; return Instant.MIN;
} }
@Override
public void triggerUpdate() {
expirationDateTime = Instant.MIN;
}
private QueryMode evalArguments(String[] args) { private QueryMode evalArguments(String[] args) {
if (args.length > 0) { if (args.length > 0) {
if (args.length > 1) { if (args.length > 1) {
@ -501,7 +506,11 @@ public class SolcastObject implements SolarForecast {
} }
private String getTimeRange() { private String getTimeRange() {
return "Valid range: " + dateOutputFormatter.format(getForecastBegin()) + " - " if (getForecastBegin().isBefore(Instant.MAX) && getForecastEnd().isAfter(Instant.MIN)) {
+ dateOutputFormatter.format(getForecastEnd()); return "Valid range: " + dateOutputFormatter.format(getForecastBegin()) + " - "
+ dateOutputFormatter.format(getForecastEnd());
} else {
return "Invalid time range";
}
} }
} }

View File

@ -84,7 +84,9 @@ public class SolcastPlaneHandler extends BaseThingHandler implements SolarForeca
if (handler != null) { if (handler != null) {
if (handler instanceof SolcastBridgeHandler sbh) { if (handler instanceof SolcastBridgeHandler sbh) {
bridgeHandler = Optional.of(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); sbh.addPlane(this);
} else { } else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
@ -159,9 +161,10 @@ public class SolcastPlaneHandler extends BaseThingHandler implements SolarForeca
estimateRequest.header(HttpHeader.AUTHORIZATION, BEARER + bridge.getApiKey()); estimateRequest.header(HttpHeader.AUTHORIZATION, BEARER + bridge.getApiKey());
ContentResponse crEstimate = estimateRequest.send(); ContentResponse crEstimate = estimateRequest.send();
if (crEstimate.getStatus() == 200) { 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(), SolcastObject localForecast = new SolcastObject(thing.getUID().getAsString(),
crEstimate.getContentAsString(), crEstimate.getContentAsString(), expiration, bridge);
Instant.now().plus(configuration.refreshInterval, ChronoUnit.MINUTES), bridge);
// get forecast // get forecast
Request forecastRequest = httpClient.newRequest(forecastUrl); Request forecastRequest = httpClient.newRequest(forecastUrl);

View File

@ -9,9 +9,9 @@
<label>Rooftop Resource Id</label> <label>Rooftop Resource Id</label>
<description>Resource Id of Solcast rooftop site</description> <description>Resource Id of Solcast rooftop site</description>
</parameter> </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> <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> <default>120</default>
</parameter> </parameter>
</config-description> </config-description>

View File

@ -6,11 +6,11 @@ addon.solarforecast.description = Solar Forecast for your location
# thing types # thing types
thing-type.solarforecast.fs-plane.label = ForecastSolar PV Plane 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.label = ForecastSolar Site
thing-type.solarforecast.fs-site.description = Site location for Forecast Solar 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.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.label = Solcast Site
thing-type.solarforecast.sc-site.description = Solcast service site definition 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.label = Inverter Kilowatt Peak
thing-type.config.solarforecast.fs-site.inverterKwp.description = Inverter maximum kilowatt peak capability 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.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.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.label = Rooftop Resource Id
thing-type.config.solarforecast.sc-plane.resourceId.description = Resource Id of Solcast rooftop site thing-type.config.solarforecast.sc-plane.resourceId.description = Resource Id of Solcast rooftop site
thing-type.config.solarforecast.sc-site.apiKey.label = API Key thing-type.config.solarforecast.sc-site.apiKey.label = API Key
@ -107,3 +107,5 @@ actionForecastBeginLabel = Forecast Startpoint
actionForecastBeginDesc = Returns earliest timestamp of forecast data actionForecastBeginDesc = Returns earliest timestamp of forecast data
actionForecastEndLabel = Forecast End actionForecastEndLabel = Forecast End
actionForecastEndDesc = Returns latest timestamp of forecast data actionForecastEndDesc = Returns latest timestamp of forecast data
actionTriggerUpdateLabel = Trigger Forecast Update
actionTriggerUpdateDesc = Triggers manual update of forecast data

View File

@ -22,8 +22,10 @@ import java.time.ZoneId;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map;
import javax.measure.quantity.Energy; import javax.measure.quantity.Energy;
@ -503,7 +505,7 @@ class SolcastTest {
@Test @Test
void testTimes() { void testTimes() {
String utcTimeString = "2022-07-17T19:30:00.0000000Z"; 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); ZonedDateTime zdt = so.getZdtFromUTC(utcTimeString);
assertNotNull(zdt); assertNotNull(zdt);
assertEquals("2022-07-17T21:30+02:00[Europe/Berlin]", zdt.toString(), "ZonedDateTime"); assertEquals("2022-07-17T21:30+02:00[Europe/Berlin]", zdt.toString(), "ZonedDateTime");
@ -676,6 +678,72 @@ class SolcastTest {
scph2.dispose(); 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 @Test
void testCombinedEnergyTimeSeries() { void testCombinedEnergyTimeSeries() {
setFixedTimeJul18(); setFixedTimeJul18();