Zonneplan ONE custom component

Because the number of hours that is available as forecast is not always the same one the next 8hours are dedicated sensors. But the full forecast is available as sensor attribute, see GitHub - fsaris/home-assistant-zonneplan-one: Unofficial Zonneplan integration for Home Assistant Current Zonneplan Electricity tariff

For usage examples see for instance Finally got the forecast :D ... · fsaris/home-assistant-zonneplan-one · Discussion #47 · GitHub

Thank you for this! I have bought the Zonneplan Nexus and because of this I started using Home Assistant to make a first step in automation ideas. Your script (besides the nicely working api) is the first thing that I actually got working :slight_smile:

(HA has quite a learning curve…)

[update]
I’ve got a small request, I would really like to subtract/add the charging/discharging of the Nexus in the stats/view etc., could someone explain to me how to do this?

Got the same problem, did you already fixed it?

@Erikkyw @BasPost
Every day at 0:00 the sensor becomes unavailable. I have made template sensor which will have a value of 0 at 0:00, that fixes the problem

## zonneplan.yaml
## Create template sensors with value "0" when original sensors are unavailable at start every day
template:
  - sensor:
      - name: "ZP Electricity returned today"
        unit_of_measurement: kWh
        device_class: energy
        state_class: total_increasing
        icon: mdi:lightning-bolt
        state: >
          {% if is_number(states('sensor.zonneplan_electricity_returned_today')) %}
            {{ states('sensor.zonneplan_electricity_returned_today') }}
          {% else %}
            0
          {% endif %}

      - name: "ZP Electricity production costs today"
        unit_of_measurement: EUR
        device_class: monetary
        state_class: total
        icon: mdi:cash
        state: >
          {% if is_number(states('sensor.zonneplan_electricity_production_costs_today')) %}
            {{ states('sensor.zonneplan_electricity_production_costs_today') }}
          {% else %}
            0
          {% endif %}

      - name: "ZP Electricity consumption today"
        unit_of_measurement: kWh
        device_class: energy
        state_class: total_increasing
        icon: mdi:lightning-bolt
        state: >
          {% if is_number(states('sensor.zonneplan_electricity_consumption_today')) %}
            {{ states('sensor.zonneplan_electricity_consumption_today') }}
          {% else %}
            0
          {% endif %}

      - name: "ZP Electricity delivery costs today"
        unit_of_measurement: EUR
        device_class: monetary
        state_class: total
        icon: mdi:cash
        state: >
          {% if is_number(states('sensor.zonneplan_electricity_delivery_costs_today')) %}
            {{ states('sensor.zonneplan_electricity_delivery_costs_today') }}
          {% else %}
            0
          {% endif %}

and the card:

type: custom:stack-in-card
cards:
  - type: horizontal-stack
    cards:
      - square: false
        type: grid
        cards:
          - type: custom:mushroom-title-card
            title: Zonneplan
            subtitle: Vandaag
          - type: custom:mushroom-title-card
            title: >
              € {{ ((states.sensor.zp_electricity_delivery_costs_today.state |
              float) -
              (states.sensor.zp_electricity_production_costs_today.state |
              float)) | round(2) }}
            card_mod:
              style: |
                .title {
                  text-align: right;
                  font-size: 40px!important;
                  }
        columns: 2
        style: |
          ha-card {
            border: none;
          }
  - type: horizontal-stack
    cards:
      - square: false
        type: grid
        cards:
          - type: custom:mushroom-title-card
            title: Afname
            subtitle: >-
              {{ states('sensor.zp_electricity_consumption_today') | round(2)}}
              kWh  x {{
              (((states.sensor.zp_electricity_delivery_costs_today.state |
              float) / (states.sensor.zp_electricity_consumption_today.state |
              float)) * 100) | round(2) }} ct
            card_mod:
              style: |
                .title {
                  font-size: 18px!important;
                  color: rgba(255, 255, 255,0.5)!important;
                  font-weight: 300!important;
                  }
          - type: custom:mushroom-title-card
            title: Teruglevering
            subtitle: >-
              {{ states ('sensor.zp_electricity_returned_today') | round(2) }}
              kWh x  {{ (((
              states.sensor.zp_electricity_production_costs_today.state | float)
              / (states.sensor.zp_electricity_returned_today.state | float)) *
              100) | round(2) }} ct
            card_mod:
              style: |
                .title {
                  font-size: 18px!important;
                  color: rgba(255, 255, 255,0.5)!important;
                  text-align: right;
                  font-weight: 300!important;
                  }
                .subtitle { 
                  text-align: right!important;
                  }
        columns: 2
        style: |
          ha-card {
            border: none;
          }
