File kvmd-python3.9-compat.patch of Package kvmd

From db5b9ec5c699b1d54ec3a162e30e94c281de5057 Mon Sep 17 00:00:00 2001
From: Oleg Girko <ol@infoserver.lv>
Date: Wed, 12 Mar 2025 14:10:58 +0000
Subject: [PATCH] Fix Python 3.9 compatibility.

The following changes are mafe to preserve Python 3.9 compatibility.
* Do not use match statement.
* Import annotations from __future__ to allow type unions with "|" syntax.
* Pass loop argument to asyncio.Queue() constructor in kvmd/aiogp.py
  file, but only if Python version < 3.10 (partially reverts 1d4b39ef1).
* Don't use typing.Self.

This is needed for RHEL 9 that has Python 3.9 by default as well as many
packages built for Python 3.9 that are necessary for KVMD.

Signed-off-by: Oleg Girko <ol@infoserver.lv>
---
 genmap.py                              |  2 +
 kvmd/aiogp.py                          |  6 +-
 kvmd/aiomulti.py                       |  2 +
 kvmd/aioproc.py                        |  2 +
 kvmd/aiotools.py                       |  2 +
 kvmd/apps/__init__.py                  |  2 +
 kvmd/apps/edidconf/__init__.py         |  2 +
 kvmd/apps/htpasswd/__init__.py         |  2 +
 kvmd/apps/ipmi/__init__.py             |  2 +
 kvmd/apps/ipmi/server.py               |  2 +
 kvmd/apps/janus/__init__.py            |  2 +
 kvmd/apps/janus/runner.py              |  2 +
 kvmd/apps/janus/stun.py                |  2 +
 kvmd/apps/kvmd/__init__.py             |  2 +
 kvmd/apps/kvmd/api/log.py              |  2 +
 kvmd/apps/kvmd/api/msd.py              |  2 +
 kvmd/apps/kvmd/auth.py                 |  2 +
 kvmd/apps/kvmd/info/__init__.py        |  2 +
 kvmd/apps/kvmd/info/auth.py            |  2 +
 kvmd/apps/kvmd/info/base.py            |  2 +
 kvmd/apps/kvmd/info/extras.py          |  2 +
 kvmd/apps/kvmd/info/fan.py             |  2 +
 kvmd/apps/kvmd/info/hw.py              |  2 +
 kvmd/apps/kvmd/info/meta.py            |  2 +
 kvmd/apps/kvmd/info/system.py          |  2 +
 kvmd/apps/kvmd/ocr.py                  |  2 +
 kvmd/apps/kvmd/server.py               |  2 +
 kvmd/apps/kvmd/streamer.py             |  2 +
 kvmd/apps/kvmd/switch/__init__.py      | 23 ++++---
 kvmd/apps/kvmd/switch/chain.py         | 94 +++++++++++++-------------
 kvmd/apps/kvmd/switch/device.py        |  2 +
 kvmd/apps/kvmd/switch/proto.py         | 15 ++--
 kvmd/apps/kvmd/switch/state.py         |  2 +
 kvmd/apps/kvmd/switch/types.py         |  2 +
 kvmd/apps/kvmd/sysunit.py              |  2 +
 kvmd/apps/kvmd/ugpio.py                |  2 +
 kvmd/apps/media/__init__.py            |  2 +
 kvmd/apps/media/server.py              |  2 +
 kvmd/apps/ngxmkconf/__init__.py        |  2 +
 kvmd/apps/otg/__init__.py              |  2 +
 kvmd/apps/otg/hid/keyboard.py          |  2 +
 kvmd/apps/otg/hid/mouse.py             |  2 +
 kvmd/apps/otgconf/__init__.py          |  2 +
 kvmd/apps/otgmsd/__init__.py           |  2 +
 kvmd/apps/otgnet/__init__.py           |  2 +
 kvmd/apps/pst/__init__.py              |  2 +
 kvmd/apps/pstrun/__init__.py           |  2 +
 kvmd/apps/swctl/__init__.py            | 44 ++++++------
 kvmd/apps/totp/__init__.py             |  2 +
 kvmd/apps/vnc/__init__.py              |  2 +
 kvmd/apps/vnc/rfb/encodings.py         |  2 +
 kvmd/apps/vnc/rfb/stream.py            |  2 +
 kvmd/apps/vnc/server.py                |  2 +
 kvmd/apps/watchdog/__init__.py         |  2 +
 kvmd/clients/__init__.py               |  5 +-
 kvmd/clients/kvmd.py                   |  2 +
 kvmd/edid.py                           |  2 +
 kvmd/htclient.py                       |  2 +
 kvmd/htserver.py                       |  2 +
 kvmd/inotify.py                        |  2 +
 kvmd/logging.py                        |  2 +
 kvmd/plugins/atx/gpio.py               |  2 +
 kvmd/plugins/auth/http.py              |  2 +
 kvmd/plugins/hid/__init__.py           |  2 +
 kvmd/plugins/hid/_mcu/__init__.py      |  2 +
 kvmd/plugins/hid/_mcu/gpio.py          |  2 +
 kvmd/plugins/hid/bt/__init__.py        |  2 +
 kvmd/plugins/hid/bt/bluez.py           |  2 +
 kvmd/plugins/hid/bt/server.py          |  2 +
 kvmd/plugins/hid/ch9329/__init__.py    |  2 +
 kvmd/plugins/hid/ch9329/mouse.py       | 21 +++---
 kvmd/plugins/hid/otg/__init__.py       |  2 +
 kvmd/plugins/hid/otg/device.py         |  2 +
 kvmd/plugins/hid/otg/events.py         |  2 +
 kvmd/plugins/hid/otg/keyboard.py       |  2 +
 kvmd/plugins/hid/otg/mouse.py          |  2 +
 kvmd/plugins/hid/spi.py                |  2 +
 kvmd/plugins/msd/__init__.py           |  2 +
 kvmd/plugins/msd/disabled.py           |  2 +
 kvmd/plugins/msd/otg/__init__.py       |  2 +
 kvmd/plugins/ugpio/__init__.py         |  2 +
 kvmd/plugins/ugpio/ezcoo.py            |  2 +
 kvmd/plugins/ugpio/gpio.py             |  2 +
 kvmd/plugins/ugpio/hidrelay.py         |  2 +
 kvmd/plugins/ugpio/hue.py              |  2 +
 kvmd/plugins/ugpio/ipmi.py             |  2 +
 kvmd/plugins/ugpio/locator.py          |  2 +
 kvmd/plugins/ugpio/pway.py             |  2 +
 kvmd/plugins/ugpio/pwm.py              |  2 +
 kvmd/plugins/ugpio/tesmart.py          |  2 +
 kvmd/plugins/ugpio/wol.py              |  2 +
 kvmd/plugins/ugpio/xh_hk4401.py        |  2 +
 kvmd/tools.py                          |  2 +
 kvmd/validators/__init__.py            |  2 +
 kvmd/validators/basic.py               |  2 +
 kvmd/validators/ugpio.py               |  2 +
 kvmd/yamlconf/__init__.py              |  2 +
 testenv/linters/mypy.ini               |  2 +-
 testenv/tests/apps/kvmd/test_auth.py   |  2 +
 testenv/tests/plugins/auth/test_pam.py |  2 +
 100 files changed, 290 insertions(+), 104 deletions(-)

