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
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."""
# 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,

View File

@ -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"]
}

View File

@ -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