Exemple #1
0
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)
Exemple #2
0
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
Exemple #3
0
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"
Exemple #4
0
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)
Exemple #6
0
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)
Exemple #8
0
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))
Exemple #10
0
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()