Getting the most likely weather forecast for the next 4 hours

I have been struggling all day to achieve a simple thing, several AIs could not help me fix it.

I am trying to create a sensor that shows me the most likely weather forecast for the next 4 hours. I just want to grab the forecasts and determine which element appears most often.

So for example:
[‘sunny’, ‘sunny’, ‘sunny’, ‘rainy’] should result in ‘sunny’
[‘sunny’, ‘sunny’, ‘rainy’, ‘rainy’] should result in ‘inconclusive’
I think this transports my point, if one condition occurs more often than all the others, that one wins, otherwise its inconclusive.

I was successful in extracting this array with 4 strings, but was not able to implement the remaining functionality. Any help is appreciated. Here is how I did it:

template:
  - trigger:
      - platform: state
        entity_id: weather.forecast_home
    action:
      - service: weather.get_forecasts
        data:
          type: hourly
        target:
          entity_id: weather.forecast_home
        response_variable: hourly
    sensor:
      - name: Local Weather Forecast Hourly
        unique_id: local_weather_forecast_hourly
        state: "{{ now() }}"
        attributes:
          forecast: "{{ hourly['weather.forecast_home'].forecast }}"
  - sensor:
    - name: "Most Likely Weather Next 4 Hours"
      unique_id: most_likely_weather_next_4_hours
      state: >
          {% set forecast_list = state_attr('sensor.local_weather_forecast_hourly', 'forecast') | default([]) %}
          {% set conditions = forecast_list | map(attribute='condition') | list %}
          {% if conditions | length > 4 %}
            {% set conditions = conditions[:4] %}
          {% endif %}
          {% set condition_counts = {} %}
          {% for condition in conditions %}
            {% set condition_counts = condition_counts | merge({ condition: (condition_counts[condition] | default(0)) + 1 }) %}
          {% endfor %}
          {% set most_likely_condition = condition_counts | dictsort(true, 'value') | first %}
          {{ most_likely_condition[0] }}

This fails: TemplateAssertionError: No filter named ‘merge’. All other attempts to create this dictionary also failed.

There is no filter named merge in jinja or HA template extensions that I can find, not sure where you go that from. Closest thing I could find is merge_response() documented here, and it does not look like it can be used as a filter.

{% set forecast_list = state_attr('sensor.local_weather_forecast_hourly', 'forecast') | default([]) %}
{% set conditions = (forecast_list | map(attribute='condition') | list)[:4] %}
{%- set u_cond = conditions | unique | list %}
{%- set u_count = u_cond | count %}
{%- if u_count == 4 %}
  inconclusive
{%- elif u_count == 1 %}
  {{ u_cond[0] }}
{%- else %}
  {%- set ns = namespace(d=[]) %}
  {%- for c in u_cond %}
    {%- set ns.d = ns.d + [(c, conditions|select('eq', c)| list | count)] %}
  {%- endfor %}
  {% set c_comp = (ns.d)|map(attribute=1)|unique|list|count %}
  {{ (ns.d | max(attribute=1))[0] if c_comp != 1 else "inconclusive"}}
{%- endif %}

Thank you, that works perfectly! Did not know I needed the namespace construct to work with the dictionary inside a loop

Hey Drew & Jörg!

This is almost exactly what I’ve been looking for, a few minor changes and it works for me too. I have been trying to fiddle with the formatting afterward though and can’t figure out how to add it to the namespace item. The formatting being “Partly Cloudy” rather than “partlycloudy”.