diff --git a/genmap.py b/genmap.py
index 61952c9e..f9af235b 100755
--- a/genmap.py
+++ b/genmap.py
@@ -21,6 +21,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import sys
 import csv
 import textwrap
diff --git a/kvmd/aiogp.py b/kvmd/aiogp.py
index 3ee0ea2b..2be9a4f7 100644
--- a/kvmd/aiogp.py
+++ b/kvmd/aiogp.py
@@ -20,6 +20,9 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
+import sys
 import asyncio
 import threading
 import dataclasses
@@ -135,7 +138,8 @@ class _DebouncedValue:
         self.__notifier = notifier
         self.__loop = loop
 
-        self.__queue: "asyncio.Queue[bool]" = asyncio.Queue()  # type: ignore
+        queue_kwargs = ({"loop": loop} if sys.version_info < (3, 10) else {})
+        self.__queue: "asyncio.Queue[bool]" = asyncio.Queue(**queue_kwargs)  # type: ignore
         self.__task = loop.create_task(self.__consumer_task_loop())
 
     def set(self, value: bool) -> None:
diff --git a/kvmd/aiomulti.py b/kvmd/aiomulti.py
index a4537204..b544e741 100644
--- a/kvmd/aiomulti.py
+++ b/kvmd/aiomulti.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import multiprocessing
 import queue
 
diff --git a/kvmd/aioproc.py b/kvmd/aioproc.py
index 376df004..878a490e 100644
--- a/kvmd/aioproc.py
+++ b/kvmd/aioproc.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import os
 import signal
 import asyncio
diff --git a/kvmd/aiotools.py b/kvmd/aiotools.py
index f400ad3c..847c2dfa 100644
--- a/kvmd/aiotools.py
+++ b/kvmd/aiotools.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import os
 import signal
 import asyncio
diff --git a/kvmd/apps/__init__.py b/kvmd/apps/__init__.py
index 763c7c79..4a8f7639 100644
--- a/kvmd/apps/__init__.py
+++ b/kvmd/apps/__init__.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import sys
 import os
 import functools
diff --git a/kvmd/apps/edidconf/__init__.py b/kvmd/apps/edidconf/__init__.py
index e21f797b..a6362922 100644
--- a/kvmd/apps/edidconf/__init__.py
+++ b/kvmd/apps/edidconf/__init__.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import sys
 import os
 import subprocess
diff --git a/kvmd/apps/htpasswd/__init__.py b/kvmd/apps/htpasswd/__init__.py
index 9e857abc..007cb87a 100644
--- a/kvmd/apps/htpasswd/__init__.py
+++ b/kvmd/apps/htpasswd/__init__.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import sys
 import os
 import getpass
diff --git a/kvmd/apps/ipmi/__init__.py b/kvmd/apps/ipmi/__init__.py
index 2f6cebd8..1295ca04 100644
--- a/kvmd/apps/ipmi/__init__.py
+++ b/kvmd/apps/ipmi/__init__.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 from ...clients.kvmd import KvmdClient
 
 from ... import htclient
diff --git a/kvmd/apps/ipmi/server.py b/kvmd/apps/ipmi/server.py
index 2fc897f9..080fbb74 100644
--- a/kvmd/apps/ipmi/server.py
+++ b/kvmd/apps/ipmi/server.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import os
 import select
 import asyncio
diff --git a/kvmd/apps/janus/__init__.py b/kvmd/apps/janus/__init__.py
index 8489fade..fe96e0a5 100644
--- a/kvmd/apps/janus/__init__.py
+++ b/kvmd/apps/janus/__init__.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 from .. import init
 
 from .runner import JanusRunner
diff --git a/kvmd/apps/janus/runner.py b/kvmd/apps/janus/runner.py
index 9e426021..8799fc07 100644
--- a/kvmd/apps/janus/runner.py
+++ b/kvmd/apps/janus/runner.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
 import asyncio
 import asyncio.subprocess
 import socket
diff --git a/kvmd/apps/janus/stun.py b/kvmd/apps/janus/stun.py
index 41cd86e7..f7e08209 100644
--- a/kvmd/apps/janus/stun.py
+++ b/kvmd/apps/janus/stun.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
 import asyncio
 import socket
 import ipaddress
diff --git a/kvmd/apps/kvmd/__init__.py b/kvmd/apps/kvmd/__init__.py
index 088a62ef..01095243 100644
--- a/kvmd/apps/kvmd/__init__.py
+++ b/kvmd/apps/kvmd/__init__.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 from ...logging import get_logger
 
 from ...plugins.hid import get_hid_class
diff --git a/kvmd/apps/kvmd/api/log.py b/kvmd/apps/kvmd/api/log.py
index c82d6bd9..b3583560 100644
--- a/kvmd/apps/kvmd/api/log.py
+++ b/kvmd/apps/kvmd/api/log.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 from aiohttp.web import Request
 from aiohttp.web import StreamResponse
 
