jmri_openlcb.beans

Event-bound OpenLCB beans.

OpenLCB doesn't use addresses — it uses 64-bit event IDs. Each bean binds two event IDs (one per state) at construction:

When a turnout's commanded state changes, the bean produces the matching event on the bus. When an inbound event matches a bean's configured event ID, that bean's known state moves to the corresponding value.

Because the event-ID mapping has to be provided at bean construction time, the standard Manager.provide(system_name) shape doesn't fit: there's no way to derive the events from a string alone. We expose a configure(system_name, closed_event, thrown_event) method on the manager for that.

  1"""Event-bound OpenLCB beans.
  2
  3OpenLCB doesn't use addresses — it uses 64-bit event IDs. Each bean
  4binds two event IDs (one per state) at construction:
  5
  6* :class:`OlcbTurnout` — ``closed_event`` and ``thrown_event``.
  7* :class:`OlcbSensor` — ``active_event`` and ``inactive_event``.
  8
  9When a turnout's commanded state changes, the bean *produces* the
 10matching event on the bus. When an inbound event matches a bean's
 11configured event ID, that bean's *known* state moves to the corresponding
 12value.
 13
 14Because the event-ID mapping has to be provided at bean construction
 15time, the standard ``Manager.provide(system_name)`` shape doesn't fit:
 16there's no way to derive the events from a string alone. We expose a
 17``configure(system_name, closed_event, thrown_event)`` method on the
 18manager for that.
 19"""
 20from __future__ import annotations
 21
 22from jmri_core.beans.sensor import Sensor
 23from jmri_core.beans.turnout import Turnout
 24from jmri_core.manager import Manager
 25from jmri_core.state import SensorState, TurnoutState
 26
 27from jmri_openlcb.events import EventId, EventReport
 28from jmri_openlcb.traffic import OlcbTrafficController
 29
 30# ----- Turnout -------------------------------------------------------------
 31
 32
 33class OlcbTurnout(Turnout):
 34    """A turnout bound to two event IDs (closed/thrown)."""
 35
 36    __slots__ = ("_closed_event", "_tc", "_thrown_event")
 37
 38    def __init__(
 39        self,
 40        system_name: str,
 41        closed_event: EventId,
 42        thrown_event: EventId,
 43        tc: OlcbTrafficController,
 44    ) -> None:
 45        super().__init__(system_name)
 46        self._closed_event = closed_event
 47        self._thrown_event = thrown_event
 48        self._tc = tc
 49
 50    @property
 51    def closed_event(self) -> EventId:
 52        return self._closed_event
 53
 54    @property
 55    def thrown_event(self) -> EventId:
 56        return self._thrown_event
 57
 58    async def set_commanded_state(self, state: TurnoutState) -> None:
 59        if state == TurnoutState.CLOSED:
 60            await self._tc.produce_event(self._closed_event)
 61        elif state == TurnoutState.THROWN:
 62            await self._tc.produce_event(self._thrown_event)
 63        await super().set_commanded_state(state)
 64
 65
 66class OlcbTurnoutManager(Manager[OlcbTurnout]):
 67    """Manager that creates turnouts only via :meth:`configure`."""
 68
 69    type_letter = "T"
 70
 71    def __init__(self, system_prefix: str, *, tc: OlcbTrafficController) -> None:
 72        super().__init__(system_prefix)
 73        self._tc = tc
 74        self._by_event: dict[EventId, tuple[OlcbTurnout, TurnoutState]] = {}
 75        self._unsub = tc.events.connect(self._on_event)
 76
 77    def _create(self, system_name: str) -> OlcbTurnout:
 78        raise NotImplementedError(
 79            "OpenLCB turnouts require explicit event-ID configuration; "
 80            "use OlcbTurnoutManager.configure(...) instead of provide()"
 81        )
 82
 83    async def configure(
 84        self,
 85        system_name: str,
 86        closed_event: EventId,
 87        thrown_event: EventId,
 88    ) -> OlcbTurnout:
 89        """Create and register a turnout with the given event bindings."""
 90        bean = OlcbTurnout(system_name, closed_event, thrown_event, self._tc)
 91        await self.register(bean)
 92        self._by_event[closed_event] = (bean, TurnoutState.CLOSED)
 93        self._by_event[thrown_event] = (bean, TurnoutState.THROWN)
 94        return bean
 95
 96    async def _on_event(self, report: EventReport) -> None:
 97        binding = self._by_event.get(report.event)
 98        if binding is None:
 99            return
