evcc-io/evcc

Enable Dutch CO2 forecast data from ned.nl

Closed this issue ยท 22 comments

Is your feature request related to a problem? Please describe.
Just like green-grid-compass for DE I would like to use the CO2 forecast data from ned.nl api to charge when the CO2 intensity is lowest. I have the API description from ned.nl and looked into the existing template from green-grid-compass but I'm not sure it matches and I'm not sure if we need some custom coding for this.

Describe the solution you'd like
Feed the ned.nl co2 forecast data in evcc the regular way

Describe alternatives you've considered
There is no alternative

Additional context
Here is the request and response I think we need:
https://api.ned.nl/v1/utilizations?point=0&type=27&granularity=5&granularitytimezone=1&classification=1&activity=1&validfrom[strictly_before]=2025-09-19&validfrom[after]=2025-09-18
And you need to pass your own API key in the header (X-AUTH-TOKEN)

The response you get is as follows. Per hour you get the production co2 intensity. The emission factor is kg/kwh. You will need to multiply by 1000 to get grams/kwh. Timezone is in UTC, I think Amsterdam is +2 so that needs to be added.

To me it is not clear how evcc will pass this data into the correct data model.

[
{
"id": 70667476442,
"point": "/v1/points/0",
"type": "/v1/types/27",
"granularity": "/v1/granularities/5",
"granularitytimezone": "/v1/granularity_time_zones/0",
"activity": "/v1/activities/1",
"classification": "/v1/classifications/1",
"capacity": 12299354,
"volume": 12299354,
"percentage": 0.29002851247787476,
"emission": 591726,
"emissionfactor": 0.048111073672771454,
"validfrom": "2025-09-17T22:00:00+00:00",
"validto": "2025-09-17T23:00:00+00:00",
"lastupdate": "2025-09-17T23:51:01+00:00"
},
{
"id": 70667476443,
"point": "/v1/points/0",
"type": "/v1/types/27",
"granularity": "/v1/granularities/5",
"granularitytimezone": "/v1/granularity_time_zones/0",
"activity": "/v1/activities/1",
"classification": "/v1/classifications/1",
"capacity": 12078946,
"volume": 12078946,
"percentage": 0.284828245639801,
"emission": 578856,
"emissionfactor": 0.04792322590947151,
"validfrom": "2025-09-17T23:00:00+00:00",
"validto": "2025-09-18T00:00:00+00:00",
"lastupdate": "2025-09-17T23:51:01+00:00"
},
{
"id": 70667476444,
"point": "/v1/points/0",
"type": "/v1/types/27",
"granularity": "/v1/granularities/5",
"granularitytimezone": "/v1/granularity_time_zones/0",
"activity": "/v1/activities/1",
"classification": "/v1/classifications/1",
"capacity": 12016713,
"volume": 12016713,
"percentage": 0.28335800766944885,
"emission": 572384,
"emissionfactor": 0.04763232544064522,
"validfrom": "2025-09-18T00:00:00+00:00",
"validto": "2025-09-18T01:00:00+00:00",
"lastupdate": "2025-09-18T06:51:52+00:00"
},
{
"id": 70667476445,
"point": "/v1/points/0",
"type": "/v1/types/27",
"granularity": "/v1/granularities/5",
"granularitytimezone": "/v1/granularity_time_zones/0",
"activity": "/v1/activities/1",
"classification": "/v1/classifications/1",
"capacity": 11921556,
"volume": 11921556,
"percentage": 0.2811115086078644,
"emission": 576367,
"emissionfactor": 0.048347726464271545,
"validfrom": "2025-09-18T01:00:00+00:00",
"validto": "2025-09-18T02:00:00+00:00",
"lastupdate": "2025-09-18T06:51:52+00:00"
},
{
"id": 70667476446,
"point": "/v1/points/0",
"type": "/v1/types/27",
"granularity": "/v1/granularities/5",
"granularitytimezone": "/v1/granularity_time_zones/0",
"activity": "/v1/activities/1",
"classification": "/v1/classifications/1",
"capacity": 11690028,
"volume": 11690028,
"percentage": 0.2756494879722595,
"emission": 591566,
"emissionfactor": 0.05060817673802376,
"validfrom": "2025-09-18T02:00:00+00:00",
"validto": "2025-09-18T03:00:00+00:00",
"lastupdate": "2025-09-18T06:51:52+00:00"
},
{
"id": 70667476447,
"point": "/v1/points/0",
"type": "/v1/types/27",
"granularity": "/v1/granularities/5",
"granularitytimezone": "/v1/granularity_time_zones/0",
"activity": "/v1/activities/1",
"classification": "/v1/classifications/1",
"capacity": 11765407,
"volume": 11765407,
"percentage": 0.27742400765419006,
"emission": 772390,
"emissionfactor": 0.06565447151660919,
"validfrom": "2025-09-18T03:00:00+00:00",
"validto": "2025-09-18T04:00:00+00:00",
"lastupdate": "2025-09-18T06:51:52+00:00"
},
{
"id": 70667858845,
"point": "/v1/points/0",
"type": "/v1/types/27",
"granularity": "/v1/granularities/5",
"granularitytimezone": "/v1/granularity_time_zones/0",
"activity": "/v1/activities/1",
"classification": "/v1/classifications/1",
"capacity": 12674174,
"volume": 12674174,
"percentage": 0.29884976148605347,
"emission": 1373020,
"emissionfactor": 0.10822277516126633,
"validfrom": "2025-09-18T04:00:00+00:00",
"validto": "2025-09-18T05:00:00+00:00",
"lastupdate": "2025-09-18T06:51:52+00:00"
},
{
"id": 70667858846,
"point": "/v1/points/0",
"type": "/v1/types/27",
"granularity": "/v1/granularities/5",
"granularitytimezone": "/v1/granularity_time_zones/0",
"activity": "/v1/activities/1",
"classification": "/v1/classifications/1",
"capacity": 12501352,
"volume": 12501352,
"percentage": 0.2947717607021332,
"emission": 1249863,
"emissionfactor": 0.09995684772729874,
"validfrom": "2025-09-18T05:00:00+00:00",
"validto": "2025-09-18T06:00:00+00:00",
"lastupdate": "2025-09-18T06:51:52+00:00"
},
{
"id": 70667858847,
"point": "/v1/points/0",
"type": "/v1/types/27",
"granularity": "/v1/granularities/5",
"granularitytimezone": "/v1/granularity_time_zones/0",
"activity": "/v1/activities/1",
"classification": "/v1/classifications/1",
"capacity": 13102331,
"volume": 13102331,
"percentage": 0.30893924832344055,
"emission": 1223473,
"emissionfactor": 0.09344395250082016,
"validfrom": "2025-09-18T06:00:00+00:00",
"validto": "2025-09-18T07:00:00+00:00",
"lastupdate": "2025-09-18T06:51:52+00:00"
},
{
"id": 70667858848,
"point": "/v1/points/0",
"type": "/v1/types/27",
"granularity": "/v1/granularities/5",
"granularitytimezone": "/v1/granularity_time_zones/0",
"activity": "/v1/activities/1",
"classification": "/v1/classifications/1",
"capacity": 14516297,
"volume": 14516297,
"percentage": 0.3422757387161255,
"emission": 1158549,
"emissionfactor": 0.07985607534646988,
"validfrom": "2025-09-18T07:00:00+00:00",
"validto": "2025-09-18T08:00:00+00:00",
"lastupdate": "2025-09-18T06:51:52+00:00"
},
{
"id": 70667858849,
"point": "/v1/points/0",
"type": "/v1/types/27",
"granularity": "/v1/granularities/5",
"granularitytimezone": "/v1/granularity_time_zones/0",
"activity": "/v1/activities/1",
"classification": "/v1/classifications/1",
"capacity": 16108655,
"volume": 16108655,
"percentage": 0.37981799244880676,
"emission": 1042087,
"emissionfactor": 0.06472565233707428,
"validfrom": "2025-09-18T08:00:00+00:00",
"validto": "2025-09-18T09:00:00+00:00",
"lastupdate": "2025-09-18T06:51:52+00:00"
},
{
"id": 70667858850,
"point": "/v1/points/0",
"type": "/v1/types/27",
"granularity": "/v1/granularities/5",
"granularitytimezone": "/v1/granularity_time_zones/0",
"activity": "/v1/activities/1",
"classification": "/v1/classifications/1",
"capacity": 18868784,
"volume": 18868784,
"percentage": 0.4448930025100708,
"emission": 1354094,
"emissionfactor": 0.07201482355594635,
"validfrom": "2025-09-18T09:00:00+00:00",
"validto": "2025-09-18T10:00:00+00:00",
"lastupdate": "2025-09-18T06:51:52+00:00"
},
{
"id": 70667858851,
"point": "/v1/points/0",
"type": "/v1/types/27",
"granularity": "/v1/granularities/5",
"granularitytimezone": "/v1/granularity_time_zones/0",
"activity": "/v1/activities/1",
"classification": "/v1/classifications/1",
"capacity": 21226996,
"volume": 21226996,
"percentage": 0.5004910230636597,
"emission": 1263475,
"emissionfactor": 0.05964517593383789,
"validfrom": "2025-09-18T10:00:00+00:00",
"validto": "2025-09-18T11:00:00+00:00",
"lastupdate": "2025-09-18T06:51:52+00:00"
},
{
"id": 70667858852,
"point": "/v1/points/0",
"type": "/v1/types/27",
"granularity": "/v1/granularities/5",
"granularitytimezone": "/v1/granularity_time_zones/0",
"activity": "/v1/activities/1",
"classification": "/v1/classifications/1",
"capacity": 22080068,
"volume": 22080068,
"percentage": 0.5205997228622437,
"emission": 1059516,
"emissionfactor": 0.047968048602342606,
"validfrom": "2025-09-18T11:00:00+00:00",
"validto": "2025-09-18T12:00:00+00:00",
"lastupdate": "2025-09-18T06:51:52+00:00"
},
{
"id": 70667858853,
"point": "/v1/points/0",
"type": "/v1/types/27",
"granularity": "/v1/granularities/5",
"granularitytimezone": "/v1/granularity_time_zones/0",
"activity": "/v1/activities/1",
"classification": "/v1/classifications/1",
"capacity": 21907030,
"volume": 21907030,
"percentage": 0.5165150165557861,
"emission": 1214354,
"emissionfactor": 0.05508185178041458,
"validfrom": "2025-09-18T12:00:00+00:00",
"validto": "2025-09-18T13:00:00+00:00",
"lastupdate": "2025-09-18T06:51:52+00:00"
},
{
"id": 70667858854,
"point": "/v1/points/0",
"type": "/v1/types/27",
"granularity": "/v1/granularities/5",
"granularitytimezone": "/v1/granularity_time_zones/0",
"activity": "/v1/activities/1",
"classification": "/v1/classifications/1",
"capacity": 20906501,
"volume": 20906501,
"percentage": 0.49292001128196716,
"emission": 496947,
"emissionfactor": 0.023770399391651154,
"validfrom": "2025-09-18T13:00:00+00:00",
"validto": "2025-09-18T14:00:00+00:00",
"lastupdate": "2025-09-18T06:51:52+00:00"
},
{
"id": 70667858855,
"point": "/v1/points/0",
"type": "/v1/types/27",
"granularity": "/v1/granularities/5",
"granularitytimezone": "/v1/granularity_time_zones/0",
"activity": "/v1/activities/1",
"classification": "/v1/classifications/1",
"capacity": 20234666,
"volume": 20234666,
"percentage": 0.4770754873752594,
"emission": 672707,
"emissionfactor": 0.03326810151338577,
"validfrom": "2025-09-18T14:00:00+00:00",
"validto": "2025-09-18T15:00:00+00:00",
"lastupdate": "2025-09-18T06:51:52+00:00"
},
{
"id": 70667858856,
"point": "/v1/points/0",
"type": "/v1/types/27",
"granularity": "/v1/granularities/5",
"granularitytimezone": "/v1/granularity_time_zones/0",
"activity": "/v1/activities/1",
"classification": "/v1/classifications/1",
"capacity": 19546645,
"volume": 19546645,
"percentage": 0.46084949374198914,
"emission": 1350043,
"emissionfactor": 0.06927517801523209,
"validfrom": "2025-09-18T15:00:00+00:00",
"validto": "2025-09-18T16:00:00+00:00",
"lastupdate": "2025-09-18T06:51:52+00:00"
},
{
"id": 70667858857,
"point": "/v1/points/0",
"type": "/v1/types/27",
"granularity": "/v1/granularities/5",
"granularitytimezone": "/v1/granularity_time_zones/0",
"activity": "/v1/activities/1",
"classification": "/v1/classifications/1",
"capacity": 19628984,
"volume": 19628984,
"percentage": 0.4627862572669983,
"emission": 2965874,
"emissionfactor": 0.15163224935531616,
"validfrom": "2025-09-18T16:00:00+00:00",
"validto": "2025-09-18T17:00:00+00:00",
"lastupdate": "2025-09-18T06:51:52+00:00"
},
{
"id": 70667858858,
"point": "/v1/points/0",
"type": "/v1/types/27",
"granularity": "/v1/granularities/5",
"granularitytimezone": "/v1/granularity_time_zones/0",
"activity": "/v1/activities/1",
"classification": "/v1/classifications/1",
"capacity": 17408598,
"volume": 17408598,
"percentage": 0.41043275594711304,
"emission": 3379816,
"emissionfactor": 0.19445550441741943,
"validfrom": "2025-09-18T17:00:00+00:00",
"validto": "2025-09-18T18:00:00+00:00",
"lastupdate": "2025-09-18T06:51:52+00:00"
},
{
"id": 70667858859,
"point": "/v1/points/0",
"type": "/v1/types/27",
"granularity": "/v1/granularities/5",
"granularitytimezone": "/v1/granularity_time_zones/0",
"activity": "/v1/activities/1",
"classification": "/v1/classifications/1",
"capacity": 15800057,
"volume": 15800057,
"percentage": 0.3725054860115051,
"emission": 2888802,
"emissionfactor": 0.18277600407600403,
"validfrom": "2025-09-18T18:00:00+00:00",
"validto": "2025-09-18T19:00:00+00:00",
"lastupdate": "2025-09-18T06:51:52+00:00"
},
{
"id": 70667858860,
"point": "/v1/points/0",
"type": "/v1/types/27",
"granularity": "/v1/granularities/5",
"granularitytimezone": "/v1/granularity_time_zones/0",
"activity": "/v1/activities/1",
"classification": "/v1/classifications/1",
"capacity": 15505350,
"volume": 15505350,
"percentage": 0.36555373668670654,
"emission": 2895724,
"emissionfactor": 0.18674825131893158,
"validfrom": "2025-09-18T19:00:00+00:00",
"validto": "2025-09-18T20:00:00+00:00",
"lastupdate": "2025-09-18T06:51:52+00:00"
},
{
"id": 70667858861,
"point": "/v1/points/0",
"type": "/v1/types/27",
"granularity": "/v1/granularities/5",
"granularitytimezone": "/v1/granularity_time_zones/0",
"activity": "/v1/activities/1",
"classification": "/v1/classifications/1",
"capacity": 14816687,
"volume": 14816687,
"percentage": 0.34931474924087524,
"emission": 2601636,
"emissionfactor": 0.17554624378681183,
"validfrom": "2025-09-18T20:00:00+00:00",
"validto": "2025-09-18T21:00:00+00:00",
"lastupdate": "2025-09-18T06:51:52+00:00"
},
{
"id": 70667858862,
"point": "/v1/points/0",
"type": "/v1/types/27",
"granularity": "/v1/granularities/5",
"granularitytimezone": "/v1/granularity_time_zones/0",
"activity": "/v1/activities/1",
"classification": "/v1/classifications/1",
"capacity": 12294905,
"volume": 12294905,
"percentage": 0.28985899686813354,
"emission": 1403875,
"emissionfactor": 0.11418475210666656,
"validfrom": "2025-09-18T21:00:00+00:00",
"validto": "2025-09-18T22:00:00+00:00",
"lastupdate": "2025-09-18T06:51:52+00:00"
}
]

