diff --git a/bundles/org.openhab.binding.amberelectric/README.md b/bundles/org.openhab.binding.amberelectric/README.md index 253556d7751..991898d6c38 100644 --- a/bundles/org.openhab.binding.amberelectric/README.md +++ b/bundles/org.openhab.binding.amberelectric/README.md @@ -1,6 +1,6 @@ # Amber Electric Binding -A binding that supports the Australian energy retailer Amber's API () 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 () 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 -- '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 | +| 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 diff --git a/bundles/org.openhab.binding.amberelectric/src/main/java/org/openhab/binding/amberelectric/internal/AmberElectricConfiguration.java b/bundles/org.openhab.binding.amberelectric/src/main/java/org/openhab/binding/amberelectric/internal/AmberElectricConfiguration.java index 31c9d21a426..92a3c2493a7 100644 --- a/bundles/org.openhab.binding.amberelectric/src/main/java/org/openhab/binding/amberelectric/internal/AmberElectricConfiguration.java +++ b/bundles/org.openhab.binding.amberelectric/src/main/java/org/openhab/binding/amberelectric/internal/AmberElectricConfiguration.java @@ -24,4 +24,5 @@ public class AmberElectricConfiguration { public String apiKey = ""; public String nmi = ""; public long refresh = 60; + public long forecasts = 288; } diff --git a/bundles/org.openhab.binding.amberelectric/src/main/java/org/openhab/binding/amberelectric/internal/AmberElectricHandler.java b/bundles/org.openhab.binding.amberelectric/src/main/java/org/openhab/binding/amberelectric/internal/AmberElectricHandler.java index def6d8e3217..1ae51f4b9fd 100644 --- a/bundles/org.openhab.binding.amberelectric/src/main/java/org/openhab/binding/amberelectric/internal/AmberElectricHandler.java +++ b/bundles/org.openhab.binding.amberelectric/src/main/java/org/openhab/binding/amberelectric/internal/AmberElectricHandler.java @@ -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 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()); diff --git a/bundles/org.openhab.binding.amberelectric/src/main/java/org/openhab/binding/amberelectric/internal/AmberElectricWebTargets.java b/bundles/org.openhab.binding.amberelectric/src/main/java/org/openhab/binding/amberelectric/internal/AmberElectricWebTargets.java index 0804a47cc1e..d4f3540441c 100644 --- a/bundles/org.openhab.binding.amberelectric/src/main/java/org/openhab/binding/amberelectric/internal/AmberElectricWebTargets.java +++ b/bundles/org.openhab.binding.amberelectric/src/main/java/org/openhab/binding/amberelectric/internal/AmberElectricWebTargets.java @@ -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) { diff --git a/bundles/org.openhab.binding.amberelectric/src/main/java/org/openhab/binding/amberelectric/internal/api/CurrentPrices.java b/bundles/org.openhab.binding.amberelectric/src/main/java/org/openhab/binding/amberelectric/internal/api/CurrentPrices.java index d10be7b69cf..56006e79769 100644 --- a/bundles/org.openhab.binding.amberelectric/src/main/java/org/openhab/binding/amberelectric/internal/api/CurrentPrices.java +++ b/bundles/org.openhab.binding.amberelectric/src/main/java/org/openhab/binding/amberelectric/internal/api/CurrentPrices.java @@ -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; - } } diff --git a/bundles/org.openhab.binding.amberelectric/src/main/java/org/openhab/binding/amberelectric/internal/api/Sites.java b/bundles/org.openhab.binding.amberelectric/src/main/java/org/openhab/binding/amberelectric/internal/api/Sites.java index 5fecfde2b77..136e301bf06 100644 --- a/bundles/org.openhab.binding.amberelectric/src/main/java/org/openhab/binding/amberelectric/internal/api/Sites.java +++ b/bundles/org.openhab.binding.amberelectric/src/main/java/org/openhab/binding/amberelectric/internal/api/Sites.java @@ -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() { } } diff --git a/bundles/org.openhab.binding.amberelectric/src/main/resources/OH-INF/i18n/amberelectric.properties b/bundles/org.openhab.binding.amberelectric/src/main/resources/OH-INF/i18n/amberelectric.properties index fc39b8515fb..aa3e527977c 100644 --- a/bundles/org.openhab.binding.amberelectric/src/main/resources/OH-INF/i18n/amberelectric.properties +++ b/bundles/org.openhab.binding.amberelectric/src/main/resources/OH-INF/i18n/amberelectric.properties @@ -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 diff --git a/bundles/org.openhab.binding.amberelectric/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.amberelectric/src/main/resources/OH-INF/thing/thing-types.xml index 4e6abc92af6..ece3d0833a5 100644 --- a/bundles/org.openhab.binding.amberelectric/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.amberelectric/src/main/resources/OH-INF/thing/thing-types.xml @@ -50,6 +50,7 @@ Specifies the refresh interval in seconds 60 + true