mirror of
https://github.com/home-assistant/core.git
synced 2025-06-25 01:21:51 +02:00
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:
@ -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,
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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
|
||||
|
Reference in New Issue
Block a user