Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix keyboard_remote for python 3.11 #94570

Merged
merged 8 commits into from
Jun 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
81 changes: 48 additions & 33 deletions homeassistant/components/keyboard_remote/__init__.py
Original file line number Diff line number Diff line change
@@ -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
lanrat marked this conversation as resolved.
Show resolved Hide resolved
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

Expand Down Expand Up @@ -64,9 +67,9 @@

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

Expand Down Expand Up @@ -110,16 +114,12 @@ async def async_start_monitoring(self, event):
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)
Expand All @@ -134,7 +134,9 @@ async def async_start_monitoring(self, event):
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)
Expand All @@ -146,18 +148,27 @@ async def async_stop_monitoring(self, event):

_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()
await self.monitor_task

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)."""

Expand Down Expand Up @@ -187,52 +198,54 @@ def get_device_handler(self, descriptor):
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
)
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)
Expand All @@ -248,8 +261,9 @@ async def async_keyrepeat(self, path, name, code, delay, 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."""
_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(
Expand All @@ -261,7 +275,7 @@ async def async_start_monitoring(self, dev):
)
_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):
Expand Down Expand Up @@ -295,6 +309,7 @@ async def async_monitor_input(self, dev):
_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
lanrat marked this conversation as resolved.
Show resolved Hide resolved
if event.type is ecodes.EV_KEY:
if event.value in self.key_values:
_LOGGER.debug(categorize(event))
Expand All @@ -313,7 +328,7 @@ async def async_monitor_input(self, dev):
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,
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/keyboard_remote/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
8 changes: 4 additions & 4 deletions requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down