card_mod:
  style: |
    ha-card {
      background-color: #00A964;
      --primary-text-color: #FFFFFF;
      --secondary-text-color: #FFFFFF;
      border-radius: 20px;
      padding: 5px 5px;
        }

I’m sharing an updated version of the pricing forecast chart. It uses the apexchart HACS card.

type: custom:apexcharts-card
graph_span: 35h
span:
  start: hour
  offset: "-2h"
header:
  show: false
stacked: true
apex_config:
  chart:
    height: 170
    fontFamily: inherit
  legend:
    show: false
  tooltip:
    shared: false
    x:
      format: d MMM, HH:00
    "y":
      formatter: |
        EVAL:function(value) {
          return value.toFixed(1).replace('.', ',') + " ct";
        }
  plotOptions:
    bar:
      borderRadius: 4
      columnWidth: 65%
  grid:
    borderColor: rgba(0,0,0,0.06)
    strokeDashArray: 0
    xaxis:
      lines:
        show: false
    yaxis:
      lines:
        show: true
  dataLabels:
    background:
      enabled: true
      borderWidth: 0
      borderRadius: 6
      dropShadow:
        enabled: false
    formatter: |
      EVAL:function(value) { 
        if (value === null || typeof value === 'undefined') return "";
        return Math.round(value) + " ct"; 
      }
  xaxis:
    labels:
      format: HH
      style:
        colors: "#a0a0a0"
    axisBorder:
      show: false
    axisTicks:
      show: false
    tooltip:
      enabled: false
  yaxis:
    labels:
      formatter: |
        EVAL:function(value) { return Math.round(value); }
      style:
        colors: "#a0a0a0"
  annotations:
    xaxis:
      - x: EVAL:new Date().setHours(24,0,0,0)
        strokeDashArray: 0
        borderColor: "#e0e0e0"
        borderWidth: 1
        label:
          text: morgen
          borderWidth: 0
          orientation: horizontal
          offsetY: -9
          style:
            color: "#a0a0a0"
            background: transparent
            fontSize: 12px
    points:
      - x: EVAL:new Date().setMinutes(0,0,0)
        "y": >-
          EVAL:(parseFloat(document.querySelector('home-assistant')?.hass?.states['sensor.zonneplan_current_electricity_tariff']?.state
          || 0) * 100)
        marker:
          size: 0
        label:
          text: ▼
          borderWidth: 0
          offsetY: 5
          style:
            color: "#888888"
            background: transparent
            fontSize: 16px
series:
  - entity: sensor.zonneplan_current_electricity_tariff
    name: Tarief
    type: column
    color: "#e0e0e0"
    show:
      datalabels: false
    data_generator: |
      const currentHourStart = new Date().setMinutes(0,0,0);
      return entity.attributes.forecast.map((e) => {
        const t = new Date(e.datetime).getTime();
        const p = (e.electricity_price / 10000000) * 100;
        return t < currentHourStart ? [t, p] : [t, null];
      });
  - entity: sensor.zonneplan_current_electricity_tariff
    name: Tarief
    type: column
    color: "#3de385"
    show:
      datalabels: false
    data_generator: |
      const currentHourStart = new Date().setMinutes(0,0,0);
      return entity.attributes.forecast.map((e) => {
        const t = new Date(e.datetime).getTime();
        const p = (e.electricity_price / 10000000) * 100;
        const isLow = e.tariff_group.includes("low") || p < 0;
        return (t >= currentHourStart && isLow) ? [t, p] : [t, null];
      });
  - entity: sensor.zonneplan_current_electricity_tariff
    name: Tarief
    type: column
    color: "#00a964"
    show:
      datalabels: false
    data_generator: |
      const currentHourStart = new Date().setMinutes(0,0,0);
      return entity.attributes.forecast.map((e) => {
        const t = new Date(e.datetime).getTime();
        const p = (e.electricity_price / 10000000) * 100;
        const isLow = e.tariff_group.includes("low") || p < 0;
        return (t >= currentHourStart && !isLow) ? [t, p] : [t, null];
      });
  - entity: sensor.zonneplan_current_electricity_tariff
    name: Max
    type: line
    color: "#00a964"
    show:
      datalabels: true
    data_generator: |
      const now = Date.now() - 3600000; 
      let max = {t: null, p: -999};
      entity.attributes.forecast.forEach(e => {
        const t = new Date(e.datetime).getTime();
        const p = (e.electricity_price / 10000000) * 100;
        if (t >= now && p > max.p) max = {t, p};
      });
      return entity.attributes.forecast.map(e => {
         const t = new Date(e.datetime).getTime();
         return (t === max.t) ? [t, max.p] : [t, null];
      });
  - entity: sensor.zonneplan_current_electricity_tariff
    name: Min
    type: line
    color: "#3de385"
    show:
      datalabels: true
    data_generator: |
      const now = Date.now() - 3600000; 
      let min = {t: null, p: 999};
      entity.attributes.forecast.forEach(e => {
        const t = new Date(e.datetime).getTime();
        const p = (e.electricity_price / 10000000) * 100;
        if (t >= now && p < min.p) min = {t, p};
      });
      return entity.attributes.forecast.map(e => {
         const t = new Date(e.datetime).getTime();
         return (t === min.t) ? [t, min.p] : [t, null];
      });

