Merge pull request #32650 from home-assistant/rc

0.106.6
This commit is contained in:
Paulus Schoutsen 2020-03-10 13:01:35 -07:00 committed by GitHub
commit 2c1da182e2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 142 additions and 51 deletions

View File

@ -3,7 +3,7 @@
"name": "Coronavirus (COVID-19)", "name": "Coronavirus (COVID-19)",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/coronavirus", "documentation": "https://www.home-assistant.io/integrations/coronavirus",
"requirements": ["coronavirus==1.0.1"], "requirements": ["coronavirus==1.1.0"],
"ssdp": [], "ssdp": [],
"zeroconf": [], "zeroconf": [],
"homekit": {}, "homekit": {},

View File

@ -97,7 +97,12 @@ class FacebookNotificationService(BaseNotificationService):
else: else:
recipient = {"id": target} 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( resp = requests.post(
BASE_URL, BASE_URL,
data=json.dumps(body), data=json.dumps(body),

View File

@ -123,10 +123,8 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool
hass, username, password, icloud_dir, max_interval, gps_accuracy_threshold, hass, username, password, icloud_dir, max_interval, gps_accuracy_threshold,
) )
await hass.async_add_executor_job(account.setup) 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: for platform in PLATFORMS:
hass.async_create_task( hass.async_create_task(

View File

@ -10,6 +10,7 @@ from pyicloud.services.findmyiphone import AppleDevice
from homeassistant.components.zone import async_active_zone from homeassistant.components.zone import async_active_zone
from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.const import ATTR_ATTRIBUTION
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.dispatcher import dispatcher_send
from homeassistant.helpers.event import track_point_in_utc_time from homeassistant.helpers.event import track_point_in_utc_time
from homeassistant.helpers.storage import Store from homeassistant.helpers.storage import Store
@ -37,7 +38,7 @@ from .const import (
DEVICE_STATUS, DEVICE_STATUS,
DEVICE_STATUS_CODES, DEVICE_STATUS_CODES,
DEVICE_STATUS_SET, DEVICE_STATUS_SET,
SERVICE_UPDATE, DOMAIN,
) )
ATTRIBUTION = "Data provided by Apple iCloud" ATTRIBUTION = "Data provided by Apple iCloud"
@ -91,7 +92,7 @@ class IcloudAccount:
self._family_members_fullname = {} self._family_members_fullname = {}
self._devices = {} self._devices = {}
self.unsub_device_tracker = None self.listeners = []
def setup(self) -> None: def setup(self) -> None:
"""Set up an iCloud account.""" """Set up an iCloud account."""
@ -104,13 +105,17 @@ class IcloudAccount:
_LOGGER.error("Error logging into iCloud Service: %s", error) _LOGGER.error("Error logging into iCloud Service: %s", error)
return return
user_info = None
try: try:
api_devices = self.api.devices
# Gets device owners infos # Gets device owners infos
user_info = self.api.devices.response["userInfo"] user_info = api_devices.response["userInfo"]
except PyiCloudNoDevicesException: except (KeyError, PyiCloudNoDevicesException):
_LOGGER.error("No iCloud device found") _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']}" self._owner_fullname = f"{user_info['firstName']} {user_info['lastName']}"
@ -132,13 +137,21 @@ class IcloudAccount:
api_devices = {} api_devices = {}
try: try:
api_devices = self.api.devices api_devices = self.api.devices
except PyiCloudNoDevicesException:
_LOGGER.error("No iCloud device found")
return
except Exception as err: # pylint: disable=broad-except except Exception as err: # pylint: disable=broad-except
_LOGGER.error("Unknown iCloud error: %s", err) _LOGGER.error("Unknown iCloud error: %s", err)
self._fetch_interval = 5 self._fetch_interval = 2
dispatcher_send(self.hass, SERVICE_UPDATE) 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( track_point_in_utc_time(
self.hass, self.hass,
self.keep_alive, self.keep_alive,
@ -147,10 +160,19 @@ class IcloudAccount:
return return
# Gets devices infos # Gets devices infos
new_device = False
for device in api_devices: for device in api_devices:
status = device.status(DEVICE_STATUS_SET) status = device.status(DEVICE_STATUS_SET)
device_id = status[DEVICE_ID] device_id = status[DEVICE_ID]
device_name = status[DEVICE_NAME] 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: if self._devices.get(device_id, None) is not None:
# Seen device -> updating # Seen device -> updating
@ -165,9 +187,14 @@ class IcloudAccount:
) )
self._devices[device_id] = IcloudDevice(self, device, status) self._devices[device_id] = IcloudDevice(self, device, status)
self._devices[device_id].update(status) self._devices[device_id].update(status)
new_device = True
self._fetch_interval = self._determine_interval() 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( track_point_in_utc_time(
self.hass, self.hass,
self.keep_alive, self.keep_alive,
@ -291,6 +318,16 @@ class IcloudAccount:
"""Return the account devices.""" """Return the account devices."""
return self._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: class IcloudDevice:
"""Representation of a iCloud device.""" """Representation of a iCloud device."""
@ -348,6 +385,8 @@ class IcloudDevice:
and self._status[DEVICE_LOCATION][DEVICE_LOCATION_LATITUDE] and self._status[DEVICE_LOCATION][DEVICE_LOCATION_LATITUDE]
): ):
location = self._status[DEVICE_LOCATION] location = self._status[DEVICE_LOCATION]
if self._location is None:
dispatcher_send(self._account.hass, self._account.signal_device_new)
self._location = location self._location = location
def play_sound(self) -> None: def play_sound(self) -> None:

View File

@ -1,7 +1,6 @@
"""iCloud component constants.""" """iCloud component constants."""
DOMAIN = "icloud" DOMAIN = "icloud"
SERVICE_UPDATE = f"{DOMAIN}_update"
CONF_MAX_INTERVAL = "max_interval" CONF_MAX_INTERVAL = "max_interval"
CONF_GPS_ACCURACY_THRESHOLD = "gps_accuracy_threshold" CONF_GPS_ACCURACY_THRESHOLD = "gps_accuracy_threshold"

View File

@ -5,17 +5,16 @@ from typing import Dict
from homeassistant.components.device_tracker import SOURCE_TYPE_GPS from homeassistant.components.device_tracker import SOURCE_TYPE_GPS
from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.components.device_tracker.config_entry import TrackerEntity
from homeassistant.config_entries import ConfigEntry 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.dispatcher import async_dispatcher_connect
from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.typing import HomeAssistantType
from .account import IcloudDevice from .account import IcloudAccount, IcloudDevice
from .const import ( from .const import (
DEVICE_LOCATION_HORIZONTAL_ACCURACY, DEVICE_LOCATION_HORIZONTAL_ACCURACY,
DEVICE_LOCATION_LATITUDE, DEVICE_LOCATION_LATITUDE,
DEVICE_LOCATION_LONGITUDE, DEVICE_LOCATION_LONGITUDE,
DOMAIN, DOMAIN,
SERVICE_UPDATE,
) )
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -30,25 +29,45 @@ async def async_setup_scanner(
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistantType, entry: ConfigEntry, async_add_entities hass: HomeAssistantType, entry: ConfigEntry, async_add_entities
): ) -> None:
"""Configure a dispatcher connection based on a config entry.""" """Set up device tracker for iCloud component."""
username = entry.data[CONF_USERNAME] account = hass.data[DOMAIN][entry.unique_id]
tracked = set()
for device in hass.data[DOMAIN][username].devices.values(): @callback
if device.location is None: def update_account():
_LOGGER.debug("No position found for %s", device.name) """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 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): class IcloudTrackerEntity(TrackerEntity):
"""Represent a tracked device.""" """Represent a tracked device."""
def __init__(self, device: IcloudDevice): def __init__(self, account: IcloudAccount, device: IcloudDevice):
"""Set up the iCloud tracker entity.""" """Set up the iCloud tracker entity."""
self._account = account
self._device = device self._device = device
self._unsub_dispatcher = None self._unsub_dispatcher = None
@ -115,7 +134,7 @@ class IcloudTrackerEntity(TrackerEntity):
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Register state update callback.""" """Register state update callback."""
self._unsub_dispatcher = async_dispatcher_connect( 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): async def async_will_remove_from_hass(self):

