diff --git a/custom_components/ge_home/__init__.py b/custom_components/ge_home/__init__.py index 7b3f082..f088fd7 100644 --- a/custom_components/ge_home/__init__.py +++ b/custom_components/ge_home/__init__.py @@ -7,7 +7,9 @@ import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.const import CONF_REGION +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from .const import DOMAIN +from .exceptions import HaAuthError, HaCannotConnect from .update_coordinator import GeHomeUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -42,9 +44,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): coordinator = GeHomeUpdateCoordinator(hass, entry) hass.data[DOMAIN][entry.entry_id] = coordinator - if not await coordinator.async_setup(): - return False - + try: + if not await coordinator.async_setup(): + return False + except HaCannotConnect: + raise ConfigEntryNotReady("Could not connect to SmartHQ") + except HaAuthError: + raise ConfigEntryAuthFailed("Could not authenticate to SmartHQ") + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, coordinator.shutdown) return True diff --git a/custom_components/ge_home/binary_sensor.py b/custom_components/ge_home/binary_sensor.py index addce09..9172f27 100644 --- a/custom_components/ge_home/binary_sensor.py +++ b/custom_components/ge_home/binary_sensor.py @@ -5,30 +5,34 @@ from typing import Callable from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import DOMAIN +from .devices import ApplianceApi from .entities import GeErdBinarySensor from .update_coordinator import GeHomeUpdateCoordinator _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable): - """GE Home sensors.""" + """GE Home binary sensors.""" + _LOGGER.debug('Adding GE Binary Sensor Entities') coordinator: GeHomeUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + #apis = coordinator.appliance_apis.values() - # This should be a NOP, but let's be safe - with async_timeout.timeout(20): - await coordinator.initialization_future + @callback + def async_devices_discovered(apis: list[ApplianceApi]): + _LOGGER.debug(f'Found {len(apis):d} appliance APIs') - apis = coordinator.appliance_apis.values() - _LOGGER.debug(f'Found {len(apis):d} appliance APIs') - entities = [ - entity - for api in apis - for entity in api.entities - if isinstance(entity, GeErdBinarySensor) and not isinstance(entity, SwitchEntity) - ] - _LOGGER.debug(f'Found {len(entities):d} binary sensors ') - async_add_entities(entities) + entities = [ + entity + for api in apis + for entity in api.entities + if isinstance(entity, GeErdBinarySensor) and not isinstance(entity, SwitchEntity) + ] + _LOGGER.debug(f'Found {len(entities):d} binary sensors') + async_add_entities(entities) + + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered) diff --git a/custom_components/ge_home/button.py b/custom_components/ge_home/button.py index 8e594bd..724bb17 100644 --- a/custom_components/ge_home/button.py +++ b/custom_components/ge_home/button.py @@ -4,9 +4,11 @@ import logging from typing import Callable from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import DOMAIN +from .devices import ApplianceApi from .entities import GeErdButton from .update_coordinator import GeHomeUpdateCoordinator @@ -15,19 +17,19 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable): """GE Home buttons.""" + _LOGGER.debug('Adding GE Button Entities') coordinator: GeHomeUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] - # This should be a NOP, but let's be safe - with async_timeout.timeout(20): - await coordinator.initialization_future + @callback + def async_devices_discovered(apis: list[ApplianceApi]): + _LOGGER.debug(f'Found {len(apis):d} appliance APIs') + entities = [ + entity + for api in apis + for entity in api.entities + if isinstance(entity, GeErdButton) + ] + _LOGGER.debug(f'Found {len(entities):d} buttons ') + async_add_entities(entities) - apis = coordinator.appliance_apis.values() - _LOGGER.debug(f'Found {len(apis):d} appliance APIs') - entities = [ - entity - for api in apis - for entity in api.entities - if isinstance(entity, GeErdButton) - ] - _LOGGER.debug(f'Found {len(entities):d} buttons ') - async_add_entities(entities) + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered) \ No newline at end of file diff --git a/custom_components/ge_home/climate.py b/custom_components/ge_home/climate.py index fbdb94d..c74a24b 100644 --- a/custom_components/ge_home/climate.py +++ b/custom_components/ge_home/climate.py @@ -5,31 +5,32 @@ from typing import Callable from homeassistant.components.climate import ClimateEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from .entities import GeClimate from .const import DOMAIN +from .devices import ApplianceApi from .update_coordinator import GeHomeUpdateCoordinator _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable): """GE Climate Devices.""" + _LOGGER.debug('Adding GE Climate Entities') coordinator: GeHomeUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] - # This should be a NOP, but let's be safe - with async_timeout.timeout(20): - await coordinator.initialization_future - _LOGGER.debug('Coordinator init future finished') + @callback + def async_devices_discovered(apis: list[ApplianceApi]): + _LOGGER.debug(f'Found {len(apis):d} appliance APIs') + entities = [ + entity + for api in apis + for entity in api.entities + if isinstance(entity, GeClimate) + ] + _LOGGER.debug(f'Found {len(entities):d} climate entities') + async_add_entities(entities) - apis = list(coordinator.appliance_apis.values()) - _LOGGER.debug(f'Found {len(apis):d} appliance APIs') - entities = [ - entity - for api in apis - for entity in api.entities - if isinstance(entity, GeClimate) - ] - _LOGGER.debug(f'Found {len(entities):d} climate entities') - async_add_entities(entities) + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered) \ No newline at end of file diff --git a/custom_components/ge_home/light.py b/custom_components/ge_home/light.py index 310cabf..0d6a787 100644 --- a/custom_components/ge_home/light.py +++ b/custom_components/ge_home/light.py @@ -4,10 +4,13 @@ import logging from typing import Callable from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + from .const import DOMAIN from .entities import GeErdLight +from .devices import ApplianceApi from .update_coordinator import GeHomeUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -20,19 +23,17 @@ async def async_setup_entry( _LOGGER.debug("Adding GE Home lights") coordinator: GeHomeUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] - # This should be a NOP, but let's be safe - with async_timeout.timeout(20): - await coordinator.initialization_future - _LOGGER.debug("Coordinator init future finished") + @callback + def async_devices_discovered(apis: list[ApplianceApi]): + _LOGGER.debug(f"Found {len(apis):d} appliance APIs") + entities = [ + entity + for api in apis + for entity in api.entities + if isinstance(entity, GeErdLight) + and entity.erd_code in api.appliance._property_cache + ] + _LOGGER.debug(f"Found {len(entities):d} lights") + async_add_entities(entities) - apis = list(coordinator.appliance_apis.values()) - _LOGGER.debug(f"Found {len(apis):d} appliance APIs") - entities = [ - entity - for api in apis - for entity in api.entities - if isinstance(entity, GeErdLight) - and entity.erd_code in api.appliance._property_cache - ] - _LOGGER.debug(f"Found {len(entities):d} lights") - async_add_entities(entities) + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered) diff --git a/custom_components/ge_home/number.py b/custom_components/ge_home/number.py index eed2189..ff95109 100644 --- a/custom_components/ge_home/number.py +++ b/custom_components/ge_home/number.py @@ -4,9 +4,12 @@ import logging from typing import Callable from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + from .const import DOMAIN +from .devices import ApplianceApi from .entities import GeErdNumber from .update_coordinator import GeHomeUpdateCoordinator @@ -15,19 +18,19 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable): """GE Home numbers.""" + _LOGGER.debug('Adding GE Number Entities') coordinator: GeHomeUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] - # This should be a NOP, but let's be safe - with async_timeout.timeout(20): - await coordinator.initialization_future + @callback + def async_devices_discovered(apis: list[ApplianceApi]): + _LOGGER.debug(f'Found {len(apis):d} appliance APIs') + entities = [ + entity + for api in apis + for entity in api.entities + if isinstance(entity, GeErdNumber) + ] + _LOGGER.debug(f'Found {len(entities):d} numbers ') + async_add_entities(entities) - apis = coordinator.appliance_apis.values() - _LOGGER.debug(f'Found {len(apis):d} appliance APIs') - entities = [ - entity - for api in apis - for entity in api.entities - if isinstance(entity, GeErdNumber) - ] - _LOGGER.debug(f'Found {len(entities):d} numbers ') - async_add_entities(entities) + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered) diff --git a/custom_components/ge_home/select.py b/custom_components/ge_home/select.py index bfa6b0b..27c3903 100644 --- a/custom_components/ge_home/select.py +++ b/custom_components/ge_home/select.py @@ -4,9 +4,11 @@ import logging from typing import Callable from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import DOMAIN +from .devices import ApplianceApi from .entities import GeErdSelect from .update_coordinator import GeHomeUpdateCoordinator @@ -20,19 +22,17 @@ async def async_setup_entry( _LOGGER.debug("Adding GE Home selects") coordinator: GeHomeUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] - # This should be a NOP, but let's be safe - with async_timeout.timeout(20): - await coordinator.initialization_future - _LOGGER.debug("Coordinator init future finished") + @callback + def async_devices_discovered(apis: list[ApplianceApi]): + _LOGGER.debug(f"Found {len(apis):d} appliance APIs") + entities = [ + entity + for api in apis + for entity in api.entities + if isinstance(entity, GeErdSelect) + and entity.erd_code in api.appliance._property_cache + ] + _LOGGER.debug(f"Found {len(entities):d} selectors") + async_add_entities(entities) - apis = list(coordinator.appliance_apis.values()) - _LOGGER.debug(f"Found {len(apis):d} appliance APIs") - entities = [ - entity - for api in apis - for entity in api.entities - if isinstance(entity, GeErdSelect) - and entity.erd_code in api.appliance._property_cache - ] - _LOGGER.debug(f"Found {len(entities):d} selectors") - async_add_entities(entities) + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered) diff --git a/custom_components/ge_home/sensor.py b/custom_components/ge_home/sensor.py index 2dea9a1..662d5d1 100644 --- a/custom_components/ge_home/sensor.py +++ b/custom_components/ge_home/sensor.py @@ -6,7 +6,8 @@ import voluptuous as vol from datetime import timedelta from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers import entity_platform from .const import ( @@ -16,6 +17,7 @@ from .const import ( SERVICE_SET_INT_VALUE ) from .entities import GeErdSensor +from .devices import ApplianceApi from .update_coordinator import GeHomeUpdateCoordinator ATTR_DURATION = "duration" @@ -31,21 +33,19 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn # Get the platform platform = entity_platform.async_get_current_platform() - # This should be a NOP, but let's be safe - with async_timeout.timeout(20): - await coordinator.initialization_future - _LOGGER.debug('Coordinator init future finished') + @callback + def async_devices_discovered(apis: list[ApplianceApi]): + _LOGGER.debug(f'Found {len(apis):d} appliance APIs') + entities = [ + entity + for api in apis + for entity in api.entities + if isinstance(entity, GeErdSensor) and entity.erd_code in api.appliance._property_cache + ] + _LOGGER.debug(f'Found {len(entities):d} sensors') + async_add_entities(entities) - apis = list(coordinator.appliance_apis.values()) - _LOGGER.debug(f'Found {len(apis):d} appliance APIs') - entities = [ - entity - for api in apis - for entity in api.entities - if isinstance(entity, GeErdSensor) and entity.erd_code in api.appliance._property_cache - ] - _LOGGER.debug(f'Found {len(entities):d} sensors') - async_add_entities(entities) + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered) # register set_timer entity service platform.async_register_entity_service( diff --git a/custom_components/ge_home/switch.py b/custom_components/ge_home/switch.py index 78cf896..452fc21 100644 --- a/custom_components/ge_home/switch.py +++ b/custom_components/ge_home/switch.py @@ -4,10 +4,13 @@ import logging from typing import Callable from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + from .entities import GeErdSwitch from .const import DOMAIN +from .devices import ApplianceApi from .update_coordinator import GeHomeUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -17,18 +20,16 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn _LOGGER.debug('Adding GE Home switches') coordinator: GeHomeUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] - # This should be a NOP, but let's be safe - with async_timeout.timeout(20): - await coordinator.initialization_future - _LOGGER.debug('Coordinator init future finished') + @callback + def async_devices_discovered(apis: list[ApplianceApi]): + _LOGGER.debug(f'Found {len(apis):d} appliance APIs') + entities = [ + entity + for api in apis + for entity in api.entities + if isinstance(entity, GeErdSwitch) and entity.erd_code in api.appliance._property_cache + ] + _LOGGER.debug(f'Found {len(entities):d} switches') + async_add_entities(entities) - apis = list(coordinator.appliance_apis.values()) - _LOGGER.debug(f'Found {len(apis):d} appliance APIs') - entities = [ - entity - for api in apis - for entity in api.entities - if isinstance(entity, GeErdSwitch) and entity.erd_code in api.appliance._property_cache - ] - _LOGGER.debug(f'Found {len(entities):d} switches') - async_add_entities(entities) + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered) diff --git a/custom_components/ge_home/update_coordinator.py b/custom_components/ge_home/update_coordinator.py index 8711d98..545a82c 100644 --- a/custom_components/ge_home/update_coordinator.py +++ b/custom_components/ge_home/update_coordinator.py @@ -21,6 +21,7 @@ from .exceptions import HaAuthError, HaCannotConnect from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import ( @@ -64,7 +65,6 @@ class GeHomeUpdateCoordinator(DataUpdateCoordinator): self._got_roster = False self._init_done = False self._retry_count = 0 - self.initialization_future = asyncio.Future() def create_ge_client( self, event_loop: Optional[asyncio.AbstractEventLoop] @@ -95,6 +95,11 @@ class GeHomeUpdateCoordinator(DataUpdateCoordinator): def appliance_apis(self) -> Dict[str, ApplianceApi]: return self._appliance_apis + @property + def signal_ready(self) -> str: + """Event specific per entry to signal readiness""" + return f"{DOMAIN}-ready-{self._config_entry.entry_id}" + @property def online(self) -> bool: """ @@ -167,12 +172,6 @@ class GeHomeUpdateCoordinator(DataUpdateCoordinator): except Exception: raise HaCannotConnect("Unknown connection failure") - try: - with async_timeout.timeout(ASYNC_TIMEOUT): - await self.initialization_future - except (asyncio.CancelledError, asyncio.TimeoutError): - raise HaCannotConnect("Initialization timed out") - return True async def async_start_client(self): @@ -322,16 +321,17 @@ class GeHomeUpdateCoordinator(DataUpdateCoordinator): async def async_maybe_trigger_all_ready(self): """See if we're all ready to go, and if so, let the games begin.""" - if self._init_done or self.initialization_future.done(): + if self._init_done: # Been here, done this return if self._got_roster and self.all_appliances_updated: - _LOGGER.debug("Ready to go. Waiting 2 seconds and setting init future result.") - # The the flag and wait to prevent two different fun race conditions + _LOGGER.debug("Ready to go, sending ready signal") self._init_done = True - await asyncio.sleep(2) - self.initialization_future.set_result(True) await self.client.async_event(EVENT_ALL_APPLIANCES_READY, None) + async_dispatcher_send( + self.hass, + self.signal_ready, + list(self.appliance_apis.values())) def _get_retry_delay(self) -> int: delay = MIN_RETRY_DELAY * 2 ** (self._retry_count - 1) diff --git a/custom_components/ge_home/water_heater.py b/custom_components/ge_home/water_heater.py index 72e4602..94a8357 100644 --- a/custom_components/ge_home/water_heater.py +++ b/custom_components/ge_home/water_heater.py @@ -5,10 +5,13 @@ from typing import Callable from homeassistant.components.water_heater import WaterHeaterEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + from .entities import GeAbstractWaterHeater from .const import DOMAIN +from .devices import ApplianceApi from .update_coordinator import GeHomeUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -18,18 +21,16 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn _LOGGER.debug('Adding GE "Water Heaters"') coordinator: GeHomeUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] - # This should be a NOP, but let's be safe - with async_timeout.timeout(20): - await coordinator.initialization_future - _LOGGER.debug('Coordinator init future finished') + @callback + def async_devices_discovered(apis: list[ApplianceApi]): + _LOGGER.debug(f'Found {len(apis):d} appliance APIs') + entities = [ + entity + for api in apis + for entity in api.entities + if isinstance(entity, GeAbstractWaterHeater) + ] + _LOGGER.debug(f'Found {len(entities):d} "water heaters"') + async_add_entities(entities) - apis = list(coordinator.appliance_apis.values()) - _LOGGER.debug(f'Found {len(apis):d} appliance APIs') - entities = [ - entity - for api in apis - for entity in api.entities - if isinstance(entity, GeAbstractWaterHeater) - ] - _LOGGER.debug(f'Found {len(entities):d} "water heaters"') - async_add_entities(entities) + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)