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/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/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): 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/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) diff --git a/requirements_all.txt b/requirements_all.txt index 111450d0193..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 @@ -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..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 @@ -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 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"]}