class Icloud(DeviceScanner): """Representation of an iCloud account.""" def __init__(self, hass, username, password, name, see, filter_devices): """Initialize an iCloud account.""" self.hass = hass self.username = username self.password = password self.api = None self.accountname = name self.filter_devices = filter_devices self.devices = {} self.seen_devices = {} self._overridestates = {} self._intervals = {} self.see = see self._trusted_device = None self._verification_code = None self._attrs = {} self._attrs[ATTR_ACCOUNTNAME] = name self.reset_account_icloud() randomseconds = random.randint(10, 59) track_utc_time_change( self.hass, self.keep_alive, second=randomseconds) def reset_account_icloud(self): """Reset an iCloud account.""" from pyicloud import PyiCloudService from pyicloud.exceptions import ( PyiCloudFailedLoginException, PyiCloudNoDevicesException) icloud_dir = self.hass.config.path('icloud') if not os.path.exists(icloud_dir): os.makedirs(icloud_dir) try: self.api = PyiCloudService( self.username, self.password, cookie_directory=icloud_dir, verify=True) except PyiCloudFailedLoginException as error: self.api = None _LOGGER.error("Error logging into iCloud Service: %s", error) return try: self.devices = {} self._overridestates = {} self._intervals = {} for device in self.api.devices: status = device.status(DEVICESTATUSSET) devicename = slugify(status['name'].replace(' ', '', 99)) if devicename not in self.devices: self.devices[devicename] = device self._intervals[devicename] = 1 self._overridestates[devicename] = None except PyiCloudNoDevicesException: _LOGGER.error('No iCloud Devices found!') def icloud_trusted_device_callback(self, callback_data): """Handle chosen trusted devices.""" self._trusted_device = int(callback_data.get('trusted_device')) self._trusted_device = self.api.trusted_devices[self._trusted_device] if not self.api.send_verification_code(self._trusted_device): _LOGGER.error("Failed to send verification code") self._trusted_device = None return if self.accountname in _CONFIGURING: request_id = _CONFIGURING.pop(self.accountname) configurator = self.hass.components.configurator configurator.request_done(request_id) # Trigger the next step immediately self.icloud_need_verification_code() def icloud_need_trusted_device(self): """We need a trusted device.""" configurator = self.hass.components.configurator if self.accountname in _CONFIGURING: return devicesstring = '' devices = self.api.trusted_devices for i, device in enumerate(devices): devicename = device.get( 'deviceName', 'SMS to %s' % device.get('phoneNumber')) devicesstring += "{}: {};".format(i, devicename) _CONFIGURING[self.accountname] = configurator.request_config( 'iCloud {}'.format(self.accountname), self.icloud_trusted_device_callback, description=( 'Please choose your trusted device by entering' ' the index from this list: ' + devicesstring), entity_picture="/static/images/config_icloud.png", submit_caption='Confirm', fields=[{'id': 'trusted_device', 'name': 'Trusted Device'}] ) def icloud_verification_callback(self, callback_data): """Handle the chosen trusted device.""" from pyicloud.exceptions import PyiCloudException self._verification_code = callback_data.get('code') try: if not self.api.validate_verification_code( self._trusted_device, self._verification_code): raise PyiCloudException('Unknown failure') except PyiCloudException as error: # Reset to the initial 2FA state to allow the user to retry _LOGGER.error("Failed to verify verification code: %s", error) self._trusted_device = None self._verification_code = None # Trigger the next step immediately self.icloud_need_trusted_device() if self.accountname in _CONFIGURING: request_id = _CONFIGURING.pop(self.accountname) configurator = self.hass.components.configurator configurator.request_done(request_id) def icloud_need_verification_code(self): """Return the verification code.""" configurator = self.hass.components.configurator if self.accountname in _CONFIGURING: return _CONFIGURING[self.accountname] = configurator.request_config( 'iCloud {}'.format(self.accountname), self.icloud_verification_callback, description=('Please enter the validation code:'), entity_picture="/static/images/config_icloud.png", submit_caption='Confirm', fields=[{'id': 'code', 'name': 'code'}] ) def keep_alive(self, now): """Keep the API alive.""" if self.api is None: self.reset_account_icloud() if self.api is None: return if self.api.requires_2fa: from pyicloud.exceptions import PyiCloudException try: if self._trusted_device is None: self.icloud_need_trusted_device() return if self._verification_code is None: self.icloud_need_verification_code() return self.api.authenticate() if self.api.requires_2fa: raise Exception('Unknown failure') self._trusted_device = None self._verification_code = None except PyiCloudException as error: _LOGGER.error("Error setting up 2FA: %s", error) #else: # self.api.authenticate() currentminutes = dt_util.now().hour * 60 + dt_util.now().minute try: for devicename in self.devices: interval = self._intervals.get(devicename, 1) if ((currentminutes % interval == 0) or (interval > 10 and currentminutes % interval in [2, 4])): if (self.filter_devices in devicename): _LOGGER.debug("Updating device " + devicename) self.api.authenticate() self.update_device(devicename) except ValueError: _LOGGER.debug("iCloud API returned an error") def determine_interval(self, devicename, latitude, longitude, battery): """Calculate new interval.""" distancefromhome = None zone_state = self.hass.states.get('zone.home') zone_state_lat = zone_state.attributes['latitude'] zone_state_long = zone_state.attributes['longitude'] distancefromhome = distance( latitude, longitude, zone_state_lat, zone_state_long) distancefromhome = round(distancefromhome / 1000, 1) currentzone = active_zone(self.hass, latitude, longitude) if ((currentzone is not None and currentzone == self._overridestates.get(devicename)) or (currentzone is None and self._overridestates.get(devicename) == 'away')): return self._overridestates[devicename] = None if currentzone is not None: self._intervals[devicename] = 30 return if distancefromhome is None: return if distancefromhome > 25: self._intervals[devicename] = round(distancefromhome / 2, 0) elif distancefromhome > 10: self._intervals[devicename] = 5 else: self._intervals[devicename] = 1 if battery is not None and battery <= 33 and distancefromhome > 3: self._intervals[devicename] = self._intervals[devicename] * 2 def update_device(self, devicename): """Update the device_tracker entity.""" from pyicloud.exceptions import PyiCloudNoDevicesException # An entity will not be created by see() when track=false in # 'known_devices.yaml', but we need to see() it at least once entity = self.hass.states.get(ENTITY_ID_FORMAT.format(devicename)) if entity is None and devicename in self.seen_devices: return attrs = {} kwargs = {} if self.api is None: return if self.filter_devices not in devicename: return try: for device in self.api.devices: if str(device) != str(self.devices[devicename]): continue status = device.status(DEVICESTATUSSET) dev_id = status['name'].replace(' ', '', 99) dev_id = slugify(dev_id) attrs[ATTR_DEVICESTATUS] = DEVICESTATUSCODES.get( status['deviceStatus'], 'error') attrs[ATTR_LOWPOWERMODE] = status['lowPowerMode'] attrs[ATTR_BATTERYSTATUS] = status['batteryStatus'] attrs[ATTR_ACCOUNTNAME] = self.accountname status = device.status(DEVICESTATUSSET) battery = status.get('batteryLevel', 0) * 100 location = status['location'] if location: self.determine_interval( devicename, location['latitude'], location['longitude'], battery) interval = self._intervals.get(devicename, 1) attrs[ATTR_INTERVAL] = interval accuracy = location['horizontalAccuracy'] kwargs['dev_id'] = dev_id kwargs['host_name'] = status['name'] kwargs['gps'] = (location['latitude'], location['longitude']) kwargs['battery'] = battery kwargs['gps_accuracy'] = accuracy kwargs[ATTR_ATTRIBUTES] = attrs self.see(**kwargs) self.seen_devices[devicename] = True except PyiCloudNoDevicesException: _LOGGER.error("No iCloud Devices found") def lost_iphone(self, devicename): """Call the lost iPhone function if the device is found.""" if self.api is None: return self.api.authenticate() for device in self.api.devices: if devicename is None or device == self.devices[devicename]: device.play_sound() def update_icloud(self, devicename=None): """Authenticate against iCloud and scan for devices.""" from pyicloud.exceptions import PyiCloudNoDevicesException if self.api is None: return try: if devicename is not None: if devicename in self.devices: self.devices[devicename].location() else: _LOGGER.error("devicename %s unknown for account %s", devicename, self._attrs[ATTR_ACCOUNTNAME]) else: for device in self.devices: self.devices[device].location() except PyiCloudNoDevicesException: _LOGGER.error("No iCloud Devices found") def setinterval(self, interval=None, devicename=None): """Set the interval of the given devices.""" devs = [devicename] if devicename else self.devices for device in devs: devid = '{}.{}'.format(DOMAIN, device) devicestate = self.hass.states.get(devid) if interval is not None: if devicestate is not None: self._overridestates[device] = active_zone( self.hass, float(devicestate.attributes.get('latitude', 0)), float(devicestate.attributes.get('longitude', 0))) if self._overridestates[device] is None: self._overridestates[device] = 'away' self._intervals[device] = interval else: self._overridestates[device] = None self.update_device(device)
class IcloudAccount: """Representation of an iCloud account.""" def __init__( self, hass: HomeAssistantType, username: str, password: str, icloud_dir: Store, account_name: str, max_interval: int, gps_accuracy_threshold: int, ): """Initialize an iCloud account.""" self.hass = hass self._username = username self._password = password self._name = account_name or slugify(username.partition("@")[0]) self._fetch_interval = max_interval self._max_interval = max_interval self._gps_accuracy_threshold = gps_accuracy_threshold self._icloud_dir = icloud_dir self.api = None self._owner_fullname = None self._family_members_fullname = {} self._devices = {} self.unsub_device_tracker = None def setup(self): """Set up an iCloud account.""" try: self.api = PyiCloudService(self._username, self._password, self._icloud_dir.path) except PyiCloudFailedLoginException as error: self.api = None _LOGGER.error("Error logging into iCloud Service: %s", error) return user_info = None try: # Gets device owners infos user_info = self.api.devices.response["userInfo"] except PyiCloudNoDevicesException: _LOGGER.error("No iCloud Devices found") self._owner_fullname = f"{user_info['firstName']} {user_info['lastName']}" self._family_members_fullname = {} for prs_id, member in user_info["membersInfo"].items(): self._family_members_fullname[ prs_id] = f"{member['firstName']} {member['lastName']}" self._devices = {} self.update_devices() def update_devices(self) -> None: """Update iCloud devices.""" if self.api is None: return api_devices = {} try: api_devices = self.api.devices except PyiCloudNoDevicesException: _LOGGER.error("No iCloud Devices found") # Gets devices infos for device in api_devices: status = device.status(DEVICE_STATUS_SET) device_id = status[DEVICE_ID] device_name = status[DEVICE_NAME] if self._devices.get(device_id, None) is not None: # Seen device -> updating _LOGGER.debug("Updating iCloud device: %s", device_name) self._devices[device_id].update(status) else: # New device, should be unique _LOGGER.debug( "Adding iCloud device: %s [model: %s]", device_name, status[DEVICE_RAW_DEVICE_MODEL], ) self._devices[device_id] = IcloudDevice(self, device, status) self._devices[device_id].update(status) dispatcher_send(self.hass, TRACKER_UPDATE) self._fetch_interval = self._determine_interval() track_point_in_utc_time( self.hass, self.keep_alive, utcnow() + timedelta(minutes=self._fetch_interval), ) def _determine_interval(self) -> int: """Calculate new interval between two API fetch (in minutes).""" intervals = {} for device in self._devices.values(): if device.location is None: continue current_zone = run_callback_threadsafe( self.hass.loop, async_active_zone, self.hass, device.location[DEVICE_LOCATION_LATITUDE], device.location[DEVICE_LOCATION_LONGITUDE], ).result() if current_zone is not None: intervals[device.name] = self._max_interval continue zones = ( self.hass.states.get(entity_id) for entity_id in sorted(self.hass.states.entity_ids("zone"))) distances = [] for zone_state in zones: zone_state_lat = zone_state.attributes[ DEVICE_LOCATION_LATITUDE] zone_state_long = zone_state.attributes[ DEVICE_LOCATION_LONGITUDE] zone_distance = distance( device.location[DEVICE_LOCATION_LATITUDE], device.location[DEVICE_LOCATION_LONGITUDE], zone_state_lat, zone_state_long, ) distances.append(round(zone_distance / 1000, 1)) if not distances: continue mindistance = min(distances) # Calculate out how long it would take for the device to drive # to the nearest zone at 120 km/h: interval = round(mindistance / 2, 0) # Never poll more than once per minute interval = max(interval, 1) if interval > 180: # Three hour drive? # This is far enough that they might be flying interval = self._max_interval if (device.battery_level is not None and device.battery_level <= 33 and mindistance > 3): # Low battery - let's check half as often interval = interval * 2 intervals[device.name] = interval return max( int(min(intervals.items(), key=operator.itemgetter(1))[1]), self._max_interval, ) def keep_alive(self, now=None) -> None: """Keep the API alive.""" if self.api is None: self.setup() if self.api is None: return self.api.authenticate() self.update_devices() def get_devices_with_name(self, name: str) -> [any]: """Get devices by name.""" result = [] name_slug = slugify(name.replace(" ", "", 99)) for device in self.devices.values(): if slugify(device.name.replace(" ", "", 99)) == name_slug: result.append(device) if not result: raise Exception(f"No device with name {name}") return result @property def name(self) -> str: """Return the account name.""" return self._name @property def username(self) -> str: """Return the account username.""" return self._username @property def owner_fullname(self) -> str: """Return the account owner fullname.""" return self._owner_fullname @property def family_members_fullname(self) -> Dict[str, str]: """Return the account family members fullname.""" return self._family_members_fullname @property def fetch_interval(self) -> int: """Return the account fetch interval.""" return self._fetch_interval @property def devices(self) -> Dict[str, any]: """Return the account devices.""" return self._devices
class IcloudAccount: """Representation of an iCloud account.""" def __init__( self, hass: HomeAssistantType, username: str, password: str, icloud_dir: Store, max_interval: int, gps_accuracy_threshold: int, ): """Initialize an iCloud account.""" self.hass = hass self._username = username self._password = password self._fetch_interval = max_interval self._max_interval = max_interval self._gps_accuracy_threshold = gps_accuracy_threshold self._icloud_dir = icloud_dir self.api: PyiCloudService = None self._owner_fullname = None self._family_members_fullname = {} self._devices = {} self.listeners = [] def setup(self) -> None: """Set up an iCloud account.""" try: self.api = PyiCloudService( self._username, self._password, self._icloud_dir.path ) except PyiCloudFailedLoginException as error: self.api = None _LOGGER.error("Error logging into iCloud Service: %s", error) return try: api_devices = self.api.devices # Gets device owners infos user_info = api_devices.response["userInfo"] except (KeyError, PyiCloudNoDevicesException): _LOGGER.error("No iCloud device found") 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._family_members_fullname = {} if user_info.get("membersInfo") is not None: for prs_id, member in user_info["membersInfo"].items(): self._family_members_fullname[ prs_id ] = f"{member['firstName']} {member['lastName']}" self._devices = {} self.update_devices() def update_devices(self) -> None: """Update iCloud devices.""" if self.api is None: return api_devices = {} try: api_devices = self.api.devices except Exception as err: # pylint: disable=broad-except _LOGGER.error("Unknown iCloud error: %s", err) self._fetch_interval = 2 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( self.hass, self.keep_alive, utcnow() + timedelta(minutes=self._fetch_interval), ) return # Gets devices infos new_device = False for device in api_devices: status = device.status(DEVICE_STATUS_SET) device_id = status[DEVICE_ID] 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: # Seen device -> updating _LOGGER.debug("Updating iCloud device: %s", device_name) self._devices[device_id].update(status) else: # New device, should be unique _LOGGER.debug( "Adding iCloud device: %s [model: %s]", device_name, status[DEVICE_RAW_DEVICE_MODEL], ) self._devices[device_id] = IcloudDevice(self, device, status) self._devices[device_id].update(status) new_device = True self._fetch_interval = self._determine_interval() dispatcher_send(self.hass, self.signal_device_update) if new_device: dispatcher_send(self.hass, self.signal_device_new) track_point_in_utc_time( self.hass, self.keep_alive, utcnow() + timedelta(minutes=self._fetch_interval), ) def _determine_interval(self) -> int: """Calculate new interval between two API fetch (in minutes).""" intervals = {"default": self._max_interval} for device in self._devices.values(): # Max interval if no location if device.location is None: continue current_zone = run_callback_threadsafe( self.hass.loop, async_active_zone, self.hass, device.location[DEVICE_LOCATION_LATITUDE], device.location[DEVICE_LOCATION_LONGITUDE], device.location[DEVICE_LOCATION_HORIZONTAL_ACCURACY], ).result() # Max interval if in zone if current_zone is not None: continue zones = ( self.hass.states.get(entity_id) for entity_id in sorted(self.hass.states.entity_ids("zone")) ) distances = [] for zone_state in zones: zone_state_lat = zone_state.attributes[DEVICE_LOCATION_LATITUDE] zone_state_long = zone_state.attributes[DEVICE_LOCATION_LONGITUDE] zone_distance = distance( device.location[DEVICE_LOCATION_LATITUDE], device.location[DEVICE_LOCATION_LONGITUDE], zone_state_lat, zone_state_long, ) distances.append(round(zone_distance / 1000, 1)) # Max interval if no zone if not distances: continue mindistance = min(distances) # Calculate out how long it would take for the device to drive # to the nearest zone at 120 km/h: interval = round(mindistance / 2, 0) # Never poll more than once per minute interval = max(interval, 1) if interval > 180: # Three hour drive? # This is far enough that they might be flying interval = self._max_interval if ( device.battery_level is not None and device.battery_level <= 33 and mindistance > 3 ): # Low battery - let's check half as often interval = interval * 2 intervals[device.name] = interval return max( int(min(intervals.items(), key=operator.itemgetter(1))[1]), self._max_interval, ) def keep_alive(self, now=None) -> None: """Keep the API alive.""" if self.api is None: self.setup() if self.api is None: return self.api.authenticate() self.update_devices() def get_devices_with_name(self, name: str) -> [any]: """Get devices by name.""" result = [] name_slug = slugify(name.replace(" ", "", 99)) for device in self.devices.values(): if slugify(device.name.replace(" ", "", 99)) == name_slug: result.append(device) if not result: raise Exception(f"No device with name {name}") return result @property def username(self) -> str: """Return the account username.""" return self._username @property def owner_fullname(self) -> str: """Return the account owner fullname.""" return self._owner_fullname @property def family_members_fullname(self) -> Dict[str, str]: """Return the account family members fullname.""" return self._family_members_fullname @property def fetch_interval(self) -> int: """Return the account fetch interval.""" return self._fetch_interval @property def devices(self) -> Dict[str, any]: """Return the account 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 IcloudAccount: """Representation of an iCloud account.""" def __init__( self, hass: HomeAssistant, username: str, password: str, icloud_dir: Store, with_family: bool, max_interval: int, gps_accuracy_threshold: int, config_entry: ConfigEntry, ) -> None: """Initialize an iCloud account.""" self.hass = hass self._username = username self._password = password self._with_family = with_family self._fetch_interval: float = max_interval self._max_interval = max_interval self._gps_accuracy_threshold = gps_accuracy_threshold self._icloud_dir = icloud_dir self.api: PyiCloudService | None = None self._owner_fullname: str | None = None self._family_members_fullname: dict[str, str] = {} self._devices: dict[str, IcloudDevice] = {} self._retried_fetch = False self._config_entry = config_entry self.listeners: list[CALLBACK_TYPE] = [] def setup(self) -> None: """Set up an iCloud account.""" try: self.api = PyiCloudService( self._username, self._password, self._icloud_dir.path, with_family=self._with_family, ) if self.api.requires_2fa: # Trigger a new log in to ensure the user enters the 2FA code again. raise PyiCloudFailedLoginException except PyiCloudFailedLoginException: self.api = None # Login failed which means credentials need to be updated. _LOGGER.error( ("Your password for '%s' is no longer working; Go to the " "Integrations menu and click on Configure on the discovered Apple " "iCloud card to login again"), self._config_entry.data[CONF_USERNAME], ) self._require_reauth() return try: api_devices = self.api.devices # Gets device owners infos user_info = api_devices.response["userInfo"] except ( PyiCloudServiceNotActivatedException, PyiCloudNoDevicesException, ) as err: _LOGGER.error("No iCloud device found") raise ConfigEntryNotReady from err self._owner_fullname = f"{user_info['firstName']} {user_info['lastName']}" self._family_members_fullname = {} if user_info.get("membersInfo") is not None: for prs_id, member in user_info["membersInfo"].items(): self._family_members_fullname[ prs_id] = f"{member['firstName']} {member['lastName']}" self._devices = {} self.update_devices() def update_devices(self) -> None: """Update iCloud devices.""" if self.api is None: return if self.api.requires_2fa: self._require_reauth() return api_devices = {} try: api_devices = self.api.devices except Exception as err: # pylint: disable=broad-except _LOGGER.error("Unknown iCloud error: %s", err) self._fetch_interval = 2 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 # Gets devices infos new_device = False for device in api_devices: status = device.status(DEVICE_STATUS_SET) device_id = status[DEVICE_ID] device_name = status[DEVICE_NAME] if (status[DEVICE_BATTERY_STATUS] == "Unknown" or status.get(DEVICE_BATTERY_LEVEL) is None): continue if self._devices.get(device_id) is not None: # Seen device -> updating _LOGGER.debug("Updating iCloud device: %s", device_name) self._devices[device_id].update(status) else: # New device, should be unique _LOGGER.debug( "Adding iCloud device: %s [model: %s]", device_name, status[DEVICE_RAW_DEVICE_MODEL], ) self._devices[device_id] = IcloudDevice(self, device, status) self._devices[device_id].update(status) new_device = True if (DEVICE_STATUS_CODES.get(list(api_devices)[0][DEVICE_STATUS]) == "pending" and not self._retried_fetch): _LOGGER.debug("Pending devices, trying again in 15s") self._fetch_interval = 0.25 self._retried_fetch = True else: self._fetch_interval = self._determine_interval() self._retried_fetch = False dispatcher_send(self.hass, self.signal_device_update) if new_device: dispatcher_send(self.hass, self.signal_device_new) track_point_in_utc_time( self.hass, self.keep_alive, utcnow() + timedelta(minutes=self._fetch_interval), ) def _require_reauth(self): """Require the user to log in again.""" self.hass.add_job( self.hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_REAUTH}, data={ **self._config_entry.data, "unique_id": self._config_entry.unique_id, }, )) def _determine_interval(self) -> int: """Calculate new interval between two API fetch (in minutes).""" intervals = {"default": self._max_interval} for device in self._devices.values(): # Max interval if no location if device.location is None: continue current_zone = run_callback_threadsafe( self.hass.loop, async_active_zone, self.hass, device.location[DEVICE_LOCATION_LATITUDE], device.location[DEVICE_LOCATION_LONGITUDE], device.location[DEVICE_LOCATION_HORIZONTAL_ACCURACY], ).result() # Max interval if in zone if current_zone is not None: continue zones = ( self.hass.states.get(entity_id) for entity_id in sorted(self.hass.states.entity_ids("zone"))) distances = [] for zone_state in zones: if zone_state is None: continue zone_state_lat = zone_state.attributes[ DEVICE_LOCATION_LATITUDE] zone_state_long = zone_state.attributes[ DEVICE_LOCATION_LONGITUDE] zone_distance = distance( device.location[DEVICE_LOCATION_LATITUDE], device.location[DEVICE_LOCATION_LONGITUDE], zone_state_lat, zone_state_long, ) if zone_distance is not None: distances.append(round(zone_distance / 1000, 1)) # Max interval if no zone if not distances: continue mindistance = min(distances) # Calculate out how long it would take for the device to drive # to the nearest zone at 120 km/h: interval = round(mindistance / 2) # Never poll more than once per minute interval = max(interval, 1) if interval > 180: # Three hour drive? # This is far enough that they might be flying interval = self._max_interval if (device.battery_level is not None and device.battery_level <= 33 and mindistance > 3): # Low battery - let's check half as often interval = interval * 2 intervals[device.name] = interval return max( int(min(intervals.items(), key=operator.itemgetter(1))[1]), self._max_interval, ) def keep_alive(self, now=None) -> None: """Keep the API alive.""" if self.api is None: self.setup() if self.api is None: return self.api.authenticate() self.update_devices() def get_devices_with_name(self, name: str) -> list[Any]: """Get devices by name.""" result = [] name_slug = slugify(name.replace(" ", "", 99)) for device in self.devices.values(): if slugify(device.name.replace(" ", "", 99)) == name_slug: result.append(device) if not result: raise Exception(f"No device with name {name}") return result @property def username(self) -> str: """Return the account username.""" return self._username @property def owner_fullname(self) -> str | None: """Return the account owner fullname.""" return self._owner_fullname @property def family_members_fullname(self) -> dict[str, str]: """Return the account family members fullname.""" return self._family_members_fullname @property def fetch_interval(self) -> float: """Return the account fetch interval.""" return self._fetch_interval @property def devices(self) -> dict[str, Any]: """Return the account 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 Icloud(DeviceScanner): """Representation of an iCloud account.""" def __init__(self, hass, username, password, name, max_interval, gps_accuracy_threshold, see): """Initialize an iCloud account.""" self.hass = hass self.username = username self.password = password self.api = None self.accountname = name self.devices = {} self.seen_devices = {} self._overridestates = {} self._intervals = {} self._max_interval = max_interval self._gps_accuracy_threshold = gps_accuracy_threshold self.see = see self._trusted_device = None self._verification_code = None self._attrs = {} self._attrs[ATTR_ACCOUNTNAME] = name self.reset_account_icloud() randomseconds = random.randint(10, 59) track_utc_time_change(self.hass, self.keep_alive, second=randomseconds) def reset_account_icloud(self): """Reset an iCloud account.""" from pyicloud import PyiCloudService from pyicloud.exceptions import ( PyiCloudFailedLoginException, PyiCloudNoDevicesException, ) icloud_dir = self.hass.config.path("icloud") if not os.path.exists(icloud_dir): os.makedirs(icloud_dir) try: self.api = PyiCloudService(self.username, self.password, cookie_directory=icloud_dir, verify=True) except PyiCloudFailedLoginException as error: self.api = None _LOGGER.error("Error logging into iCloud Service: %s", error) return try: self.devices = {} self._overridestates = {} self._intervals = {} for device in self.api.devices: status = device.status(DEVICESTATUSSET) _LOGGER.debug("Device Status is %s", status) devicename = slugify(status["name"].replace(" ", "", 99)) _LOGGER.info("Adding icloud device: %s", devicename) if devicename in self.devices: _LOGGER.error("Multiple devices with name: %s", devicename) continue self.devices[devicename] = device self._intervals[devicename] = 1 self._overridestates[devicename] = None except PyiCloudNoDevicesException: _LOGGER.error("No iCloud Devices found!") def icloud_trusted_device_callback(self, callback_data): """Handle chosen trusted devices.""" self._trusted_device = int(callback_data.get("trusted_device")) self._trusted_device = self.api.trusted_devices[self._trusted_device] if not self.api.send_verification_code(self._trusted_device): _LOGGER.error("Failed to send verification code") self._trusted_device = None return if self.accountname in _CONFIGURING: request_id = _CONFIGURING.pop(self.accountname) configurator = self.hass.components.configurator configurator.request_done(request_id) # Trigger the next step immediately self.icloud_need_verification_code() def icloud_need_trusted_device(self): """We need a trusted device.""" configurator = self.hass.components.configurator if self.accountname in _CONFIGURING: return devicesstring = "" devices = self.api.trusted_devices for i, device in enumerate(devices): devicename = device.get("deviceName", "SMS to %s" % device.get("phoneNumber")) devicesstring += "{}: {};".format(i, devicename) _CONFIGURING[self.accountname] = configurator.request_config( "iCloud {}".format(self.accountname), self.icloud_trusted_device_callback, description=("Please choose your trusted device by entering" " the index from this list: " + devicesstring), entity_picture="/static/images/config_icloud.png", submit_caption="Confirm", fields=[{ "id": "trusted_device", "name": "Trusted Device" }], ) def icloud_verification_callback(self, callback_data): """Handle the chosen trusted device.""" from pyicloud.exceptions import PyiCloudException self._verification_code = callback_data.get("code") try: if not self.api.validate_verification_code( self._trusted_device, self._verification_code): raise PyiCloudException("Unknown failure") except PyiCloudException as error: # Reset to the initial 2FA state to allow the user to retry _LOGGER.error("Failed to verify verification code: %s", error) self._trusted_device = None self._verification_code = None # Trigger the next step immediately self.icloud_need_trusted_device() if self.accountname in _CONFIGURING: request_id = _CONFIGURING.pop(self.accountname) configurator = self.hass.components.configurator configurator.request_done(request_id) def icloud_need_verification_code(self): """Return the verification code.""" configurator = self.hass.components.configurator if self.accountname in _CONFIGURING: return _CONFIGURING[self.accountname] = configurator.request_config( "iCloud {}".format(self.accountname), self.icloud_verification_callback, description=("Please enter the validation code:"), entity_picture="/static/images/config_icloud.png", submit_caption="Confirm", fields=[{ "id": "code", "name": "code" }], ) def keep_alive(self, now): """Keep the API alive.""" if self.api is None: self.reset_account_icloud() if self.api is None: return if self.api.requires_2fa: from pyicloud.exceptions import PyiCloudException try: if self._trusted_device is None: self.icloud_need_trusted_device() return if self._verification_code is None: self.icloud_need_verification_code() return self.api.authenticate() if self.api.requires_2fa: raise Exception("Unknown failure") self._trusted_device = None self._verification_code = None except PyiCloudException as error: _LOGGER.error("Error setting up 2FA: %s", error) else: self.api.authenticate() currentminutes = dt_util.now().hour * 60 + dt_util.now().minute try: for devicename in self.devices: interval = self._intervals.get(devicename, 1) if (currentminutes % interval == 0) or (interval > 10 and currentminutes % interval in [2, 4]): self.update_device(devicename) except ValueError: _LOGGER.debug("iCloud API returned an error") def determine_interval(self, devicename, latitude, longitude, battery): """Calculate new interval.""" currentzone = run_callback_threadsafe(self.hass.loop, async_active_zone, self.hass, latitude, longitude).result() if (currentzone is not None and currentzone == self._overridestates.get(devicename)) or ( currentzone is None and self._overridestates.get(devicename) == "away"): return zones = (self.hass.states.get(entity_id) for entity_id in sorted(self.hass.states.entity_ids("zone"))) distances = [] for zone_state in zones: zone_state_lat = zone_state.attributes["latitude"] zone_state_long = zone_state.attributes["longitude"] zone_distance = distance(latitude, longitude, zone_state_lat, zone_state_long) distances.append(round(zone_distance / 1000, 1)) if distances: mindistance = min(distances) else: mindistance = None self._overridestates[devicename] = None if currentzone is not None: self._intervals[devicename] = self._max_interval return if mindistance is None: return # Calculate out how long it would take for the device to drive to the # nearest zone at 120 km/h: interval = round(mindistance / 2, 0) # Never poll more than once per minute interval = max(interval, 1) if interval > 180: # Three hour drive? This is far enough that they might be flying interval = 30 if battery is not None and battery <= 33 and mindistance > 3: # Low battery - let's check half as often interval = interval * 2 self._intervals[devicename] = interval def update_device(self, devicename): """Update the device_tracker entity.""" from pyicloud.exceptions import PyiCloudNoDevicesException # An entity will not be created by see() when track=false in # 'known_devices.yaml', but we need to see() it at least once entity = self.hass.states.get(ENTITY_ID_FORMAT.format(devicename)) if entity is None and devicename in self.seen_devices: return attrs = {} kwargs = {} if self.api is None: return try: for device in self.api.devices: if str(device) != str(self.devices[devicename]): continue status = device.status(DEVICESTATUSSET) _LOGGER.debug("Device Status is %s", status) dev_id = status["name"].replace(" ", "", 99) dev_id = slugify(dev_id) attrs[ATTR_DEVICESTATUS] = DEVICESTATUSCODES.get( status["deviceStatus"], "error") attrs[ATTR_LOWPOWERMODE] = status["lowPowerMode"] attrs[ATTR_BATTERYSTATUS] = status["batteryStatus"] attrs[ATTR_ACCOUNTNAME] = self.accountname status = device.status(DEVICESTATUSSET) battery = status.get("batteryLevel", 0) * 100 location = status["location"] if location and location["horizontalAccuracy"]: horizontal_accuracy = int(location["horizontalAccuracy"]) if horizontal_accuracy < self._gps_accuracy_threshold: self.determine_interval( devicename, location["latitude"], location["longitude"], battery, ) interval = self._intervals.get(devicename, 1) attrs[ATTR_INTERVAL] = interval accuracy = location["horizontalAccuracy"] kwargs["dev_id"] = dev_id kwargs["host_name"] = status["name"] kwargs["gps"] = (location["latitude"], location["longitude"]) kwargs["battery"] = battery kwargs["gps_accuracy"] = accuracy kwargs[ATTR_ATTRIBUTES] = attrs self.see(**kwargs) self.seen_devices[devicename] = True except PyiCloudNoDevicesException: _LOGGER.error("No iCloud Devices found") def lost_iphone(self, devicename): """Call the lost iPhone function if the device is found.""" if self.api is None: return self.api.authenticate() for device in self.api.devices: if str(device) == str(self.devices[devicename]): _LOGGER.info("Playing Lost iPhone sound for %s", devicename) device.play_sound() def update_icloud(self, devicename=None): """Request device information from iCloud and update device_tracker.""" from pyicloud.exceptions import PyiCloudNoDevicesException if self.api is None: return try: if devicename is not None: if devicename in self.devices: self.update_device(devicename) else: _LOGGER.error( "devicename %s unknown for account %s", devicename, self._attrs[ATTR_ACCOUNTNAME], ) else: for device in self.devices: self.update_device(device) except PyiCloudNoDevicesException: _LOGGER.error("No iCloud Devices found") def setinterval(self, interval=None, devicename=None): """Set the interval of the given devices.""" devs = [devicename] if devicename else self.devices for device in devs: devid = "{}.{}".format(DOMAIN, device) devicestate = self.hass.states.get(devid) if interval is not None: if devicestate is not None: self._overridestates[device] = run_callback_threadsafe( self.hass.loop, async_active_zone, self.hass, float(devicestate.attributes.get("latitude", 0)), float(devicestate.attributes.get("longitude", 0)), ).result() if self._overridestates[device] is None: self._overridestates[device] = "away" self._intervals[device] = interval else: self._overridestates[device] = None self.update_device(device)
class Icloud(DeviceScanner): """Representation of an iCloud account.""" def __init__(self, hass, username, password, name, see): """Initialize an iCloud account.""" self.hass = hass self.username = username self.password = password self.api = None self.accountname = name self.devices = {} self.seen_devices = {} self._overridestates = {} self._intervals = {} self.see = see self._trusted_device = None self._verification_code = None self._attrs = {} self._attrs[ATTR_ACCOUNTNAME] = name self.reset_account_icloud() randomseconds = random.randint(10, 59) track_utc_time_change( self.hass, self.keep_alive, second=randomseconds) def reset_account_icloud(self): """Reset an iCloud account.""" from pyicloud import PyiCloudService from pyicloud.exceptions import ( PyiCloudFailedLoginException, PyiCloudNoDevicesException) icloud_dir = self.hass.config.path('icloud') if not os.path.exists(icloud_dir): os.makedirs(icloud_dir) try: self.api = PyiCloudService( self.username, self.password, cookie_directory=icloud_dir, verify=True) except PyiCloudFailedLoginException as error: self.api = None _LOGGER.error("Error logging into iCloud Service: %s", error) return try: self.devices = {} self._overridestates = {} self._intervals = {} for device in self.api.devices: status = device.status(DEVICESTATUSSET) devicename = slugify(status['name'].replace(' ', '', 99)) if devicename not in self.devices: self.devices[devicename] = device self._intervals[devicename] = 1 self._overridestates[devicename] = None except PyiCloudNoDevicesException: _LOGGER.error('No iCloud Devices found!') def icloud_trusted_device_callback(self, callback_data): """Handle chosen trusted devices.""" self._trusted_device = int(callback_data.get('trusted_device')) self._trusted_device = self.api.trusted_devices[self._trusted_device] if not self.api.send_verification_code(self._trusted_device): _LOGGER.error("Failed to send verification code") self._trusted_device = None return if self.accountname in _CONFIGURING: request_id = _CONFIGURING.pop(self.accountname) configurator = self.hass.components.configurator configurator.request_done(request_id) # Trigger the next step immediately self.icloud_need_verification_code() def icloud_need_trusted_device(self): """We need a trusted device.""" configurator = self.hass.components.configurator if self.accountname in _CONFIGURING: return devicesstring = '' devices = self.api.trusted_devices for i, device in enumerate(devices): devicename = device.get( 'deviceName', 'SMS to %s' % device.get('phoneNumber')) devicesstring += "{}: {};".format(i, devicename) _CONFIGURING[self.accountname] = configurator.request_config( 'iCloud {}'.format(self.accountname), self.icloud_trusted_device_callback, description=( 'Please choose your trusted device by entering' ' the index from this list: ' + devicesstring), entity_picture="/static/images/config_icloud.png", submit_caption='Confirm', fields=[{'id': 'trusted_device', 'name': 'Trusted Device'}] ) def icloud_verification_callback(self, callback_data): """Handle the chosen trusted device.""" from pyicloud.exceptions import PyiCloudException self._verification_code = callback_data.get('code') try: if not self.api.validate_verification_code( self._trusted_device, self._verification_code): raise PyiCloudException('Unknown failure') except PyiCloudException as error: # Reset to the initial 2FA state to allow the user to retry _LOGGER.error("Failed to verify verification code: %s", error) self._trusted_device = None self._verification_code = None # Trigger the next step immediately self.icloud_need_trusted_device() if self.accountname in _CONFIGURING: request_id = _CONFIGURING.pop(self.accountname) configurator = self.hass.components.configurator configurator.request_done(request_id) def icloud_need_verification_code(self): """Return the verification code.""" configurator = self.hass.components.configurator if self.accountname in _CONFIGURING: return _CONFIGURING[self.accountname] = configurator.request_config( 'iCloud {}'.format(self.accountname), self.icloud_verification_callback, description=('Please enter the validation code:'), entity_picture="/static/images/config_icloud.png", submit_caption='Confirm', fields=[{'id': 'code', 'name': 'code'}] ) def keep_alive(self, now): """Keep the API alive.""" if self.api is None: self.reset_account_icloud() if self.api is None: return if self.api.requires_2fa: from pyicloud.exceptions import PyiCloudException try: if self._trusted_device is None: self.icloud_need_trusted_device() return if self._verification_code is None: self.icloud_need_verification_code() return self.api.authenticate() if self.api.requires_2fa: raise Exception('Unknown failure') self._trusted_device = None self._verification_code = None except PyiCloudException as error: _LOGGER.error("Error setting up 2FA: %s", error) else: self.api.authenticate() currentminutes = dt_util.now().hour * 60 + dt_util.now().minute try: for devicename in self.devices: interval = self._intervals.get(devicename, 1) if ((currentminutes % interval == 0) or (interval > 10 and currentminutes % interval in [2, 4])): self.update_device(devicename) except ValueError: _LOGGER.debug("iCloud API returned an error") def determine_interval(self, devicename, latitude, longitude, battery): """Calculate new interval.""" distancefromhome = None zone_state = self.hass.states.get('zone.home') zone_state_lat = zone_state.attributes['latitude'] zone_state_long = zone_state.attributes['longitude'] distancefromhome = distance( latitude, longitude, zone_state_lat, zone_state_long) distancefromhome = round(distancefromhome / 1000, 1) currentzone = active_zone(self.hass, latitude, longitude) if ((currentzone is not None and currentzone == self._overridestates.get(devicename)) or (currentzone is None and self._overridestates.get(devicename) == 'away')): return self._overridestates[devicename] = None if currentzone is not None: self._intervals[devicename] = 30 return if distancefromhome is None: return if distancefromhome > 25: self._intervals[devicename] = round(distancefromhome / 2, 0) elif distancefromhome > 10: self._intervals[devicename] = 5 else: self._intervals[devicename] = 1 if battery is not None and battery <= 33 and distancefromhome > 3: self._intervals[devicename] = self._intervals[devicename] * 2 def update_device(self, devicename): """Update the device_tracker entity.""" from pyicloud.exceptions import PyiCloudNoDevicesException # An entity will not be created by see() when track=false in # 'known_devices.yaml', but we need to see() it at least once entity = self.hass.states.get(ENTITY_ID_FORMAT.format(devicename)) if entity is None and devicename in self.seen_devices: return attrs = {} kwargs = {} if self.api is None: return try: for device in self.api.devices: if str(device) != str(self.devices[devicename]): continue status = device.status(DEVICESTATUSSET) dev_id = status['name'].replace(' ', '', 99) dev_id = slugify(dev_id) attrs[ATTR_DEVICESTATUS] = DEVICESTATUSCODES.get( status['deviceStatus'], 'error') attrs[ATTR_LOWPOWERMODE] = status['lowPowerMode'] attrs[ATTR_BATTERYSTATUS] = status['batteryStatus'] attrs[ATTR_ACCOUNTNAME] = self.accountname status = device.status(DEVICESTATUSSET) battery = status.get('batteryLevel', 0) * 100 location = status['location'] if location: self.determine_interval( devicename, location['latitude'], location['longitude'], battery) interval = self._intervals.get(devicename, 1) attrs[ATTR_INTERVAL] = interval accuracy = location['horizontalAccuracy'] kwargs['dev_id'] = dev_id kwargs['host_name'] = status['name'] kwargs['gps'] = (location['latitude'], location['longitude']) kwargs['battery'] = battery kwargs['gps_accuracy'] = accuracy kwargs[ATTR_ATTRIBUTES] = attrs self.see(**kwargs) self.seen_devices[devicename] = True except PyiCloudNoDevicesException: _LOGGER.error("No iCloud Devices found") def lost_iphone(self, devicename): """Call the lost iPhone function if the device is found.""" if self.api is None: return self.api.authenticate() for device in self.api.devices: if devicename is None or device == self.devices[devicename]: device.play_sound() def update_icloud(self, devicename=None): """Authenticate against iCloud and scan for devices.""" from pyicloud.exceptions import PyiCloudNoDevicesException if self.api is None: return try: if devicename is not None: if devicename in self.devices: self.devices[devicename].location() else: _LOGGER.error("devicename %s unknown for account %s", devicename, self._attrs[ATTR_ACCOUNTNAME]) else: for device in self.devices: self.devices[device].location() except PyiCloudNoDevicesException: _LOGGER.error("No iCloud Devices found") def setinterval(self, interval=None, devicename=None): """Set the interval of the given devices.""" devs = [devicename] if devicename else self.devices for device in devs: devid = '{}.{}'.format(DOMAIN, device) devicestate = self.hass.states.get(devid) if interval is not None: if devicestate is not None: self._overridestates[device] = active_zone( self.hass, float(devicestate.attributes.get('latitude', 0)), float(devicestate.attributes.get('longitude', 0))) if self._overridestates[device] is None: self._overridestates[device] = 'away' self._intervals[device] = interval else: self._overridestates[device] = None self.update_device(device)
class Icloud(object): """Represent an icloud account in Home Assistant.""" def __init__(self, hass, username, password, name, see): """Initialize an iCloud account.""" self.hass = hass self.username = username self.password = password self.api = None self.accountname = name self.devices = {} self.seen_devices = {} self._overridestates = {} self._intervals = {} self.see = see self._trusted_device = None self._verification_code = None self._attrs = {} self._attrs[ATTR_ACCOUNTNAME] = name self.reset_account_icloud() randomseconds = random.randint(10, 59) track_utc_time_change(self.hass, self.keep_alive, second=randomseconds) def reset_account_icloud(self): """Reset an icloud account.""" from pyicloud import PyiCloudService from pyicloud.exceptions import PyiCloudFailedLoginException, PyiCloudNoDevicesException icloud_dir = self.hass.config.path("icloud") if not os.path.exists(icloud_dir): os.makedirs(icloud_dir) try: self.api = PyiCloudService(self.username, self.password, cookie_directory=icloud_dir, verify=True) except PyiCloudFailedLoginException as error: self.api = None _LOGGER.error("Error logging into iCloud Service: %s", error) return try: self.devices = {} self._overridestates = {} self._intervals = {} for device in self.api.devices: status = device.status(DEVICESTATUSSET) devicename = slugify(status["name"].replace(" ", "", 99)) if devicename not in self.devices: self.devices[devicename] = device self._intervals[devicename] = 1 self._overridestates[devicename] = None except PyiCloudNoDevicesException: _LOGGER.error("No iCloud Devices found!") def icloud_trusted_device_callback(self, callback_data): """The trusted device is chosen.""" self._trusted_device = int(callback_data.get("0", "0")) self._trusted_device = self.api.trusted_devices[self._trusted_device] if self.accountname in _CONFIGURING: request_id = _CONFIGURING.pop(self.accountname) configurator = get_component("configurator") configurator.request_done(request_id) def icloud_need_trusted_device(self): """We need a trusted device.""" configurator = get_component("configurator") if self.accountname in _CONFIGURING: return devicesstring = "" devices = self.api.trusted_devices for i, device in enumerate(devices): devicesstring += "{}: {};".format(i, device.get("deviceName")) _CONFIGURING[self.accountname] = configurator.request_config( self.hass, "iCloud {}".format(self.accountname), self.icloud_trusted_device_callback, description=("Please choose your trusted device by entering" " the index from this list: " + devicesstring), entity_picture="/static/images/config_icloud.png", submit_caption="Confirm", fields=[{"id": "0"}], ) def icloud_verification_callback(self, callback_data): """The trusted device is chosen.""" self._verification_code = callback_data.get("0") if self.accountname in _CONFIGURING: request_id = _CONFIGURING.pop(self.accountname) configurator = get_component("configurator") configurator.request_done(request_id) def icloud_need_verification_code(self): """We need a verification code.""" configurator = get_component("configurator") if self.accountname in _CONFIGURING: return if self.api.send_verification_code(self._trusted_device): self._verification_code = "waiting" _CONFIGURING[self.accountname] = configurator.request_config( self.hass, "iCloud {}".format(self.accountname), self.icloud_verification_callback, description=("Please enter the validation code:"), entity_picture="/static/images/config_icloud.png", submit_caption="Confirm", fields=[{"code": "0"}], ) def keep_alive(self, now): """Keep the api alive.""" from pyicloud.exceptions import PyiCloud2FARequiredError if self.api is None: self.reset_account_icloud() if self.api is None: return if self.api.requires_2fa: try: self.api.authenticate() except PyiCloud2FARequiredError: if self._trusted_device is None: self.icloud_need_trusted_device() return if self._verification_code is None: self.icloud_need_verification_code() return if self._verification_code == "waiting": return if self.api.validate_verification_code(self._trusted_device, self._verification_code): self._verification_code = None else: self.api.authenticate() currentminutes = dt_util.now().hour * 60 + dt_util.now().minute for devicename in self.devices: interval = self._intervals.get(devicename, 1) if (currentminutes % interval == 0) or (interval > 10 and currentminutes % interval in [2, 4]): self.update_device(devicename) def determine_interval(self, devicename, latitude, longitude, battery): """Calculate new interval.""" distancefromhome = None zone_state = self.hass.states.get("zone.home") zone_state_lat = zone_state.attributes["latitude"] zone_state_long = zone_state.attributes["longitude"] distancefromhome = distance(latitude, longitude, zone_state_lat, zone_state_long) distancefromhome = round(distancefromhome / 1000, 1) currentzone = active_zone(self.hass, latitude, longitude) if (currentzone is not None and currentzone == self._overridestates.get(devicename)) or ( currentzone is None and self._overridestates.get(devicename) == "away" ): return self._overridestates[devicename] = None if currentzone is not None: self._intervals[devicename] = 30 return if distancefromhome is None: return if distancefromhome > 25: self._intervals[devicename] = round(distancefromhome / 2, 0) elif distancefromhome > 10: self._intervals[devicename] = 5 else: self._intervals[devicename] = 1 if battery is not None and battery <= 33 and distancefromhome > 3: self._intervals[devicename] = self._intervals[devicename] * 2 def update_device(self, devicename): """Update the device_tracker entity.""" from pyicloud.exceptions import PyiCloudNoDevicesException # An entity will not be created by see() when track=false in # 'known_devices.yaml', but we need to see() it at least once entity = self.hass.states.get(ENTITY_ID_FORMAT.format(devicename)) if entity is None and devicename in self.seen_devices: return attrs = {} kwargs = {} if self.api is None: return try: for device in self.api.devices: if str(device) != str(self.devices[devicename]): continue status = device.status(DEVICESTATUSSET) dev_id = status["name"].replace(" ", "", 99) dev_id = slugify(dev_id) attrs[ATTR_DEVICESTATUS] = DEVICESTATUSCODES.get(status["deviceStatus"], "error") attrs[ATTR_LOWPOWERMODE] = status["lowPowerMode"] attrs[ATTR_BATTERYSTATUS] = status["batteryStatus"] attrs[ATTR_ACCOUNTNAME] = self.accountname status = device.status(DEVICESTATUSSET) battery = status.get("batteryLevel", 0) * 100 location = status["location"] if location: self.determine_interval(devicename, location["latitude"], location["longitude"], battery) interval = self._intervals.get(devicename, 1) attrs[ATTR_INTERVAL] = interval accuracy = location["horizontalAccuracy"] kwargs["dev_id"] = dev_id kwargs["host_name"] = status["name"] kwargs["gps"] = (location["latitude"], location["longitude"]) kwargs["battery"] = battery kwargs["gps_accuracy"] = accuracy kwargs[ATTR_ATTRIBUTES] = attrs self.see(**kwargs) self.seen_devices[devicename] = True except PyiCloudNoDevicesException: _LOGGER.error("No iCloud Devices found!") def lost_iphone(self, devicename): """Call the lost iphone function if the device is found.""" if self.api is None: return self.api.authenticate() for device in self.api.devices: if devicename is None or device == self.devices[devicename]: device.play_sound() def update_icloud(self, devicename=None): """Authenticate against iCloud and scan for devices.""" from pyicloud.exceptions import PyiCloudNoDevicesException if self.api is None: return try: if devicename is not None: if devicename in self.devices: self.devices[devicename].update_icloud() else: _LOGGER.error("devicename %s unknown for account %s", devicename, self._attrs[ATTR_ACCOUNTNAME]) else: for device in self.devices: self.devices[device].update_icloud() except PyiCloudNoDevicesException: _LOGGER.error("No iCloud Devices found!") def setinterval(self, interval=None, devicename=None): """Set the interval of the given devices.""" devs = [devicename] if devicename else self.devices for device in devs: devid = DOMAIN + "." + device devicestate = self.hass.states.get(devid) if interval is not None: if devicestate is not None: self._overridestates[device] = active_zone( self.hass, float(devicestate.attributes.get("latitude", 0)), float(devicestate.attributes.get("longitude", 0)), ) if self._overridestates[device] is None: self._overridestates[device] = "away" self._intervals[device] = interval else: self._overridestates[device] = None self.update_device(device)
class Icloud(Entity): # pylint: disable=too-many-instance-attributes """ Represents a Proximity in Home Assistant. """ def __init__(self, hass, username, password, name, ignored_devices, getevents): # pylint: disable=too-many-arguments self.hass = hass self.username = username self.password = password self.accountname = name self._max_wait_seconds = 120 self._request_interval_seconds = 10 self._interval = 1 self.api = None self.devices = {} self.getevents = getevents self.events = {} self.currentevents = {} self.nextevents = {} self._ignored_devices = ignored_devices self._ignored_identifiers = {} self.entity_id = generate_entity_id( ENTITY_ID_FORMAT_ICLOUD, self.accountname, hass=self.hass) if self.username is None or self.password is None: _LOGGER.error('Must specify a username and password') else: try: # Attempt the login to iCloud self.api = PyiCloudService(self.username, self.password, verify=True) for device in self.api.devices: status = device.status(DEVICESTATUSSET) devicename = re.sub(r"(\s|\W|')", '', status['name']).lower() if (devicename not in self.devices and devicename not in self._ignored_devices): idevice = IDevice(self.hass, self, devicename, device) idevice.update_ha_state() self.devices[devicename] = idevice elif devicename in self._ignored_devices: self._ignored_identifiers[devicename] = device if self.getevents: from_dt = dt_util.now() to_dt = from_dt + timedelta(days=7) events = self.api.calendar.events(from_dt, to_dt) new_events = sorted(events.list_of_dict, key=operator.attrgetter('startDate')) starttime = None endtime = None duration = None title = None tz = pytz.utc location = None guid = None for event in new_events: tz = event['tz'] if tz is None: tz = pytz.utc else: tz = timezone(tz) tempnow = dt_util.now(tz) guid = event['guid'] starttime = event['startDate'] startdate = datetime(starttime[1], starttime[2], starttime[3], starttime[4], starttime[5], 0, 0, tz) endtime = event['endDate'] enddate = datetime(endtime[1], endtime[2], endtime[3], endtime[4], endtime[5], 0, 0, tz) duration = event['duration'] title = event['title'] location = event['location'] strnow = tempnow.strftime("%Y%m%d%H%M%S") strstart = startdate.strftime("%Y%m%d%H%M%S") strend = enddate.strftime("%Y%m%d%H%M%S") if strnow > strstart and strend > strnow: ievent = IEvent(self.hass, self, guid, TYPE_CURRENT) ievent.update_ha_state() self.currentevents[guid] = ievent self.currentevents[guid].keep_alive(starttime, endtime, duration, title, tz, location) starttime = None endtime = None duration = None title = None tz = pytz.utc location = None guid = None starttime = None endtime = None duration = None title = None tz = pytz.utc location = None guid = None for event in new_events: tz = event['tz'] if tz is None: tz = pytz.utc else: tz = timezone(tz) tempnow = dt_util.now(tz) guid = event['guid'] starttime = event['startDate'] startdate = datetime(starttime[1], starttime[2], starttime[3], starttime[4], starttime[5], 0, 0, tz) endtime = event['endDate'] enddate = datetime(endtime[1], endtime[2], endtime[3], endtime[4], endtime[5], 0, 0, tz) duration = event['duration'] title = event['title'] location = event['location'] strnow = tempnow.strftime("%Y%m%d%H%M%S") strstart = startdate.strftime("%Y%m%d%H%M%S") strend = enddate.strftime("%Y%m%d%H%M%S") if strnow < strstart: ievent = IEvent(self.hass, self, guid, TYPE_NEXT) ievent.update_ha_state() self.nextevents[guid] = ievent self.nextevents[guid].keep_alive(starttime, endtime, duration, title, tz, location) except PyiCloudFailedLoginException as error: _LOGGER.error('Error logging into iCloud Service: %s', error) @property def state(self): """ returns the state of the icloud tracker """ return self.api is not None @property def state_attributes(self): """ returns the friendlyname of the icloud tracker """ return { ATTR_ACCOUNTNAME: self.accountname } @property def icon(self): """Return the icon to use for device if any.""" return 'mdi:account' def keep_alive(self): """ Keeps the api alive """ if self.api is None: try: # Attempt the login to iCloud self.api = PyiCloudService(self.username, self.password, verify=True) except PyiCloudFailedLoginException as error: _LOGGER.error('Error logging into iCloud Service: %s', error) if self.api is not None: self.api.authenticate() for devicename in self.devices: self.devices[devicename].keep_alive() if self.getevents: from_dt = dt_util.now() to_dt = from_dt + timedelta(days=7) events = self.api.calendar.events(from_dt, to_dt) new_events = sorted(events.list_of_dict, key=operator.attrgetter('startDate')) starttime = None endtime = None duration = None title = None tz = pytz.utc location = None guid = None for event in new_events: tz = event['tz'] if tz is None: tz = pytz.utc else: tz = timezone(tz) tempnow = dt_util.now(tz) guid = event['guid'] starttime = event['startDate'] startdate = datetime(starttime[1], starttime[2], starttime[3], starttime[4], starttime[5], 0, 0, tz) endtime = event['endDate'] enddate = datetime(endtime[1], endtime[2], endtime[3], endtime[4], endtime[5], 0, 0, tz) duration = event['duration'] title = event['title'] location = event['location'] strnow = tempnow.strftime("%Y%m%d%H%M%S") strstart = startdate.strftime("%Y%m%d%H%M%S") strend = enddate.strftime("%Y%m%d%H%M%S") if strnow > strstart and strend > strnow: if guid not in self.currentevents: ievent = IEvent(self.hass, self, guid, TYPE_CURRENT) ievent.update_ha_state() self.currentevents[guid] = ievent self.currentevents[guid].keep_alive(starttime, endtime, duration, title, tz, location) starttime = None endtime = None duration = None title = None tz = pytz.utc location = None guid = None for addedevent in self.currentevents: found = False eventguid = self.currentevents[addedevent].eventguid for event in new_events: if event['guid'] == eventguid: found = True if not found: ent_id = generate_entity_id(ENTITY_ID_FORMAT_EVENT, eventguid, hass=self.hass) self.hass.states.remove(ent_id) del self.currentevents[addedevent] else: self.currentevents[addedevent].check_alive() starttime = None endtime = None duration = None title = None tz = pytz.utc location = None guid = None for event in new_events: tz = event['tz'] if tz is None: tz = pytz.utc else: tz = timezone(tz) tempnow = dt_util.now(tz) guid = event['guid'] starttime = event['startDate'] startdate = datetime(starttime[1], starttime[2], starttime[3], starttime[4], starttime[5], 0, 0, tz) endtime = event['endDate'] enddate = datetime(endtime[1], endtime[2], endtime[3], endtime[4], endtime[5], 0, 0, tz) duration = event['duration'] title = event['title'] location = event['location'] strnow = tempnow.strftime("%Y%m%d%H%M%S") strstart = startdate.strftime("%Y%m%d%H%M%S") strend = enddate.strftime("%Y%m%d%H%M%S") if strnow < strstart: if guid not in self.nextevents: ievent = IEvent(self.hass, self, guid, TYPE_NEXT) ievent.update_ha_state() self.nextevents[guid] = ievent self.nextevents[guid].keep_alive(starttime, endtime, duration, title, tz, location) for addedevent in self.nextevents: found = False eventguid = self.nextevents[addedevent].eventguid for event in new_events: if event['guid'] == eventguid: found = True if not found: ent_id = generate_entity_id(ENTITY_ID_FORMAT_EVENT, eventguid, hass=self.hass) self.hass.states.remove(ent_id) del self.nextevents[addedevent] else: self.nextevents[addedevent].check_alive() def lost_iphone(self, devicename): """ Calls the lost iphone function if the device is found """ if self.api is not None: self.api.authenticate() if devicename is not None: if devicename in self.devices: self.devices[devicename].play_sound() else: _LOGGER.error("devicename %s unknown for account %s", devicename, self.accountname) else: for device in self.devices: self.devices[device].play_sound() def update_icloud(self, see, devicename=None): """ Authenticate against iCloud and scan for devices. """ if self.api is not None: from pyicloud.exceptions import PyiCloudNoDevicesException try: # The session timeouts if we are not using it so we # have to re-authenticate. This will send an email. self.api.authenticate() if devicename is not None: if devicename in self.devices: self.devices[devicename].update_icloud(see) else: _LOGGER.error("devicename %s unknown for account %s", devicename, self.accountname) else: for device in self.devices: self.devices[device].update_icloud(see) except PyiCloudNoDevicesException: _LOGGER.error('No iCloud Devices found!') def setinterval(self, interval=None, devicename=None): if devicename is None: for device in self.devices: device.setinterval(interval) device.update_icloud(see) elif devicename in self.devices: self.devices[devicename] = setinterval(interval) self.devices[devicename].update_icloud(see)
def update_location(self, kwargs): from pyicloud import PyiCloudService from pyicloud.exceptions import (PyiCloudFailedLoginException, PyiCloudNoDevicesException, PyiCloudException) try: self.my_log("updating location for " + self.args["account_name"]) api = PyiCloudService(self.args["account_name"], self.args["account_passwd"]) if api.requires_2fa: self.my_log('requires_2fa is True for account ' + self.args["account_name"]) api.authenticate() if api.requires_2fa: raise Exception('Unknown failure') try: if "iphone_id" in self.args: location = api.devices[self.args["iphone_id"]].location() status = api.devices[self.args["iphone_id"]].status() else: location = api.iphone.location() status = api.iphone.status() self.my_log(json.dumps(location)) self.my_log(json.dumps(status)) topic = "owntracks/iphone/" + self.args["name"] data = {} data['_type'] = "location" data['tid'] = self.args["name"] data['lon'] = location["longitude"] data['lat'] = location["latitude"] data['acc'] = int(location["horizontalAccuracy"]) data['batt'] = int(status["batteryLevel"] * 100) data['_cp'] = True data['tst'] = calendar.timegm(time.gmtime()) json_data = json.dumps(data, ensure_ascii=False) # self.my_log(json_data) self.call_service("mqtt/publish", topic=topic, payload=json_data, qos="1", retain="false") except PyiCloudNoDevicesException: self.my_log("no devices found", True) except Exception as error: self.my_log("error (1): " + error, True) except PyiCloudFailedLoginException as error: self.my_log("error in login: "******"error setting up 2FA: " + error, True) except Exception as error: self.my_log("error (2): " + str(error))
class Icloud(Entity): # pylint: disable=too-many-instance-attributes """Represent an icloud account in Home Assistant.""" def __init__(self, hass, username, password, cookiedirectory, name, ignored_devices, getevents, googletraveltime): """Initialize an iCloud account.""" # pylint: disable=too-many-arguments,too-many-branches # pylint: disable=too-many-statements,too-many-locals self.hass = hass self.username = username self.password = password self.cookiedir = cookiedirectory self.accountname = name self._max_wait_seconds = 120 self._request_interval_seconds = 10 self._interval = 1 self.api = None self.devices = {} self.getevents = getevents self.events = {} self.currentevents = {} self.nextevents = {} self._ignored_devices = ignored_devices self._ignored_identifiers = {} self.googletraveltime = googletraveltime self._currentevents = 0 self._nextevents = 0 self.entity_id = generate_entity_id(ENTITY_ID_FORMAT_ICLOUD, self.accountname, hass=self.hass) if self.username is None or self.password is None: _LOGGER.error('Must specify a username and password') else: from pyicloud import PyiCloudService from pyicloud.exceptions import PyiCloudFailedLoginException try: # Attempt the login to iCloud self.api = PyiCloudService(self.username, self.password, cookie_directory=self.cookiedir, verify=True) for device in self.api.devices: status = device.status(DEVICESTATUSSET) devicename = slugify(status['name'].replace(' ', '', 99)) if (devicename not in self.devices and devicename not in self._ignored_devices): gtt = None if devicename in self.googletraveltime: gtt = self.googletraveltime[devicename] idevice = IDevice(self.hass, self, devicename, device, gtt) idevice.update_ha_state() self.devices[devicename] = idevice elif devicename in self._ignored_devices: self._ignored_identifiers[devicename] = device if self.getevents: from_dt = dt_util.now() to_dt = from_dt + timedelta(days=7) events = self.api.calendar.events(from_dt, to_dt) new_events = sorted(events, key=self.get_key) for event in new_events: tzone = event['tz'] if tzone is None: tzone = pytz.utc else: tzone = timezone(tzone) tempnow = dt_util.now(tzone) guid = event['guid'] starttime = event['startDate'] startdate = datetime(starttime[1], starttime[2], starttime[3], starttime[4], starttime[5], 0, 0, tzone) endtime = event['endDate'] enddate = datetime(endtime[1], endtime[2], endtime[3], endtime[4], endtime[5], 0, 0, tzone) duration = event['duration'] title = event['title'] location = event['location'] strnow = tempnow.strftime("%Y%m%d%H%M%S") strstart = startdate.strftime("%Y%m%d%H%M%S") strend = enddate.strftime("%Y%m%d%H%M%S") if strnow > strstart and strend > strnow: ievent = IEvent(self.hass, self, guid, TYPE_CURRENT) ievent.update_ha_state() self.currentevents[guid] = ievent self.currentevents[guid].keep_alive( starttime, endtime, duration, title, tzone, location) for event in new_events: tzone = event['tz'] if tzone is None: tzone = pytz.utc else: tzone = timezone(tzone) tempnow = dt_util.now(tzone) guid = event['guid'] starttime = event['startDate'] startdate = datetime(starttime[1], starttime[2], starttime[3], starttime[4], starttime[5], 0, 0, tzone) endtime = event['endDate'] enddate = datetime(endtime[1], endtime[2], endtime[3], endtime[4], endtime[5], 0, 0, tzone) duration = event['duration'] title = event['title'] location = event['location'] strnow = tempnow.strftime("%Y%m%d%H%M%S") strstart = startdate.strftime("%Y%m%d%H%M%S") strend = enddate.strftime("%Y%m%d%H%M%S") if strnow < strstart: ievent = IEvent(self.hass, self, guid, TYPE_NEXT) ievent.update_ha_state() self.nextevents[guid] = ievent self.nextevents[guid].keep_alive( starttime, endtime, duration, title, tzone, location) except PyiCloudFailedLoginException as error: _LOGGER.error('Error logging into iCloud Service: %s', error) @property def state(self): """Return the state of the icloud account.""" return self.api is not None @property def state_attributes(self): """Return the attributes of the icloud account.""" if self.getevents: return { ATTR_ACCOUNTNAME: self.accountname, ATTR_CURRENT_EVENTS: self._currentevents, ATTR_NEXT_EVENTS: self._nextevents } else: return {ATTR_ACCOUNTNAME: self.accountname} @property def icon(self): """Return the icon to use for device if any.""" return 'mdi:account' @staticmethod def get_key(item): """Sort key of events.""" return item.get('startDate') def keep_alive(self): """Keep the api alive.""" # pylint: disable=too-many-locals,too-many-branches,too-many-statements if self.api is None: from pyicloud import PyiCloudService from pyicloud.exceptions import PyiCloudFailedLoginException try: # Attempt the login to iCloud self.api = PyiCloudService(self.username, self.password, cookie_directory=self.cookiedir, verify=True) except PyiCloudFailedLoginException as error: _LOGGER.error('Error logging into iCloud Service: %s', error) if self.api is not None: if not self.getevents: self.api.authenticate() for devicename in self.devices: self.devices[devicename].keep_alive() if self.getevents: from_dt = dt_util.now() to_dt = from_dt + timedelta(days=7) events = self.api.calendar.events(from_dt, to_dt) new_events = sorted(events, key=self.get_key) for event in new_events: tzone = event['tz'] if tzone is None: tzone = pytz.utc else: tzone = timezone(tzone) tempnow = dt_util.now(tzone) guid = event['guid'] starttime = event['startDate'] startdate = datetime(starttime[1], starttime[2], starttime[3], starttime[4], starttime[5], 0, 0, tzone) endtime = event['endDate'] enddate = datetime(endtime[1], endtime[2], endtime[3], endtime[4], endtime[5], 0, 0, tzone) duration = event['duration'] title = event['title'] location = event['location'] strnow = tempnow.strftime("%Y%m%d%H%M%S") strstart = startdate.strftime("%Y%m%d%H%M%S") strend = enddate.strftime("%Y%m%d%H%M%S") if strnow > strstart and strend > strnow: if guid not in self.currentevents: ievent = IEvent(self.hass, self, guid, TYPE_CURRENT) ievent.update_ha_state() self.currentevents[guid] = ievent self.currentevents[guid].keep_alive( starttime, endtime, duration, title, tzone, location) for addedevent in self.currentevents: found = False eventguid = self.currentevents[addedevent].eventguid for event in new_events: if event['guid'] == eventguid: found = True if not found: ent_id = generate_entity_id(ENTITY_ID_FORMAT_EVENT, eventguid, hass=self.hass) self.hass.states.remove(ent_id) del self.currentevents[addedevent] else: self.currentevents[addedevent].check_alive() for event in new_events: tzone = event['tz'] if tzone is None: tzone = pytz.utc else: tzone = timezone(tzone) tempnow = dt_util.now(tzone) guid = event['guid'] starttime = event['startDate'] startdate = datetime(starttime[1], starttime[2], starttime[3], starttime[4], starttime[5], 0, 0, tzone) endtime = event['endDate'] enddate = datetime(endtime[1], endtime[2], endtime[3], endtime[4], endtime[5], 0, 0, tzone) duration = event['duration'] title = event['title'] location = event['location'] strnow = tempnow.strftime("%Y%m%d%H%M%S") strstart = startdate.strftime("%Y%m%d%H%M%S") strend = enddate.strftime("%Y%m%d%H%M%S") if strnow < strstart: if guid not in self.nextevents: ievent = IEvent(self.hass, self, guid, TYPE_NEXT) ievent.update_ha_state() self.nextevents[guid] = ievent self.nextevents[guid].keep_alive( starttime, endtime, duration, title, tzone, location) for addedevent in self.nextevents: found = False eventguid = self.nextevents[addedevent].eventguid for event in new_events: if event['guid'] == eventguid: found = True if not found: ent_id = generate_entity_id(ENTITY_ID_FORMAT_EVENT, eventguid, hass=self.hass) self.hass.states.remove(ent_id) del self.nextevents[addedevent] else: self.nextevents[addedevent].check_alive() self._currentevents = 0 self._nextevents = 0 for entity_id in self.hass.states.entity_ids('ievent'): state = self.hass.states.get(entity_id) friendlyname = state.attributes.get(ATTR_FRIENDLY_NAME) if friendlyname == 'nextevent': self._nextevents = self._nextevents + 1 elif friendlyname == 'currentevent': self._currentevents = self._currentevents + 1 self.update_ha_state() def lost_iphone(self, devicename): """Call the lost iphone function if the device is found.""" if self.api is not None: self.api.authenticate() if devicename is not None: if devicename in self.devices: self.devices[devicename].lost_iphone() else: _LOGGER.error("devicename %s unknown for account %s", devicename, self.accountname) else: for device in self.devices: self.devices[device].lost_iphone() def update_icloud(self, devicename=None): """Authenticate against iCloud and scan for devices.""" if self.api is not None: from pyicloud.exceptions import PyiCloudNoDevicesException try: # The session timeouts if we are not using it so we # have to re-authenticate. This will send an email. self.api.authenticate() if devicename is not None: if devicename in self.devices: self.devices[devicename].update_icloud() else: _LOGGER.error("devicename %s unknown for account %s", devicename, self.accountname) else: for device in self.devices: self.devices[device].update_icloud() except PyiCloudNoDevicesException: _LOGGER.error('No iCloud Devices found!') def setinterval(self, interval=None, devicename=None): """Set the interval of the given devices.""" if devicename is None: for device in self.devices: self.devices[device].setinterval(interval) self.devices[device].update_icloud() elif devicename in self.devices: self.devices[devicename].setinterval(interval) self.devices[devicename].update_icloud()