diff --git a/kvmd/apps/kvmd/api/msd.py b/kvmd/apps/kvmd/api/msd.py
index 2fa2eb9b..0fb5fce6 100644
--- a/kvmd/apps/kvmd/api/msd.py
+++ b/kvmd/apps/kvmd/api/msd.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import lzma
 import time
 
diff --git a/kvmd/apps/kvmd/auth.py b/kvmd/apps/kvmd/auth.py
index bf979836..6a4a6ad6 100644
--- a/kvmd/apps/kvmd/auth.py
+++ b/kvmd/apps/kvmd/auth.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import secrets
 import pyotp
 
diff --git a/kvmd/apps/kvmd/info/__init__.py b/kvmd/apps/kvmd/info/__init__.py
index 9ede5489..5040c74d 100644
--- a/kvmd/apps/kvmd/info/__init__.py
+++ b/kvmd/apps/kvmd/info/__init__.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import asyncio
 
 from typing import AsyncGenerator
diff --git a/kvmd/apps/kvmd/info/auth.py b/kvmd/apps/kvmd/info/auth.py
index 301cffe3..c07b6042 100644
--- a/kvmd/apps/kvmd/info/auth.py
+++ b/kvmd/apps/kvmd/info/auth.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 from typing import AsyncGenerator
 
 from .... import aiotools
diff --git a/kvmd/apps/kvmd/info/base.py b/kvmd/apps/kvmd/info/base.py
index d090ed34..16cd0379 100644
--- a/kvmd/apps/kvmd/info/base.py
+++ b/kvmd/apps/kvmd/info/base.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 from typing import AsyncGenerator
 
 
diff --git a/kvmd/apps/kvmd/info/extras.py b/kvmd/apps/kvmd/info/extras.py
index a855ad15..02ae2f73 100644
--- a/kvmd/apps/kvmd/info/extras.py
+++ b/kvmd/apps/kvmd/info/extras.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import os
 import re
 import asyncio
diff --git a/kvmd/apps/kvmd/info/fan.py b/kvmd/apps/kvmd/info/fan.py
index 8f3f69c8..080aa809 100644
--- a/kvmd/apps/kvmd/info/fan.py
+++ b/kvmd/apps/kvmd/info/fan.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import copy
 
 from typing import AsyncGenerator
diff --git a/kvmd/apps/kvmd/info/hw.py b/kvmd/apps/kvmd/info/hw.py
index 81cd1af6..e8a7f8aa 100644
--- a/kvmd/apps/kvmd/info/hw.py
+++ b/kvmd/apps/kvmd/info/hw.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import os
 import asyncio
 import copy
diff --git a/kvmd/apps/kvmd/info/meta.py b/kvmd/apps/kvmd/info/meta.py
index 996e648a..44154e40 100644
--- a/kvmd/apps/kvmd/info/meta.py
+++ b/kvmd/apps/kvmd/info/meta.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 from typing import AsyncGenerator
 
 from ....logging import get_logger
diff --git a/kvmd/apps/kvmd/info/system.py b/kvmd/apps/kvmd/info/system.py
index d4a450de..a541fbfb 100644
--- a/kvmd/apps/kvmd/info/system.py
+++ b/kvmd/apps/kvmd/info/system.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import os
 import asyncio
 import platform
diff --git a/kvmd/apps/kvmd/ocr.py b/kvmd/apps/kvmd/ocr.py
index 367c0c80..f4e6104a 100644
--- a/kvmd/apps/kvmd/ocr.py
+++ b/kvmd/apps/kvmd/ocr.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import os
 import stat
 import io
diff --git a/kvmd/apps/kvmd/server.py b/kvmd/apps/kvmd/server.py
index 858ba1b6..1f7fbe56 100644
--- a/kvmd/apps/kvmd/server.py
+++ b/kvmd/apps/kvmd/server.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import dataclasses
 
 from typing import Callable
diff --git a/kvmd/apps/kvmd/streamer.py b/kvmd/apps/kvmd/streamer.py
index 08c48eb1..d1a5b792 100644
--- a/kvmd/apps/kvmd/streamer.py
+++ b/kvmd/apps/kvmd/streamer.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import signal
 import asyncio
 import asyncio.subprocess
diff --git a/kvmd/apps/kvmd/switch/__init__.py b/kvmd/apps/kvmd/switch/__init__.py
index 49bfbd7d..9d178b3f 100644
--- a/kvmd/apps/kvmd/switch/__init__.py
+++ b/kvmd/apps/kvmd/switch/__init__.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import os
 import asyncio
 
@@ -295,17 +297,16 @@ class Switch:  # pylint: disable=too-many-public-methods
 
     async def __systask_events(self) -> None:
         async for event in self.__chain.poll_events():
-            match event:
-                case DeviceFoundEvent():
-                    await self.__load_configs()
-                case ChainTruncatedEvent():
-                    self.__cache.truncate(event.units)
-                case PortActivatedEvent():
-                    self.__cache.update_active_port(event.port)
-                case UnitStateEvent():
-                    self.__cache.update_unit_state(event.unit, event.state)
-                case UnitAtxLedsEvent():
-                    self.__cache.update_unit_atx_leds(event.unit, event.atx_leds)
+            if event == DeviceFoundEvent():
+                await self.__load_configs()
+            elif event == ChainTruncatedEvent():
+                self.__cache.truncate(event.units)
+            elif event == PortActivatedEvent():
+                self.__cache.update_active_port(event.port)
+            elif event == UnitStateEvent():
+                self.__cache.update_unit_state(event.unit, event.state)
+            elif event == UnitAtxLedsEvent():
+                self.__cache.update_unit_atx_leds(event.unit, event.atx_leds)
 
     async def __load_configs(self) -> None:
         async with self.__lock:
diff --git a/kvmd/apps/kvmd/switch/chain.py b/kvmd/apps/kvmd/switch/chain.py
index 8e4d94eb..916af547 100644
--- a/kvmd/apps/kvmd/switch/chain.py
+++ b/kvmd/apps/kvmd/switch/chain.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import multiprocessing
 import queue
 import select
@@ -310,59 +312,57 @@ class Chain:  # pylint: disable=too-many-instance-attributes
     def __consume_commands(self) -> None:
         while not self.__cmd_queue.empty():
             cmd = self.__cmd_queue.get()
