Convert MetOffice to use UI for configuration (#34900)

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
Ian Harcombe 2020-06-15 11:02:25 +01:00 committed by GitHub
parent 9a867cbb75
commit c96458c7e4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 2602 additions and 227 deletions

View file

@ -244,6 +244,7 @@ homeassistant/components/melissa/* @kennedyshead
homeassistant/components/met/* @danielhiversen homeassistant/components/met/* @danielhiversen
homeassistant/components/meteo_france/* @victorcerutti @oncleben31 @Quentame homeassistant/components/meteo_france/* @victorcerutti @oncleben31 @Quentame
homeassistant/components/meteoalarm/* @rolfberkenbosch homeassistant/components/meteoalarm/* @rolfberkenbosch
homeassistant/components/metoffice/* @MrHarcombe
homeassistant/components/miflora/* @danielhiversen @ChristianKuehnel homeassistant/components/miflora/* @danielhiversen @ChristianKuehnel
homeassistant/components/mikrotik/* @engrbm87 homeassistant/components/mikrotik/* @engrbm87
homeassistant/components/mill/* @danielhiversen homeassistant/components/mill/* @danielhiversen

View file

@ -1 +1,86 @@
"""The metoffice component.""" """The Met Office integration."""
import asyncio
import logging
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import (
DEFAULT_SCAN_INTERVAL,
DOMAIN,
METOFFICE_COORDINATOR,
METOFFICE_DATA,
METOFFICE_NAME,
)
from .data import MetOfficeData
_LOGGER = logging.getLogger(__name__)
PLATFORMS = ["sensor", "weather"]
async def async_setup(hass: HomeAssistant, config: dict):
"""Set up the Met Office weather component."""
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up a Met Office entry."""
latitude = entry.data[CONF_LATITUDE]
longitude = entry.data[CONF_LONGITUDE]
api_key = entry.data[CONF_API_KEY]
site_name = entry.data[CONF_NAME]
metoffice_data = MetOfficeData(hass, api_key, latitude, longitude)
await metoffice_data.async_update_site()
if metoffice_data.site_name is None:
raise ConfigEntryNotReady()
metoffice_coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
name=f"MetOffice Coordinator for {site_name}",
update_method=metoffice_data.async_update,
update_interval=DEFAULT_SCAN_INTERVAL,
)
metoffice_hass_data = hass.data.setdefault(DOMAIN, {})
metoffice_hass_data[entry.entry_id] = {
METOFFICE_DATA: metoffice_data,
METOFFICE_COORDINATOR: metoffice_coordinator,
METOFFICE_NAME: site_name,
}
# Fetch initial data so we have data when entities subscribe
await metoffice_coordinator.async_refresh()
if metoffice_data.now is None:
raise ConfigEntryNotReady()
for component in PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, component)
)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Unload a config entry."""
unload_ok = all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(entry, component)
for component in PLATFORMS
]
)
)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
if not hass.data[DOMAIN]:
hass.data.pop(DOMAIN)
return unload_ok

View file

@ -0,0 +1,79 @@
"""Config flow for Met Office integration."""
import logging
import voluptuous as vol
from homeassistant import config_entries, core, exceptions
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
from homeassistant.helpers import config_validation as cv
from .const import DOMAIN # pylint: disable=unused-import
from .data import MetOfficeData
_LOGGER = logging.getLogger(__name__)
async def validate_input(hass: core.HomeAssistant, data):
"""Validate that the user input allows us to connect to DataPoint.
Data has the keys from DATA_SCHEMA with values provided by the user.
"""
latitude = data[CONF_LATITUDE]
longitude = data[CONF_LONGITUDE]
api_key = data[CONF_API_KEY]
metoffice_data = MetOfficeData(hass, api_key, latitude, longitude)
await metoffice_data.async_update_site()
if metoffice_data.site_name is None:
raise CannotConnect()
return {"site_name": metoffice_data.site_name}
class MetOfficeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Met Office weather integration."""
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
async def async_step_user(self, user_input=None):
"""Handle the initial step."""
errors = {}
if user_input is not None:
await self.async_set_unique_id(
f"{user_input[CONF_LATITUDE]}_{user_input[CONF_LONGITUDE]}"
)
self._abort_if_unique_id_configured()
try:
info = await validate_input(self.hass, user_input)
except CannotConnect:
errors["base"] = "cannot_connect"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
user_input[CONF_NAME] = info["site_name"]
return self.async_create_entry(
title=user_input[CONF_NAME], data=user_input
)
data_schema = vol.Schema(
{
vol.Required(CONF_API_KEY): str,
vol.Required(
CONF_LATITUDE, default=self.hass.config.latitude
): cv.latitude,
vol.Required(
CONF_LONGITUDE, default=self.hass.config.longitude
): cv.longitude,
},
)
return self.async_show_form(
step_id="user", data_schema=data_schema, errors=errors
)
class CannotConnect(exceptions.HomeAssistantError):
"""Error to indicate we cannot connect."""

View file

@ -0,0 +1,51 @@
"""Constants for Met Office Integration."""
from datetime import timedelta
DOMAIN = "metoffice"
DEFAULT_NAME = "Met Office"
ATTRIBUTION = "Data provided by the Met Office"
DEFAULT_SCAN_INTERVAL = timedelta(minutes=15)
METOFFICE_DATA = "metoffice_data"
METOFFICE_COORDINATOR = "metoffice_coordinator"
METOFFICE_MONITORED_CONDITIONS = "metoffice_monitored_conditions"
METOFFICE_NAME = "metoffice_name"
MODE_3HOURLY = "3hourly"
CONDITION_CLASSES = {
"cloudy": ["7", "8"],
"fog": ["5", "6"],
"hail": ["19", "20", "21"],
"lightning": ["30"],
"lightning-rainy": ["28", "29"],
"partlycloudy": ["2", "3"],
"pouring": ["13", "14", "15"],
"rainy": ["9", "10", "11", "12"],
"snowy": ["22", "23", "24", "25", "26", "27"],
"snowy-rainy": ["16", "17", "18"],
"sunny": ["0", "1"],
"windy": [],
"windy-variant": [],
"exceptional": [],
}
VISIBILITY_CLASSES = {
"VP": "Very Poor",
"PO": "Poor",
"MO": "Moderate",
"GO": "Good",
"VG": "Very Good",
"EX": "Excellent",
}
VISIBILITY_DISTANCE_CLASSES = {
"VP": "<1",
"PO": "1-4",
"MO": "4-10",
"GO": "10-20",
"VG": "20-40",
"EX": ">40",
}

View file

@ -0,0 +1,78 @@
"""Common Met Office Data class used by both sensor and entity."""
import logging
import datapoint
from .const import MODE_3HOURLY
_LOGGER = logging.getLogger(__name__)
class MetOfficeData:
"""Get current and forecast data from Datapoint.
Please note that the 'datapoint' library is not asyncio-friendly, so some
calls have had to be wrapped with the standard hassio helper
async_add_executor_job.
"""
def __init__(self, hass, api_key, latitude, longitude):
"""Initialize the data object."""
self._hass = hass
self._datapoint = datapoint.connection(api_key=api_key)
self._site = None
# Public attributes
self.latitude = latitude
self.longitude = longitude
# Holds the current data from the Met Office
self.site_id = None
self.site_name = None
self.now = None
async def async_update_site(self):
"""Async wrapper for getting the DataPoint site."""
return await self._hass.async_add_executor_job(self._update_site)
def _update_site(self):
"""Return the nearest DataPoint Site to the held latitude/longitude."""
try:
new_site = self._datapoint.get_nearest_forecast_site(
latitude=self.latitude, longitude=self.longitude
)
if self._site is None or self._site.id != new_site.id:
self._site = new_site
self.now = None
self.site_id = self._site.id
self.site_name = self._site.name
except datapoint.exceptions.APIException as err:
_LOGGER.error("Received error from Met Office Datapoint: %s", err)
self._site = None
self.site_id = None
self.site_name = None
self.now = None
return self._site
async def async_update(self):
"""Async wrapper for update method."""
return await self._hass.async_add_executor_job(self._update)
def _update(self):
"""Get the latest data from DataPoint."""
if self._site is None:
_LOGGER.error("No Met Office forecast site held, check logs for problems")
return
try:
forecast = self._datapoint.get_forecast_for_site(
self._site.id, MODE_3HOURLY
)
self.now = forecast.now()
except (ValueError, datapoint.exceptions.APIException) as err:
_LOGGER.error("Check Met Office connection: %s", err.args)
self.now = None

View file

@ -3,5 +3,6 @@
"name": "Met Office", "name": "Met Office",
"documentation": "https://www.home-assistant.io/integrations/metoffice", "documentation": "https://www.home-assistant.io/integrations/metoffice",
"requirements": ["datapoint==0.9.5"], "requirements": ["datapoint==0.9.5"],
"codeowners": [] "codeowners": ["@MrHarcombe"],
"config_flow": true
} }

View file

@ -1,27 +1,31 @@
"""Support for UK Met Office weather service.""" """Support for UK Met Office weather service."""
from datetime import timedelta
import logging import logging
import datapoint as dp
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import ( from homeassistant.const import (
ATTR_ATTRIBUTION, ATTR_ATTRIBUTION,
CONF_API_KEY, DEVICE_CLASS_HUMIDITY,
CONF_LATITUDE, DEVICE_CLASS_TEMPERATURE,
CONF_LONGITUDE,
CONF_MONITORED_CONDITIONS,
CONF_NAME,
LENGTH_KILOMETERS, LENGTH_KILOMETERS,
SPEED_MILES_PER_HOUR, SPEED_MILES_PER_HOUR,
TEMP_CELSIUS, TEMP_CELSIUS,
UNIT_PERCENTAGE, UNIT_PERCENTAGE,
UV_INDEX, UV_INDEX,
) )
import homeassistant.helpers.config_validation as cv from homeassistant.core import callback
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle from homeassistant.helpers.typing import ConfigType, HomeAssistantType
from .const import (
ATTRIBUTION,
CONDITION_CLASSES,
DOMAIN,
METOFFICE_COORDINATOR,
METOFFICE_DATA,
METOFFICE_NAME,
VISIBILITY_CLASSES,
VISIBILITY_DISTANCE_CLASSES,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -30,175 +34,190 @@ ATTR_SENSOR_ID = "sensor_id"
ATTR_SITE_ID = "site_id" ATTR_SITE_ID = "site_id"
ATTR_SITE_NAME = "site_name" ATTR_SITE_NAME = "site_name"
ATTRIBUTION = "Data provided by the Met Office" # Sensor types are defined as:
# variable -> [0]title, [1]device_class, [2]units, [3]icon, [4]enabled_by_default
CONDITION_CLASSES = {
"cloudy": ["7", "8"],
"fog": ["5", "6"],
"hail": ["19", "20", "21"],
"lightning": ["30"],
"lightning-rainy": ["28", "29"],
"partlycloudy": ["2", "3"],
"pouring": ["13", "14", "15"],
"rainy": ["9", "10", "11", "12"],
"snowy": ["22", "23", "24", "25", "26", "27"],
"snowy-rainy": ["16", "17", "18"],
"sunny": ["0", "1"],
"windy": [],
"windy-variant": [],
"exceptional": [],
}
DEFAULT_NAME = "Met Office"
VISIBILITY_CLASSES = {
"VP": "<1",
"PO": "1-4",
"MO": "4-10",
"GO": "10-20",
"VG": "20-40",
"EX": ">40",
}
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=35)
# Sensor types are defined like: Name, units
SENSOR_TYPES = { SENSOR_TYPES = {
"name": ["Station Name", None], "name": ["Station Name", None, None, "mdi:label-outline", False],
"weather": ["Weather", None], "weather": [
"temperature": ["Temperature", TEMP_CELSIUS], "Weather",
"feels_like_temperature": ["Feels Like Temperature", TEMP_CELSIUS], None,
"wind_speed": ["Wind Speed", SPEED_MILES_PER_HOUR], None,
"wind_direction": ["Wind Direction", None], "mdi:weather-sunny", # but will adapt to current conditions
"wind_gust": ["Wind Gust", SPEED_MILES_PER_HOUR], True,
"visibility": ["Visibility", None], ],
"visibility_distance": ["Visibility Distance", LENGTH_KILOMETERS], "temperature": ["Temperature", DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None, True],
"uv": ["UV", UV_INDEX], "feels_like_temperature": [
"precipitation": ["Probability of Precipitation", UNIT_PERCENTAGE], "Feels Like Temperature",
"humidity": ["Humidity", UNIT_PERCENTAGE], DEVICE_CLASS_TEMPERATURE,
TEMP_CELSIUS,
None,
False,
],
"wind_speed": [
"Wind Speed",
None,
SPEED_MILES_PER_HOUR,
"mdi:weather-windy",
True,
],
"wind_direction": ["Wind Direction", None, None, "mdi:compass-outline", False],
"wind_gust": ["Wind Gust", None, SPEED_MILES_PER_HOUR, "mdi:weather-windy", False],
"visibility": ["Visibility", None, None, "mdi:eye", False],
"visibility_distance": [
"Visibility Distance",
None,
LENGTH_KILOMETERS,
"mdi:eye",
False,
],
"uv": ["UV Index", None, UV_INDEX, "mdi:weather-sunny-alert", True],
"precipitation": [
"Probability of Precipitation",
None,
UNIT_PERCENTAGE,
"mdi:weather-rainy",
True,
],
"humidity": ["Humidity", DEVICE_CLASS_HUMIDITY, UNIT_PERCENTAGE, None, False],
} }
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_API_KEY): cv.string,
vol.Required(CONF_MONITORED_CONDITIONS, default=[]): vol.All(
cv.ensure_list, [vol.In(SENSOR_TYPES)]
),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Inclusive(
CONF_LATITUDE, "coordinates", "Latitude and longitude must exist together"
): cv.latitude,
vol.Inclusive(
CONF_LONGITUDE, "coordinates", "Latitude and longitude must exist together"
): cv.longitude,
}
)
async def async_setup_entry(
hass: HomeAssistantType, entry: ConfigType, async_add_entities
) -> None:
"""Set up the Met Office weather sensor platform."""
hass_data = hass.data[DOMAIN][entry.entry_id]
def setup_platform(hass, config, add_entities, discovery_info=None): async_add_entities(
"""Set up the Met Office sensor platform.""" [
api_key = config.get(CONF_API_KEY) MetOfficeCurrentSensor(entry.data, hass_data, sensor_type)
latitude = config.get(CONF_LATITUDE, hass.config.latitude) for sensor_type in SENSOR_TYPES
longitude = config.get(CONF_LONGITUDE, hass.config.longitude) ],
name = config.get(CONF_NAME) False,
)
datapoint = dp.connection(api_key=api_key)
if None in (latitude, longitude):
_LOGGER.error("Latitude or longitude not set in Home Assistant config")
return
try:
site = datapoint.get_nearest_site(latitude=latitude, longitude=longitude)
except dp.exceptions.APIException as err:
_LOGGER.error("Received error from Met Office Datapoint: %s", err)
return
if not site:
_LOGGER.error("Unable to get nearest Met Office forecast site")
return
data = MetOfficeCurrentData(hass, datapoint, site)
data.update()
if data.data is None:
return
sensors = []
for variable in config[CONF_MONITORED_CONDITIONS]:
sensors.append(MetOfficeCurrentSensor(site, data, variable, name))
add_entities(sensors, True)
class MetOfficeCurrentSensor(Entity): class MetOfficeCurrentSensor(Entity):
"""Implementation of a Met Office current sensor.""" """Implementation of a Met Office current weather condition sensor."""
def __init__(self, site, data, condition, name): def __init__(self, entry_data, hass_data, sensor_type):
"""Initialize the sensor.""" """Initialize the sensor."""
self._condition = condition self._data = hass_data[METOFFICE_DATA]
self.data = data self._coordinator = hass_data[METOFFICE_COORDINATOR]
self._name = name
self.site = site self._type = sensor_type
self._name = f"{hass_data[METOFFICE_NAME]} {SENSOR_TYPES[self._type][0]}"
self._unique_id = f"{SENSOR_TYPES[self._type][0]}_{self._data.latitude}_{self._data.longitude}"
self.metoffice_site_id = None
self.metoffice_site_name = None
self.metoffice_now = None
@property @property
def name(self): def name(self):
"""Return the name of the sensor.""" """Return the name of the sensor."""
return f"{self._name} {SENSOR_TYPES[self._condition][0]}" return self._name
@property
def unique_id(self):
"""Return the unique of the sensor."""
return self._unique_id
@property @property
def state(self): def state(self):
"""Return the state of the sensor.""" """Return the state of the sensor."""
if self._condition == "visibility_distance" and hasattr( value = None
self.data.data, "visibility"
if self._type == "visibility_distance" and hasattr(
self.metoffice_now, "visibility"
): ):
return VISIBILITY_CLASSES.get(self.data.data.visibility.value) value = VISIBILITY_DISTANCE_CLASSES.get(self.metoffice_now.visibility.value)
if hasattr(self.data.data, self._condition):
variable = getattr(self.data.data, self._condition) if self._type == "visibility" and hasattr(self.metoffice_now, "visibility"):
if self._condition == "weather": value = VISIBILITY_CLASSES.get(self.metoffice_now.visibility.value)
return [
k elif self._type == "weather" and hasattr(self.metoffice_now, self._type):
for k, v in CONDITION_CLASSES.items() value = [
if self.data.data.weather.value in v k
][0] for k, v in CONDITION_CLASSES.items()
return variable.value if self.metoffice_now.weather.value in v
return None ][0]
elif hasattr(self.metoffice_now, self._type):
value = getattr(self.metoffice_now, self._type)
if not isinstance(value, int):
value = value.value
return value
@property @property
def unit_of_measurement(self): def unit_of_measurement(self):
"""Return the unit of measurement.""" """Return the unit of measurement."""
return SENSOR_TYPES[self._condition][1] return SENSOR_TYPES[self._type][2]
@property
def icon(self):
"""Return the icon for the entity card."""
value = SENSOR_TYPES[self._type][3]
if self._type == "weather":
value = self.state
if value is None:
value = "sunny"
elif value == "partlycloudy":
value = "partly-cloudy"
value = f"mdi:weather-{value}"
return value
@property
def device_class(self):
"""Return the device class of the sensor."""
return SENSOR_TYPES[self._type][1]
@property @property
def device_state_attributes(self): def device_state_attributes(self):
"""Return the state attributes of the device.""" """Return the state attributes of the device."""
attr = {} return {
attr[ATTR_ATTRIBUTION] = ATTRIBUTION ATTR_ATTRIBUTION: ATTRIBUTION,
attr[ATTR_LAST_UPDATE] = self.data.data.date ATTR_LAST_UPDATE: self.metoffice_now.date if self.metoffice_now else None,
attr[ATTR_SENSOR_ID] = self._condition ATTR_SENSOR_ID: self._type,
attr[ATTR_SITE_ID] = self.site.id ATTR_SITE_ID: self.metoffice_site_id if self.metoffice_site_id else None,
attr[ATTR_SITE_NAME] = self.site.name ATTR_SITE_NAME: self.metoffice_site_name
return attr if self.metoffice_site_name
else None,
}
def update(self): async def async_added_to_hass(self) -> None:
"""Update current conditions.""" """Set up a listener and load data."""
self.data.update() self.async_on_remove(
self._coordinator.async_add_listener(self._update_callback)
)
self._update_callback()
async def async_update(self):
"""Schedule a custom update via the common entity update service."""
await self._coordinator.async_request_refresh()
class MetOfficeCurrentData: @callback
"""Get data from Datapoint.""" def _update_callback(self) -> None:
"""Load data from integration."""
self.metoffice_site_id = self._data.site_id
self.metoffice_site_name = self._data.site_name
self.metoffice_now = self._data.now
self.async_write_ha_state()
def __init__(self, hass, datapoint, site): @property
"""Initialize the data object.""" def should_poll(self) -> bool:
self._datapoint = datapoint """Entities do not individually poll."""
self._site = site return False
self.data = None
@Throttle(MIN_TIME_BETWEEN_UPDATES) @property
def update(self): def entity_registry_enabled_default(self) -> bool:
"""Get the latest data from Datapoint.""" """Return if the entity should be enabled when first added to the entity registry."""
try: return SENSOR_TYPES[self._type][4]
forecast = self._datapoint.get_forecast_for_site(self._site.id, "3hourly")
self.data = forecast.now() @property
except (ValueError, dp.exceptions.APIException) as err: def available(self):
_LOGGER.error("Check Met Office %s", err.args) """Return if state is available."""
self.data = None return self.metoffice_site_id is not None and self.metoffice_now is not None

View file

@ -0,0 +1,22 @@
{
"config": {
"step": {
"user": {
"description": "The latitude and longitude will be used to find the closest weather station.",
"title": "Connect to the UK Met Office",
"data": {
"api_key": "Met Office DataPoint API key",
"latitude": "Latitude",
"longitude": "Longitude"
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
}
}

View file

@ -0,0 +1,23 @@
{
"config": {
"abort": {
"already_configured": "Device is already configured"
},
"error": {
"cannot_connect": "Failed to connect, please try again",
"unknown": "Unexpected error"
},
"step": {
"user": {
"data": {
"api_key": "Met Office DataPoint API key",
"latitude": "Latitude",
"longitude": "Longitude",
"name": "Friendly name"
},
"description": "The latitude and longitude will be used to find the closest weather station.",
"title": "Connect to the UK Met Office"
}
}
}
}

View file

@ -1,127 +1,161 @@
"""Support for UK Met Office weather service.""" """Support for UK Met Office weather service."""
import logging import logging
import datapoint as dp from homeassistant.components.weather import WeatherEntity
import voluptuous as vol from homeassistant.const import LENGTH_KILOMETERS, TEMP_CELSIUS
from homeassistant.core import callback
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
from homeassistant.components.weather import PLATFORM_SCHEMA, WeatherEntity from .const import (
from homeassistant.const import ( ATTRIBUTION,
CONF_API_KEY, CONDITION_CLASSES,
CONF_LATITUDE, DEFAULT_NAME,
CONF_LONGITUDE, DOMAIN,
CONF_NAME, METOFFICE_COORDINATOR,
TEMP_CELSIUS, METOFFICE_DATA,
METOFFICE_NAME,
VISIBILITY_CLASSES,
VISIBILITY_DISTANCE_CLASSES,
) )
from homeassistant.helpers import config_validation as cv
from .sensor import ATTRIBUTION, CONDITION_CLASSES, MetOfficeCurrentData
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = "Met Office"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_entry(
{ hass: HomeAssistantType, entry: ConfigType, async_add_entities
vol.Required(CONF_API_KEY): cv.string, ) -> None:
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, """Set up the Met Office weather sensor platform."""
vol.Inclusive( hass_data = hass.data[DOMAIN][entry.entry_id]
CONF_LATITUDE, "coordinates", "Latitude and longitude must exist together"
): cv.latitude,
vol.Inclusive(
CONF_LONGITUDE, "coordinates", "Latitude and longitude must exist together"
): cv.longitude,
}
)
async_add_entities(
def setup_platform(hass, config, add_entities, discovery_info=None): [MetOfficeWeather(entry.data, hass_data,)], False,
"""Set up the Met Office weather platform.""" )
name = config.get(CONF_NAME)
datapoint = dp.connection(api_key=config.get(CONF_API_KEY))
latitude = config.get(CONF_LATITUDE, hass.config.latitude)
longitude = config.get(CONF_LONGITUDE, hass.config.longitude)
if None in (latitude, longitude):
_LOGGER.error("Latitude or longitude not set in Home Assistant config")
return
try:
site = datapoint.get_nearest_site(latitude=latitude, longitude=longitude)
except dp.exceptions.APIException as err:
_LOGGER.error("Received error from Met Office Datapoint: %s", err)
return
if not site:
_LOGGER.error("Unable to get nearest Met Office forecast site")
return
data = MetOfficeCurrentData(hass, datapoint, site)
try:
data.update()
except (ValueError, dp.exceptions.APIException) as err:
_LOGGER.error("Received error from Met Office Datapoint: %s", err)
return
add_entities([MetOfficeWeather(site, data, name)], True)
class MetOfficeWeather(WeatherEntity): class MetOfficeWeather(WeatherEntity):
"""Implementation of a Met Office weather condition.""" """Implementation of a Met Office weather condition."""
def __init__(self, site, data, name): def __init__(self, entry_data, hass_data):
"""Initialise the platform with a data instance and site.""" """Initialise the platform with a data instance."""
self._name = name self._data = hass_data[METOFFICE_DATA]
self.data = data self._coordinator = hass_data[METOFFICE_COORDINATOR]
self.site = site
def update(self): self._name = f"{DEFAULT_NAME} {hass_data[METOFFICE_NAME]}"
"""Update current conditions.""" self._unique_id = f"{self._data.latitude}_{self._data.longitude}"
self.data.update()
self.metoffice_now = None
@property @property
def name(self): def name(self):
"""Return the name of the sensor.""" """Return the name of the sensor."""
return f"{self._name} {self.site.name}" return self._name
@property
def unique_id(self):
"""Return the unique of the sensor."""
return self._unique_id
@property @property
def condition(self): def condition(self):
"""Return the current condition.""" """Return the current condition."""
return [ return (
k for k, v in CONDITION_CLASSES.items() if self.data.data.weather.value in v [
][0] k
for k, v in CONDITION_CLASSES.items()
if self.metoffice_now.weather.value in v
][0]
if self.metoffice_now
else None
)
@property @property
def temperature(self): def temperature(self):
"""Return the platform temperature.""" """Return the platform temperature."""
return self.data.data.temperature.value return (
self.metoffice_now.temperature.value
if self.metoffice_now and self.metoffice_now.temperature
else None
)
@property @property
def temperature_unit(self): def temperature_unit(self):
"""Return the unit of measurement.""" """Return the unit of measurement."""
return TEMP_CELSIUS return TEMP_CELSIUS
@property
def visibility(self):
"""Return the platform visibility."""
_visibility = None
if hasattr(self.metoffice_now, "visibility"):
_visibility = f"{VISIBILITY_CLASSES.get(self.metoffice_now.visibility.value)} - {VISIBILITY_DISTANCE_CLASSES.get(self.metoffice_now.visibility.value)}"
return _visibility
@property
def visibility_unit(self):
"""Return the unit of measurement."""
return LENGTH_KILOMETERS
@property @property
def pressure(self): def pressure(self):
"""Return the mean sea-level pressure.""" """Return the mean sea-level pressure."""
return None return (
self.metoffice_now.pressure.value
if self.metoffice_now and self.metoffice_now.pressure
else None
)
@property @property
def humidity(self): def humidity(self):
"""Return the relative humidity.""" """Return the relative humidity."""
return self.data.data.humidity.value return (
self.metoffice_now.humidity.value
if self.metoffice_now and self.metoffice_now.humidity
else None
)
@property @property
def wind_speed(self): def wind_speed(self):
"""Return the wind speed.""" """Return the wind speed."""
return self.data.data.wind_speed.value return (
self.metoffice_now.wind_speed.value
if self.metoffice_now and self.metoffice_now.wind_speed
else None
)
@property @property
def wind_bearing(self): def wind_bearing(self):
"""Return the wind bearing.""" """Return the wind bearing."""
return self.data.data.wind_direction.value return (
self.metoffice_now.wind_direction.value
if self.metoffice_now and self.metoffice_now.wind_direction
else None
)
@property @property
def attribution(self): def attribution(self):
"""Return the attribution.""" """Return the attribution."""
return ATTRIBUTION return ATTRIBUTION
async def async_added_to_hass(self) -> None:
"""Set up a listener and load data."""
self.async_on_remove(
self._coordinator.async_add_listener(self._update_callback)
)
self._update_callback()
@callback
def _update_callback(self) -> None:
"""Load data from integration."""
self.metoffice_now = self._data.now
self.async_write_ha_state()
@property
def should_poll(self) -> bool:
"""Entities do not individually poll."""
return False
@property
def available(self):
"""Return if state is available."""
return self.metoffice_now is not None

View file

@ -94,6 +94,7 @@ FLOWS = [
"melcloud", "melcloud",
"met", "met",
"meteo_france", "meteo_france",
"metoffice",
"mikrotik", "mikrotik",
"mill", "mill",
"minecraft_server", "minecraft_server",

View file

@ -202,6 +202,9 @@ coronavirus==1.1.1
# homeassistant.components.datadog # homeassistant.components.datadog
datadog==0.15.0 datadog==0.15.0
# homeassistant.components.metoffice
datapoint==0.9.5
# homeassistant.components.ihc # homeassistant.components.ihc
# homeassistant.components.namecheapdns # homeassistant.components.namecheapdns
# homeassistant.components.ohmconnect # homeassistant.components.ohmconnect

View file

@ -0,0 +1 @@
"""Tests for the metoffice component."""

View file

@ -0,0 +1,22 @@
"""Fixtures for Met Office weather integration tests."""
from datapoint.exceptions import APIException
import pytest
from tests.async_mock import patch
@pytest.fixture()
def mock_simple_manager_fail():
"""Mock datapoint Manager with default values for testing in config_flow."""
with patch("datapoint.Manager") as mock_manager:
instance = mock_manager.return_value
instance.get_nearest_forecast_site.side_effect = APIException()
instance.get_forecast_for_site.side_effect = APIException()
instance.latitude = None
instance.longitude = None
instance.site = None
instance.site_id = None
instance.site_name = None
instance.now = None
yield mock_manager

View file

@ -0,0 +1,58 @@
"""Helpers for testing Met Office DataPoint."""
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S%z"
TEST_DATETIME_STRING = "2020-04-25 12:00:00+0000"
TEST_API_KEY = "test-metoffice-api-key"
TEST_LATITUDE_WAVERTREE = 53.38374
TEST_LONGITUDE_WAVERTREE = -2.90929
TEST_SITE_NAME_WAVERTREE = "Wavertree"
TEST_LATITUDE_KINGSLYNN = 52.75556
TEST_LONGITUDE_KINGSLYNN = 0.44231
TEST_SITE_NAME_KINGSLYNN = "King's Lynn"
METOFFICE_CONFIG_WAVERTREE = {
CONF_API_KEY: TEST_API_KEY,
CONF_LATITUDE: TEST_LATITUDE_WAVERTREE,
CONF_LONGITUDE: TEST_LONGITUDE_WAVERTREE,
CONF_NAME: TEST_SITE_NAME_WAVERTREE,
}
METOFFICE_CONFIG_KINGSLYNN = {
CONF_API_KEY: TEST_API_KEY,
CONF_LATITUDE: TEST_LATITUDE_KINGSLYNN,
CONF_LONGITUDE: TEST_LONGITUDE_KINGSLYNN,
CONF_NAME: TEST_SITE_NAME_KINGSLYNN,
}
KINGSLYNN_SENSOR_RESULTS = {
"weather": ("weather", "sunny"),
"visibility": ("visibility", "Very Good"),
"visibility_distance": ("visibility_distance", "20-40"),
"temperature": ("temperature", "14"),
"feels_like_temperature": ("feels_like_temperature", "13"),
"uv": ("uv_index", "6"),
"precipitation": ("probability_of_precipitation", "0"),
"wind_direction": ("wind_direction", "E"),
"wind_gust": ("wind_gust", "7"),
"wind_speed": ("wind_speed", "2"),
"humidity": ("humidity", "60"),
}
WAVERTREE_SENSOR_RESULTS = {
"weather": ("weather", "sunny"),
"visibility": ("visibility", "Good"),
"visibility_distance": ("visibility_distance", "10-20"),
"temperature": ("temperature", "17"),
"feels_like_temperature": ("feels_like_temperature", "14"),
"uv": ("uv_index", "5"),
"precipitation": ("probability_of_precipitation", "0"),
"wind_direction": ("wind_direction", "SSE"),
"wind_gust": ("wind_gust", "16"),
"wind_speed": ("wind_speed", "9"),
"humidity": ("humidity", "50"),
}

View file

@ -0,0 +1,122 @@
"""Test the National Weather Service (NWS) config flow."""
import json
from homeassistant import config_entries, setup
from homeassistant.components.metoffice.const import DOMAIN
from .const import (
METOFFICE_CONFIG_WAVERTREE,
TEST_API_KEY,
TEST_LATITUDE_WAVERTREE,
TEST_LONGITUDE_WAVERTREE,
TEST_SITE_NAME_WAVERTREE,
)
from tests.async_mock import patch
from tests.common import MockConfigEntry, load_fixture
async def test_form(hass, requests_mock):
"""Test we get the form."""
hass.config.latitude = TEST_LATITUDE_WAVERTREE
hass.config.longitude = TEST_LONGITUDE_WAVERTREE
# all metoffice test data encapsulated in here
mock_json = json.loads(load_fixture("metoffice.json"))
all_sites = json.dumps(mock_json["all_sites"])
requests_mock.get("/public/data/val/wxfcs/all/json/sitelist/", text=all_sites)
await setup.async_setup_component(hass, "persistent_notification", {})
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == "form"
assert result["errors"] == {}
with patch(
"homeassistant.components.metoffice.async_setup", return_value=True
) as mock_setup, patch(
"homeassistant.components.metoffice.async_setup_entry", return_value=True,
) as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], {"api_key": TEST_API_KEY}
)
assert result2["type"] == "create_entry"
assert result2["title"] == TEST_SITE_NAME_WAVERTREE
assert result2["data"] == {
"api_key": TEST_API_KEY,
"latitude": TEST_LATITUDE_WAVERTREE,
"longitude": TEST_LONGITUDE_WAVERTREE,
"name": TEST_SITE_NAME_WAVERTREE,
}
await hass.async_block_till_done()
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
async def test_form_already_configured(hass, requests_mock):
"""Test we handle duplicate entries."""
hass.config.latitude = TEST_LATITUDE_WAVERTREE
hass.config.longitude = TEST_LONGITUDE_WAVERTREE
# all metoffice test data encapsulated in here
mock_json = json.loads(load_fixture("metoffice.json"))
all_sites = json.dumps(mock_json["all_sites"])
requests_mock.get("/public/data/val/wxfcs/all/json/sitelist/", text=all_sites)
requests_mock.get(
"/public/data/val/wxfcs/all/json/354107?res=3hourly", text="",
)
MockConfigEntry(
domain=DOMAIN,
unique_id=f"{TEST_LATITUDE_WAVERTREE}_{TEST_LONGITUDE_WAVERTREE}",
data=METOFFICE_CONFIG_WAVERTREE,
).add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_USER},
data=METOFFICE_CONFIG_WAVERTREE,
)
assert result["type"] == "abort"
assert result["reason"] == "already_configured"
async def test_form_cannot_connect(hass, requests_mock):
"""Test we handle cannot connect error."""
hass.config.latitude = TEST_LATITUDE_WAVERTREE
hass.config.longitude = TEST_LONGITUDE_WAVERTREE
requests_mock.get("/public/data/val/wxfcs/all/json/sitelist/", text="")
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], {"api_key": TEST_API_KEY},
)
assert result2["type"] == "form"
assert result2["errors"] == {"base": "cannot_connect"}
async def test_form_unknown_error(hass, mock_simple_manager_fail):
"""Test we handle unknown error."""
mock_instance = mock_simple_manager_fail.return_value
mock_instance.get_nearest_forecast_site.side_effect = ValueError
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], {"api_key": TEST_API_KEY},
)
assert result2["type"] == "form"
assert result2["errors"] == {"base": "unknown"}

View file

@ -0,0 +1,117 @@
"""The tests for the Met Office sensor component."""
from datetime import datetime, timezone
import json
from homeassistant.components.metoffice.const import ATTRIBUTION, DOMAIN
from .const import (
DATETIME_FORMAT,
KINGSLYNN_SENSOR_RESULTS,
METOFFICE_CONFIG_KINGSLYNN,
METOFFICE_CONFIG_WAVERTREE,
TEST_DATETIME_STRING,
TEST_SITE_NAME_KINGSLYNN,
TEST_SITE_NAME_WAVERTREE,
WAVERTREE_SENSOR_RESULTS,
)
from tests.async_mock import Mock, patch
from tests.common import MockConfigEntry, load_fixture
@patch(
"datapoint.Forecast.datetime.datetime",
Mock(now=Mock(return_value=datetime(2020, 4, 25, 12, tzinfo=timezone.utc))),
)
async def test_one_sensor_site_running(hass, requests_mock):
"""Test the Met Office sensor platform."""
# all metoffice test data encapsulated in here
mock_json = json.loads(load_fixture("metoffice.json"))
all_sites = json.dumps(mock_json["all_sites"])
wavertree_hourly = json.dumps(mock_json["wavertree_hourly"])
requests_mock.get("/public/data/val/wxfcs/all/json/sitelist/", text=all_sites)
requests_mock.get(
"/public/data/val/wxfcs/all/json/354107?res=3hourly", text=wavertree_hourly,
)
entry = MockConfigEntry(domain=DOMAIN, data=METOFFICE_CONFIG_WAVERTREE,)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
running_sensor_ids = hass.states.async_entity_ids("sensor")
assert len(running_sensor_ids) > 0
for running_id in running_sensor_ids:
sensor = hass.states.get(running_id)
sensor_id = sensor.attributes.get("sensor_id")
sensor_name, sensor_value = WAVERTREE_SENSOR_RESULTS[sensor_id]
assert sensor.state == sensor_value
assert (
sensor.attributes.get("last_update").strftime(DATETIME_FORMAT)
== TEST_DATETIME_STRING
)
assert sensor.attributes.get("site_id") == "354107"
assert sensor.attributes.get("site_name") == TEST_SITE_NAME_WAVERTREE
assert sensor.attributes.get("attribution") == ATTRIBUTION
@patch(
"datapoint.Forecast.datetime.datetime",
Mock(now=Mock(return_value=datetime(2020, 4, 25, 12, tzinfo=timezone.utc))),
)
async def test_two_sensor_sites_running(hass, requests_mock):
"""Test we handle two sets of sensors running for two different sites."""
# all metoffice test data encapsulated in here
mock_json = json.loads(load_fixture("metoffice.json"))
all_sites = json.dumps(mock_json["all_sites"])
wavertree_hourly = json.dumps(mock_json["wavertree_hourly"])
kingslynn_hourly = json.dumps(mock_json["kingslynn_hourly"])
requests_mock.get("/public/data/val/wxfcs/all/json/sitelist/", text=all_sites)
requests_mock.get(
"/public/data/val/wxfcs/all/json/354107?res=3hourly", text=wavertree_hourly
)
requests_mock.get(
"/public/data/val/wxfcs/all/json/322380?res=3hourly", text=kingslynn_hourly
)
entry = MockConfigEntry(domain=DOMAIN, data=METOFFICE_CONFIG_WAVERTREE,)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
entry2 = MockConfigEntry(domain=DOMAIN, data=METOFFICE_CONFIG_KINGSLYNN,)
entry2.add_to_hass(hass)
await hass.config_entries.async_setup(entry2.entry_id)
await hass.async_block_till_done()
running_sensor_ids = hass.states.async_entity_ids("sensor")
assert len(running_sensor_ids) > 0
for running_id in running_sensor_ids:
sensor = hass.states.get(running_id)
sensor_id = sensor.attributes.get("sensor_id")
if sensor.attributes.get("site_id") == "354107":
sensor_name, sensor_value = WAVERTREE_SENSOR_RESULTS[sensor_id]
assert sensor.state == sensor_value
assert (
sensor.attributes.get("last_update").strftime(DATETIME_FORMAT)
== TEST_DATETIME_STRING
)
assert sensor.attributes.get("sensor_id") == sensor_id
assert sensor.attributes.get("site_id") == "354107"
assert sensor.attributes.get("site_name") == TEST_SITE_NAME_WAVERTREE
assert sensor.attributes.get("attribution") == ATTRIBUTION
else:
sensor_name, sensor_value = KINGSLYNN_SENSOR_RESULTS[sensor_id]
assert sensor.state == sensor_value
assert (
sensor.attributes.get("last_update").strftime(DATETIME_FORMAT)
== TEST_DATETIME_STRING
)
assert sensor.attributes.get("sensor_id") == sensor_id
assert sensor.attributes.get("site_id") == "322380"
assert sensor.attributes.get("site_name") == TEST_SITE_NAME_KINGSLYNN
assert sensor.attributes.get("attribution") == ATTRIBUTION

View file

@ -0,0 +1,159 @@
"""The tests for the Met Office sensor component."""
from datetime import datetime, timedelta, timezone
import json
from homeassistant.components.metoffice.const import DOMAIN
from homeassistant.const import STATE_UNAVAILABLE
from homeassistant.util import utcnow
from .const import (
METOFFICE_CONFIG_KINGSLYNN,
METOFFICE_CONFIG_WAVERTREE,
WAVERTREE_SENSOR_RESULTS,
)
from tests.async_mock import Mock, patch
from tests.common import MockConfigEntry, async_fire_time_changed, load_fixture
@patch(
"datapoint.Forecast.datetime.datetime",
Mock(now=Mock(return_value=datetime(2020, 4, 25, 12, tzinfo=timezone.utc))),
)
async def test_site_cannot_connect(hass, requests_mock):
"""Test we handle cannot connect error."""
requests_mock.get("/public/data/val/wxfcs/all/json/sitelist/", text="")
requests_mock.get("/public/data/val/wxfcs/all/json/354107?res=3hourly", text="")
entry = MockConfigEntry(domain=DOMAIN, data=METOFFICE_CONFIG_WAVERTREE,)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert hass.states.get("weather.met_office_wavertree") is None
for sensor_id in WAVERTREE_SENSOR_RESULTS:
sensor_name, sensor_value = WAVERTREE_SENSOR_RESULTS[sensor_id]
sensor = hass.states.get(f"sensor.wavertree_{sensor_name}")
assert sensor is None
@patch(
"datapoint.Forecast.datetime.datetime",
Mock(now=Mock(return_value=datetime(2020, 4, 25, 12, tzinfo=timezone.utc))),
)
async def test_site_cannot_update(hass, requests_mock):
"""Test we handle cannot connect error."""
# all metoffice test data encapsulated in here
mock_json = json.loads(load_fixture("metoffice.json"))
all_sites = json.dumps(mock_json["all_sites"])
wavertree_hourly = json.dumps(mock_json["wavertree_hourly"])
requests_mock.get("/public/data/val/wxfcs/all/json/sitelist/", text=all_sites)
requests_mock.get(
"/public/data/val/wxfcs/all/json/354107?res=3hourly", text=wavertree_hourly
)
entry = MockConfigEntry(domain=DOMAIN, data=METOFFICE_CONFIG_WAVERTREE,)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
entity = hass.states.get("weather.met_office_wavertree")
assert entity
requests_mock.get("/public/data/val/wxfcs/all/json/354107?res=3hourly", text="")
future_time = utcnow() + timedelta(minutes=20)
async_fire_time_changed(hass, future_time)
await hass.async_block_till_done()
entity = hass.states.get("weather.met_office_wavertree")
assert entity.state == STATE_UNAVAILABLE
@patch(
"datapoint.Forecast.datetime.datetime",
Mock(now=Mock(return_value=datetime(2020, 4, 25, 12, tzinfo=timezone.utc))),
)
async def test_one_weather_site_running(hass, requests_mock):
"""Test the Met Office weather platform."""
# all metoffice test data encapsulated in here
mock_json = json.loads(load_fixture("metoffice.json"))
all_sites = json.dumps(mock_json["all_sites"])
wavertree_hourly = json.dumps(mock_json["wavertree_hourly"])
requests_mock.get("/public/data/val/wxfcs/all/json/sitelist/", text=all_sites)
requests_mock.get(
"/public/data/val/wxfcs/all/json/354107?res=3hourly", text=wavertree_hourly,
)
entry = MockConfigEntry(domain=DOMAIN, data=METOFFICE_CONFIG_WAVERTREE,)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
# Wavertree weather platform expected results
entity = hass.states.get("weather.met_office_wavertree")
assert entity
assert entity.state == "sunny"
assert entity.attributes.get("temperature") == 17
assert entity.attributes.get("wind_speed") == 9
assert entity.attributes.get("wind_bearing") == "SSE"
assert entity.attributes.get("visibility") == "Good - 10-20"
assert entity.attributes.get("humidity") == 50
@patch(
"datapoint.Forecast.datetime.datetime",
Mock(now=Mock(return_value=datetime(2020, 4, 25, 12, tzinfo=timezone.utc))),
)
async def test_two_weather_sites_running(hass, requests_mock):
"""Test we handle two different weather sites both running."""
# all metoffice test data encapsulated in here
mock_json = json.loads(load_fixture("metoffice.json"))
all_sites = json.dumps(mock_json["all_sites"])
wavertree_hourly = json.dumps(mock_json["wavertree_hourly"])
kingslynn_hourly = json.dumps(mock_json["kingslynn_hourly"])
requests_mock.get("/public/data/val/wxfcs/all/json/sitelist/", text=all_sites)
requests_mock.get(
"/public/data/val/wxfcs/all/json/354107?res=3hourly", text=wavertree_hourly
)
requests_mock.get(
"/public/data/val/wxfcs/all/json/322380?res=3hourly", text=kingslynn_hourly
)
entry = MockConfigEntry(domain=DOMAIN, data=METOFFICE_CONFIG_WAVERTREE,)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
entry2 = MockConfigEntry(domain=DOMAIN, data=METOFFICE_CONFIG_KINGSLYNN,)
entry2.add_to_hass(hass)
await hass.config_entries.async_setup(entry2.entry_id)
await hass.async_block_till_done()
# Wavertree weather platform expected results
entity = hass.states.get("weather.met_office_wavertree")
assert entity
assert entity.state == "sunny"
assert entity.attributes.get("temperature") == 17
assert entity.attributes.get("wind_speed") == 9
assert entity.attributes.get("wind_bearing") == "SSE"
assert entity.attributes.get("visibility") == "Good - 10-20"
assert entity.attributes.get("humidity") == 50
# King's Lynn weather platform expected results
entity = hass.states.get("weather.met_office_king_s_lynn")
assert entity
assert entity.state == "sunny"
assert entity.attributes.get("temperature") == 14
assert entity.attributes.get("wind_speed") == 2
assert entity.attributes.get("wind_bearing") == "E"
assert entity.attributes.get("visibility") == "Very Good - 20-40"
assert entity.attributes.get("humidity") == 60

1499
tests/fixtures/metoffice.json vendored Normal file

File diff suppressed because it is too large Load diff