This is where I’m at, less the bits that I can’t get to work.

      - name: Overnight Unified Conditions
        unique_id: overnight_unified_conditions
        state: >
          {% set metNo_forecast = hourly['weather.forecast_home'].forecast -%}
          {% set envCan_forecast = hourly['weather.montra_c_al_forecast'].forecast -%}
          {% set pirate_forecast = hourly['weather.pirateweather'].forecast -%}

          {%- set overnight_start = now().replace(hour=18, minute=0, second=0, microsecond=0) -%}
          {%- set overnight_end = now().replace(hour=6, minute=0, second=0, microsecond=0) + timedelta(days=1) -%}
          {%- set forecast_1 = metNo_forecast | selectattr('datetime', '>=', overnight_start.isoformat()) | selectattr('datetime', '<=', overnight_end.isoformat()) | list -%}
          {%- set forecast_2 = envCan_forecast | selectattr('datetime', '>=', overnight_start.isoformat()) | selectattr('datetime', '<=', overnight_end.isoformat()) | list -%}
          {%- set forecast_3 = pirate_forecast | selectattr('datetime', '>=', overnight_start.isoformat()) | selectattr('datetime', '<=', overnight_end.isoformat()) | list -%}

          {% set forecast_list = forecast_1 + forecast_2 + forecast_3 %}
          {% set conditions = (forecast_list | map(attribute='condition') | list)[:12] %}
          {%- set u_cond = conditions | unique | list %}
          {%- set u_count = u_cond | count %}
          {%- if u_count == 12 %}
            inconclusive
          {%- elif u_count == 1 %}
            {{ u_cond[0] }}
          {%- else %}
            {%- set ns = namespace(d=[]) %}
            {%- for c in u_cond %}
              {%- set ns.d = ns.d + [(c, conditions|select('eq', c)| list | count)] %}
            {%- endfor %}
            {% set c_comp = (ns.d)|map(attribute=1)|unique|list|count %}
            {{ (ns.d | max(attribute=1))[0] if c_comp != 1 else "Mixed Conditions"}}
          {%- endif %}

          {% if is_state("sensor.overnight_unified_conditions", "sunny") %}
          Sunny
          {% elif is_state("sensor.overnight_unified_conditions", "cloudy") %}
          Cloudy
          {% elif is_state("sensor.overnight_unified_conditions", "clear-night") %}
          Clear Night
          {% elif is_state("sensor.overnight_unified_conditions", "fog") %}
          Foggy
          {% elif is_state("sensor.overnight_unified_conditions", "hail") %}
          Hailing
          {% elif is_state("sensor.overnight_unified_conditions", "lightning") %}
          Lightning Storms
          {% elif is_state("sensor.overnight_unified_conditions", "lightning-rainy") %}
          Showers & Lightning Storms
          {% elif is_state("sensor.overnight_unified_conditions", "partlycloudy") %}
          Partly Cloudy
          {% elif is_state("sensor.overnight_unified_conditions", "pouring") %}
          Pouring Rain
          {% elif is_state("sensor.overnight_unified_conditions", "rainy") %}
          Raining
          {% elif is_state("sensor.overnight_unified_conditions", "snowy") %}
          Snowing
          {% elif is_state("sensor.overnight_unified_conditions", "snowy-rainy") %}
          Sleet
          {% elif is_state("sensor.overnight_unified_conditions", "windy") %}
          Windy
          {% elif is_state("sensor.overnight_unified_conditions", "windy-variant") %}
          Cloudy & Windy
          {% elif is_state("sensor.overnight_unified_conditions", "exceptional") %}
          Exceptional Conditions
          {% else %}
          Mixed Conditions
          {% endif %}

How is that supposed to work? While setting the sensor’s state, it’s asking if the sensor’s current state is “sunny”… but the sensor’s state could never get to “sunny”, because it’s supposed to be overwritten to be “Sunny”.

Instead of “printing” the outputs of the first if/then, you need to set a variable to those values, then check that variable’s value against your options for formatting.

FWIW, this could be simplified quite a bit by using a dictionary instead of the long list of ifs and elifs.

How would I do that exactly? I have been playing with a conditions map, but i am not sure how to reference it and change the value.

            {% set conditions_map = {
              'rainy': "frequent rain showers",
              'snowy': "periods of snow",
              'fog': "occasional fog",
              'sunny': "some sunshine",
              'clear': "clear skies",
              'clear-night': "clear skies",
              'partlycloudy': "a mix of sun and clouds",
              'windy': "windy",
              'windy-variant': "windy and some clouds",
              'cloudy': "overcast skies",
              'sleet': "some sleet",
              'hail': "hail",
              'lightning': "lightning",
              'lightning-rainy': "thunderstorms",
              'pouring': "heavy rainfall",
              'snowy-rainy': "mixed snow and rain",
              'exceptional': "unusual weather"
            } %}

Not really sure where to go from here.

Use the dictionary method get().

