jmri_core.beans.block

Block — a track section with optional occupancy detection.

A block carries:

  • a state (UNOCCUPIED/OCCUPIED/UNKNOWN/UNDETECTED);
  • an opaque value (typically a loco identifier, set when occupancy detection
    • reporters can identify the train);
  • an optional bound sensor; when bound, the block follows the sensor's active/inactive state automatically.
 1"""``Block`` — a track section with optional occupancy detection.
 2
 3A block carries:
 4  * a state (UNOCCUPIED/OCCUPIED/UNKNOWN/UNDETECTED);
 5  * an opaque value (typically a loco identifier, set when occupancy detection
 6    + reporters can identify the train);
 7  * an optional bound sensor; when bound, the block follows the sensor's
 8    active/inactive state automatically.
 9"""
10from __future__ import annotations
11
12from dataclasses import dataclass
13from typing import Any
14
15from jmri_core.bean import AbstractNamedBean
16from jmri_core.beans.sensor import Sensor, SensorStateChange
17from jmri_core.manager import Manager
18from jmri_core.state import BlockState, SensorState
19
20
21@dataclass(frozen=True, slots=True)
22class BlockChange:
23    kind: str  # "state" | "value"
24    previous: Any
25    current: Any
26
27
28class Block(AbstractNamedBean):
29    """Track block — possibly with an attached occupancy sensor."""
30
31    __slots__ = ("_disconnect_sensor", "_sensor", "_state", "_value")
32
33    type_letter = "B"
34
35    def __init__(self, system_name: str, user_name: str | None = None) -> None:
36        super().__init__(system_name, user_name)
37        self._state: BlockState = BlockState.UNKNOWN
38        self._value: Any = None
39        self._sensor: Sensor | None = None
40        self._disconnect_sensor: Any = None
41
42    @property
43    def state(self) -> BlockState:
44        return self._state
45
46    @property
47    def value(self) -> Any:
48        return self._value
49
50    @property
51    def sensor(self) -> Sensor | None:
52        return self._sensor
53
54    async def set_value(self, value: Any) -> None:
55        prev = self._value
56        if prev == value:
57            return
58        self._value = value
59        await self.state_changed.emit(BlockChange("value", prev, value))
60
61    async def _set_state(self, state: BlockState) -> None:
62        prev = self._state
63        if prev == state:
64            return
65        self._state = state
66        await self.state_changed.emit(BlockChange("state", prev, state))
67
68    def attach_sensor(self, sensor: Sensor | None) -> None:
69        """Bind a sensor whose state drives this block's state.
70
71        Pass ``None`` to detach. Detaching disconnects the listener so the
72        sensor reference can be garbage-collected.
73        """
74        if self._disconnect_sensor is not None:
75            self._disconnect_sensor()
76            self._disconnect_sensor = None
77        self._sensor = sensor
78        if sensor is not None:
79            self._disconnect_sensor = sensor.state_changed.connect(self._on_sensor_change)
80            # Seed initial state from the sensor.
81            self._state = _sensor_to_block_state(sensor.known_state)
82
83    async def _on_sensor_change(self, change: SensorStateChange) -> None:
84        await self._set_state(_sensor_to_block_state(change.current))
85
86
87def _sensor_to_block_state(s: SensorState) -> BlockState:
88    if s == SensorState.ACTIVE:
89        return BlockState.OCCUPIED
90    if s == SensorState.INACTIVE:
91        return BlockState.UNOCCUPIED
92    return BlockState.UNKNOWN
93
94
95class BlockManager(Manager[Block]):
96    type_letter = "B"
97
98    def _create(self, system_name: str) -> Block:
99        return Block(system_name)
@dataclass(frozen=True, slots=True)
class BlockChange:
22@dataclass(frozen=True, slots=True)
23class BlockChange:
24    kind: str  # "state" | "value"
25    previous: Any
26    current: Any
BlockChange(kind: str, previous: Any, current: Any)
kind: str
previous: Any
current: Any
class Block(jmri_core.bean.AbstractNamedBean):
29class Block(AbstractNamedBean):
30    """Track block — possibly with an attached occupancy sensor."""
31
32    __slots__ = ("_disconnect_sensor", "_sensor", "_state", "_value")
33
34    type_letter = "B"
35
36    def __init__(self, system_name: str, user_name: str | None = None) -> None:
37        super().__init__(system_name, user_name)
38        self._state: BlockState = BlockState.UNKNOWN
39        self._value: Any = None
40        self._sensor: Sensor | None = None
41        self._disconnect_sensor: Any = None
42
43    @property
44    def state(self) -> BlockState:
45        return self._state
46
47    @property
48    def value(self) -> Any:
49        return self._value
50
51    @property
52    def sensor(self) -> Sensor | None:
53        return self._sensor
54
55    async def set_value(self, value: Any) -> None:
56        prev = self._value
57        if prev == value:
58            return
59        self._value = value
60        await self.state_changed.emit(BlockChange("value", prev, value))
61
62    async def _set_state(self, state: BlockState) -> None:
63        prev = self._state
64        if prev == state:
65            return
66        self._state = state
67        await self.state_changed.emit(BlockChange("state", prev, state))
68
69    def attach_sensor(self, sensor: Sensor | None) -> None:
70        """Bind a sensor whose state drives this block's state.
71
72        Pass ``None`` to detach. Detaching disconnects the listener so the
73        sensor reference can be garbage-collected.
74        """
75        if self._disconnect_sensor is not None:
76            self._disconnect_sensor()
77            self._disconnect_sensor = None
78        self._sensor = sensor
79        if sensor is not None:
80            self._disconnect_sensor = sensor.state_changed.connect(self._on_sensor_change)
81            # Seed initial state from the sensor.
82            self._state = _sensor_to_block_state(sensor.known_state)
83
84    async def _on_sensor_change(self, change: SensorStateChange) -> None:
85        await self._set_state(_sensor_to_block_state(change.current))

Track block — possibly with an attached occupancy sensor.

Block(system_name: str, user_name: str | None = None)
36    def __init__(self, system_name: str, user_name: str | None = None) -> None:
37        super().__init__(system_name, user_name)
38        self._state: BlockState = BlockState.UNKNOWN
39        self._value: Any = None
40        self._sensor: Sensor | None = None
41        self._disconnect_sensor: Any = None
type_letter = 'B'
state: jmri_core.state.BlockState
43    @property
44    def state(self) -> BlockState:
45        return self._state
value: Any
47    @property
48    def value(self) -> Any:
49        return self._value
sensor: jmri_core.beans.Sensor | None
51    @property
52    def sensor(self) -> Sensor | None:
53        return self._sensor
async def set_value(self, value: Any) -> None:
55    async def set_value(self, value: Any) -> None:
56        prev = self._value
57        if prev == value:
58            return
59        self._value = value
60        await self.state_changed.emit(BlockChange("value", prev, value))
def attach_sensor(self, sensor: jmri_core.beans.Sensor | None) -> None:
69    def attach_sensor(self, sensor: Sensor | None) -> None:
70        """Bind a sensor whose state drives this block's state.
71
72        Pass ``None`` to detach. Detaching disconnects the listener so the
73        sensor reference can be garbage-collected.
74        """
75        if self._disconnect_sensor is not None:
76            self._disconnect_sensor()
77            self._disconnect_sensor = None
78        self._sensor = sensor
79        if sensor is not None:
80            self._disconnect_sensor = sensor.state_changed.connect(self._on_sensor_change)
81            # Seed initial state from the sensor.
82            self._state = _sensor_to_block_state(sensor.known_state)

Bind a sensor whose state drives this block's state.

Pass None to detach. Detaching disconnects the listener so the sensor reference can be garbage-collected.

 96class BlockManager(Manager[Block]):
 97    type_letter = "B"
 98
 99    def _create(self, system_name: str) -> Block:
100        return Block(system_name)

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.
type_letter = 'B'