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:
Johann Kellerman 2016-07-26 23:53:31 +02:00 committed by GitHub
parent fed2c33b54
commit 8c728d1b4e
3 changed files with 57 additions and 39 deletions

View file

@ -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

View file

@ -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

View file

@ -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, {}))