100        bean, target_state = binding
101        await bean._set_known_state(target_state)
102
103
104# ----- Sensor --------------------------------------------------------------
105
106
107class OlcbSensor(Sensor):
108    """A sensor bound to two event IDs (active/inactive)."""
109
110    __slots__ = ("_active_event", "_inactive_event")
111
112    def __init__(
113        self,
114        system_name: str,
115        active_event: EventId,
116        inactive_event: EventId,
117    ) -> None:
118        super().__init__(system_name)
119        self._active_event = active_event
120        self._inactive_event = inactive_event
121
122    @property
123    def active_event(self) -> EventId:
124        return self._active_event
125
126    @property
127    def inactive_event(self) -> EventId:
128        return self._inactive_event
129
130
131class OlcbSensorManager(Manager[OlcbSensor]):
132    type_letter = "S"
133
134    def __init__(self, system_prefix: str, *, tc: OlcbTrafficController) -> None:
135        super().__init__(system_prefix)
136        self._by_event: dict[EventId, tuple[OlcbSensor, SensorState]] = {}
137        self._unsub = tc.events.connect(self._on_event)
138
139    def _create(self, system_name: str) -> OlcbSensor:
140        raise NotImplementedError(
141            "OpenLCB sensors require explicit event-ID configuration; "
142            "use OlcbSensorManager.configure(...)"
143        )
144
145    async def configure(
146        self,
147        system_name: str,
148        active_event: EventId,
149        inactive_event: EventId,
150    ) -> OlcbSensor:
151        bean = OlcbSensor(system_name, active_event, inactive_event)
152        await self.register(bean)
153        self._by_event[active_event] = (bean, SensorState.ACTIVE)
154        self._by_event[inactive_event] = (bean, SensorState.INACTIVE)
155        return bean
156
157    async def _on_event(self, report: EventReport) -> None:
158        binding = self._by_event.get(report.event)
159        if binding is None:
160            return
161        bean, target_state = binding
162        await bean._set_known_state(target_state)
class OlcbTurnout(jmri_core.beans.turnout.Turnout):
34class OlcbTurnout(Turnout):
35    """A turnout bound to two event IDs (closed/thrown)."""
36
37    __slots__ = ("_closed_event", "_tc", "_thrown_event")
38
39    def __init__(
40        self,
41        system_name: str,
42        closed_event: EventId,
43        thrown_event: EventId,
44        tc: OlcbTrafficController,
45    ) -> None:
46        super().__init__(system_name)
47        self._closed_event = closed_event
48        self._thrown_event = thrown_event
49        self._tc = tc
50
51    @property
52    def closed_event(self) -> EventId:
53        return self._closed_event
54
55    @property
56    def thrown_event(self) -> EventId:
57        return self._thrown_event
58
59    async def set_commanded_state(self, state: TurnoutState) -> None:
60        if state == TurnoutState.CLOSED:
61            await self._tc.produce_event(self._closed_event)
62        elif state == TurnoutState.THROWN:
63            await self._tc.produce_event(self._thrown_event)
64        await super().set_commanded_state(state)

A turnout bound to two event IDs (closed/thrown).

