diff --git a/homeassistant/components/keyboard_remote/__init__.py b/homeassistant/components/keyboard_remote/__init__.py index f0f1497f940..e3d280c2944 100644 --- a/homeassistant/components/keyboard_remote/__init__.py +++ b/homeassistant/components/keyboard_remote/__init__.py @@ -1,11 +1,14 @@ """Receive signals from a keyboard and use it as a remote control.""" # pylint: disable=import-error +from __future__ import annotations + import asyncio from contextlib import suppress import logging import os +from typing import Any -import aionotify +from asyncinotify import Inotify, Mask from evdev import InputDevice, categorize, ecodes, list_devices import voluptuous as vol @@ -64,9 +67,9 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the keyboard_remote.""" - config = config[DOMAIN] + domain_config: list[dict[str, Any]] = config[DOMAIN] - remote = KeyboardRemote(hass, config) + remote = KeyboardRemote(hass, domain_config) remote.setup() return True @@ -75,12 +78,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: class KeyboardRemote: """Manage device connection/disconnection using inotify to asynchronously monitor.""" - def __init__(self, hass, config): + def __init__(self, hass: HomeAssistant, config: list[dict[str, Any]]) -> None: """Create handlers and setup dictionaries to keep track of them.""" self.hass = hass self.handlers_by_name = {} self.handlers_by_descriptor = {} - self.active_handlers_by_descriptor = {} + self.active_handlers_by_descriptor: dict[str, asyncio.Future] = {} + self.inotify = None self.watcher = None self.monitor_task = None @@ -110,16 +114,12 @@ class KeyboardRemote: connected, and start monitoring for device connection/disconnection. """ - # start watching - self.watcher = aionotify.Watcher() - self.watcher.watch( - alias="devinput", - path=DEVINPUT, - flags=aionotify.Flags.CREATE - | aionotify.Flags.ATTRIB - | aionotify.Flags.DELETE, + _LOGGER.debug("Start monitoring") + + self.inotify = Inotify() + self.watcher = self.inotify.add_watch( + DEVINPUT, Mask.CREATE | Mask.ATTRIB | Mask.DELETE ) - await self.watcher.setup(self.hass.loop) # add initial devices (do this AFTER starting watcher in order to # avoid race conditions leading to missing device connections) @@ -134,7 +134,9 @@ class KeyboardRemote: continue self.active_handlers_by_descriptor[descriptor] = handler - initial_start_monitoring.add(handler.async_start_monitoring(dev)) + initial_start_monitoring.add( + asyncio.create_task(handler.async_device_start_monitoring(dev)) + ) if initial_start_monitoring: await asyncio.wait(initial_start_monitoring) @@ -146,6 +148,10 @@ class KeyboardRemote: _LOGGER.debug("Cleanup on shutdown") + if self.inotify and self.watcher: + self.inotify.rm_watch(self.watcher) + self.watcher = None + if self.monitor_task is not None: if not self.monitor_task.done(): self.monitor_task.cancel() @@ -153,11 +159,16 @@ class KeyboardRemote: handler_stop_monitoring = set() for handler in self.active_handlers_by_descriptor.values(): - handler_stop_monitoring.add(handler.async_stop_monitoring()) - + handler_stop_monitoring.add( + asyncio.create_task(handler.async_device_stop_monitoring()) + ) if handler_stop_monitoring: await asyncio.wait(handler_stop_monitoring) + if self.inotify: + self.inotify.close() + self.inotify = None + def get_device_handler(self, descriptor): """Find the correct device handler given a descriptor (path).""" @@ -187,20 +198,21 @@ class KeyboardRemote: async def async_monitor_devices(self): """Monitor asynchronously for device connection/disconnection or permissions changes.""" + _LOGGER.debug("Start monitoring loop") + try: - while True: - event = await self.watcher.get_event() + async for event in self.inotify: descriptor = f"{DEVINPUT}/{event.name}" + _LOGGER.debug("got events for %s: %s", descriptor, event.mask) descriptor_active = descriptor in self.active_handlers_by_descriptor - if (event.flags & aionotify.Flags.DELETE) and descriptor_active: + if (event.mask & Mask.DELETE) and descriptor_active: handler = self.active_handlers_by_descriptor[descriptor] del self.active_handlers_by_descriptor[descriptor] - await handler.async_stop_monitoring() + await handler.async_device_stop_monitoring() elif ( - (event.flags & aionotify.Flags.CREATE) - or (event.flags & aionotify.Flags.ATTRIB) + (event.mask & Mask.CREATE) or (event.mask & Mask.ATTRIB) ) and not descriptor_active: dev, handler = await self.hass.async_add_executor_job( self.get_device_handler, descriptor @@ -208,31 +220,32 @@ class KeyboardRemote: if handler is None: continue self.active_handlers_by_descriptor[descriptor] = handler - await handler.async_start_monitoring(dev) + await handler.async_device_start_monitoring(dev) except asyncio.CancelledError: + _LOGGER.debug("Monitoring canceled") return class DeviceHandler: """Manage input events using evdev with asyncio.""" - def __init__(self, hass, dev_block): + def __init__(self, hass: HomeAssistant, dev_block: dict[str, Any]) -> None: """Fill configuration data.""" self.hass = hass - key_types = dev_block.get(TYPE) + key_types = dev_block[TYPE] self.key_values = set() for key_type in key_types: self.key_values.add(KEY_VALUE[key_type]) - self.emulate_key_hold = dev_block.get(EMULATE_KEY_HOLD) - self.emulate_key_hold_delay = dev_block.get(EMULATE_KEY_HOLD_DELAY) - self.emulate_key_hold_repeat = dev_block.get(EMULATE_KEY_HOLD_REPEAT) + self.emulate_key_hold = dev_block[EMULATE_KEY_HOLD] + self.emulate_key_hold_delay = dev_block[EMULATE_KEY_HOLD_DELAY] + self.emulate_key_hold_repeat = dev_block[EMULATE_KEY_HOLD_REPEAT] self.monitor_task = None self.dev = None - async def async_keyrepeat(self, path, name, code, delay, repeat): + async def async_device_keyrepeat(self, path, name, code, delay, repeat): """Emulate keyboard delay/repeat behaviour by sending key events on a timer.""" await asyncio.sleep(delay) @@ -248,8 +261,9 @@ class KeyboardRemote: ) await asyncio.sleep(repeat) - async def async_start_monitoring(self, dev): + async def async_device_start_monitoring(self, dev): """Start event monitoring task and issue event.""" + _LOGGER.debug("Keyboard async_device_start_monitoring, %s", dev.name) if self.monitor_task is None: self.dev = dev self.monitor_task = self.hass.async_create_task( @@ -261,7 +275,7 @@ class KeyboardRemote: ) _LOGGER.debug("Keyboard (re-)connected, %s", dev.name) - async def async_stop_monitoring(self): + async def async_device_stop_monitoring(self): """Stop event monitoring task and issue event.""" if self.monitor_task is not None: with suppress(OSError): @@ -295,6 +309,7 @@ class KeyboardRemote: _LOGGER.debug("Start device monitoring") await self.hass.async_add_executor_job(dev.grab) async for event in dev.async_read_loop(): + # pylint: disable=no-member if event.type is ecodes.EV_KEY: if event.value in self.key_values: _LOGGER.debug(categorize(event)) @@ -313,7 +328,7 @@ class KeyboardRemote: and self.emulate_key_hold ): repeat_tasks[event.code] = self.hass.async_create_task( - self.async_keyrepeat( + self.async_device_keyrepeat( dev.path, dev.name, event.code, diff --git a/homeassistant/components/keyboard_remote/manifest.json b/homeassistant/components/keyboard_remote/manifest.json index d319ba93ce2..2b298901ca9 100644 --- a/homeassistant/components/keyboard_remote/manifest.json +++ b/homeassistant/components/keyboard_remote/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/keyboard_remote", "iot_class": "local_push", "loggers": ["aionotify", "evdev"], - "requirements": ["evdev==1.4.0", "aionotify==0.2.0"] + "requirements": ["evdev==1.6.1", "asyncinotify==4.0.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index eac5083a293..8f360bcdf80 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -297,9 +297,6 @@ aiomusiccast==0.14.8 # homeassistant.components.nanoleaf aionanoleaf==0.2.1 -# homeassistant.components.keyboard_remote -aionotify==0.2.0 - # homeassistant.components.notion aionotion==2023.05.5 @@ -451,6 +448,9 @@ asterisk-mbox==0.5.0 # homeassistant.components.yeelight async-upnp-client==0.33.2 +# homeassistant.components.keyboard_remote +asyncinotify==4.0.2 + # homeassistant.components.supla asyncpysupla==0.0.5 @@ -758,7 +758,7 @@ eternalegypt==0.0.16 eufylife-ble-client==0.1.7 # homeassistant.components.keyboard_remote -# evdev==1.4.0 +# evdev==1.6.1 # homeassistant.components.evohome evohome-async==0.3.15