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/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
|
||||||
|
|
|
@ -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",
|
"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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
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."""
|
"""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
|
||||||
|
|
|
@ -94,6 +94,7 @@ FLOWS = [
|
||||||
"melcloud",
|
"melcloud",
|
||||||
"met",
|
"met",
|
||||||
"meteo_france",
|
"meteo_france",
|
||||||
|
"metoffice",
|
||||||
"mikrotik",
|
"mikrotik",
|
||||||
"mill",
|
"mill",
|
||||||
"minecraft_server",
|
"minecraft_server",
|
||||||
|
|
|
@ -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
|
||||||
|
|
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