11import time
22from typing import List , Optional
3+ from dataclasses import dataclass
34
4- from caproto import ReadNotifyResponse
5+ from caproto import ReadNotifyResponse , timestamp_to_epics
56from caproto .threading .client import PV
67from caproto .threading .client import Context as CAContext
78
89from forwarder .application_logger import get_logger
910from forwarder .metrics import Counter , Summary , sanitise_metric_name
1011from forwarder .metrics .statistics_reporter import StatisticsReporter
1112from forwarder .update_handlers .serialiser_tracker import SerialiserTracker
13+ from forwarder .update_handlers .un00_serialiser import un00_CASerialiser
1214
1315
1416class CAUpdateHandler :
@@ -34,6 +36,7 @@ def __init__(
3436 self ._processing_errors_metric = processing_errors_metric
3537 self ._processing_latency_metric = None
3638 self ._receive_latency_metric = None
39+ self ._last_update = 0
3740 if self ._statistics_reporter :
3841 try :
3942 self ._processing_latency_metric = Summary (
@@ -72,19 +75,59 @@ def __init__(
7275 ctrl_sub .add_callback (self ._unit_callback )
7376
7477 def _unit_callback (self , sub , response : ReadNotifyResponse ):
78+ # sometimes caproto gives us a unit callback before a monitor callback.
79+ # in this case, to avoid just dropping the unit update, approximate
80+ # by using the current time.
81+ fallback_timestamp = time .time ()
82+
83+ self ._logger .debug ("CA Unit callback called for %s" , self ._pv_name )
84+
7585 old_unit = self ._current_unit
7686 try :
77- self ._current_unit = response .metadata .units .decode ("utf-8" )
87+ new_unit = response .metadata .units .decode ("utf-8" )
88+ if new_unit is not None :
89+ # we get a unit callback with blank units if the value has updated but the EGU field
90+ # has not.
91+ self ._current_unit = new_unit
7892 except AttributeError :
79- return
80- if old_unit is not None and old_unit != self ._current_unit :
81- self ._logger .error (
93+ self ._current_unit = None
94+
95+ if old_unit != self ._current_unit :
96+ self ._logger .info (
8297 f'Display unit of (ca) PV with name "{ self ._pv_name } " changed from "{ old_unit } " to "{ self ._current_unit } ".'
8398 )
84- if self ._processing_errors_metric :
85- self ._processing_errors_metric .inc ()
99+ for serialiser_tracker in self .serialiser_tracker_list :
100+ # Only let the unit serialiser deal with this update - as it has no value the other
101+ # serialisers will fall over.
102+ if isinstance (serialiser_tracker .serialiser , un00_CASerialiser ):
103+
104+ # The next bit is pretty hacky. We are mocking the ReadNotifyResponse
105+ # as by default its metadata is immutable/read-only, but we need to append the
106+ # timestamp here.
107+ @dataclass
108+ class StupidMetaData :
109+ timestamp : float
110+ units : str
111+
112+ @dataclass
113+ class StupidResponse :
114+ metadata : StupidMetaData
115+
116+
117+ update_time = self ._last_update if self ._last_update > 0 else fallback_timestamp
118+ self ._logger .debug (f"about to publish update. units: { self ._current_unit } , timestamp: { update_time } " )
119+ meta = StupidMetaData (timestamp = update_time , units = self ._current_unit )
120+ response = StupidResponse (metadata = meta )
121+ serialiser_tracker .process_ca_message (response ) # type: ignore
122+
86123
87124 def _monitor_callback (self , sub , response : ReadNotifyResponse ):
125+ self ._logger .debug ("CA Monitor callback called for %s" , self ._pv_name )
126+ try :
127+ self ._last_update = response .metadata .timestamp
128+ except Exception :
129+ self ._logger .warning ("Error getting timestamp for %s" , sub .pv .name )
130+
88131 if self ._receive_latency_metric :
89132 try :
90133 response_timestamp = response .metadata .timestamp .seconds + (
0 commit comments