UK MET Office Severe Weather Warnings

In last year’s bad weather I cobbled together a markdown card to show severe weather warnings from the UK Met Office. But I’m redoing my lovelace interface now and have taken the time to tidy it up. Posting in case it helps give anyone ideas. The icons change type and colour according to the type and severity of the warning(s).

(it’s just test data currently)

The top section is this custom card https://github.com/Yevgenium/lovelace-weather-card-chart. The bottom is a markdown card both are wrapped in a stack-in-card https://github.com/custom-cards/stack-in-card.

I use a simple flow in node-red to grab the RSS feed with any items, convert to JSON and write it to an attribute in a sensor. Then I parse it into a table in the markdown card. I’ve also styles the weather card a bit to make them work better together.

Node-red config

[{"id":"32ad5b17.730c84","type":"xml","z":"df31568f.39e0d8","name":"","property":"payload","attr":"","chr":"","x":610,"y":580,"wires":[["cfaf461c.029f98"]]},{"id":"96430af9.396928","type":"http request","z":"df31568f.39e0d8","name":"get warnings","method":"GET","ret":"txt","paytoqs":false,"url":"http://www.metoffice.gov.uk/public/data/PWSCache/WarningsRSS/Region/yh","tls":"","persist":false,"proxy":"","authType":"","x":410,"y":580,"wires":[["32ad5b17.730c84"]]},{"id":"fda8626.3c450a","type":"inject","z":"df31568f.39e0d8","name":"","topic":"","payload":"","payloadType":"date","repeat":"3600","crontab":"","once":true,"onceDelay":0.1,"x":210,"y":580,"wires":[["96430af9.396928"]]},{"id":"cfaf461c.029f98","type":"ha-entity","z":"df31568f.39e0d8","name":"weather_warnings","server":"cc436e5e.bbf7e","version":1,"debugenabled":false,"outputs":1,"entityType":"sensor","config":[{"property":"name","value":"weather_warnings"},{"property":"device_class","value":""},{"property":"icon","value":""},{"property":"unit_of_measurement","value":""}],"state":"","stateType":"date","attributes":[{"property":"items","value":"payload.rss.channel[0].item","valueType":"msg"}],"resend":true,"outputLocation":"","outputLocationType":"none","x":870,"y":580,"wires":[[]]},{"id":"cc436e5e.bbf7e","type":"server","z":"","name":"Home Assistant","legacy":false,"rejectUnauthorizedCerts":true,"ha_boolean":"y|yes|true|on|home|open","connectionDelay":true,"cacheJson":false}]

Lovelace config:

type: custom:stack-in-card
cards:

  - type: custom:weather-card-chart
    title: Weather
    weather: weather.openweathermap
    style: |
      ha-card {
        --iron-icon-width: 35px;
        --iron-icon-height: 35px;
      }

      .attributes div {
        text-align: left;
      }

      .main {
        font-size: 20pt;
      }

      .main sup {
        font-size: 12pt;
      }

      .main ha-icon {
        --iron-icon-width: 55px;
        --iron-icon-height: 55px;
      }

  - type: markdown
    style: |
      ha-card {
        --iron-icon-width: 50px;
        --iron-icon-height: 50px;
      }
    content: |
      {% if state_attr('sensor.weather_warnings','items') != None %}
        {% for item in state_attr('sensor.weather_warnings','items') %}
          {% for type, icon in [('RAIN', 'weather-pouring'), ('THUNDERSTORMS', 'weather-lightning-rainy'),
                                ('WIND', 'weather-windy'), ('SNOW', 'weather-snowy-heavy'), 
                                ('LIGHTNING', 'weather-lightning'), ('ICE', 'snowflake-alert'),
                                ('FOG', 'weather-fog')] if type == item['metadata:warningType']|trim("[]'") %}
      ---

      | | | |
      | --- | --- | --- |
      | <font color = {%- if 'Yellow' in item.description|trim("'[]'") %}'gold'
                      {%- elif 'Amber' in item.description|trim("'[]'") %}'darkorange'
                      {%- else %}'firebrick'
                      {%- endif %}><ha-icon icon={{ "'mdi:" + icon + "'" }}></ha-icon></font> | | {{ item.description|trim("'[]'") }}  |
 
          {% endfor %}
        {% endfor %}
      {% endif %}

