[solarforecast] Add manual update feature (#17335)

Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
Signed-off-by: Ciprian Pascu <contact@ciprianpascu.ro>
This commit is contained in:
Bernd Weymann 2024-09-09 08:14:12 +02:00 committed by Ciprian Pascu
parent ebe6327579
commit b5cc85373d
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
| 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
```

View File

@ -85,6 +85,11 @@ public interface SolarForecast {
*/
Instant getForecastEnd();
/**
* Forces update in the next scheduling cycle
*/
void triggerUpdate();
/**
* 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) {
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);

View 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");

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
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";
}
}
}

View 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);

View 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>

View 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

View 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();