Australian Users: Amber electric

Just signed up to amber electric in australia (whole sale prices, most of the time cheap. With huge price spike during peak usage in summer ($3-$19 per kwh)

So you need to not use energy in this time.

Anyway they have a crude api. Wondering if anyone has set it up with hubitat? If not any tips on how to get going? Willing to try

They are working on a publicly available pricing API, but in the meantime, they can provide access on an ad hoc basis for users who want to use it .

Here is a sample

We don't yet have a fully public API that we guarantee backwards compatibility for, but hope to have one in place in the next 6 months. In the meantime we have a semi-internal API you can use. Details below:

curl -X POST \\<https://api.amberelectric.com.au/prices/listprices> \\-H 'Content-Type: application/json' \\-d '{ "postcode": "5000" }'

From the data there, the formula for usage prices is:

data.staticPrices.E1.totalfixedKWHPrice + data.staticPrices.E1.lossFactor * data.variablePricesAndRenewables.[period].wholesaleKWHPrice

And the formula for Export to Grid prices is:

data.staticPrices.B1.totalfixedKWHPrice - data.staticPrices.B1.lossFactor * data.variablePricesAndRenewables.[period].wholesaleKWHPrice

All timestamps in the data are market time (ie Brisbane time) and at period end , not start (so if it's 915am, the current period is the 930am one). These prices are GST inclusive, so divide by 1.1 if you need them exclusive.

Main use case is to get the current prices for my post code and just use rule machine from there

1 Like

Seems straight forward enough, from what you have provided. I haven't seem it on the forum, but am by no means across everything that goes on. If you want to have a crack yourself I could provide some examples of what I have done, or you are welcome to take a look at some of my code as an example:

There should hopefully be enough to get you started. I'd start by setting up a device driver with the refresh capability, define some attributes, create a refresh method and make some of the API calls inside that refresh method to start with, you can use my code as an example of building up the headers and parsing responses, etc.

Start small, output the result of the call to the logs, then slowly start to write the code to capture it into the attributes. Once you are comfortable with this, perhaps start organising the code into more appropriate structure of different methods, etc. Perhaps even looking at other capabilities if you need them for interacting with HE and other apps.

I'm not going to claim to have the best code, so I'm certainly open to others providing input or guidance on better solutions than mine.

Happy to take a look at any code and give you a hand, but my time in this space can be sporadic, so can't guarantee how quickly I may respond.

Enjoy...

Simon

1 Like

Thanks very much. I'll give it a go! May take me a while as I've never done it

2 Likes

I’m waiting to be switched over to Amber at the moment. Keen to see what you come up with!

It will take a bit of time as I'm new to it but the community here is pretty awesome. Can you code @pjam73?

Happy to try myself great examples above.

1 Like

Yes, I’m a programmer. As Simon said earlier, this should be relatively simple but there is a fair bit of “boilerplate” involved in getting a driver setup up and doing something regularly. The actual API call looks like it will be easy.

1 Like

Ah. I am not a programmer. I'm sure I could get there but having a look it's going to take a while. You're welcome to start something as well.

I spent a couple of hours yesterday on this and have got a price being calculated, but it doesn’t seem to match the app at this point. I’ve found some other code so I’ll compare with that and try to understand the pricing model.
Would you just want the current import and export prices for the current 30 minute period? Or maybe prices for the next 6 periods or something?
If the latter, and you want to be able to read those from Rule Machine, I think the attributes would have to be something like:
importprice0
importprice30
importprice60
importprice90

etc.

2 Likes

Yeah good point. You want to be able to know what current price is and what next price will be over say next four periods ..so you can prep your air con to cool like crazy and then switch off before it kicks in

The main delayed use case would be knowing well in advanced that it was spiking soon and you can prepare the cooling.

Glad to hear you're looking at it

If you get it going I'll happily buy you lunch or a six pack!

For those that have access to this data and are amber electric customers who also have a smart thermostat, there are some really cool things you could do with rule machine in terms of perfect timing for when to turn the air conditioning on if it's going to be hot in the next 6 hours for example.

I'm thinking about using the Google chrome integration to announce if it sees a spike in electricity usage (I have efergy energy monitoring hooked up ).It can warn and say whatever you just turned on please turn it off to save money as it's quite expensive now.

I am with amber and really like them

Yeah I've been tracking the costs since signing up (I'm not with then yet in cooling off period)

And noticing a difference between forecast and actual. Especially last night in melbourne where I am around 6pm cause of the hot weather. Forecast was low by 12c. But my friends who use it say it's usually more accurate when it gets closer to the time.

Here's a simple device driver that gets the current import and export prices as c/kWh.

The price doesn't quite match what the app and updated website shows. I've only been a customer for less that a week but I get the impression that the website and the app also had a slight discrepancy. I think the old website was using this API - now it seems to use a graphql API query.
Once we work out how future pricing should be represented it can be extended, most of the code is already there.

/**
 *	Amber Electric Price Watch
 *  Author: Paul Donovan (paul@donovansbrain.co.uk)
 *
 *	Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 *	in compliance with the License. You may obtain a copy of the License at:
 *
 *			http://www.apache.org/licenses/LICENSE-2.0
 *
 *	Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
 *	on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
 *	for the specific language governing permissions and limitations under the License.
 *
 */

static String version() {
	return "0.1"
}

static String pricesApi() {
    return "https://api.amberelectric.com.au/prices/listprices"
}

preferences {
    input("postcode", "string", title:"Postcode",
		required: true)
    input ("pollingInterval", "enum", title: "Polling Interval (minutes)", 
           options: ["5", "10", "15", "30", "60", "180"], 
           defaultValue: "15", required: true)
	input("debugEnable", "bool", title: "Enable debug logging?", defaultValue: true)
}

metadata {
	definition (name: "Amber Electric Price Watch", namespace: "donovansbrain", author: "pjam73") {

		capability "Refresh"
		capability "Polling"

		attribute "importPrice", "number"
        attribute "exportPrice", "number"
        
        command "refresh"
	}
}

void poll() {
	pullData()
}

void refresh() {
	pullData()
}

void updated() {
	if (!state.updatedLastRanAt || now() >= state.updatedLastRanAt + 2000) {
		state.updatedLastRanAt = now()
		trace "updated() called with settings: ${settings.inspect()}".toString()
        
		pullData()
		startPoll()
		if(debugEnable) runIn(1800,logsOff)
	} else {
		trace "updated() ran within the last 2 seconds - skipping"
	}
}

void logsOff() {
	debug "debug logging disabled..."
	device.updateSetting("debugEnable",[value:"false",type:"bool"])
}

void startPoll() {
	unschedule()
	// Schedule polling based on preference setting
	def sec = Math.round(Math.floor(Math.random() * 60))
	def min = Math.round(Math.floor(Math.random() * settings.pollingInterval.toInteger()))
	String cron = "${sec} ${min}/${settings.pollingInterval.toInteger()} * * * ?" // every N min
	trace "startPoll: schedule('$cron', pullData)".toString()
	schedule(cron, pullData)
}



void pullData() {

	log.info "Requesting data from Amber API"

    def postParams = [
        uri: pricesApi(),
        contentType: "application/json",
        requestContentType: "application/json",
        body : ["postcode": settings.postcode.toString()]
	]
    
    try {
        httpPost(postParams) {resp ->
            if (resp.status == 200) {
                def amberData = resp.data.data
                
                def staticImportPrices = amberData.staticPrices.E1
                def staticExportPrices = amberData.staticPrices.B1
                def variablePrices = amberData.variablePricesAndRenewables
                
                def recent30minPeriod = variablePrices.findAll { it.periodSource == "30MIN" && it.periodType == "ACTUAL" }[-1]
                def fiveMinPeriods = variablePrices.findAll { it.periodSource == "5MIN" }
                def recent5minPeriod = fiveMinPeriods? fiveMinPeriods.first() : null
                
                def impp = calculateImportPrice(staticImportPrices, recent30minPeriod)
                def expp = calculateExportPrice(staticExportPrices, recent30minPeriod)
                
                log.info "import price: ${impp}, export price: ${expp}"
                
                sendEvent(name: "importPrice", value: impp, unit: "c/kWh")
                sendEvent(name: "exportPrice", value: expp, unit: "c/kWh")
      
                
            } else {
                debug "error: HTTP status" : resp.status.toString()
            }
            
        }
    } 
	catch(Exception e) {
		log.warn "error occured: ${e}"
	}

}

def calculateImportPrice(staticImport, variablePeriod) {
    def importPrice = staticImport.totalfixedKWHPrice.toBigDecimal() + staticImport.lossFactor.toBigDecimal() * variablePeriod.wholesaleKWHPrice.toBigDecimal()
    importPrice = Math.round(importPrice)
    return importPrice
}

def calculateExportPrice(staticExport, variablePeriod) {
    def exportPrice = staticExport.totalfixedKWHPrice.toBigDecimal() - staticExport.lossFactor.toBigDecimal() * variablePeriod.wholesaleKWHPrice.toBigDecimal()
    exportPrice = Math.round(exportPrice)
    return exportPrice
}

void debug(String msg) {
	if(debugEnable) log.debug device.displayName+' - '+msg
}

void trace(String msg) {
	if(debugEnable) log.trace device.displayName+' - '+msg
}
2 Likes

fantastic work @pjam73 thank you so much.

I'll contact amber and tell them the api doesn't match the app/website and see what they say. Also will ask about future pricing.

for some reason I have two community accounts . I am @JoelD

The API I'm using is fetching the future prices. I'm just not storing it in any attributes yet because I/we need to think about how we'd like to use it. Do we just want a rolling next30, next60 next90 etc. prices, do we want a highExpenseTime set to e.g. '2020-11-05 19:00' if the price at 7pm > 100c ? etc.

hmm. Fair enough - Most people would use this with rule machine so it comes down to what is easier to use there I guess?

My use case would be as follows:
IF thermostat is HOT AND next30 Price = < X THEN
Turn on AirCon

But also I'd see a use case where if you see a price spike coming up and it's going to be hot soon for example you'd want to pre cool the house prior.
SO:
IF weather forecast is HOT and next60/90 Price = > X THEN
Turn on AirCon
If next30 price => Y
TURN off Air Con

Both use cases would use the next30/60/90 data quite well.

I think if users wanted to track highExepenseTime = date/time and that is configurable it would help but I see this been managed in RM.

what do you think?

here's the response on why it's not accurate and more details on their api:

We don't yet have a fully public API that we guarantee backwards compatibility for. We're planning to launch a fully supported public API in the next 12 months, but until then this API will give you indicative prices.
Note that you will need to switch to the new API when it is released.

Here are the full details for accessing our current API:
1. GUI (ie Postman)
Ensure you enter the method as POST and the postcode payload in the Body , not Params .
​​

​​

2. Windows based curl

curl -v -i -L https://api.amberelectric.com.au/prices/listprices -H "Content-Type: application/json" --data "{\"postcode\":\"3000\"}"

3. Unix based curl

curl -X POST \\<https://api.amberelectric.com.au/prices/listprices> \\-H 'Content-Type: application/json' \\-d '{ "postcode": "3000" }'

From the data there, the formula for usage prices is:

data.staticPrices.E1.totalfixedKWHPrice + data.staticPrices.E1.lossFactor * data.variablePricesAndRenewables.[period].wholesaleKWHPrice

And the formula for Export to Grid prices is:

data.staticPrices.B1.totalfixedKWHPrice - data.staticPrices.B1.lossFactor * data.variablePricesAndRenewables.[period].wholesaleKWHPrice

See the bottom of this email for a full example.

All timestamps in the data are market time (ie Brisbane time) and at period end , not start (so if it's 9:15am, the current period is the 9:30am one). These prices are GST inclusive, so divide by 1.1 if you need them exclusive.

Let me know if you have further questions - hopefully the example below will make things clear!

Example: Get the current general price for postcode 3000 @ 18:15 on 13/10/2020 in Melbourne
( full results in attached file listprices-postcode_3000-time_2020-10-13T17_15_57.json )

From the formula, first find the static prices (prices that do not change every 30 minutes). Use data.staticPrices.E1 as this represents the static prices for general usage.

{
"serviceResponseType": 1,
"data": {
"currentNEMtime": "2020-10-13T17:15:57",
"postcode": "3000",
...
"staticPrices": {

"E1": {
"dataAvailable": true,
"networkDailyPrice": "28.63043692",
"basicMeterDailyPrice": "0",
"additionalSmartMeterDailyPrice": "22",
"amberDailyPrice": "32.87671233",
"totalDailyPrice": "83.50714925",
"networkKWHPrice": "7.766",
"marketKWHPrice": "1.718",
"greenKWHPrice": "4.2471",
"carbonNeutralKWHPrice": "0.11",
"lossFactor": "1.04063232",
"offsetKWHPrice": "0.11",
"totalfixedKWHPrice": "9.59400",
...
},
"E2": {
...
},

"B1": {
...
}
},
"variablePricesAndRenewables": [ ... ]
},
"message": ""
}

Note: data.staticPrices.E2 is for controlled load usage and data.staticPricesB1 is for solar exports.

Store the two static price fields:

  • totalfixedKWHPrice = 9.59400
  • lossFactor = 1.04063232

Now find the variable price (price that updates every 5 minutes and is averaged over a 30 minute block). Variable prices are stored with 30 minute timestamps in market time (Brisbane time).

13/10/2020 is daylight savings time in Melbourne, so for 18:15 in Melbourne , find the timestamp for 17:15 in market time (since market time ignores daylight savings). 30 minute timestamps are stored by the 30 minute period end , so the timestamp in market time is 2020-10-13T17:30:00.

{
"serviceResponseType": 1,
"data": {
...
"staticPrices": { ... },
"variablePricesAndRenewables": [
{
"periodType": "ACTUAL",
"semiScheduledGeneration": "1140.9",
"operationalDemand": "5017.25",
"rooftopSolar": "182.761",
"createdAt": "2020-10-13T17:15:41",
"wholesaleKWHPrice": "4.2856000000000005",
"region": "VIC1",
"period": "2020-10-12T17:30:00",
"renewablesPercentage": "0.25454965383727074",
"periodSource": "30MIN",
"percentileRank": "0.4915254237288136"
},
...
{
"periodType": "ACTUAL",
"operationalDemand": "30532.94000",
"rooftopSolar": "190.04",
"wholesaleKWHPrice": "5.39879",

"region": "VIC1",
"period": "2020-10-13T17:30:00",

"periodSource": "5MIN",
"latestPeriod": "2020-10-13T17:15:00",
"usage": "30532.94000",
"renewablesPercentage": "0.17017",
"percentileRank": "0.8050847457627118"
},
{
"periodType": "FORECAST",
"semiScheduledGeneration": "645.927",
"operationalDemand": "5140.97",
"rooftopSolar": "56.497",
"forecastedAt": "2020-10-13T17:00:00",
"forecastedAt+period": "2020-10-13T17:00:00+2020-10-13T18:00:00",
"createdAt": "2020-10-13T17:15:41",
"wholesaleKWHPrice": "5.2541863",
"region": "VIC1",
"period": "2020-10-13T18:00:00", "renewablesPercentage": "0.13514737082505765",
"periodSource": "30MIN",
"percentileRank": "0.7796610169491526"
},
...
]
},
"message": ""
}

Store the variable price field:

  • wholesaleKWHPrice = 5.39879

A few things to note here:

  • For the current live price, find the latest period with type ACTUAL
    • If periodSource is 30MIN the price has been finalised

    • If periodSource is 5MIN the price is still fluctuating every 5 minutes

  • Periods with type FORECAST are prices in the future

Now calculate the current price:

currentWholesalePrice = data.staticPrices.E1.totalfixedKWHPrice + data.staticPrices.E1.lossFactor * data.variablePricesAndRenewables.[period].wholesaleKWHPrice 
currentWholesalePrice = 9.59400 + (1.04063232 * 5.39879)
currentWholesalePrice = 15.21 cents

As indicated by periodSource and latestPeriod fields, this price updates every 5 minutes. If you want to match the live price in the Amber app, you should create an average of these 5 minute live prices over each 30 minute block.

1 Like

Not sure if this shows why it's not accurate or shows any other features I didn't post originally . Only explanation is that it's not backwards compatible fully.

Very cheap pricing Right now !! Time to use washing machine!

If only I had an electric car!

I think what would be really useful for this driver would be for it to show the cheapest price over the next 24 hours. So people could put this on a dashboard somewhere so they could glance at it and then set the dishwasher to be delayed by x hours or the washing machine or dryer rather than having to look it up in the app as well.

or even show where the cheapest price in the next 24 hours is in an hour format?