Hopefully, this will be of some use to people when the winter weather comes back.

5 Likes

Nice.
I have something similar but nowhere near as nice as yours. I don’t use NodeRed but the markdown card looks very nice. (As does the weather card)

Thanks.

How do you get the RSS feed? Or do you get the data a different way? I went straight to Node-red as I’m familiar with it. I’d be interested in updating this to work purely in home assistant.

I use a Feedparser sensor by @iantrich:

There is a core RSS Reader but I didn’t discover it until after I’d started using Feedparser. I keep meaning to see if I could substitute it in as it always seems to make sense to me to keep to as few custom components as possible.

But you know what it’s like when something works…

Anyway, here is my config.

  - platform: feedparser
    name: Met Office RSS Feed South East Weather Warnings
    feed_url: 'http://metoffice.gov.uk/public/data/PWSCache/WarningsRSS/Region/se'
    date_format: '%a, %b %d %I:%M %p'

Which I then template out a count just for my area within the region:

  - platform: template
    sensors:
      #==========================================================
      #=== Number of weather warnings affecting 'MY AREA'
      #==========================================================
      met_office_london_weather_warnings:
        entity_id: sensor.met_office_rss_feed_south_east_weather_warnings
        value_template: >
          {% set ns = namespace(warning_count = 0) %}
          {% for entries in state_attr('sensor.met_office_rss_feed_south_east_weather_warnings', 'entries') %}
            {% if 'MY AREA' in states.sensor.met_office_rss_feed_south_east_weather_warnings.attributes.entries[loop.index - 1].summary %}
              {% set ns.warning_count = ns.warning_count + 1 %}
            {% endif %}
          {% endfor %}

          {{ ns.warning_count }}

I then have an overcomplicated automation triggered by a state change to the sensor that converts the warnings to natural language for use in my morning announcement. And a markdown card, but it is nothing like as pretty as yours.

Yet :wink:

2 Likes

I did look at the core RSS integration, but it seems (I may be wrong) that it only generates events and doesn’t populate a sensor. I’m not sure whether it’s possible to create and populate sensors from automations triggered by the events and it sounds like it could get complicated.

I’ll take a look at Feedparser though, that looks interesting, thanks for the info.

Hi,
Did you had the chance to update the Lovelace Config? I’m curious about it! :slight_smile:
Cheers

not sure what you mean.

they have recently changed their format, I now use this:

- type: markdown
    style: "ha-card { --mdc-icon-size: 60px; }"
    content: |
      {% if state_attr('sensor.weather_warnings','items') != None %}
        {% for item in state_attr('sensor.weather_warnings','items') %}
          {% for type, icon in [('rain', 'weather-pouring'), ('thunderstorms', 'weather-lightning-rainy'),
                                ('wind', 'weather-windy'), ('snow', 'weather-snowy-heavy'), 
                                ('lightning', 'weather-lightning'), ('ice', 'car-traction-control'),
                                ('fog', 'weather-fog')] if type == item.description[0].split(' ')[3]|trim(',') %}
      ---
      | | | |
      | --- | --- | --- |
      | <font color = {%- if 'Yellow' == item.description[0].split(' ')[0]|trim(',') %}'gold'
                      {%- elif 'Amber' == item.description[0].split(' ')[0]|trim(',') %}'darkorange'
                      {%- else %}'firebrick'
                      {%- endif %}><ha-icon icon={{ "'mdi:" + icon + "'" }}></ha-icon></font> | | {{ item.description|trim("'[]'") }}  |
          {% endfor %}
        {% endfor %}
      {% endif %}
1 Like

Thanks! It helped me a lot to improve my code! great stuff :slight_smile:

Sorry, not been around for a while. They changed the format a few weeks back, my code now looks very similar to what you’ve posted. I’m sure it will help others, thanks.

I have started a new thread around the new(-ish) Met Office DataHub but maybe someone here has already got the necessary to help me with this.
How do I get the JSON from my python calls into Homeassistant and one of your lovely cards?
Thanks

for any one interested. I’ve got this working with feedparser rather than nodered. Massive props to @eggman as I wouldn’t have been able to do this with their work.

