Long-term average daily usage statistic

I've been noodling around with a Bambu Lab 3D printer via the Bambu integration from HACS. One of the sensors you get is a total_usage value, in hours, which is of course cumulative. I can use the statistics-graph card in change mode to show the daily usage going back three months.

What I'd like to do is derive a value for average daily usage over that kind of long period. For example, value from the start of today minus the value from 60 days before that, divided by 60. Obviously the data is there in the long-term part of the database, and this sounds like the kind of thing you'd want to see on a power dashboard, but I haven't found an obvious way to just do that. I thought I was onto something with the history statistics sensor from helpers, which has the right kind of timing parameters, but seems to only be able to count the time an entity's state has a particular value, which isn't what we have here.

Does anyone have a suggestion as to the right approach to this?

You can create a daily usage sensor using the utility meter integration/helper but it will only work from the moment it is created. It won't retroactively look at your already existing data.

Did you already try the Statistics graph card with:

  • Chart type: Bar
  • Period: Day
  • Days to show: 60
  • Show stat types: Mean

That should give you a daily mean amount over sixty days.

If I select the "total usage" entity with the statistics graph card, the mean, min and max options are greyed out. Only sum, state and change are available to be selected.

What is shown as attributes when you enter this entity in the Developer tools → States page, and what is shown on the Developer tools → Statistics page?

state_class: total_increasing
unit_of_measurement: h
device_class: duration
friendly_name: P2S Total usage

The statistics page shows statistics unit as h, source recorder and there's an icon of a dot rolling up a slope, or something like that.

As a cumulative value, this seems plausible but I don't know any internals so maybe something is misclassified. I guess mean on the statistics card might have worked if the values were daily rather than cumulative?

Conincidence or not, just today I wanted to have something similar for my water usage.
I created a not-very-nice (not a dev myself but a pretty OK hacker, my own views :slight_smile: ) ... pyscscript for that.
If it helps, I can also have a look if you get stuck and see if I can modify it to your specific situation. The entity can be chosen and it will add the averages as attributes to it.
Note: this is only a few hours old and I have not yet the time to see how to embed this in my cards/graphs

# <config>/pyscript/water_stats.py
from datetime import datetime, timedelta, timezone

@service(supports_response="only")
async def get_statistics_water(
    days: int = 180,
    entity_id: str = None,
):
    """yaml
    description: Get water usage averages over a configurable number of days.
    fields:
      days:
        description: Number of days to look back
        required: false
        default: 180
        selector:
          number:
            min: 1
            max: 365
            mode: box
      entity_id:
        description: The entity ID of the total water sensor
        required: false
        default: "sensor.water_total_increasing"
        selector:
          entity:
            domain: sensor
    """    
    target_eid = entity_id
    statistic_ids = [target_eid]
    days = int(days)

    def fmt(dt):
        return dt.astimezone(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")

    now = datetime.now(timezone.utc)
    start = now - timedelta(days=days)

    # --- Daily average ---
    resp = await service.call("recorder", "get_statistics",
        statistic_ids=statistic_ids,
        start_time=fmt(start),
        end_time=fmt(now),
        period="day",
        types=["change"],
        return_response=True)
    daily_raw = resp["statistics"].get(target_eid, [])
    daily_values = [round(e["change"], 3) for e in daily_raw if e.get("change") is not None and e["change"] >= 0]
    avg_daily = round(sum(daily_values) / len(daily_values), 3) if daily_values else 0

    # --- Weekly average ---
    resp = await service.call("recorder", "get_statistics",
        statistic_ids=statistic_ids,
        start_time=fmt(start),
        end_time=fmt(now),
        period="week",
        types=["change"],
        return_response=True)
    weekly_raw = resp["statistics"].get(target_eid, [])
    weekly_values = [round(e["change"], 3) for e in weekly_raw if e.get("change") is not None and e["change"] >= 0]
    avg_weekly = round(sum(weekly_values) / len(weekly_values), 3) if weekly_values else 0

    # --- Monthly average ---
    resp = await service.call("recorder", "get_statistics",
        statistic_ids=statistic_ids,
        start_time=fmt(start),
        end_time=fmt(now),
        period="month",
        types=["change"],
        return_response=True)
    monthly_raw = resp["statistics"].get(target_eid, [])
    monthly_values = [round(e["change"], 3) for e in monthly_raw if e.get("change") is not None and e["change"] >= 0]
    avg_monthly = round(sum(monthly_values) / len(monthly_values), 3) if monthly_values else 0

    log.info("Water stats (%s days): daily=%s, weekly=%s, monthly=%s m³", days, avg_daily, avg_weekly, avg_monthly)

    state.setattr(f"{target_eid}.avg_daily_m3", avg_daily)
    state.setattr(f"{target_eid}.avg_weekly_m3", avg_weekly)
    state.setattr(f"{target_eid}.avg_monthly_m3", avg_monthly)
    state.setattr(f"{target_eid}.stats_days", days)

    return {
        "avg_daily_m3": avg_daily,
        "avg_weekly_m3": avg_weekly,
        "avg_monthly_m3": avg_monthly,
        "days": days,
    }

I hadn't heard of pyscript before this, but even though I'm not much of a Pythonista, that code looks comprehensible to me. Thanks; I will look into this; it might take a couple of days to get my head round it.

It is a often missed opportunity but it does mean to have (or want to learn) python. Aside me there are plenty of others that like to share their experience.... to conquer the challenge itself is often the only reward needed :slight_smile: ...just share/ask !

Edit: The 'yaml' part in the code-snippet is to define any input (entity/days).... I would propose to just add pyscript as a integration, then add this and play along. No issues to use AI but also loads of warnings on AI.. depending on which one you query, they suck....regardless if you pay for them or not. AI does (!) have value to get to a direction but finetuning is not very good (imo). For me AI is great as I know what I want, I understand python OK-ish so I can judge the feedback :slight_smile:

Try this