iCloud device tracker: Multiple Devices with name: xxx

I tried using the iCloud device tracker, but I get the following error:

Multiple devices with name: xxx
22:55 components/device_tracker/icloud.py (ERROR)

I get this error twice, because I have an iPhone and a Macbook linked to iCloud. In my iCloud settings I don’t see multiple devices. I removed my iPhone from iCloud and connected it again, but this didn’t solve the problem. Does anyone know what is happening here?

I’m getting the same error ever since I’ve upgraded my Home Assistant to version 0.65.5.
The thing is, my iCloud component is is configured for both mine and my wife’s accounts, but i get this error with my wife’s account only.

Maybe it’s got something to to do with the IOS version…

My Macbook is using macOS High Sierra 10.13.3. My iPhone version is 11.2.6.

Error from log:

2018-03-18 03:07:18 ERROR (SyncWorker_8) [homeassistant.components.device_tracker.icloud] Multiple devices with name: fabbafsiphone
2018-03-18 03:07:19 ERROR (SyncWorker_8) [homeassistant.components.device_tracker.icloud] Multiple devices with name: fabbafsmacbookpro

Yeah I’ve also upgraded my wife’s phone version to the latest but to no avail.
It’s weird that it’s happening with only one of my 2 accounts…

@anon90333909 did you perhaps made any progress with this issue?

I figured it out! :sunglasses:

HA uses the pyicloud library.
When the icloud platform of the device_tracker component starts , it calls the devices property of the PyiCloudService class imported from pyicloud library.
The devices property returns the list of all the devices associated with the account in question, including all the previous old devices.
The platform first lists all devices and only later checks which device is the active one.

For instance, every time I’ve upgraded to a new device, I always changed the name of the device:

  • tomer's iphone4
  • tomer's iphone5s
  • tomer's iphone6s

So I have no problems…

My wife on the other end, never does that, so all of her devices are named hava's iphone.
So that means that every time the device_tracker starts, it tries to list multiple devices with the same name, hence the error.

To fix that, I’ve copied the icloud platform from HA’s github and added an if statement validating the device’s statusCode is 200, the old devices always return 205.
I’ve placed the file in /config/custom_components/device_tracker/icloud.py so HA overwrites the original component with the custom one.

That fixed it for me.

I’ll test it a bit more and eventually try to add a PR for ha with the fix.
For now, here is the “fixed” icloud platform, all I did is add the if statements in line 192 and line 399:

"""
Platform that supports scanning iCloud.

For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.icloud/
"""
import logging
import random
import os

import voluptuous as vol

from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
from homeassistant.components.device_tracker import (
    PLATFORM_SCHEMA, DOMAIN, ATTR_ATTRIBUTES, ENTITY_ID_FORMAT, DeviceScanner)
from homeassistant.components.zone import active_zone
from homeassistant.helpers.event import track_utc_time_change
import homeassistant.helpers.config_validation as cv
from homeassistant.util import slugify
import homeassistant.util.dt as dt_util
from homeassistant.util.location import distance

_LOGGER = logging.getLogger(__name__)

REQUIREMENTS = ['pyicloud==0.9.1']

CONF_IGNORED_DEVICES = 'ignored_devices'
CONF_ACCOUNTNAME = 'account_name'

# entity attributes
ATTR_ACCOUNTNAME = 'account_name'
ATTR_INTERVAL = 'interval'
ATTR_DEVICENAME = 'device_name'
ATTR_BATTERY = 'battery'
ATTR_DISTANCE = 'distance'
ATTR_DEVICESTATUS = 'device_status'
ATTR_LOWPOWERMODE = 'low_power_mode'
ATTR_BATTERYSTATUS = 'battery_status'

ICLOUDTRACKERS = {}

_CONFIGURING = {}

DEVICESTATUSSET = ['features', 'maxMsgChar', 'darkWake', 'fmlyShare',
                   'deviceStatus', 'remoteLock', 'activationLocked',
                   'deviceClass', 'id', 'deviceModel', 'rawDeviceModel',
                   'passcodeLength', 'canWipeAfterLock', 'trackingInfo',
                   'location', 'msg', 'batteryLevel', 'remoteWipe',
                   'thisDevice', 'snd', 'prsId', 'wipeInProgress',
                   'lowPowerMode', 'lostModeEnabled', 'isLocating',
                   'lostModeCapable', 'mesg', 'name', 'batteryStatus',
                   'lockedTimestamp', 'lostTimestamp', 'locationCapable',
                   'deviceDisplayName', 'lostDevice', 'deviceColor',
                   'wipedTimestamp', 'modelDisplayName', 'locationEnabled',
                   'isMac', 'locFoundEnabled']