created a Apex Chart dahboard with some options about current, next hour price and average of today

type: custom:apexcharts-card
graph_span: 36h
span:
  start: hour
  offset: "-4h"
header:
  show: true
  title: Energieprijs Zonneplan
  show_states: true
  colorize_states: false
apex_config:
  chart:
    height: 220
    fontFamily: inherit
    stacked: true
  legend:
    show: false
  tooltip:
    shared: false
    x:
      format: d MMM, HH:00
    "y":
      formatter: |
        EVAL:function(value) {
          if (value === null || typeof value === 'undefined') return "";
          return value.toFixed(1).replace('.', ',') + " ct";
        }
  plotOptions:
    bar:
      borderRadius: 1
      columnWidth: 60%
  grid:
    borderColor: rgba(255,255,255,0.06)
    strokeDashArray: 0
    xaxis:
      lines:
        show: false
    yaxis:
      lines:
        show: false
  dataLabels:
    background:
      enabled: true
      borderWidth: 0
      borderRadius: 6
      dropShadow:
        enabled: false
    formatter: |
      EVAL:function(value) {
        if (value === null || typeof value === 'undefined') return "";
        return Math.round(value) + " ct";
      }
  xaxis:
    labels:
      format: HH
      style:
        colors: "#a0a0a0"
    axisBorder:
      show: false
    axisTicks:
      show: false
    tooltip:
      enabled: false
  yaxis:
    min: -10
    labels:
      formatter: |
        EVAL:function(value) {
          return Math.round(value);
        }
      style:
        colors: "#a0a0a0"
  annotations:
    xaxis:
      - x: EVAL:new Date().setHours(24,0,0,0)
        strokeDashArray: 0
        borderColor: "#e0e0e0"
        borderWidth: 1
        label:
          text: morgen
          borderWidth: 0
          orientation: horizontal
          offsetY: -9
          style:
            color: "#a0a0a0"
            background: transparent
            fontSize: 12px
    yaxis:
      - "y": -10
        borderColor: rgba(255,255,255,0.10)
        borderWidth: 1
        strokeDashArray: 0
      - "y": 0
        borderColor: rgba(255,255,255,0.15)
        borderWidth: 1
        strokeDashArray: 0
      - "y": 10
        borderColor: rgba(255,255,255,0.10)
        borderWidth: 1
        strokeDashArray: 0
      - "y": 20
        borderColor: rgba(255,255,255,0.10)
        borderWidth: 1
        strokeDashArray: 0
      - "y": 30
        borderColor: rgba(255,255,255,0.10)
        borderWidth: 1
        strokeDashArray: 0
      - "y": 40
        borderColor: rgba(255,255,255,0.10)
        borderWidth: 1
        strokeDashArray: 0
    points:
      - x: EVAL:new Date().setMinutes(0,0,0)
        "y": |
          EVAL:(parseFloat(
            document.querySelector('home-assistant')?.hass?.states['sensor.zonneplan_current_electricity_tariff']?.state || 0
          ) * 100)
        marker:
          size: 0
        label:
          text: ▼
          borderWidth: 0
          offsetY: 5
          style:
            color: "#888888"
            background: transparent
            fontSize: 16px
