Skip to content

Commit 330e8e7

Browse files
committed
feat: deprecate ShutterState
1 parent d33bf00 commit 330e8e7

5 files changed

Lines changed: 165 additions & 133 deletions

File tree

bec_lib/bec_lib/bl_states.py

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -194,14 +194,6 @@ def validate_limits(self) -> DeviceWithinLimitsStateConfig:
194194
return self
195195

196196

197-
class ShutterStateConfig(DeviceStateConfig):
198-
"""
199-
Configuration for a shutter state.
200-
"""
201-
202-
state_type: ClassVar[str] = "ShutterState"
203-
204-
205197
C = TypeVar("C", bound=BeamlineStateConfig)
206198
D = TypeVar("D", bound=DeviceStateConfig)
207199

@@ -416,33 +408,6 @@ def _update_device_state(self, msg_obj: MessageObject) -> messages.BeamlineState
416408
return self.evaluate(msg)
417409

418410

419-
class ShutterState(DeviceBeamlineState[ShutterStateConfig]):
420-
"""
421-
A state that checks if the shutter is open.
422-
423-
Example:
424-
shutter_state = ShutterStateConfig(
425-
name="shutter_open",
426-
device="shutter1",
427-
)
428-
bec.beamline_states.add(shutter_state)
429-
"""
430-
431-
CONFIG_CLASS = ShutterStateConfig
432-
433-
def evaluate(
434-
self, msg: messages.DeviceMessage, *args, **kwargs
435-
) -> messages.BeamlineStateMessage:
436-
val = msg.signals.get(self.signal_name, {}).get("value", "").lower()
437-
if val == "open":
438-
return messages.BeamlineStateMessage(
439-
name=self.config.name, status="valid", label="Shutter is open."
440-
)
441-
return messages.BeamlineStateMessage(
442-
name=self.config.name, status="invalid", label="Shutter is closed."
443-
)
444-
445-
446411
class DeviceWithinLimitsState(DeviceBeamlineState[DeviceWithinLimitsStateConfig]):
447412
"""
448413
A state that checks if a device signal is within limits.

bec_lib/tests/test_beamline_states.py

Lines changed: 108 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ class DemoConfig(BaseModel):
4444

4545
class TestConfigModels:
4646
def test_beamline_state_config_valid_name(self):
47-
config = bl_states.BeamlineStateConfig(name="shutter_open")
48-
assert config.name == "shutter_open"
47+
config = bl_states.BeamlineStateConfig(name="sample_x_limits")
48+
assert config.name == "sample_x_limits"
4949

5050
@pytest.mark.parametrize("invalid_name", ["state-name", "class", "add", "remove", "show_all"])
5151
def test_beamline_state_config_invalid_name(self, invalid_name):
@@ -91,18 +91,25 @@ def evaluate(self, *args, **kwargs):
9191

9292
class TestDeviceBeamlineState:
9393
def test_start_requires_connector(self, dm_with_devices):
94-
state = bl_states.ShutterState(
95-
name="shutter_open", device="samy", signal="samy", device_manager=dm_with_devices
94+
state = bl_states.DeviceWithinLimitsState(
95+
name="sample_y_limits",
96+
device="samy",
97+
signal="samy",
98+
low_limit=0.0,
99+
high_limit=10.0,
100+
device_manager=dm_with_devices,
96101
)
97102

98103
with pytest.raises(RuntimeError, match="Redis connector is not set"):
99104
state.start()
100105

101106
def test_start_registers_device_callback(self, connected_connector, dm_with_devices):
102-
state = bl_states.ShutterState(
103-
name="shutter_open",
107+
state = bl_states.DeviceWithinLimitsState(
108+
name="sample_x_limits",
104109
device="samx",
105110
signal="samx",
111+
low_limit=0.0,
112+
high_limit=10.0,
106113
redis_connector=connected_connector,
107114
device_manager=dm_with_devices,
108115
)
@@ -115,10 +122,12 @@ def test_start_registers_device_callback(self, connected_connector, dm_with_devi
115122
)
116123

117124
def test_stop_unregisters_device_callback(self, connected_connector, dm_with_devices):
118-
state = bl_states.ShutterState(
119-
name="shutter_open",
125+
state = bl_states.DeviceWithinLimitsState(
126+
name="sample_x_limits",
120127
device="samx",
121128
signal="samx",
129+
low_limit=0.0,
130+
high_limit=10.0,
122131
redis_connector=connected_connector,
123132
device_manager=dm_with_devices,
124133
)
@@ -134,50 +143,55 @@ def test_stop_unregisters_device_callback(self, connected_connector, dm_with_dev
134143
def test_update_device_state_publishes_when_state_changes(
135144
self, connected_connector, dm_with_devices
136145
):
137-
state = bl_states.ShutterState(
138-
name="shutter_open",
146+
state = bl_states.DeviceWithinLimitsState(
147+
name="sample_x_limits",
139148
device="samx",
140149
signal="samx",
150+
low_limit=0.0,
151+
high_limit=10.0,
141152
redis_connector=connected_connector,
142153
device_manager=dm_with_devices,
143154
)
144155

145156
msg = messages.DeviceMessage(
146-
signals={"samx": {"value": "open", "timestamp": 1.0}}, metadata={"stream": "primary"}
157+
signals={"samx": {"value": 5.0, "timestamp": 1.0}}, metadata={"stream": "primary"}
147158
)
148-
msg_obj = MessageObject(value=msg, topic="test")
149159

150160
state._update_device_state(MessageObject(value=msg, topic="test"))
151161

152162
assert state._last_state is not None
153163
assert state._last_state.status == "valid"
154164
out = connected_connector.xread(
155-
MessageEndpoints.beamline_state("shutter_open"), from_start=True
165+
MessageEndpoints.beamline_state("sample_x_limits"), from_start=True
156166
)
157167
assert out is not None
158168
assert out[0]["data"].status == "valid"
159169

160170

161171
class TestConcreteStates:
162-
def test_shutter_state_open_and_closed(self, connected_connector, dm_with_devices):
163-
state = bl_states.ShutterState(
164-
name="shutter_open",
172+
def test_device_within_limits_state_valid_and_invalid(
173+
self, connected_connector, dm_with_devices
174+
):
175+
state = bl_states.DeviceWithinLimitsState(
176+
name="sample_x_limits",
165177
device="samx",
166178
signal="samx",
179+
low_limit=0.0,
180+
high_limit=10.0,
167181
redis_connector=connected_connector,
168182
device_manager=dm_with_devices,
169183
)
170184
state.start()
171185

172-
open_msg = messages.DeviceMessage(
173-
signals={"samx": {"value": "OPEN", "timestamp": 1.0}}, metadata={"stream": "primary"}
186+
valid_msg = messages.DeviceMessage(
187+
signals={"samx": {"value": 5.0, "timestamp": 1.0}}, metadata={"stream": "primary"}
174188
)
175-
closed_msg = messages.DeviceMessage(
176-
signals={"samx": {"value": "closed", "timestamp": 2.0}}, metadata={"stream": "primary"}
189+
invalid_msg = messages.DeviceMessage(
190+
signals={"samx": {"value": 11.0, "timestamp": 2.0}}, metadata={"stream": "primary"}
177191
)
178192

179-
assert state.evaluate(open_msg).status == "valid"
180-
assert state.evaluate(closed_msg).status == "invalid"
193+
assert state.evaluate(valid_msg).status == "valid"
194+
assert state.evaluate(invalid_msg).status == "invalid"
181195

182196
def test_device_within_limits_state(self, connected_connector, dm_with_devices):
183197
state = bl_states.DeviceWithinLimitsState(
@@ -253,9 +267,14 @@ def test_manager_is_ready_when_no_state_update_exists(self, connected_connector)
253267

254268
def test_manager_loads_existing_state_update_on_init(self, connected_connector):
255269
config = messages.BeamlineStateConfig(
256-
name="shutter_open",
257-
state_type="ShutterState",
258-
parameters={"name": "shutter_open", "device": "samy"},
270+
name="sample_y_limits",
271+
state_type="DeviceWithinLimitsState",
272+
parameters={
273+
"name": "sample_y_limits",
274+
"device": "samy",
275+
"low_limit": 0.0,
276+
"high_limit": 10.0,
277+
},
259278
)
260279
connected_connector.xadd(
261280
MessageEndpoints.available_beamline_states(),
@@ -268,14 +287,14 @@ def test_manager_loads_existing_state_update_on_init(self, connected_connector):
268287
manager = BeamlineStateManager(client)
269288

270289
assert manager.ready is True
271-
assert "shutter_open" in manager._states
272-
assert isinstance(getattr(manager, "shutter_open"), BeamlineStateClientBase)
290+
assert "sample_y_limits" in manager._states
291+
assert isinstance(getattr(manager, "sample_y_limits"), BeamlineStateClientBase)
273292

274293
def test_manager_rejects_abstract_state_type_on_init(self, connected_connector):
275294
config = messages.BeamlineStateConfig(
276-
name="shutter_open",
295+
name="generic_device_state",
277296
state_type="DeviceBeamlineState",
278-
parameters={"name": "shutter_open", "device": "samy"},
297+
parameters={"name": "generic_device_state", "device": "samy"},
279298
)
280299
connected_connector.xadd(
281300
MessageEndpoints.available_beamline_states(),
@@ -290,17 +309,24 @@ def test_manager_rejects_abstract_state_type_on_init(self, connected_connector):
290309

291310
def test_on_state_update_creates_client_attribute(self, state_manager):
292311
config = messages.BeamlineStateConfig(
293-
name="shutter_open",
294-
state_type="ShutterState",
295-
parameters={"name": "shutter_open", "device": "samy"},
312+
name="sample_y_limits",
313+
state_type="DeviceWithinLimitsState",
314+
parameters={
315+
"name": "sample_y_limits",
316+
"device": "samy",
317+
"low_limit": 0.0,
318+
"high_limit": 10.0,
319+
},
296320
)
297321
update = messages.AvailableBeamlineStatesMessage(states=[config])
298322

299323
state_manager._on_state_update({"data": update}, parent=state_manager)
300324

301-
assert "shutter_open" in state_manager._states
302-
assert isinstance(state_manager._states["shutter_open"], bl_states.ShutterStateConfig)
303-
assert isinstance(getattr(state_manager, "shutter_open"), BeamlineStateClientBase)
325+
assert "sample_y_limits" in state_manager._states
326+
assert isinstance(
327+
state_manager._states["sample_y_limits"], bl_states.DeviceWithinLimitsStateConfig
328+
)
329+
assert isinstance(getattr(state_manager, "sample_y_limits"), BeamlineStateClientBase)
304330

305331
def test_update_parameters_from_client_updates_state_and_publishes(self, state_manager):
306332
config = messages.BeamlineStateConfig(
@@ -355,48 +381,60 @@ def test_external_parameter_update_refreshes_existing_client_state(self, state_m
355381

356382
def test_client_get_returns_unknown_without_status_message(self, state_manager):
357383
config = messages.BeamlineStateConfig(
358-
name="shutter_open",
359-
state_type="ShutterState",
360-
parameters={"name": "shutter_open", "device": "samy"},
384+
name="sample_y_limits",
385+
state_type="DeviceWithinLimitsState",
386+
parameters={
387+
"name": "sample_y_limits",
388+
"device": "samy",
389+
"low_limit": 0.0,
390+
"high_limit": 10.0,
391+
},
361392
)
362393
update = messages.AvailableBeamlineStatesMessage(states=[config])
363394
state_manager._on_state_update({"data": update}, parent=state_manager)
364395

365-
result = state_manager.shutter_open.get()
396+
result = state_manager.sample_y_limits.get()
366397
assert result == {"status": "unknown", "label": "No state information available."}
367398

368399
def test_client_get_returns_latest_status_message(self, state_manager):
369400
config = messages.BeamlineStateConfig(
370-
name="shutter_open",
371-
state_type="ShutterState",
372-
parameters={"name": "shutter_open", "device": "samy"},
401+
name="sample_y_limits",
402+
state_type="DeviceWithinLimitsState",
403+
parameters={
404+
"name": "sample_y_limits",
405+
"device": "samy",
406+
"low_limit": 0.0,
407+
"high_limit": 10.0,
408+
},
373409
)
374410
update = messages.AvailableBeamlineStatesMessage(states=[config])
375411
state_manager._on_state_update({"data": update}, parent=state_manager)
376412

377413
state_manager._connector.xadd(
378-
MessageEndpoints.beamline_state("shutter_open"),
414+
MessageEndpoints.beamline_state("sample_y_limits"),
379415
{
380416
"data": messages.BeamlineStateMessage(
381-
name="shutter_open", status="valid", label="ok"
417+
name="sample_y_limits", status="valid", label="ok"
382418
)
383419
},
384420
max_size=1,
385421
)
386422

387-
result = state_manager.shutter_open.get()
423+
result = state_manager.sample_y_limits.get()
388424
assert result == {"status": "valid", "label": "ok"}
389425

390426
def test_add_waits_for_initial_state_message(self, state_manager):
391-
state = bl_states.ShutterStateConfig(name="shutter_open", device="samy")
427+
state = bl_states.DeviceWithinLimitsStateConfig(
428+
name="sample_y_limits", device="samy", low_limit=0.0, high_limit=10.0
429+
)
392430

393431
def publish_initial_state():
394432
time.sleep(0.05)
395433
state_manager._connector.xadd(
396-
MessageEndpoints.beamline_state("shutter_open"),
434+
MessageEndpoints.beamline_state("sample_y_limits"),
397435
{
398436
"data": messages.BeamlineStateMessage(
399-
name="shutter_open", status="valid", label="ok"
437+
name="sample_y_limits", status="valid", label="ok"
400438
)
401439
},
402440
max_size=1,
@@ -409,43 +447,52 @@ def publish_initial_state():
409447
finally:
410448
publisher.join()
411449

412-
assert state_manager.shutter_open.get() == {"status": "valid", "label": "ok"}
450+
assert state_manager.sample_y_limits.get() == {"status": "valid", "label": "ok"}
413451

414452
def test_add_rejects_abstract_device_state_config(self, state_manager):
415-
state = bl_states.DeviceStateConfig(name="shutter_open", device="samy")
453+
state = bl_states.DeviceStateConfig(name="generic_device_state", device="samy")
416454

417455
with pytest.raises(ValueError, match="not a concrete beamline state"):
418456
state_manager.add(state)
419457

420458
def test_add_and_delete_publish_updates(self, state_manager):
421-
state = bl_states.ShutterStateConfig(name="shutter_open", device="samy")
459+
state = bl_states.DeviceWithinLimitsStateConfig(
460+
name="sample_y_limits", device="samy", low_limit=0.0, high_limit=10.0
461+
)
422462

423463
with mock.patch.object(state_manager, "_wait_for_initial_state"):
424464
state_manager.add(state)
425-
assert "shutter_open" in state_manager._states
465+
assert "sample_y_limits" in state_manager._states
426466

427-
state_manager.delete("shutter_open")
428-
assert "shutter_open" not in state_manager._states
467+
state_manager.delete("sample_y_limits")
468+
assert "sample_y_limits" not in state_manager._states
429469

430470
def test_client_remove_state(self, state_manager):
431471
config = messages.BeamlineStateConfig(
432-
name="shutter_open",
433-
state_type="ShutterState",
434-
parameters={"name": "shutter_open", "device": "samy"},
472+
name="sample_y_limits",
473+
state_type="DeviceWithinLimitsState",
474+
parameters={
475+
"name": "sample_y_limits",
476+
"device": "samy",
477+
"low_limit": 0.0,
478+
"high_limit": 10.0,
479+
},
435480
)
436481
update = messages.AvailableBeamlineStatesMessage(states=[config])
437482
state_manager._on_state_update({"data": update}, parent=state_manager)
438483

439-
state_manager.shutter_open.remove()
484+
state_manager.sample_y_limits.remove()
440485

441-
assert "shutter_open" not in state_manager._states
486+
assert "sample_y_limits" not in state_manager._states
442487

443488
def test_show_all_prints_table(self, state_manager, capsys):
444-
state = bl_states.ShutterStateConfig(name="shutter_open", device="samy")
489+
state = bl_states.DeviceWithinLimitsStateConfig(
490+
name="sample_y_limits", device="samy", low_limit=0.0, high_limit=10.0
491+
)
445492
with mock.patch.object(state_manager, "_wait_for_initial_state"):
446493
state_manager.add(state)
447494

448495
state_manager.show_all()
449496

450497
captured = capsys.readouterr()
451-
assert "shutter_open" in (captured.out + captured.err)
498+
assert "sample_y_limits" in (captured.out + captured.err)

0 commit comments

Comments
 (0)