Skip to content

Commit 72fbebc

Browse files
committed
Dbus client report handler
1 parent e50ca75 commit 72fbebc

File tree

5 files changed

+135
-28
lines changed

5 files changed

+135
-28
lines changed

crystalfontz/dbus/client/__init__.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,22 @@
77
from crystalfontz.dbus.config import StagedConfig
88
from crystalfontz.dbus.domain import ConfigM
99
from crystalfontz.dbus.interface import DBUS_NAME, DbusInterface
10+
from crystalfontz.dbus.report import DbusReportHandler
1011

1112

1213
class DbusClient(DbusInterface):
1314
"""
1415
A DBus client for the Crystalfontz device.
1516
"""
1617

17-
def __init__(self: Self, bus: Optional[SdBus] = None) -> None:
18+
def __init__(
19+
self: Self,
20+
bus: Optional[SdBus] = None,
21+
report_handler: Optional[DbusReportHandler] = None,
22+
) -> None:
1823
client = Mock(name="client", side_effect=NotImplementedError("client"))
1924
self.subscribe = Mock(name="client.subscribe")
20-
super().__init__(client)
25+
super().__init__(client, report_handler=report_handler)
2126

2227
cast(Any, self)._proxify(DBUS_NAME, "/", bus=bus)
2328

crystalfontz/dbus/client/cli.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
VersionsM,
5353
)
5454
from crystalfontz.dbus.error import handle_dbus_error
55+
from crystalfontz.dbus.report import DbusClientCliReportHandler
5556
from crystalfontz.gpio import GpioDriveMode, GpioFunction
5657
from crystalfontz.lcd import LcdRegister
5758
from crystalfontz.temperature import (
@@ -203,7 +204,10 @@ async def load() -> None:
203204
elif user is None:
204205
select_system_bus()
205206

206-
client = DbusClient()
207+
report_handler = DbusClientCliReportHandler()
208+
report_handler.mode = output
209+
210+
client = DbusClient(report_handler=report_handler)
207211
ctx.obj = Obj(
208212
client=client,
209213
log_level=log_level,

crystalfontz/dbus/interface.py

Lines changed: 7 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,9 @@
8383
VersionsT,
8484
)
8585
from crystalfontz.dbus.domain.temperature import TemperatureDisplayItemT
86+
from crystalfontz.dbus.report import DbusInterfaceReportHandler
8687
from crystalfontz.error import ConnectionError
87-
from crystalfontz.report import KeyActivityReport, ReportHandler, TemperatureReport
88+
from crystalfontz.report import ReportHandler
8889

8990
Ok = bool
9091

@@ -93,25 +94,8 @@
9394
DBUS_NAME = "org.jfhbrook.crystalfontz"
9495

9596

96-
class DbusReportHandler(ReportHandler):
97-
def __init__(self: Self) -> None:
98-
self.iface: "Optional[DbusInterface]" = None
99-
100-
async def on_key_activity(self: Self, report: KeyActivityReport) -> None:
101-
if not self.iface:
102-
return
103-
104-
self.iface.key_activity_reports.emit(KeyActivityReportM.pack(report))
105-
106-
async def on_temperature(self: Self, report: TemperatureReport) -> None:
107-
if not self.iface:
108-
return
109-
110-
self.iface.temperature_reports.emit(TemperatureReportM.pack(report))
111-
112-
11397
async def load_client(
114-
report_handler: Optional[DbusReportHandler], config_file: Optional[str]
98+
report_handler: Optional[ReportHandler], config_file: Optional[str]
11599
) -> Client:
116100
config: Config = Config.from_file(config_file)
117101

@@ -130,17 +114,17 @@ class DbusInterface( # type: ignore
130114
def __init__(
131115
self: Self,
132116
client: Client,
133-
report_handler: Optional[DbusReportHandler] = None,
117+
report_handler: Optional[ReportHandler] = None,
134118
config_file: Optional[str] = None,
135119
) -> None:
136120
super().__init__()
137121
self._config: Config = Config.from_file(config_file)
138122
self.client: Client = client
139123
self._client_lock: asyncio.Lock = asyncio.Lock()
140-
self._report_handler = report_handler
124+
self.report_handler = report_handler
141125

142-
if self._report_handler:
143-
self._report_handler.iface = self
126+
if isinstance(self.report_handler, DbusInterfaceReportHandler):
127+
self.report_handler.iface = self
144128

145129
@dbus_property_async(ConfigM.t)
146130
def config(self: Self) -> ConfigT:

crystalfontz/dbus/report.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
from abc import ABC
2+
import asyncio
3+
import json
4+
from typing import Any, Optional, Protocol, Self, TypeVar
5+
6+
from crystalfontz.dbus.domain import (
7+
KeyActivityReportM,
8+
TemperatureReportM,
9+
)
10+
from crystalfontz.format import OutputMode
11+
from crystalfontz.report import (
12+
KeyActivityReport,
13+
ReportHandler,
14+
TemperatureReport,
15+
)
16+
17+
T = TypeVar("T")
18+
19+
20+
class DbusInterfaceProtocol(Protocol):
21+
# These types are NOT DbusSignalAsync[T]. They're defined as such in the
22+
# interface class, but are modified by metaclass. This gets pretty
23+
# confusing for the type checker! So in this case, we cheese it.
24+
key_activity_reports: Any
25+
temperature_reports: Any
26+
27+
28+
class DbusReportHandler(ReportHandler, ABC):
29+
def __init__(self: Self) -> None:
30+
self.iface: Optional[DbusInterfaceProtocol] = None
31+
32+
33+
class DbusInterfaceReportHandler(DbusReportHandler):
34+
"""
35+
A report handler which emits reports on a supplied interface.
36+
"""
37+
38+
def __init__(self: Self) -> None:
39+
self.iface: Optional[DbusInterfaceProtocol] = None
40+
41+
async def on_key_activity(self: Self, report: KeyActivityReport) -> None:
42+
if not self.iface:
43+
return
44+
45+
self.iface.key_activity_reports.emit(KeyActivityReportM.pack(report))
46+
47+
async def on_temperature(self: Self, report: TemperatureReport) -> None:
48+
if not self.iface:
49+
return
50+
51+
self.iface.temperature_reports.emit(TemperatureReportM.pack(report))
52+
53+
54+
class DbusClientReportHandler(DbusReportHandler):
55+
"""
56+
A report handler which listens to reports emitted by a dbus interface.
57+
"""
58+
59+
def __init__(self: Self) -> None:
60+
super().__init__()
61+
62+
self._key_activity_task: Optional[asyncio.Task] = None
63+
self._temperature_task: Optional[asyncio.Task] = None
64+
65+
async def _listen_key_activity(self: Self) -> None:
66+
if self.iface:
67+
async for report in self.iface.key_activity_reports:
68+
await self.on_key_activity(KeyActivityReportM.unpack(report))
69+
70+
async def _listen_temperature(self: Self) -> None:
71+
if self.iface:
72+
async for report in self.iface.temperature_reports:
73+
await self.on_temperature(TemperatureReportM.unpack(report))
74+
75+
async def listen(self: Self) -> None:
76+
self._key_activity_task = asyncio.create_task(self._listen_key_activity())
77+
self._temperature_task = asyncio.create_task(self._listen_temperature())
78+
79+
async def _wait(self: Self) -> None:
80+
if self._key_activity_task:
81+
try:
82+
await self._key_activity_task
83+
except asyncio.CancelledError:
84+
pass
85+
86+
if self._temperature_task:
87+
try:
88+
await self._temperature_task
89+
except asyncio.CancelledError:
90+
pass
91+
92+
async def stop(self: Self) -> None:
93+
if self._key_activity_task:
94+
self._key_activity_task.cancel()
95+
if self._temperature_task:
96+
self._temperature_task.cancel()
97+
98+
await self._wait()
99+
100+
101+
class DbusClientCliReportHandler(DbusClientReportHandler):
102+
mode: Optional[OutputMode] = None
103+
104+
async def on_key_activity(self: Self, report: KeyActivityReport) -> None:
105+
if self.mode == "json":
106+
print(json.dumps(report.as_dict()))
107+
elif self.mode == "text":
108+
print(repr(report))
109+
110+
async def on_temperature(self: Self, report: TemperatureReport) -> None:
111+
if self.mode == "json":
112+
print(json.dumps(report.as_dict()))
113+
elif self.mode == "text":
114+
print(repr(report))

crystalfontz/dbus/service/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@
1818
from crystalfontz.dbus.interface import (
1919
DBUS_NAME,
2020
DbusInterface,
21-
DbusReportHandler,
2221
load_client,
2322
)
23+
from crystalfontz.dbus.report import DbusInterfaceReportHandler
2424

2525
logger = logging.getLogger(__name__)
2626

@@ -31,7 +31,7 @@ async def service(config_file: Optional[str] = None) -> DbusInterface:
3131
"""
3232

3333
client = await load_client(
34-
report_handler=DbusReportHandler(), config_file=config_file
34+
report_handler=DbusInterfaceReportHandler(), config_file=config_file
3535
)
3636
iface = DbusInterface(client, config_file=config_file)
3737

0 commit comments

Comments
 (0)