DEVICESTATUSCODES = {
    '200': 'online',
    '201': 'offline',
    '203': 'pending',
    '204': 'unregistered',
}

SERVICE_SCHEMA = vol.Schema({
    vol.Optional(ATTR_ACCOUNTNAME): vol.All(cv.ensure_list, [cv.slugify]),
    vol.Optional(ATTR_DEVICENAME): cv.slugify,
    vol.Optional(ATTR_INTERVAL): cv.positive_int,
})

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
    vol.Required(CONF_USERNAME): cv.string,
    vol.Required(CONF_PASSWORD): cv.string,
    vol.Optional(ATTR_ACCOUNTNAME): cv.slugify,
})


def setup_scanner(hass, config: dict, see, discovery_info=None):
    """Set up the iCloud Scanner."""
    username = config.get(CONF_USERNAME)
    password = config.get(CONF_PASSWORD)
    account = config.get(CONF_ACCOUNTNAME, slugify(username.partition('@')[0]))

    icloudaccount = Icloud(hass, username, password, account, see)

    if icloudaccount.api is not None:
        ICLOUDTRACKERS[account] = icloudaccount

    else:
        _LOGGER.error("No ICLOUDTRACKERS added")
        return False

    def lost_iphone(call):
        """Call the lost iPhone function if the device is found."""
        accounts = call.data.get(ATTR_ACCOUNTNAME, ICLOUDTRACKERS)
        devicename = call.data.get(ATTR_DEVICENAME)
        for account in accounts:
            if account in ICLOUDTRACKERS:
                ICLOUDTRACKERS[account].lost_iphone(devicename)
    hass.services.register(DOMAIN, 'icloud_lost_iphone', lost_iphone,
                           schema=SERVICE_SCHEMA)

    def update_icloud(call):
        """Call the update function of an iCloud account."""
        accounts = call.data.get(ATTR_ACCOUNTNAME, ICLOUDTRACKERS)
        devicename = call.data.get(ATTR_DEVICENAME)
        for account in accounts:
            if account in ICLOUDTRACKERS:
                ICLOUDTRACKERS[account].update_icloud(devicename)
    hass.services.register(DOMAIN, 'icloud_update', update_icloud,
                           schema=SERVICE_SCHEMA)

    def reset_account_icloud(call):
        """Reset an iCloud account."""
        accounts = call.data.get(ATTR_ACCOUNTNAME, ICLOUDTRACKERS)
        for account in accounts:
            if account in ICLOUDTRACKERS:
                ICLOUDTRACKERS[account].reset_account_icloud()
    hass.services.register(DOMAIN, 'icloud_reset_account',
                           reset_account_icloud, schema=SERVICE_SCHEMA)

    def setinterval(call):
        """Call the update function of an iCloud account."""
        accounts = call.data.get(ATTR_ACCOUNTNAME, ICLOUDTRACKERS)
        interval = call.data.get(ATTR_INTERVAL)
        devicename = call.data.get(ATTR_DEVICENAME)
        for account in accounts:
            if account in ICLOUDTRACKERS:
                ICLOUDTRACKERS[account].setinterval(interval, devicename)

    hass.services.register(DOMAIN, 'icloud_set_interval', setinterval,
                           schema=SERVICE_SCHEMA)

    # Tells the bootstrapper that the component was successfully initialized
    return True


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)
                '''tomerfi: added status validation to avoid old devices being listed'''
                if status['deviceStatus'] in ['200', '201']:
                    devicename = slugify(status['name'].replace(' ', '', 99))
                    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 = 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

        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] = 30
            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
            # home - check every half hour
            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)
                '''tomerfi: added status validation to avoid old devices being listed'''
                if status['deviceStatus'] in ['200', '201']:
                  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)

3 Likes

Well done. I never could have done that. I might give this a shot soon and will reply again.

1 Like

I know this is an older posting but does this still work? I have got some new phones and know having the Multiple devices with name: sidneysiphone for my daughters phone.

Still happening for me. I wish I could get the icloud2.py script working

I was having this problem too and just found an easy fix. Simply go in and rename the conflicting devices. If IOS, go to Settings -> General -> About -> Name and adjust it accordingly. Reboot smartphone for good measure, restart HA. Problem solved.

So I just got this and I’m not sure what changed (no new phones) but my device was renamed to iphone so it conflicted with my wifes. I change the name and all is good.

Much obliged! This patch worked a treat, and solved my wife’s phone naming issues lol

1 Like

@TomerFi Did you ever create a PR with this fix? I am experiencing the same thing, as my daughter just got a new phone. I think your fix should be integrated into the main branch for others that might experience this issue, using the iCloud device tracking integration.

Keith

I already have a PR that will solve this, work in progress :wink: