aioebus 0.2.2__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. {aioebus-0.2.2 → aioebus-0.3.0}/PKG-INFO +1 -1
  2. {aioebus-0.2.2 → aioebus-0.3.0}/pyproject.toml +1 -1
  3. {aioebus-0.2.2 → aioebus-0.3.0}/src/aioebus/__init__.py +3 -2
  4. {aioebus-0.2.2 → aioebus-0.3.0}/src/aioebus/engine.py +25 -0
  5. {aioebus-0.2.2 → aioebus-0.3.0}/src/aioebus.egg-info/PKG-INFO +1 -1
  6. {aioebus-0.2.2 → aioebus-0.3.0}/tests/test_engine.py +34 -0
  7. {aioebus-0.2.2 → aioebus-0.3.0}/LICENSE +0 -0
  8. {aioebus-0.2.2 → aioebus-0.3.0}/README.md +0 -0
  9. {aioebus-0.2.2 → aioebus-0.3.0}/setup.cfg +0 -0
  10. {aioebus-0.2.2 → aioebus-0.3.0}/src/aioebus/active.py +0 -0
  11. {aioebus-0.2.2 → aioebus-0.3.0}/src/aioebus/codecs.py +0 -0
  12. {aioebus-0.2.2 → aioebus-0.3.0}/src/aioebus/conditions.py +0 -0
  13. {aioebus-0.2.2 → aioebus-0.3.0}/src/aioebus/convert.py +0 -0
  14. {aioebus-0.2.2 → aioebus-0.3.0}/src/aioebus/frame.py +0 -0
  15. {aioebus-0.2.2 → aioebus-0.3.0}/src/aioebus/messages.py +0 -0
  16. {aioebus-0.2.2 → aioebus-0.3.0}/src/aioebus/passive.py +0 -0
  17. {aioebus-0.2.2 → aioebus-0.3.0}/src/aioebus/poll.py +0 -0
  18. {aioebus-0.2.2 → aioebus-0.3.0}/src/aioebus/py.typed +0 -0
  19. {aioebus-0.2.2 → aioebus-0.3.0}/src/aioebus/scan.py +0 -0
  20. {aioebus-0.2.2 → aioebus-0.3.0}/src/aioebus/translations.py +0 -0
  21. {aioebus-0.2.2 → aioebus-0.3.0}/src/aioebus/transport.py +0 -0
  22. {aioebus-0.2.2 → aioebus-0.3.0}/src/aioebus.egg-info/SOURCES.txt +0 -0
  23. {aioebus-0.2.2 → aioebus-0.3.0}/src/aioebus.egg-info/dependency_links.txt +0 -0
  24. {aioebus-0.2.2 → aioebus-0.3.0}/src/aioebus.egg-info/top_level.txt +0 -0
  25. {aioebus-0.2.2 → aioebus-0.3.0}/tests/test_active.py +0 -0
  26. {aioebus-0.2.2 → aioebus-0.3.0}/tests/test_active_read.py +0 -0
  27. {aioebus-0.2.2 → aioebus-0.3.0}/tests/test_bitfields.py +0 -0
  28. {aioebus-0.2.2 → aioebus-0.3.0}/tests/test_codecs.py +0 -0
  29. {aioebus-0.2.2 → aioebus-0.3.0}/tests/test_condition_eval.py +0 -0
  30. {aioebus-0.2.2 → aioebus-0.3.0}/tests/test_conditions.py +0 -0
  31. {aioebus-0.2.2 → aioebus-0.3.0}/tests/test_datatypes.py +0 -0
  32. {aioebus-0.2.2 → aioebus-0.3.0}/tests/test_frame.py +0 -0
  33. {aioebus-0.2.2 → aioebus-0.3.0}/tests/test_messages.py +0 -0
  34. {aioebus-0.2.2 → aioebus-0.3.0}/tests/test_passive.py +0 -0
  35. {aioebus-0.2.2 → aioebus-0.3.0}/tests/test_poll.py +0 -0
  36. {aioebus-0.2.2 → aioebus-0.3.0}/tests/test_scan.py +0 -0
  37. {aioebus-0.2.2 → aioebus-0.3.0}/tests/test_translations.py +0 -0
  38. {aioebus-0.2.2 → aioebus-0.3.0}/tests/test_transport.py +0 -0
  39. {aioebus-0.2.2 → aioebus-0.3.0}/tests/test_write.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aioebus
3
- Version: 0.2.2
3
+ Version: 0.3.0
4
4
  Summary: Standalone async eBUS library (decode-first). No ebusd daemon required.
5
5
  Author-email: dneprojects <dne.projects.1@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "aioebus"
3
- version = "0.2.2"
3
+ version = "0.3.0"
4
4
  description = "Standalone async eBUS library (decode-first). No ebusd daemon required."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -20,7 +20,7 @@ from .active import (
20
20
  )
21
21
  from .codecs import Codec, decode, encode, make_codec
22
22
  from .conditions import ConditionContext, context_from_scan, evaluate, evaluate_all
23
- from .engine import EbusEngine, value_key
23
+ from .engine import EbusEngine, EngineStats, value_key
24
24
  from .frame import Telegram, crc8, escape, parse_telegram, unescape
25
25
  from .messages import FieldDef, MessageDef, parse_csv
26
26
  from .passive import MessageRegistry, decode_raw
@@ -34,6 +34,7 @@ __all__ = [
34
34
  "Codec",
35
35
  "ConditionContext",
36
36
  "EbusEngine",
37
+ "EngineStats",
37
38
  "EnhDecoder",
38
39
  "FieldDef",
39
40
  "MessageDef",
@@ -64,4 +65,4 @@ __all__ = [
64
65
  "unescape",
65
66
  "value_key",
66
67
  ]
67
- __version__ = "0.2.2"
68
+ __version__ = "0.3.0"
@@ -40,6 +40,19 @@ class _Active:
40
40
  driver: _CommandDriver | None = None
41
41
 
42
42
 
43
+ @dataclass
44
+ class EngineStats:
45
+ """Live counters for the adapter link, meant to be surfaced as the adapter's
46
+ own diagnostic entities (connection, bus activity, poll success)."""
47
+
48
+ connected: bool = False # the stream is up and bytes are arriving
49
+ telegrams: int = 0 # telegrams decoded off the bus passively (free)
50
+ polls_ok: int = 0 # scheduled polls that got an answer
51
+ polls_failed: int = 0 # scheduled polls that stayed unanswered (backed off)
52
+ writes: int = 0 # active writes performed
53
+ last_telegram: float | None = None # monotonic time of the last bus telegram
54
+
55
+
43
56
  class EbusEngine:
44
57
  """Drive one eBUS adapter connection: passive decode + interleaved actives."""
45
58
 
@@ -68,6 +81,7 @@ class EbusEngine:
68
81
  self._active: _Active | None = None
69
82
  self._queue: deque[_Active] = deque()
70
83
  self.cache: dict[tuple[str, str, str], object] = {}
84
+ self.stats = EngineStats()
71
85
  self._stop = False
72
86
 
73
87
  # -- public API ---------------------------------------------------------
@@ -81,11 +95,14 @@ class EbusEngine:
81
95
  break
82
96
  if not chunk:
83
97
  break
98
+ self.stats.connected = True
84
99
  for code, data in self._dec.feed(chunk):
85
100
  await self._on_event(code, data)
101
+ self.stats.connected = False
86
102
 
87
103
  def stop(self) -> None:
88
104
  self._stop = True
105
+ self.stats.connected = False
89
106
 
90
107
  def attach(self, send: Callable[[bytes], Awaitable[None]],
91
108
  recv: Callable[[], Awaitable[bytes]]) -> None:
@@ -97,6 +114,7 @@ class EbusEngine:
97
114
  self._buf.clear()
98
115
  self._active = None
99
116
  self._stop = False
117
+ self.stats.connected = False
100
118
 
101
119
  async def read(self, msg: MessageDef, timeout: float = 4.0) -> dict:
102
120
  """Actively read a message as soon as the bus is idle; return its values."""
@@ -154,6 +172,8 @@ class EbusEngine:
154
172
  return
155
173
  if not tg.crc_ok:
156
174
  return
175
+ self.stats.telegrams += 1
176
+ self.stats.last_telegram = time.monotonic()
157
177
  decoded = self.registry.decode(tg)
158
178
  if decoded is not None:
159
179
  self._store(*decoded)
@@ -193,6 +213,10 @@ class EbusEngine:
193
213
  if drv.error is None:
194
214
  if isinstance(drv, ReadDriver) and act.msg is not None:
195
215
  self._store(act.msg, act.msg.decode_labeled(slave=drv.result))
216
+ if act.future is None: # a scheduled poll, not an explicit read
217
+ self.stats.polls_ok += 1
218
+ elif isinstance(drv, WriteDriver):
219
+ self.stats.writes += 1
196
220
  if act.future is not None and not act.future.done():
197
221
  act.future.set_result(drv.result)
198
222
  return
@@ -219,3 +243,4 @@ class EbusEngine:
219
243
  # the same dead register, starving the rest; the backoff then fades a
220
244
  # persistently unanswered register out of the rotation entirely.
221
245
  self.scheduler.mark_failure(act.msg, time.monotonic())
246
+ self.stats.polls_failed += 1
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aioebus
3
- Version: 0.2.2
3
+ Version: 0.3.0
4
4
  Summary: Standalone async eBUS library (decode-first). No ebusd daemon required.
5
5
  Author-email: dneprojects <dne.projects.1@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -112,6 +112,40 @@ class _ArbLossAdapter:
112
112
  return bytes(out)
113
113
 
114
114
 
115
+ def test_stats_track_connection_and_bus_activity():
116
+ """A decoded bus telegram counts as activity and marks the link connected;
117
+ an unanswered poll is counted as a failed poll."""
118
+ async def inner():
119
+ link = _Link()
120
+ eng = EbusEngine(link.send, link.recv, REG)
121
+ assert eng.stats.connected is False
122
+ task = asyncio.create_task(eng.run())
123
+ link.inject(encode_received_frame(bytes.fromhex("3138b509030d1800740003910100bb00")))
124
+ await asyncio.sleep(0.05)
125
+ assert eng.stats.connected is True
126
+ assert eng.stats.telegrams == 1
127
+ assert eng.stats.last_telegram is not None
128
+ eng.stop()
129
+ link.inject(b"")
130
+ await asyncio.wait_for(task, 1)
131
+ assert eng.stats.connected is False
132
+
133
+ msg = BY["Statenumber"]
134
+ sched = PollScheduler([msg], base_interval=10.0)
135
+ eng2 = EbusEngine(link.send, link.recv, REG, scheduler=sched)
136
+
137
+ class _FailedDriver:
138
+ error = "timeout waiting for slave"
139
+
140
+ act = _Active(lambda: _FailedDriver(), msg, None) # future None -> a poll
141
+ act.driver = _FailedDriver()
142
+ eng2._active = act
143
+ await eng2._finish_active()
144
+ assert eng2.stats.polls_failed == 1
145
+
146
+ asyncio.run(inner())
147
+
148
+
115
149
  def test_arbitration_loss_captures_winner_passively():
116
150
  """When our poll loses arbitration, the telegram that won is decoded
117
151
  passively (seeded with the winner's address) instead of being discarded."""
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes