The UK Met Office publishes its weather forecasts via two APIs: DataPoint (old, with a deprecation timeline) and DataHub (new, still being developed).
The official Met Office HA integration uses DataPoint. Due to a Python version incompatibility between the DataPoint libraries and HA’s requirements, the official integration was disabled for 2024.2 (although re-enabled in the .2 point release), so with help from others pointing out DataHub and the Template Weather Provider integration, I got a free DataHub API key and cobbled together a DataHub-based weather entity. Here are all the bits (click each section to expand):
First, three RESTful sensors to pull in the hourly, three-hourly and daily forecasts. I use the RESTFul integration as opposed to the sensor platform, so these are under the rest: config
- resource: https://data.hub.api.metoffice.gov.uk/sitespecific/v0/point/hourly?datasource=BD1&includeLocationName=true&latitude=[MY_LATITUDE]&longitude=[MY_LONGITUDE]&excludeParameterMetadata=true
headers:
apikey: "MY_VERY_LONG_API_KEY"
scan_interval: 3600
sensor:
- name: Local Datahub Hourly
value_template: "{{ value_json['features'][0]['properties']['modelRunDate'] }}"
json_attributes_path: $.features.0.properties
json_attributes:
- timeSeries
- resource: https://data.hub.api.metoffice.gov.uk/sitespecific/v0/point/three-hourly?datasource=BD1&includeLocationName=true&latitude=[MY_LATITUDE]&longitude=[MY_LONGITUDE]&excludeParameterMetadata=true
headers:
apikey: "MY_VERY_LONG_API_KEY"
scan_interval: 3600
sensor:
- name: Local Datahub 3 Hourly
value_template: "{{ value_json['features'][0]['properties']['modelRunDate'] }}"
json_attributes_path: $.features.0.properties
json_attributes:
- timeSeries
- resource: https://data.hub.api.metoffice.gov.uk/sitespecific/v0/point/daily?datasource=BD1&includeLocationName=true&latitude=[MY_LATITUDE]&longitude=[MY_LONGITUDE]&excludeParameterMetadata=true
headers:
apikey: "MY_VERY_LONG_API_KEY"
scan_interval: 3600
sensor:
- name: Local Datahub Daily
value_template: "{{ value_json['features'][0]['properties']['modelRunDate'] }}"
json_attributes_path: $.features.0.properties
json_attributes:
- timeSeries
Next, a macro (reusable template) in custom_templates/met_office_codes.jinja to convert DataHub's "significant weather codes" into the weather conditions HA is expecting
{% macro code2ha(x) -%}
{{ {
0: "clear-night",
1: "sunny",
2: "partlycloudy",
3: "partlycloudy",
4: "Not used",
5: "fog",
6: "fog",
7: "cloudy",
8: "cloudy",
9: "rainy",
10: "rainy",
11: "rainy",
12: "rainy",
13: "pouring",
14: "pouring",
15: "pouring",
16: "snowy-rainy",
17: "snowy-rainy",
18: "snowy-rainy",
19: "hail",
20: "hail",
21: "hail",
22: "snowy",
23: "snowy",
24: "snowy",
25: "snowy",
26: "snowy",
27: "snowy",
28: "lightning-rainy",
29: "lightning-rainy",
30: "lightning"}.get(x, "n/a") -}}
{% endmacro %}
Another macro in custom_templates/direction.jinja to generate wind direction compass descriptions from bearing degrees, written to be flexible in the number of points and outputs — I use it elsewhere to generate a set of eight arrow symbols
{% macro dir(bearing=0,tokens=('N','NNE','NE','ENE','E','ESE','SE','SSE','S','SSW','SW','WSW','W','WNW','NW','NNW'),range=360,centre=True) -%}
{% set ns=namespace(tokens=tokens) -%}
{% if centre -%}
{% set ns.tokens = [tokens[0]] -%}
{% for t in tokens[1:] -%}
{% set ns.tokens = ns.tokens + [t] + [t] -%}
{% endfor -%}
{% set ns.tokens = ns.tokens + [tokens[0]] -%}
{% endif -%}
{% set divs = ns.tokens|count -%}
{% set bclip = ((0,bearing,range-1)|sort)[1] -%}
{{ ns.tokens[(bclip * divs // range)|int] -}}
{% endmacro -%}
Some template sensors (under template: config) to pull the current condition from the forecast.
The forecast data often extends a couple of hours into the past, so the current_weather
pulls out the data point closest to the current time. Its state is the index in the forecast list of that data point, referred to in its attribute.
- sensor:
- name: Local current weather
state: >
{% set tsl = state_attr('sensor.local_datahub_hourly','timeSeries')|map(attribute='time')|map('as_timestamp')|list %}
{% set ts = tsl|select('>=',(now()-timedelta(minutes=30))|as_timestamp())|first %}
{{ tsl.index(ts) }}
attributes:
status: "{{ state_attr('sensor.local_datahub_hourly','timeSeries')[this.state|int(0)] }}"
availability: "{{ states('sensor.local_datahub_hourly') not in ('unavailable', 'unknown') }}"
- sensor:
- name: Local weather condition
state: >
{% from 'met_office_codes.jinja' import code2ha %}
{{ code2ha(state_attr('sensor.local_current_weather','status')['significantWeatherCode']) }}
attributes:
timestamp: "{{ state_attr('sensor.local_current_weather','status')['time'] }}"
availability: "{{ states('sensor.local_current_weather') not in ('unavailable', 'unknown') }}"
- sensor:
- name: Local wind bearing
state: >
{% from 'direction.jinja' import dir %}
{{ dir(state_attr('sensor.local_current_weather','status')['windDirectionFrom10m']) }}
attributes:
timestamp: "{{ state_attr('sensor.local_current_weather','status')['time'] }}"
availability: "{{ states('sensor.local_current_weather') not in ('unavailable', 'unknown') }}"
The big one: the template weather entity.
This is under the weather: config, and I’ve put it in weather.yaml
with an !include
in the main config. Once it’s working, any changes can be pulled in with a reload of template entities.
I use my own outside temperature sensor for the current temperature. If you want to use the supplied data, use this in place of my line:
temperature_template: "{{ state_attr('sensor.local_current_weather','status')['screenTemperature'] }}"
The entity expects a twice-daily (day/night) forecast, which I generate from the three-hourly by taking the 00:00 and 12:00 points, and additionally finding the min and max temperature, average pressure, windspeed etc from the surrounding points.
Units are set to match the incoming data, and HA will display as per your settings. Most of the work here is mapping the DataHub keys to the appropriate weather entity keys. All forecasts are filtered to include future points only.
There is a lot of checking for the existence of the template sensors above to prevent errors on startup due to the order of evaluation.
- platform: template
name: "Met Office Datahub"
unique_id: met_office_datahub
condition_template: "{{ states('sensor.local_weather_condition') }}"
temperature_template: "{{ states('sensor.outside_temperature')|float(0) }}"
apparent_temperature_template: >
{% if state_attr('sensor.local_current_weather','status') is mapping %}
{{ state_attr('sensor.local_current_weather','status').get('feelsLikeTemperature', 0) }}
{% else %}
0
{% endif %}
temperature_unit: "°C"
humidity_template: >
{% if state_attr('sensor.local_current_weather','status') is mapping %}
{{ state_attr('sensor.local_current_weather','status').get('screenRelativeHumidity', 0) }}
{% else %}
0
{% endif %}
attribution_template: "Met Office DataHub plus local"
pressure_template: >
{% if state_attr('sensor.local_current_weather','status') is mapping %}
{{ state_attr('sensor.local_current_weather','status').get('mslp', 0) }}
{% else %}
0
{% endif %}
pressure_unit: "Pa"
visibility_template: >
{% if state_attr('sensor.local_current_weather','status') is mapping %}
{{ state_attr('sensor.local_current_weather','status').get('visibility', 0) }}
{% else %}
0
{% endif %}
visibility_unit: "m"
wind_speed_template: >
{% if state_attr('sensor.local_current_weather','status') is mapping %}
{{ state_attr('sensor.local_current_weather','status').get('windSpeed10m', 0) }}
{% else %}
0
{% endif %}
wind_speed_unit: "m/s"
wind_bearing_template: "{{ states('sensor.local_wind_bearing') }}"
forecast_hourly_template: >
{% from 'met_office_codes.jinja' import code2ha %}
{% from 'direction.jinja' import dir %}
{% set dh = state_attr('sensor.local_datahub_hourly','timeSeries') %}
{% set ns = namespace(forecast=[]) %}
{% for ts in dh -%}
{% if ts['time']|as_datetime > now() %}
{% set tsd = { 'datetime': (ts['time']|as_datetime).isoformat(),
'condition': code2ha(ts['significantWeatherCode']),
'precipitation_probability': ts['probOfPrecipitation'],
'wind_bearing': dir(ts['windDirectionFrom10m']),
'humidity': ts['screenRelativeHumidity'],
'pressure': ts['mslp'],
'uv_index': ts['uvIndex'],
'temperature': ts['screenTemperature']|round(0),
'apparent_temperature': ts['feelsLikeTemperature']|round(0),
'wind_speed': ts['windSpeed10m']|round(0) } -%}
{% set ns.forecast = ns.forecast + [tsd] -%}
{% endif %}
{% endfor %}
{{ ns.forecast }}
forecast_twice_daily_template: >
{% from 'met_office_codes.jinja' import code2ha %}
{% set dh = state_attr('sensor.local_datahub_3_hourly','timeSeries') %}
{% set ns = namespace(forecast=[]) %}
{% for ts in dh -%}
{% if 'T00' in ts['time'] or 'T12' in ts['time'] and ts['time']|as_datetime > now() -%}
{% set index = (dh|map(attribute='time')|list).index(ts['time']) -%}
{% set dhr = dh[((0,index-1,dh|count)|sort)[1]:((0,index+2,dh|count)|sort)[1]] %}
{% set tsd = { 'datetime': (ts['time']|as_datetime).isoformat(),
'is_daytime': 'T12' in ts['time'],
'condition': code2ha(ts['significantWeatherCode']),
'humidity': dhr|map(attribute='screenRelativeHumidity')|average|round(0),
'pressure': dhr|map(attribute='mslp')|average|round(0),
'temperature': dhr|map(attribute='maxScreenAirTemp')|max|round(0),
'templow': dhr|map(attribute='minScreenAirTemp')|min|round(0),
'precipitation_probability': dhr|map(attribute='probOfPrecipitation')|average|round(0),
'wind_speed': dhr|map(attribute='windSpeed10m')|average|round(0) } -%}
{% set ns.forecast = ns.forecast + [tsd] -%}
{% endif -%}
{% endfor %}
{{ ns.forecast }}
forecast_daily_template: >
{% from 'met_office_codes.jinja' import code2ha %}
{% set dh = state_attr('sensor.local_datahub_daily','timeSeries') %}
{% set ns = namespace(forecast=[]) %}
{% for ts in dh -%}
{% if ts['time']|as_datetime > now() %}
{% set tsd = { 'datetime': (ts['time']|as_datetime).isoformat(),
'condition': code2ha(ts.get('daySignificantWeatherCode', ts.get('nightSignificantWeatherCode', 'n/a'))),
'humidity': ts['middayRelativeHumidity'],
'pressure': ts['middayMslp'],
'temperature': ts['dayMaxScreenTemperature']|round(0),
'templow': ts['nightMinScreenTemperature']|round(0),
'wind_speed': ts['midday10MWindSpeed']|round(0) } -%}
{% set ns.forecast = ns.forecast + [tsd] -%}
{% endif %}
{% endfor %}
{{ ns.forecast }}
The finished result — click the cog icon to set the units you prefer:
Obviously, the long-term ideal would be to have a proper weather integration using DataHub. Hopefully someone will conjure one up so I don’t have to learn how to write integrations