### 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.
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
+```
+
*/
Instant getForecastEnd();
+ /**
+ * Forces update in the next scheduling cycle
+ */
+ void triggerUpdate();
+
/**
* Get TimeSeries for Power forecast
*
}
}
+ @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);
}
return ((SolarForecastActions) actions).getForecastEnd();
}
+ public static void triggerUpdate(ThingActions actions) {
+ ((SolarForecastActions) actions).triggerUpdate();
+ }
+
@Override
public void setThingHandler(ThingHandler handler) {
thingHandler = Optional.of(handler);
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");
}
}
- 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) {
return Instant.MIN;
}
+ @Override
+ public void triggerUpdate() {
+ expirationDateTime = Instant.MIN;
+ }
+
private QueryMode evalArguments(String[] args) {
if (args.length > 0) {
if (args.length > 1) {
}
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";
+ }
}
}
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,
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);
<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>
# 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
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
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
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;
@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");
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();