[GH-ISSUE #479] Feature request: create tibber query #1955

Open
opened 2026-03-20 20:07:58 +01:00 by sascha_hemi · 7 comments
Owner

Originally created by @sebnaf on GitHub (Jun 24, 2025).
Original GitHub issue: https://github.com/OpenEPaperLink/OpenEPaperLink/issues/479

I really do like the "dayahead" integration as well as representation.

however, it differs a lot from current tibber pricing.

I would love to see a tibber integration with the two parameters:

I did a quick mockup which returns the values as close as the dayahead integration:

[{"time":1750759200,"price":"0.19","currency":"EUR"},{"time":1750762800,"price":"0.18","currency":"EUR"},{"time":1750766400,"price":"0.19","currency":"EUR"},{"time":1750770000,"price":"0.20","currency":"EUR"},{"time":1750773600,"price":"0.21","currency":"EUR"},{"time":1750777200,"price":"0.21","currency":"EUR"},{"time":1750780800,"price":"0.28","currency":"EUR"},{"time":1750784400,"price":"0.33","currency":"EUR"},{"time":1750788000,"price":"0.36","currency":"EUR"},{"time":1750791600,"price":"0.36","currency":"EUR"},{"time":1750795200,"price":"0.33","currency":"EUR"},{"time":1750798800,"price":"0.33","currency":"EUR"},{"time":1750802400,"price":"0.30","currency":"EUR"},{"time":1750806000,"price":"0.29","currency":"EUR"},{"time":1750809600,"price":"0.29","currency":"EUR"},{"time":1750813200,"price":"0.29","currency":"EUR"},{"time":1750816800,"price":"0.29","currency":"EUR"},{"time":1750820400,"price":"0.31","currency":"EUR"},{"time":1750824000,"price":"0.34","currency":"EUR"},{"time":1750827600,"price":"0.34","currency":"EUR"},{"time":1750831200,"price":"0.33","currency":"EUR"},{"time":1750834800,"price":"0.29","currency":"EUR"},{"time":1750838400,"price":"0.22","currency":"EUR"},{"time":1750842000,"price":"0.21","currency":"EUR"},{"time":1750845600,"price":"0.21","currency":"EUR"},{"time":1750849200,"price":"0.21","currency":"EUR"},{"time":1750852800,"price":"0.21","currency":"EUR"},{"time":1750856400,"price":"0.21","currency":"EUR"},{"time":1750860000,"price":"0.23","currency":"EUR"},{"time":1750863600,"price":"0.31","currency":"EUR"},{"time":1750867200,"price":"0.34","currency":"EUR"},{"time":1750870800,"price":"0.37","currency":"EUR"},{"time":1750874400,"price":"0.45","currency":"EUR"},{"time":1750878000,"price":"0.50","currency":"EUR"},{"time":1750881600,"price":"0.42","currency":"EUR"},{"time":1750885200,"price":"0.36","currency":"EUR"}]

````


the code itself is below: not nice, but working.

If somebody could integrate this into one of the next released: i'd be very happy.

````
function doGet(e) {
  var logger = Logger.log;
  
  // Get API key from URL params or fall back to placeholder
  var apiKey = e?.parameter?.apiKey || 'YOUR_TIBBER_API_KEY_HERE';
  
  // Currency format for output (defaults to EUR)
  var currencyUnit = e?.parameter?.currency || 'EUR';
  
  // Check if user wants energy price only (without tax)
  var useEnergyPrice = e?.parameter?.energy === 'true' || e?.parameter?.energy === '1';
  var priceType = useEnergyPrice ? 'energy' : 'total';
  
  logger('Got params: apiKey=' + (apiKey ? apiKey.substring(0, 8) + '***' : 'missing') + ', currency=' + currencyUnit + ', energyOnly=' + useEnergyPrice);
  
  if (apiKey === 'YOUR_TIBBER_API_KEY_HERE') {
    logger('Error: Please provide a valid Tibber API key');
    return ContentService.createTextOutput('Error: Please provide a valid Tibber API key. Get one from https://developer.tibber.com/settings/access-token')
      .setMimeType(ContentService.MimeType.TEXT);
  }

  try {
    // Build GraphQL query for today and tomorrow prices
    var query = `{
      viewer {
        homes {
          currentSubscription {
            priceInfo {
              today {
                startsAt
                total
                energy
                tax
                currency
              }
              tomorrow {
                startsAt
                total
                energy
                tax
                currency
              }
            }
          }
        }
      }
    }`;

    var payload = {
      'query': query
    };

    var options = {
      'method': 'POST',
      'headers': {
        'Authorization': 'Bearer ' + apiKey,
        'Content-Type': 'application/json'
      },
      'payload': JSON.stringify(payload)
    };

    // try api call with retries
    var data;
    var responseText;
    var maxRetries = 3;
    var retryCount = 0;
    var success = false;
    
    while (retryCount < maxRetries && !success) {
      try {
        logger('calling tibber api (attempt ' + (retryCount + 1) + ')');
        var response = UrlFetchApp.fetch('https://api.tibber.com/v1-beta/gql', options);
        responseText = response.getContentText();
        data = JSON.parse(responseText);
        success = true;
        logger('api call successful');
      } catch (apiError) {
        retryCount++;
        logger('api call failed (attempt ' + retryCount + '): ' + apiError.toString());
        
        if (retryCount < maxRetries) {
          logger('waiting before retry...');
          Utilities.sleep(1000 * retryCount); // wait 1s, 2s, 3s between retries
        } else {
          throw new Error('api failed after ' + maxRetries + ' attempts: ' + apiError.toString());
        }
      }
    }

    logger('api response: ' + responseText);

    if (data.errors) {
      logger('api returned errors: ' + JSON.stringify(data.errors));
      return ContentService.createTextOutput('Error: ' + JSON.stringify(data.errors))
        .setMimeType(ContentService.MimeType.TEXT);
    }

    if (!data.data || !data.data.viewer || !data.data.viewer.homes || data.data.viewer.homes.length === 0) {
      logger('no homes found in response');
      return ContentService.createTextOutput('Error: No homes found. Make sure your API key is valid and you have a Tibber subscription.')
        .setMimeType(ContentService.MimeType.TEXT);
    }

    var home = data.data.viewer.homes[0]; // just use the first home
    var priceInfo = home.currentSubscription.priceInfo;
    
    var jsonData = [];
    
    // grab today's hourly prices
    if (priceInfo.today && priceInfo.today.length > 0) {
      for (var i = 0; i < priceInfo.today.length; i++) {
        var priceEntry = priceInfo.today[i];
        var epochTime = new Date(priceEntry.startsAt).getTime() / 1000;
        var apiCurrency = priceEntry.currency || 'NOK';
        
        // pick energy or total price based on what user wants
        var priceValue = (priceType === 'energy') ? priceEntry.energy : priceEntry.total;
        var convertedPrice = parseFloat(priceValue);
        
        // format the price as string to preserve 2 decimal places
        if (currencyUnit === 'ct') {
          // convert to cents
          convertedPrice = (convertedPrice * 100).toFixed(2);
        } else {
          // format to 2 decimal places for proper currency display
          convertedPrice = convertedPrice.toFixed(2);
        }
        
        jsonData.push({
          time: epochTime,
          price: convertedPrice,
          currency: currencyUnit
        });
      }
    }
    
    // same thing for tomorrow if we have it
    if (priceInfo.tomorrow && priceInfo.tomorrow.length > 0) {
      for (var i = 0; i < priceInfo.tomorrow.length; i++) {
        var priceEntry = priceInfo.tomorrow[i];
        var epochTime = new Date(priceEntry.startsAt).getTime() / 1000;
        var apiCurrency = priceEntry.currency || 'NOK';
        
        // pick energy or total price
        var priceValue = (priceType === 'energy') ? priceEntry.energy : priceEntry.total;
        var convertedPrice = parseFloat(priceValue);
        
        // format the price as string to preserve 2 decimal places
        if (currencyUnit === 'ct') {
          // convert to cents
          convertedPrice = (convertedPrice * 100).toFixed(2);
        } else {
          // format to 2 decimal places for proper currency display
          convertedPrice = convertedPrice.toFixed(2);
        }
        
        jsonData.push({
          time: epochTime,
          price: convertedPrice,
          currency: currencyUnit
        });
      }
    }

    // sort by time just to be safe
    jsonData.sort(function(a, b) {
      return a.time - b.time;
    });

    // keep only last 36 entries like before
    if (jsonData.length > 36) {
      jsonData.splice(0, jsonData.length - 36);
    }

    var jsonOutput = JSON.stringify(jsonData);
    logger('returning data: ' + jsonOutput);

    return ContentService.createTextOutput(jsonOutput)
      .setMimeType(ContentService.MimeType.JSON);

  } catch (error) {
    logger('something went wrong: ' + error.toString());
    return ContentService.createTextOutput('Error: ' + error.toString())
      .setMimeType(ContentService.MimeType.TEXT);
  }
}

// wrapper function for easier testing
function getTibberPrices(apiKey) {
  return doGet({parameter: {apiKey: apiKey}});
}

// basic test function - put your real api key here
function testTibberAPI() {
  var testApiKey = 'YOUR_ACTUAL_TIBBER_API_KEY_HERE'; // change this
  
  console.log('testing api call...');
  var result = doGet({parameter: {apiKey: testApiKey}});
  console.log('got back:', result.getContent());
  
  return result;
}

// no cache anymore - function kept for compatibility
function clearTibberCache() {
  console.log('no cache to clear');
  return 'No cache';
}

// debug function with more info
function debugTibberAPI() {
  var testApiKey = 'YOUR_ACTUAL_TIBBER_API_KEY_HERE'; // put your key here
  
  if (testApiKey === 'YOUR_ACTUAL_TIBBER_API_KEY_HERE') {
    console.log('need to set your api key first');
    return;
  }
  
  console.log('testing with key: ' + testApiKey.substring(0, 8) + '***');
  
  try {
    var result = doGet({parameter: {apiKey: testApiKey}});
    var content = result.getContent();
    
    console.log('api call worked');
    console.log('raw response:', content);
    
    // see if we can parse it
    try {
      var parsed = JSON.parse(content);
      console.log('got', parsed.length, 'price points');
      if (parsed.length > 0) {
        console.log('first:', new Date(parsed[0].time * 1000), 'price:', parsed[0].price);
        console.log('last:', new Date(parsed[parsed.length-1].time * 1000), 'price:', parsed[parsed.length-1].price);
      }
    } catch (parseError) {
      console.log('could not parse json - probably an error message');
    }
    
    return result;
    
  } catch (error) {
    console.log('api call failed:', error.toString());
    return null;
  }
}
````
Originally created by @sebnaf on GitHub (Jun 24, 2025). Original GitHub issue: https://github.com/OpenEPaperLink/OpenEPaperLink/issues/479 I really do like the "dayahead" integration as well as representation. however, it differs a lot from current tibber pricing. I would love to see a tibber integration with the two parameters: - the tibber api key(requestable here: https://developer.tibber.com/settings/access-token) - the currency of the user (tibber returns just a pricing using the current users profile). I did a quick mockup which returns the values as close as the dayahead integration: ````` [{"time":1750759200,"price":"0.19","currency":"EUR"},{"time":1750762800,"price":"0.18","currency":"EUR"},{"time":1750766400,"price":"0.19","currency":"EUR"},{"time":1750770000,"price":"0.20","currency":"EUR"},{"time":1750773600,"price":"0.21","currency":"EUR"},{"time":1750777200,"price":"0.21","currency":"EUR"},{"time":1750780800,"price":"0.28","currency":"EUR"},{"time":1750784400,"price":"0.33","currency":"EUR"},{"time":1750788000,"price":"0.36","currency":"EUR"},{"time":1750791600,"price":"0.36","currency":"EUR"},{"time":1750795200,"price":"0.33","currency":"EUR"},{"time":1750798800,"price":"0.33","currency":"EUR"},{"time":1750802400,"price":"0.30","currency":"EUR"},{"time":1750806000,"price":"0.29","currency":"EUR"},{"time":1750809600,"price":"0.29","currency":"EUR"},{"time":1750813200,"price":"0.29","currency":"EUR"},{"time":1750816800,"price":"0.29","currency":"EUR"},{"time":1750820400,"price":"0.31","currency":"EUR"},{"time":1750824000,"price":"0.34","currency":"EUR"},{"time":1750827600,"price":"0.34","currency":"EUR"},{"time":1750831200,"price":"0.33","currency":"EUR"},{"time":1750834800,"price":"0.29","currency":"EUR"},{"time":1750838400,"price":"0.22","currency":"EUR"},{"time":1750842000,"price":"0.21","currency":"EUR"},{"time":1750845600,"price":"0.21","currency":"EUR"},{"time":1750849200,"price":"0.21","currency":"EUR"},{"time":1750852800,"price":"0.21","currency":"EUR"},{"time":1750856400,"price":"0.21","currency":"EUR"},{"time":1750860000,"price":"0.23","currency":"EUR"},{"time":1750863600,"price":"0.31","currency":"EUR"},{"time":1750867200,"price":"0.34","currency":"EUR"},{"time":1750870800,"price":"0.37","currency":"EUR"},{"time":1750874400,"price":"0.45","currency":"EUR"},{"time":1750878000,"price":"0.50","currency":"EUR"},{"time":1750881600,"price":"0.42","currency":"EUR"},{"time":1750885200,"price":"0.36","currency":"EUR"}] ```` the code itself is below: not nice, but working. If somebody could integrate this into one of the next released: i'd be very happy. ```` function doGet(e) { var logger = Logger.log; // Get API key from URL params or fall back to placeholder var apiKey = e?.parameter?.apiKey || 'YOUR_TIBBER_API_KEY_HERE'; // Currency format for output (defaults to EUR) var currencyUnit = e?.parameter?.currency || 'EUR'; // Check if user wants energy price only (without tax) var useEnergyPrice = e?.parameter?.energy === 'true' || e?.parameter?.energy === '1'; var priceType = useEnergyPrice ? 'energy' : 'total'; logger('Got params: apiKey=' + (apiKey ? apiKey.substring(0, 8) + '***' : 'missing') + ', currency=' + currencyUnit + ', energyOnly=' + useEnergyPrice); if (apiKey === 'YOUR_TIBBER_API_KEY_HERE') { logger('Error: Please provide a valid Tibber API key'); return ContentService.createTextOutput('Error: Please provide a valid Tibber API key. Get one from https://developer.tibber.com/settings/access-token') .setMimeType(ContentService.MimeType.TEXT); } try { // Build GraphQL query for today and tomorrow prices var query = `{ viewer { homes { currentSubscription { priceInfo { today { startsAt total energy tax currency } tomorrow { startsAt total energy tax currency } } } } } }`; var payload = { 'query': query }; var options = { 'method': 'POST', 'headers': { 'Authorization': 'Bearer ' + apiKey, 'Content-Type': 'application/json' }, 'payload': JSON.stringify(payload) }; // try api call with retries var data; var responseText; var maxRetries = 3; var retryCount = 0; var success = false; while (retryCount < maxRetries && !success) { try { logger('calling tibber api (attempt ' + (retryCount + 1) + ')'); var response = UrlFetchApp.fetch('https://api.tibber.com/v1-beta/gql', options); responseText = response.getContentText(); data = JSON.parse(responseText); success = true; logger('api call successful'); } catch (apiError) { retryCount++; logger('api call failed (attempt ' + retryCount + '): ' + apiError.toString()); if (retryCount < maxRetries) { logger('waiting before retry...'); Utilities.sleep(1000 * retryCount); // wait 1s, 2s, 3s between retries } else { throw new Error('api failed after ' + maxRetries + ' attempts: ' + apiError.toString()); } } } logger('api response: ' + responseText); if (data.errors) { logger('api returned errors: ' + JSON.stringify(data.errors)); return ContentService.createTextOutput('Error: ' + JSON.stringify(data.errors)) .setMimeType(ContentService.MimeType.TEXT); } if (!data.data || !data.data.viewer || !data.data.viewer.homes || data.data.viewer.homes.length === 0) { logger('no homes found in response'); return ContentService.createTextOutput('Error: No homes found. Make sure your API key is valid and you have a Tibber subscription.') .setMimeType(ContentService.MimeType.TEXT); } var home = data.data.viewer.homes[0]; // just use the first home var priceInfo = home.currentSubscription.priceInfo; var jsonData = []; // grab today's hourly prices if (priceInfo.today && priceInfo.today.length > 0) { for (var i = 0; i < priceInfo.today.length; i++) { var priceEntry = priceInfo.today[i]; var epochTime = new Date(priceEntry.startsAt).getTime() / 1000; var apiCurrency = priceEntry.currency || 'NOK'; // pick energy or total price based on what user wants var priceValue = (priceType === 'energy') ? priceEntry.energy : priceEntry.total; var convertedPrice = parseFloat(priceValue); // format the price as string to preserve 2 decimal places if (currencyUnit === 'ct') { // convert to cents convertedPrice = (convertedPrice * 100).toFixed(2); } else { // format to 2 decimal places for proper currency display convertedPrice = convertedPrice.toFixed(2); } jsonData.push({ time: epochTime, price: convertedPrice, currency: currencyUnit }); } } // same thing for tomorrow if we have it if (priceInfo.tomorrow && priceInfo.tomorrow.length > 0) { for (var i = 0; i < priceInfo.tomorrow.length; i++) { var priceEntry = priceInfo.tomorrow[i]; var epochTime = new Date(priceEntry.startsAt).getTime() / 1000; var apiCurrency = priceEntry.currency || 'NOK'; // pick energy or total price var priceValue = (priceType === 'energy') ? priceEntry.energy : priceEntry.total; var convertedPrice = parseFloat(priceValue); // format the price as string to preserve 2 decimal places if (currencyUnit === 'ct') { // convert to cents convertedPrice = (convertedPrice * 100).toFixed(2); } else { // format to 2 decimal places for proper currency display convertedPrice = convertedPrice.toFixed(2); } jsonData.push({ time: epochTime, price: convertedPrice, currency: currencyUnit }); } } // sort by time just to be safe jsonData.sort(function(a, b) { return a.time - b.time; }); // keep only last 36 entries like before if (jsonData.length > 36) { jsonData.splice(0, jsonData.length - 36); } var jsonOutput = JSON.stringify(jsonData); logger('returning data: ' + jsonOutput); return ContentService.createTextOutput(jsonOutput) .setMimeType(ContentService.MimeType.JSON); } catch (error) { logger('something went wrong: ' + error.toString()); return ContentService.createTextOutput('Error: ' + error.toString()) .setMimeType(ContentService.MimeType.TEXT); } } // wrapper function for easier testing function getTibberPrices(apiKey) { return doGet({parameter: {apiKey: apiKey}}); } // basic test function - put your real api key here function testTibberAPI() { var testApiKey = 'YOUR_ACTUAL_TIBBER_API_KEY_HERE'; // change this console.log('testing api call...'); var result = doGet({parameter: {apiKey: testApiKey}}); console.log('got back:', result.getContent()); return result; } // no cache anymore - function kept for compatibility function clearTibberCache() { console.log('no cache to clear'); return 'No cache'; } // debug function with more info function debugTibberAPI() { var testApiKey = 'YOUR_ACTUAL_TIBBER_API_KEY_HERE'; // put your key here if (testApiKey === 'YOUR_ACTUAL_TIBBER_API_KEY_HERE') { console.log('need to set your api key first'); return; } console.log('testing with key: ' + testApiKey.substring(0, 8) + '***'); try { var result = doGet({parameter: {apiKey: testApiKey}}); var content = result.getContent(); console.log('api call worked'); console.log('raw response:', content); // see if we can parse it try { var parsed = JSON.parse(content); console.log('got', parsed.length, 'price points'); if (parsed.length > 0) { console.log('first:', new Date(parsed[0].time * 1000), 'price:', parsed[0].price); console.log('last:', new Date(parsed[parsed.length-1].time * 1000), 'price:', parsed[parsed.length-1].price); } } catch (parseError) { console.log('could not parse json - probably an error message'); } return result; } catch (error) { console.log('api call failed:', error.toString()); return null; } } ````
sascha_hemi added the enhancement label 2026-03-20 20:07:58 +01:00
Author
Owner

@nlimper commented on GitHub (Jun 24, 2025):

The DayAhead content is universal within the EU and uses ENTSOE as the source. The Tibber prices are derived from it (I don't know about Germany, but in the Netherlands: 12.68 fixed surcharge, and tax percentage 21 will result in the correct Tibber price).
Keep in mind that shortly (within a few months) the hourly charges will change to a 15 minute interval. As soon as ENTSOE proves a stable output for that, I will fix the dayahead content.
I'm not very keen on implementing a brand-specific solution, but feel free to do a pull request for Tibber content.

<!-- gh-comment-id:3001801014 --> @nlimper commented on GitHub (Jun 24, 2025): The DayAhead content is universal within the EU and uses ENTSOE as the source. The Tibber prices are derived from it (I don't know about Germany, but in the Netherlands: 12.68 fixed surcharge, and tax percentage 21 will result in the correct Tibber price). Keep in mind that shortly (within a few months) the hourly charges will change to a 15 minute interval. As soon as ENTSOE proves a stable output for that, I will fix the dayahead content. I'm not very keen on implementing a brand-specific solution, but feel free to do a pull request for Tibber content.
Author
Owner

@sebnaf commented on GitHub (Jun 24, 2025):

Thanks for your reply, @nlimper .

I am afraid I am not able to fully develop the tibber version myself due to lack of knowledge.

The tibber pricing varies quite a lot depending from the german region you are living in. As we speak I do see +21 cents compared to the ENTSOE source.

Therefore I came up with the mockup…

<!-- gh-comment-id:3001824730 --> @sebnaf commented on GitHub (Jun 24, 2025): Thanks for your reply, @nlimper . I am afraid I am not able to fully develop the tibber version myself due to lack of knowledge. The tibber pricing varies quite a lot depending from the german region you are living in. As we speak I do see +21 cents compared to the ENTSOE source. Therefore I came up with the mockup…
Author
Owner

@nlimper commented on GitHub (Jun 24, 2025):

Can you find a link which explains the calculation of the Tibber price? In the end they are based in ENTSOE prices in some way.

<!-- gh-comment-id:3001862519 --> @nlimper commented on GitHub (Jun 24, 2025): Can you find a link which explains the calculation of the Tibber price? In the end they *are* based in ENTSOE prices in some way.
Author
Owner

@sebnaf commented on GitHub (Jun 24, 2025):

This is not possible I am afraid.

Tibber prices can vary from city to city mainly because the total electricity cost for each customer includes not just the hourly market price (EPEX Spot, Nordpool, etc.), but also several local charges and fees. These include grid usage fees, concession fees, and sometimes local taxes, all of which are set by regional network operators and municipal authorities. Since these additional costs depend on your specific location and local regulations, the final price shown in the Tibber app will differ between cities, even if the wholesale electricity price is the same everywhere.
Additionally, some cities or regions may have slightly different calculation methods for these fees, which can also lead to price differences.

I am aware of the 15 minutes pricing model, however I am not happy with this. Tibber’s 15-minute pricing reflects real-time electricity market fluctuations, but for many household activities—like running a washing machine or charging an electric car—15 minutes is too short a period to be practical. Most appliances and devices need at least one hour to complete a cycle or charge safely and efficiently. Calculating an hourly average price makes it easier to plan and optimize energy usage, ensuring you get the best value for typical, real-world usage patterns without risking damage or inconvenience.

<!-- gh-comment-id:3001940211 --> @sebnaf commented on GitHub (Jun 24, 2025): This is not possible I am afraid. Tibber prices can vary from city to city mainly because the total electricity cost for each customer includes not just the hourly market price (EPEX Spot, Nordpool, etc.), but also several local charges and fees. These include grid usage fees, concession fees, and sometimes local taxes, all of which are set by regional network operators and municipal authorities. Since these additional costs depend on your specific location and local regulations, the final price shown in the Tibber app will differ between cities, even if the wholesale electricity price is the same everywhere. Additionally, some cities or regions may have slightly different calculation methods for these fees, which can also lead to price differences. I am aware of the 15 minutes pricing model, however I am not happy with this. Tibber’s 15-minute pricing reflects real-time electricity market fluctuations, but for many household activities—like running a washing machine or charging an electric car—15 minutes is too short a period to be practical. Most appliances and devices need at least one hour to complete a cycle or charge safely and efficiently. Calculating an hourly average price makes it easier to plan and optimize energy usage, ensuring you get the best value for typical, real-world usage patterns without risking damage or inconvenience.
Author
Owner

@sebnaf commented on GitHub (Jun 25, 2025):

Today's diff…

Image
Image

<!-- gh-comment-id:3005241533 --> @sebnaf commented on GitHub (Jun 25, 2025): Today's diff… ![Image](https://github.com/user-attachments/assets/d132e5eb-2baa-4b55-9a92-52d6d26427a8) ![Image](https://github.com/user-attachments/assets/7cdae1d0-1081-471c-9fa6-f1a23f6b8b26)
Author
Owner

@nlimper commented on GitHub (Jun 25, 2025):

Looks like the ENTSOE Germany region already switched to 15 minute intervals. I will fix it at some point, but it will take a while. Of course, if anyone wants to give it a go, they're welcome to fix it too.

<!-- gh-comment-id:3005522555 --> @nlimper commented on GitHub (Jun 25, 2025): Looks like the ENTSOE Germany region already switched to 15 minute intervals. I will fix it at some point, but it will take a while. Of course, if anyone wants to give it a go, they're welcome to fix it too.
Author
Owner

@sebnaf commented on GitHub (Jul 9, 2025):

Update: I have solved this by using the Tibber integration in home assistant and rendering the content.

If somebody is interested in this, there you go:

alias: Tibber E-Paper Display (stündlich plus 1 min)
description: ""
triggers:
  - minutes: "1"
    trigger: time_pattern
conditions:
  - condition: template
    value_template: "{{ now().minute == 1 }}"
actions:
  - data:
      device_id: 079b0----4232a
      backgroundcolor: white
      dryrun: false
      payload: >
        {% set prices = state_attr('sensor.tibber_electricity_price', 'today') +
        state_attr('sensor.tibber_electricity_price', 'tomorrow') %} {% set
        today_str = now().strftime('%Y-%m-%d') %} {% set todays = prices |
        selectattr('startsAt', 'search', today_str) | list %} {% set min_price =
        (todays | map(attribute='total') | min * 100) | round(1) %} {% set
        max_price = (todays | map(attribute='total') | max * 100) | round(1) %}
        {% set avg_price = ((todays | map(attribute='total') | list | sum) /
        (todays | length)) %} {% set maxp = prices[:24] | map(attribute='total')
        | max %} {% set now_hour = (now() - timedelta(minutes=1)).hour %} {% set
        display_time = (now() - timedelta(minutes=1)).strftime('%H:%M') %} {%
        set max_ct = (maxp * 100) | round(0) | int %} {% set price_labels =
        range(10, ((max_ct // 10 + 1) * 10) + 1, 10) %} {% set avg_y = 130 -
        ((avg_price / maxp) * 80) | round(0) %} [ {
          "type": "text", "value": "{{ display_time }}", "x": 10, "y": 10,
          "size": 24, "color": "black", "font": "fonts/bahnschrift20.vlw"
        }, {
          "type": "text", "value": "MIN:", "x": 80, "y": 8,
          "size": 10, "color": "black", "align": "right"
        }, {
          "type": "text", "value": "MAX:", "x": 95, "y": 20,
          "size": 10, "color": "black"
        }, {
          "type": "text", "value": "{{ min_price }}", "x": 125, "y": 8,
          "size": 10, "color": "black"
        }, {
          "type": "text", "value": "{{ max_price }}", "x": 125, "y": 19,
          "size": 10, "color": "black"
        }, {
          "type": "text",
          "value": "{{ (state_attr('sensor.tibber_electricity_price','current').total * 100) | round(1) }}",
          "x": 165, "y": 9,
          "size": 24, "color": "black", "font": "fonts/bahnschrift20.vlw", "align": "right"
        }, {
          "type": "text", "value": "ct/kWh", "x": 226, "y": 12,
          "size": 16, "color": "black", "font": "fonts/bahnschrift20"
        } {% for p in price_labels %} ,{
          "type": "text", "value": "{{ p }}", "x": 5,
          "y": {{ 130 - (p / (maxp * 100) * 80) | round(0) }},
          "size": 10, "color": "black", "font": "fonts/twcondensed20.vlw"
        }, {
          "type": "line",
          "x_start": 26, "x_end": 291,
          "y_start": {{ 130 - (p / (maxp * 100) * 80) | round(0) }},
          "y_end": {{ 130 - (p / (maxp * 100) * 80) | round(0) }},
          "fill": "half_black"
        } {% endfor %} , {
          "type": "line",
          "x_start": 26, "x_end": 291,
          "y_start": {{ avg_y }},
          "y_end": {{ avg_y }},
          "fill": "half_red"
        } {% for hr in prices[:24] %} ,{
          {% set idx = loop.index0 %}
          {% set stunde = hr.startsAt[11:13] | int %}
          {% set height = ((hr.total / maxp) * 80) | round(0) %}
          {% set x = 30 + idx * 11 %}
          {% set y = 130 - height %}
          {% set level = hr.level.upper() %}
          {% set fill =
            'white' if level == 'VERY_CHEAP' else
            'gray' if level == 'CHEAP' else
            'black' if level == 'NORMAL' else
            'half_red' if level == 'EXPENSIVE' else
            'red' %}
          "type": "rectangle",
          "x_start": {{ x }}, "x_end": {{ x + 6 }},
          "y_start": {{ y }}, "y_end": 130,
          "fill": "{{ fill }}"
        }, {
          "type": "text",
          "value": "{{ stunde }}",
          "x": {{ x }}, "y": 135,
          "size": 8, "color": "black", "font": "twcondensed20.wlv"
        } {% if stunde == now_hour %} , {
          "type": "polygon",
          "points": [ [{{ x + 4 }},139], [{{ x }},146], [{{ x + 8 }},146] ],
          "fill": "red"
        } {% endif %} {% endfor %} ]
    action: open_epaper_link.drawcustom
<!-- gh-comment-id:3051726492 --> @sebnaf commented on GitHub (Jul 9, 2025): Update: I have solved this by using the Tibber integration in home assistant and rendering the content. If somebody is interested in this, there you go: ````` alias: Tibber E-Paper Display (stündlich plus 1 min) description: "" triggers: - minutes: "1" trigger: time_pattern conditions: - condition: template value_template: "{{ now().minute == 1 }}" actions: - data: device_id: 079b0----4232a backgroundcolor: white dryrun: false payload: > {% set prices = state_attr('sensor.tibber_electricity_price', 'today') + state_attr('sensor.tibber_electricity_price', 'tomorrow') %} {% set today_str = now().strftime('%Y-%m-%d') %} {% set todays = prices | selectattr('startsAt', 'search', today_str) | list %} {% set min_price = (todays | map(attribute='total') | min * 100) | round(1) %} {% set max_price = (todays | map(attribute='total') | max * 100) | round(1) %} {% set avg_price = ((todays | map(attribute='total') | list | sum) / (todays | length)) %} {% set maxp = prices[:24] | map(attribute='total') | max %} {% set now_hour = (now() - timedelta(minutes=1)).hour %} {% set display_time = (now() - timedelta(minutes=1)).strftime('%H:%M') %} {% set max_ct = (maxp * 100) | round(0) | int %} {% set price_labels = range(10, ((max_ct // 10 + 1) * 10) + 1, 10) %} {% set avg_y = 130 - ((avg_price / maxp) * 80) | round(0) %} [ { "type": "text", "value": "{{ display_time }}", "x": 10, "y": 10, "size": 24, "color": "black", "font": "fonts/bahnschrift20.vlw" }, { "type": "text", "value": "MIN:", "x": 80, "y": 8, "size": 10, "color": "black", "align": "right" }, { "type": "text", "value": "MAX:", "x": 95, "y": 20, "size": 10, "color": "black" }, { "type": "text", "value": "{{ min_price }}", "x": 125, "y": 8, "size": 10, "color": "black" }, { "type": "text", "value": "{{ max_price }}", "x": 125, "y": 19, "size": 10, "color": "black" }, { "type": "text", "value": "{{ (state_attr('sensor.tibber_electricity_price','current').total * 100) | round(1) }}", "x": 165, "y": 9, "size": 24, "color": "black", "font": "fonts/bahnschrift20.vlw", "align": "right" }, { "type": "text", "value": "ct/kWh", "x": 226, "y": 12, "size": 16, "color": "black", "font": "fonts/bahnschrift20" } {% for p in price_labels %} ,{ "type": "text", "value": "{{ p }}", "x": 5, "y": {{ 130 - (p / (maxp * 100) * 80) | round(0) }}, "size": 10, "color": "black", "font": "fonts/twcondensed20.vlw" }, { "type": "line", "x_start": 26, "x_end": 291, "y_start": {{ 130 - (p / (maxp * 100) * 80) | round(0) }}, "y_end": {{ 130 - (p / (maxp * 100) * 80) | round(0) }}, "fill": "half_black" } {% endfor %} , { "type": "line", "x_start": 26, "x_end": 291, "y_start": {{ avg_y }}, "y_end": {{ avg_y }}, "fill": "half_red" } {% for hr in prices[:24] %} ,{ {% set idx = loop.index0 %} {% set stunde = hr.startsAt[11:13] | int %} {% set height = ((hr.total / maxp) * 80) | round(0) %} {% set x = 30 + idx * 11 %} {% set y = 130 - height %} {% set level = hr.level.upper() %} {% set fill = 'white' if level == 'VERY_CHEAP' else 'gray' if level == 'CHEAP' else 'black' if level == 'NORMAL' else 'half_red' if level == 'EXPENSIVE' else 'red' %} "type": "rectangle", "x_start": {{ x }}, "x_end": {{ x + 6 }}, "y_start": {{ y }}, "y_end": 130, "fill": "{{ fill }}" }, { "type": "text", "value": "{{ stunde }}", "x": {{ x }}, "y": 135, "size": 8, "color": "black", "font": "twcondensed20.wlv" } {% if stunde == now_hour %} , { "type": "polygon", "points": [ [{{ x + 4 }},139], [{{ x }},146], [{{ x + 8 }},146] ], "fill": "red" } {% endif %} {% endfor %} ] action: open_epaper_link.drawcustom `````
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/OpenEPaperLink#1955