Skip to content

Commit

Permalink
Fix keyboard_remote for python 3.11 (#94570)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
2 people authored and balloob committed Jun 15, 2023
1 parent f67577e commit d28d909
Show file tree
Hide file tree
Showing 3 changed files with 53 additions and 38 deletions.
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
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
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 @@ -222,9 +222,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 @@ -382,6 +379,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 @@ -695,7 +695,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

0 comments on commit d28d909

Please sign in to comment.