Date math template

I’m trying to replace my ISP data use sensors as using their API is unreliable.

To complicate things my data plan rolls over on the 12th day of every month.

I have utility meters set up and reliably counting the the GB used in and out of my router. Getting the utility meters to reset on the 12th is easy using the offset option.

However I would like to implement a projected use sensor to alert me if I’m likely to go over my monthly quota. To do this I need days_remaining and days_used sensors.

Days remaining is easy if the day of the month is less than 12 (days_remaining = 12 - now().day). However I’m struggling with the math if the day of the month is 12 or greater. How do I get the total number of days in the current month?

Do I need a lookup map for the number of days in each month (edit: no that wont work - because of leap years)?

Or is there an easier way to do this with the new timedelta function?

It does not seem to support months so I’m guessing not.

Something like this simple template perhaps ? :wink:

{{ ((as_timestamp((as_timestamp(states('sensor.date')) | int +86400*30) |  timestamp_custom('%Y-%m-12', false)) | int - now().timestamp()) |  int /86400) | int +1 }}

It takes todays date and adds 30 days so it will be
2020-12-16

then it does a timestamp_custom to replace the last digits to 12
2020-12-12

makes that a unix time, subtracts now unix time, and divides with 86400 and adds one day.

So you need to add this to a template that sees if todays day is more or less than day 12 of the month

EDIT: Now that I think about it, it might fail on the +30 days. It should probably be +31. But that could be problematic if it’s february.

What if the month only has 28 days, or if it has 31?

I think I’ve found a way:

{{ now().replace(month=now().month +1, day=12) - now() }}

Gives: 25 days, 23:59:59.999966, which I can truncate to days only easy enough.

1 Like

Yeah I just thought about that and edited that in…
That is a nice way of doing it! :+1:

It is consistently giving a few micro-seconds shy of the extra day needed ( 23:59:59.999966) even when doing this:

{{ (now().replace(month=now().month +1, day=12, hour=0, minute=0, second=0) - now().replace(hour=0, minute=0, second=0)) }}

I guess I can always add a day. It’s just odd that this template does not fix that.

Edit: Sorted:

{% set datetime = now() %}
{{ ((datetime.replace(month=now().month +1, day=12) - datetime)|string).split(" ")[0] }}

–> 26

For anyone searching in future:

- platform: template
  sensors:
    days_remaining:
      friendly_name: "Days Remaining"
      unit_of_measurement: 'days'
      value_template: >
        {% set datetime = now() %}
        {% if datetime.day < 12 %}
          {{ 12 - datetime.day }}
        {% else %}
          {{ ((datetime.replace(month=now().month +1, day=12) - datetime)|string).split(" ")[0] }}
        {% endif %}
    days_used:
      friendly_name: "Days Used"
      unit_of_measurement: 'days'
      value_template: >
        {% set datetime = now() %}
        {% if datetime.day >= 12 %}
          {{ datetime.day - 11 }}
        {% else %}
          {{ ((datetime - datetime.replace(month=now().month -1, day=12))|string).split(" ")[0] }}
        {% endif %}

And what I was actually after:

- platform: template
  sensors:
    projected_use:
      friendly_name: "Projected Use"
      unit_of_measurement: 'GB'
      value_template: >
        {% if states('sensor.days_used') not in [ '0', 'unavailable', 'none', 'unknown' ] %}
          {% set use_per_day = states('sensor.nbn_gb_used')|float / states('sensor.days_used')|int %}
          {{ ( states('sensor.nbn_gb_used')|float + use_per_day * states('sensor.days_remaining')|int )|round(0) }}
        {% else %}
          N/A
        {% endif %}

FYI
Instead of converting the timedelta object to string and then splitting it and reporting the zeroth item to show days, you can report it directly like this:

{{ (now().replace(month=now().month + 1, day=12) - now()).days }}
1 Like

Thanks Taras.

Updated:

- platform: template
  sensors:
    days_remaining:
      friendly_name: "Days Remaining"
      unit_of_measurement: 'days'
      value_template: >
        {% set datetime = now() %}
        {% if datetime.day < 12 %}
          {{ 12 - datetime.day }}
        {% else %}
          {{ (datetime.replace(month=now().month +1, day=12) - datetime).days }}
        {% endif %}
    days_used:
      friendly_name: "Days Used"
      unit_of_measurement: 'days'
      value_template: >
        {% set datetime = now() %}
        {% if datetime.day >= 12 %}
          {{ datetime.day - 11 }}
        {% else %}
          {{ (datetime - datetime.replace(month=now().month -1, day=12)).days }}
        {% endif %}

I was playing around with your template trying to figure out how it worked and I think I found an error in your templates.

For the remaining days template what you have there now will work for every month except December. At that time when you add 1 to the month you will get a 13 as the resulting month which will give you an error since the month can’t be more than 12.

So for December you’ll have to compensate for that by moving the datetime to January of the next year.

I think this should fix it (at least it worked in the template editor):

{% set datetime = now() %}
{% if datetime.day < 12 %}
  {{ 12 - datetime.day }}
{% else %}
  {% if datetime.month == 12 %}
    {{ (datetime.replace(year=now().year + 1, month=1, day=12) - datetime).days }}
  {% else %}
    {{ (datetime.replace(month=now().month + 1, day=12) - datetime).days }}
  {% endif %}
{% endif %}

And you’ll have to do something similar for the days used and move the second datetime to December of the previous year.

I think…

2 Likes

Yeah you’re right, that will be an issue. I’ll see if I get time to fix it later today.

Thanks for spotting that Finity. Saved me a bit of grief next month.

Updated to:

- platform: template
  sensors:
    days_remaining:
      friendly_name: "Days Remaining"
      unit_of_measurement: 'days'
      value_template: >
        {% set datetime = now() %}
        {% if datetime.day < 12 %}
          {{ 12 - datetime.day }}
        {% else %}
          {% if datetime.month == 12 %}
            {{ (datetime.replace(year=now().year + 1, month=1, day=12) - datetime).days }}
          {% else %}
            {{ (datetime.replace(month=now().month + 1, day=12) - datetime).days }}
          {% endif %}
        {% endif %}
    days_used:
      friendly_name: "Days Used"
      unit_of_measurement: 'days'
      value_template: >
        {% set datetime = now() %}
        {% if datetime.day >= 12 %}
          {{ datetime.day - 11 }}
        {% else %}
          {% if datetime.month == 1 %}
            {{ (datetime - datetime.replace(year=now().year - 1, month=12, day=12)).days }}
          {% else %}
            {{ (datetime - datetime.replace(month=now().month - 1, day=12)).days }}
          {% endif %}
        {% endif %}
1 Like