Happy to take a PR for a new template! Please note that the core team won't be working on this.

I see the pull request is merged already. If there are any technical issues left we will make a new PR to fix. For now I think we can close the issue.

Cool! Can this now be used in 0.208.0?

If so, how?

The Nationaal Energie Dashboard was recently updated with the forecast ability and yes that is cool indeed. This template consumes their API.

If you configure this in the tariff section then it should work. Please note that it was not rigorously tested and can have some issues that need to be fixed. It was merged into production a bit quickly.

I think you need this configuration.
tariffs:
co2:
type: template
template: ned
apiKey: # API Key, you need to make your own account at ned.nl and then you need to pass the API key here

I tried this, but with no success.

[tariff] ERROR 2025/09/27 14:30:05 unexpected status: 400 (Bad Request)
[main  ] ERROR 2025/09/27 14:30:05 creating tariff co2 failed: cannot create tariff type 'template': cannot create tariff type 'custom': unexpected status: 400 (Bad Request)
andig commented

Trace log?

I don't see much more:

[tariff] ERROR 2025/09/27 14:42:08 unexpected status: 400 (Bad Request)
[main ] ERROR 2025/09/27 14:42:08 creating tariff co2 failed: cannot create tariff type 'template': cannot create tariff type 'custom': unexpected status: 400 (Bad Request)
[site ] INFO 2025/09/27 14:42:08 co2: โœ“

