Convert MetOffice to use UI for configuration (#34900)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
9a867cbb75
commit
c96458c7e4
19 changed files with 2602 additions and 227 deletions
|
@ -244,6 +244,7 @@ homeassistant/components/melissa/* @kennedyshead
|
|||
homeassistant/components/met/* @danielhiversen
|
||||
homeassistant/components/meteo_france/* @victorcerutti @oncleben31 @Quentame
|
||||
homeassistant/components/meteoalarm/* @rolfberkenbosch
|
||||
homeassistant/components/metoffice/* @MrHarcombe
|
||||
homeassistant/components/miflora/* @danielhiversen @ChristianKuehnel
|
||||
homeassistant/components/mikrotik/* @engrbm87
|
||||
homeassistant/components/mill/* @danielhiversen
|
||||
|
|
|
@ -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
|
||||
|
|
79
homeassistant/components/metoffice/config_flow.py
Normal file
79
homeassistant/components/metoffice/config_flow.py
Normal 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."""
|
51
homeassistant/components/metoffice/const.py
Normal file
51
homeassistant/components/metoffice/const.py
Normal 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",
|
||||
}
|
78
homeassistant/components/metoffice/data.py
Normal file
78
homeassistant/components/metoffice/data.py
Normal 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
|
|
@ -3,5 +3,6 @@
|
|||
"name": "Met Office",
|
||||
"documentation": "https://www.home-assistant.io/integrations/metoffice",
|
||||
"requirements": ["datapoint==0.9.5"],
|
||||
"codeowners": []
|
||||
"codeowners": ["@MrHarcombe"],
|
||||
"config_flow": true
|
||||
}
|
||||
|
|
|
@ -1,27 +1,31 @@
|
|||
"""Support for UK Met Office weather service."""
|
||||
from datetime import timedelta
|
||||
|
||||
import logging
|
||||
|
||||
import datapoint as dp
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||
from homeassistant.const import (
|
||||
ATTR_ATTRIBUTION,
|
||||
CONF_API_KEY,
|
||||
CONF_LATITUDE,
|
||||
CONF_LONGITUDE,
|
||||
CONF_MONITORED_CONDITIONS,
|
||||
CONF_NAME,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
LENGTH_KILOMETERS,
|
||||
SPEED_MILES_PER_HOUR,
|
||||
TEMP_CELSIUS,
|
||||
UNIT_PERCENTAGE,
|
||||
UV_INDEX,
|
||||
)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.core import callback
|
||||
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__)
|
||||
|
||||
|
@ -30,175 +34,190 @@ ATTR_SENSOR_ID = "sensor_id"
|
|||
ATTR_SITE_ID = "site_id"
|
||||
ATTR_SITE_NAME = "site_name"
|
||||
|
||||
ATTRIBUTION = "Data provided by the Met Office"
|
||||
|
||||
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 are defined as:
|
||||
# variable -> [0]title, [1]device_class, [2]units, [3]icon, [4]enabled_by_default
|
||||
SENSOR_TYPES = {
|
||||
"name": ["Station Name", None],
|
||||
"weather": ["Weather", None],
|
||||
"temperature": ["Temperature", TEMP_CELSIUS],
|
||||
"feels_like_temperature": ["Feels Like Temperature", TEMP_CELSIUS],
|
||||
"wind_speed": ["Wind Speed", SPEED_MILES_PER_HOUR],
|
||||
"wind_direction": ["Wind Direction", None],
|
||||
"wind_gust": ["Wind Gust", SPEED_MILES_PER_HOUR],
|
||||
"visibility": ["Visibility", None],
|
||||
"visibility_distance": ["Visibility Distance", LENGTH_KILOMETERS],
|
||||
"uv": ["UV", UV_INDEX],
|
||||
"precipitation": ["Probability of Precipitation", UNIT_PERCENTAGE],
|
||||
"humidity": ["Humidity", UNIT_PERCENTAGE],
|
||||
"name": ["Station Name", None, None, "mdi:label-outline", False],
|
||||
"weather": [
|
||||
"Weather",
|
||||
None,
|
||||
None,
|
||||
"mdi:weather-sunny", # but will adapt to current conditions
|
||||
True,
|
||||
],
|
||||
"temperature": ["Temperature", DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None, True],
|
||||
"feels_like_temperature": [
|
||||
"Feels Like Temperature",
|
||||
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):
|
||||
"""Set up the Met Office sensor platform."""
|
||||
api_key = config.get(CONF_API_KEY)
|
||||
latitude = config.get(CONF_LATITUDE, hass.config.latitude)
|
||||
longitude = config.get(CONF_LONGITUDE, hass.config.longitude)
|
||||
name = config.get(CONF_NAME)
|
||||
|
||||
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)
|
||||
async_add_entities(
|
||||
[
|
||||
MetOfficeCurrentSensor(entry.data, hass_data, sensor_type)
|
||||
for sensor_type in SENSOR_TYPES
|
||||
],
|
||||
False,
|
||||
)
|
||||
|
||||
|
||||
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."""
|
||||
self._condition = condition
|
||||
self.data = data
|
||||
self._name = name
|
||||
self.site = site
|
||||
self._data = hass_data[METOFFICE_DATA]
|
||||
self._coordinator = hass_data[METOFFICE_COORDINATOR]
|
||||
|
||||
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
|
||||
def name(self):
|
||||
"""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
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
if self._condition == "visibility_distance" and hasattr(
|
||||
self.data.data, "visibility"
|
||||
value = None
|
||||
|
||||
if self._type == "visibility_distance" and hasattr(
|
||||
self.metoffice_now, "visibility"
|
||||
):
|
||||
return VISIBILITY_CLASSES.get(self.data.data.visibility.value)
|
||||
if hasattr(self.data.data, self._condition):
|
||||
variable = getattr(self.data.data, self._condition)
|
||||
if self._condition == "weather":
|
||||
return [
|
||||
k
|
||||
for k, v in CONDITION_CLASSES.items()
|
||||
if self.data.data.weather.value in v
|
||||
][0]
|
||||
return variable.value
|
||||
return None
|
||||
value = VISIBILITY_DISTANCE_CLASSES.get(self.metoffice_now.visibility.value)
|
||||
|
||||
if self._type == "visibility" and hasattr(self.metoffice_now, "visibility"):
|
||||
value = VISIBILITY_CLASSES.get(self.metoffice_now.visibility.value)
|
||||
|
||||
elif self._type == "weather" and hasattr(self.metoffice_now, self._type):
|
||||
value = [
|
||||
k
|
||||
for k, v in CONDITION_CLASSES.items()
|
||||
if self.metoffice_now.weather.value in v
|
||||
][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
|
||||
def unit_of_measurement(self):
|
||||
"""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
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes of the device."""
|
||||
attr = {}
|
||||
attr[ATTR_ATTRIBUTION] = ATTRIBUTION
|
||||
attr[ATTR_LAST_UPDATE] = self.data.data.date
|
||||
attr[ATTR_SENSOR_ID] = self._condition
|
||||
attr[ATTR_SITE_ID] = self.site.id
|
||||
attr[ATTR_SITE_NAME] = self.site.name
|
||||
return attr
|
||||
return {
|
||||
ATTR_ATTRIBUTION: ATTRIBUTION,
|
||||
ATTR_LAST_UPDATE: self.metoffice_now.date if self.metoffice_now else None,
|
||||
ATTR_SENSOR_ID: self._type,
|
||||
ATTR_SITE_ID: self.metoffice_site_id if self.metoffice_site_id else None,
|
||||
ATTR_SITE_NAME: self.metoffice_site_name
|
||||
if self.metoffice_site_name
|
||||
else None,
|
||||
}
|
||||
|
||||
def update(self):
|
||||
"""Update current conditions."""
|
||||
self.data.update()
|
||||
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()
|
||||
|
||||
async def async_update(self):
|
||||
"""Schedule a custom update via the common entity update service."""
|
||||
await self._coordinator.async_request_refresh()
|
||||
|
||||
class MetOfficeCurrentData:
|
||||
"""Get data from Datapoint."""
|
||||
@callback
|
||||
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):
|
||||
"""Initialize the data object."""
|
||||
self._datapoint = datapoint
|
||||
self._site = site
|
||||
self.data = None
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
"""Entities do not individually poll."""
|
||||
return False
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Get the latest data from Datapoint."""
|
||||
try:
|
||||
forecast = self._datapoint.get_forecast_for_site(self._site.id, "3hourly")
|
||||
self.data = forecast.now()
|
||||
except (ValueError, dp.exceptions.APIException) as err:
|
||||
_LOGGER.error("Check Met Office %s", err.args)
|
||||
self.data = None
|
||||
@property
|
||||
def entity_registry_enabled_default(self) -> bool:
|
||||
"""Return if the entity should be enabled when first added to the entity registry."""
|
||||
return SENSOR_TYPES[self._type][4]
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return if state is available."""
|
||||
return self.metoffice_site_id is not None and self.metoffice_now is not None
|
||||
|
|
22
homeassistant/components/metoffice/strings.json
Normal file
22
homeassistant/components/metoffice/strings.json
Normal 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%]"
|
||||
}
|
||||
}
|
||||
}
|
23
homeassistant/components/metoffice/translations/en.json
Normal file
23
homeassistant/components/metoffice/translations/en.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,127 +1,161 @@
|
|||
"""Support for UK Met Office weather service."""
|
||||
|
||||
import logging
|
||||
|
||||
import datapoint as dp
|
||||
import voluptuous as vol
|
||||
from homeassistant.components.weather import WeatherEntity
|
||||
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 homeassistant.const import (
|
||||
CONF_API_KEY,
|
||||
CONF_LATITUDE,
|
||||
CONF_LONGITUDE,
|
||||
CONF_NAME,
|
||||
TEMP_CELSIUS,
|
||||
from .const import (
|
||||
ATTRIBUTION,
|
||||
CONDITION_CLASSES,
|
||||
DEFAULT_NAME,
|
||||
DOMAIN,
|
||||
METOFFICE_COORDINATOR,
|
||||
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__)
|
||||
|
||||
DEFAULT_NAME = "Met Office"
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_API_KEY): cv.string,
|
||||
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):
|
||||
"""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)
|
||||
async_add_entities(
|
||||
[MetOfficeWeather(entry.data, hass_data,)], False,
|
||||
)
|
||||
|
||||
|
||||
class MetOfficeWeather(WeatherEntity):
|
||||
"""Implementation of a Met Office weather condition."""
|
||||
|
||||
def __init__(self, site, data, name):
|
||||
"""Initialise the platform with a data instance and site."""
|
||||
self._name = name
|
||||
self.data = data
|
||||
self.site = site
|
||||
def __init__(self, entry_data, hass_data):
|
||||
"""Initialise the platform with a data instance."""
|
||||
self._data = hass_data[METOFFICE_DATA]
|
||||
self._coordinator = hass_data[METOFFICE_COORDINATOR]
|
||||
|
||||
def update(self):
|
||||
"""Update current conditions."""
|
||||
self.data.update()
|
||||
self._name = f"{DEFAULT_NAME} {hass_data[METOFFICE_NAME]}"
|
||||
self._unique_id = f"{self._data.latitude}_{self._data.longitude}"
|
||||
|
||||
self.metoffice_now = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""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
|
||||
def condition(self):
|
||||
"""Return the current condition."""
|
||||
return [
|
||||
k for k, v in CONDITION_CLASSES.items() if self.data.data.weather.value in v
|
||||
][0]
|
||||
return (
|
||||
[
|
||||
k
|
||||
for k, v in CONDITION_CLASSES.items()
|
||||
if self.metoffice_now.weather.value in v
|
||||
][0]
|
||||
if self.metoffice_now
|
||||
else None
|
||||
)
|
||||
|
||||
@property
|
||||
def temperature(self):
|
||||
"""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
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
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
|
||||
def pressure(self):
|
||||
"""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
|
||||
def humidity(self):
|
||||
"""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
|
||||
def wind_speed(self):
|
||||
"""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
|
||||
def wind_bearing(self):
|
||||
"""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
|
||||
def attribution(self):
|
||||
"""Return the 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
|
||||
|
|
|
@ -94,6 +94,7 @@ FLOWS = [
|
|||
"melcloud",
|
||||
"met",
|
||||
"meteo_france",
|
||||
"metoffice",
|
||||
"mikrotik",
|
||||
"mill",
|
||||
"minecraft_server",
|
||||
|
|
|
@ -202,6 +202,9 @@ coronavirus==1.1.1
|
|||
# homeassistant.components.datadog
|
||||
datadog==0.15.0
|
||||
|
||||
# homeassistant.components.metoffice
|
||||
datapoint==0.9.5
|
||||
|
||||
# homeassistant.components.ihc
|
||||
# homeassistant.components.namecheapdns
|
||||
# homeassistant.components.ohmconnect
|
||||
|
|
1
tests/components/metoffice/__init__.py
Normal file
1
tests/components/metoffice/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
"""Tests for the metoffice component."""
|
22
tests/components/metoffice/conftest.py
Normal file
22
tests/components/metoffice/conftest.py
Normal 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
|
58
tests/components/metoffice/const.py
Normal file
58
tests/components/metoffice/const.py
Normal 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"),
|
||||
}
|
122
tests/components/metoffice/test_config_flow.py
Normal file
122
tests/components/metoffice/test_config_flow.py
Normal 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"}
|
117
tests/components/metoffice/test_sensor.py
Normal file
117
tests/components/metoffice/test_sensor.py
Normal 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
|
159
tests/components/metoffice/test_weather.py
Normal file
159
tests/components/metoffice/test_weather.py
Normal 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
1499
tests/fixtures/metoffice.json
vendored
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue