diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 45d4171d7af09a..ad33cc5e42bfca 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -88,6 +88,8 @@ STATUS_STOPPED = 2 STATUS_WAIT = 3 +PLATFORMS = ["sensor"] + def _has_all_unique_names_and_ports(bridges): """Validate that each homekit bridge configured has a unique name.""" @@ -234,7 +236,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): safe_mode, advertise_ip, interface_choice, - entry.entry_id, + entry, ) await hass.async_add_executor_job(homekit.setup) @@ -271,7 +273,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): homekit = hass.data[DOMAIN][entry.entry_id][HOMEKIT] if homekit.status == STATUS_RUNNING: - await homekit.async_stop() + unload_ok = await homekit.async_stop() for _ in range(0, SHUTDOWN_TIMEOUT): if not await hass.async_add_executor_job( @@ -280,9 +282,10 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): _LOGGER.info("Waiting for the HomeKit server to shutdown.") await asyncio.sleep(1) - hass.data[DOMAIN].pop(entry.entry_id) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) - return True + return unload_ok async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry): @@ -389,7 +392,7 @@ def __init__( safe_mode, advertise_ip=None, interface_choice=None, - entry_id=None, + config_entry=None, ): """Initialize a HomeKit object.""" self.hass = hass @@ -401,7 +404,8 @@ def __init__( self._safe_mode = safe_mode self._advertise_ip = advertise_ip self._interface_choice = interface_choice - self._entry_id = entry_id + self._config_entry = config_entry + self._entry_id = config_entry.entry_id self.status = STATUS_READY self.bridge = None @@ -513,11 +517,19 @@ async def async_start(self, *args): await self.hass.async_add_executor_job(self._start, bridged_states) await self._async_register_bridge() + for component in PLATFORMS: + self.hass.async_create_task( + self.hass.config_entries.async_forward_entry_setup( + self._config_entry, component + ) + ) + async def _async_register_bridge(self): """Register the bridge as a device so homekit_controller and exclude it from discovery.""" registry = await device_registry.async_get_registry(self.hass) registry.async_get_or_create( config_entry_id=self._entry_id, + identifiers={(DOMAIN, self.driver.state.mac)}, connections={ (device_registry.CONNECTION_NETWORK_MAC, self.driver.state.mac) }, @@ -565,6 +577,16 @@ async def async_stop(self, *args): self.status = STATUS_STOPPED _LOGGER.debug("Driver stop for %s", self._name) self.hass.add_job(self.driver.stop) + return all( + await asyncio.gather( + *[ + self.hass.config_entries.async_forward_entry_unload( + self._config_entry, component + ) + for component in PLATFORMS + ] + ) + ) @callback def _async_configure_linked_battery_sensors(self, ent_reg, device_lookup, state): diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index ac6e8969d91f56..30918f592e9496 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -246,6 +246,7 @@ def __init__( self.config = config or {} self.entity_id = entity_id self.hass = hass + self.name = name self.debounce = {} self._char_battery = None self._char_charging = None diff --git a/homeassistant/components/homekit/entity.py b/homeassistant/components/homekit/entity.py new file mode 100644 index 00000000000000..34beb2a02fca5e --- /dev/null +++ b/homeassistant/components/homekit/entity.py @@ -0,0 +1,41 @@ +"""The homekitintegration base entity.""" + +from homeassistant.helpers.entity import Entity + +from .const import DOMAIN, MANUFACTURER + + +class HomeKitEntity(Entity): + """Base class for homekit entities.""" + + def __init__(self, bridge_mac, acc, model): + """Initialize the sensor.""" + super().__init__() + self.model = model + self.acc = acc + self.bridge_mac = bridge_mac + self.base_unique_id = f"{bridge_mac}_{acc.aid}" + + @property + def device_info(self): + """Entity device info.""" + device_info = { + "identifiers": {(DOMAIN, self.base_unique_id)}, + "name": self.name, + "manufacturer": MANUFACTURER, + "model": self.model, + "via_device": (DOMAIN, self.bridge_mac), + } + return device_info + + @property + def should_poll(self): + """Update from callback.""" + return False + + async def async_remove(self): + """Remove from entity registry on remove.""" + entity_registry = await self.hass.helpers.entity_registry.async_get_registry() + if entity_registry.async_get(self.entity_id): + entity_registry.async_remove(self.entity_id) + return await super().async_remove() diff --git a/homeassistant/components/homekit/sensor.py b/homeassistant/components/homekit/sensor.py new file mode 100644 index 00000000000000..9792f5a1d5c37a --- /dev/null +++ b/homeassistant/components/homekit/sensor.py @@ -0,0 +1,66 @@ +"""Support for homekit sensors.""" +import logging + +from homeassistant.core import callback + +from .const import DOMAIN, HOMEKIT +from .entity import HomeKitEntity +from .type_media_players import TelevisionMediaPlayer + +_LOGGER = logging.getLogger(__name__) + +HOMEKIT_REMOTE_MODEL = "HomeKit Remote" + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the homekit sensors.""" + + homekit = hass.data[DOMAIN][config_entry.entry_id][HOMEKIT] + mac = homekit.driver.state.mac + entities = [] + + for acc in homekit.bridge.accessories.values(): + if isinstance(acc, TelevisionMediaPlayer): + entities.append(RemoteKeySensor(mac, acc)) + + async_add_entities(entities, False) + + +class RemoteKeySensor(HomeKitEntity): + """Representation of an homekit remote control sensor.""" + + def __init__(self, mac, acc): + """Init the remote control sensor.""" + super().__init__(mac, acc, HOMEKIT_REMOTE_MODEL) + self._state = None + + @property + def name(self): + """Sensor Name.""" + return f"{self.acc.name} {self.model}" + + @property + def unique_id(self): + """Sensor Uniqueid.""" + return f"{self.base_unique_id}_remote" + + @property + def state(self): + """Key pressed.""" + return self._state + + @callback + def _async_key_pressed(self, value): + """Update the sensor with the keypress.""" + # Press + self._state = value + self.async_write_ha_state() + # .. and release + self._state = None + self.async_write_ha_state() + + async def async_added_to_hass(self): + """Subscribe to updates.""" + self.async_on_remove( + self.acc.async_add_remote_key_listener(self._async_key_pressed) + ) diff --git a/homeassistant/components/homekit/type_media_players.py b/homeassistant/components/homekit/type_media_players.py index 209baad125ed8f..ad431d87c670d2 100644 --- a/homeassistant/components/homekit/type_media_players.py +++ b/homeassistant/components/homekit/type_media_players.py @@ -1,5 +1,6 @@ """Class to hold all media player accessories.""" import logging +from typing import Callable from pyhap.const import CATEGORY_SWITCH, CATEGORY_TELEVISION @@ -36,6 +37,7 @@ STATE_STANDBY, STATE_UNKNOWN, ) +from homeassistant.core import CALLBACK_TYPE, callback from .accessories import TYPES, HomeAccessory from .const import ( @@ -238,7 +240,7 @@ def __init__(self, *args): state = self.hass.states.get(self.entity_id) self.support_select_source = False - + self._remote_key_listeners = [] self.sources = [] # Add additional characteristics if volume or input selection supported @@ -356,9 +358,31 @@ def set_input_source(self, value): params = {ATTR_ENTITY_ID: self.entity_id, ATTR_INPUT_SOURCE: source} self.call_service(DOMAIN, SERVICE_SELECT_SOURCE, params) + @callback + def async_add_remote_key_listener( + self, update_callback: CALLBACK_TYPE + ) -> Callable[[], None]: + """Listen for remote key updates.""" + self._remote_key_listeners.append(update_callback) + + @callback + def remove_listener() -> None: + """Remove remote key listener.""" + self.async_remove_remote_key_listener(update_callback) + + return remove_listener + + @callback + def async_remove_remote_key_listener(self, update_callback: CALLBACK_TYPE) -> None: + """Remove remote key listener.""" + self._remote_key_listeners.remove(update_callback) + def set_remote_key(self, value): """Send remote key value if call came from HomeKit.""" _LOGGER.debug("%s: Set remote key to %s", self.entity_id, value) + for update_callback in self._remote_key_listeners: + update_callback(value) + service = MEDIA_PLAYER_KEYS.get(value) if service: # Handle Play Pause