-            match cmd:
-                case _CmdSetActual():
-                    self.__actual = cmd.actual
-
-                case _CmdSetActivePort():
-                    # Может быть вызвано изнутри при синхронизации
-                    self.__active_port = cmd.port
-                    self.__queue_event(PortActivatedEvent(self.__active_port))
-
-                case _CmdSetPortBeacon():
-                    (unit, ch) = self.get_real_unit_channel(cmd.port)
-                    self.__device.request_beacon(unit, ch, cmd.on)
-
-                case _CmdSetUnitBeacon():
-                    ch = (4 if cmd.downlink else 5)
-                    self.__device.request_beacon(cmd.unit, ch, cmd.on)
-
-                case _CmdAtxClick():
-                    (unit, ch) = self.get_real_unit_channel(cmd.port)
-                    if unit < len(self.__units):
-                        (allowed, powered) = self.__units[unit].is_atx_allowed(ch)
-                        if allowed and (cmd.if_powered is None or cmd.if_powered == powered):
-                            delay_ms = min(int(cmd.delay * 1000), 0xFFFF)
-                            if cmd.reset:
-                                self.__device.request_atx_cr(unit, ch, delay_ms)
-                            else:
-                                self.__device.request_atx_cp(unit, ch, delay_ms)
-
-                case _CmdSetEdids():
-                    self.__edids = cmd.edids
-
-                case _CmdSetColors():
-                    self.__colors = cmd.colors
-
-                case _CmdRebootUnit():
-                    self.__device.request_reboot(cmd.unit, cmd.bootloader)
+            if cmd == _CmdSetActual():
+                self.__actual = cmd.actual
+
+            elif cmd == _CmdSetActivePort():
+                # Может быть вызвано изнутри при синхронизации
+                self.__active_port = cmd.port
+                self.__queue_event(PortActivatedEvent(self.__active_port))
+
+            elif cmd == _CmdSetPortBeacon():
+                (unit, ch) = self.get_real_unit_channel(cmd.port)
+                self.__device.request_beacon(unit, ch, cmd.on)
+
+            elif cmd == _CmdSetUnitBeacon():
+                ch = (4 if cmd.downlink else 5)
+                self.__device.request_beacon(cmd.unit, ch, cmd.on)
+
+            elif cmd == _CmdAtxClick():
+                (unit, ch) = self.get_real_unit_channel(cmd.port)
+                if unit < len(self.__units):
+                    (allowed, powered) = self.__units[unit].is_atx_allowed(ch)
+                    if allowed and (cmd.if_powered is None or cmd.if_powered == powered):
+                        delay_ms = min(int(cmd.delay * 1000), 0xFFFF)
+                        if cmd.reset:
+                            self.__device.request_atx_cr(unit, ch, delay_ms)
+                        else:
+                            self.__device.request_atx_cp(unit, ch, delay_ms)
+
+            elif cmd == _CmdSetEdids():
+                self.__edids = cmd.edids
+
+            elif cmd == _CmdSetColors():
+                self.__colors = cmd.colors
+
+            elif cmd == _CmdRebootUnit():
+                self.__device.request_reboot(cmd.unit, cmd.bootloader)
 
     def __update_units(self, resp: Response) -> None:
         units = resp.header.unit + 1
         while len(self.__units) < units:
             self.__units.append(_UnitContext())
 
-        match resp.body:
-            case UnitState():
-                if not resp.body.flags.has_downlink and len(self.__units) > units:
-                    del self.__units[units:]
-                    self.__queue_event(ChainTruncatedEvent(units))
-                self.__units[resp.header.unit].state = resp.body
-                self.__queue_event(UnitStateEvent(resp.header.unit, resp.body))
-
-            case UnitAtxLeds():
-                self.__units[resp.header.unit].atx_leds = resp.body
-                self.__queue_event(UnitAtxLedsEvent(resp.header.unit, resp.body))
+        if resp.body == UnitState():
+            if not resp.body.flags.has_downlink and len(self.__units) > units:
+                del self.__units[units:]
+                self.__queue_event(ChainTruncatedEvent(units))
+            self.__units[resp.header.unit].state = resp.body
+            self.__queue_event(UnitStateEvent(resp.header.unit, resp.body))
+
+        elif resp.body == UnitAtxLeds():
+            self.__units[resp.header.unit].atx_leds = resp.body
+            self.__queue_event(UnitAtxLedsEvent(resp.header.unit, resp.body))
 
     def __adjust_start_port(self) -> None:
         if self.__active_port < 0:
diff --git a/kvmd/apps/kvmd/switch/device.py b/kvmd/apps/kvmd/switch/device.py
index b56cc406..44548e0a 100644
--- a/kvmd/apps/kvmd/switch/device.py
+++ b/kvmd/apps/kvmd/switch/device.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import os
 import random
 import types
diff --git a/kvmd/apps/kvmd/switch/proto.py b/kvmd/apps/kvmd/switch/proto.py
index d4f43f84..13a3af80 100644
--- a/kvmd/apps/kvmd/switch/proto.py
+++ b/kvmd/apps/kvmd/switch/proto.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import struct
 import dataclasses
 
@@ -284,12 +286,11 @@ class Response:
     @classmethod
     def unpack(cls, msg: bytes) -> Optional["Response"]:
         header = Header.unpack(msg)
-        match header.op:
-            case Header.NAK:
-                return Response(header, Nak.unpack(msg, Header.SIZE))
-            case Header.STATE:
-                return Response(header, UnitState.unpack(msg, Header.SIZE))
-            case Header.ATX_LEDS:
-                return Response(header, UnitAtxLeds.unpack(msg, Header.SIZE))
+        if header.op == Header.NAK:
+            return Response(header, Nak.unpack(msg, Header.SIZE))
+        elif header.op == Header.STATE:
+            return Response(header, UnitState.unpack(msg, Header.SIZE))
+        elif header.op == Header.ATX_LEDS:
+            return Response(header, UnitAtxLeds.unpack(msg, Header.SIZE))
         # raise RuntimeError(f"Unknown OP in the header: {header!r}")
         return None
