Fix keyboard_remote for python 3.11 (#94570)

* started work to update keyboard_remote to work with python 3.11

* updated function names

* all checks pass

* fixed asyncio for python 3.11

* cleanup

* Update homeassistant/components/keyboard_remote/__init__.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Update __init__.py

added:
from __future__ import annotations

* Fix typing

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
Ian Foster 2023-06-14 13:06:55 -07:00 committed by GitHub
parent e539344d22
commit e998320053
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 53 additions and 38 deletions

View File

@ -1,11 +1,14 @@
"""Receive signals from a keyboard and use it as a remote control.""" """Receive signals from a keyboard and use it as a remote control."""
# pylint: disable=import-error # pylint: disable=import-error
from __future__ import annotations
import asyncio import asyncio
from contextlib import suppress from contextlib import suppress
import logging import logging
import os import os
from typing import Any
import aionotify from asyncinotify import Inotify, Mask
from evdev import InputDevice, categorize, ecodes, list_devices from evdev import InputDevice, categorize, ecodes, list_devices
import voluptuous as vol import voluptuous as vol
@ -64,9 +67,9 @@ CONFIG_SCHEMA = vol.Schema(
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the keyboard_remote.""" """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() remote.setup()
return True return True
@ -75,12 +78,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
class KeyboardRemote: class KeyboardRemote:
"""Manage device connection/disconnection using inotify to asynchronously monitor.""" """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.""" """Create handlers and setup dictionaries to keep track of them."""
self.hass = hass self.hass = hass
self.handlers_by_name = {} self.handlers_by_name = {}
self.handlers_by_descriptor = {} 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.watcher = None
self.monitor_task = None self.monitor_task = None
@ -110,16 +114,12 @@ class KeyboardRemote:
connected, and start monitoring for device connection/disconnection. connected, and start monitoring for device connection/disconnection.
""" """
# start watching _LOGGER.debug("Start monitoring")
self.watcher = aionotify.Watcher()
self.watcher.watch( self.inotify = Inotify()
alias="devinput", self.watcher = self.inotify.add_watch(
path=DEVINPUT, DEVINPUT, Mask.CREATE | Mask.ATTRIB | Mask.DELETE
flags=aionotify.Flags.CREATE
| aionotify.Flags.ATTRIB
| aionotify.Flags.DELETE,
) )
await self.watcher.setup(self.hass.loop)
# add initial devices (do this AFTER starting watcher in order to # add initial devices (do this AFTER starting watcher in order to
# avoid race conditions leading to missing device connections) # avoid race conditions leading to missing device connections)
@ -134,7 +134,9 @@ class KeyboardRemote:
continue continue
self.active_handlers_by_descriptor[descriptor] = handler 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: if initial_start_monitoring:
await asyncio.wait(initial_start_monitoring) await asyncio.wait(initial_start_monitoring)
@ -146,6 +148,10 @@ class KeyboardRemote:
_LOGGER.debug("Cleanup on shutdown") _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 self.monitor_task is not None:
if not self.monitor_task.done(): if not self.monitor_task.done():
self.monitor_task.cancel() self.monitor_task.cancel()
@ -153,11 +159,16 @@ class KeyboardRemote:
handler_stop_monitoring = set() handler_stop_monitoring = set()
for handler in self.active_handlers_by_descriptor.values(): 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: if handler_stop_monitoring:
await asyncio.wait(handler_stop_monitoring) await asyncio.wait(handler_stop_monitoring)
if self.inotify:
self.inotify.close()
self.inotify = None
def get_device_handler(self, descriptor): def get_device_handler(self, descriptor):
"""Find the correct device handler given a descriptor (path).""" """Find the correct device handler given a descriptor (path)."""
@ -187,20 +198,21 @@ class KeyboardRemote:
async def async_monitor_devices(self): async def async_monitor_devices(self):
"""Monitor asynchronously for device connection/disconnection or permissions changes.""" """Monitor asynchronously for device connection/disconnection or permissions changes."""
_LOGGER.debug("Start monitoring loop")
try: try:
while True: async for event in self.inotify:
event = await self.watcher.get_event()
descriptor = f"{DEVINPUT}/{event.name}" descriptor = f"{DEVINPUT}/{event.name}"
_LOGGER.debug("got events for %s: %s", descriptor, event.mask)
descriptor_active = descriptor in self.active_handlers_by_descriptor 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] handler = self.active_handlers_by_descriptor[descriptor]
del 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 ( elif (
(event.flags & aionotify.Flags.CREATE) (event.mask & Mask.CREATE) or (event.mask & Mask.ATTRIB)
or (event.flags & aionotify.Flags.ATTRIB)
) and not descriptor_active: ) and not descriptor_active:
dev, handler = await self.hass.async_add_executor_job( dev, handler = await self.hass.async_add_executor_job(
self.get_device_handler, descriptor self.get_device_handler, descriptor
@ -208,31 +220,32 @@ class KeyboardRemote:
if handler is None: if handler is None:
continue continue
self.active_handlers_by_descriptor[descriptor] = handler self.active_handlers_by_descriptor[descriptor] = handler
await handler.async_start_monitoring(dev) await handler.async_device_start_monitoring(dev)
except asyncio.CancelledError: except asyncio.CancelledError:
_LOGGER.debug("Monitoring canceled")
return return
class DeviceHandler: class DeviceHandler:
"""Manage input events using evdev with asyncio.""" """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.""" """Fill configuration data."""
self.hass = hass self.hass = hass
key_types = dev_block.get(TYPE) key_types = dev_block[TYPE]
self.key_values = set() self.key_values = set()
for key_type in key_types: for key_type in key_types:
self.key_values.add(KEY_VALUE[key_type]) self.key_values.add(KEY_VALUE[key_type])
self.emulate_key_hold = dev_block.get(EMULATE_KEY_HOLD) self.emulate_key_hold = dev_block[EMULATE_KEY_HOLD]
self.emulate_key_hold_delay = dev_block.get(EMULATE_KEY_HOLD_DELAY) self.emulate_key_hold_delay = dev_block[EMULATE_KEY_HOLD_DELAY]
self.emulate_key_hold_repeat = dev_block.get(EMULATE_KEY_HOLD_REPEAT) self.emulate_key_hold_repeat = dev_block[EMULATE_KEY_HOLD_REPEAT]
self.monitor_task = None self.monitor_task = None
self.dev = 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.""" """Emulate keyboard delay/repeat behaviour by sending key events on a timer."""
await asyncio.sleep(delay) await asyncio.sleep(delay)
@ -248,8 +261,9 @@ class KeyboardRemote:
) )
await asyncio.sleep(repeat) 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.""" """Start event monitoring task and issue event."""
_LOGGER.debug("Keyboard async_device_start_monitoring, %s", dev.name)
if self.monitor_task is None: if self.monitor_task is None:
self.dev = dev self.dev = dev
self.monitor_task = self.hass.async_create_task( self.monitor_task = self.hass.async_create_task(
@ -261,7 +275,7 @@ class KeyboardRemote:
) )
_LOGGER.debug("Keyboard (re-)connected, %s", dev.name) _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.""" """Stop event monitoring task and issue event."""
if self.monitor_task is not None: if self.monitor_task is not None:
with suppress(OSError): with suppress(OSError):
@ -295,6 +309,7 @@ class KeyboardRemote:
_LOGGER.debug("Start device monitoring") _LOGGER.debug("Start device monitoring")
await self.hass.async_add_executor_job(dev.grab) await self.hass.async_add_executor_job(dev.grab)
async for event in dev.async_read_loop(): async for event in dev.async_read_loop():
# pylint: disable=no-member
if event.type is ecodes.EV_KEY: if event.type is ecodes.EV_KEY:
if event.value in self.key_values: if event.value in self.key_values:
_LOGGER.debug(categorize(event)) _LOGGER.debug(categorize(event))
@ -313,7 +328,7 @@ class KeyboardRemote:
and self.emulate_key_hold and self.emulate_key_hold
): ):
repeat_tasks[event.code] = self.hass.async_create_task( repeat_tasks[event.code] = self.hass.async_create_task(
self.async_keyrepeat( self.async_device_keyrepeat(
dev.path, dev.path,
dev.name, dev.name,
event.code, event.code,

View File

@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/keyboard_remote", "documentation": "https://www.home-assistant.io/integrations/keyboard_remote",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["aionotify", "evdev"], "loggers": ["aionotify", "evdev"],
"requirements": ["evdev==1.4.0", "aionotify==0.2.0"] "requirements": ["evdev==1.6.1", "asyncinotify==4.0.2"]
} }

View File

@ -297,9 +297,6 @@ aiomusiccast==0.14.8
# homeassistant.components.nanoleaf # homeassistant.components.nanoleaf
aionanoleaf==0.2.1 aionanoleaf==0.2.1
# homeassistant.components.keyboard_remote
aionotify==0.2.0
# homeassistant.components.notion # homeassistant.components.notion
aionotion==2023.05.5 aionotion==2023.05.5
@ -451,6 +448,9 @@ asterisk-mbox==0.5.0
# homeassistant.components.yeelight # homeassistant.components.yeelight
async-upnp-client==0.33.2 async-upnp-client==0.33.2
# homeassistant.components.keyboard_remote
asyncinotify==4.0.2
# homeassistant.components.supla # homeassistant.components.supla
asyncpysupla==0.0.5 asyncpysupla==0.0.5
@ -758,7 +758,7 @@ eternalegypt==0.0.16
eufylife-ble-client==0.1.7 eufylife-ble-client==0.1.7
# homeassistant.components.keyboard_remote # homeassistant.components.keyboard_remote
# evdev==1.4.0 # evdev==1.6.1
# homeassistant.components.evohome # homeassistant.components.evohome
evohome-async==0.3.15 evohome-async==0.3.15