Other considerations:

  • OP was only interested in 4 hours from one source, so the slice made using [:4] was to get just those 4 hours. But, you are checking 12 hours from 3 sources and using the selectattr() filters to get your desired . By slicing the combined list with [:12], you are effectively only using 1 of the sources… so just leave that out.
  • The variable u_count is the count of how many unique condition types are returned. In OP’s case u_count equaling 4 meant that all his results were different. Requiring u_count to be exactly 12 different conditions types to be present for “Mixed Conditions” to be returned is a bit strange. I would probably use a smaller number and a comparison like >=. Since you are covering a wider timespan, you could also just get rid of that part altogether so it always finds the modal condition.
  • The function statistical_mode has been added since the original post and it works on lists of strings as well as numbers, so we can get rid of the loop and use that instead.
template:
  - triggers:
      - trigger: ....
    actions:
      - action: weather.get_forecasts
        target:
          entity_id:
            - weather.forecast_home
            - weather.montra_c_al_forecast
            - weather.pirateweather
        data:
          type: hourly
        response_variable: hourly
    sensor:
      - name: Overnight Unified Conditions
        unique_id: overnight_unified_conditions
        state: >
          {%-set overnight_start = today_at('18:00') -%}
          {%- set overnight_end = (overnight_start + timedelta(hours=12)).isoformat() -%}

          {%-set conditions = (merge_response(hourly) 
          | selectattr('datetime', '>=', overnight_start.isoformat()) 
          | selectattr('datetime', '<=', overnight_end) 
          | map(attribute='condition') | list) %}

          {%- set u_cond = conditions | unique | list %}
          {%- set u_count = u_cond | count %}
          {%- if u_count >= 8 %}
            {%- set cond = 'Mixed Conditions' %}
          {%- elif u_count == 1 %}
            {%- set cond = u_cond[0] %}
          {%- else %}
            {%- set cond = statistical_mode(conditions)|default('Mixed Conditions',1) %}
          {%- endif %}

          {%-set conditions_map = { 
            'sunny': 'Sunny',
            'cloudy': 'Cloudy',
            'clear-night': 'Clear Night',
            'fog': 'Foggy',
            'hail': 'Hailing',
            'lightning': 'Lightning Storms',
            'lightning-rainy': 'Showers & Lightning Storms',
            'partlycloudy': 'Partly Cloudy',
            'pouring': 'Pouring Rain',
            'rainy': 'Raining',
            'snowy': 'Snowing',
            'snowy-rainy': 'Sleet',
            'windy': 'Windy',
            'windy-variant': 'Cloudy & Windy',
            'exceptional': 'Exceptional Conditions' } %}
          {{ conditions_map.get(cond, 'Mixed Conditions') }}

Thanks again Drew for your excellent explanation of how this stuff works. I’m a bit out in the sticks with what I’m trying to do, so it really does help to have clear explanations of these functions as I continue to play with them and learn. Help from people like you is absolutely invaluable and so appreciated. Thank you so much.

I have been playing with your last solution, and am getting it closer to where I need it. This is where I am at right now:

template:
  - triggers:
      - trigger: ....
    actions:
      - action: weather.get_forecasts
        target:
          entity_id:
            - weather.forecast_home
            - weather.montra_c_al_forecast
            - weather.pirateweather
        data:
          type: hourly
        response_variable: hourly
    sensor:
      - name: Overnight Unified Conditions
        unique_id: overnight_unified_conditions
        state: >
          {%-set overnight_start = today_at('18:00') -%}
          {%- set overnight_end = (overnight_start + timedelta(hours=12)).isoformat() -%}

          {%-set conditions = (merge_response(hourly) 
          | selectattr('datetime', '>=', overnight_start.isoformat()) 
          | selectattr('datetime', '<=', overnight_end) 
          | map(attribute='condition') | list) %}

          {%- set u_cond = conditions | unique | list %}
          {%- set u_count = u_cond | count %}
          {%- if u_count >= 3 %}
            {%- set cond = 'Strange Weather' %}
          {%- elif u_count == 2 %}
            {%- set cond = 'Variable Conditions: ' + u_cond[0] + ' and ' + u_cond[1] %}
          {%- elif u_count == 1 %}
            {%- set cond = u_cond[0] %}
          {%- else %}
            {%- set cond = statistical_mode(conditions)|default('Mixed Conditions',1) %}
          {%- endif %}

          {%-set conditions_map = { 
            'sunny': 'Sunny',
            'cloudy': 'Cloudy',
            'clear-night': 'Clear Night',
            'fog': 'Foggy',
            'hail': 'Hailing',
            'lightning': 'Lightning Storms',
            'lightning-rainy': 'Showers & Lightning Storms',
            'partlycloudy': 'Partly Cloudy',
            'pouring': 'Pouring Rain',
            'rainy': 'Raining',
            'snowy': 'Snowing',
            'snowy-rainy': 'Sleet',
            'windy': 'Windy',
            'windy-variant': 'Cloudy & Windy',
            'exceptional': 'Exceptional Conditions' } %}

          {{ conditions_map.get(cond, 'Mixed Conditions') }}

Essentially, if there are more than two conditions “Strange Weather” would be a fine state for me… (Tom Waits fan here). If there are 2 conditions “Variable Conditions: blah and blah” would be good. “u_cond[0]” works fine on its own for a single condition. I’m not sure what the {%- set cond = statistical_mode(conditions)|default('Mixed Conditions',1) %} and {{ conditions_map.get(cond, 'Mixed Conditions') }} functions do exactly. So far, with my modified code, it only produces “Mixed Conditions”.

The closest result to what I seek produced with the code as it is in this state is {{ cond }}, but that is still not formatted. It is very close.

That set statement gets the condition type that occurs most frequently in the list saved to the variable conditions and saves that to the variable cond. So if there are 25 “rainy”, 5 “lightning-rainy”, and 6 “pouring” in conditions it will save “rainy” in cond.

This expression is what “prints” the final output that becomes the state. It takes the dictionary conditions_map and returns the value from the key that matches the output of cond (or “Mixed Conditions” if there is no match).

Since you are covering 12 hours, I would suggest something in the 5-8 range… otherwise it will say “Strange Weather” a lot.

template:
  - triggers:
      #Your trigger(s)
    actions:
      - action: weather.get_forecasts
        target:
          entity_id:
            - weather.forecast_home
            - weather.montra_c_al_forecast
            - weather.pirateweather
        data:
          type: hourly
        response_variable: hourly
    sensor:
      - name: Overnight Unified Conditions
        unique_id: overnight_unified_conditions
        state: >
          {%-set overnight_start = today_at('18:00') -%}
          {%- set overnight_end = (overnight_start + timedelta(hours=12)).isoformat() -%}

          {%-set conditions = (merge_response(hourly) 
          | selectattr('datetime', '>=', overnight_start.isoformat()) 
          | selectattr('datetime', '<=', overnight_end) 
          | map(attribute='condition') | list) %}

          {%- set u_cond = conditions | unique | list %}
          
          {%- set u_count = u_cond | count %}
          {%- if u_count >= 6 %}
            {%- set cond = 'strange' %}
          {%- elif u_count == 1 %}
            {%- set cond = u_cond[0] %}
          {%- else %}
            {%- set cond = statistical_mode(conditions)|default('Mixed Conditions',1) %}
          {%- endif %}

          {%-set conditions_map = { 
            'sunny': 'Sunny',
            'cloudy': 'Cloudy',
            'clear-night': 'Clear Night',
            'fog': 'Foggy',
            'hail': 'Hailing',
            'lightning': 'Lightning Storms',
            'lightning-rainy': 'Showers & Lightning Storms',
            'partlycloudy': 'Partly Cloudy',
            'pouring': 'Pouring Rain',
            'rainy': 'Raining',
            'snowy': 'Snowing',
            'snowy-rainy': 'Sleet',
            'windy': 'Windy',
            'windy-variant': 'Cloudy & Windy',
            'exceptional': 'Exceptional Conditions'
            } %}

          {%- if u_count == 2 %}
            Variable Conditions:  {{conditions_map.get(u_cond[0])}} and {{conditions_map.get(u_cond[1])}}
          {%- else %}
            {{ conditions_map.get(cond, 'Strange Weather') }}
          {%- endif %}
1 Like

Hello again;

So that worked like a charm. Thanks again!

1 Like

you can use my blueprint [Blueprint] Weather Forecast Alerts (TTS + AI, pre-roll, hourly precip timing)