Tag: pv statistics

  • Generating a Beautiful Annual Solar PV Chart in Home Assistant

    This beautiful apexcharts chart gives me a clean annual view of solar PV performance month on month.

    chart showing annual solar pv generation month to month

    I show two things at once:

    1. Total kWh per month as orange columns.
    2. Average daily PV for that month as a dashed line.

    That pairing makes it easy to compare seasonal variation and spot how consistent generation is within each month.


    1) The time window

    I want a rolling 12-month view, aligned to the current year start, updating every 30 minutes.

    graph_span: 12month
    update_interval: 30m
    span:
      start: year
      offset: "-0d"

    2) Monthly totals as columns

    I read SolarEdge lifetime energy, convert Wh to kWh, then sum by month. I also enable statistics so ApexCharts aligns changes to month boundaries for accurate month on month totals.

    - entity: sensor.solaredge_lifetime_energy
      name: PV Last 12 Months
      color: var(--energy-solar-color)
      type: column
      float_precision: 0
      yaxis_id: first
      transform: return x / 1000;
      show:
        datalabels: true
      group_by:
        func: sum
        duration: 1month
        fill: zero
      statistics:
        type: change
        period: month
        align: end

    Why:

    • transform converts Wh to kWh.
    • group_by: sum gives a single monthly bar.
    • statistics: period: month ensures the change is aligned to the end of each month.

    3) Average daily PV as a dashed line

    I reuse the same entity and conversion, but average across the month, with statistics aligned by day. This produces a smooth “average day in this month” figure.

    - entity: sensor.solaredge_lifetime_energy
      name: PV Last 12 Months
      type: line
      stroke_dash: 3
      float_precision: 0
      color: "#3399FF"
      yaxis_id: second
      transform: return x / 1000;
      show:
        datalabels: true
      group_by:
        func: avg
        duration: 1month
        fill: zero
      statistics:
        type: change
        period: day
        align: end
      extend_to: false

    Why:

    • group_by: avg over a month produces the per-month daily average.
    • statistics: period: day uses daily changes to derive a meaningful daily rate.
    • stroke_dash: 3 makes the line read as a secondary metric.

    4) Dual y-axes for tidy scales

    Totals and averages sit on different ranges, so I bind columns to the left axis and the line to the right axis. Both are hidden to keep the design clean.

    yaxis:
      - id: first
        decimals: 2
        show: false
        min: 0
      - id: second
        opposite: true
        decimals: 0
        min: 0
        max: 80
        show: false

    5) Bar styling and stroke

    I keep columns compact with a subtle corner radius, and give lines a clear white stroke outline for contrast on dark themes.

    apex_config:
      plotOptions:
        bar:
          columnWidth: 28
          borderRadius: 1
      stroke:
        width: 2
        colors:
          - "#FFFFFF"

    6) Data labels with a custom formatter

    The line’s labels always show. Column labels only show for useful values, which keeps winter months uncluttered.

    apex_config:
      dataLabels:
        offsetY: -10
        style:
          fontSize: 10
        formatter: |
          EVAL: function(value, {seriesIndex, dataPointIndex, w }) {
            const roundedValue = Number(value).toFixed(0);
            if (seriesIndex === 1) {
              return roundedValue;
            }
            return roundedValue < 100 ? '' : roundedValue;
          }

    Why:

    • When seriesIndex === 1 (the dashed line), always show the rounded value.
    • For the column series, hide values below 100 kWh to avoid visual noise.

    7) Minimal legend and tooltip

    This is a glanceable tile, so I remove the legend and tooltips.

    apex_config:
      legend:
        show: false
      tooltip:
        enabled: false

    8) Month labels

    Three-letter month labels save space and remain readable.

    apex_config:
      xaxis:
        labels:
          hideOverlappingLabels: false
          rotate: 90
          show: true
          style:
            fontSize: 9
          format: MMM

    Full YAML

    type: custom:apexcharts-card
    graph_span: 12month
    update_interval: 30m
    span:
      start: year
      offset: "-0d"
    header:
      show: true
      title: Total Solar PV 2025 (kWh)
    series:
      - entity: sensor.solaredge_lifetime_energy
        name: PV Last 12 Months
        color: var(--energy-solar-color)
        type: column
        float_precision: 0
        yaxis_id: first
        transform: return x / 1000;
        show:
          datalabels: true
        group_by:
          func: sum
          duration: 1month
          fill: zero
        statistics:
          type: change
          period: month
          align: end
      - entity: sensor.solaredge_lifetime_energy
        name: PV Last 12 Months
        type: line
        stroke_dash: 3
        float_precision: 0
        color: "#3399FF"
        yaxis_id: second
        transform: return x / 1000;
        show:
          datalabels: true
        group_by:
          func: avg
          duration: 1month
          fill: zero
        statistics:
          type: change
          period: day
          align: end
        extend_to: false
    yaxis:
      - id: first
        decimals: 2
        show: false
        min: 0
      - id: second
        opposite: true
        decimals: 0
        min: 0
        max: 80
        show: false
    apex_config:
      chart:
        height: 300px
      legend:
        show: false
      plotOptions:
        bar:
          columnWidth: 28
          borderRadius: 1
      stroke:
        width: 2
        colors:
          - "#FFFFFF"
      tooltip:
        enabled: false
      dataLabels:
        offsetY: -10
        style:
          fontSize: 10
        formatter: |
          EVAL: function(value, {seriesIndex, dataPointIndex, w }) {
            const roundedValue = Number(value).toFixed(0);
            if (seriesIndex === 1) {
              return roundedValue;
            }
            return roundedValue < 100 ? '' : roundedValue;      
          }      
      xaxis:
        labels:
          hideOverlappingLabels: false
          rotate: 90
          show: true
          style:
            fontSize: 9
          format: MMM