Example #1
0
    async def async_setup(self) -> bool:
        """Initialize this hub instance."""
        entry = self.config_entry
        port = (entry.options.get(CONF_SERVER_PORT,
                                  entry.data.get(CONF_SERVER_PORT)) or 0)

        _LOGGER.debug("Initializing Hubitat hub with event server on port %s",
                      port)
        self._hub = HubitatHub(self.host, self.app_id, self.token, port)

        await self._hub.start()

        hub = self._hub
        hass = self.hass
        config_entry = self.config_entry

        for component in PLATFORMS:
            hass.async_create_task(
                hass.config_entries.async_forward_entry_setup(
                    config_entry, component))
        _LOGGER.debug("Registered platforms")

        # Create an entity for the Hubitat hub with basic hub information
        hass.states.async_set(
            self.entity_id,
            "connected",
            {
                CONF_ID: f"{hub.host}::{hub.app_id}",
                CONF_HOST: hub.host,
                ATTR_HIDDEN: True,
                CONF_TEMPERATURE_UNIT: self.temperature_unit,
            },
        )

        return True
Example #2
0
    async def async_setup(self) -> bool:
        """Initialize this hub instance."""
        entry = self.config_entry
        url = entry.options.get(CONF_SERVER_URL,
                                entry.data.get(CONF_SERVER_URL))
        port = entry.options.get(CONF_SERVER_PORT,
                                 entry.data.get(CONF_SERVER_PORT))

        # Previous versions of the integration may have saved a value of "" for
        # server_url with the assumption that a use_server_url flag would control
        # it's use. The current version uses a value of null for "no user URL"
        # rather than a flag.
        if url == "":
            url = None

        _LOGGER.debug("Initializing Hubitat hub with event server on port %s",
                      port)
        self._hub = HubitatHub(self.host,
                               self.app_id,
                               self.token,
                               port=port,
                               event_url=url)

        await self._hub.start()

        hub = self._hub
        hass = self.hass
        config_entry = self.config_entry

        for platform in PLATFORMS:
            hass.async_create_task(
                hass.config_entries.async_forward_entry_setup(
                    config_entry, platform))

        _LOGGER.debug("Registered platforms")

        # Create an entity for the Hubitat hub with basic hub information
        hass.states.async_set(
            self.entity_id,
            "connected",
            {
                CONF_ID: f"{hub.host}::{hub.app_id}",
                CONF_HOST: hub.host,
                ATTR_HIDDEN: True,
                CONF_TEMPERATURE_UNIT: self.temperature_unit,
            },
        )

        return True
