From 121d9677323c3bb31a168fabf2053ea70188f970 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 2 Mar 2020 13:40:11 -0800 Subject: [PATCH 1/2] Add coronavirus integration (#32413) * Add coronavirus integration * Update homeassistant/components/coronavirus/manifest.json Co-Authored-By: Franck Nijhof Co-authored-by: Franck Nijhof --- CODEOWNERS | 1 + .../coronavirus/.translations/en.json | 13 ++++ .../components/coronavirus/__init__.py | 75 +++++++++++++++++++ .../components/coronavirus/config_flow.py | 41 ++++++++++ homeassistant/components/coronavirus/const.py | 6 ++ .../components/coronavirus/manifest.json | 12 +++ .../components/coronavirus/sensor.py | 69 +++++++++++++++++ .../components/coronavirus/strings.json | 13 ++++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/coronavirus/__init__.py | 1 + .../coronavirus/test_config_flow.py | 33 ++++++++ 13 files changed, 271 insertions(+) create mode 100644 homeassistant/components/coronavirus/.translations/en.json create mode 100644 homeassistant/components/coronavirus/__init__.py create mode 100644 homeassistant/components/coronavirus/config_flow.py create mode 100644 homeassistant/components/coronavirus/const.py create mode 100644 homeassistant/components/coronavirus/manifest.json create mode 100644 homeassistant/components/coronavirus/sensor.py create mode 100644 homeassistant/components/coronavirus/strings.json create mode 100644 tests/components/coronavirus/__init__.py create mode 100644 tests/components/coronavirus/test_config_flow.py diff --git a/CODEOWNERS b/CODEOWNERS index a8057197827..e3c0120d816 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -68,6 +68,7 @@ homeassistant/components/config/* @home-assistant/core homeassistant/components/configurator/* @home-assistant/core homeassistant/components/conversation/* @home-assistant/core homeassistant/components/coolmaster/* @OnFreund +homeassistant/components/coronavirus/* @home_assistant/core homeassistant/components/counter/* @fabaff homeassistant/components/cover/* @home-assistant/core homeassistant/components/cpuspeed/* @fabaff diff --git a/homeassistant/components/coronavirus/.translations/en.json b/homeassistant/components/coronavirus/.translations/en.json new file mode 100644 index 00000000000..ad7a3cf2cdf --- /dev/null +++ b/homeassistant/components/coronavirus/.translations/en.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "country": "Country" + }, + "title": "Pick a country to monitor" + } + }, + "title": "Coronavirus" + } +} \ No newline at end of file diff --git a/homeassistant/components/coronavirus/__init__.py b/homeassistant/components/coronavirus/__init__.py new file mode 100644 index 00000000000..95c3cd1c024 --- /dev/null +++ b/homeassistant/components/coronavirus/__init__.py @@ -0,0 +1,75 @@ +"""The Coronavirus integration.""" +import asyncio +from datetime import timedelta +import logging + +import aiohttp +import async_timeout +import coronavirus + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers import aiohttp_client, update_coordinator + +from .const import DOMAIN + +PLATFORMS = ["sensor"] + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the Coronavirus component.""" + # Make sure coordinator is initialized. + await get_coordinator(hass) + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Coronavirus from a config entry.""" + 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 + ] + ) + ) + + return unload_ok + + +async def get_coordinator(hass): + """Get the data update coordinator.""" + if DOMAIN in hass.data: + return hass.data[DOMAIN] + + async def async_get_cases(): + try: + with async_timeout.timeout(10): + return { + case.id: case + for case in await coronavirus.get_cases( + aiohttp_client.async_get_clientsession(hass) + ) + } + except (asyncio.TimeoutError, aiohttp.ClientError): + raise update_coordinator.UpdateFailed + + hass.data[DOMAIN] = update_coordinator.DataUpdateCoordinator( + hass, + logging.getLogger(__name__), + name=DOMAIN, + update_method=async_get_cases, + update_interval=timedelta(hours=1), + ) + await hass.data[DOMAIN].async_refresh() + return hass.data[DOMAIN] diff --git a/homeassistant/components/coronavirus/config_flow.py b/homeassistant/components/coronavirus/config_flow.py new file mode 100644 index 00000000000..59d25e16709 --- /dev/null +++ b/homeassistant/components/coronavirus/config_flow.py @@ -0,0 +1,41 @@ +"""Config flow for Coronavirus integration.""" +import logging + +import voluptuous as vol + +from homeassistant import config_entries + +from . import get_coordinator +from .const import DOMAIN, OPTION_WORLDWIDE # pylint:disable=unused-import + +_LOGGER = logging.getLogger(__name__) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Coronavirus.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + _options = None + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + + if self._options is None: + self._options = {OPTION_WORLDWIDE: "Worldwide"} + coordinator = await get_coordinator(self.hass) + for case_id in sorted(coordinator.data): + self._options[case_id] = coordinator.data[case_id].country + + if user_input is not None: + return self.async_create_entry( + title=self._options[user_input["country"]], data=user_input + ) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema({vol.Required("country"): vol.In(self._options)}), + errors=errors, + ) diff --git a/homeassistant/components/coronavirus/const.py b/homeassistant/components/coronavirus/const.py new file mode 100644 index 00000000000..e1ffa64e88c --- /dev/null +++ b/homeassistant/components/coronavirus/const.py @@ -0,0 +1,6 @@ +"""Constants for the Coronavirus integration.""" +from coronavirus import DEFAULT_SOURCE + +DOMAIN = "coronavirus" +OPTION_WORLDWIDE = "__worldwide" +ATTRIBUTION = f"Data provided by {DEFAULT_SOURCE.NAME}" diff --git a/homeassistant/components/coronavirus/manifest.json b/homeassistant/components/coronavirus/manifest.json new file mode 100644 index 00000000000..d99a9b621a2 --- /dev/null +++ b/homeassistant/components/coronavirus/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "coronavirus", + "name": "Coronavirus (COVID-19)", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/coronavirus", + "requirements": ["coronavirus==1.0.1"], + "ssdp": [], + "zeroconf": [], + "homekit": {}, + "dependencies": [], + "codeowners": ["@home_assistant/core"] +} diff --git a/homeassistant/components/coronavirus/sensor.py b/homeassistant/components/coronavirus/sensor.py new file mode 100644 index 00000000000..770ab78b43e --- /dev/null +++ b/homeassistant/components/coronavirus/sensor.py @@ -0,0 +1,69 @@ +"""Sensor platform for the Corona virus.""" +from homeassistant.const import ATTR_ATTRIBUTION +from homeassistant.helpers.entity import Entity + +from . import get_coordinator +from .const import ATTRIBUTION, OPTION_WORLDWIDE + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Defer sensor setup to the shared sensor module.""" + coordinator = await get_coordinator(hass) + + async_add_entities( + CoronavirusSensor(coordinator, config_entry.data["country"], info_type) + for info_type in ("confirmed", "recovered", "deaths", "current") + ) + + +class CoronavirusSensor(Entity): + """Sensor representing corona virus data.""" + + name = None + unique_id = None + + def __init__(self, coordinator, country, info_type): + """Initialize coronavirus sensor.""" + if country == OPTION_WORLDWIDE: + self.name = f"Worldwide {info_type}" + else: + self.name = f"{coordinator.data[country].country} {info_type}" + self.unique_id = f"{country}-{info_type}" + self.coordinator = coordinator + self.country = country + self.info_type = info_type + + @property + def available(self): + """Return if sensor is available.""" + return self.coordinator.last_update_success and ( + self.country in self.coordinator.data or self.country == OPTION_WORLDWIDE + ) + + @property + def state(self): + """State of the sensor.""" + if self.country == OPTION_WORLDWIDE: + return sum( + getattr(case, self.info_type) for case in self.coordinator.data.values() + ) + + return getattr(self.coordinator.data[self.country], self.info_type) + + @property + def unit_of_measurement(self): + """Return unit of measurement.""" + return "people" + + @property + def device_state_attributes(self): + """Return device attributes.""" + return {ATTR_ATTRIBUTION: ATTRIBUTION} + + async def async_added_to_hass(self): + """When entity is added to hass.""" + self.coordinator.async_add_listener(self.async_write_ha_state) + + async def async_will_remove_from_hass(self): + """When entity will be removed from hass.""" + self.coordinator.async_remove_listener(self.async_write_ha_state) diff --git a/homeassistant/components/coronavirus/strings.json b/homeassistant/components/coronavirus/strings.json new file mode 100644 index 00000000000..13cd5f04012 --- /dev/null +++ b/homeassistant/components/coronavirus/strings.json @@ -0,0 +1,13 @@ +{ + "config": { + "title": "Coronavirus", + "step": { + "user": { + "title": "Pick a country to monitor", + "data": { + "country": "Country" + } + } + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 39a9bccf607..9173714a6f6 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -17,6 +17,7 @@ FLOWS = [ "cast", "cert_expiry", "coolmaster", + "coronavirus", "daikin", "deconz", "dialogflow", diff --git a/requirements_all.txt b/requirements_all.txt index d3f81477a80..52f06eda1bf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -398,6 +398,9 @@ connect-box==0.2.5 # homeassistant.components.xiaomi_miio construct==2.9.45 +# homeassistant.components.coronavirus +coronavirus==1.0.1 + # homeassistant.scripts.credstash # credstash==1.15.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bf9106725b5..42e71a8996e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -143,6 +143,9 @@ colorlog==4.1.0 # homeassistant.components.xiaomi_miio construct==2.9.45 +# homeassistant.components.coronavirus +coronavirus==1.0.1 + # homeassistant.scripts.credstash # credstash==1.15.0 diff --git a/tests/components/coronavirus/__init__.py b/tests/components/coronavirus/__init__.py new file mode 100644 index 00000000000..2274a51506d --- /dev/null +++ b/tests/components/coronavirus/__init__.py @@ -0,0 +1 @@ +"""Tests for the Coronavirus integration.""" diff --git a/tests/components/coronavirus/test_config_flow.py b/tests/components/coronavirus/test_config_flow.py new file mode 100644 index 00000000000..6d940d8e53d --- /dev/null +++ b/tests/components/coronavirus/test_config_flow.py @@ -0,0 +1,33 @@ +"""Test the Coronavirus config flow.""" +from asynctest import patch + +from homeassistant import config_entries, setup +from homeassistant.components.coronavirus.const import DOMAIN, OPTION_WORLDWIDE + + +async def test_form(hass): + """Test we get the form.""" + 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("coronavirus.get_cases", return_value=[],), patch( + "homeassistant.components.coronavirus.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.coronavirus.async_setup_entry", return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {"country": OPTION_WORLDWIDE}, + ) + + assert result2["type"] == "create_entry" + assert result2["title"] == "Worldwide" + assert result2["data"] == { + "country": OPTION_WORLDWIDE, + } + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 From 52809396d45a25bba8fa616fa1c960c7c28a540d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 2 Mar 2020 13:40:57 -0800 Subject: [PATCH 2/2] Bumped version to 0.106.3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 11b3c87db27..473fe1e3ace 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 106 -PATCH_VERSION = "2" +PATCH_VERSION = "3" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 0)