diff --git a/src/fastcs/attributes/attr_r.py b/src/fastcs/attributes/attr_r.py index ae67c6d1..3309e2a2 100644 --- a/src/fastcs/attributes/attr_r.py +++ b/src/fastcs/attributes/attr_r.py @@ -38,7 +38,9 @@ def __init__( ) self._update_callback: AttrIOUpdateCallback[DType_T] | None = None """Callback to update the value of the attribute with an IO to the source""" - self._on_update_callbacks: list[AttrOnUpdateCallback[DType_T]] | None = None + self._on_update_callbacks: ( + list[tuple[AttrOnUpdateCallback[DType_T], bool]] | None + ) = None """Callbacks to publish changes to the value of the attribute""" self._on_update_events: set[PredicateEvent[DType_T]] = set() """Events to set when the value satisifies some predicate""" @@ -75,6 +77,7 @@ async def update(self, value: Any) -> None: "Attribute set", value=value, value_type=type(value), attribute=self ) + _previous_value = self._value self._value = self._datatype.validate(value) self._on_update_events -= { @@ -82,17 +85,22 @@ async def update(self, value: Any) -> None: } if self._on_update_callbacks is not None: + callbacks_to_call: list[AttrOnUpdateCallback[DType_T]] = [ + cb + for cb, always in self._on_update_callbacks + if always or not self.datatype.equal(self._value, _previous_value) + ] try: - await asyncio.gather( - *[cb(self._value) for cb in self._on_update_callbacks] - ) + await asyncio.gather(*[cb(self._value) for cb in callbacks_to_call]) except Exception as e: logger.opt(exception=e).error( "On update callbacks failed", attribute=self, value=value ) raise - def add_on_update_callback(self, callback: AttrOnUpdateCallback[DType_T]) -> None: + def add_on_update_callback( + self, callback: AttrOnUpdateCallback[DType_T], always: bool = False + ) -> None: """Add a callback to be called when the value of the attribute is updated The callback will be called with the updated value. @@ -100,7 +108,7 @@ def add_on_update_callback(self, callback: AttrOnUpdateCallback[DType_T]) -> Non """ if self._on_update_callbacks is None: self._on_update_callbacks = [] - self._on_update_callbacks.append(callback) + self._on_update_callbacks.append((callback, always)) def set_update_callback(self, callback: AttrIOUpdateCallback[DType_T]): """Set the callback to update the value of the attribute from the source diff --git a/tests/test_attributes.py b/tests/test_attributes.py index d6472932..27025f48 100644 --- a/tests/test_attributes.py +++ b/tests/test_attributes.py @@ -104,10 +104,11 @@ async def update(attr: AttrR): @pytest.mark.asyncio async def test_attributes(): device = {"state": "Idle", "number": 1, "count": False} - ui = {"state": "", "number": 0, "count": False} + ui = {"state": "", "number": 0, "count": False, "update_count": 0} async def update_ui(value, key): ui[key] = value + ui["update_count"] += 1 async def send(_attr, value, key): device[key] = value @@ -116,9 +117,14 @@ async def device_add(): device["number"] += 1 attr_r = AttrR(String()) - attr_r.add_on_update_callback(partial(update_ui, key="state")) + attr_r.add_on_update_callback(partial(update_ui, key="state"), always=False) await attr_r.update(device["state"]) assert ui["state"] == "Idle" + # Update with new value triggers callback + assert ui["update_count"] == 1 + await attr_r.update(device["state"]) + # Identical update does not trigger callback as always=False + assert ui["update_count"] == 1 attr_rw = AttrRW(Int()) attr_rw._on_put_callback = partial(send, key="number") diff --git a/tests/transports/epics/pva/test_p4p.py b/tests/transports/epics/pva/test_p4p.py index 6c013c6e..3a1a06ac 100644 --- a/tests/transports/epics/pva/test_p4p.py +++ b/tests/transports/epics/pva/test_p4p.py @@ -232,8 +232,9 @@ async def _wait_and_set_attr_r(): await controller.a.update(40_000) await controller.b.update(-0.99) await asyncio.sleep(0.05) - await controller.a.update(-100) await controller.b.update(-0.99) + # Identical value, so will not cause a readback update + await controller.a.update(-100) await controller.b.update(-0.9111111) a_values, b_values = [], [] @@ -253,7 +254,7 @@ async def _wait_and_set_attr_r(): serve.cancel() wait_and_set_attr_r.cancel() assert a_values == [0, 40_000, -100] - assert b_values == [0.0, -0.99, -0.99, -0.91] # Last is -0.91 because of prec + assert b_values == [0.0, -0.99, -0.91] # Last is -0.91 because of prec def test_pvi_grouping():