View File

@ -3,14 +3,15 @@ import logging
from typing import Dict from typing import Dict
from homeassistant.config_entries import ConfigEntry 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.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.icon import icon_for_battery_level from homeassistant.helpers.icon import icon_for_battery_level
from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.typing import HomeAssistantType
from .account import IcloudDevice from .account import IcloudAccount, IcloudDevice
from .const import DOMAIN, SERVICE_UPDATE from .const import DOMAIN
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -18,23 +19,44 @@ _LOGGER = logging.getLogger(__name__)
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistantType, entry: ConfigEntry, async_add_entities hass: HomeAssistantType, entry: ConfigEntry, async_add_entities
) -> None: ) -> None:
"""Set up iCloud devices sensors based on a config entry.""" """Set up device tracker for iCloud component."""
username = entry.data[CONF_USERNAME] account = hass.data[DOMAIN][entry.unique_id]
tracked = set()
entities = [] @callback
for device in hass.data[DOMAIN][username].devices.values(): def update_account():
if device.battery_level is not None: """Update the values of the account."""
_LOGGER.debug("Adding battery sensor for %s", device.name) add_entities(account, async_add_entities, tracked)
entities.append(IcloudDeviceBatterySensor(device))
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): class IcloudDeviceBatterySensor(Entity):
"""Representation of a iCloud device battery sensor.""" """Representation of a iCloud device battery sensor."""
def __init__(self, device: IcloudDevice): def __init__(self, account: IcloudAccount, device: IcloudDevice):
"""Initialize the battery sensor.""" """Initialize the battery sensor."""
self._account = account
self._device = device self._device = device
self._unsub_dispatcher = None self._unsub_dispatcher = None
@ -94,7 +116,7 @@ class IcloudDeviceBatterySensor(Entity):
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Register state update callback.""" """Register state update callback."""
self._unsub_dispatcher = async_dispatcher_connect( 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): async def async_will_remove_from_hass(self):

View File

@ -2,7 +2,7 @@
"domain": "velbus", "domain": "velbus",
"name": "Velbus", "name": "Velbus",
"documentation": "https://www.home-assistant.io/integrations/velbus", "documentation": "https://www.home-assistant.io/integrations/velbus",
"requirements": ["python-velbus==2.0.41"], "requirements": ["python-velbus==2.0.42"],
"config_flow": true, "config_flow": true,
"dependencies": [], "dependencies": [],
"codeowners": ["@Cereal2nd", "@brefra"] "codeowners": ["@Cereal2nd", "@brefra"]

View File

@ -1,7 +1,7 @@
"""Constants used by Home Assistant components.""" """Constants used by Home Assistant components."""
MAJOR_VERSION = 0 MAJOR_VERSION = 0
MINOR_VERSION = 106 MINOR_VERSION = 106
PATCH_VERSION = "5" PATCH_VERSION = "6"
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__ = f"{__short_version__}.{PATCH_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER = (3, 7, 0) REQUIRED_PYTHON_VER = (3, 7, 0)

View File

@ -399,7 +399,7 @@ connect-box==0.2.5
construct==2.9.45 construct==2.9.45
# homeassistant.components.coronavirus # homeassistant.components.coronavirus
coronavirus==1.0.1 coronavirus==1.1.0
# homeassistant.scripts.credstash # homeassistant.scripts.credstash
# credstash==1.15.0 # credstash==1.15.0
@ -1643,7 +1643,7 @@ python-telnet-vlc==1.0.4
python-twitch-client==0.6.0 python-twitch-client==0.6.0
# homeassistant.components.velbus # homeassistant.components.velbus
python-velbus==2.0.41 python-velbus==2.0.42
# homeassistant.components.vlc # homeassistant.components.vlc
python-vlc==1.1.2 python-vlc==1.1.2

View File

@ -144,7 +144,7 @@ colorlog==4.1.0
construct==2.9.45 construct==2.9.45
# homeassistant.components.coronavirus # homeassistant.components.coronavirus
coronavirus==1.0.1 coronavirus==1.1.0
# homeassistant.scripts.credstash # homeassistant.scripts.credstash
# credstash==1.15.0 # credstash==1.15.0
@ -581,7 +581,7 @@ python-nest==4.1.0
python-twitch-client==0.6.0 python-twitch-client==0.6.0
# homeassistant.components.velbus # homeassistant.components.velbus
python-velbus==2.0.41 python-velbus==2.0.42
# homeassistant.components.awair # homeassistant.components.awair
python_awair==0.0.4 python_awair==0.0.4

View File

@ -30,6 +30,8 @@ class TestFacebook(unittest.TestCase):
expected_body = { expected_body = {
"recipient": {"phone_number": target[0]}, "recipient": {"phone_number": target[0]},
"message": {"text": message}, "message": {"text": message},
"messaging_type": "MESSAGE_TAG",
"tag": "ACCOUNT_UPDATE",
} }
assert mock.last_request.json() == expected_body assert mock.last_request.json() == expected_body
@ -53,6 +55,8 @@ class TestFacebook(unittest.TestCase):
expected_body = { expected_body = {
"recipient": {"phone_number": target}, "recipient": {"phone_number": target},
"message": {"text": message}, "message": {"text": message},
"messaging_type": "MESSAGE_TAG",
"tag": "ACCOUNT_UPDATE",
} }
assert request.json() == expected_body assert request.json() == expected_body
@ -77,7 +81,12 @@ class TestFacebook(unittest.TestCase):
assert mock.called assert mock.called
assert mock.call_count == 1 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 assert mock.last_request.json() == expected_body
expected_params = {"access_token": ["page-access-token"]} expected_params = {"access_token": ["page-access-token"]}