Example #3
0
class Hub:
    """Representation of a Hubitat hub."""
    def __init__(self, hass: HomeAssistant, entry: ConfigEntry, index: int):
        """Initialize a Hubitat manager."""
        if CONF_HOST not in entry.data:
            raise ValueError("Missing host in config entry")
        if CONF_APP_ID not in entry.data:
            raise ValueError("Missing app ID in config entry")
        if CONF_ACCESS_TOKEN not in entry.data:
            raise ValueError("Missing access token in config entry")

        self.hass = hass
        self.config_entry = entry
        self.entities: List["HubitatEntity"] = []
        self.event_emitters: List["HubitatEventEmitter"] = []

        self._temperature_unit = (entry.options.get(
            CONF_TEMPERATURE_UNIT, entry.data.get(CONF_TEMPERATURE_UNIT))
                                  or TEMP_F)

        if index == 1:
            self._hub_entity_id = "hubitat.hub"
        else:
            self._hub_entity_id = f"hubitat.hub_{index}"

        self.unsub_config_listener = entry.add_update_listener(_update_entry)

    @property
    def app_id(self) -> str:
        """The Maker API app ID for this hub."""
        return cast(str, self.config_entry.data.get(CONF_APP_ID))

    @property
    def devices(self) -> Mapping[str, Device]:
        """The Hubitat devices known to this hub."""
        return self._hub.devices

    @property
    def entity_id(self) -> str:
        """The entity ID of this hub."""
        return self._hub_entity_id

    @property
    def host(self) -> str:
        """The IP address of the  associated Hubitat hub."""
        return cast(
            str,
            self.config_entry.options.get(
                CONF_HOST, self.config_entry.data.get(CONF_HOST)),
        )

    @property
    def id(self) -> str:
        """A unique ID for this hub instance."""
        return get_hub_short_id(self._hub)

    @property
    def mac(self) -> Optional[str]:
        """The MAC address of the  associated Hubitat hub."""
        return self._hub.mac

    @property
    def port(self) -> Optional[int]:
        """The port used for the Hubitat event receiver."""
        return self._hub.port

    @property
    def event_url(self) -> str:
        """The event URL that Hubitat should POST events to."""
        return self._hub.event_url

    @property
    def token(self) -> str:
        """The token used to access the Maker API."""
        return cast(str, self.config_entry.data.get(CONF_ACCESS_TOKEN))

    @property
    def temperature_unit(self) -> str:
        """The units used for temperature values."""
        return self._temperature_unit

    def add_device_listener(self, device_id: str, listener: Listener) -> None:
        """Add a listener for events for a specific device."""
        self._hub.add_device_listener(device_id, listener)

    def add_entities(self, entities: Sequence["HubitatEntity"]) -> None:
        """Add entities to this hub."""
        self.entities.extend(entities)

    def add_event_emitters(self,
                           emitters: Sequence["HubitatEventEmitter"]) -> None:
        """Add event emitters to this hub."""
        self.event_emitters.extend(emitters)

    def remove_device_listeners(self, device_id: str) -> None:
        """Remove all listeners for a specific device."""
        self._hub.remove_device_listeners(device_id)

    def set_temperature_unit(self, temp_unit: str) -> None:
        """Set the hub's temperature units."""
        _LOGGER.debug("Setting hub temperature unit to %s", temp_unit)
        self._temperature_unit = temp_unit

    def stop(self) -> None:
        """Stop the hub."""
        self._hub.stop()

    async def unload(self) -> None:
        """Unload the hub."""
        for emitter in self.event_emitters:
            await emitter.async_will_remove_from_hass()
        self.unsub_config_listener()

    async def async_setup(self) -> bool:
        """Initialize this hub instance."""
        entry = self.config_entry
        url = entry.options.get(CONF_SERVER_URL,
                                entry.data.get(CONF_SERVER_URL))
        port = entry.options.get(CONF_SERVER_PORT,
                                 entry.data.get(CONF_SERVER_PORT))

        # Previous versions of the integration may have saved a value of "" for
        # server_url with the assumption that a use_server_url flag would control
        # it's use. The current version uses a value of null for "no user URL"
        # rather than a flag.
        if url == "":
            url = None

        _LOGGER.debug("Initializing Hubitat hub with event server on port %s",
                      port)
        self._hub = HubitatHub(self.host,
                               self.app_id,
                               self.token,
                               port=port,
                               event_url=url)

        await self._hub.start()

        hub = self._hub
        hass = self.hass
        config_entry = self.config_entry

        for platform in PLATFORMS:
            hass.async_create_task(
                hass.config_entries.async_forward_entry_setup(
                    config_entry, platform))

        _LOGGER.debug("Registered platforms")

        # Create an entity for the Hubitat hub with basic hub information
        hass.states.async_set(
            self.entity_id,
            "connected",
            {
                CONF_ID: f"{hub.host}::{hub.app_id}",
                CONF_HOST: hub.host,
                ATTR_HIDDEN: True,
                CONF_TEMPERATURE_UNIT: self.temperature_unit,
            },
        )

        return True

    async def async_update_device_registry(self) -> None:
        """Add a device for this hub to the device registry."""
        dreg = cast(DeviceRegistry, await
                    device_registry.async_get_registry(self.hass))
        dreg.async_get_or_create(
            config_entry_id=self.config_entry.entry_id,
            connections={(device_registry.CONNECTION_NETWORK_MAC,
                          self._hub.mac)},
            identifiers={(DOMAIN, self.id)},
            manufacturer="Hubitat",
            name="Hubitat Elevation",
        )

    @staticmethod
    async def async_update_options(hass: HomeAssistant,
                                   config_entry: ConfigEntry) -> None:
        """Handle options update."""
        _LOGGER.debug("Handling options update...")
        hub = get_hub(hass, config_entry.entry_id)

        host: Optional[str] = config_entry.options.get(
            CONF_HOST, config_entry.data.get(CONF_HOST))
        if host is not None and host != hub.host:
            await hub.set_host(host)
            _LOGGER.debug("Set hub host to %s", host)

        port = (config_entry.options.get(
            CONF_SERVER_PORT, config_entry.data.get(CONF_SERVER_PORT)) or 0)
        if port != hub.port:
            await hub.set_port(port)
            _LOGGER.debug("Set event server port to %s", port)

        url = config_entry.options.get(CONF_SERVER_URL,
                                       config_entry.data.get(CONF_SERVER_URL))
        if url == "":
            url = None
        if url != hub.event_url:
            await hub.set_event_url(url)
            _LOGGER.debug("Set event server URL to %s", url)

        temp_unit = (config_entry.options.get(
            CONF_TEMPERATURE_UNIT,
            config_entry.data.get(CONF_TEMPERATURE_UNIT)) or TEMP_F)
        if temp_unit != hub.temperature_unit:
            hub.set_temperature_unit(temp_unit)
            for entity in hub.entities:
                if entity.device_class == DEVICE_CLASS_TEMPERATURE:
                    entity.update_state()
            _LOGGER.debug("Set temperature units to %s", temp_unit)

        hass.states.async_set(
            hub.entity_id,
            "connected",
            {
                CONF_HOST: hub.host,
                CONF_TEMPERATURE_UNIT: hub.temperature_unit
            },
        )

    async def check_config(self) -> None:
        """Verify that the hub is accessible."""
        await self._hub.check_config()

    async def refresh_device(self, device_id: str) -> None:
        """Load current data for a specific device."""
        await self._hub.refresh_device(device_id)

    async def send_command(self, device_id: str, command: str,
                           arg: Optional[Union[str, int]]) -> None:
        """Send a device command to Hubitat."""
        await self._hub.send_command(device_id, command, arg)

    async def set_host(self, host: str) -> None:
        """Set the host address that the Hubitat hub is accessible at."""
        _LOGGER.debug("Setting Hubitat host to %s", host)
        self._hub.set_host(host)

    async def set_port(self, port: int) -> None:
        """Set the port that the event listener server will listen on."""
        _LOGGER.debug("Setting event listener port to %s", port)
        await self._hub.set_port(port)

    async def set_event_url(self, url: Optional[str]) -> None:
        """Set the port that the event listener server will listen on."""
        _LOGGER.debug("Setting event server URL to %s", url)
        await self._hub.set_event_url(url)
