async def main(): global sensor_ids, current_tuple evloop = asyncio.get_running_loop() cli = APIClient(evloop, HOST, PORT, None, client_info="datadump", keepalive=1.0) terminating = False async def reconnect(): print("connection failed, reconnecting...", file=sys.stderr) try: await cli.connect(login=True, on_stop=async_on_stop) print("connected!", file=sys.stderr) await cli.subscribe_states(async_on_state) except: print("error reconnecting", file=sys.stderr) asyncio.create_task(reconnect()) async def async_on_stop(): if terminating: return asyncio.create_task(reconnect()) await cli.connect(login=True, on_stop=async_on_stop) entities, _ = await cli.list_entities_services() sensor_ids = [0] * len(RELEVANT_SENSORS) for e in entities: idx = RELEVANT_SENSORS.index( e.object_id) if e.object_id in RELEVANT_SENSORS else None if idx != None: sensor_ids[idx] = e.key current_tuple = [None] * len(RELEVANT_SENSORS) await cli.subscribe_states(async_on_state) prompt = Prompt(evloop) await prompt("press enter to stop capturing data") terminating = True await cli.disconnect() await asyncio.sleep(3)
async def fetch_device_info(self) -> str | None: """Fetch device info from API and return any errors.""" zeroconf_instance = await zeroconf.async_get_instance(self.hass) assert self._host is not None assert self._port is not None cli = APIClient( self._host, self._port, "", zeroconf_instance=zeroconf_instance, noise_psk=self._noise_psk, ) try: await cli.connect() self._device_info = await cli.device_info() except RequiresEncryptionAPIError: return ERROR_REQUIRES_ENCRYPTION_KEY except InvalidEncryptionKeyAPIError: return "invalid_psk" except ResolveAPIError: return "resolve_error" except APIConnectionError: return "connection_error" finally: await cli.disconnect(force=True) self._name = self._device_info.name return None
async def try_login(self): """Try logging in to device and return any errors.""" cli = APIClient(self.hass.loop, self._host, self._port, self._password) try: await cli.connect(login=True) except APIConnectionError: await cli.disconnect(force=True) return "invalid_password" return None
async def start(self): try: self.c = c = APIClient(self.loop, self.host, 6053, 'MyPassword') await c.connect(login=True) await c.subscribe_states(on_state=self.on_state) except OSError: loop.stop() return self.loop.create_task(self.start_requesting_image_stream_forever())
async def async_run_logs(config, address): conf = config["api"] port: int = int(conf[CONF_PORT]) password: str = conf[CONF_PASSWORD] noise_psk: Optional[str] = None if CONF_ENCRYPTION in conf: noise_psk = conf[CONF_ENCRYPTION][CONF_KEY] _LOGGER.info("Starting log output from %s using esphome API", address) zc = zeroconf.Zeroconf() cli = APIClient( address, port, password, client_info=f"ESPHome Logs {__version__}", noise_psk=noise_psk, ) first_connect = True def on_log(msg): time_ = datetime.now().time().strftime("[%H:%M:%S]") text = msg.message.decode("utf8", "backslashreplace") safe_print(time_ + text) async def on_connect(): nonlocal first_connect try: await cli.subscribe_logs( on_log, log_level=LogLevel.LOG_LEVEL_VERY_VERBOSE, dump_config=first_connect, ) first_connect = False except APIConnectionError: cli.disconnect() async def on_disconnect(): _LOGGER.warning("Disconnected from API") zc = zeroconf.Zeroconf() reconnect = ReconnectLogic( client=cli, on_connect=on_connect, on_disconnect=on_disconnect, zeroconf_instance=zc, ) await reconnect.start() try: while True: await asyncio.sleep(60) except KeyboardInterrupt: await reconnect.stop() zc.close()
async def fetch_device_info(self): """Fetch device info from API and return any errors.""" cli = APIClient(self.hass.loop, self._host, self._port, "") try: await cli.connect() device_info = await cli.device_info() except APIConnectionError as err: if "resolving" in str(err): return "resolve_error", None return "connection_error", None finally: await cli.disconnect(force=True) return None, device_info
async def fetch_device_info(self): """Fetch device info from API and return any errors.""" from aioesphomeapi import APIClient, APIConnectionError cli = APIClient(self.hass.loop, self._host, self._port, '') try: await cli.connect() device_info = await cli.device_info() except APIConnectionError as err: if 'resolving' in str(err): return 'resolve_error', None return 'connection_error', None finally: await cli.disconnect(force=True) return None, device_info
async def try_login(self): """Try logging in to device and return any errors.""" zeroconf_instance = await zeroconf.async_get_instance(self.hass) cli = APIClient( self.hass.loop, self._host, self._port, self._password, zeroconf_instance=zeroconf_instance, ) try: await cli.connect(login=True) except APIConnectionError: await cli.disconnect(force=True) return "invalid_auth" return None
async def try_login(self): """Try logging in to device and return any errors.""" from aioesphomeapi import APIClient, APIConnectionError cli = APIClient(self.hass.loop, self._host, self._port, self._password) try: await cli.start() await cli.connect() except APIConnectionError: await cli.stop(force=True) return 'connection_error' try: await cli.login() except APIConnectionError: return 'invalid_password' finally: await cli.stop(force=True) return None
async def try_login(self) -> str | None: """Try logging in to device and return any errors.""" zeroconf_instance = await zeroconf.async_get_instance(self.hass) assert self._host is not None assert self._port is not None cli = APIClient( self._host, self._port, self._password, zeroconf_instance=zeroconf_instance, noise_psk=self._noise_psk, ) try: await cli.connect(login=True) except InvalidAuthAPIError: return "invalid_auth" except APIConnectionError: return "connection_error" finally: await cli.disconnect(force=True) return None
async def fetch_device_info(self) -> tuple[str | None, DeviceInfo | None]: """Fetch device info from API and return any errors.""" zeroconf_instance = await zeroconf.async_get_instance(self.hass) assert self._host is not None assert self._port is not None cli = APIClient( self.hass.loop, self._host, self._port, "", zeroconf_instance=zeroconf_instance, ) try: await cli.connect() device_info = await cli.device_info() except APIConnectionError as err: if "resolving" in str(err): return "resolve_error", None return "connection_error", None finally: await cli.disconnect(force=True) return None, device_info
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up the esphome component.""" host = entry.data[CONF_HOST] port = entry.data[CONF_PORT] password = entry.data[CONF_PASSWORD] noise_psk = entry.data.get(CONF_NOISE_PSK) device_id = None zeroconf_instance = await zeroconf.async_get_instance(hass) cli = APIClient( host, port, password, client_info=f"Home Assistant {const.__version__}", zeroconf_instance=zeroconf_instance, noise_psk=noise_psk, ) domain_data = DomainData.get(hass) entry_data = RuntimeEntryData( client=cli, entry_id=entry.entry_id, store=domain_data.get_or_create_store(hass, entry), ) domain_data.set_entry_data(entry, entry_data) async def on_stop(event: Event) -> None: """Cleanup the socket client on HA stop.""" await _cleanup_instance(hass, entry) # Use async_listen instead of async_listen_once so that we don't deregister # the callback twice when shutting down Home Assistant. # "Unable to remove unknown listener <function EventBus.async_listen_once.<locals>.onetime_listener>" entry_data.cleanup_callbacks.append( hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, on_stop)) @callback def async_on_state(state: EntityState) -> None: """Send dispatcher updates when a new state is received.""" entry_data.async_update_state(hass, state) @callback def async_on_service_call(service: HomeassistantServiceCall) -> None: """Call service when user automation in ESPHome config is triggered.""" domain, service_name = service.service.split(".", 1) service_data = service.data if service.data_template: try: data_template = { key: Template(value) # type: ignore[no-untyped-call] for key, value in service.data_template.items() } template.attach(hass, data_template) service_data.update( template.render_complex(data_template, service.variables)) except TemplateError as ex: _LOGGER.error("Error rendering data template for %s: %s", host, ex) return if service.is_event: # ESPHome uses servicecall packet for both events and service calls # Ensure the user can only send events of form 'esphome.xyz' if domain != "esphome": _LOGGER.error( "Can only generate events under esphome domain! (%s)", host) return # Call native tag scan if service_name == "tag_scanned": tag_id = service_data["tag_id"] hass.async_create_task( hass.components.tag.async_scan_tag(tag_id, device_id)) return hass.bus.async_fire(service.service, service_data) else: hass.async_create_task( hass.services.async_call(domain, service_name, service_data, blocking=True)) async def _send_home_assistant_state(entity_id: str, attribute: str | None, state: State | None) -> None: """Forward Home Assistant states to ESPHome.""" if state is None or (attribute and attribute not in state.attributes): return send_state = state.state if attribute: attr_val = state.attributes[attribute] # ESPHome only handles "on"/"off" for boolean values if isinstance(attr_val, bool): send_state = "on" if attr_val else "off" else: send_state = attr_val await cli.send_home_assistant_state(entity_id, attribute, str(send_state)) @callback def async_on_state_subscription(entity_id: str, attribute: str | None = None) -> None: """Subscribe and forward states for requested entities.""" async def send_home_assistant_state_event(event: Event) -> None: """Forward Home Assistant states updates to ESPHome.""" # Only communicate changes to the state or attribute tracked if (event.data.get("old_state") is not None and "new_state" in event.data and ((not attribute and event.data["old_state"].state == event.data["new_state"].state) or (attribute and attribute in event.data["old_state"].attributes and attribute in event.data["new_state"].attributes and event.data["old_state"].attributes[attribute] == event.data["new_state"].attributes[attribute]))): return await _send_home_assistant_state(event.data["entity_id"], attribute, event.data.get("new_state")) unsub = async_track_state_change_event( hass, [entity_id], send_home_assistant_state_event) entry_data.disconnect_callbacks.append(unsub) # Send initial state hass.async_create_task( _send_home_assistant_state(entity_id, attribute, hass.states.get(entity_id))) async def on_connect() -> None: """Subscribe to states and list entities on successful API login.""" nonlocal device_id try: entry_data.device_info = await cli.device_info() assert cli.api_version is not None entry_data.api_version = cli.api_version entry_data.available = True device_id = _async_setup_device_registry(hass, entry, entry_data.device_info) entry_data.async_update_device_state(hass) entity_infos, services = await cli.list_entities_services() await entry_data.async_update_static_infos(hass, entry, entity_infos) await _setup_services(hass, entry_data, services) await cli.subscribe_states(async_on_state) await cli.subscribe_service_calls(async_on_service_call) await cli.subscribe_home_assistant_states( async_on_state_subscription) hass.async_create_task(entry_data.async_save_to_store()) except APIConnectionError as err: _LOGGER.warning("Error getting initial data for %s: %s", host, err) # Re-connection logic will trigger after this await cli.disconnect() async def on_disconnect() -> None: """Run disconnect callbacks on API disconnect.""" for disconnect_cb in entry_data.disconnect_callbacks: disconnect_cb() entry_data.disconnect_callbacks = [] entry_data.available = False entry_data.async_update_device_state(hass) async def on_connect_error(err: Exception) -> None: """Start reauth flow if appropriate connect error type.""" if isinstance( err, (RequiresEncryptionAPIError, InvalidEncryptionKeyAPIError)): entry.async_start_reauth(hass) reconnect_logic = ReconnectLogic( client=cli, on_connect=on_connect, on_disconnect=on_disconnect, zeroconf_instance=zeroconf_instance, name=host, on_connect_error=on_connect_error, ) async def complete_setup() -> None: """Complete the config entry setup.""" infos, services = await entry_data.async_load_from_store() await entry_data.async_update_static_infos(hass, entry, infos) await _setup_services(hass, entry_data, services) await reconnect_logic.start() entry_data.cleanup_callbacks.append(reconnect_logic.stop_callback) hass.async_create_task(complete_setup()) return True
async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: """Set up the esphome component.""" hass.data.setdefault(DATA_KEY, {}) host = entry.data[CONF_HOST] port = entry.data[CONF_PORT] password = entry.data[CONF_PASSWORD] cli = APIClient(hass.loop, host, port, password, client_info="Home Assistant {}".format(const.__version__)) # Store client in per-config-entry hass.data store = Store(hass, STORAGE_VERSION, STORAGE_KEY.format(entry.entry_id), encoder=JSONEncoder) entry_data = hass.data[DATA_KEY][entry.entry_id] = RuntimeEntryData( client=cli, entry_id=entry.entry_id, store=store, ) async def on_stop(event: Event) -> None: """Cleanup the socket client on HA stop.""" await _cleanup_instance(hass, entry) entry_data.cleanup_callbacks.append( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_stop)) @callback def async_on_state(state: EntityState) -> None: """Send dispatcher updates when a new state is received.""" entry_data.async_update_state(hass, state) @callback def async_on_service_call(service: ServiceCall) -> None: """Call service when user automation in ESPHome config is triggered.""" domain, service_name = service.service.split('.', 1) service_data = service.data if service.data_template: try: data_template = { key: Template(value) for key, value in service.data_template.items() } template.attach(hass, data_template) service_data.update( template.render_complex(data_template, service.variables)) except TemplateError as ex: _LOGGER.error('Error rendering data template: %s', ex) return hass.async_create_task( hass.services.async_call(domain, service_name, service_data, blocking=True)) async def send_home_assistant_state(entity_id: str, _, new_state: Optional[State]) -> None: """Forward Home Assistant states to ESPHome.""" if new_state is None: return await cli.send_home_assistant_state(entity_id, new_state.state) @callback def async_on_state_subscription(entity_id: str) -> None: """Subscribe and forward states for requested entities.""" unsub = async_track_state_change(hass, entity_id, send_home_assistant_state) entry_data.disconnect_callbacks.append(unsub) # Send initial state hass.async_create_task( send_home_assistant_state(entity_id, None, hass.states.get(entity_id))) async def on_login() -> None: """Subscribe to states and list entities on successful API login.""" try: entry_data.device_info = await cli.device_info() entry_data.available = True await _async_setup_device_registry(hass, entry, entry_data.device_info) entry_data.async_update_device_state(hass) entity_infos, services = await cli.list_entities_services() entry_data.async_update_static_infos(hass, entity_infos) await _setup_services(hass, entry_data, services) await cli.subscribe_states(async_on_state) await cli.subscribe_service_calls(async_on_service_call) await cli.subscribe_home_assistant_states( async_on_state_subscription) hass.async_create_task(entry_data.async_save_to_store()) except APIConnectionError as err: _LOGGER.warning("Error getting initial data: %s", err) # Re-connection logic will trigger after this await cli.disconnect() try_connect = await _setup_auto_reconnect_logic(hass, cli, entry, host, on_login) async def complete_setup() -> None: """Complete the config entry setup.""" tasks = [] for component in HA_COMPONENTS: tasks.append( hass.config_entries.async_forward_entry_setup( entry, component)) await asyncio.wait(tasks) infos, services = await entry_data.async_load_from_store() entry_data.async_update_static_infos(hass, infos) await _setup_services(hass, entry_data, services) # Create connection attempt outside of HA's tracked task in order # not to delay startup. hass.loop.create_task(try_connect(is_disconnect=False)) hass.async_create_task(complete_setup()) return True
async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: """Set up the esphome component.""" # pylint: disable=redefined-outer-name from aioesphomeapi import APIClient, APIConnectionError hass.data.setdefault(DOMAIN, {}) host = entry.data[CONF_HOST] port = entry.data[CONF_PORT] password = entry.data[CONF_PASSWORD] cli = APIClient(hass.loop, host, port, password) await cli.start() # Store client in per-config-entry hass.data entry_data = hass.data[DOMAIN][entry.entry_id] = RuntimeEntryData( client=cli, entry_id=entry.entry_id) async def on_stop(event: Event) -> None: """Cleanup the socket client on HA stop.""" await _cleanup_instance(hass, entry) entry_data.cleanup_callbacks.append( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_stop)) try_connect = await _setup_auto_reconnect_logic(hass, cli, entry, host) @callback def async_on_state(state: 'EntityState') -> None: """Send dispatcher updates when a new state is received.""" entry_data.async_update_state(hass, state) async def on_login() -> None: """Subscribe to states and list entities on successful API login.""" try: entry_data.device_info = await cli.device_info() entry_data.available = True entry_data.async_update_device_state(hass) entity_infos = await cli.list_entities() entry_data.async_update_static_infos(hass, entity_infos) await cli.subscribe_states(async_on_state) except APIConnectionError as err: _LOGGER.warning("Error getting initial data: %s", err) # Re-connection logic will trigger after this await cli.disconnect() cli.on_login = on_login # This is a bit of a hack: We schedule complete_setup into the # event loop and return immediately (return True) # # Usually, we should avoid that so that HA can track which components # have been started successfully and which failed to be set up. # That doesn't work here for two reasons: # - We have our own re-connect logic # - Before we do the first try_connect() call, we need to make sure # all dispatcher event listeners have been connected, so # async_forward_entry_setup needs to be awaited. However, if we # would await async_forward_entry_setup() in async_setup_entry(), # we would end up with a deadlock. # # Solution is: complete the setup outside of the async_setup_entry() # function. HA will wait until the first connection attempt is made # before starting up (as it should), but if the first connection attempt # fails we will schedule all next re-connect attempts outside of the # tracked tasks (hass.loop.create_task). This way HA won't stall startup # forever until a connection is successful. async def complete_setup() -> None: """Complete the config entry setup.""" tasks = [] for component in HA_COMPONENTS: tasks.append( hass.config_entries.async_forward_entry_setup( entry, component)) await asyncio.wait(tasks) # If first connect fails, the next re-connect will be scheduled # outside of _pending_task, in order not to delay HA startup # indefinitely await try_connect(is_disconnect=False) hass.async_create_task(complete_setup()) return True
async def constructor( cls, identifier: str, name: str, cfg: Dict[str, Any], state_defaults: Dict[str, Any], core: "Core", unique_identifier: str) -> "Item": cfg = cast(Dict[str, Any], cls.config_schema(cfg or {})) item = cls() item.entities = {} item.core = core item.identifier = identifier item.unique_identifier = unique_identifier item.name = name item.cfg = cfg item.actions = {} item.states = StateProxy(item, core) item.status = ItemStatus.OFFLINE storage = Storage( f"item_data/{unique_identifier}", 1, core=core, storage_init=lambda: {}, loader=item.load_config, dumper=item.dump_config) storage_data: StorageConfig = storage.load_data() api = APIClient( core.loop, cfg["host"], cfg["port"], cfg["password"], client_info=f"HomeControl {VERSION_STRING}" ) item.api = api connected, _ = await asyncio.wait( {core.loop.create_task(item.connect())}, timeout=6) if connected: entities, services = await api.list_entities_services() for service in services: item.actions[service.name] = partial( item._run_service, service) device_info = await api.device_info() storage_data.entities.clear() for entity in entities: storage_data.entities.append(StorageEntity( entity=entity, entity_type=type(entity).__name__ )) storage.schedule_save(storage_data) else: entities = [storage_entity.entity for storage_entity in storage_data.entities] device_info = storage_data.device_info version_state = StateDef(default=device_info.esphome_version) version_state.register_state(item.states, "version", item) for entity in entities: entity_type = ENTITY_TYPES.get(type(entity).__name__) if not entity_type: LOGGER.info("Did not add entity %s", entity) continue unique_e_identifier = f"{unique_identifier}_{entity.object_id}" entity_identifier = f"{identifier}_{entity.object_id}" entity_item = await entity_type.constructor( identifier=entity_identifier, name=entity.name, core=core, unique_identifier=unique_e_identifier, device=item, entity=entity ) item.entities[entity.key] = entity_item await core.item_manager.register_item(entity_item) return item
async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: """Set up the esphome component.""" # pylint: disable=redefined-outer-name from aioesphomeapi import APIClient, APIConnectionError hass.data.setdefault(DOMAIN, {}) host = entry.data[CONF_HOST] port = entry.data[CONF_PORT] password = entry.data[CONF_PASSWORD] cli = APIClient(hass.loop, host, port, password, client_info="Home Assistant {}".format(const.__version__)) # Store client in per-config-entry hass.data store = Store(hass, STORAGE_VERSION, STORAGE_KEY.format(entry.entry_id), encoder=JSONEncoder) entry_data = hass.data[DOMAIN][entry.entry_id] = RuntimeEntryData( client=cli, entry_id=entry.entry_id, store=store, ) async def on_stop(event: Event) -> None: """Cleanup the socket client on HA stop.""" await _cleanup_instance(hass, entry) entry_data.cleanup_callbacks.append( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_stop)) @callback def async_on_state(state: 'EntityState') -> None: """Send dispatcher updates when a new state is received.""" entry_data.async_update_state(hass, state) @callback def async_on_service_call(service: 'ServiceCall') -> None: """Call service when user automation in ESPHome config is triggered.""" domain, service_name = service.service.split('.', 1) service_data = service.data if service.data_template: try: data_template = { key: Template(value) for key, value in service.data_template.items() } template.attach(hass, data_template) service_data.update( template.render_complex(data_template, service.variables)) except TemplateError as ex: _LOGGER.error('Error rendering data template: %s', ex) return hass.async_create_task( hass.services.async_call(domain, service_name, service_data, blocking=True)) async def send_home_assistant_state(entity_id: str, _, new_state: Optional[str]) -> None: """Forward Home Assistant states to ESPHome.""" if new_state is None: return await cli.send_home_assistant_state(entity_id, new_state) @callback def async_on_state_subscription(entity_id: str) -> None: """Subscribe and forward states for requested entities.""" unsub = async_track_state_change(hass, entity_id, send_home_assistant_state) entry_data.disconnect_callbacks.append(unsub) # Send initial state hass.async_create_task( send_home_assistant_state(entity_id, None, hass.states.get(entity_id))) async def on_login() -> None: """Subscribe to states and list entities on successful API login.""" try: entry_data.device_info = await cli.device_info() entry_data.available = True await _async_setup_device_registry(hass, entry, entry_data.device_info) entry_data.async_update_device_state(hass) entity_infos = await cli.list_entities() entry_data.async_update_static_infos(hass, entity_infos) await cli.subscribe_states(async_on_state) await cli.subscribe_service_calls(async_on_service_call) await cli.subscribe_home_assistant_states( async_on_state_subscription) hass.async_create_task(entry_data.async_save_to_store()) except APIConnectionError as err: _LOGGER.warning("Error getting initial data: %s", err) # Re-connection logic will trigger after this await cli.disconnect() try_connect = await _setup_auto_reconnect_logic(hass, cli, entry, host, on_login) # This is a bit of a hack: We schedule complete_setup into the # event loop and return immediately (return True) # # Usually, we should avoid that so that HA can track which components # have been started successfully and which failed to be set up. # That doesn't work here for two reasons: # - We have our own re-connect logic # - Before we do the first try_connect() call, we need to make sure # all dispatcher event listeners have been connected, so # async_forward_entry_setup needs to be awaited. However, if we # would await async_forward_entry_setup() in async_setup_entry(), # we would end up with a deadlock. # # Solution is: complete the setup outside of the async_setup_entry() # function. HA will wait until the first connection attempt is made # before starting up (as it should), but if the first connection attempt # fails we will schedule all next re-connect attempts outside of the # tracked tasks (hass.loop.create_task). This way HA won't stall startup # forever until a connection is successful. async def complete_setup() -> None: """Complete the config entry setup.""" tasks = [] for component in HA_COMPONENTS: tasks.append( hass.config_entries.async_forward_entry_setup( entry, component)) await asyncio.wait(tasks) infos = await entry_data.async_load_from_store() entry_data.async_update_static_infos(hass, infos) # If first connect fails, the next re-connect will be scheduled # outside of _pending_task, in order not to delay HA startup # indefinitely await try_connect(is_disconnect=False) hass.async_create_task(complete_setup()) return True
async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: """Set up the esphome component.""" hass.data.setdefault(DATA_KEY, {}) host = entry.data[CONF_HOST] port = entry.data[CONF_PORT] password = entry.data[CONF_PASSWORD] zeroconf_instance = await zeroconf.async_get_instance(hass) cli = APIClient( hass.loop, host, port, password, client_info=f"Home Assistant {const.__version__}", zeroconf_instance=zeroconf_instance, ) # Store client in per-config-entry hass.data store = Store(hass, STORAGE_VERSION, f"esphome.{entry.entry_id}", encoder=JSONEncoder) entry_data = hass.data[DATA_KEY][entry.entry_id] = RuntimeEntryData( client=cli, entry_id=entry.entry_id, store=store) async def on_stop(event: Event) -> None: """Cleanup the socket client on HA stop.""" await _cleanup_instance(hass, entry) # Use async_listen instead of async_listen_once so that we don't deregister # the callback twice when shutting down Home Assistant. # "Unable to remove unknown listener <function EventBus.async_listen_once.<locals>.onetime_listener>" entry_data.cleanup_callbacks.append( hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, on_stop)) @callback def async_on_state(state: EntityState) -> None: """Send dispatcher updates when a new state is received.""" entry_data.async_update_state(hass, state) @callback def async_on_service_call(service: HomeassistantServiceCall) -> None: """Call service when user automation in ESPHome config is triggered.""" domain, service_name = service.service.split(".", 1) service_data = service.data if service.data_template: try: data_template = { key: Template(value) for key, value in service.data_template.items() } template.attach(hass, data_template) service_data.update( template.render_complex(data_template, service.variables)) except TemplateError as ex: _LOGGER.error("Error rendering data template for %s: %s", host, ex) return if service.is_event: # ESPHome uses servicecall packet for both events and service calls # Ensure the user can only send events of form 'esphome.xyz' if domain != "esphome": _LOGGER.error( "Can only generate events under esphome domain! (%s)", host) return hass.bus.async_fire(service.service, service_data) else: hass.async_create_task( hass.services.async_call(domain, service_name, service_data, blocking=True)) async def send_home_assistant_state_event(event: Event) -> None: """Forward Home Assistant states updates to ESPHome.""" new_state = event.data.get("new_state") if new_state is None: return entity_id = event.data.get("entity_id") await cli.send_home_assistant_state(entity_id, new_state.state) async def _send_home_assistant_state(entity_id: str, new_state: Optional[State]) -> None: """Forward Home Assistant states to ESPHome.""" await cli.send_home_assistant_state(entity_id, new_state.state) @callback def async_on_state_subscription(entity_id: str) -> None: """Subscribe and forward states for requested entities.""" unsub = async_track_state_change_event( hass, [entity_id], send_home_assistant_state_event) entry_data.disconnect_callbacks.append(unsub) new_state = hass.states.get(entity_id) if new_state is None: return # Send initial state hass.async_create_task(_send_home_assistant_state( entity_id, new_state)) async def on_login() -> None: """Subscribe to states and list entities on successful API login.""" try: entry_data.device_info = await cli.device_info() entry_data.available = True await _async_setup_device_registry(hass, entry, entry_data.device_info) entry_data.async_update_device_state(hass) entity_infos, services = await cli.list_entities_services() await entry_data.async_update_static_infos(hass, entry, entity_infos) await _setup_services(hass, entry_data, services) await cli.subscribe_states(async_on_state) await cli.subscribe_service_calls(async_on_service_call) await cli.subscribe_home_assistant_states( async_on_state_subscription) hass.async_create_task(entry_data.async_save_to_store()) except APIConnectionError as err: _LOGGER.warning("Error getting initial data for %s: %s", host, err) # Re-connection logic will trigger after this await cli.disconnect() try_connect = await _setup_auto_reconnect_logic(hass, cli, entry, host, on_login) async def complete_setup() -> None: """Complete the config entry setup.""" infos, services = await entry_data.async_load_from_store() await entry_data.async_update_static_infos(hass, entry, infos) await _setup_services(hass, entry_data, services) # Create connection attempt outside of HA's tracked task in order # not to delay startup. hass.loop.create_task(try_connect(is_disconnect=False)) hass.async_create_task(complete_setup()) return True
async def start(self): self.c = c = APIClient(self.loop, '10.2.0.21', 6053, 'MyPassword') await c.connect(login=True) await c.subscribe_states(on_state=self.on_state) await c.request_image_stream()