RSS Amplifier

Firesphere's musings · Jul 28, 2026

I've got the power 🔌!

0
Sign in to vote or save

Firesphere.dev

A Powershop Update

Powershop is in the process of updating all their clients to their new mobile/web application. With this change, the old import Powershop data from downloaded CSV, does not work anymore.

That needs fixing, of course.

pexels super santosh 554742

Everything is different now

Where the old Powershop interface used a "remember token", that kept working, and the only way to get the data down, was via a CSV download, the new Powershop interface relies on JWT and GraphQL to pull the data in, and display it using (a lot of) JavaScript in the frontend.

In simpler terms, the old method was the tried and true CSV data, the new method is "communicating with an overly complicated JavaScript library".

The good thing about the JWT token, is that there are a lot of libraries around to make things happen, and downloading the data feels less hacky, and more an actual integration.

Also, honestly? The interface looks vibe-coded... And why are there 5 calls to DataDog for every 1 call to the actual backend? Does everything really needs to be that tracked?

Are you just looking for the codebase? Check my Codeberg repository. Version 2.0 and up is for the latest Powershop Lab.

No official API

Despite moving to a modern framework and having mostly built the API already, Powershop does not have an official API as of yet.

Admittedly, they have a constant warning in their new (web)application, that lets you know that not everything is there yet.

That means that there's still some hacking around to do!

The challenge

To get things working, I divided the changes up in 3 parts:

  1. Be able to login
  2. Be able to pull the data
  3. Translate the pulled data to Home Assistant

Part 1 requires GraphQL and JWT, part 2 requires grabbing requests as the web-interface makes them, and part 3 was already largely there thanks to previous work.

Logging in

As the new system uses JWT, the obvious choice was to use PyJWT. That was the easy part.

The hardest part really, was to get the required keys and such.

As the new system uses Firebase for its authentication, the API key for Firebase is needed to even be able to get started.

Turns out that everything is stored on the local machine. That makes life easier, a quick look at the local storage showed a Firebase object. With the required API Key. Step one of two completed!

Part two, the refresh token. JWT is an interesting authentication system, as in that a user logs in for a very short period, but receives 2 secrets. The first is the "access_token", that does the authentication, and the second is a "refresh_token". The refresh token gives the user the ability to request a new access_token.

A refresh token is sometimes also called a "long lived token", because it is valid for longer. It does not, however, contain all the information needed to log in, only the information needed to get a new refresh token, which can then be used to log in again.

Confusing? A little bit.

Thankfully, the before mentioned local storage object of Firebase, not only contains the API key, it also contains the refresh token and the access token! Score!

The local storage object is named something like
"firebase:authUser:aShorterGibberishStringKey:[DEFAULT]:", where the gibberish string key is the API key for Firebase.

The value of this storage object, has everything needed to get going. This is what it looks like, with sensitive values replaced:

javascript:

  1. {

  2. "uid": "UNIQUE_ID",

  3. "email": "user@example.com",

  4. "emailVerified": true,

  5. "displayName": "John Doe",

  6. "isAnonymous": false,

  7. "providerData": [

  8. {

  9. "providerId": "password",

  10. "uid": "user@example.com",

  11. "displayName": "John Doe",

  12. "email": "user@example.com",

  13. "phoneNumber": null,

  14. "photoURL": null

  15. }

  16. ],

  17. "stsTokenManager": {

  18. "refreshToken": "aLongStringOfRandomGibberishThatIsTheRefreshToken",

  19. "accessToken": "aThreePartString.SeparatedByDots.ItIsTheAccessToken",

  20. "expirationTime": 12345678987654321

  21. },

  22. "createdAt": "1784762171932",

  23. "lastLoginAt": "1785143998317",

  24. "apiKey": "aShorterGibberishStringKey",

  25. "appName": "DEFAULT"

  26. }

The API key, the refresh token, and the access token are clearly marked.

Getting access

A small bit of Python will get us a new refresh and access token. The code is slightly altered to display correctly, but the gist is the same.

python:

  1. refresh = {

  2. "refreshToken": payload.stsTokenManager.get('refreshToken'),

  3. "grant_type": "refresh_token"

  4. }

  5. alg = "RS256"

  6. result = requests.post(f"https://securetoken.googleapis.com/v1/token?key={payload.get('apiKey')}",

  7. data=refresh)

  8. decoded = json.loads(result.text)

  9. jwt_token = jwt.decode(decoded.get("access_token"), algorithms=["HS256", "RS256"], verify=False, options={"verify_signature": False})

This bit of code gives us a JWT token that we can use to grab data from Powershop!

I've set verify_signature to false, because I do not have the private key needed to validate that the authorization matches the signature. Luckily, that's not even needed for me to have or know. So this will do just fine.

Pulling data

For this part of the code, I cheated a bit...

I looked at the request done by the web portal, and pulled out the specific request and the query that it did, and simply pasted that into my application. Then, it was a matter of getting the variables, such as meter ID, ICP, property ID, et cetera, and put them in the right place. With that, I actually got a correct response back immediately!

Sigh. GraphQL

GraphQL is one of those ways of communicating over the internet, that is excessively verbose and key/value heavy.

The response from the API has a very deeply nested response:

javascript:

  1. {

  2. "data": {

  3. "account": {

  4. "id": "",

  5. "property": {

  6. "edges": [

  7. {

  8. "node": {

  9. "source": "Amphio",

  10. "value": "12345.670000000000000000",

  11. "unit": "kwh",

  12. "readAt": "1970-01-01T12:00:00+12:00",

  13. "metaData": {

  14. "utilityFilters": {

  15. "readingFrequencyType": "POINT_IN_TIME",

  16. "readingDirection": "CONSUMPTION",

  17. "registerId": "1",

  18. "deviceId": "123456789",

  19. "marketSupplyPointId": "0000123456AB1C2",

  20. "readingQuality": "ACTUAL",

  21. "__typename": "ElectricityFiltersOutput"

  22. },

  23. "statistics": [],

  24. "__typename": "MeasurementsMetadataOutput"

  25. },

  26. "__typename": "MeasurementType"

  27. },

  28. "__typename": "MeasurementEdge"

  29. }

  30. ]

  31. }

  32. }

  33. }

  34. }

I've left out a lot, but for every hour of a value record, there is an entire block of the "node" response above.

Reformatting everything to be a simpler structure was a lot of searching. The metadata reflects the query I made, e.g. the "readingQuality" there, is ACTUAL, because I requested only actual readings.

There really is a lot of data being sent backward and forward, for very little actual useful information. This is also why I am not a big fan of GraphQL.

Translating it all to Statistics Import

This was a lot of for-looping, and figuring out, but I managed to untangle it all. In the end, it required mostly looping over all the nodes of the response, and figuring out if it was a peak, off-peak, or weekend, for the given date and time.

Aside from pulling all the data in, it was largely using existing code to get me to this stage. Including but not limited to using my old code and re-purposing it.

Once that was all done, I used a bit of my old code to push it all into Home Assistant.

And then....

Then a problem

It was too easy to work immediately, of course.

I made a mistake, and now my measured energy usage in Home Assistant doubled every hour... One + in the wrong place, and everything was doubling up all the time.

This wasn't hard to resolve, but it did cause me some headaches figuring out what my starting point should be, and finding the last correct values in my Home Assistant database.

All said and done, it did work! I'm currently testing it manually every day, to see if I can rely on it doing what I want it to do.

The link to the repo is above, and I'll be updating the README with instructions on how to get started with the new Powershop Lab version as soon as I have time.

Update: Now also costs

As the new GQL API also supplies cost, from the day that the new Labs platform is active for the Powershop user, it also provides the cost per hour, and a standing charge.

I've decided to include the cost per hour, so it can be displayed on the Home Assistant dashboard too.It's imported the same way with the statistics importer API, and requires zero additional configuration.

Many thanks to Daniel Compton for pointing out and providing a starting point to fix some small bugs!

I've omitted the Standing Charge, as it is currently not a core feature of HA, and therefore there are multiple implementations, but none actually without hacking around in HA. I suspect a standing charge feature will be added to the dashboard sooner or later, in a less hacky way than the current options.

Therefore, I decided against it. If you want to include your standing charge, it's a static daily price you can look up on the Powershop website (It's NZ$ 1.95 for Wellington).

Read the original on firesphere.dev

Comments

Nothing yet. Say the first thing.

    Sign in to join the conversation.