jmri_loconet.beans
LocoNet-aware bean subclasses + their managers + LnPowerManager.
LnTurnoutoverridesset_commanded_stateto pushOPC_SW_REQand updatesknown_statefrom inboundOPC_SW_REP.LnSensorupdates its state from inboundOPC_INPUT_REP.LnPowerManagersendsOPC_GPON/OPC_GPOFF/OPC_IDLEand tracks state from inbound copies.
1"""LocoNet-aware bean subclasses + their managers + LnPowerManager. 2 3* :class:`LnTurnout` overrides ``set_commanded_state`` to push 4 ``OPC_SW_REQ`` and updates ``known_state`` from inbound ``OPC_SW_REP``. 5* :class:`LnSensor` updates its state from inbound ``OPC_INPUT_REP``. 6* :class:`LnPowerManager` sends ``OPC_GPON`` / ``OPC_GPOFF`` / ``OPC_IDLE`` 7 and tracks state from inbound copies. 8""" 9from __future__ import annotations 10 11from jmri_core.beans.power import PowerManager 12from jmri_core.beans.sensor import Sensor 13from jmri_core.beans.turnout import Turnout 14from jmri_core.manager import Manager 15from jmri_core.state import PowerState, SensorState, TurnoutState 16 17from jmri_loconet.messages import ( 18 LocoNetMessage, 19 OpcGpOff, 20 OpcGpOn, 21 OpcIdle, 22 OpcInputRep, 23 OpcSwRep, 24 OpcSwReq, 25) 26from jmri_loconet.traffic import LocoNetTrafficController 27 28 29class LnTurnout(Turnout): 30 """LocoNet-aware turnout: sends OPC_SW_REQ on commanded-state change.""" 31 32 __slots__ = ("_address", "_tc") 33 34 def __init__(self, system_name: str, address: int, tc: LocoNetTrafficController) -> None: 35 super().__init__(system_name) 36 self._address = address 37 self._tc = tc 38 39 @property 40 def address(self) -> int: 41 return self._address 42 43 async def set_commanded_state(self, state: TurnoutState) -> None: 44 if state in (TurnoutState.CLOSED, TurnoutState.THROWN): 45 await self._tc.send(OpcSwReq( 46 address=self._address, 47 closed=(state == TurnoutState.CLOSED), 48 output_on=True, 49 )) 50 # Update the in-memory state via the base implementation; for 51 # DIRECT-feedback turnouts known_state will mirror this until/unless 52 # a layout report overrides it. 53 await super().set_commanded_state(state) 54 55 56class LnTurnoutManager(Manager[LnTurnout]): 57 """Manager that constructs LocoNet turnouts and tracks them by address.""" 58 59 type_letter = "T" 60 61 def __init__(self, system_prefix: str, *, tc: LocoNetTrafficController) -> None: 62 super().__init__(system_prefix) 63 self._tc = tc 64 self._by_address: dict[int, LnTurnout] = {} 65 self._unsub = tc.inbound.connect(self._on_message) 66 67 def _create(self, system_name: str) -> LnTurnout: 68 address = _parse_address(system_name, self.full_prefix) 69 bean = LnTurnout(system_name, address, self._tc) 70 self._by_address[address] = bean 71 return bean 72 73 async def deregister(self, bean: LnTurnout) -> None: 74 self._by_address.pop(bean.address, None) 75 await super().deregister(bean) 76 77 async def _on_message(self, msg: LocoNetMessage) -> None: 78 if not isinstance(msg, OpcSwRep): 79 return 80 bean = self._by_address.get(msg.address) 81 if bean is None: 82 return 83 state = TurnoutState.CLOSED if msg.closed else TurnoutState.THROWN 84 await bean._set_known_state(state) 85 86 87class LnSensor(Sensor): 88 """LocoNet-aware sensor — read-only from the layout.""" 89 90 __slots__ = ("_address",) 91 92 def __init__(self, system_name: str, address: int) -> None: 93 super().__init__(system_name) 94 self._address = address 95 96 @property 97 def address(self) -> int: 98 return self._address 99 100 101class LnSensorManager(Manager[LnSensor]): 102 """Manager that constructs LocoNet sensors and listens for OPC_INPUT_REP.""" 103 104 type_letter = "S" 105 106 def __init__(self, system_prefix: str, *, tc: LocoNetTrafficController) -> None: 107 super().__init__(system_prefix) 108 self._tc = tc 109 self._by_address: dict[int, LnSensor] = {} 110 self._unsub = tc.inbound.connect(self._on_message) 111 112 def _create(self, system_name: str) -> LnSensor: 113 address = _parse_address(system_name, self.full_prefix) 114 bean = LnSensor(system_name, address) 115 self._by_address[address] = bean 116 return bean 117 118 async def _on_message(self, msg: LocoNetMessage) -> None: 119 if not isinstance(msg, OpcInputRep): 120 return 121 bean = self._by_address.get(msg.address) 122 if bean is None: 123 # Sensors aren't always pre-created; clients can opt in via provide(). 124 return 125 state = SensorState.ACTIVE if msg.active else SensorState.INACTIVE 126 await bean._set_known_state(state) 127 128 129class LnPowerManager(PowerManager): 130 """LocoNet track power. Sends OPC_GPON/OPC_GPOFF/OPC_IDLE.""" 131 132 def __init__(self, tc: LocoNetTrafficController) -> None: 133 super().__init__() 134 self._tc = tc 135 self._unsub = tc.inbound.connect(self._on_message) 136 137 async def set_power(self, state: PowerState) -> None: 138 if state == PowerState.ON: 139 await self._tc.send(OpcGpOn()) 140 elif state == PowerState.OFF: 141 await self._tc.send(OpcGpOff()) 142 elif state == PowerState.IDLE: 143 await self._tc.send(OpcIdle()) 144 await self._set_state(state) 145 146 async def _on_message(self, msg: LocoNetMessage) -> None: 147 if isinstance(msg, OpcGpOn): 148 await self._set_state(PowerState.ON) 149 elif isinstance(msg, OpcGpOff): 150 await self._set_state(PowerState.OFF) 151 elif isinstance(msg, OpcIdle): 152 await self._set_state(PowerState.IDLE) 153 154 155# ----- helpers -------------------------------------------------------------- 156 157 158def _parse_address(system_name: str, full_prefix: str) -> int: 159 """Extract the numeric address from a LocoNet system name (``"LT12"`` → 12).""" 160 rest = system_name.removeprefix(full_prefix) 161 try: 162 return int(rest) 163 except ValueError as e: 164 raise ValueError( 165 f"LocoNet system name {system_name!r} must end in a numeric address" 166 ) from e
30class LnTurnout(Turnout): 31 """LocoNet-aware turnout: sends OPC_SW_REQ on commanded-state change.""" 32 33 __slots__ = ("_address", "_tc") 34 35 def __init__(self, system_name: str, address: int, tc: LocoNetTrafficController) -> None: 36 super().__init__(system_name) 37 self._address = address 38 self._tc = tc 39 40 @property 41 def address(self) -> int: 42 return self._address 43 44 async def set_commanded_state(self, state: TurnoutState) -> None: 45 if state in (TurnoutState.CLOSED, TurnoutState.THROWN): 46 await self._tc.send(OpcSwReq( 47 address=self._address, 48 closed=(state == TurnoutState.CLOSED), 49 output_on=True, 50 )) 51 # Update the in-memory state via the base implementation; for 52 # DIRECT-feedback turnouts known_state will mirror this until/unless 53 # a layout report overrides it. 54 await super().set_commanded_state(state)
LocoNet-aware turnout: sends OPC_SW_REQ on commanded-state change.
LnTurnout( system_name: str, address: int, tc: jmri_loconet.LocoNetTrafficController)
44 async def set_commanded_state(self, state: TurnoutState) -> None: 45 if state in (TurnoutState.CLOSED, TurnoutState.THROWN): 46 await self._tc.send(OpcSwReq( 47 address=self._address, 48 closed=(state == TurnoutState.CLOSED), 49 output_on=True, 50 )) 51 # Update the in-memory state via the base implementation; for 52 # DIRECT-feedback turnouts known_state will mirror this until/unless 53 # a layout report overrides it. 54 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().
57class LnTurnoutManager(Manager[LnTurnout]): 58 """Manager that constructs LocoNet turnouts and tracks them by address.""" 59 60 type_letter = "T" 61 62 def __init__(self, system_prefix: str, *, tc: LocoNetTrafficController) -> None: 63 super().__init__(system_prefix) 64 self._tc = tc 65 self._by_address: dict[int, LnTurnout] = {} 66 self._unsub = tc.inbound.connect(self._on_message) 67 68 def _create(self, system_name: str) -> LnTurnout: 69 address = _parse_address(system_name, self.full_prefix) 70 bean = LnTurnout(system_name, address, self._tc) 71 self._by_address[address] = bean 72 return bean 73 74 async def deregister(self, bean: LnTurnout) -> None: 75 self._by_address.pop(bean.address, None) 76 await super().deregister(bean) 77 78 async def _on_message(self, msg: LocoNetMessage) -> None: 79 if not isinstance(msg, OpcSwRep): 80 return 81 bean = self._by_address.get(msg.address) 82 if bean is None: 83 return 84 state = TurnoutState.CLOSED if msg.closed else TurnoutState.THROWN 85 await bean._set_known_state(state)
Manager that constructs LocoNet turnouts and tracks them by address.
LnTurnoutManager( system_prefix: str, *, tc: jmri_loconet.LocoNetTrafficController)
88class LnSensor(Sensor): 89 """LocoNet-aware sensor — read-only from the layout.""" 90 91 __slots__ = ("_address",) 92 93 def __init__(self, system_name: str, address: int) -> None: 94 super().__init__(system_name) 95 self._address = address 96 97 @property 98 def address(self) -> int: 99 return self._address
LocoNet-aware sensor — read-only from the layout.
102class LnSensorManager(Manager[LnSensor]): 103 """Manager that constructs LocoNet sensors and listens for OPC_INPUT_REP.""" 104 105 type_letter = "S" 106 107 def __init__(self, system_prefix: str, *, tc: LocoNetTrafficController) -> None: 108 super().__init__(system_prefix) 109 self._tc = tc 110 self._by_address: dict[int, LnSensor] = {} 111 self._unsub = tc.inbound.connect(self._on_message) 112 113 def _create(self, system_name: str) -> LnSensor: 114 address = _parse_address(system_name, self.full_prefix) 115 bean = LnSensor(system_name, address) 116 self._by_address[address] = bean 117 return bean 118 119 async def _on_message(self, msg: LocoNetMessage) -> None: 120 if not isinstance(msg, OpcInputRep): 121 return 122 bean = self._by_address.get(msg.address) 123 if bean is None: 124 # Sensors aren't always pre-created; clients can opt in via provide(). 125 return 126 state = SensorState.ACTIVE if msg.active else SensorState.INACTIVE 127 await bean._set_known_state(state)
Manager that constructs LocoNet sensors and listens for OPC_INPUT_REP.
LnSensorManager( system_prefix: str, *, tc: jmri_loconet.LocoNetTrafficController)
130class LnPowerManager(PowerManager): 131 """LocoNet track power. Sends OPC_GPON/OPC_GPOFF/OPC_IDLE.""" 132 133 def __init__(self, tc: LocoNetTrafficController) -> None: 134 super().__init__() 135 self._tc = tc 136 self._unsub = tc.inbound.connect(self._on_message) 137 138 async def set_power(self, state: PowerState) -> None: 139 if state == PowerState.ON: 140 await self._tc.send(OpcGpOn()) 141 elif state == PowerState.OFF: 142 await self._tc.send(OpcGpOff()) 143 elif state == PowerState.IDLE: 144 await self._tc.send(OpcIdle()) 145 await self._set_state(state) 146 147 async def _on_message(self, msg: LocoNetMessage) -> None: 148 if isinstance(msg, OpcGpOn): 149 await self._set_state(PowerState.ON) 150 elif isinstance(msg, OpcGpOff): 151 await self._set_state(PowerState.OFF) 152 elif isinstance(msg, OpcIdle): 153 await self._set_state(PowerState.IDLE)
LocoNet track power. Sends OPC_GPON/OPC_GPOFF/OPC_IDLE.
LnPowerManager(tc: jmri_loconet.LocoNetTrafficController)
138 async def set_power(self, state: PowerState) -> None: 139 if state == PowerState.ON: 140 await self._tc.send(OpcGpOn()) 141 elif state == PowerState.OFF: 142 await self._tc.send(OpcGpOff()) 143 elif state == PowerState.IDLE: 144 await self._tc.send(OpcIdle()) 145 await self._set_state(state)
Command the layout to state. Base impl just updates in-memory.