OlcbTurnout( system_name: str, closed_event: jmri_openlcb.EventId, thrown_event: jmri_openlcb.EventId, tc: jmri_openlcb.OlcbTrafficController)
39    def __init__(
40        self,
41        system_name: str,
42        closed_event: EventId,
43        thrown_event: EventId,
44        tc: OlcbTrafficController,
45    ) -> None:
46        super().__init__(system_name)
47        self._closed_event = closed_event
48        self._thrown_event = thrown_event
49        self._tc = tc
closed_event: jmri_openlcb.EventId
51    @property
52    def closed_event(self) -> EventId:
53        return self._closed_event
thrown_event: jmri_openlcb.EventId
55    @property
56    def thrown_event(self) -> EventId:
57        return self._thrown_event
async def set_commanded_state(self, state: jmri_core.state.TurnoutState) -> None:
59    async def set_commanded_state(self, state: TurnoutState) -> None:
60        if state == TurnoutState.CLOSED:
61            await self._tc.produce_event(self._closed_event)
62        elif state == TurnoutState.THROWN:
63            await self._tc.produce_event(self._thrown_event)
64        await super().set_commanded_state(state)

Ask the layout to throw or close the switch.

The base implementation only updates the in-memory commanded state and emits the signal. Driver subclasses override this to push a hardware message and then call super().set_commanded_state().

class OlcbTurnoutManager(jmri_core.manager.Manager[jmri_openlcb.beans.OlcbTurnout]):
 67class OlcbTurnoutManager(Manager[OlcbTurnout]):
 68    """Manager that creates turnouts only via :meth:`configure`."""
 69
 70    type_letter = "T"
 71
 72    def __init__(self, system_prefix: str, *, tc: OlcbTrafficController) -> None:
 73        super().__init__(system_prefix)
 74        self._tc = tc
 75        self._by_event: dict[EventId, tuple[OlcbTurnout, TurnoutState]] = {}
 76        self._unsub = tc.events.connect(self._on_event)
 77
 78    def _create(self, system_name: str) -> OlcbTurnout:
 79        raise NotImplementedError(
 80            "OpenLCB turnouts require explicit event-ID configuration; "
 81            "use OlcbTurnoutManager.configure(...) instead of provide()"
 82        )
 83
 84    async def configure(
 85        self,
 86        system_name: str,
 87        closed_event: EventId,
 88        thrown_event: EventId,
 89    ) -> OlcbTurnout:
 90        """Create and register a turnout with the given event bindings."""
 91        bean = OlcbTurnout(system_name, closed_event, thrown_event, self._tc)
 92        await self.register(bean)
 93        self._by_event[closed_event] = (bean, TurnoutState.CLOSED)
 94        self._by_event[thrown_event] = (bean, TurnoutState.THROWN)
 95        return bean
 96
 97    async def _on_event(self, report: EventReport) -> None:
 98        binding = self._by_event.get(report.event)
 99        if binding is None:
100            return
101        bean, target_state = binding
102        await bean._set_known_state(target_state)

Manager that creates turnouts only via configure().

OlcbTurnoutManager( system_prefix: str, *, tc: jmri_openlcb.OlcbTrafficController)
72    def __init__(self, system_prefix: str, *, tc: OlcbTrafficController) -> None:
73        super().__init__(system_prefix)
74        self._tc = tc
75        self._by_event: dict[EventId, tuple[OlcbTurnout, TurnoutState]] = {}
76        self._unsub = tc.events.connect(self._on_event)
type_letter = 'T'
async def configure( self, system_name: str, closed_event: jmri_openlcb.EventId, thrown_event: jmri_openlcb.EventId) -> OlcbTurnout:
84    async def configure(
85        self,
86        system_name: str,
87        closed_event: EventId,
88        thrown_event: EventId,
89    ) -> OlcbTurnout:
90        """Create and register a turnout with the given event bindings."""
91        bean = OlcbTurnout(system_name, closed_event, thrown_event, self._tc)
92        await self.register(bean)
93        self._by_event[closed_event] = (bean, TurnoutState.CLOSED)
94        self._by_event[thrown_event] = (bean, TurnoutState.THROWN)
95        return bean

Create and register a turnout with the given event bindings.