image

- platform: feedparser
  name: Weather Alerts
  feed_url: "https://www.metoffice.gov.uk/public/data/PWSCache/WarningsRSS/Region/wm"
  date_format: "%a, %b %d %I:%M %p"

lovelace card

type: 'custom:stack-in-card'
cards:
  - type: weather-forecast
    entity: weather.openweathermap
  - type: markdown
    style: |
      ha-card {
          --iron-icon-width: 50px;
          --iron-icon-height: 50px;
        }
    content: >
      {% if state_attr('sensor.weather_alerts','entries') != 0 %}
        {% for item in state_attr('sensor.weather_alerts','entries') %}
          {% for type, icon in [('rain', 'weather-pouring'), ('thunderstorms', 'weather-lightning-rainy'),
                                ('wind', 'weather-windy'), ('snow', 'weather-snowy-heavy'), 
                                ('lightning', 'weather-lightning'), ('ice', 'car-traction-control'),
                                ('fog', 'weather-fog')] if type == item.summary.split(' ')[3]|trim(',') %}
      ---

      | | | |

      | --- | --- | --- |

      | <font color = {%- if 'Yellow' == item.summary.split(' ')[0]|trim(',')
      %}'gold'
                      {%- elif 'Amber' == item.summary.split(' ')[0]|trim(',') %}'darkorange'
                      {%- else %}'firebrick'
                      {%- endif %}><ha-icon icon={{ "'mdi:" + icon + "'" }}></ha-icon></font> | | {{ item.summary|trim("'[]'") }}  |
          {% endfor %}
        {% endfor %}
      {% endif %}

1 Like

I’ve made some improvements to my feedparser version previously posted. This now supports “extream heat” and “thunderstorm” alerts. I’ve also reworked the summary text into something which I can layout a little better. Date time formats can be changed to your liking simply by updating the format codes.

You will also need to update the region in the regex to match your region. I strip it out because I know my region and only interested in the actual places affected.

Sensor

- platform: feedparser
  name: Weather Alerts
  feed_url: "https://www.metoffice.gov.uk/public/data/PWSCache/WarningsRSS/Region/wm"
  date_format: "%a, %b %d %I:%M %p"

Lovelace Card - needs card_mod

type: custom:stack-in-card
cards:
  - type: weather-forecast
    entity: weather.openweathermap
  - type: conditional
    conditions:
      - entity: sensor.weather_alerts
        state_not: '0'
    card:
      type: markdown
      card_mod:
        style:
          .: |
            ha-card {
              --mdc-icon-size: 40px;
            }
          ha-markdown:
            $: |
              td {
                vertical-align: top;
              }
      content: |
        {% if state_attr('sensor.weather_alerts','entries') != 0 %}
          {% for item in state_attr('sensor.weather_alerts','entries') %}
            {% for type, icon in [('rain', 'weather-pouring'), ('thunderstorms', 'weather-lightning-rainy'),
                                  ('wind', 'weather-windy'), ('snow', 'weather-snowy-heavy'), 
                                  ('lightning', 'weather-lightning'), ('ice', 'car-traction-control'),
                                  ('fog', 'weather-fog'), ('extreme heat', 'weather-sunny-alert'), ('thunderstorm', 'weather-lightning')] if type == item.summary | regex_findall_index('.*warning of (.*) affecting.*', ignorecase=True) %}
              {% set color = item.summary.split(' ')[0] %}
              {% set summary = item.summary | regex_findall_index('(.*) affecting West Midlands: (.*) valid from (.*) to (.*)', ignorecase=True) %}
              {% set time_from = as_timestamp(strptime(summary[2], "%H%M %a %d %b")) | timestamp_custom("%H:%M %d/%m") %}
              {% set time_to = as_timestamp(strptime(summary[3], "%H%M %a %d %b")) | timestamp_custom("%H:%M %d/%m") %}
        | | | |
        | --- | --- | --- |
        | <font color = {%- if 'Yellow' == color %}'gold'
                        {%- elif 'Amber' == color %}'darkorange'
                        {%- else %}'firebrick'
                        {%- endif %}><ha-icon icon={{ "'mdi:" + icon + "'" }}></ha-icon></font> | | **{{ summary[0] | title }}**<br />{{ time_from }} - {{ time_to }}<br />{{ summary[1] }} |
            {% endfor %}
          {% endfor %}
        {% endif %}

Hope someone finds this useful

4 Likes

Thank you for this! exactly what I have been looking for

1 Like

You are most welcome. Credit to the OP for coming up with the idea in the first place.

1 Like

If anyone else spends hours trying to get this working, don’t forget to edit this regex:

regex_findall_index('(.*) affecting West Midlands: (.*) valid from (.*) to (.*)', ignorecase=True) %}

and change West Midlands to whatever is reported in your sensor.weather_alerts state!

1 Like

Hi I am having problems with this. I am using this feed trying to capture alerts for London: Met Office warnings for London & South East England

I recently configured this OK using the details above. For South East change the last part of the URL in the Feedparser sensor from “wm” to “se” and in the Lovelace card change “West Midlands” to “London & South East England”. You’ll need to add Feedparser via HACS if you don’t already have it. I used the standard “vertical-stack” for the cards - you’ll need to add “stack-in-card” via HACS if you want to use that.

1 Like

Ah perfect :+1:

Ok, I don’t post this here because I’m particularly proud of it, in fact I wholeheartedly welcome improvements…

This is a sensor with a state of the number of current weather warnings with the attribute ‘warnings’ giving a list of current warnings and the validity period in natural language.

For example currently my sensor shows this:

As I said I just post it here in case it is interesting to anyone, not as an example of great coding!

I use it for phone notifications. Persistent notifications and as others do here, a Lovelace card.

image

sensor:
  #================================================================================
  #=== RSS Feed for Met Office Severe weather warnings (London and the South East)
  #=== Note that these warnings do not all necessarily affect 'Greater London'
  #================================================================================
  - platform: feedparser
    name: Met Office RSS Feed South East Weather Warnings
    feed_url: 'http://metoffice.gov.uk/public/data/PWSCache/WarningsRSS/Region/se'
    date_format: '%a, %b %d %I:%M %p'