diff --git a/kvmd/apps/kvmd/switch/state.py b/kvmd/apps/kvmd/switch/state.py
index e49d0062..5d0b7ac5 100644
--- a/kvmd/apps/kvmd/switch/state.py
+++ b/kvmd/apps/kvmd/switch/state.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import asyncio
 import dataclasses
 import time
diff --git a/kvmd/apps/kvmd/switch/types.py b/kvmd/apps/kvmd/switch/types.py
index 32225f06..5f47edef 100644
--- a/kvmd/apps/kvmd/switch/types.py
+++ b/kvmd/apps/kvmd/switch/types.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import re
 import struct
 import uuid
diff --git a/kvmd/apps/kvmd/sysunit.py b/kvmd/apps/kvmd/sysunit.py
index bcb41c34..f9f02f55 100644
--- a/kvmd/apps/kvmd/sysunit.py
+++ b/kvmd/apps/kvmd/sysunit.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import types
 
 import dbus_next
diff --git a/kvmd/apps/kvmd/ugpio.py b/kvmd/apps/kvmd/ugpio.py
index a3c453f5..31eb3060 100644
--- a/kvmd/apps/kvmd/ugpio.py
+++ b/kvmd/apps/kvmd/ugpio.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import asyncio
 import copy
 
diff --git a/kvmd/apps/media/__init__.py b/kvmd/apps/media/__init__.py
index 325a817c..baa4ce69 100644
--- a/kvmd/apps/media/__init__.py
+++ b/kvmd/apps/media/__init__.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 from ...clients.streamer import StreamerFormats
 from ...clients.streamer import MemsinkStreamerClient
 
diff --git a/kvmd/apps/media/server.py b/kvmd/apps/media/server.py
index 1f96b353..954f93f5 100644
--- a/kvmd/apps/media/server.py
+++ b/kvmd/apps/media/server.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import asyncio
 import dataclasses
 
diff --git a/kvmd/apps/ngxmkconf/__init__.py b/kvmd/apps/ngxmkconf/__init__.py
index 67c25bcb..954dade3 100644
--- a/kvmd/apps/ngxmkconf/__init__.py
+++ b/kvmd/apps/ngxmkconf/__init__.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import os
 import argparse
 
diff --git a/kvmd/apps/otg/__init__.py b/kvmd/apps/otg/__init__.py
index e1e886cb..8c50e55d 100644
--- a/kvmd/apps/otg/__init__.py
+++ b/kvmd/apps/otg/__init__.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import os
 import re
 import shutil
diff --git a/kvmd/apps/otg/hid/keyboard.py b/kvmd/apps/otg/hid/keyboard.py
index e3232afa..bc0c0a4a 100644
--- a/kvmd/apps/otg/hid/keyboard.py
+++ b/kvmd/apps/otg/hid/keyboard.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 from . import Hid
 
 
diff --git a/kvmd/apps/otg/hid/mouse.py b/kvmd/apps/otg/hid/mouse.py
index 2fcb578c..85134426 100644
--- a/kvmd/apps/otg/hid/mouse.py
+++ b/kvmd/apps/otg/hid/mouse.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 from . import Hid
 
 
diff --git a/kvmd/apps/otgconf/__init__.py b/kvmd/apps/otgconf/__init__.py
index b57f0df1..f58334a6 100644
--- a/kvmd/apps/otgconf/__init__.py
+++ b/kvmd/apps/otgconf/__init__.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import os
 import json
 import contextlib
diff --git a/kvmd/apps/otgmsd/__init__.py b/kvmd/apps/otgmsd/__init__.py
index a289307f..b0761c83 100644
--- a/kvmd/apps/otgmsd/__init__.py
+++ b/kvmd/apps/otgmsd/__init__.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import errno
 import argparse
 
diff --git a/kvmd/apps/otgnet/__init__.py b/kvmd/apps/otgnet/__init__.py
index 35c0bc45..313bce2d 100644
--- a/kvmd/apps/otgnet/__init__.py
+++ b/kvmd/apps/otgnet/__init__.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import asyncio
 import ipaddress
 import dataclasses
diff --git a/kvmd/apps/pst/__init__.py b/kvmd/apps/pst/__init__.py
index d6ff2896..6327cd9d 100644
--- a/kvmd/apps/pst/__init__.py
+++ b/kvmd/apps/pst/__init__.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 from ...logging import get_logger
 
 from .. import init
diff --git a/kvmd/apps/pstrun/__init__.py b/kvmd/apps/pstrun/__init__.py
index d55835fd..385f6060 100644
--- a/kvmd/apps/pstrun/__init__.py
+++ b/kvmd/apps/pstrun/__init__.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import sys
 import os
 import signal
diff --git a/kvmd/apps/swctl/__init__.py b/kvmd/apps/swctl/__init__.py
index d5915d74..831650a4 100644
--- a/kvmd/apps/swctl/__init__.py
+++ b/kvmd/apps/swctl/__init__.py
@@ -132,29 +132,27 @@ def main() -> None:  # pylint: disable=too-many-statements
 
     with Device(opts.device) as device:
         wait_rid: (int | None) = None
