From 90007a04d384e9004a42d529f92e553b9ffa76c7 Mon Sep 17 00:00:00 2001 From: Daren Lord Date: Thu, 12 Nov 2015 23:37:15 -0700 Subject: [PATCH 1/7] Adding iCloud device_tracker component. Allow to track devices registered with iCloud --- .../components/device_tracker/icloud.py | 131 ++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 homeassistant/components/device_tracker/icloud.py diff --git a/homeassistant/components/device_tracker/icloud.py b/homeassistant/components/device_tracker/icloud.py new file mode 100644 index 00000000000..f193e68bc22 --- /dev/null +++ b/homeassistant/components/device_tracker/icloud.py @@ -0,0 +1,131 @@ +""" +homeassistant.components.device_tracker.icloud +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Device tracker platform that supports scanning a iCloud devices. + +It does require that your device has registered with Find My iPhone. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/device_tracker.icloud/ +""" +import logging +from datetime import timedelta +import threading + +from homeassistant.const import CONF_USERNAME, CONF_PASSWORD +from homeassistant.helpers import validate_config +from homeassistant.util import Throttle +from homeassistant.components.device_tracker import DOMAIN + +import re + +# Return cached results if last scan was less then this time ago +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=60) + +_LOGGER = logging.getLogger(__name__) + +REQUIREMENTS = ['https://github.com/picklepete/pyicloud/archive/' + '80f6cd6decc950514b8dc43b30c5bded81b34d5f.zip' + '#pyicloud==0.8.0', + 'certifi'] + + +#pylint: disable=unused-argument +def get_scanner(hass, config): + """ Validates config and returns a iPhone Scanner. """ + if not validate_config(config, + {DOMAIN: [CONF_USERNAME, CONF_PASSWORD]}, + _LOGGER): + return None + + scanner = ICloudDeviceScanner(config[DOMAIN]) + + return scanner if scanner.success_init else None + + +class ICloudDeviceScanner(object): + """ + This class looks up devices from your iCloud account + and can report on their lat and long if registered. + """ + + def __init__(self, config): + from pyicloud import PyiCloudService + from pyicloud.exceptions import PyiCloudFailedLoginException + from pyicloud.exceptions import PyiCloudNoDevicesException + + self.username = config[CONF_USERNAME] + self.password = config[CONF_PASSWORD] + + self.lock = threading.Lock() + + self.last_results = {} + + # Get the data from iCloud + try: + _LOGGER.info('Logging into iCloud Services') + self._api = PyiCloudService(self.username, self.password, verify=True) + except PyiCloudFailedLoginException: + _LOGGER.exception("Failed login to iCloud Service." + + "Verify Username and Password") + return + + try: + devices = self.get_devices() + except PyiCloudNoDevicesException: + _LOGGER.exception("No iCloud Devices found.") + return + + self.success_init = devices is not None + + if self.success_init: + self.last_results = devices + else: + _LOGGER.error('Issues getting iCloud results') + + def scan_devices(self): + """ + Scans for new devices and return a list containing found devices id's + """ + + self._update_info() + + return [device for device in self.last_results] + + def get_device_name(self, mac): + """ Returns the name of the given device or None if we don't know """ + try: + return next(device for device in self.last_results + if device == mac) + except StopIteration: + return None + + @Throttle(MIN_TIME_BETWEEN_SCANS) + def _update_info(self): + """ Retrieve the latest information from iCloud + Returns a bool if scanning is successful + """ + + if not self.success_init: + return + + with self.lock: + _LOGGER.info('Scanning iCloud Devices') + + self.last_results = self.get_devices() or {} + + def get_devices(self): + devices = {} + for device in self._api.devices: + try: + devices[device.status()['name']] = { + 'device_id': re.sub(r'(\s*|\W*)', device.status()['name'], ''), + 'host_name': device.status()['name'], + 'gps': (device.location()['latitude'], + device.location()['longitude']), + 'battery': device.status()['batteryLevel']*100 + } + except TypeError: + # Device is not tracked. + continue + return devices \ No newline at end of file From c60bb35d4ae7a277117b490ad37e7f000479ca67 Mon Sep 17 00:00:00 2001 From: Daren Lord Date: Thu, 12 Nov 2015 23:40:30 -0700 Subject: [PATCH 2/7] Fixed lint errors --- homeassistant/components/device_tracker/icloud.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/device_tracker/icloud.py b/homeassistant/components/device_tracker/icloud.py index f193e68bc22..f4b3998ab40 100644 --- a/homeassistant/components/device_tracker/icloud.py +++ b/homeassistant/components/device_tracker/icloud.py @@ -30,7 +30,6 @@ REQUIREMENTS = ['https://github.com/picklepete/pyicloud/archive/' 'certifi'] -#pylint: disable=unused-argument def get_scanner(hass, config): """ Validates config and returns a iPhone Scanner. """ if not validate_config(config, @@ -64,7 +63,9 @@ class ICloudDeviceScanner(object): # Get the data from iCloud try: _LOGGER.info('Logging into iCloud Services') - self._api = PyiCloudService(self.username, self.password, verify=True) + self._api = PyiCloudService(self.username, + self.password, + verify=True) except PyiCloudFailedLoginException: _LOGGER.exception("Failed login to iCloud Service." + "Verify Username and Password") @@ -119,7 +120,9 @@ class ICloudDeviceScanner(object): for device in self._api.devices: try: devices[device.status()['name']] = { - 'device_id': re.sub(r'(\s*|\W*)', device.status()['name'], ''), + 'device_id': re.sub(r'(\s*|\W*)', + device.status()['name'], + ''), 'host_name': device.status()['name'], 'gps': (device.location()['latitude'], device.location()['longitude']), @@ -128,4 +131,4 @@ class ICloudDeviceScanner(object): except TypeError: # Device is not tracked. continue - return devices \ No newline at end of file + return devices From fff6b24449d8ea77aefb66eb603b9f5ad5309061 Mon Sep 17 00:00:00 2001 From: Daren Lord Date: Sat, 21 Nov 2015 21:04:28 -0700 Subject: [PATCH 3/7] Switching to new device scanner setup. --- .../components/device_tracker/icloud.py | 163 ++++++------------ 1 file changed, 54 insertions(+), 109 deletions(-) diff --git a/homeassistant/components/device_tracker/icloud.py b/homeassistant/components/device_tracker/icloud.py index f4b3998ab40..e99d5fc0e30 100644 --- a/homeassistant/components/device_tracker/icloud.py +++ b/homeassistant/components/device_tracker/icloud.py @@ -1,134 +1,79 @@ """ homeassistant.components.device_tracker.icloud ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Device tracker platform that supports scanning a iCloud devices. +Device tracker platform that supports scanning iCloud devices. -It does require that your device has registered with Find My iPhone. +It does require that your device has beend registered with Find My iPhone. + +Note: that this may cause battery drainage as it wakes up your device to +get the current location. + +Note: You may receive an email from Apple stating that someone has logged +into your account. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.icloud/ """ import logging -from datetime import timedelta -import threading from homeassistant.const import CONF_USERNAME, CONF_PASSWORD -from homeassistant.helpers import validate_config -from homeassistant.util import Throttle -from homeassistant.components.device_tracker import DOMAIN - +from homeassistant.helpers.event import track_utc_time_change +from pyicloud import PyiCloudService +from pyicloud.exceptions import PyiCloudFailedLoginException +from pyicloud.exceptions import PyiCloudNoDevicesException import re -# Return cached results if last scan was less then this time ago -MIN_TIME_BETWEEN_SCANS = timedelta(seconds=60) +SCAN_INTERVAL = 60 _LOGGER = logging.getLogger(__name__) REQUIREMENTS = ['https://github.com/picklepete/pyicloud/archive/' '80f6cd6decc950514b8dc43b30c5bded81b34d5f.zip' - '#pyicloud==0.8.0', - 'certifi'] + '#pyicloud==0.8.0'] -def get_scanner(hass, config): - """ Validates config and returns a iPhone Scanner. """ - if not validate_config(config, - {DOMAIN: [CONF_USERNAME, CONF_PASSWORD]}, - _LOGGER): - return None +def setup_scanner(hass, config, see): - scanner = ICloudDeviceScanner(config[DOMAIN]) + # Get the username and password from the configuration + username = config[CONF_USERNAME] + password = config[CONF_PASSWORD] - return scanner if scanner.success_init else None + try: + _LOGGER.info('Logging into iCloud Account') + # Attempt the login to iCloud + api = PyiCloudService(username, + password, + verify=True) + except PyiCloudFailedLoginException as e: + _LOGGER.exception('Error logging into iCloud Service: {0}'.format(str(e))) - -class ICloudDeviceScanner(object): - """ - This class looks up devices from your iCloud account - and can report on their lat and long if registered. - """ - - def __init__(self, config): - from pyicloud import PyiCloudService - from pyicloud.exceptions import PyiCloudFailedLoginException - from pyicloud.exceptions import PyiCloudNoDevicesException - - self.username = config[CONF_USERNAME] - self.password = config[CONF_PASSWORD] - - self.lock = threading.Lock() - - self.last_results = {} - - # Get the data from iCloud + def update_icloud(now): try: - _LOGGER.info('Logging into iCloud Services') - self._api = PyiCloudService(self.username, - self.password, - verify=True) - except PyiCloudFailedLoginException: - _LOGGER.exception("Failed login to iCloud Service." + - "Verify Username and Password") - return + # The session timeouts if we are not using it so we have to re-authenticate. This will send an email. + api.authenticate() + # Loop through every device registered with the iCloud account + for device in api.devices: + status = device.status() + location = device.location() + # If the device has a location add it. If not do nothing + if location: + see( + dev_id=re.sub(r"(\s|\W|')", + '', + status['name']), + 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 as e: + _LOGGER.exception('No iCloud Devices found!') - try: - devices = self.get_devices() - except PyiCloudNoDevicesException: - _LOGGER.exception("No iCloud Devices found.") - return - - self.success_init = devices is not None - - if self.success_init: - self.last_results = devices - else: - _LOGGER.error('Issues getting iCloud results') - - def scan_devices(self): - """ - Scans for new devices and return a list containing found devices id's - """ - - self._update_info() - - return [device for device in self.last_results] - - def get_device_name(self, mac): - """ Returns the name of the given device or None if we don't know """ - try: - return next(device for device in self.last_results - if device == mac) - except StopIteration: - return None - - @Throttle(MIN_TIME_BETWEEN_SCANS) - def _update_info(self): - """ Retrieve the latest information from iCloud - Returns a bool if scanning is successful - """ - - if not self.success_init: - return - - with self.lock: - _LOGGER.info('Scanning iCloud Devices') - - self.last_results = self.get_devices() or {} - - def get_devices(self): - devices = {} - for device in self._api.devices: - try: - devices[device.status()['name']] = { - 'device_id': re.sub(r'(\s*|\W*)', - device.status()['name'], - ''), - 'host_name': device.status()['name'], - 'gps': (device.location()['latitude'], - device.location()['longitude']), - 'battery': device.status()['batteryLevel']*100 - } - except TypeError: - # Device is not tracked. - continue - return devices + track_utc_time_change( + hass, + update_icloud, + second=range(0, 60, SCAN_INTERVAL) + ) \ No newline at end of file From 807485473163e659a35d5bd16622d8d86db19a13 Mon Sep 17 00:00:00 2001 From: Daren Lord Date: Sat, 21 Nov 2015 21:12:41 -0700 Subject: [PATCH 4/7] Fixing formatting --- .../components/device_tracker/icloud.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/device_tracker/icloud.py b/homeassistant/components/device_tracker/icloud.py index e99d5fc0e30..5968d7d8ac8 100644 --- a/homeassistant/components/device_tracker/icloud.py +++ b/homeassistant/components/device_tracker/icloud.py @@ -33,6 +33,9 @@ REQUIREMENTS = ['https://github.com/picklepete/pyicloud/archive/' def setup_scanner(hass, config, see): + """ + Set up the iCloud Scanner + """ # Get the username and password from the configuration username = config[CONF_USERNAME] @@ -45,11 +48,17 @@ def setup_scanner(hass, config, see): password, verify=True) except PyiCloudFailedLoginException as e: - _LOGGER.exception('Error logging into iCloud Service: {0}'.format(str(e))) + _LOGGER.exception( + 'Error logging into iCloud Service: {0}'.format(str(e)) + ) 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. + # The session timeouts if we are not using it so we + # have to re-authenticate. This will send an email. api.authenticate() # Loop through every device registered with the iCloud account for device in api.devices: @@ -69,11 +78,11 @@ def setup_scanner(hass, config, see): else: # No location found for the device so continue continue - except PyiCloudNoDevicesException as e: + except PyiCloudNoDevicesException: _LOGGER.exception('No iCloud Devices found!') track_utc_time_change( hass, update_icloud, second=range(0, 60, SCAN_INTERVAL) - ) \ No newline at end of file + ) From e3d4e3ad4db6ad32ee6ff16a08e8be656343e1e4 Mon Sep 17 00:00:00 2001 From: Daren Lord Date: Fri, 4 Dec 2015 09:08:46 -0700 Subject: [PATCH 5/7] Increasing scan interval. Moved imports. --- homeassistant/components/device_tracker/icloud.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/device_tracker/icloud.py b/homeassistant/components/device_tracker/icloud.py index 5968d7d8ac8..2986bbec595 100644 --- a/homeassistant/components/device_tracker/icloud.py +++ b/homeassistant/components/device_tracker/icloud.py @@ -18,12 +18,9 @@ import logging from homeassistant.const import CONF_USERNAME, CONF_PASSWORD from homeassistant.helpers.event import track_utc_time_change -from pyicloud import PyiCloudService -from pyicloud.exceptions import PyiCloudFailedLoginException -from pyicloud.exceptions import PyiCloudNoDevicesException import re -SCAN_INTERVAL = 60 +SCAN_INTERVAL = 1800 _LOGGER = logging.getLogger(__name__) @@ -36,6 +33,9 @@ def setup_scanner(hass, config, see): """ Set up the iCloud Scanner """ + from pyicloud import PyiCloudService + from pyicloud.exceptions import PyiCloudFailedLoginException + from pyicloud.exceptions import PyiCloudNoDevicesException # Get the username and password from the configuration username = config[CONF_USERNAME] @@ -47,10 +47,11 @@ def setup_scanner(hass, config, see): api = PyiCloudService(username, password, verify=True) - except PyiCloudFailedLoginException as e: + except PyiCloudFailedLoginException as error: _LOGGER.exception( - 'Error logging into iCloud Service: {0}'.format(str(e)) + 'Error logging into iCloud Service: {0}'.format(error) ) + return def update_icloud(now): """ @@ -79,7 +80,7 @@ def setup_scanner(hass, config, see): # No location found for the device so continue continue except PyiCloudNoDevicesException: - _LOGGER.exception('No iCloud Devices found!') + _LOGGER.info('No iCloud Devices found!') track_utc_time_change( hass, From 9ecc08c0c8c97d655610f80f3c9090b68704401f Mon Sep 17 00:00:00 2001 From: Daren Lord Date: Fri, 4 Dec 2015 09:19:16 -0700 Subject: [PATCH 6/7] Adding in pyicloud to requirements_all.txt --- homeassistant/components/device_tracker/icloud.py | 4 ++-- requirements_all.txt | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/device_tracker/icloud.py b/homeassistant/components/device_tracker/icloud.py index 2986bbec595..d196cc40107 100644 --- a/homeassistant/components/device_tracker/icloud.py +++ b/homeassistant/components/device_tracker/icloud.py @@ -16,9 +16,9 @@ https://home-assistant.io/components/device_tracker.icloud/ """ import logging +import re from homeassistant.const import CONF_USERNAME, CONF_PASSWORD from homeassistant.helpers.event import track_utc_time_change -import re SCAN_INTERVAL = 1800 @@ -49,7 +49,7 @@ def setup_scanner(hass, config, see): verify=True) except PyiCloudFailedLoginException as error: _LOGGER.exception( - 'Error logging into iCloud Service: {0}'.format(error) + 'Error logging into iCloud Service: {}'.format(error) ) return diff --git a/requirements_all.txt b/requirements_all.txt index 2715ca3288d..2f5d27d0b7d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -170,3 +170,6 @@ https://github.com/persandstrom/python-verisure/archive/9873c4527f01b1ba1f72ae60 # homeassistant.components.zwave pydispatcher==2.0.5 + +# homeassistant.sensor.icloud +https://github.com/picklepete/pyicloud/archive/80f6cd6decc950514b8dc43b30c5bded81b34d5f.zip#pyicloud==0.8.0 From 254889e3fd6655b312f9f6fd95d0cfe6eed31d9d Mon Sep 17 00:00:00 2001 From: Daren Lord Date: Fri, 4 Dec 2015 09:23:05 -0700 Subject: [PATCH 7/7] Fixing logging for pylint --- homeassistant/components/device_tracker/icloud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/icloud.py b/homeassistant/components/device_tracker/icloud.py index d196cc40107..a4adaa547bc 100644 --- a/homeassistant/components/device_tracker/icloud.py +++ b/homeassistant/components/device_tracker/icloud.py @@ -49,7 +49,7 @@ def setup_scanner(hass, config, see): verify=True) except PyiCloudFailedLoginException as error: _LOGGER.exception( - 'Error logging into iCloud Service: {}'.format(error) + 'Error logging into iCloud Service: %s' % error ) return