class OlcbSensor(jmri_core.beans.sensor.Sensor):
108class OlcbSensor(Sensor):
109    """A sensor bound to two event IDs (active/inactive)."""
110
111    __slots__ = ("_active_event", "_inactive_event")
112
113    def __init__(
114        self,
115        system_name: str,
116        active_event: EventId,
117        inactive_event: EventId,
118    ) -> None:
119        super().__init__(system_name)
120        self._active_event = active_event
121        self._inactive_event = inactive_event
122
123    @property
124    def active_event(self) -> EventId:
125        return self._active_event
126
127    @property
128    def inactive_event(self) -> EventId:
129        return self._inactive_event

A sensor bound to two event IDs (active/inactive).

OlcbSensor( system_name: str, active_event: jmri_openlcb.EventId, inactive_event: jmri_openlcb.EventId)
113    def __init__(
114        self,
115        system_name: str,
116        active_event: EventId,
117        inactive_event: EventId,
118    ) -> None:
119        super().__init__(system_name)
120        self._active_event = active_event
121        self._inactive_event = inactive_event
active_event: jmri_openlcb.EventId
123    @property
124    def active_event(self) -> EventId:
125        return self._active_event
inactive_event: jmri_openlcb.EventId
127    @property
128    def inactive_event(self) -> EventId:
129        return self._inactive_event
class OlcbSensorManager(jmri_core.manager.Manager[jmri_openlcb.beans.OlcbSensor]):
132class OlcbSensorManager(Manager[OlcbSensor]):
133    type_letter = "S"
134
135    def __init__(self, system_prefix: str, *, tc: OlcbTrafficController) -> None:
136        super().__init__(system_prefix)
137        self._by_event: dict[EventId, tuple[OlcbSensor, SensorState]] = {}
138        self._unsub = tc.events.connect(self._on_event)
139
140    def _create(self, system_name: str) -> OlcbSensor:
141        raise NotImplementedError(
142            "OpenLCB sensors require explicit event-ID configuration; "
143            "use OlcbSensorManager.configure(...)"
144        )
145
146    async def configure(
147        self,
148        system_name: str,
149        active_event: EventId,
150        inactive_event: EventId,
151    ) -> OlcbSensor:
152        bean = OlcbSensor(system_name, active_event, inactive_event)
153        await self.register(bean)
154        self._by_event[active_event] = (bean, SensorState.ACTIVE)
155        self._by_event[inactive_event] = (bean, SensorState.INACTIVE)
156        return bean
157
158    async def _on_event(self, report: EventReport) -> None:
159        binding = self._by_event.get(report.event)
160        if binding is None:
161            return
162        bean, target_state = binding
163        await bean._set_known_state(target_state)

Per-type bean registry. Subclass and override _create().

Attributes set on the class describe the bean type:

  • type_letter — single uppercase letter the bean class owns (e.g. "T" for Turnout). Must be set by subclass.
  • system_prefix — connection prefix (e.g. "L"). Set per-instance because one bean class can have many connection-specific managers.
OlcbSensorManager( system_prefix: str, *, tc: jmri_openlcb.OlcbTrafficController)
135    def __init__(self, system_prefix: str, *, tc: OlcbTrafficController) -> None:
136        super().__init__(system_prefix)
137        self._by_event: dict[EventId, tuple[OlcbSensor, SensorState]] = {}
138        self._unsub = tc.events.connect(self._on_event)
type_letter = 'S'
async def configure( self, system_name: str, active_event: jmri_openlcb.EventId, inactive_event: jmri_openlcb.EventId) -> OlcbSensor:
146    async def configure(
147        self,
148        system_name: str,
149        active_event: EventId,
150        inactive_event: EventId,
151    ) -> OlcbSensor:
152        bean = OlcbSensor(system_name, active_event, inactive_event)
153        await self.register(bean)
154        self._by_event[active_event] = (bean, SensorState.ACTIVE)
155        self._by_event[inactive_event] = (bean, SensorState.INACTIVE)
156        return bean