Update icloud device_tracker (#2614)
* slugify() for dev_id (fixes #2162) [Keep space replacement to not impact known_devices.yaml] * pyicloud upgrade 0.9.1 * config validation * Only poll icloud every 4 minutes... * Immediately pull device state on HASS start * Added new test with icloud char e' acute [chr(233)] * Suppress pyicloud logging
This commit is contained in:
parent
fed2c33b54
commit
8c728d1b4e
3 changed files with 57 additions and 39 deletions
|
@ -5,17 +5,27 @@ For more details about this platform, please refer to the documentation at
|
|||
https://home-assistant.io/components/device_tracker.icloud/
|
||||
"""
|
||||
import logging
|
||||
import re
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.const import (CONF_PASSWORD, CONF_USERNAME,
|
||||
EVENT_HOMEASSISTANT_START)
|
||||
from homeassistant.helpers.event import track_utc_time_change
|
||||
from homeassistant.util import slugify
|
||||
from homeassistant.components.device_tracker import PLATFORM_SCHEMA
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['pyicloud==0.8.3']
|
||||
REQUIREMENTS = ['pyicloud==0.9.1']
|
||||
|
||||
CONF_INTERVAL = 'interval'
|
||||
DEFAULT_INTERVAL = 8
|
||||
KEEPALIVE_INTERVAL = 4
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_USERNAME): vol.Coerce(str),
|
||||
vol.Required(CONF_PASSWORD): vol.Coerce(str),
|
||||
vol.Optional(CONF_INTERVAL, default=8): vol.All(vol.Coerce(int),
|
||||
vol.Range(min=1))
|
||||
})
|
||||
|
||||
|
||||
def setup_scanner(hass, config, see):
|
||||
|
@ -23,38 +33,32 @@ def setup_scanner(hass, config, see):
|
|||
from pyicloud import PyiCloudService
|
||||
from pyicloud.exceptions import PyiCloudFailedLoginException
|
||||
from pyicloud.exceptions import PyiCloudNoDevicesException
|
||||
logging.getLogger("pyicloud.base").setLevel(logging.WARNING)
|
||||
|
||||
# Get the username and password from the configuration.
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
|
||||
if username is None or password is None:
|
||||
_LOGGER.error('Must specify a username and password')
|
||||
return False
|
||||
username = config[CONF_USERNAME]
|
||||
password = config[CONF_PASSWORD]
|
||||
|
||||
try:
|
||||
_LOGGER.info('Logging into iCloud Account')
|
||||
# Attempt the login to iCloud
|
||||
api = PyiCloudService(username,
|
||||
password,
|
||||
verify=True)
|
||||
api = PyiCloudService(username, password, verify=True)
|
||||
except PyiCloudFailedLoginException as error:
|
||||
_LOGGER.exception('Error logging into iCloud Service: %s', error)
|
||||
return False
|
||||
|
||||
def keep_alive(now):
|
||||
"""Keep authenticating iCloud connection."""
|
||||
"""Keep authenticating iCloud connection.
|
||||
|
||||
The session timeouts if we are not using it so we
|
||||
have to re-authenticate & this will send an email.
|
||||
"""
|
||||
api.authenticate()
|
||||
_LOGGER.info("Authenticate against iCloud")
|
||||
|
||||
track_utc_time_change(hass, keep_alive, second=0)
|
||||
|
||||
def update_icloud(now):
|
||||
"""Authenticate against iCloud and scan for devices."""
|
||||
try:
|
||||
# The session timeouts if we are not using it so we
|
||||
# have to re-authenticate. This will send an email.
|
||||
api.authenticate()
|
||||
keep_alive(None)
|
||||
# Loop through every device registered with the iCloud account
|
||||
for device in api.devices:
|
||||
status = device.status()
|
||||
|
@ -62,24 +66,23 @@ def setup_scanner(hass, config, see):
|
|||
# If the device has a location add it. If not do nothing
|
||||
if location:
|
||||
see(
|
||||
dev_id=re.sub(r"(\s|\W|')",
|
||||
'',
|
||||
status['name']),
|
||||
dev_id=slugify(status['name'].replace(' ', '', 99)),
|
||||
host_name=status['name'],
|
||||
gps=(location['latitude'], location['longitude']),
|
||||
battery=status['batteryLevel']*100,
|
||||
gps_accuracy=location['horizontalAccuracy']
|
||||
)
|
||||
else:
|
||||
# No location found for the device so continue
|
||||
continue
|
||||
except PyiCloudNoDevicesException:
|
||||
_LOGGER.info('No iCloud Devices found!')
|
||||
|
||||
track_utc_time_change(
|
||||
hass, update_icloud,
|
||||
minute=range(0, 60, config.get(CONF_INTERVAL, DEFAULT_INTERVAL)),
|
||||
second=0
|
||||
)
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, update_icloud)
|
||||
|
||||
update_minutes = list(range(0, 60, config[CONF_INTERVAL]))
|
||||
# Schedule keepalives between the updates
|
||||
keepalive_minutes = list(x for x in range(0, 60, KEEPALIVE_INTERVAL)
|
||||
if x not in update_minutes)
|
||||
|
||||
track_utc_time_change(hass, update_icloud, second=0, minute=update_minutes)
|
||||
track_utc_time_change(hass, keep_alive, second=0, minute=keepalive_minutes)
|
||||
|
||||
return True
|
||||
|
|
|
@ -291,7 +291,7 @@ pyfttt==0.3
|
|||
pyhomematic==0.1.10
|
||||
|
||||
# homeassistant.components.device_tracker.icloud
|
||||
pyicloud==0.8.3
|
||||
pyicloud==0.9.1
|
||||
|
||||
# homeassistant.components.sensor.lastfm
|
||||
pylast==1.6.0
|
||||
|
|
|
@ -46,17 +46,17 @@ class TestComponentsDeviceTracker(unittest.TestCase):
|
|||
|
||||
self.assertFalse(device_tracker.is_on(self.hass, entity_id))
|
||||
|
||||
def test_reading_broken_yaml_config(self):
|
||||
def test_reading_broken_yaml_config(self): # pylint: disable=no-self-use
|
||||
"""Test when known devices contains invalid data."""
|
||||
with tempfile.NamedTemporaryFile() as fp:
|
||||
with tempfile.NamedTemporaryFile() as fpt:
|
||||
# file is empty
|
||||
assert device_tracker.load_config(fp.name, None, False, 0) == []
|
||||
assert device_tracker.load_config(fpt.name, None, False, 0) == []
|
||||
|
||||
fp.write('100'.encode('utf-8'))
|
||||
fp.flush()
|
||||
fpt.write('100'.encode('utf-8'))
|
||||
fpt.flush()
|
||||
|
||||
# file contains a non-dict format
|
||||
assert device_tracker.load_config(fp.name, None, False, 0) == []
|
||||
assert device_tracker.load_config(fpt.name, None, False, 0) == []
|
||||
|
||||
def test_reading_yaml_config(self):
|
||||
"""Test the rendering of the YAML configuration."""
|
||||
|
@ -79,6 +79,7 @@ class TestComponentsDeviceTracker(unittest.TestCase):
|
|||
"""Test with no YAML file."""
|
||||
self.assertTrue(device_tracker.setup(self.hass, {}))
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
def test_adding_unknown_device_to_config(self):
|
||||
"""Test the adding of unknown devices to configuration file."""
|
||||
scanner = get_component('device_tracker.test').SCANNER
|
||||
|
@ -169,7 +170,7 @@ class TestComponentsDeviceTracker(unittest.TestCase):
|
|||
device_tracker.DOMAIN: {CONF_PLATFORM: 'test'}}))
|
||||
|
||||
self.assertTrue(self.hass.states.get(entity_id)
|
||||
.attributes.get(ATTR_HIDDEN))
|
||||
.attributes.get(ATTR_HIDDEN))
|
||||
|
||||
def test_group_all_devices(self):
|
||||
"""Test grouping of devices."""
|
||||
|
@ -211,6 +212,20 @@ class TestComponentsDeviceTracker(unittest.TestCase):
|
|||
mac=mac, dev_id=dev_id, host_name=host_name,
|
||||
location_name=location_name, gps=gps)
|
||||
|
||||
@patch('homeassistant.components.device_tracker.DeviceTracker.see')
|
||||
def test_see_service_unicode_dev_id(self, mock_see):
|
||||
"""Test the see service with a unicode dev_id and NO MAC."""
|
||||
self.assertTrue(device_tracker.setup(self.hass, {}))
|
||||
params = {
|
||||
'dev_id': chr(233), # e' acute accent from icloud
|
||||
'host_name': 'example.com',
|
||||
'location_name': 'Work',
|
||||
'gps': [.3, .8]
|
||||
}
|
||||
device_tracker.see(self.hass, **params)
|
||||
self.hass.pool.block_till_done()
|
||||
mock_see.assert_called_once_with(**params)
|
||||
|
||||
def test_not_write_duplicate_yaml_keys(self):
|
||||
"""Test that the device tracker will not generate invalid YAML."""
|
||||
self.assertTrue(device_tracker.setup(self.hass, {}))
|
||||
|
|
Loading…
Add table
Reference in a new issue