10
10
# specific language governing permissions and limitations under the License.
11
11
12
12
import asyncio
13
+ import datetime
13
14
import logging
14
15
import warnings
15
16
from typing import Generic , Type , Optional , List , Any , Union , Dict
16
17
18
+ from . import timer
17
19
from .base import Writable , T , Readable , Subscribable , UninitializedError , Reading
18
20
from .expressions import ExpressionWrapper
19
21
@@ -46,7 +48,7 @@ def __init__(self, type_: Type[T], name: Optional[str] = None, initial_value: Op
46
48
self ._value : Optional [T ] = initial_value
47
49
self ._variable_fields : Dict [str , "VariableField" ] = {}
48
50
49
- # Create VariableFields for each typeannotated field of the type if it is typing.NamedTuple-based.
51
+ # Create VariableFields for each type-annotated field of the type if it is typing.NamedTuple-based.
50
52
if issubclass (type_ , tuple ) and type_ .__annotations__ :
51
53
for name , field_type in type_ .__annotations__ .items ():
52
54
variable_field = VariableField (self , name , field_type )
@@ -73,11 +75,16 @@ async def _write(self, value: T, origin: List[Any]) -> None:
73
75
self ._value = value
74
76
if old_value != value : # if a single field is different, the full value will also be different
75
77
logger .info ("New value %s for Variable %s from %s" , value , self , origin [:1 ])
76
- self ._publish (value , origin )
77
- for name , field in self ._variable_fields .items ():
78
- field ._recursive_publish (getattr (value , name ),
79
- None if old_value is None else getattr (old_value , name ),
80
- origin )
78
+ self ._do_all_publish (old_value , origin )
79
+
80
+ def _do_all_publish (self , old_value : Optional [T ], origin : List [Any ]) -> None :
81
+ logger .debug ("Publishing value %s for Variable %s" , self ._value , self )
82
+ assert self ._value is not None
83
+ self ._publish (self ._value , origin )
84
+ for name , field in self ._variable_fields .items ():
85
+ field ._recursive_publish (getattr (self ._value , name ),
86
+ None if old_value is None else getattr (old_value , name ),
87
+ origin )
81
88
82
89
async def read (self ) -> T :
83
90
if self ._value is None :
@@ -96,7 +103,7 @@ def EX(self) -> ExpressionWrapper:
96
103
97
104
def __repr__ (self ) -> str :
98
105
if self .name :
99
- return "<Variable \" {}\" >" .format (self .name )
106
+ return "<{} \" {}\" >" .format (self . __class__ . __name__ , self .name )
100
107
else :
101
108
return super ().__repr__ ()
102
109
@@ -158,3 +165,49 @@ async def read(self) -> T:
158
165
@property
159
166
def EX (self ) -> ExpressionWrapper :
160
167
return ExpressionWrapper (self )
168
+
169
+
170
+ class DelayedVariable (Variable [T ], Generic [T ]):
171
+ """
172
+ A Variable object, which delays the updates to avoid publishing half-updated values
173
+
174
+ This is achieved by delaying the publishing of a newly received value by a configurable amount of time
175
+ (`publish_delay`). If more value updates are received while a previous update publishing is still pending, the
176
+ latest value will be published at the originally scheduled publishing time. There will be no publishing of the
177
+ intermediate values. The next value update received after the publishing will be delayed by the configured delay
178
+ time again, resulting in a maximum update interval of the specified delay time.
179
+
180
+ This is similar (but slightly different) to the behaviour of :class:`shc.misc.RateLimitedSubscription`.
181
+
182
+ :param type_: The Variable's value type (used for its ``.type`` attribute, i.e. for the *Connectable* type
183
+ checking mechanism)
184
+ :param name: An optional name of the variable. Used for logging and future displaying purposes.
185
+ :param initial_value: An optional initial value for the Variable. If not provided and no default provider is
186
+ set via :meth:`set_provider`, the Variable is initialized with a None value and any :meth:`read` request
187
+ will raise an :exc:`shc.base.UninitializedError` until the first value update is received.
188
+ :param publish_delay: Amount of time to delay the publishing of a new value.
189
+ """
190
+ def __init__ (self , type_ : Type [T ], name : Optional [str ] = None , initial_value : Optional [T ] = None ,
191
+ publish_delay : datetime .timedelta = datetime .timedelta (seconds = 0.25 )):
192
+ super ().__init__ (type_ , name , initial_value )
193
+ self ._publish_delay = publish_delay
194
+ self ._pending_publish_task : Optional [asyncio .Task ] = None
195
+ self ._latest_origin : List [Any ] = []
196
+
197
+ async def _write (self , value : T , origin : List [Any ]) -> None :
198
+ old_value = self ._value
199
+ self ._value = value
200
+ self ._latest_origin = origin
201
+ if old_value != value : # if a single field is different, the full value will also be different
202
+ logger .info ("New value %s for Variable %s from %s" , value , self , origin [:1 ])
203
+ if not self ._pending_publish_task :
204
+ self ._pending_publish_task = asyncio .create_task (self ._wait_and_publish (old_value ))
205
+ timer .timer_supervisor .add_temporary_task (self ._pending_publish_task )
206
+
207
+ async def _wait_and_publish (self , old_value : Optional [T ]) -> None :
208
+ try :
209
+ await asyncio .sleep (self ._publish_delay .total_seconds ())
210
+ except asyncio .CancelledError :
211
+ pass
212
+ self ._do_all_publish (old_value , self ._latest_origin )
213
+ self ._pending_publish_task = None
0 commit comments