mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 15:11:59 +01:00
[solarforecast] Add manual update feature (#17335)
Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>
This commit is contained in:
parent
4f5cb48e13
commit
8baf9f1efb
@ -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
|
||||
```
|
||||
|
||||
|
@ -85,6 +85,11 @@ public interface SolarForecast {
|
||||
*/
|
||||
Instant getForecastEnd();
|
||||
|
||||
/**
|
||||
* Forces update in the next scheduling cycle
|
||||
*/
|
||||
void triggerUpdate();
|
||||
|
||||
/**
|
||||
* Get TimeSeries for Power forecast
|
||||
*
|
||||
|
@ -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);
|
||||
|
@ -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");
|
||||
|
@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
|
Loading…
Reference in New Issue
Block a user