text: """Support for tracking people."""
  import logging
  from typing import List, Optional, cast
  
  import voluptuous as vol
  
  from homeassistant.auth import EVENT_USER_REMOVED
  from homeassistant.components import websocket_api
  from homeassistant.components.device_tracker import (
      ATTR_SOURCE_TYPE,
      DOMAIN as DEVICE_TRACKER_DOMAIN,
      SOURCE_TYPE_GPS,
  )
  from homeassistant.const import (
      ATTR_EDITABLE,
      ATTR_ENTITY_ID,
      ATTR_GPS_ACCURACY,
      ATTR_ID,
      ATTR_LATITUDE,
      ATTR_LONGITUDE,
      ATTR_NAME,
      CONF_ID,
      CONF_NAME,
      CONF_TYPE,
      EVENT_HOMEASSISTANT_START,
      SERVICE_RELOAD,
      STATE_HOME,
      STATE_NOT_HOME,
      STATE_UNAVAILABLE,
      STATE_UNKNOWN,
  )
  from homeassistant.core import (
      Event,
      HomeAssistant,
      ServiceCall,
      State,
      callback,
      split_entity_id,
  )
  from homeassistant.helpers import (
      collection,
      config_validation as cv,
      entity_registry,
      service,
  )
  from homeassistant.helpers.entity_component import EntityComponent
  from homeassistant.helpers.event import async_track_state_change_event
  from homeassistant.helpers.restore_state import RestoreEntity
  from homeassistant.helpers.storage import Store
  from homeassistant.helpers.typing import ConfigType, HomeAssistantType
  from homeassistant.loader import bind_hass
  
  _LOGGER = logging.getLogger(__name__)
  
  ATTR_SOURCE = "source"
  ATTR_USER_ID = "user_id"
  
  CONF_DEVICE_TRACKERS = "device_trackers"
  CONF_USER_ID = "user_id"
  CONF_PICTURE = "picture"
  
  DOMAIN = "person2"
  
  STORAGE_KEY = DOMAIN
  STORAGE_VERSION = 2
  # Device tracker states to ignore
  IGNORE_STATES = (STATE_UNKNOWN, STATE_UNAVAILABLE)
  
  PERSON_SCHEMA = vol.Schema(
      {
          vol.Required(CONF_ID): cv.string,
          vol.Required(CONF_NAME): cv.string,
          vol.Optional(CONF_USER_ID): cv.string,
          vol.Optional(CONF_DEVICE_TRACKERS, default=[]): vol.All(
              cv.ensure_list, cv.entities_domain(DEVICE_TRACKER_DOMAIN)
          ),
          vol.Optional(CONF_PICTURE): cv.string,
      }
  )
  
  CONFIG_SCHEMA = vol.Schema(
      {
          vol.Optional(DOMAIN, default=[]): vol.All(
              cv.ensure_list, cv.remove_falsy, [PERSON_SCHEMA]
          )
      },
      extra=vol.ALLOW_EXTRA,
  )
  
  
  @bind_hass
  async def async_create_person(hass, name, *, user_id=None, device_trackers=None):
      """Create a new person."""
      await hass.data[DOMAIN][1].async_create_item(
          {
              ATTR_NAME: name,
              ATTR_USER_ID: user_id,
              CONF_DEVICE_TRACKERS: device_trackers or [],
          }
      )
  
  
  @bind_hass
  async def async_add_user_device_tracker(
      hass: HomeAssistant, user_id: str, device_tracker_entity_id: str
  ):
      """Add a device tracker to a person linked to a user."""
      coll = cast(PersonStorageCollection, hass.data[DOMAIN][1])
  
      for person in coll.async_items():
          if person.get(ATTR_USER_ID) != user_id:
              continue
  
          device_trackers = person[CONF_DEVICE_TRACKERS]
  
          if device_tracker_entity_id in device_trackers:
              return
  
          await coll.async_update_item(
              person[collection.CONF_ID],
              {CONF_DEVICE_TRACKERS: device_trackers + [device_tracker_entity_id]},
          )
          break
  
  
  CREATE_FIELDS = {
      vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)),
      vol.Optional(CONF_USER_ID): vol.Any(str, None),
      vol.Optional(CONF_DEVICE_TRACKERS, default=list): vol.All(
          cv.ensure_list, cv.entities_domain(DEVICE_TRACKER_DOMAIN)
      ),
      vol.Optional(CONF_PICTURE): vol.Any(str, None),
  }
  
  
  UPDATE_FIELDS = {
      vol.Optional(CONF_NAME): vol.All(str, vol.Length(min=1)),
      vol.Optional(CONF_USER_ID): vol.Any(str, None),
      vol.Optional(CONF_DEVICE_TRACKERS, default=list): vol.All(
          cv.ensure_list, cv.entities_domain(DEVICE_TRACKER_DOMAIN)
      ),
      vol.Optional(CONF_PICTURE): vol.Any(str, None),
  }
  
  
  class PersonStore(Store):
      """Person storage."""
  
      async def _async_migrate_func(self, old_version, old_data):
          """Migrate to the new version.
  
          Migrate storage to use format of collection helper.
          """
          return {"items": old_data["persons"]}
  
  
  class PersonStorageCollection(collection.StorageCollection):
      """Person collection stored in storage."""
  
      CREATE_SCHEMA = vol.Schema(CREATE_FIELDS)
      UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS)
  
      def __init__(
          self,
          store: Store,
          logger: logging.Logger,
          id_manager: collection.IDManager,
          yaml_collection: collection.YamlCollection,
      ):
          """Initialize a person storage collection."""
          super().__init__(store, logger, id_manager)
          self.yaml_collection = yaml_collection
  
      async def _async_load_data(self) -> Optional[dict]:
          """Load the data.
  
          A past bug caused onboarding to create invalid person objects.
          This patches it up.
          """
          data = await super()._async_load_data()
  
          if data is None:
              return data
  
          for person in data["items"]:
              if person[CONF_DEVICE_TRACKERS] is None:
                  person[CONF_DEVICE_TRACKERS] = []
  
          return data
  
      async def async_load(self) -> None:
          """Load the Storage collection."""
          await super().async_load()
          self.hass.bus.async_listen(
              entity_registry.EVENT_ENTITY_REGISTRY_UPDATED, self._entity_registry_updated
          )
  
      async def _entity_registry_updated(self, event) -> None:
          """Handle entity registry updated."""
          if event.data["action"] != "remove":
              return
  
          entity_id = event.data[ATTR_ENTITY_ID]
  
          if split_entity_id(entity_id)[0] != "device_tracker":
              return
  
          for person in list(self.data.values()):
              if entity_id not in person[CONF_DEVICE_TRACKERS]:
                  continue
  
              await self.async_update_item(
                  person[collection.CONF_ID],
                  {
                      CONF_DEVICE_TRACKERS: [
                          devt
                          for devt in person[CONF_DEVICE_TRACKERS]
                          if devt != entity_id
                      ]
                  },
              )
  
      async def _process_create_data(self, data: dict) -> dict:
          """Validate the config is valid."""
          data = self.CREATE_SCHEMA(data)
  
          user_id = data.get(CONF_USER_ID)
  
          if user_id is not None:
              await self._validate_user_id(user_id)
  
          return data
  
      @callback
      def _get_suggested_id(self, info: dict) -> str:
          """Suggest an ID based on the config."""
          return info[CONF_NAME]
  
      async def _update_data(self, data: dict, update_data: dict) -> dict:
          """Return a new updated data object."""
          update_data = self.UPDATE_SCHEMA(update_data)
  
          user_id = update_data.get(CONF_USER_ID)
  
          if user_id is not None and user_id != data.get(CONF_USER_ID):
              await self._validate_user_id(user_id)
  
          return {**data, **update_data}
  
      async def _validate_user_id(self, user_id):
          """Validate the used user_id."""
          if await self.hass.auth.async_get_user(user_id) is None:
              raise ValueError("User does not exist")
  
          for persons in (self.data.values(), self.yaml_collection.async_items()):
              if any(person for person in persons if person.get(CONF_USER_ID) == user_id):
                  raise ValueError("User already taken")
  
  
  async def filter_yaml_data(hass: HomeAssistantType, persons: List[dict]) -> List[dict]:
      """Validate YAML data that we can't validate via schema."""
      filtered = []
      person_invalid_user = []
  
      for person_conf in persons:
          user_id = person_conf.get(CONF_USER_ID)
  
          if user_id is not None:
              if await hass.auth.async_get_user(user_id) is None:
                  _LOGGER.error(
                      "Invalid user_id detected for person %s",
                      person_conf[collection.CONF_ID],
                  )
                  person_invalid_user.append(
                      f"- Person {person_conf[CONF_NAME]} (id: {person_conf[collection.CONF_ID]}) points at invalid user {user_id}"
                  )
                  continue
  
          filtered.append(person_conf)
  
      if person_invalid_user:
          hass.components.persistent_notification.async_create(
              f"""
  The following persons point at invalid users:
  
  {"- ".join(person_invalid_user)}
              """,
              "Invalid Person Configuration",
              DOMAIN,
          )
  
      return filtered
  
  
  async def async_setup(hass: HomeAssistantType, config: ConfigType):
      """Set up the person component."""
      entity_component = EntityComponent(_LOGGER, DOMAIN, hass)
      id_manager = collection.IDManager()
      yaml_collection = collection.YamlCollection(
          logging.getLogger(f"{__name__}.yaml_collection"), id_manager
      )
      storage_collection = PersonStorageCollection(
          PersonStore(hass, STORAGE_VERSION, STORAGE_KEY),
          logging.getLogger(f"{__name__}.storage_collection"),
          id_manager,
          yaml_collection,
      )
  
      collection.sync_entity_lifecycle(
          hass, DOMAIN, DOMAIN, entity_component, yaml_collection, Person
      )
      collection.sync_entity_lifecycle(
          hass, DOMAIN, DOMAIN, entity_component, storage_collection, Person.from_yaml
      )
  
      await yaml_collection.async_load(
          await filter_yaml_data(hass, config.get(DOMAIN, []))
      )
      await storage_collection.async_load()
  
      hass.data[DOMAIN] = (yaml_collection, storage_collection)
  
      collection.StorageCollectionWebsocket(
          storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS
      ).async_setup(hass, create_list=False)
  
      websocket_api.async_register_command(hass, ws_list_person)
  
      async def _handle_user_removed(event: Event) -> None:
          """Handle a user being removed."""
          user_id = event.data[ATTR_USER_ID]
          for person in storage_collection.async_items():
              if person[CONF_USER_ID] == user_id:
                  await storage_collection.async_update_item(
                      person[CONF_ID], {CONF_USER_ID: None}
                  )
  
      hass.bus.async_listen(EVENT_USER_REMOVED, _handle_user_removed)
  
      async def async_reload_yaml(call: ServiceCall):
          """Reload YAML."""
          conf = await entity_component.async_prepare_reload(skip_reset=True)
          if conf is None:
              return
          await yaml_collection.async_load(
              await filter_yaml_data(hass, conf.get(DOMAIN, []))
          )
  
      service.async_register_admin_service(
          hass, DOMAIN, SERVICE_RELOAD, async_reload_yaml
      )
  
      return True
  
  
  class Person(RestoreEntity):
      """Represent a tracked person."""
  
      def __init__(self, config):
          """Set up person."""
          self._config = config
          self.editable = True
          self._latitude = None
          self._longitude = None
          self._gps_accuracy = None
          self._source = None
          self._state = None
          self._unsub_track_device = None
  
      @classmethod
      def from_yaml(cls, config):
          """Return entity instance initialized from yaml storage."""
          person = cls(config)
          person.editable = False
          return person
  
      @property
      def name(self):
          """Return the name of the entity."""
          return self._config[CONF_NAME]
  
      @property
      def entity_picture(self) -> Optional[str]:
          """Return entity picture."""
          return self._config.get(CONF_PICTURE)
  
      @property
      def should_poll(self):
          """Return True if entity has to be polled for state.
  
          False if entity pushes its state to HA.
          """
          return False
  
      @property
      def state(self):
          """Return the state of the person."""
          return self._state
  
      @property
      def state_attributes(self):
          """Return the state attributes of the person."""
          data = {ATTR_EDITABLE: self.editable, ATTR_ID: self.unique_id}
          if self._latitude is not None:
              data[ATTR_LATITUDE] = self._latitude
          if self._longitude is not None:
              data[ATTR_LONGITUDE] = self._longitude
          if self._gps_accuracy is not None:
              data[ATTR_GPS_ACCURACY] = self._gps_accuracy
          if self._source is not None:
              data[ATTR_SOURCE] = self._source
          user_id = self._config.get(CONF_USER_ID)
          if user_id is not None:
              data[ATTR_USER_ID] = user_id
          return data
  
      @property
      def unique_id(self):
          """Return a unique ID for the person."""
          return self._config[CONF_ID]
  
      async def async_added_to_hass(self):
          """Register device trackers."""
          await super().async_added_to_hass()
          state = await self.async_get_last_state()
          if state:
              self._parse_source_state(state)
  
          if self.hass.is_running:
              # Update person now if hass is already running.
              await self.async_update_config(self._config)
          else:
              # Wait for hass start to not have race between person
              # and device trackers finishing setup.
              async def person_start_hass(now):
                  await self.async_update_config(self._config)
  
              self.hass.bus.async_listen_once(
                  EVENT_HOMEASSISTANT_START, person_start_hass
              )
  
      async def async_update_config(self, config):
          """Handle when the config is updated."""
          self._config = config
  
          if self._unsub_track_device is not None:
              self._unsub_track_device()
              self._unsub_track_device = None
  
          trackers = self._config[CONF_DEVICE_TRACKERS]
  
          if trackers:
              _LOGGER.debug("Subscribe to device trackers for %s", self.entity_id)
  
              self._unsub_track_device = async_track_state_change_event(
                  self.hass, trackers, self._async_handle_tracker_update
              )
  
          self._update_state()
  
      @callback
      def _async_handle_tracker_update(self, event):
          """Handle the device tracker state changes."""
          self._update_state()
  
      @callback
      def _update_state(self):
          """Update the state."""
          latest_non_gps_home = latest_not_home = latest_gps = latest = None
          for entity_id in self._config[CONF_DEVICE_TRACKERS]:
              state = self.hass.states.get(entity_id)
  
              if not state or state.state in IGNORE_STATES:
                  continue
  
              if state.attributes.get(ATTR_SOURCE_TYPE) == SOURCE_TYPE_GPS:
                  latest_gps = _get_latest(latest_gps, state)
              elif state.state == STATE_HOME:
                  latest_non_gps_home = _get_latest(latest_non_gps_home, state)
              elif state.state == STATE_NOT_HOME:
                  latest_not_home = _get_latest(latest_not_home, state)
  
          if latest_non_gps_home:
              latest = latest_non_gps_home
          elif latest_gps and latest_gps.state != STATE_HOME:
              latest = latest_gps
          else:
              latest = latest_not_home
  
          if latest:
              self._parse_source_state(latest, latest_gps)
          else:
              self._state = None
              self._source = None
              self._latitude = None
              self._longitude = None
              self._gps_accuracy = None
  
          self.async_write_ha_state()
  
      @callback
      def _parse_source_state(self, state, state_gps=None):
          """Parse source state and set person attributes.
  
          This is a device tracker state or the restored person state.
          """
          self._state = state.state
          self._source = state.entity_id
          if state_gps:
              state = state_gps
          self._latitude = state.attributes.get(ATTR_LATITUDE)
          self._longitude = state.attributes.get(ATTR_LONGITUDE)
          self._gps_accuracy = state.attributes.get(ATTR_GPS_ACCURACY)
  
  
  @websocket_api.websocket_command({vol.Required(CONF_TYPE): "person/list"})
  def ws_list_person(
      hass: HomeAssistantType, connection: websocket_api.ActiveConnection, msg
  ):
      """List persons."""
      yaml, storage = hass.data[DOMAIN]
      connection.send_result(
          msg[ATTR_ID], {"storage": storage.async_items(), "config": yaml.async_items()}
      )
  
  
  def _get_latest(prev: Optional[State], curr: State):
      """Get latest state."""
      if prev is None or curr.last_updated > prev.last_updated:
          return curr
      return prev
  