jmri_logixng.actions.digital
Digital actions — set bean state, conditionally execute, log.
Each action holds a serializable description of what to do; the actual
lookup of the bean happens at execute() time against the active
connection in the ExecutionContext. This decouples authoring
from binding: the same automation file can run against
InternalConnection in tests and a real LocoNet/NCE connection in
production.
1"""Digital actions — set bean state, conditionally execute, log. 2 3Each action holds a serializable description of *what* to do; the actual 4lookup of the bean happens at :meth:`execute` time against the active 5connection in the :class:`ExecutionContext`. This decouples authoring 6from binding: the same automation file can run against 7``InternalConnection`` in tests and a real LocoNet/NCE connection in 8production. 9""" 10from __future__ import annotations 11 12from typing import TYPE_CHECKING, Any, ClassVar 13 14from jmri_core.state import LightState, SensorState, TurnoutState 15 16from jmri_logixng.context import ExecutionContext 17from jmri_logixng.registry import register 18from jmri_logixng.tree import Action, Expression 19 20if TYPE_CHECKING: 21 pass 22 23 24@register 25class SetTurnoutState(Action): 26 """Set a turnout's commanded state.""" 27 28 type_tag: ClassVar[str] = "action:set-turnout" 29 30 def __init__( 31 self, 32 *, 33 turnout: str, 34 state: TurnoutState, 35 user_name: str = "", 36 node_id: str | None = None, 37 ) -> None: 38 super().__init__(user_name=user_name, node_id=node_id) 39 self.turnout = turnout 40 self.state = state 41 42 async def execute(self, ctx: ExecutionContext) -> None: 43 bean = ctx.connection.turnouts.get(self.turnout) 44 if bean is None: 45 ctx.log.warning("SetTurnoutState: no turnout %r", self.turnout) 46 return 47 ctx.emit_trace(f"set turnout {self.turnout} → {self.state.name}") 48 await bean.set_commanded_state(self.state) 49 50 def extra_fields(self) -> dict[str, Any]: 51 return {"turnout": self.turnout, "state": self.state.name} 52 53 54@register 55class SetSensorState(Action): 56 """Set a sensor's known state (mostly for tests/simulation).""" 57 58 type_tag: ClassVar[str] = "action:set-sensor" 59 60 def __init__( 61 self, 62 *, 63 sensor: str, 64 state: SensorState, 65 user_name: str = "", 66 node_id: str | None = None, 67 ) -> None: 68 super().__init__(user_name=user_name, node_id=node_id) 69 self.sensor = sensor 70 self.state = state 71 72 async def execute(self, ctx: ExecutionContext) -> None: 73 bean = ctx.connection.sensors.get(self.sensor) 74 if bean is None: 75 ctx.log.warning("SetSensorState: no sensor %r", self.sensor) 76 return 77 ctx.emit_trace(f"set sensor {self.sensor} → {self.state.name}") 78 await bean._set_known_state(self.state) 79 80 def extra_fields(self) -> dict[str, Any]: 81 return {"sensor": self.sensor, "state": self.state.name} 82 83 84@register 85class SetLightState(Action): 86 """Set a light's state.""" 87 88 type_tag: ClassVar[str] = "action:set-light" 89 90 def __init__( 91 self, 92 *, 93 light: str, 94 state: LightState, 95 user_name: str = "", 96 node_id: str | None = None, 97 ) -> None: 98 super().__init__(user_name=user_name, node_id=node_id) 99 self.light = light 100 self.state = state 101 102 async def execute(self, ctx: ExecutionContext) -> None: 103 bean = ctx.connection.lights.get(self.light) 104 if bean is None: 105 ctx.log.warning("SetLightState: no light %r", self.light) 106 return 107 ctx.emit_trace(f"set light {self.light} → {self.state.name}") 108 await bean.set_state(self.state) 109 110 def extra_fields(self) -> dict[str, Any]: 111 return {"light": self.light, "state": self.state.name} 112 113 114@register 115class SetMemory(Action): 116 """Write a value into a memory bean.""" 117 118 type_tag: ClassVar[str] = "action:set-memory" 119 120 def __init__( 121 self, 122 *, 123 memory: str, 124 value: Any, 125 user_name: str = "", 126 node_id: str | None = None, 127 ) -> None: 128 super().__init__(user_name=user_name, node_id=node_id) 129 self.memory = memory 130 self.value = value 131 132 async def execute(self, ctx: ExecutionContext) -> None: 133 bean = ctx.connection.memories.get(self.memory) 134 if bean is None: 135 ctx.log.warning("SetMemory: no memory %r", self.memory) 136 return 137 ctx.emit_trace(f"set memory {self.memory} → {self.value!r}") 138 await bean.set_value(self.value) 139 140 def extra_fields(self) -> dict[str, Any]: 141 return {"memory": self.memory, "value": self.value} 142 143 144@register 145class IfThenElse(Action): 146 """Evaluate *condition*; run *then_action* if true, *else_action* otherwise.""" 147 148 type_tag: ClassVar[str] = "action:if-then-else" 149 150 def __init__( 151 self, 152 *, 153 condition: Expression[bool], 154 then_action: Action, 155 else_action: Action | None = None, 156 user_name: str = "", 157 node_id: str | None = None, 158 ) -> None: 159 super().__init__(user_name=user_name, node_id=node_id) 160 self.condition = condition 161 self.then_action = then_action 162 self.else_action = else_action 163 164 async def execute(self, ctx: ExecutionContext) -> None: 165 result = await self.condition.evaluate(ctx) 166 ctx.emit_trace(f"if-then-else condition={result}") 167 if result: 168 await self.then_action.execute(ctx) 169 elif self.else_action is not None: 170 await self.else_action.execute(ctx) 171 172 def extra_fields(self) -> dict[str, Any]: 173 out: dict[str, Any] = { 174 "condition": self.condition.to_dict(), 175 "then": self.then_action.to_dict(), 176 } 177 if self.else_action is not None: 178 out["else"] = self.else_action.to_dict() 179 return out 180 181 182@register 183class LogAction(Action): 184 """Emit a log message. Mostly useful for debugging automations.""" 185 186 type_tag: ClassVar[str] = "action:log" 187 188 def __init__( 189 self, 190 *, 191 message: str, 192 level: str = "info", 193 user_name: str = "", 194 node_id: str | None = None, 195 ) -> None: 196 super().__init__(user_name=user_name, node_id=node_id) 197 self.message = message 198 self.level = level 199 200 async def execute(self, ctx: ExecutionContext) -> None: 201 ctx.emit_trace(f"log[{self.level}]: {self.message}") 202 getattr(ctx.log, self.level, ctx.log.info)(self.message) 203 204 def extra_fields(self) -> dict[str, Any]: 205 return {"message": self.message, "level": self.level}
25@register 26class SetTurnoutState(Action): 27 """Set a turnout's commanded state.""" 28 29 type_tag: ClassVar[str] = "action:set-turnout" 30 31 def __init__( 32 self, 33 *, 34 turnout: str, 35 state: TurnoutState, 36 user_name: str = "", 37 node_id: str | None = None, 38 ) -> None: 39 super().__init__(user_name=user_name, node_id=node_id) 40 self.turnout = turnout 41 self.state = state 42 43 async def execute(self, ctx: ExecutionContext) -> None: 44 bean = ctx.connection.turnouts.get(self.turnout) 45 if bean is None: 46 ctx.log.warning("SetTurnoutState: no turnout %r", self.turnout) 47 return 48 ctx.emit_trace(f"set turnout {self.turnout} → {self.state.name}") 49 await bean.set_commanded_state(self.state) 50 51 def extra_fields(self) -> dict[str, Any]: 52 return {"turnout": self.turnout, "state": self.state.name}
Set a turnout's commanded state.
43 async def execute(self, ctx: ExecutionContext) -> None: 44 bean = ctx.connection.turnouts.get(self.turnout) 45 if bean is None: 46 ctx.log.warning("SetTurnoutState: no turnout %r", self.turnout) 47 return 48 ctx.emit_trace(f"set turnout {self.turnout} → {self.state.name}") 49 await bean.set_commanded_state(self.state)
51 def extra_fields(self) -> dict[str, Any]: 52 return {"turnout": self.turnout, "state": self.state.name}
Hook for subclasses to add their own fields to to_dict.
Inherited Members
55@register 56class SetSensorState(Action): 57 """Set a sensor's known state (mostly for tests/simulation).""" 58 59 type_tag: ClassVar[str] = "action:set-sensor" 60 61 def __init__( 62 self, 63 *, 64 sensor: str, 65 state: SensorState, 66 user_name: str = "", 67 node_id: str | None = None, 68 ) -> None: 69 super().__init__(user_name=user_name, node_id=node_id) 70 self.sensor = sensor 71 self.state = state 72 73 async def execute(self, ctx: ExecutionContext) -> None: 74 bean = ctx.connection.sensors.get(self.sensor) 75 if bean is None: 76 ctx.log.warning("SetSensorState: no sensor %r", self.sensor) 77 return 78 ctx.emit_trace(f"set sensor {self.sensor} → {self.state.name}") 79 await bean._set_known_state(self.state) 80 81 def extra_fields(self) -> dict[str, Any]: 82 return {"sensor": self.sensor, "state": self.state.name}
Set a sensor's known state (mostly for tests/simulation).
73 async def execute(self, ctx: ExecutionContext) -> None: 74 bean = ctx.connection.sensors.get(self.sensor) 75 if bean is None: 76 ctx.log.warning("SetSensorState: no sensor %r", self.sensor) 77 return 78 ctx.emit_trace(f"set sensor {self.sensor} → {self.state.name}") 79 await bean._set_known_state(self.state)
81 def extra_fields(self) -> dict[str, Any]: 82 return {"sensor": self.sensor, "state": self.state.name}
Hook for subclasses to add their own fields to to_dict.
Inherited Members
85@register 86class SetLightState(Action): 87 """Set a light's state.""" 88 89 type_tag: ClassVar[str] = "action:set-light" 90 91 def __init__( 92 self, 93 *, 94 light: str, 95 state: LightState, 96 user_name: str = "", 97 node_id: str | None = None, 98 ) -> None: 99 super().__init__(user_name=user_name, node_id=node_id) 100 self.light = light 101 self.state = state 102 103 async def execute(self, ctx: ExecutionContext) -> None: 104 bean = ctx.connection.lights.get(self.light) 105 if bean is None: 106 ctx.log.warning("SetLightState: no light %r", self.light) 107 return 108 ctx.emit_trace(f"set light {self.light} → {self.state.name}") 109 await bean.set_state(self.state) 110 111 def extra_fields(self) -> dict[str, Any]: 112 return {"light": self.light, "state": self.state.name}
Set a light's state.
103 async def execute(self, ctx: ExecutionContext) -> None: 104 bean = ctx.connection.lights.get(self.light) 105 if bean is None: 106 ctx.log.warning("SetLightState: no light %r", self.light) 107 return 108 ctx.emit_trace(f"set light {self.light} → {self.state.name}") 109 await bean.set_state(self.state)
111 def extra_fields(self) -> dict[str, Any]: 112 return {"light": self.light, "state": self.state.name}
Hook for subclasses to add their own fields to to_dict.
Inherited Members
115@register 116class SetMemory(Action): 117 """Write a value into a memory bean.""" 118 119 type_tag: ClassVar[str] = "action:set-memory" 120 121 def __init__( 122 self, 123 *, 124 memory: str, 125 value: Any, 126 user_name: str = "", 127 node_id: str | None = None, 128 ) -> None: 129 super().__init__(user_name=user_name, node_id=node_id) 130 self.memory = memory 131 self.value = value 132 133 async def execute(self, ctx: ExecutionContext) -> None: 134 bean = ctx.connection.memories.get(self.memory) 135 if bean is None: 136 ctx.log.warning("SetMemory: no memory %r", self.memory) 137 return 138 ctx.emit_trace(f"set memory {self.memory} → {self.value!r}") 139 await bean.set_value(self.value) 140 141 def extra_fields(self) -> dict[str, Any]: 142 return {"memory": self.memory, "value": self.value}
Write a value into a memory bean.
133 async def execute(self, ctx: ExecutionContext) -> None: 134 bean = ctx.connection.memories.get(self.memory) 135 if bean is None: 136 ctx.log.warning("SetMemory: no memory %r", self.memory) 137 return 138 ctx.emit_trace(f"set memory {self.memory} → {self.value!r}") 139 await bean.set_value(self.value)
141 def extra_fields(self) -> dict[str, Any]: 142 return {"memory": self.memory, "value": self.value}
Hook for subclasses to add their own fields to to_dict.
Inherited Members
145@register 146class IfThenElse(Action): 147 """Evaluate *condition*; run *then_action* if true, *else_action* otherwise.""" 148 149 type_tag: ClassVar[str] = "action:if-then-else" 150 151 def __init__( 152 self, 153 *, 154 condition: Expression[bool], 155 then_action: Action, 156 else_action: Action | None = None, 157 user_name: str = "", 158 node_id: str | None = None, 159 ) -> None: 160 super().__init__(user_name=user_name, node_id=node_id) 161 self.condition = condition 162 self.then_action = then_action 163 self.else_action = else_action 164 165 async def execute(self, ctx: ExecutionContext) -> None: 166 result = await self.condition.evaluate(ctx) 167 ctx.emit_trace(f"if-then-else condition={result}") 168 if result: 169 await self.then_action.execute(ctx) 170 elif self.else_action is not None: 171 await self.else_action.execute(ctx) 172 173 def extra_fields(self) -> dict[str, Any]: 174 out: dict[str, Any] = { 175 "condition": self.condition.to_dict(), 176 "then": self.then_action.to_dict(), 177 } 178 if self.else_action is not None: 179 out["else"] = self.else_action.to_dict() 180 return out
Evaluate condition; run then_action if true, else_action otherwise.
151 def __init__( 152 self, 153 *, 154 condition: Expression[bool], 155 then_action: Action, 156 else_action: Action | None = None, 157 user_name: str = "", 158 node_id: str | None = None, 159 ) -> None: 160 super().__init__(user_name=user_name, node_id=node_id) 161 self.condition = condition 162 self.then_action = then_action 163 self.else_action = else_action
173 def extra_fields(self) -> dict[str, Any]: 174 out: dict[str, Any] = { 175 "condition": self.condition.to_dict(), 176 "then": self.then_action.to_dict(), 177 } 178 if self.else_action is not None: 179 out["else"] = self.else_action.to_dict() 180 return out
Hook for subclasses to add their own fields to to_dict.
Inherited Members
183@register 184class LogAction(Action): 185 """Emit a log message. Mostly useful for debugging automations.""" 186 187 type_tag: ClassVar[str] = "action:log" 188 189 def __init__( 190 self, 191 *, 192 message: str, 193 level: str = "info", 194 user_name: str = "", 195 node_id: str | None = None, 196 ) -> None: 197 super().__init__(user_name=user_name, node_id=node_id) 198 self.message = message 199 self.level = level 200 201 async def execute(self, ctx: ExecutionContext) -> None: 202 ctx.emit_trace(f"log[{self.level}]: {self.message}") 203 getattr(ctx.log, self.level, ctx.log.info)(self.message) 204 205 def extra_fields(self) -> dict[str, Any]: 206 return {"message": self.message, "level": self.level}
Emit a log message. Mostly useful for debugging automations.
205 def extra_fields(self) -> dict[str, Any]: 206 return {"message": self.message, "level": self.level}
Hook for subclasses to add their own fields to to_dict.