Example #4
0
class Hub:
    """Representation of a Hubitat hub."""
    def __init__(self, hass: HomeAssistant, entry: ConfigEntry, index: int):
        """Initialize a Hubitat manager."""
        if CONF_HOST not in entry.data:
            raise ValueError("Missing host in config entry")
        if CONF_APP_ID not in entry.data:
            raise ValueError("Missing app ID in config entry")
        if CONF_ACCESS_TOKEN not in entry.data:
            raise ValueError("Missing access token in config entry")

        self.hass = hass
        self.config_entry = entry
        self.token = cast(str, self.config_entry.data.get(CONF_ACCESS_TOKEN))
        self.entities: list[UpdateableEntity] = []
        self.event_emitters: list[Removable] = []

        self._temperature_unit = (entry.options.get(
            CONF_TEMPERATURE_UNIT, entry.data.get(CONF_TEMPERATURE_UNIT))
                                  or TEMP_F)

        if index == 1:
            self._hub_entity_id = "hubitat.hub"
        else:
            self._hub_entity_id = f"hubitat.hub_{index}"

        self.unsub_config_listener = entry.add_update_listener(_update_entry)

    @property
    def app_id(self) -> str:
        """The Maker API app ID for this hub."""
        return cast(str, self.config_entry.data.get(CONF_APP_ID))

    @property
    def devices(self) -> Mapping[str, Device]:
        """The Hubitat devices known to this hub."""
        return self._hub.devices

    @property
    def entity_id(self) -> str:
        """The entity ID of this hub."""
        return self._hub_entity_id

    @property
    def host(self) -> str:
        """The IP address of the  associated Hubitat hub."""
        return cast(
            str,
            self.config_entry.options.get(
                CONF_HOST, self.config_entry.data.get(CONF_HOST)),
        )

    @property
    def id(self) -> str:
        """A unique ID for this hub instance."""
        return get_hub_short_id(self._hub)

    @property
    def mac(self) -> Optional[str]:
        """The MAC address of the  associated Hubitat hub."""
        return self._hub.mac

    @property
    def port(self) -> Optional[int]:
        """The port used for the Hubitat event receiver."""
        return self._hub.port

    @property
    def event_url(self) -> Optional[str]:
        """The event URL that Hubitat should POST events to."""
        return self._hub.event_url

    @property
    def ssl_context(self) -> Optional[SSLContext]:
        """The SSLContext that the event listener server is using."""
        return self._hub.ssl_context

    @property
    def mode(self) -> Optional[str]:
        """Return the current mode of this hub."""
        return self._hub.mode

    @property
    def modes(self) -> Optional[list[str]]:
        """Return the available modes of this hub."""
        return self._hub.modes

    @property
    def mode_supported(self) -> Optional[bool]:
        """Return true if this hub supports mode setting and status."""
        return self._hub.mode_supported

    @property
    def hsm_status(self) -> Optional[str]:
        """Return the current HSM status of this hub."""
        return self._hub.hsm_status

    @property
    def hsm_supported(self) -> Optional[bool]:
        """Return true if this hub supports HSM setting and status."""
        return self._hub.hsm_supported

    @property
    def temperature_unit(self) -> str:
        """The units used for temperature values."""
        return self._temperature_unit

    def add_device_listener(self, device_id: str, listener: Listener) -> None:
        """Add a listener for events for a specific device."""
        if device_id == self.id:
            self._hub_device_listeners.append(listener)
        else:
            if device_id not in self._device_listeners:
                self._device_listeners[device_id] = []
            self._device_listeners[device_id].append(listener)

    def add_entities(self, entities: Sequence[UpdateableEntity]) -> None:
        """Add entities to this hub."""
        self.entities.extend(entities)

    def add_event_emitters(self, emitters: Sequence[Removable]) -> None:
        """Add event emitters to this hub."""
        self.event_emitters.extend(emitters)

    def remove_device_listeners(self, device_id: str) -> None:
        """Remove all listeners for a specific device."""
        self._device_listeners[device_id] = []
        self._hub_device_listeners = []

    async def set_mode(self, mode: str) -> None:
        """Set the hub mode"""
        _LOGGER.debug("Setting hub mode to %s", mode)
        return await self._hub.set_mode(mode)

    async def set_hsm(self, mode: str) -> None:
        """Set the hub HSM"""
        _LOGGER.debug("Setting hub HSM to %s", mode)
        return await self._hub.set_hsm(mode)

    def set_temperature_unit(self, temp_unit: str) -> None:
        """Set the hub's temperature units."""
        _LOGGER.debug("Setting hub temperature unit to %s", temp_unit)
        self._temperature_unit = temp_unit

    def stop(self) -> None:
        """Stop the hub."""
        self._hub.stop()
        self._device_listeners = {}
        self._hub_device_listeners = []

    async def unload(self) -> None:
        """Unload the hub."""
        for emitter in self.event_emitters:
            await emitter.async_will_remove_from_hass()
        self.unsub_config_listener()

    async def async_setup(self) -> bool:
        """Initialize this hub instance."""
        entry = self.config_entry
        url = entry.options.get(CONF_SERVER_URL,
                                entry.data.get(CONF_SERVER_URL))
        port = entry.options.get(CONF_SERVER_PORT,
                                 entry.data.get(CONF_SERVER_PORT))

        # Previous versions of the integration may have saved a value of "" for
        # server_url with the assumption that a use_server_url flag would control
        # it's use. The current version uses a value of null for "no user URL"
        # rather than a flag.
        if url == "":
            url = None

        ssl_cert = entry.options.get(CONF_SERVER_SSL_CERT,
                                     entry.data.get(CONF_SERVER_SSL_CERT))
        ssl_key = entry.options.get(CONF_SERVER_SSL_KEY,
                                    entry.data.get(CONF_SERVER_SSL_KEY))
        ssl_context = _create_ssl_context(ssl_cert, ssl_key)

        _LOGGER.debug(
            "Initializing Hubitat hub with event server on port %s with SSL %s",
            port,
            "disabled" if ssl_context is None else "enabled",
        )
        self._hub = HubitatHub(
            self.host,
            self.app_id,
            self.token,
            port=port,
            event_url=url,
            ssl_context=ssl_context,
        )

        await self._hub.start()

        self._hub_device_listeners: list[Listener] = []
        self._device_listeners: dict[str, list[Listener]] = {}

        hub = self._hub
        hass = self.hass
        config_entry = self.config_entry

        # setup proxy Device representing the hub that can be used for linked
        # entities
        self.device = Device({
            "id":
            self.id,
            "label":
            HUB_DEVICE_NAME,
            "name":
            HUB_DEVICE_NAME,
            "attributes": [
                {
                    "name": "mode",
                    "currentValue": None,
                    "dataType": "ENUM",
                },
                {
                    "name": "hsm_status",
                    "currentValue": None,
                    "dataType": "ENUM",
                },
            ],
            "capabilities": [],
            "commands": [],
        })

        # Add a listener for every device exported by the hub. The listener
        # will re-export the Hubitat event as a hubitat_event in HA if it
        # matches a trigger condition.
        for device_id in hub.devices:
            hub.add_device_listener(device_id, self.handle_event)

        # Update device identifiers to include the Maker API instance ID to
        # ensure that devices coming from separate hubs (or Maker API installs)
        # are handled properly.
        _update_device_ids(self.id, self.hass)

        # Initialize entities
        for platform in PLATFORMS:
            hass.async_create_task(
                hass.config_entries.async_forward_entry_setup(
                    config_entry, platform))

        _LOGGER.debug("Registered platforms")

        # Create an entity for the Hubitat hub with basic hub information
        hass.states.async_set(
            self.entity_id,
            "connected",
            {
                CONF_ID: f"{hub.host}::{hub.app_id}",
                CONF_HOST: hub.host,
                ATTR_HIDDEN: True,
                CONF_TEMPERATURE_UNIT: self.temperature_unit,
            },
        )

        if self.mode_supported:

            def handle_mode_event(event: Event):
                self.device.update_attr("mode", cast(str, event.value))
                for listener in self._hub_device_listeners:
                    listener(event)

            self._hub.add_mode_listener(handle_mode_event)
            if self.mode:
                self.device.update_attr("mode", self.mode)

        if self.hsm_supported:

            def handle_hsm_status_event(event: Event):
                self.device.update_attr("hsm_status", cast(str, event.value))
                for listener in self._hub_device_listeners:
                    listener(event)

            self._hub.add_hsm_listener(handle_hsm_status_event)
            if self.hsm_status:
                self.device.update_attr("hsm_status", self.hsm_status)

        return True

    def async_update_device_registry(self) -> None:
        """Add a device for this hub to the device registry."""
        dreg = device_registry.async_get(self.hass)
        dreg.async_get_or_create(
            config_entry_id=self.config_entry.entry_id,
            connections={(device_registry.CONNECTION_NETWORK_MAC,
                          self._hub.mac)},
            identifiers={(DOMAIN, self.id)},
            manufacturer="Hubitat",
            name=HUB_NAME,
        )

    @staticmethod
    async def async_update_options(hass: HomeAssistant,
                                   config_entry: ConfigEntry) -> None:
        """Handle options update."""
        _LOGGER.debug("Handling options update...")
        hub = get_hub(hass, config_entry.entry_id)

        host: Optional[str] = config_entry.options.get(
            CONF_HOST, config_entry.data.get(CONF_HOST))
        if host is not None and host != hub.host:
            await hub.set_host(host)
            _LOGGER.debug("Set hub host to %s", host)

        port = (config_entry.options.get(
            CONF_SERVER_PORT, config_entry.data.get(CONF_SERVER_PORT)) or 0)
        if port != hub.port:
            await hub.set_port(port)
            _LOGGER.debug("Set event server port to %s", port)

        url = config_entry.options.get(CONF_SERVER_URL,
                                       config_entry.data.get(CONF_SERVER_URL))
        if url == "":
            url = None
        if url != hub.event_url:
            await hub.set_event_url(url)
            _LOGGER.debug("Set event server URL to %s", url)

        ssl_cert = config_entry.options.get(
            CONF_SERVER_SSL_CERT, config_entry.data.get(CONF_SERVER_SSL_CERT))
        ssl_key = config_entry.options.get(
            CONF_SERVER_SSL_KEY, config_entry.data.get(CONF_SERVER_SSL_KEY))
        ssl_context = _create_ssl_context(ssl_cert, ssl_key)
        await hub.set_ssl_context(ssl_context)
        _LOGGER.debug("Set event server SSL cert to %s and SSL key to %s",
                      ssl_cert, ssl_key)

        temp_unit = (config_entry.options.get(
            CONF_TEMPERATURE_UNIT,
            config_entry.data.get(CONF_TEMPERATURE_UNIT)) or TEMP_F)
        if temp_unit != hub.temperature_unit:
            hub.set_temperature_unit(temp_unit)
            for entity in hub.entities:
                if entity.device_class == DEVICE_CLASS_TEMPERATURE:
                    entity.update_state()
            _LOGGER.debug("Set temperature units to %s", temp_unit)

        hass.states.async_set(
            hub.entity_id,
            "connected",
            {
                CONF_HOST: hub.host,
                CONF_TEMPERATURE_UNIT: hub.temperature_unit
            },
        )

    async def check_config(self) -> None:
        """Verify that the hub is accessible."""
        await self._hub.check_config()

    async def refresh_device(self, device_id: str) -> None:
        """Load current data for a specific device."""
        await self._hub.refresh_device(device_id)

    async def send_command(self, device_id: str, command: str,
                           arg: Optional[Union[str, int]]) -> None:
        """Send a device command to Hubitat."""
        await self._hub.send_command(device_id, command, arg)

    async def set_host(self, host: str) -> None:
        """Set the host address that the Hubitat hub is accessible at."""
        _LOGGER.debug("Setting Hubitat host to %s", host)
        self._hub.set_host(host)

    async def set_port(self, port: int) -> None:
        """Set the port that the event listener server will listen on."""
        _LOGGER.debug("Setting event listener port to %s", port)
        await self._hub.set_port(port)

    async def set_ssl_context(self, ssl_context: Optional[SSLContext]) -> None:
        """Set the SSLContext that the event listener server will use."""
        if ssl_context is None:
            _LOGGER.warn("Disabling SSL for event listener server")
        else:
            _LOGGER.warn("Enabling SSL for event listener server")
        await self._hub.set_ssl_context(ssl_context)

    async def set_event_url(self, url: Optional[str]) -> None:
        """Set the port that the event listener server will listen on."""
        _LOGGER.debug("Setting event server URL to %s", url)
        await self._hub.set_event_url(url)

    def handle_event(self, event: Event) -> None:
        """Handle events received from the Hubitat hub."""
        if self._device_listeners[event.device_id]:
            for listener in self._device_listeners[event.device_id]:
                listener(event)
        if event.attribute in _TRIGGER_ATTRS:
            evt = dict(event)
            evt[ATTR_ATTRIBUTE] = _TRIGGER_ATTR_MAP[event.attribute]
            evt[ATTR_HUB] = self.id
            evt[ATTR_HA_DEVICE_ID] = get_hub_device_id(self, event.device_id)
            self.hass.bus.async_fire(CONF_HUBITAT_EVENT, evt)
            _LOGGER.debug("Emitted event %s", evt)
