66
77from fastcs .attributes .attribute import Attribute
88from fastcs .attributes .attribute_io_ref import AttributeIORefT
9+ from fastcs .attributes .util import AttrValuePredicate , PredicateEvent
910from fastcs .datatypes import DataType , DType_T
1011from fastcs .logging import bind_logger
1112
@@ -39,6 +40,8 @@ def __init__(
3940 """Callback to update the value of the attribute with an IO to the source"""
4041 self ._on_update_callbacks : list [AttrOnUpdateCallback [DType_T ]] | None = None
4142 """Callbacks to publish changes to the value of the attribute"""
43+ self ._on_update_events : set [PredicateEvent [DType_T ]] = set ()
44+ """Events to set when the value satisifies some predicate"""
4245
4346 def get (self ) -> DType_T :
4447 """Get the cached value of the attribute."""
@@ -51,6 +54,9 @@ async def update(self, value: Any) -> None:
5154 generally only be called from an IO or a controller that is updating the value
5255 from some underlying source.
5356
57+ Any update callbacks will be called with the new value and any update events
58+ with predicates satisfied by the new value will be set.
59+
5460 To request a change to the setpoint of the attribute, use the ``put`` method,
5561 which will attempt to apply the change to the underlying source.
5662
@@ -67,6 +73,10 @@ async def update(self, value: Any) -> None:
6773
6874 self ._value = self ._datatype .validate (value )
6975
76+ self ._on_update_events -= {
77+ e for e in self ._on_update_events if e .set (self ._value )
78+ }
79+
7080 if self ._on_update_callbacks is not None :
7181 try :
7282 await asyncio .gather (
@@ -115,3 +125,69 @@ async def update_attribute():
115125 raise
116126
117127 return update_attribute
128+
129+ async def wait_for_predicate (
130+ self , predicate : AttrValuePredicate [DType_T ], * , timeout : float
131+ ):
132+ """Wait for the predicate to be satisfied when called with the current value
133+
134+ Args:
135+ predicate: The predicate to test - a callable that takes the attribute
136+ value and returns True if the event should be set
137+ timeout: The timeout in seconds
138+
139+ """
140+ if predicate (self ._value ):
141+ self .log_event (
142+ "Predicate already satisfied" , predicate = predicate , attribute = self
143+ )
144+ return
145+
146+ self ._on_update_events .add (update_event := PredicateEvent (predicate ))
147+
148+ self .log_event ("Waiting for predicate" , predicate = predicate , attribute = self )
149+ try :
150+ await asyncio .wait_for (update_event .wait (), timeout )
151+ except TimeoutError :
152+ self ._on_update_events .remove (update_event )
153+ raise TimeoutError (
154+ f"Timeout waiting { timeout } s for { self .full_name } predicate { predicate } "
155+ f" - current value: { self ._value } "
156+ ) from None
157+
158+ self .log_event ("Predicate satisfied" , predicate = predicate , attribute = self )
159+
160+ async def wait_for_value (self , target_value : DType_T , * , timeout : float ):
161+ """Wait for self._value to equal the target value
162+
163+ Args:
164+ target_value: The target value to wait for
165+ timeout: The timeout in seconds
166+
167+ Raises:
168+ TimeoutError: If the attribute does not reach the target value within the
169+ timeout
170+
171+ """
172+ if self ._value == target_value :
173+ self .log_event (
174+ "Current value already equals target value" ,
175+ target_value = target_value ,
176+ attribute = self ,
177+ )
178+ return
179+
180+ def predicate (v : DType_T ) -> bool :
181+ return v == target_value
182+
183+ try :
184+ await self .wait_for_predicate (predicate , timeout = timeout )
185+ except TimeoutError :
186+ raise TimeoutError (
187+ f"Timeout waiting { timeout } s for { self .full_name } value { target_value } "
188+ f" - current value: { self ._value } "
189+ ) from None
190+
191+ self .log_event (
192+ "Value equals target value" , target_valuevalue = target_value , attribute = self
193+ )
0 commit comments