template:
  #=== Met Office London Weather Warnings
  - trigger:
      - platform: state
        entity_id: sensor.met_office_rss_feed_south_east_weather_warnings
        attribute: entries

      - platform: homeassistant
        event: start

      - platform: state
        entity_id: sensor.date

    sensor:
      name: Met Office London Weather Warnings
      state: >
        {% set ns = namespace(warning_count = 0) %}
        {% for entries in state_attr('sensor.met_office_rss_feed_south_east_weather_warnings', 'entries') %}
          {% if 'Greater London' in states.sensor.met_office_rss_feed_south_east_weather_warnings.attributes.entries[loop.index - 1].summary %}
            {% set ns.warning_count = ns.warning_count + 1 %}
          {% endif %}
        {% endfor %}

        {{ ns.warning_count }}
      attributes:
        warnings: >
          {% set warning_count = states('sensor.met_office_london_weather_warnings') %}
          {% set ns = namespace(json = '[') %}

          {% for entries in states.sensor.met_office_rss_feed_south_east_weather_warnings.attributes.entries %}
            {% if 'Greater London' in states.sensor.met_office_rss_feed_south_east_weather_warnings.attributes.entries[loop.index - 1].summary %}
              {% set ns.json = ns.json + '{"warning": "' + states.sensor.met_office_rss_feed_south_east_weather_warnings.attributes.entries[loop.index - 1].title + '", "period": "' %}

              {% set summary = states.sensor.met_office_rss_feed_south_east_weather_warnings.attributes.entries[loop.index - 1].summary %}
              {% set valid_period = summary.split(' valid from ')[1] %}
              {% set valid_from = valid_period.split(' to ')[0] %}
              {% set valid_to = valid_period.split(' to ')[1] %}

              {% set days_map = { 'Sat': 'Saturday', 'Sun': 'Sunday', 'Mon': 'Monday', 'Tue': 'Tuesday',
                                  'Wed': 'Wednesday', 'Thu': 'Thursday', 'Fri': 'Friday'} %}

              {% set months_map = { 'Jan': 'January', 'Feb': 'February', 'Mar': 'March', 'Apr': 'April',
                                    'May': 'May', 'Jun': 'June', 'Jul': 'July', 'Aug': 'August',
                                    'Sep': 'September', 'Oct': 'October', 'Nov': 'November', 'Dec': 'December'} %}

              {#                                                  #}
              {#-- Macro to build the valid from and to phrases --#}
              {#   --------------------------------------------   #}
              {#                                                  #}

              {% macro valid_from_to(validity_time) %}

              {#                              #}
              {#-- Extract hour and minutes --#}
              {#                              #}
              {% set validity_hour = validity_time.split(' ')[0][0:2] | int %}
              {% set validity_minutes = validity_time.split(' ')[0][2:4] | int %}

              {#                           #}
              {#-- Get period of the day --#}
              {#                           #}
              {% if validity_hour == 12 %}
                {% set validity_day_period = '' %}
              {% elif validity_hour == 0 %}
                {% set validity_day_period = '' %}
              {% elif validity_hour < 12 %}
                {% set validity_day_period = "morning" %}
              {% elif validity_hour > 16 %}
                {% set validity_day_period = "evening" %}
              {% else %}
                {% set validity_day_period = "afternoon" %}
              {% endif %}

              {#                                                  #}
              {#-- Convert time to 12 hour format and get AM/PM --#}
              {#                                                  #}
              {% set am_pm = '' %}
              {% if validity_hour == 12 and validity_minutes == 0 %}
                {% set validity_hour = 'noon' %}
              {% elif validity_hour == 11 and (50 < validity_minutes <= 59) %}
                {% set validity_hour = 'noon' %}
              {% elif validity_hour == 0  and validity_minutes == 0 %}
                {% set validity_hour = 'the beginning of ' %}
              {% elif validity_hour == 23 and (50 < validity_minutes <= 59) %}
                {% set validity_hour = 'the end of ' %}
              {% elif validity_hour > 12 %}
                {% set am_pm = 'pm' %}
                {% set validity_hour = validity_hour - 12 %}
              {% else %}
                {% set am_pm = 'am' %}
              {% endif %}

              {#                     #}
              {#-- Extract minutes --#}              
              {#                     #}
              {% if (0 <= validity_minutes < 10) or (50 < validity_minutes <= 59) %}
                {% set validity_minutes = am_pm %}
              {% else %}
                {% if validity_minutes < 10 %}
                  {% set validity_minutes = '0' ~ validity_minutes %}
                {% endif %}
                {% set validity_minutes = ':' ~ validity_minutes ~ am_pm %}
              {% endif %}

              {#                                 #}
              {#-- Extract day, month and date --#}
              {#                                 #}
              {% set validity_day = days_map[validity_time.split(' ')[1][0:3]] %}
              {% set validity_month = months_map[validity_time.split(' ')[-1]] %}
              {% set validity_date = validity_time.split(' ')[-2] | int | string %}

              {#                         #}
              {#-- Get suffix for date --#}
              {#                         #}
              {% if validity_date  in ('1', '21', '31') %}
                {% set suffix = 'st ' %}
              {% elif validity_date   in ('2', '22') %}
                {% set suffix = 'nd ' %}
              {% elif validity_date   in ('3', '23') %}
                {% set suffix = 'rd ' %}
              {% else %}
                {% set suffix = 'th ' %}
              {% endif %}

              {#                                        #}
              {#-- Build the final phrase             --#}
              {#-- First we need to zero pad the date --#}
              {#                                        #}
              {% set zero_padded_date = '{:02}'.format(validity_time[5:].split(' ')[1] | int) %}
              {% set zero_padded_date = validity_time[5:].split(' ')[0] + ' ' + zero_padded_date + ' ' + validity_time[5:].split(' ')[2] %}
              {% if as_timestamp(now()) | timestamp_custom('%a %d %b') == zero_padded_date %}
                {% if validity_hour == 'noon' %}
                  {% set phrase = validity_hour  ~ ' today' %}
                {% elif validity_hour == 'the beginning of ' %}
                  {% set phrase = validity_hour  ~ ' tomorrow' %}
                {% else %}
                  {% set phrase = validity_hour ~ validity_minutes ~ ' this ' ~ validity_day_period %}
                {% endif %}
              {% elif now().day + 1 == validity_date | int %}
                {% set phrase = validity_hour ~ validity_minutes ~ ' tomorrow ' ~ validity_day_period %}
              {% elif now().day - 1 == validity_date | int %}
                {% set phrase = validity_hour ~ validity_minutes ~ ' yesterday ' ~ validity_day_period %}
              {% else %}
                {% if validity_hour == 'the beginning of ' or 
                      validity_hour == 'the end of ' %}
                  {% set phrase = validity_hour ~ validity_minutes ~ validity_day ~ ' the ' ~ validity_date ~ suffix ~ 'of ' ~ validity_month %}
                {% else %}
                  {% set phrase = validity_hour ~ validity_minutes ~ ' on ' ~ validity_day ~ ' the ' ~ validity_date ~ suffix ~ 'of ' ~ validity_month %}
                {% endif %}
              {% endif %}

              {{ phrase }}

              {% endmacro %}

              {#                  #}
              {#-- End of Macro --#}
              {#   ------------   #}
              {#                  #}

              {#                                           #}
              {#-- Get the valid to and valid from phrases #}
              {#                                           #}
              {% if valid_to[:2] == '00' %}
                {% if valid_to[2:4] == '00' %}
                  {% set to = 'the beginning of ' ~ days_map[valid_to[5:8]] %}
                {% else %}
                  {% set to = valid_to[2:4] ~ ' minutes past midnight on' ~ valid_to[4:] %}
                {% endif %}
              {% else %}
                {% set to = valid_from_to(valid_to) %}
              {% endif %}

              {% if valid_from[:2] == '00' %}
                {% if valid_from[2:4] == '00' %}
                  {% set from = 'the beginning of ' ~ days_map[valid_to[5:8]] %}
                {% else %}
                  {% set from = valid_from[2:4] ~ ' minutes past midnight on' ~ valid_from[4:] %}
                {% endif %}
              {% else %}
                {% set from = valid_from_to(valid_from) %}
              {% endif %}

              {% if 'the beginning of' in from and
                    'the end of' not in to %}
                {% if now().strftime('%A') in from %}
                  {% set from = 'the beginning of today' %}
                {% elif (as_timestamp(now()) + 86400) | timestamp_custom('%A') in from %}
                  {% set from = 'the beginning of tomorrow' %}
                {% else %}
                {% endif %}

              {% elif 'the beginning of' not in to and
                    'the end of' in to %}
                {% if now().strftime('%A') in from %}
                  {% set to = 'the end of today' %}
                {% elif (as_timestamp(now()) + 86400) | timestamp_custom('%A') in from %}
                  {% set to = 'the end of tomorrow' %}
                {% else %}
                {% endif %}

              {% elif 'the beginning of' in from and
                    'the end of' in to %}
                {% if now().strftime('%A') in from %}
                  {% set from = 'All day today' %}
                {% elif (as_timestamp(now()) + 86400) | timestamp_custom('%A') in from %}
                  {% set from = 'All day tomorrow' %}
                {% else %}
                  {% set from = from.replace('the beginning of', 'All day on') + '."' %}
                {% endif %}
              {% endif %}

              {% set ns.json = ns.json + 'Valid from ' + from | trim + ' until ' + to | trim + '."' %}

              {% set ns.json = ns.json + '}, ' %}

            {% endif %}
          {% endfor %}

          {% if ns.json == '[' %}
            {% set ns.json = ns.json + ']' %}
          {% else %}
            {% set ns.json = ns.json[0:-3] + '}]' %}
          {% endif %}

          {{ ns.json }}

Curious to see if anyone is generating notifications when their sensor.weather_alerts entity increases by one and if anyone can think of a better way to handle a notification to say a new alert has been issued:

alias: Weather Alerts
description: 'Send notification when a weather alert is issued'
trigger:
  - platform: state
    entity_id: sensor.weather_alerts
condition:
  - condition: template
    value_template: '{{ trigger.to_state.state | int > trigger.from_state.state | int }}'
action:
  - service: notify.mobile_app_nick
    data:
      message: https://www.metoffice.gov.uk/weather/warnings-and-advice/uk-warnings
      title: 'Check Weather Warnings! '
mode: single