[amberelectric] Add support for forecasts, add site data to Thing properties (#18716)
* Initial support for TimeSeries - only for current pricing, limited to next 10 periods Signed-off-by: Paul Smedley <paul@smedley.id.au>pull/18623/head
parent
64ade8912c
commit
b2ee5f2b98
|
@ -1,6 +1,6 @@
|
|||
# Amber Electric Binding
|
||||
|
||||
A binding that supports the Australian energy retailer Amber's API (<https://www.amber.com.au/>) and provides data on the current pricing for buying and selling power, as well as the current level of renewables in the NEM.
|
||||
A binding that supports the Australian energy retailer Amber's API (<https://www.amber.com.au/>) and provides data on the current pricing for buying and selling power, as well as the current level of renewables in the Australian National Electricity Market (NEM).
|
||||
|
||||
## Supported Things
|
||||
|
||||
|
@ -12,11 +12,12 @@ The binding does not support auto discovery.
|
|||
|
||||
## Thing Configuration
|
||||
|
||||
As a minimum, the IP address is needed:
|
||||
|
||||
- `apiKey` - The API key from the 'Developer' section of <https://apps.amber.com.au>
|
||||
- 'nmi' optional - the NMI for your property. Required if you have multiple properties with Amber
|
||||
- 'refresh' the refresh rate for querying the API.
|
||||
As a minimum, the apiKey is needed:
|
||||
| Thing Parameter | Default Value | Required | Advanced | Description |
|
||||
|-----------------|---------------|----------|----------|-------------------------------------------------------------------------------------------------------|
|
||||
| apiKey | N/A | Yes | No | The API key from the 'Developer' section of <https://apps.amber.com.au> |
|
||||
| nmi | N/A | No | No | The NMI (NMI (National Metering Identifier) for your property. Required if you have multiple accounts |
|
||||
| refresh | 60 | No | Yes | The refresh rate (in seconds) for querying the API. |
|
||||
|
||||
## Channels
|
||||
|
||||
|
@ -29,7 +30,7 @@ As a minimum, the IP address is needed:
|
|||
| controlled-load-status | String | Current price status of controlled load import |
|
||||
| feed-in-status | String | Current price status of Feed-In |
|
||||
| nem-time | String | NEM time of last pricing update |
|
||||
| renewables | Number:Dimensionless | Current level of renewables in the grid |
|
||||
| renewables | Number:Dimensionless | Current level of renewables in the NEM |
|
||||
| spike | Switch | Report if the grid has a current price spike |
|
||||
|
||||
## Full Example
|
||||
|
|
|
@ -24,4 +24,5 @@ public class AmberElectricConfiguration {
|
|||
public String apiKey = "";
|
||||
public String nmi = "";
|
||||
public long refresh = 60;
|
||||
public long forecasts = 288;
|
||||
}
|
||||
|
|
|
@ -12,7 +12,11 @@
|
|||
*/
|
||||
package org.openhab.binding.amberelectric.internal;
|
||||
|
||||
import static org.openhab.core.types.TimeSeries.Policy.REPLACE;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
@ -35,9 +39,15 @@ import org.openhab.core.thing.ThingStatus;
|
|||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.binding.BaseThingHandler;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.core.types.TimeSeries;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
/**
|
||||
* The {@link AmberElectricHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels.
|
||||
|
@ -58,6 +68,8 @@ public class AmberElectricHandler extends BaseThingHandler {
|
|||
private @NonNullByDefault({}) AmberElectricWebTargets webTargets;
|
||||
private @Nullable ScheduledFuture<?> pollFuture;
|
||||
|
||||
private Gson gson = new Gson();
|
||||
|
||||
public AmberElectricHandler(Thing thing) {
|
||||
super(thing);
|
||||
}
|
||||
|
@ -117,49 +129,100 @@ public class AmberElectricHandler extends BaseThingHandler {
|
|||
}
|
||||
}
|
||||
|
||||
private State convertPriceToState(double price) {
|
||||
final String electricityUnit = " AUD/kWh";
|
||||
Unit<?> unit = CurrencyUnits.getInstance().getUnit("AUD");
|
||||
return (unit == null) ? new DecimalType(price / 100) : new QuantityType<>(price / 100 + " " + electricityUnit);
|
||||
}
|
||||
|
||||
private void pollStatus() throws IOException {
|
||||
try {
|
||||
if (siteID.isEmpty()) {
|
||||
Sites sites = webTargets.getSites(apiKey, nmi);
|
||||
// add error handling
|
||||
siteID = sites.siteid;
|
||||
Configuration configuration = editConfiguration();
|
||||
configuration.put("nmi", sites.nmi);
|
||||
updateConfiguration(configuration);
|
||||
logger.debug("Detected amber siteid is {}, for nmi {}", sites.siteid, sites.nmi);
|
||||
if ("".equals(siteID)) {
|
||||
String responseSites = webTargets.getSites(apiKey);
|
||||
logger.trace("responseSites = {}", responseSites);
|
||||
JsonArray jsonArraySites = JsonParser.parseString(responseSites).getAsJsonArray();
|
||||
Sites sites = new Sites();
|
||||
for (int i = 0; i < jsonArraySites.size(); i++) {
|
||||
sites = gson.fromJson(jsonArraySites.get(i), Sites.class);
|
||||
if (sites == null) {
|
||||
return;
|
||||
}
|
||||
if (nmi.equals(sites.nmi)) {
|
||||
siteID = sites.id;
|
||||
}
|
||||
}
|
||||
if ("".equals(nmi) || "".equals(siteID)) { // nmi not specified, or not found so we take the first
|
||||
// siteid found
|
||||
sites = gson.fromJson(jsonArraySites.get(0), Sites.class);
|
||||
if (sites == null) {
|
||||
return;
|
||||
}
|
||||
siteID = sites.id;
|
||||
nmi = sites.nmi;
|
||||
Configuration configuration = editConfiguration();
|
||||
configuration.put("nmi", nmi);
|
||||
updateConfiguration(configuration);
|
||||
}
|
||||
Map<String, String> properties = editProperties();
|
||||
properties.put("network", sites.network);
|
||||
properties.put("status", sites.status);
|
||||
properties.put("activeFrom", sites.activeFrom);
|
||||
if (sites.channels != null && sites.channels.length > 0) {
|
||||
properties.put("tariff", sites.channels[0].tariff);
|
||||
}
|
||||
properties.put("intervalLength", String.valueOf(sites.intervalLength));
|
||||
updateProperties(properties);
|
||||
logger.debug("Detected amber siteid is {}, for nmi {}", sites.id, sites.nmi);
|
||||
}
|
||||
|
||||
CurrentPrices currentPrices = webTargets.getCurrentPrices(siteID, apiKey);
|
||||
final String electricityUnit = " AUD/kWh";
|
||||
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
Unit<?> unit = CurrencyUnits.getInstance().getUnit("AUD");
|
||||
if (unit == null) {
|
||||
logger.trace("Currency AUD is unknown, falling back to DecimalType");
|
||||
updateState(AmberElectricBindingConstants.CHANNEL_ELECTRICITY_PRICE,
|
||||
new DecimalType(currentPrices.elecPerKwh / 100));
|
||||
updateState(AmberElectricBindingConstants.CHANNEL_CONTROLLED_LOAD_PRICE,
|
||||
new DecimalType(currentPrices.clPerKwh / 100));
|
||||
updateState(AmberElectricBindingConstants.CHANNEL_FEED_IN_PRICE,
|
||||
new DecimalType(currentPrices.feedInPerKwh / 100));
|
||||
} else {
|
||||
updateState(AmberElectricBindingConstants.CHANNEL_ELECTRICITY_PRICE,
|
||||
new QuantityType<>(currentPrices.elecPerKwh / 100 + " " + electricityUnit));
|
||||
updateState(AmberElectricBindingConstants.CHANNEL_CONTROLLED_LOAD_PRICE,
|
||||
new QuantityType<>(currentPrices.clPerKwh / 100 + " " + electricityUnit));
|
||||
updateState(AmberElectricBindingConstants.CHANNEL_FEED_IN_PRICE,
|
||||
new QuantityType<>(currentPrices.feedInPerKwh / 100 + " " + electricityUnit));
|
||||
|
||||
String response = webTargets.getCurrentPrices(siteID, apiKey);
|
||||
JsonArray jsonArray = JsonParser.parseString(response).getAsJsonArray();
|
||||
CurrentPrices currentPrices;
|
||||
TimeSeries elecTimeSeries = new TimeSeries(REPLACE);
|
||||
TimeSeries feedInTimeSeries = new TimeSeries(REPLACE);
|
||||
|
||||
for (int i = 0; i < jsonArray.size(); i++) {
|
||||
currentPrices = gson.fromJson(jsonArray.get(i), CurrentPrices.class);
|
||||
if (currentPrices != null) {
|
||||
Instant instantStart = Instant.parse(currentPrices.startTime);
|
||||
if ("CurrentInterval".equals(currentPrices.type) && "general".equals(currentPrices.channelType)) {
|
||||
updateState(AmberElectricBindingConstants.CHANNEL_ELECTRICITY_STATUS,
|
||||
new StringType(currentPrices.descriptor));
|
||||
updateState(AmberElectricBindingConstants.CHANNEL_NEM_TIME,
|
||||
new StringType(currentPrices.nemTime));
|
||||
updateState(AmberElectricBindingConstants.CHANNEL_RENEWABLES,
|
||||
new DecimalType(currentPrices.renewables));
|
||||
updateState(AmberElectricBindingConstants.CHANNEL_SPIKE,
|
||||
OnOffType.from(!"none".equals(currentPrices.spikeStatus)));
|
||||
updateState(AmberElectricBindingConstants.CHANNEL_ELECTRICITY_PRICE,
|
||||
convertPriceToState(currentPrices.perKwh));
|
||||
elecTimeSeries.add(instantStart, convertPriceToState(currentPrices.perKwh));
|
||||
}
|
||||
if ("ForecastInterval".equals(currentPrices.type) && "general".equals(currentPrices.channelType)) {
|
||||
elecTimeSeries.add(instantStart, convertPriceToState(currentPrices.perKwh));
|
||||
}
|
||||
if ("CurrentInterval".equals(currentPrices.type) && "feedIn".equals(currentPrices.channelType)) {
|
||||
updateState(AmberElectricBindingConstants.CHANNEL_FEED_IN_STATUS,
|
||||
new StringType(currentPrices.descriptor));
|
||||
updateState(AmberElectricBindingConstants.CHANNEL_FEED_IN_PRICE,
|
||||
convertPriceToState(-1 * currentPrices.perKwh));
|
||||
feedInTimeSeries.add(instantStart, convertPriceToState(-1 * currentPrices.perKwh));
|
||||
}
|
||||
if ("ForecastInterval".equals(currentPrices.type) && "feedIn".equals(currentPrices.channelType)) {
|
||||
feedInTimeSeries.add(instantStart, convertPriceToState(-1 * currentPrices.perKwh));
|
||||
}
|
||||
if ("CurrentInterval".equals(currentPrices.type)
|
||||
&& "controlledLoad".equals(currentPrices.channelType)) {
|
||||
updateState(AmberElectricBindingConstants.CHANNEL_CONTROLLED_LOAD_STATUS,
|
||||
new StringType(currentPrices.descriptor));
|
||||
updateState(AmberElectricBindingConstants.CHANNEL_CONTROLLED_LOAD_STATUS,
|
||||
convertPriceToState(currentPrices.perKwh));
|
||||
}
|
||||
}
|
||||
}
|
||||
updateState(AmberElectricBindingConstants.CHANNEL_CONTROLLED_LOAD_STATUS,
|
||||
new StringType(currentPrices.clStatus));
|
||||
updateState(AmberElectricBindingConstants.CHANNEL_ELECTRICITY_STATUS,
|
||||
new StringType(currentPrices.elecStatus));
|
||||
updateState(AmberElectricBindingConstants.CHANNEL_FEED_IN_STATUS,
|
||||
new StringType(currentPrices.feedInStatus));
|
||||
updateState(AmberElectricBindingConstants.CHANNEL_NEM_TIME, new StringType(currentPrices.nemTime));
|
||||
updateState(AmberElectricBindingConstants.CHANNEL_RENEWABLES, new DecimalType(currentPrices.renewables));
|
||||
updateState(AmberElectricBindingConstants.CHANNEL_SPIKE,
|
||||
OnOffType.from(!"none".equals(currentPrices.spikeStatus)));
|
||||
sendTimeSeries(AmberElectricBindingConstants.CHANNEL_ELECTRICITY_PRICE, elecTimeSeries);
|
||||
sendTimeSeries(AmberElectricBindingConstants.CHANNEL_FEED_IN_PRICE, feedInTimeSeries);
|
||||
} catch (AmberElectricCommunicationException e) {
|
||||
logger.debug("Unexpected error connecting to Amber Electric API", e);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
|
|
|
@ -18,8 +18,6 @@ import java.util.Properties;
|
|||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.amberelectric.internal.api.CurrentPrices;
|
||||
import org.openhab.binding.amberelectric.internal.api.Sites;
|
||||
import org.openhab.core.io.net.http.HttpUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
@ -39,18 +37,18 @@ public class AmberElectricWebTargets {
|
|||
public AmberElectricWebTargets() {
|
||||
}
|
||||
|
||||
public Sites getSites(String apiKey, String nmi) throws AmberElectricCommunicationException {
|
||||
public String getSites(String apiKey) throws AmberElectricCommunicationException {
|
||||
String getSitesUri = BASE_URI + "sites";
|
||||
String response = invoke("GET", getSitesUri, apiKey);
|
||||
logger.trace("Received response: \"{}\"", response);
|
||||
return Sites.parse(response, nmi);
|
||||
return response;
|
||||
}
|
||||
|
||||
public CurrentPrices getCurrentPrices(String siteid, String apiKey) throws AmberElectricCommunicationException {
|
||||
String getCurrentPricesUri = BASE_URI + "sites/" + siteid + "/prices/current";
|
||||
public String getCurrentPrices(String siteid, String apiKey) throws AmberElectricCommunicationException {
|
||||
String getCurrentPricesUri = BASE_URI + "sites/" + siteid + "/prices/current?next=288";
|
||||
String response = invoke("GET", getCurrentPricesUri, apiKey);
|
||||
logger.trace("Received response: \"{}\"", response);
|
||||
return CurrentPrices.parse(response);
|
||||
return response;
|
||||
}
|
||||
|
||||
protected Properties getHttpHeaders(String accessToken) {
|
||||
|
|
|
@ -14,10 +14,6 @@ package org.openhab.binding.amberelectric.internal.api;
|
|||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
/**
|
||||
* Container class for Current Pricing, related to amberelectric
|
||||
*
|
||||
|
@ -26,43 +22,27 @@ import com.google.gson.JsonParser;
|
|||
*/
|
||||
@NonNullByDefault
|
||||
public class CurrentPrices {
|
||||
public double elecPerKwh;
|
||||
public double clPerKwh;
|
||||
public double feedInPerKwh;
|
||||
public String elecStatus = "";
|
||||
public String clStatus = "";
|
||||
public String feedInStatus = "";
|
||||
public double renewables;
|
||||
public String spikeStatus = "";
|
||||
public String type = "";
|
||||
public String date = "";
|
||||
public int duration;
|
||||
public String startTime = "";
|
||||
public String endTime = "";
|
||||
public String nemTime = "";
|
||||
public double perKwh;
|
||||
public double renewables;
|
||||
public double spotPerKwh;
|
||||
public String channelType = "";
|
||||
public String spikeStatus = "";
|
||||
public String descriptor = "";
|
||||
public boolean estimate;
|
||||
public @NonNullByDefault({}) AdvancedPrice advancedPrice;
|
||||
|
||||
public class AdvancedPrice {
|
||||
public double low;
|
||||
public double predicted;
|
||||
public double high;
|
||||
}
|
||||
|
||||
private CurrentPrices() {
|
||||
}
|
||||
|
||||
public static CurrentPrices parse(String response) {
|
||||
/* parse json string */
|
||||
JsonArray jsonArray = JsonParser.parseString(response).getAsJsonArray();
|
||||
JsonObject jsonObject = jsonArray.get(0).getAsJsonObject();
|
||||
CurrentPrices currentprices = new CurrentPrices();
|
||||
currentprices.nemTime = jsonObject.get("nemTime").getAsString();
|
||||
currentprices.renewables = jsonObject.get("renewables").getAsDouble();
|
||||
currentprices.spikeStatus = jsonObject.get("spikeStatus").getAsString();
|
||||
for (int i = 0; i < jsonArray.size(); i++) {
|
||||
jsonObject = jsonArray.get(i).getAsJsonObject();
|
||||
if ("general".equals(jsonObject.get("channelType").getAsString())) {
|
||||
currentprices.elecPerKwh = jsonObject.get("perKwh").getAsDouble();
|
||||
currentprices.elecStatus = jsonObject.get("descriptor").getAsString();
|
||||
}
|
||||
if ("feedIn".equals(jsonObject.get("channelType").getAsString())) {
|
||||
// Multiple value from API by -1 to make the value match the app
|
||||
currentprices.feedInPerKwh = -1 * jsonObject.get("perKwh").getAsDouble();
|
||||
currentprices.feedInStatus = jsonObject.get("descriptor").getAsString();
|
||||
}
|
||||
if ("controlledLoad".equals(jsonObject.get("channelType").getAsString())) {
|
||||
currentprices.clPerKwh = jsonObject.get("perKwh").getAsDouble();
|
||||
currentprices.clStatus = jsonObject.get("descriptor").getAsString();
|
||||
}
|
||||
}
|
||||
return currentprices;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,42 +14,29 @@ package org.openhab.binding.amberelectric.internal.api;
|
|||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
/**
|
||||
* Class for holding the set of parameters used to read the controller variables.
|
||||
* Container class for Sites, related to amberelectric
|
||||
*
|
||||
* @author Paul Smedley - Initial Contribution
|
||||
*
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class Sites {
|
||||
public String siteid = "";
|
||||
public String id = "";
|
||||
public String nmi = "";
|
||||
public @NonNullByDefault({}) Channels[] channels;
|
||||
public String network = "";
|
||||
public String status = "";
|
||||
public String activeFrom = "";
|
||||
public int intervalLength;
|
||||
|
||||
private Sites() {
|
||||
public class Channels {
|
||||
public String identifier = "";
|
||||
public String type = "";
|
||||
public String tariff = "";
|
||||
}
|
||||
|
||||
public static Sites parse(String response, String nem) {
|
||||
/* parse json string */
|
||||
JsonArray jsonArray = JsonParser.parseString(response).getAsJsonArray();
|
||||
Sites sites = new Sites();
|
||||
for (int i = 0; i < jsonArray.size(); i++) {
|
||||
JsonObject jsonObject = jsonArray.get(i).getAsJsonObject();
|
||||
if (nem.equals(jsonObject.get("nmi").getAsString())) {
|
||||
sites.siteid = jsonObject.get("id").getAsString();
|
||||
sites.nmi = jsonObject.get("nmi").getAsString();
|
||||
}
|
||||
}
|
||||
if ((nem.isEmpty()) || (sites.siteid.isEmpty())) { // nem not specified, or not found so we take the first
|
||||
// siteid
|
||||
// found
|
||||
JsonObject jsonObject = jsonArray.get(0).getAsJsonObject();
|
||||
sites.siteid = jsonObject.get("id").getAsString();
|
||||
sites.nmi = jsonObject.get("nmi").getAsString();
|
||||
}
|
||||
return sites;
|
||||
public Sites() {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,8 @@ thing-type.amberelectric.service.channel.feed-in-status.description = Current pr
|
|||
|
||||
thing-type.config.amberelectric.service.apiKey.label = API Key
|
||||
thing-type.config.amberelectric.service.apiKey.description = API key from the Amber website
|
||||
thing-type.config.amberelectric.service.forecasts.label = Forecasts
|
||||
thing-type.config.amberelectric.service.forecasts.description = Specifies the number of forecasts to fetch (Optional)
|
||||
thing-type.config.amberelectric.service.nmi.label = NMI
|
||||
thing-type.config.amberelectric.service.nmi.description = NMI for your address (Optional)
|
||||
thing-type.config.amberelectric.service.refresh.label = Refresh Interval
|
||||
|
|
|
@ -50,6 +50,7 @@
|
|||
<label>Refresh Interval</label>
|
||||
<description>Specifies the refresh interval in seconds</description>
|
||||
<default>60</default>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
</config-description>
|
||||
|
||||
|
|
Loading…
Reference in New Issue