-        match opts.cmd:
-            case "poll":
-                device.request_state()
-                device.request_atx_leds()
-            case "state":
-                wait_rid = device.request_state()
-            case "bootloader" | "reboot":
-                device.request_reboot(opts.unit, (opts.cmd == "bootloader"))
-                raise SystemExit()
-            case "switch":
-                wait_rid = device.request_switch(opts.unit, opts.port)
-            case "leds":
-                wait_rid = device.request_atx_leds()
-            case "click":
-                match opts.button:
-                    case "power":
-                        wait_rid = device.request_atx_cp(opts.unit, opts.port, opts.delay_ms)
-                    case "reset":
-                        wait_rid = device.request_atx_cr(opts.unit, opts.port, opts.delay_ms)
-            case "beacon":
-                wait_rid = device.request_beacon(opts.unit, opts.port, (opts.on == "on"))
-            case "set-edid":
-                wait_rid = device.request_set_edid(opts.unit, opts.port, opts.edid)
+        if opts.cmd == "poll":
+            device.request_state()
+            device.request_atx_leds()
+        elif opts.cmd == "state":
+            wait_rid = device.request_state()
+        elif opts.cmd == "bootloader" | "reboot":
+            device.request_reboot(opts.unit, (opts.cmd == "bootloader"))
+            raise SystemExit()
+        elif opts.cmd == "switch":
+            wait_rid = device.request_switch(opts.unit, opts.port)
+        elif opts.cmd == "leds":
+            wait_rid = device.request_atx_leds()
+        elif opts.cmd == "click":
+            if opts.button == "power":
+                wait_rid = device.request_atx_cp(opts.unit, opts.port, opts.delay_ms)
+            elif opts.button == "reset":
+                wait_rid = device.request_atx_cr(opts.unit, opts.port, opts.delay_ms)
+        elif opts.cmd == "beacon":
+            wait_rid = device.request_beacon(opts.unit, opts.port, (opts.on == "on"))
+        elif opts.cmd == "set-edid":
+            wait_rid = device.request_set_edid(opts.unit, opts.port, opts.edid)
 
         error_ts = time.monotonic() + 1
         while True:
diff --git a/kvmd/apps/totp/__init__.py b/kvmd/apps/totp/__init__.py
index e61a79b5..4fd9650c 100644
--- a/kvmd/apps/totp/__init__.py
+++ b/kvmd/apps/totp/__init__.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import socket
 import argparse
 
diff --git a/kvmd/apps/vnc/__init__.py b/kvmd/apps/vnc/__init__.py
index 1e2c486a..d972d806 100644
--- a/kvmd/apps/vnc/__init__.py
+++ b/kvmd/apps/vnc/__init__.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 from ...clients.kvmd import KvmdClient
 from ...clients.streamer import StreamerFormats
 from ...clients.streamer import BaseStreamerClient
diff --git a/kvmd/apps/vnc/rfb/encodings.py b/kvmd/apps/vnc/rfb/encodings.py
index 940a383f..44c1f808 100644
--- a/kvmd/apps/vnc/rfb/encodings.py
+++ b/kvmd/apps/vnc/rfb/encodings.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import dataclasses
 
 from typing import Any
diff --git a/kvmd/apps/vnc/rfb/stream.py b/kvmd/apps/vnc/rfb/stream.py
index dc3ceb1b..53add06f 100644
--- a/kvmd/apps/vnc/rfb/stream.py
+++ b/kvmd/apps/vnc/rfb/stream.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import asyncio
 import ssl
 import struct
diff --git a/kvmd/apps/vnc/server.py b/kvmd/apps/vnc/server.py
index b2ae71fa..a182a2fb 100644
--- a/kvmd/apps/vnc/server.py
+++ b/kvmd/apps/vnc/server.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import os
 import asyncio
 import socket
diff --git a/kvmd/apps/watchdog/__init__.py b/kvmd/apps/watchdog/__init__.py
index c2b03730..71f0f058 100644
--- a/kvmd/apps/watchdog/__init__.py
+++ b/kvmd/apps/watchdog/__init__.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import argparse
 import errno
 import time
diff --git a/kvmd/clients/__init__.py b/kvmd/clients/__init__.py
index e917c9f6..06f871aa 100644
--- a/kvmd/clients/__init__.py
+++ b/kvmd/clients/__init__.py
@@ -20,10 +20,11 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import types
 
 from typing import Callable
-from typing import Self
 
 import aiohttp
 
@@ -44,7 +45,7 @@ class BaseHttpClientSession:
             await self.__http_session.close()
             self.__http_session = None
 