series:
  - entity: sensor.zonneplan_current_electricity_tariff
    name: Nu
    unit: ""
    show:
      in_chart: false
      in_header: true
    transform: |
      return (Number(x) * 100).toFixed(0);
  - entity: sensor.zonneplan_current_electricity_tariff
    name: Volgend
    unit: ""
    show:
      in_chart: false
      in_header: true
    data_generator: >
      const now = new Date();

      now.setMinutes(0,0,0);


      const forecast = entity.attributes.forecast || [];

      const next = forecast.find(e => new Date(e.datetime).getTime() >
      now.getTime());


      if (!next) return [];


      const t = new Date(next.datetime).getTime();

      const p = (next.electricity_price / 10000000) * 100;


      return [[t, p]];
    transform: |
      return Number(x).toFixed(1);
  - entity: sensor.zonneplan_current_electricity_tariff
    name: Gem. vandaag
    unit: ""
    show:
      in_chart: false
      in_header: true
    data_generator: |
      const forecast = entity.attributes.forecast || [];
      const today = new Date().toDateString();

      const prices = forecast
        .filter(e => new Date(e.datetime).toDateString() === today)
        .map(e => (e.electricity_price / 10000000) * 100);

      if (!prices.length) return [];

      const avg = prices.reduce((a, b) => a + b, 0) / prices.length;
      return [[Date.now(), avg]];
    transform: |
      return Number(x).toFixed(1);
  - entity: sensor.zonneplan_current_electricity_tariff
    name: Tarief verleden
    type: column
    color: "#e0e0e0"
    show:
      datalabels: false
      in_header: false
    data_generator: |
      const currentHourStart = new Date().setMinutes(0,0,0);
      const forecast = (entity.attributes.forecast || []).slice()
        .sort((a, b) => new Date(a.datetime) - new Date(b.datetime));

      return forecast.map(e => {
        const t = new Date(e.datetime).getTime();
        const p = (e.electricity_price / 10000000) * 100;
        return t < currentHourStart ? [t, p] : [t, null];
      });
  - entity: sensor.zonneplan_current_electricity_tariff
    name: Tarief laag
    type: column
    color: "#3de385"
    show:
      datalabels: false
      in_header: false
    data_generator: |
      const currentHourStart = new Date().setMinutes(0,0,0);
      const forecast = (entity.attributes.forecast || []).slice()
        .sort((a, b) => new Date(a.datetime) - new Date(b.datetime));

      return forecast.map(e => {
        const t = new Date(e.datetime).getTime();
        const p = (e.electricity_price / 10000000) * 100;
        const low = (e.tariff_group || "").includes("low") || p < 0;
        return (t >= currentHourStart && low) ? [t, p] : [t, null];
      });
  - entity: sensor.zonneplan_current_electricity_tariff
    name: Tarief normaal
    type: column
    color: "#00a964"
    show:
      datalabels: false
      in_header: false
    data_generator: |
      const currentHourStart = new Date().setMinutes(0,0,0);
      const forecast = (entity.attributes.forecast || []).slice()
        .sort((a, b) => new Date(a.datetime) - new Date(b.datetime));

      return forecast.map(e => {
        const t = new Date(e.datetime).getTime();
        const p = (e.electricity_price / 10000000) * 100;
        const low = (e.tariff_group || "").includes("low") || p < 0;
        return (t >= currentHourStart && !low) ? [t, p] : [t, null];
      });
  - entity: sensor.zonneplan_current_electricity_tariff
    name: Max
    type: line
    color: "#00a964"
    stroke_width: 0
    show:
      datalabels: true
      in_header: false
    data_generator: |
      const forecast = entity.attributes.forecast || [];
      const today = new Date().toDateString();

      let max = { p: -999, t: null };

      forecast.forEach(e => {
        const t = new Date(e.datetime).getTime();
        const d = new Date(e.datetime).toDateString();
        const p = (e.electricity_price / 10000000) * 100;

        if (d === today && p > max.p) {
          max = { p, t };
        }
      });

      let placed = false;
      return forecast.map(e => {
        const t = new Date(e.datetime).getTime();
        const d = new Date(e.datetime).toDateString();
        const p = (e.electricity_price / 10000000) * 100;

        if (!placed && d === today && Math.abs(p - max.p) < 0.01) {
          placed = true;
          return [t, p];
        }
        return [t, null];
      });
  - entity: sensor.zonneplan_current_electricity_tariff
    name: Min
    type: line
    color: "#3de385"
    stroke_width: 0
    show:
      datalabels: true
      in_header: false
    data_generator: |
      const forecast = entity.attributes.forecast || [];
      const today = new Date().toDateString();

      let min = { p: 999, t: null };

      forecast.forEach(e => {
        const t = new Date(e.datetime).getTime();
        const d = new Date(e.datetime).toDateString();
        const p = (e.electricity_price / 10000000) * 100;

        if (d === today && p < min.p) {
          min = { p, t };
        }
      });

      let placed = false;
      return forecast.map(e => {
        const t = new Date(e.datetime).getTime();
        const d = new Date(e.datetime).toDateString();
        const p = (e.electricity_price / 10000000) * 100;

        if (!placed && d === today && Math.abs(p - min.p) < 0.01) {
          placed = true;
          return [t, p];
        }
        return [t, null];
      });

@Maccy6767 I used your card as example for in the readme with quarter hourly prices GitHub - fsaris/home-assistant-zonneplan-one: Unofficial Zonneplan integration for Home Assistant · GitHub