Example #5
0
    async def async_setup(self) -> bool:
        """Initialize this hub instance."""
        entry = self.config_entry
        url = entry.options.get(CONF_SERVER_URL,
                                entry.data.get(CONF_SERVER_URL))
        port = entry.options.get(CONF_SERVER_PORT,
                                 entry.data.get(CONF_SERVER_PORT))

        # Previous versions of the integration may have saved a value of "" for
        # server_url with the assumption that a use_server_url flag would control
        # it's use. The current version uses a value of null for "no user URL"
        # rather than a flag.
        if url == "":
            url = None

        ssl_cert = entry.options.get(CONF_SERVER_SSL_CERT,
                                     entry.data.get(CONF_SERVER_SSL_CERT))
        ssl_key = entry.options.get(CONF_SERVER_SSL_KEY,
                                    entry.data.get(CONF_SERVER_SSL_KEY))
        ssl_context = _create_ssl_context(ssl_cert, ssl_key)

        _LOGGER.debug(
            "Initializing Hubitat hub with event server on port %s with SSL %s",
            port,
            "disabled" if ssl_context is None else "enabled",
        )
        self._hub = HubitatHub(
            self.host,
            self.app_id,
            self.token,
            port=port,
            event_url=url,
            ssl_context=ssl_context,
        )

        await self._hub.start()

        self._hub_device_listeners: list[Listener] = []
        self._device_listeners: dict[str, list[Listener]] = {}

        hub = self._hub
        hass = self.hass
        config_entry = self.config_entry

        # setup proxy Device representing the hub that can be used for linked
        # entities
        self.device = Device({
            "id":
            self.id,
            "label":
            HUB_DEVICE_NAME,
            "name":
            HUB_DEVICE_NAME,
            "attributes": [
                {
                    "name": "mode",
                    "currentValue": None,
                    "dataType": "ENUM",
                },
                {
                    "name": "hsm_status",
                    "currentValue": None,
                    "dataType": "ENUM",
                },
            ],
            "capabilities": [],
            "commands": [],
        })

        # Add a listener for every device exported by the hub. The listener
        # will re-export the Hubitat event as a hubitat_event in HA if it
        # matches a trigger condition.
        for device_id in hub.devices:
            hub.add_device_listener(device_id, self.handle_event)

        # Update device identifiers to include the Maker API instance ID to
        # ensure that devices coming from separate hubs (or Maker API installs)
        # are handled properly.
        _update_device_ids(self.id, self.hass)

        # Initialize entities
        for platform in PLATFORMS:
            hass.async_create_task(
                hass.config_entries.async_forward_entry_setup(
                    config_entry, platform))

        _LOGGER.debug("Registered platforms")

        # Create an entity for the Hubitat hub with basic hub information
        hass.states.async_set(
            self.entity_id,
            "connected",
            {
                CONF_ID: f"{hub.host}::{hub.app_id}",
                CONF_HOST: hub.host,
                ATTR_HIDDEN: True,
                CONF_TEMPERATURE_UNIT: self.temperature_unit,
            },
        )

        if self.mode_supported:

            def handle_mode_event(event: Event):
                self.device.update_attr("mode", cast(str, event.value))
                for listener in self._hub_device_listeners:
                    listener(event)

            self._hub.add_mode_listener(handle_mode_event)
            if self.mode:
                self.device.update_attr("mode", self.mode)

        if self.hsm_supported:

            def handle_hsm_status_event(event: Event):
                self.device.update_attr("hsm_status", cast(str, event.value))
                for listener in self._hub_device_listeners:
                    listener(event)

            self._hub.add_hsm_listener(handle_hsm_status_event)
            if self.hsm_status:
                self.device.update_attr("hsm_status", self.hsm_status)

        return True