config is:

tariffs:
  co2:
    type: template
    template: ned
    apiKey: "xxxx"

Is there anything I can do to get more logs that are relevant for you?

... [deleted]

met trace enabled it shows this for the co2 domain.

[co2 ] TRACE 2025/09/27 15:24:09 GET https://api.ned.nl/v1/utilizations?point=0&type=27&granularity=5&granularitytimezone=0&classification=1&activity=1&validfrom[strictly_before]=2025-09-27&validfrom[after]=2025-09-29
[co2 ] TRACE 2025/09/27 15:24:09 {"@context":"\/v1\/contexts\/Error","@type":"hydra:Error","hydra:title":"An error occurred","hydra:description":"Date filter must be chronological correct"}

I think the valid from and strictly before dates are unfortunately swapped in the last version of the template. When testing the URL with postman and changing the value "validfrom[strictly_before]" to "2025-09-29" and "validfrom[after] to "2025-09-27" I get the 200 - OK.

I also get some values from today (history). I don't know if this will break evcc but it would be good practice to filter them out in the jq with some additional statements.

andig commented

Do you want to open a PR?

Hopefully just reversing the keys is enough: https://github.com/gerritjandebruin/evcc/tree/patch-2

But I have never contributed yet to EVCC, so need to run tests to be sure.
Will do that later if I have some time.

Postman seems to give valid response for:

GET /v1/utilizations?point=0&type=27&granularity=5&granularitytimezone=0&classification=1&activity=1&validfrom[strictly_before]=2025-10-02&validfrom[after]=2025-09-30 HTTP/1.1
Host: api.ned.nl
X-AUTH-TOKEN: xxx

Hi Gerrit Jan, yes this should be the solution for at least the bad request problem. I made a PR that was merged already so it should be solved in the next release.

A potential next problem will be that the api output contains some historical data that we might need to filter out with the JQ statement. But we will see.

Fijne dag!

andig commented

Outdated data points should already be ignored by evcc

@gerritjandebruin it should be working now with the latest evcc release. I can see the forecast in the GUI and in the meantime we also took care of a PR for a 15 minute interval.

There is one problem left: I see that the conversion from UTC to local time in the JQ statement is not working. This causes the GUI to show the data 2 hours later than it is supposed to be. Simply subtracting two hours from the UTC is not a good solution since we have winter/summer time in NL. Any ideas on how we can fix this?

Image

I can confirm that it is now two hours off, but shows nevertheless! :)

ChatGPT suggests that (.validfrom | strptime("%Y-%m-%dT%H:%M:%S%z") | mktime | strflocaltime("%FT%TZ")) should be (.validfrom | strptime("%Y-%m-%dT%H:%M:%S%z") | mktime | strflocaltime("%FT%T")) but I have still no dev env setup. So will try later to see whether this makes sense.

I don't know how to solve the DST problem. Should EVCC not work with UTC?

Hi @gerritjandebruin thanks for the suggestion. I tested this using JQ playground and the information from the ned.nl UI values. They line up perfectly now. I made a PR to merge this into the next release. We should be all OK now.

Good! I could not make it work myself for some reason.

@gerritjandebruin I'm not a developer but we managed it together.

Let's wait for the final applause until we see it in action ;)

@gerritjandebruin I updated to the latest evcc version and now the timing seems to be ok.

Same here!