diff --git a/homeassistant/components/device_tracker/icloud.py b/homeassistant/components/device_tracker/icloud.py index 1ff7fa9ae27..05ec82f087a 100644 --- a/homeassistant/components/device_tracker/icloud.py +++ b/homeassistant/components/device_tracker/icloud.py @@ -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 diff --git a/requirements_all.txt b/requirements_all.txt index e18f3380af6..bb7d379b0ac 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index 388fe7db415..a88b4d18de4 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -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, {}))