I’m setting up template sensors to make forecast data available for an ESPHome device and my YAML is revoltingly repetitive. It’s about as DRY as the bottom of the ocean. Surely I’m missing a better way to do this.
I want my forecast_h1 sensor to always contain data for the current hour, but the forecast API occasionally includes the forecast for the hour that just elapsed, so I can’t just assume the forecast data at index 0 is for the current hour. No problem, the forecasts each have timestamps so I just need to iterate through them until I find the one that matches the current hour, use that index for the current hour data, and increment it for subsequent hours, right?
As far as I can tell, there is no way for me to calculate this index (based on the data in response variable, hourly) once and then use it throughout my sensors. The only way I’ve been able to get this to work is to repeat the same calculation for every single point of data for every single hour. I’m doing the same calculation 48 times to get the exact same number. I made a jinja macro to help with this and apparently that needs to be imported for every single data point too.
# template: forecast.yaml
# Creates sensors from data returned by weather forecast API
- trigger:
- platform: event
event_type: homeassistant_started # update on startup
- platform: time_pattern
minutes: /10 # update every 10 minutes
action:
# get hourly forecast data
- service: weather.get_forecasts
target:
entity_id: "weather.openweathermap"
data:
type: hourly
response_variable: hourly
sensor:
- name: "Forecast H1"
unique_id: forecast_h1
state: >
{% from 'forecast_helpers.jinja' import forecast_start_idx %}
{% set hour_idx = forecast_start_idx(hourly['weather.openweathermap'].forecast, true) %}
{{ hourly['weather.openweathermap'].forecast[hour_idx].condition }}
attributes:
temperature: >
{% from 'forecast_helpers.jinja' import forecast_start_idx %}
{% set hour_idx = forecast_start_idx(hourly['weather.openweathermap'].forecast, true) %}
{{ hourly['weather.openweathermap'].forecast[hour_idx].temperature }}
precip_prob: >
{% from 'forecast_helpers.jinja' import forecast_start_idx %}
{% set hour_idx = forecast_start_idx(hourly['weather.openweathermap'].forecast, true) %}
{{ hourly['weather.openweathermap'].forecast[hour_idx].precipitation_probability }}
precip_amount: >
{% from 'forecast_helpers.jinja' import forecast_start_idx %}
{% set hour_idx = forecast_start_idx(hourly['weather.openweathermap'].forecast, true) %}
{{ hourly['weather.openweathermap'].forecast[hour_idx].precipitation }}
datetime: >
{% from 'forecast_helpers.jinja' import forecast_start_idx %}
{% set hour_idx = forecast_start_idx(hourly['weather.openweathermap'].forecast, true) %}
{{ hourly['weather.openweathermap'].forecast[hour_idx].datetime }}
label: >
{% from 'forecast_helpers.jinja' import forecast_start_idx %}
{% set hour_idx = forecast_start_idx(hourly['weather.openweathermap'].forecast, true) %}
{{ as_local(as_datetime(hourly['weather.openweathermap'].forecast[hour_idx].datetime)).strftime('%-I %p') }}
# Forecast H2 - H8...
{# forecast_helpers.jinja #}
{% macro macro_forecast_start_idx(forecast_list, check_hour, returns) %}
{% set ns = namespace(found = false) %}
{% for forecast in forecast_list %}
{% set dt = as_local(as_datetime(forecast.datetime, as_datetime("0"))) %}
{% if dt.date() == now().date() and (dt.hour == now().hour or not check_hour) %}
{% do returns(loop.index0) %}
{% set ns.found = true %}
{% break %}
{% endif %}
{% endfor %}
{% do returns(0) if not ns.found %}
{% endmacro %}
{% set forecast_start_idx = macro_forecast_start_idx | as_function %}
Please tell me there’s a better way. I’m ready to feel like an idiot for missing it.
You could then write a macro to generate that attributes dictionary for a given hour, remembering that macro outputs are generally strings, so you’d need to use to_json and from_json filters.
Immediately after the weather.get_forecasts action, define a variable, perhaps named wf for “weather forecast”, containing the functionality of your macro (i.e. it no longer needs to be a macro). However, instead of merely returning the list item’s index number, it should return the list item itself.
Each attribute simply references wf to get the value of the desired weather property (wf.temperature, wf.precipitation_probability, etc).
tl;dr
Create a variable in actions containing only the current hour’s weather forecast. Use it in attributes to get each weather property.
Instead of using a for loop in a macro, reject the “old” values before you do anything else using a variable in the action block. This is easier if you merge the response first.
Since the old values are no longer an issue, you can use a variable at the entity level to set a static index for each sensor… which can be combined with @123 's suggestion:
NOTE: The value for the “script” variable hourly in the templates above are based on a Weather integration that uses localized values for datetime. If your weather integration returns the value of datetime in UTC, please see the post by Taras below for examples that cover that.
FWIW, in order to get your example to work on my system, I needed to make the following changes:
1. Convert statement into a comment.
I got an error when reloading Template entities and it was because a comment was expressed as a statement.
Replace this (a statement):
{% Merge Response and Reject "old" forecasts %}
with this (a comment):
{# Merge Response and Reject "old" forecasts #}
2. Report time as UTC, not with local timezone offset
The rejectattr() function is comparing two datetime strings so both strings should use the same datetime format (the weather forecast data uses UTC).
Replace this (datetime reported with local timezone offset):
{% set current = today_at(now().hour~':00').isoformat() %}
with this (datetime reported as UTC):
{% set current = today_at(now().hour~':00').timestamp() | timestamp_utc %}
NOTES
When using my weather provider, the precipitation property doesn’t exist when the probability of precipitation is zero. As a consequence, an error was logged for each reference to the non-existent property. To mitigate it, I simply added a default filter to wf.precipitation.
I incorporated the value of idx directly into the wf variable’s template. This eliminates one line from each sensor’s configuration.
I had meant to make a note about this one. Not all weather integration use UTC in the response, the one I use most often is localized… I should have checked which one OpenWeather uses.