-    async def __aenter__(self) -> Self:
+    async def __aenter__(self):
         return self
 
     async def __aexit__(
diff --git a/kvmd/clients/kvmd.py b/kvmd/clients/kvmd.py
index d9b38339..439f62b2 100644
--- a/kvmd/clients/kvmd.py
+++ b/kvmd/clients/kvmd.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import asyncio
 import contextlib
 import struct
diff --git a/kvmd/edid.py b/kvmd/edid.py
index bfa62e3c..0d563a8c 100644
--- a/kvmd/edid.py
+++ b/kvmd/edid.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import os
 import re
 import dataclasses
diff --git a/kvmd/htclient.py b/kvmd/htclient.py
index 5978b189..2fa795b3 100644
--- a/kvmd/htclient.py
+++ b/kvmd/htclient.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import os
 import contextlib
 
diff --git a/kvmd/htserver.py b/kvmd/htserver.py
index 1ef3cc48..62eeab5f 100644
--- a/kvmd/htserver.py
+++ b/kvmd/htserver.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import os
 import socket
 import asyncio
diff --git a/kvmd/inotify.py b/kvmd/inotify.py
index e2cc6251..7d282d96 100644
--- a/kvmd/inotify.py
+++ b/kvmd/inotify.py
@@ -22,6 +22,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import sys
 import os
 import asyncio
diff --git a/kvmd/logging.py b/kvmd/logging.py
index a31dc24d..673ff233 100644
--- a/kvmd/logging.py
+++ b/kvmd/logging.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import sys
 import types
 import logging
diff --git a/kvmd/plugins/atx/gpio.py b/kvmd/plugins/atx/gpio.py
index 578d2717..e441e655 100644
--- a/kvmd/plugins/atx/gpio.py
+++ b/kvmd/plugins/atx/gpio.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import asyncio
 import copy
 
diff --git a/kvmd/plugins/auth/http.py b/kvmd/plugins/auth/http.py
index b59218aa..4869737c 100644
--- a/kvmd/plugins/auth/http.py
+++ b/kvmd/plugins/auth/http.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import aiohttp
 import aiohttp.web
 
diff --git a/kvmd/plugins/hid/__init__.py b/kvmd/plugins/hid/__init__.py
index a385023a..240bc51b 100644
--- a/kvmd/plugins/hid/__init__.py
+++ b/kvmd/plugins/hid/__init__.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import asyncio
 import functools
 import time
diff --git a/kvmd/plugins/hid/_mcu/__init__.py b/kvmd/plugins/hid/_mcu/__init__.py
index d6f04f76..124f98f0 100644
--- a/kvmd/plugins/hid/_mcu/__init__.py
+++ b/kvmd/plugins/hid/_mcu/__init__.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import multiprocessing
 import contextlib
 import queue
diff --git a/kvmd/plugins/hid/_mcu/gpio.py b/kvmd/plugins/hid/_mcu/gpio.py
index ce1d678b..678edc3c 100644
--- a/kvmd/plugins/hid/_mcu/gpio.py
+++ b/kvmd/plugins/hid/_mcu/gpio.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import types
 import time
 
diff --git a/kvmd/plugins/hid/bt/__init__.py b/kvmd/plugins/hid/bt/__init__.py
index 0c95a6d5..534e7896 100644
--- a/kvmd/plugins/hid/bt/__init__.py
+++ b/kvmd/plugins/hid/bt/__init__.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import multiprocessing
 import copy
 import time
diff --git a/kvmd/plugins/hid/bt/bluez.py b/kvmd/plugins/hid/bt/bluez.py
index 3e6e3f43..41076141 100644
--- a/kvmd/plugins/hid/bt/bluez.py
+++ b/kvmd/plugins/hid/bt/bluez.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import types
 
 from typing import Any
diff --git a/kvmd/plugins/hid/bt/server.py b/kvmd/plugins/hid/bt/server.py
index ad2b6982..0679d99b 100644
--- a/kvmd/plugins/hid/bt/server.py
+++ b/kvmd/plugins/hid/bt/server.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import socket
 import select
 import multiprocessing
diff --git a/kvmd/plugins/hid/ch9329/__init__.py b/kvmd/plugins/hid/ch9329/__init__.py
index 1b235090..d92fadd9 100644
--- a/kvmd/plugins/hid/ch9329/__init__.py
+++ b/kvmd/plugins/hid/ch9329/__init__.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import multiprocessing
 import queue
 import copy
diff --git a/kvmd/plugins/hid/ch9329/mouse.py b/kvmd/plugins/hid/ch9329/mouse.py
index d61bab4e..5372434f 100644
--- a/kvmd/plugins/hid/ch9329/mouse.py
+++ b/kvmd/plugins/hid/ch9329/mouse.py
@@ -43,19 +43,16 @@ class Mouse:  # pylint: disable=too-many-instance-attributes
     def is_absolute(self) -> bool:
         return self.__absolute
 
+    button_code = {
+        "left":   0x01,
+        "right":  0x02,
+        "middle": 0x04,
+        "up":     0x08,
+        "down":   0x10,
+    }
+
     def process_button(self, button: str, state: bool) -> bytes:
-        code = 0x00
-        match button:
-            case "left":
-                code = 0x01
-            case "right":
-                code = 0x02
-            case "middle":
-                code = 0x04
-            case "up":
-                code = 0x08
-            case "down":
-                code = 0x10
+        code = button_code.get(button, 0x00)
         if code:
             if state:
                 self.__buttons |= code
diff --git a/kvmd/plugins/hid/otg/__init__.py b/kvmd/plugins/hid/otg/__init__.py
index 25424257..f87bc0c8 100644
--- a/kvmd/plugins/hid/otg/__init__.py
+++ b/kvmd/plugins/hid/otg/__init__.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import copy
 
 from typing import AsyncGenerator
diff --git a/kvmd/plugins/hid/otg/device.py b/kvmd/plugins/hid/otg/device.py
index a3bc2739..8b3479f0 100644
--- a/kvmd/plugins/hid/otg/device.py
+++ b/kvmd/plugins/hid/otg/device.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import os
 import select
 import multiprocessing
diff --git a/kvmd/plugins/hid/otg/events.py b/kvmd/plugins/hid/otg/events.py
index 44f5e373..f094bb2d 100644
--- a/kvmd/plugins/hid/otg/events.py
+++ b/kvmd/plugins/hid/otg/events.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import struct
 import dataclasses
 
diff --git a/kvmd/plugins/hid/otg/keyboard.py b/kvmd/plugins/hid/otg/keyboard.py
index e82d95a3..3ac199d1 100644
--- a/kvmd/plugins/hid/otg/keyboard.py
+++ b/kvmd/plugins/hid/otg/keyboard.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 from typing import Generator
 from typing import Any
 
diff --git a/kvmd/plugins/hid/otg/mouse.py b/kvmd/plugins/hid/otg/mouse.py
index ae8d53b0..beef0c33 100644
--- a/kvmd/plugins/hid/otg/mouse.py
+++ b/kvmd/plugins/hid/otg/mouse.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 from typing import Generator
 from typing import Any
 
diff --git a/kvmd/plugins/hid/spi.py b/kvmd/plugins/hid/spi.py
index cf58b5de..570f226d 100644
--- a/kvmd/plugins/hid/spi.py
+++ b/kvmd/plugins/hid/spi.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import os
 import contextlib
 import time
diff --git a/kvmd/plugins/msd/__init__.py b/kvmd/plugins/msd/__init__.py
index b2f9d50e..0e8c5b0b 100644
--- a/kvmd/plugins/msd/__init__.py
+++ b/kvmd/plugins/msd/__init__.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import os
 import contextlib
 import time
diff --git a/kvmd/plugins/msd/disabled.py b/kvmd/plugins/msd/disabled.py
index b9f14f6e..1f6b7df3 100644
--- a/kvmd/plugins/msd/disabled.py
+++ b/kvmd/plugins/msd/disabled.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import contextlib
 
 from typing import AsyncGenerator
diff --git a/kvmd/plugins/msd/otg/__init__.py b/kvmd/plugins/msd/otg/__init__.py
index 6d0dd776..6fb43f02 100644
--- a/kvmd/plugins/msd/otg/__init__.py
+++ b/kvmd/plugins/msd/otg/__init__.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import asyncio
 import contextlib
 import dataclasses
diff --git a/kvmd/plugins/ugpio/__init__.py b/kvmd/plugins/ugpio/__init__.py
index 6f9dfaec..d4f9b3a2 100644
--- a/kvmd/plugins/ugpio/__init__.py
+++ b/kvmd/plugins/ugpio/__init__.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 from typing import Callable
 from typing import Any
 
diff --git a/kvmd/plugins/ugpio/ezcoo.py b/kvmd/plugins/ugpio/ezcoo.py
index d5bd8ef8..b0aa96d8 100644
--- a/kvmd/plugins/ugpio/ezcoo.py
+++ b/kvmd/plugins/ugpio/ezcoo.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import re
 import multiprocessing
 import functools
diff --git a/kvmd/plugins/ugpio/gpio.py b/kvmd/plugins/ugpio/gpio.py
index 6cda826b..e2720af9 100644
--- a/kvmd/plugins/ugpio/gpio.py
+++ b/kvmd/plugins/ugpio/gpio.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 from typing import Callable
 from typing import Any
 
diff --git a/kvmd/plugins/ugpio/hidrelay.py b/kvmd/plugins/ugpio/hidrelay.py
index 17f41e27..c4e44ca6 100644
--- a/kvmd/plugins/ugpio/hidrelay.py
+++ b/kvmd/plugins/ugpio/hidrelay.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import asyncio
 import contextlib
 import functools
diff --git a/kvmd/plugins/ugpio/hue.py b/kvmd/plugins/ugpio/hue.py
index 9ed9e206..581cd759 100644
--- a/kvmd/plugins/ugpio/hue.py
+++ b/kvmd/plugins/ugpio/hue.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import asyncio
 
 from typing import Callable
diff --git a/kvmd/plugins/ugpio/ipmi.py b/kvmd/plugins/ugpio/ipmi.py
index 37a7a16f..7c364115 100644
--- a/kvmd/plugins/ugpio/ipmi.py
+++ b/kvmd/plugins/ugpio/ipmi.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import asyncio
 import functools
 
diff --git a/kvmd/plugins/ugpio/locator.py b/kvmd/plugins/ugpio/locator.py
index d5cba719..26d3c277 100644
--- a/kvmd/plugins/ugpio/locator.py
+++ b/kvmd/plugins/ugpio/locator.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import asyncio
 
 from typing import Callable
diff --git a/kvmd/plugins/ugpio/pway.py b/kvmd/plugins/ugpio/pway.py
index 140cf02a..ba69a90a 100644
--- a/kvmd/plugins/ugpio/pway.py
+++ b/kvmd/plugins/ugpio/pway.py
@@ -22,6 +22,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import re
 import multiprocessing
 import functools
diff --git a/kvmd/plugins/ugpio/pwm.py b/kvmd/plugins/ugpio/pwm.py
index f202836e..6fa0092d 100644
--- a/kvmd/plugins/ugpio/pwm.py
+++ b/kvmd/plugins/ugpio/pwm.py
@@ -21,6 +21,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 from typing import Callable
 from typing import Any
 
diff --git a/kvmd/plugins/ugpio/tesmart.py b/kvmd/plugins/ugpio/tesmart.py
index bb1d39e1..4be48844 100644
--- a/kvmd/plugins/ugpio/tesmart.py
+++ b/kvmd/plugins/ugpio/tesmart.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import asyncio
 import functools
 
diff --git a/kvmd/plugins/ugpio/wol.py b/kvmd/plugins/ugpio/wol.py
index e3a93be7..14f35fe8 100644
--- a/kvmd/plugins/ugpio/wol.py
+++ b/kvmd/plugins/ugpio/wol.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import socket
 import functools
 
diff --git a/kvmd/plugins/ugpio/xh_hk4401.py b/kvmd/plugins/ugpio/xh_hk4401.py
index d7a47679..1004db39 100644
--- a/kvmd/plugins/ugpio/xh_hk4401.py
+++ b/kvmd/plugins/ugpio/xh_hk4401.py
@@ -21,6 +21,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import re
 import multiprocessing
 import functools
diff --git a/kvmd/tools.py b/kvmd/tools.py
index 6dd7d2f9..63845e72 100644
--- a/kvmd/tools.py
+++ b/kvmd/tools.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import asyncio
 import operator
 import functools
diff --git a/kvmd/validators/__init__.py b/kvmd/validators/__init__.py
index aa997ab9..d571b6a4 100644
--- a/kvmd/validators/__init__.py
+++ b/kvmd/validators/__init__.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import re
 
 from typing import Mapping
diff --git a/kvmd/validators/basic.py b/kvmd/validators/basic.py
index eae844d9..27d39615 100644
--- a/kvmd/validators/basic.py
+++ b/kvmd/validators/basic.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import re
 
 from typing import Type
diff --git a/kvmd/validators/ugpio.py b/kvmd/validators/ugpio.py
index 1cb0e3e4..6aadbccf 100644
--- a/kvmd/validators/ugpio.py
+++ b/kvmd/validators/ugpio.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 from typing import Any
 
 from . import raise_error
diff --git a/kvmd/yamlconf/__init__.py b/kvmd/yamlconf/__init__.py
index 7cd3808d..7c86ef27 100644
--- a/kvmd/yamlconf/__init__.py
+++ b/kvmd/yamlconf/__init__.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import contextlib
 import json
 
diff --git a/testenv/linters/mypy.ini b/testenv/linters/mypy.ini
index d436fd2c..00dd6881 100644
--- a/testenv/linters/mypy.ini
+++ b/testenv/linters/mypy.ini
@@ -1,5 +1,5 @@
 [mypy]
-python_version = 3.11
+python_version = 3.9
 ignore_missing_imports = true
 disallow_untyped_defs = true
 strict_optional = true
diff --git a/testenv/tests/apps/kvmd/test_auth.py b/testenv/tests/apps/kvmd/test_auth.py
index 4fa1c8ae..40df3168 100644
--- a/testenv/tests/apps/kvmd/test_auth.py
+++ b/testenv/tests/apps/kvmd/test_auth.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import os
 import contextlib
 
diff --git a/testenv/tests/plugins/auth/test_pam.py b/testenv/tests/plugins/auth/test_pam.py
index 26bf11be..b1d57c16 100644
--- a/testenv/tests/plugins/auth/test_pam.py
+++ b/testenv/tests/plugins/auth/test_pam.py
@@ -20,6 +20,8 @@
 # ========================================================================== #
 
 
+from __future__ import annotations
+
 import os
 import asyncio
 import pwd
-- 
2.51.1