From 121d9677323c3bb31a168fabf2053ea70188f970 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 2 Mar 2020 13:40:11 -0800 Subject: [PATCH 01/21] 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 02/21] 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) From 21e0df42ac0de6caf35a7c61ff7199e6beb23ba9 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 2 Mar 2020 22:55:57 +0100 Subject: [PATCH 03/21] Update azure-pipelines-release.yml for Azure Pipelines --- azure-pipelines-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml index c98f12dfac6..34827897749 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -41,7 +41,7 @@ stages: jq curl release="$(Build.SourceBranchName)" - created_by="$(curl -s https://api.github.com/repos/home-assistant/home-assistant/releases/tags/${release} | jq --raw-output '.author.login')" + created_by="$(curl -s https://api.github.com/repos/home-assistant/core/releases/tags/${release} | jq --raw-output '.author.login')" if [[ "${created_by}" =~ ^(balloob|pvizeli|fabaff|robbiet480|bramkragten|frenck)$ ]]; then exit 0 From 649ec2fc8ecf1b64b4896fe1b406548638fae87d Mon Sep 17 00:00:00 2001 From: elmurato <1382097+elmurato@users.noreply.github.com> Date: Fri, 28 Feb 2020 22:39:46 +0100 Subject: [PATCH 04/21] Fixed TypeError with old server versions (#32329) --- homeassistant/components/minecraft_server/__init__.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/homeassistant/components/minecraft_server/__init__.py b/homeassistant/components/minecraft_server/__init__.py index 789e4d8f1b8..a025c44e33c 100644 --- a/homeassistant/components/minecraft_server/__init__.py +++ b/homeassistant/components/minecraft_server/__init__.py @@ -47,7 +47,7 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) await server.async_update() server.start_periodic_update() - # Set up platform(s). + # Set up platforms. for platform in PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(config_entry, platform) @@ -103,7 +103,6 @@ class MinecraftServer: self._mc_status = MCStatus(self.host, self.port) # Data provided by 3rd party library - self.description = None self.version = None self.protocol_version = None self.latency_time = None @@ -168,7 +167,6 @@ class MinecraftServer: ) # Got answer to request, update properties. - self.description = status_response.description["text"] self.version = status_response.version.name self.protocol_version = status_response.version.protocol self.players_online = status_response.players.online @@ -185,7 +183,6 @@ class MinecraftServer: self._last_status_request_failed = False except OSError as error: # No answer to request, set all properties to unknown. - self.description = None self.version = None self.protocol_version = None self.players_online = None From fab55b0ea2f7f2a32c1b0435416e2addac9d5b7c Mon Sep 17 00:00:00 2001 From: mezz64 <2854333+mezz64@users.noreply.github.com> Date: Sun, 1 Mar 2020 09:05:37 -0500 Subject: [PATCH 05/21] Bump pyeight to 0.1.4 (#32363) --- homeassistant/components/eight_sleep/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/eight_sleep/manifest.json b/homeassistant/components/eight_sleep/manifest.json index 6372967b42b..ac7a11eed3c 100644 --- a/homeassistant/components/eight_sleep/manifest.json +++ b/homeassistant/components/eight_sleep/manifest.json @@ -2,7 +2,7 @@ "domain": "eight_sleep", "name": "Eight Sleep", "documentation": "https://www.home-assistant.io/integrations/eight_sleep", - "requirements": ["pyeight==0.1.3"], + "requirements": ["pyeight==0.1.4"], "dependencies": [], "codeowners": ["@mezz64"] } diff --git a/requirements_all.txt b/requirements_all.txt index 52f06eda1bf..3cae53619bf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1226,7 +1226,7 @@ pyeconet==0.0.11 pyedimax==0.2.1 # homeassistant.components.eight_sleep -pyeight==0.1.3 +pyeight==0.1.4 # homeassistant.components.emby pyemby==1.6 From 08f5b49dc49b03bf96fce78f741d7a1f1d0f3626 Mon Sep 17 00:00:00 2001 From: mezz64 <2854333+mezz64@users.noreply.github.com> Date: Mon, 2 Mar 2020 21:01:39 -0500 Subject: [PATCH 06/21] Catch Eight Sleep API errors, don't round None type (#32410) * Catch API errors, don't round None type * Specify error type --- .../components/eight_sleep/sensor.py | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/eight_sleep/sensor.py b/homeassistant/components/eight_sleep/sensor.py index af6de2657ce..f0fc4b5d1d6 100644 --- a/homeassistant/components/eight_sleep/sensor.py +++ b/homeassistant/components/eight_sleep/sensor.py @@ -261,14 +261,26 @@ class EightUserSensor(EightSleepUserEntity): bed_temp = None if "current" in self._sensor_root: - state_attr[ATTR_RESP_RATE] = round(self._attr["resp_rate"], 2) - state_attr[ATTR_HEART_RATE] = round(self._attr["heart_rate"], 2) + try: + state_attr[ATTR_RESP_RATE] = round(self._attr["resp_rate"], 2) + except TypeError: + state_attr[ATTR_RESP_RATE] = None + try: + state_attr[ATTR_HEART_RATE] = round(self._attr["heart_rate"], 2) + except TypeError: + state_attr[ATTR_HEART_RATE] = None state_attr[ATTR_SLEEP_STAGE] = self._attr["stage"] state_attr[ATTR_ROOM_TEMP] = room_temp state_attr[ATTR_BED_TEMP] = bed_temp elif "last" in self._sensor_root: - state_attr[ATTR_AVG_RESP_RATE] = round(self._attr["resp_rate"], 2) - state_attr[ATTR_AVG_HEART_RATE] = round(self._attr["heart_rate"], 2) + try: + state_attr[ATTR_AVG_RESP_RATE] = round(self._attr["resp_rate"], 2) + except TypeError: + state_attr[ATTR_AVG_RESP_RATE] = None + try: + state_attr[ATTR_AVG_HEART_RATE] = round(self._attr["heart_rate"], 2) + except TypeError: + state_attr[ATTR_AVG_HEART_RATE] = None state_attr[ATTR_AVG_ROOM_TEMP] = room_temp state_attr[ATTR_AVG_BED_TEMP] = bed_temp From 815502044e4d7d6634af5651dd4dcc42c141a040 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 2 Mar 2020 17:59:32 -0800 Subject: [PATCH 07/21] Coronavirus updates (#32417) * Sort countries alphabetically * Update sensor name * Add migration to stable unique IDs * Update sensor.py --- .../components/coronavirus/__init__.py | 23 +++++++- .../components/coronavirus/config_flow.py | 6 +- .../components/coronavirus/sensor.py | 4 +- homeassistant/helpers/entity_registry.py | 20 ++++++- tests/components/coronavirus/test_init.py | 55 +++++++++++++++++++ 5 files changed, 100 insertions(+), 8 deletions(-) create mode 100644 tests/components/coronavirus/test_init.py diff --git a/homeassistant/components/coronavirus/__init__.py b/homeassistant/components/coronavirus/__init__.py index 95c3cd1c024..d5dbcd9f3f4 100644 --- a/homeassistant/components/coronavirus/__init__.py +++ b/homeassistant/components/coronavirus/__init__.py @@ -8,8 +8,8 @@ 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 homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import aiohttp_client, entity_registry, update_coordinator from .const import DOMAIN @@ -25,6 +25,23 @@ async def async_setup(hass: HomeAssistant, config: dict): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Coronavirus from a config entry.""" + if isinstance(entry.data["country"], int): + hass.config_entries.async_update_entry( + entry, data={**entry.data, "country": entry.title} + ) + + @callback + def _async_migrator(entity_entry: entity_registry.RegistryEntry): + """Migrate away from unstable ID.""" + country, info_type = entity_entry.unique_id.rsplit("-", 1) + if not country.isnumeric(): + return None + return {"new_unique_id": f"{entry.title}-{info_type}"} + + await entity_registry.async_migrate_entries( + hass, entry.entry_id, _async_migrator + ) + for component in PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, component) @@ -56,7 +73,7 @@ async def get_coordinator(hass): try: with async_timeout.timeout(10): return { - case.id: case + case.country: case for case in await coronavirus.get_cases( aiohttp_client.async_get_clientsession(hass) ) diff --git a/homeassistant/components/coronavirus/config_flow.py b/homeassistant/components/coronavirus/config_flow.py index 59d25e16709..4a313a6837f 100644 --- a/homeassistant/components/coronavirus/config_flow.py +++ b/homeassistant/components/coronavirus/config_flow.py @@ -26,8 +26,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): 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 + for case in sorted( + coordinator.data.values(), key=lambda case: case.country + ): + self._options[case.country] = case.country if user_input is not None: return self.async_create_entry( diff --git a/homeassistant/components/coronavirus/sensor.py b/homeassistant/components/coronavirus/sensor.py index 770ab78b43e..20f18896431 100644 --- a/homeassistant/components/coronavirus/sensor.py +++ b/homeassistant/components/coronavirus/sensor.py @@ -25,9 +25,9 @@ class CoronavirusSensor(Entity): def __init__(self, coordinator, country, info_type): """Initialize coronavirus sensor.""" if country == OPTION_WORLDWIDE: - self.name = f"Worldwide {info_type}" + self.name = f"Worldwide Coronavirus {info_type}" else: - self.name = f"{coordinator.data[country].country} {info_type}" + self.name = f"{coordinator.data[country].country} Coronavirus {info_type}" self.unique_id = f"{country}-{info_type}" self.coordinator = coordinator self.country = country diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 5996fb6eaf7..87383d45635 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -11,7 +11,7 @@ import asyncio from collections import OrderedDict from itertools import chain import logging -from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, cast +from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, List, Optional, cast import attr @@ -560,3 +560,21 @@ def async_setup_entity_restore( states.async_set(entry.entity_id, STATE_UNAVAILABLE, attrs) hass.bus.async_listen(EVENT_HOMEASSISTANT_START, _write_unavailable_states) + + +async def async_migrate_entries( + hass: HomeAssistantType, + config_entry_id: str, + entry_callback: Callable[[RegistryEntry], Optional[dict]], +) -> None: + """Migrator of unique IDs.""" + ent_reg = await async_get_registry(hass) + + for entry in ent_reg.entities.values(): + if entry.config_entry_id != config_entry_id: + continue + + updates = entry_callback(entry) + + if updates is not None: + ent_reg.async_update_entity(entry.entity_id, **updates) # type: ignore diff --git a/tests/components/coronavirus/test_init.py b/tests/components/coronavirus/test_init.py new file mode 100644 index 00000000000..05a14f2f296 --- /dev/null +++ b/tests/components/coronavirus/test_init.py @@ -0,0 +1,55 @@ +"""Test init of Coronavirus integration.""" +from asynctest import Mock, patch + +from homeassistant.components.coronavirus.const import DOMAIN, OPTION_WORLDWIDE +from homeassistant.helpers import entity_registry +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry, mock_registry + + +async def test_migration(hass): + """Test that we can migrate coronavirus to stable unique ID.""" + nl_entry = MockConfigEntry(domain=DOMAIN, title="Netherlands", data={"country": 34}) + nl_entry.add_to_hass(hass) + worldwide_entry = MockConfigEntry( + domain=DOMAIN, title="Worldwide", data={"country": OPTION_WORLDWIDE} + ) + worldwide_entry.add_to_hass(hass) + mock_registry( + hass, + { + "sensor.netherlands_confirmed": entity_registry.RegistryEntry( + entity_id="sensor.netherlands_confirmed", + unique_id="34-confirmed", + platform="coronavirus", + config_entry_id=nl_entry.entry_id, + ), + "sensor.worldwide_confirmed": entity_registry.RegistryEntry( + entity_id="sensor.worldwide_confirmed", + unique_id="__worldwide-confirmed", + platform="coronavirus", + config_entry_id=worldwide_entry.entry_id, + ), + }, + ) + with patch( + "coronavirus.get_cases", + return_value=[ + Mock(country="Netherlands", confirmed=10, recovered=8, deaths=1, current=1), + Mock(country="Germany", confirmed=1, recovered=0, deaths=0, current=0), + ], + ): + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + ent_reg = await entity_registry.async_get_registry(hass) + + sensor_nl = ent_reg.async_get("sensor.netherlands_confirmed") + assert sensor_nl.unique_id == "Netherlands-confirmed" + + sensor_worldwide = ent_reg.async_get("sensor.worldwide_confirmed") + assert sensor_worldwide.unique_id == "__worldwide-confirmed" + + assert hass.states.get("sensor.netherlands_confirmed").state == "10" + assert hass.states.get("sensor.worldwide_confirmed").state == "11" From d6c15d2f45da136c164e95841b75da020869bfe8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 2 Mar 2020 18:04:07 -0800 Subject: [PATCH 08/21] Bumped version to 0.106.4 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 473fe1e3ace..fd4ac8cc1a1 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 = "3" +PATCH_VERSION = "4" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 0) From bfaad973182df6aae1506547f12f949a3815045c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 2 Mar 2020 18:10:38 -0800 Subject: [PATCH 09/21] Add unique ID to coronavirus (#32423) --- homeassistant/components/coronavirus/__init__.py | 3 +++ homeassistant/components/coronavirus/config_flow.py | 2 ++ homeassistant/components/coronavirus/strings.json | 3 +++ tests/components/coronavirus/test_config_flow.py | 2 +- tests/components/coronavirus/test_init.py | 3 +++ 5 files changed, 12 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/coronavirus/__init__.py b/homeassistant/components/coronavirus/__init__.py index d5dbcd9f3f4..04976a1e4c5 100644 --- a/homeassistant/components/coronavirus/__init__.py +++ b/homeassistant/components/coronavirus/__init__.py @@ -42,6 +42,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass, entry.entry_id, _async_migrator ) + if not entry.unique_id: + hass.config_entries.async_update_entry(entry, unique_id=entry.data["country"]) + for component in PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, component) diff --git a/homeassistant/components/coronavirus/config_flow.py b/homeassistant/components/coronavirus/config_flow.py index 4a313a6837f..49183dd028e 100644 --- a/homeassistant/components/coronavirus/config_flow.py +++ b/homeassistant/components/coronavirus/config_flow.py @@ -32,6 +32,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._options[case.country] = case.country if user_input is not None: + await self.async_set_unique_id(user_input["country"]) + self._abort_if_unique_id_configured() return self.async_create_entry( title=self._options[user_input["country"]], data=user_input ) diff --git a/homeassistant/components/coronavirus/strings.json b/homeassistant/components/coronavirus/strings.json index 13cd5f04012..fd4873c808c 100644 --- a/homeassistant/components/coronavirus/strings.json +++ b/homeassistant/components/coronavirus/strings.json @@ -8,6 +8,9 @@ "country": "Country" } } + }, + "abort": { + "already_configured": "This country is already configured." } } } diff --git a/tests/components/coronavirus/test_config_flow.py b/tests/components/coronavirus/test_config_flow.py index 6d940d8e53d..ef04d0df07a 100644 --- a/tests/components/coronavirus/test_config_flow.py +++ b/tests/components/coronavirus/test_config_flow.py @@ -22,9 +22,9 @@ async def test_form(hass): 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["result"].unique_id == OPTION_WORLDWIDE assert result2["data"] == { "country": OPTION_WORLDWIDE, } diff --git a/tests/components/coronavirus/test_init.py b/tests/components/coronavirus/test_init.py index 05a14f2f296..57293635570 100644 --- a/tests/components/coronavirus/test_init.py +++ b/tests/components/coronavirus/test_init.py @@ -53,3 +53,6 @@ async def test_migration(hass): assert hass.states.get("sensor.netherlands_confirmed").state == "10" assert hass.states.get("sensor.worldwide_confirmed").state == "11" + + assert nl_entry.unique_id == "Netherlands" + assert worldwide_entry.unique_id == OPTION_WORLDWIDE From cdde5a37cdfb94db0b273009ef97e88577432288 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Tue, 3 Mar 2020 18:25:32 -0800 Subject: [PATCH 10/21] Fix too many device tracker updates in log for Tesla (#32426) * Fix Tesla too many device tracker updates in log * Empty commit to re-trigger build Co-authored-by: Franck Nijhof --- homeassistant/components/tesla/device_tracker.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/tesla/device_tracker.py b/homeassistant/components/tesla/device_tracker.py index 08e5d58ba6e..f39d8055b12 100644 --- a/homeassistant/components/tesla/device_tracker.py +++ b/homeassistant/components/tesla/device_tracker.py @@ -68,3 +68,8 @@ class TeslaDeviceEntity(TeslaDevice, TrackerEntity): def source_type(self): """Return the source type, eg gps or router, of the device.""" return SOURCE_TYPE_GPS + + @property + def force_update(self): + """All updates do not need to be written to the state machine.""" + return False From f04969cf30fafd6557847dc866ef4f3d0737cc12 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 3 Mar 2020 13:56:32 -0800 Subject: [PATCH 11/21] Filter out duplicate logbook states (#32427) --- homeassistant/components/logbook/__init__.py | 11 ++++++- tests/components/logbook/test_init.py | 33 ++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index ac45a636bf7..5d07a1fa0b5 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -199,6 +199,9 @@ def humanify(hass, events): """ domain_prefixes = tuple(f"{dom}." for dom in CONTINUOUS_DOMAINS) + # Track last states to filter out duplicates + last_state = {} + # Group events in batches of GROUP_BY_MINUTES for _, g_events in groupby( events, lambda event: event.time_fired.minute // GROUP_BY_MINUTES @@ -236,9 +239,15 @@ def humanify(hass, events): # Yield entries for event in events_batch: if event.event_type == EVENT_STATE_CHANGED: - to_state = State.from_dict(event.data.get("new_state")) + # Filter out states that become same state again (force_update=True) + # or light becoming different color + if last_state.get(to_state.entity_id) == to_state.state: + continue + + last_state[to_state.entity_id] = to_state.state + domain = to_state.domain # Skip all but the last sensor state diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index 70e769a54f2..750ad17b523 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -1484,3 +1484,36 @@ async def test_humanify_script_started_event(hass): assert event2["domain"] == "script" assert event2["message"] == "started" assert event2["entity_id"] == "script.bye" + + +async def test_humanify_same_state(hass): + """Test humanifying Script Run event.""" + state_50 = ha.State("light.kitchen", "on", {"brightness": 50}).as_dict() + state_100 = ha.State("light.kitchen", "on", {"brightness": 100}).as_dict() + state_200 = ha.State("light.kitchen", "on", {"brightness": 200}).as_dict() + + events = list( + logbook.humanify( + hass, + [ + ha.Event( + EVENT_STATE_CHANGED, + { + "entity_id": "light.kitchen", + "old_state": state_50, + "new_state": state_100, + }, + ), + ha.Event( + EVENT_STATE_CHANGED, + { + "entity_id": "light.kitchen", + "old_state": state_100, + "new_state": state_200, + }, + ), + ], + ) + ) + + assert len(events) == 1 From 91b10e875faac7a114106e6ef8ddc267231c34a6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Mar 2020 11:25:50 -0600 Subject: [PATCH 12/21] =?UTF-8?q?Properly=20define=20dependency=20for=20pv?= =?UTF-8?q?output=20integration=20on=20rest=20in=E2=80=A6=20(#32435)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- homeassistant/components/pvoutput/manifest.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/pvoutput/manifest.json b/homeassistant/components/pvoutput/manifest.json index 1cc1f7aa2f6..0ca7af3485d 100644 --- a/homeassistant/components/pvoutput/manifest.json +++ b/homeassistant/components/pvoutput/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/pvoutput", "requirements": [], "dependencies": [], + "after_dependencies": ["rest"], "codeowners": ["@fabaff"] } From a0390783bb146a051d0d3403017e4819d256d573 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Wed, 4 Mar 2020 02:36:28 +0100 Subject: [PATCH 13/21] Fix pushover's ATTR_RETRY env variable typo (#32440) --- homeassistant/components/pushover/notify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/pushover/notify.py b/homeassistant/components/pushover/notify.py index bc44cbeddb7..01d4d8fddde 100644 --- a/homeassistant/components/pushover/notify.py +++ b/homeassistant/components/pushover/notify.py @@ -61,7 +61,7 @@ class PushoverNotificationService(BaseNotificationService): url = data.get(ATTR_URL, None) url_title = data.get(ATTR_URL_TITLE, None) priority = data.get(ATTR_PRIORITY, None) - retry = data.get(ATTR_PRIORITY, None) + retry = data.get(ATTR_RETRY, None) expire = data.get(ATTR_EXPIRE, None) callback_url = data.get(ATTR_CALLBACK_URL, None) timestamp = data.get(ATTR_TIMESTAMP, None) From a150d6dcf3184d2347f041b5f7eb03e8dbdd1b9a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 3 Mar 2020 13:57:09 -0800 Subject: [PATCH 14/21] Remove hassfest blacklisted rest (#32441) * Remove blacklisted deps from hassfest deps * Whitelist all internal integrations --- .../dwd_weather_warnings/manifest.json | 1 + .../components/emulated_hue/manifest.json | 1 + script/hassfest/dependencies.py | 62 ++++++++++++------- 3 files changed, 41 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/dwd_weather_warnings/manifest.json b/homeassistant/components/dwd_weather_warnings/manifest.json index 19dcf2860d7..0a9f972c84e 100644 --- a/homeassistant/components/dwd_weather_warnings/manifest.json +++ b/homeassistant/components/dwd_weather_warnings/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/dwd_weather_warnings", "requirements": [], "dependencies": [], + "after_dependencies": ["rest"], "codeowners": [] } diff --git a/homeassistant/components/emulated_hue/manifest.json b/homeassistant/components/emulated_hue/manifest.json index c3c0302dbc3..37848e6f306 100644 --- a/homeassistant/components/emulated_hue/manifest.json +++ b/homeassistant/components/emulated_hue/manifest.json @@ -4,6 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/emulated_hue", "requirements": ["aiohttp_cors==0.7.0"], "dependencies": [], + "after_dependencies": ["http"], "codeowners": [], "quality_scale": "internal" } diff --git a/script/hassfest/dependencies.py b/script/hassfest/dependencies.py index c909b6216a9..934400533e1 100644 --- a/script/hassfest/dependencies.py +++ b/script/hassfest/dependencies.py @@ -65,7 +65,7 @@ class ImportCollector(ast.NodeVisitor): # self.hass.components.hue.async_create() # Name(id=self) - # .Attribute(attr=hass) + # .Attribute(attr=hass) or .Attribute(attr=_hass) # .Attribute(attr=hue) # .Attribute(attr=async_create) if ( @@ -78,7 +78,7 @@ class ImportCollector(ast.NodeVisitor): ) or ( isinstance(node.value.value, ast.Attribute) - and node.value.value.attr == "hass" + and node.value.value.attr in ("hass", "_hass") ) ) ): @@ -89,20 +89,47 @@ class ImportCollector(ast.NodeVisitor): ALLOWED_USED_COMPONENTS = { - # This component will always be set up - "persistent_notification", - # These allow to register things without being set up - "conversation", - "frontend", - "hassio", - "system_health", - "websocket_api", + # Internal integrations + "alert", "automation", + "conversation", "device_automation", - "zone", + "frontend", + "group", + "hassio", "homeassistant", - "system_log", + "input_boolean", + "input_datetime", + "input_number", + "input_select", + "input_text", + "persistent_notification", "person", + "script", + "shopping_list", + "sun", + "system_health", + "system_log", + "timer", + "webhook", + "websocket_api", + "zone", + # Entity integrations with platforms + "alarm_control_panel", + "binary_sensor", + "climate", + "cover", + "device_tracker", + "fan", + "image_processing", + "light", + "lock", + "media_player", + "scene", + "sensor", + "switch", + "vacuum", + "water_heater", # Other "mjpeg", # base class, has no reqs or component to load. "stream", # Stream cannot install on all systems, can be imported without reqs. @@ -121,18 +148,7 @@ IGNORE_VIOLATIONS = { # This should become a helper method that integrations can submit data to ("websocket_api", "lovelace"), ("websocket_api", "shopping_list"), - # Expose HA to external systems - "homekit", - "alexa", - "google_assistant", - "emulated_hue", - "prometheus", - "conversation", "logbook", - "mobile_app", - # These should be extracted to external package - "pvoutput", - "dwd_weather_warnings", } From 6d0684431842e971b1641c7c36bd506f2d83b325 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 4 Mar 2020 02:32:13 +0100 Subject: [PATCH 15/21] UniFi - Fix websocket bug (#32449) --- homeassistant/components/unifi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index a42b136e665..85633ebf131 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifi", "requirements": [ - "aiounifi==13" + "aiounifi==14" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 3cae53619bf..111450d0193 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -199,7 +199,7 @@ aiopylgtv==0.3.3 aioswitcher==2019.4.26 # homeassistant.components.unifi -aiounifi==13 +aiounifi==14 # homeassistant.components.wwlln aiowwlln==2.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 42e71a8996e..26fd8eccdb2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -78,7 +78,7 @@ aiopylgtv==0.3.3 aioswitcher==2019.4.26 # homeassistant.components.unifi -aiounifi==13 +aiounifi==14 # homeassistant.components.wwlln aiowwlln==2.0.2 From 4080d6a822b3474393e695daaa462c49cf7f4cc8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 3 Mar 2020 18:26:19 -0800 Subject: [PATCH 16/21] Bumped version to 0.106.5 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index fd4ac8cc1a1..aca7adc28a5 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 = "4" +PATCH_VERSION = "5" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 0) From 606285466623c76a5186060b0b01e30275eb907c Mon Sep 17 00:00:00 2001 From: Quentame Date: Thu, 5 Mar 2020 08:50:39 +0100 Subject: [PATCH 17/21] Get pending iCloud devices when available + request again when needs an update (#32400) * Fetch iCloud devices again if the status is pending * Remove "No iCloud device found" double check * fix default api_devices value * Remove useless unitialisation declarations --- homeassistant/components/icloud/__init__.py | 4 +- homeassistant/components/icloud/account.py | 63 +++++++++++++++---- homeassistant/components/icloud/const.py | 1 - .../components/icloud/device_tracker.py | 45 +++++++++---- homeassistant/components/icloud/sensor.py | 48 ++++++++++---- 5 files changed, 119 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/icloud/__init__.py b/homeassistant/components/icloud/__init__.py index 687c6bf93de..1131a4eecc9 100644 --- a/homeassistant/components/icloud/__init__.py +++ b/homeassistant/components/icloud/__init__.py @@ -123,10 +123,8 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool hass, username, password, icloud_dir, max_interval, gps_accuracy_threshold, ) await hass.async_add_executor_job(account.setup) - if not account.devices: - return False - hass.data[DOMAIN][username] = account + hass.data[DOMAIN][entry.unique_id] = account for platform in PLATFORMS: hass.async_create_task( diff --git a/homeassistant/components/icloud/account.py b/homeassistant/components/icloud/account.py index 5d681539668..789ae563482 100644 --- a/homeassistant/components/icloud/account.py +++ b/homeassistant/components/icloud/account.py @@ -10,6 +10,7 @@ from pyicloud.services.findmyiphone import AppleDevice from homeassistant.components.zone import async_active_zone from homeassistant.const import ATTR_ATTRIBUTION +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.event import track_point_in_utc_time from homeassistant.helpers.storage import Store @@ -37,7 +38,7 @@ from .const import ( DEVICE_STATUS, DEVICE_STATUS_CODES, DEVICE_STATUS_SET, - SERVICE_UPDATE, + DOMAIN, ) ATTRIBUTION = "Data provided by Apple iCloud" @@ -91,7 +92,7 @@ class IcloudAccount: self._family_members_fullname = {} self._devices = {} - self.unsub_device_tracker = None + self.listeners = [] def setup(self) -> None: """Set up an iCloud account.""" @@ -104,13 +105,17 @@ class IcloudAccount: _LOGGER.error("Error logging into iCloud Service: %s", error) return - user_info = None try: + api_devices = self.api.devices # Gets device owners infos - user_info = self.api.devices.response["userInfo"] - except PyiCloudNoDevicesException: + user_info = api_devices.response["userInfo"] + except (KeyError, PyiCloudNoDevicesException): _LOGGER.error("No iCloud device found") - return + raise ConfigEntryNotReady + + if DEVICE_STATUS_CODES.get(list(api_devices)[0][DEVICE_STATUS]) == "pending": + _LOGGER.warning("Pending devices, trying again ...") + raise ConfigEntryNotReady self._owner_fullname = f"{user_info['firstName']} {user_info['lastName']}" @@ -132,13 +137,21 @@ class IcloudAccount: api_devices = {} try: api_devices = self.api.devices - except PyiCloudNoDevicesException: - _LOGGER.error("No iCloud device found") - return except Exception as err: # pylint: disable=broad-except _LOGGER.error("Unknown iCloud error: %s", err) - self._fetch_interval = 5 - dispatcher_send(self.hass, SERVICE_UPDATE) + self._fetch_interval = 2 + dispatcher_send(self.hass, self.signal_device_update) + track_point_in_utc_time( + self.hass, + self.keep_alive, + utcnow() + timedelta(minutes=self._fetch_interval), + ) + return + + if DEVICE_STATUS_CODES.get(list(api_devices)[0][DEVICE_STATUS]) == "pending": + _LOGGER.warning("Pending devices, trying again in 15s") + self._fetch_interval = 0.25 + dispatcher_send(self.hass, self.signal_device_update) track_point_in_utc_time( self.hass, self.keep_alive, @@ -147,10 +160,19 @@ class IcloudAccount: return # Gets devices infos + new_device = False for device in api_devices: status = device.status(DEVICE_STATUS_SET) device_id = status[DEVICE_ID] device_name = status[DEVICE_NAME] + device_status = DEVICE_STATUS_CODES.get(status[DEVICE_STATUS], "error") + + if ( + device_status == "pending" + or status[DEVICE_BATTERY_STATUS] == "Unknown" + or status.get(DEVICE_BATTERY_LEVEL) is None + ): + continue if self._devices.get(device_id, None) is not None: # Seen device -> updating @@ -165,9 +187,14 @@ class IcloudAccount: ) self._devices[device_id] = IcloudDevice(self, device, status) self._devices[device_id].update(status) + new_device = True self._fetch_interval = self._determine_interval() - dispatcher_send(self.hass, SERVICE_UPDATE) + + dispatcher_send(self.hass, self.signal_device_update) + if new_device: + dispatcher_send(self.hass, self.signal_device_new) + track_point_in_utc_time( self.hass, self.keep_alive, @@ -291,6 +318,16 @@ class IcloudAccount: """Return the account devices.""" return self._devices + @property + def signal_device_new(self) -> str: + """Event specific per Freebox entry to signal new device.""" + return f"{DOMAIN}-{self._username}-device-new" + + @property + def signal_device_update(self) -> str: + """Event specific per Freebox entry to signal updates in devices.""" + return f"{DOMAIN}-{self._username}-device-update" + class IcloudDevice: """Representation of a iCloud device.""" @@ -348,6 +385,8 @@ class IcloudDevice: and self._status[DEVICE_LOCATION][DEVICE_LOCATION_LATITUDE] ): location = self._status[DEVICE_LOCATION] + if self._location is None: + dispatcher_send(self._account.hass, self._account.signal_device_new) self._location = location def play_sound(self) -> None: diff --git a/homeassistant/components/icloud/const.py b/homeassistant/components/icloud/const.py index 3349615ed57..14bd4e498bd 100644 --- a/homeassistant/components/icloud/const.py +++ b/homeassistant/components/icloud/const.py @@ -1,7 +1,6 @@ """iCloud component constants.""" DOMAIN = "icloud" -SERVICE_UPDATE = f"{DOMAIN}_update" CONF_MAX_INTERVAL = "max_interval" CONF_GPS_ACCURACY_THRESHOLD = "gps_accuracy_threshold" diff --git a/homeassistant/components/icloud/device_tracker.py b/homeassistant/components/icloud/device_tracker.py index 00f35fbee85..f1c27d3f79e 100644 --- a/homeassistant/components/icloud/device_tracker.py +++ b/homeassistant/components/icloud/device_tracker.py @@ -5,17 +5,16 @@ from typing import Dict from homeassistant.components.device_tracker import SOURCE_TYPE_GPS from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_USERNAME +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import HomeAssistantType -from .account import IcloudDevice +from .account import IcloudAccount, IcloudDevice from .const import ( DEVICE_LOCATION_HORIZONTAL_ACCURACY, DEVICE_LOCATION_LATITUDE, DEVICE_LOCATION_LONGITUDE, DOMAIN, - SERVICE_UPDATE, ) _LOGGER = logging.getLogger(__name__) @@ -30,25 +29,45 @@ async def async_setup_scanner( async def async_setup_entry( hass: HomeAssistantType, entry: ConfigEntry, async_add_entities -): - """Configure a dispatcher connection based on a config entry.""" - username = entry.data[CONF_USERNAME] +) -> None: + """Set up device tracker for iCloud component.""" + account = hass.data[DOMAIN][entry.unique_id] + tracked = set() - for device in hass.data[DOMAIN][username].devices.values(): - if device.location is None: - _LOGGER.debug("No position found for %s", device.name) + @callback + def update_account(): + """Update the values of the account.""" + add_entities(account, async_add_entities, tracked) + + account.listeners.append( + async_dispatcher_connect(hass, account.signal_device_new, update_account) + ) + + update_account() + + +@callback +def add_entities(account, async_add_entities, tracked): + """Add new tracker entities from the account.""" + new_tracked = [] + + for dev_id, device in account.devices.items(): + if dev_id in tracked or device.location is None: continue - _LOGGER.debug("Adding device_tracker for %s", device.name) + new_tracked.append(IcloudTrackerEntity(account, device)) + tracked.add(dev_id) - async_add_entities([IcloudTrackerEntity(device)]) + if new_tracked: + async_add_entities(new_tracked, True) class IcloudTrackerEntity(TrackerEntity): """Represent a tracked device.""" - def __init__(self, device: IcloudDevice): + def __init__(self, account: IcloudAccount, device: IcloudDevice): """Set up the iCloud tracker entity.""" + self._account = account self._device = device self._unsub_dispatcher = None @@ -115,7 +134,7 @@ class IcloudTrackerEntity(TrackerEntity): async def async_added_to_hass(self): """Register state update callback.""" self._unsub_dispatcher = async_dispatcher_connect( - self.hass, SERVICE_UPDATE, self.async_write_ha_state + self.hass, self._account.signal_device_update, self.async_write_ha_state ) async def async_will_remove_from_hass(self): diff --git a/homeassistant/components/icloud/sensor.py b/homeassistant/components/icloud/sensor.py index e24016795d3..7dc699f6cb7 100644 --- a/homeassistant/components/icloud/sensor.py +++ b/homeassistant/components/icloud/sensor.py @@ -3,14 +3,15 @@ import logging from typing import Dict from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_USERNAME, DEVICE_CLASS_BATTERY +from homeassistant.const import DEVICE_CLASS_BATTERY +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.helpers.icon import icon_for_battery_level from homeassistant.helpers.typing import HomeAssistantType -from .account import IcloudDevice -from .const import DOMAIN, SERVICE_UPDATE +from .account import IcloudAccount, IcloudDevice +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -18,23 +19,44 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry( hass: HomeAssistantType, entry: ConfigEntry, async_add_entities ) -> None: - """Set up iCloud devices sensors based on a config entry.""" - username = entry.data[CONF_USERNAME] + """Set up device tracker for iCloud component.""" + account = hass.data[DOMAIN][entry.unique_id] + tracked = set() - entities = [] - for device in hass.data[DOMAIN][username].devices.values(): - if device.battery_level is not None: - _LOGGER.debug("Adding battery sensor for %s", device.name) - entities.append(IcloudDeviceBatterySensor(device)) + @callback + def update_account(): + """Update the values of the account.""" + add_entities(account, async_add_entities, tracked) - async_add_entities(entities, True) + account.listeners.append( + async_dispatcher_connect(hass, account.signal_device_new, update_account) + ) + + update_account() + + +@callback +def add_entities(account, async_add_entities, tracked): + """Add new tracker entities from the account.""" + new_tracked = [] + + for dev_id, device in account.devices.items(): + if dev_id in tracked or device.battery_level is None: + continue + + new_tracked.append(IcloudDeviceBatterySensor(account, device)) + tracked.add(dev_id) + + if new_tracked: + async_add_entities(new_tracked, True) class IcloudDeviceBatterySensor(Entity): """Representation of a iCloud device battery sensor.""" - def __init__(self, device: IcloudDevice): + def __init__(self, account: IcloudAccount, device: IcloudDevice): """Initialize the battery sensor.""" + self._account = account self._device = device self._unsub_dispatcher = None @@ -94,7 +116,7 @@ class IcloudDeviceBatterySensor(Entity): async def async_added_to_hass(self): """Register state update callback.""" self._unsub_dispatcher = async_dispatcher_connect( - self.hass, SERVICE_UPDATE, self.async_write_ha_state + self.hass, self._account.signal_device_update, self.async_write_ha_state ) async def async_will_remove_from_hass(self): From c0dcd9c674024c386bbd7ab9757761a5d6506f8e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 5 Mar 2020 17:19:30 -0800 Subject: [PATCH 18/21] Send messages conforming new facebook policy (#32516) --- homeassistant/components/facebook/notify.py | 7 ++++++- tests/components/facebook/test_notify.py | 11 ++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/facebook/notify.py b/homeassistant/components/facebook/notify.py index b75f2628033..dbd9be61516 100644 --- a/homeassistant/components/facebook/notify.py +++ b/homeassistant/components/facebook/notify.py @@ -97,7 +97,12 @@ class FacebookNotificationService(BaseNotificationService): else: recipient = {"id": target} - body = {"recipient": recipient, "message": body_message} + body = { + "recipient": recipient, + "message": body_message, + "messaging_type": "MESSAGE_TAG", + "tag": "ACCOUNT_UPDATE", + } resp = requests.post( BASE_URL, data=json.dumps(body), diff --git a/tests/components/facebook/test_notify.py b/tests/components/facebook/test_notify.py index c4c85d1cee0..c4675a4311a 100644 --- a/tests/components/facebook/test_notify.py +++ b/tests/components/facebook/test_notify.py @@ -30,6 +30,8 @@ class TestFacebook(unittest.TestCase): expected_body = { "recipient": {"phone_number": target[0]}, "message": {"text": message}, + "messaging_type": "MESSAGE_TAG", + "tag": "ACCOUNT_UPDATE", } assert mock.last_request.json() == expected_body @@ -53,6 +55,8 @@ class TestFacebook(unittest.TestCase): expected_body = { "recipient": {"phone_number": target}, "message": {"text": message}, + "messaging_type": "MESSAGE_TAG", + "tag": "ACCOUNT_UPDATE", } assert request.json() == expected_body @@ -77,7 +81,12 @@ class TestFacebook(unittest.TestCase): assert mock.called assert mock.call_count == 1 - expected_body = {"recipient": {"phone_number": target[0]}, "message": data} + expected_body = { + "recipient": {"phone_number": target[0]}, + "message": data, + "messaging_type": "MESSAGE_TAG", + "tag": "ACCOUNT_UPDATE", + } assert mock.last_request.json() == expected_body expected_params = {"access_token": ["page-access-token"]} From ccb8b6b9c86adfa737035bfb75ed4c3918bcb977 Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Sun, 8 Mar 2020 19:10:05 +0100 Subject: [PATCH 19/21] Update python-velbus to fix a missing data file (#32580) --- homeassistant/components/velbus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/velbus/manifest.json b/homeassistant/components/velbus/manifest.json index 3063c4445bd..4179c3e89ba 100644 --- a/homeassistant/components/velbus/manifest.json +++ b/homeassistant/components/velbus/manifest.json @@ -2,7 +2,7 @@ "domain": "velbus", "name": "Velbus", "documentation": "https://www.home-assistant.io/integrations/velbus", - "requirements": ["python-velbus==2.0.41"], + "requirements": ["python-velbus==2.0.42"], "config_flow": true, "dependencies": [], "codeowners": ["@Cereal2nd", "@brefra"] diff --git a/requirements_all.txt b/requirements_all.txt index 111450d0193..c38bf90a94c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1643,7 +1643,7 @@ python-telnet-vlc==1.0.4 python-twitch-client==0.6.0 # homeassistant.components.velbus -python-velbus==2.0.41 +python-velbus==2.0.42 # homeassistant.components.vlc python-vlc==1.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 26fd8eccdb2..e48709cfc05 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -581,7 +581,7 @@ python-nest==4.1.0 python-twitch-client==0.6.0 # homeassistant.components.velbus -python-velbus==2.0.41 +python-velbus==2.0.42 # homeassistant.components.awair python_awair==0.0.4 From 1d1f8df50961eaa03b9943e4357e82b0d4620d94 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 10 Mar 2020 11:47:07 -0700 Subject: [PATCH 20/21] Upgrade to coronavirus 1.1.0 (#32648) --- homeassistant/components/coronavirus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/coronavirus/manifest.json b/homeassistant/components/coronavirus/manifest.json index d99a9b621a2..68e73525291 100644 --- a/homeassistant/components/coronavirus/manifest.json +++ b/homeassistant/components/coronavirus/manifest.json @@ -3,7 +3,7 @@ "name": "Coronavirus (COVID-19)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/coronavirus", - "requirements": ["coronavirus==1.0.1"], + "requirements": ["coronavirus==1.1.0"], "ssdp": [], "zeroconf": [], "homekit": {}, diff --git a/requirements_all.txt b/requirements_all.txt index c38bf90a94c..b0b4bd65479 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -399,7 +399,7 @@ connect-box==0.2.5 construct==2.9.45 # homeassistant.components.coronavirus -coronavirus==1.0.1 +coronavirus==1.1.0 # homeassistant.scripts.credstash # credstash==1.15.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e48709cfc05..d657d0ba24e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -144,7 +144,7 @@ colorlog==4.1.0 construct==2.9.45 # homeassistant.components.coronavirus -coronavirus==1.0.1 +coronavirus==1.1.0 # homeassistant.scripts.credstash # credstash==1.15.0 From 39bdb562d3ce563168ae5f266b8920f48001355e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 10 Mar 2020 11:50:54 -0700 Subject: [PATCH 21/21] Bumped version to 0.106.6 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index aca7adc28a5..224083ccb74 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 = "5" +PATCH_VERSION = "6" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 0)