Mercurial > ~darius > hgwebdir.cgi > epro
comparison velib_python/vedbus.py @ 8:9c0435a617db
Import velib_python
author | Daniel O'Connor <darius@dons.net.au> |
---|---|
date | Sun, 05 Dec 2021 14:35:36 +1030 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
5:982eeffe9d95 | 8:9c0435a617db |
---|---|
1 #!/usr/bin/env python3 | |
2 # -*- coding: utf-8 -*- | |
3 | |
4 import dbus.service | |
5 import logging | |
6 import traceback | |
7 import os | |
8 import weakref | |
9 from collections import defaultdict | |
10 from ve_utils import wrap_dbus_value, unwrap_dbus_value | |
11 | |
12 # vedbus contains three classes: | |
13 # VeDbusItemImport -> use this to read data from the dbus, ie import | |
14 # VeDbusItemExport -> use this to export data to the dbus (one value) | |
15 # VeDbusService -> use that to create a service and export several values to the dbus | |
16 | |
17 # Code for VeDbusItemImport is copied from busitem.py and thereafter modified. | |
18 # All projects that used busitem.py need to migrate to this package. And some | |
19 # projects used to define there own equivalent of VeDbusItemExport. Better to | |
20 # use VeDbusItemExport, or even better the VeDbusService class that does it all for you. | |
21 | |
22 # TODOS | |
23 # 1 check for datatypes, it works now, but not sure if all is compliant with | |
24 # com.victronenergy.BusItem interface definition. See also the files in | |
25 # tests_and_examples. And see 'if type(v) == dbus.Byte:' on line 102. Perhaps | |
26 # something similar should also be done in VeDbusBusItemExport? | |
27 # 2 Shouldn't VeDbusBusItemExport inherit dbus.service.Object? | |
28 # 7 Make hard rules for services exporting data to the D-Bus, in order to make tracking | |
29 # changes possible. Does everybody first invalidate its data before leaving the bus? | |
30 # And what about before taking one object away from the bus, instead of taking the | |
31 # whole service offline? | |
32 # They should! And after taking one value away, do we need to know that someone left | |
33 # the bus? Or we just keep that value in invalidated for ever? Result is that we can't | |
34 # see the difference anymore between an invalidated value and a value that was first on | |
35 # the bus and later not anymore. See comments above VeDbusItemImport as well. | |
36 # 9 there are probably more todos in the code below. | |
37 | |
38 # Some thoughts with regards to the data types: | |
39 # | |
40 # Text from: http://dbus.freedesktop.org/doc/dbus-python/doc/tutorial.html#data-types | |
41 # --- | |
42 # Variants are represented by setting the variant_level keyword argument in the | |
43 # constructor of any D-Bus data type to a value greater than 0 (variant_level 1 | |
44 # means a variant containing some other data type, variant_level 2 means a variant | |
45 # containing a variant containing some other data type, and so on). If a non-variant | |
46 # is passed as an argument but introspection indicates that a variant is expected, | |
47 # it'll automatically be wrapped in a variant. | |
48 # --- | |
49 # | |
50 # Also the different dbus datatypes, such as dbus.Int32, and dbus.UInt32 are a subclass | |
51 # of Python int. dbus.String is a subclass of Python standard class unicode, etcetera | |
52 # | |
53 # So all together that explains why we don't need to explicitly convert back and forth | |
54 # between the dbus datatypes and the standard python datatypes. Note that all datatypes | |
55 # in python are objects. Even an int is an object. | |
56 | |
57 # The signature of a variant is 'v'. | |
58 | |
59 # Export ourselves as a D-Bus service. | |
60 class VeDbusService(object): | |
61 def __init__(self, servicename, bus=None): | |
62 # dict containing the VeDbusItemExport objects, with their path as the key. | |
63 self._dbusobjects = {} | |
64 self._dbusnodes = {} | |
65 self._ratelimiters = [] | |
66 self._dbusname = None | |
67 | |
68 # dict containing the onchange callbacks, for each object. Object path is the key | |
69 self._onchangecallbacks = {} | |
70 | |
71 # Connect to session bus whenever present, else use the system bus | |
72 self._dbusconn = bus or (dbus.SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in os.environ else dbus.SystemBus()) | |
73 | |
74 # make the dbus connection available to outside, could make this a true property instead, but ach.. | |
75 self.dbusconn = self._dbusconn | |
76 | |
77 # Register ourselves on the dbus, trigger an error if already in use (do_not_queue) | |
78 self._dbusname = dbus.service.BusName(servicename, self._dbusconn, do_not_queue=True) | |
79 | |
80 # Add the root item that will return all items as a tree | |
81 self._dbusnodes['/'] = VeDbusRootExport(self._dbusconn, '/', self) | |
82 | |
83 logging.info("registered ourselves on D-Bus as %s" % servicename) | |
84 | |
85 # To force immediate deregistering of this dbus service and all its object paths, explicitly | |
86 # call __del__(). | |
87 def __del__(self): | |
88 for node in list(self._dbusnodes.values()): | |
89 node.__del__() | |
90 self._dbusnodes.clear() | |
91 for item in list(self._dbusobjects.values()): | |
92 item.__del__() | |
93 self._dbusobjects.clear() | |
94 if self._dbusname: | |
95 self._dbusname.__del__() # Forces call to self._bus.release_name(self._name), see source code | |
96 self._dbusname = None | |
97 | |
98 # @param callbackonchange function that will be called when this value is changed. First parameter will | |
99 # be the path of the object, second the new value. This callback should return | |
100 # True to accept the change, False to reject it. | |
101 def add_path(self, path, value, description="", writeable=False, | |
102 onchangecallback=None, gettextcallback=None): | |
103 | |
104 if onchangecallback is not None: | |
105 self._onchangecallbacks[path] = onchangecallback | |
106 | |
107 item = VeDbusItemExport( | |
108 self._dbusconn, path, value, description, writeable, | |
109 self._value_changed, gettextcallback, deletecallback=self._item_deleted) | |
110 | |
111 spl = path.split('/') | |
112 for i in range(2, len(spl)): | |
113 subPath = '/'.join(spl[:i]) | |
114 if subPath not in self._dbusnodes and subPath not in self._dbusobjects: | |
115 self._dbusnodes[subPath] = VeDbusTreeExport(self._dbusconn, subPath, self) | |
116 self._dbusobjects[path] = item | |
117 logging.debug('added %s with start value %s. Writeable is %s' % (path, value, writeable)) | |
118 | |
119 # Add the mandatory paths, as per victron dbus api doc | |
120 def add_mandatory_paths(self, processname, processversion, connection, | |
121 deviceinstance, productid, productname, firmwareversion, hardwareversion, connected): | |
122 self.add_path('/Mgmt/ProcessName', processname) | |
123 self.add_path('/Mgmt/ProcessVersion', processversion) | |
124 self.add_path('/Mgmt/Connection', connection) | |
125 | |
126 # Create rest of the mandatory objects | |
127 self.add_path('/DeviceInstance', deviceinstance) | |
128 self.add_path('/ProductId', productid) | |
129 self.add_path('/ProductName', productname) | |
130 self.add_path('/FirmwareVersion', firmwareversion) | |
131 self.add_path('/HardwareVersion', hardwareversion) | |
132 self.add_path('/Connected', connected) | |
133 | |
134 # Callback function that is called from the VeDbusItemExport objects when a value changes. This function | |
135 # maps the change-request to the onchangecallback given to us for this specific path. | |
136 def _value_changed(self, path, newvalue): | |
137 if path not in self._onchangecallbacks: | |
138 return True | |
139 | |
140 return self._onchangecallbacks[path](path, newvalue) | |
141 | |
142 def _item_deleted(self, path): | |
143 self._dbusobjects.pop(path) | |
144 for np in list(self._dbusnodes.keys()): | |
145 if np != '/': | |
146 for ip in self._dbusobjects: | |
147 if ip.startswith(np + '/'): | |
148 break | |
149 else: | |
150 self._dbusnodes[np].__del__() | |
151 self._dbusnodes.pop(np) | |
152 | |
153 def __getitem__(self, path): | |
154 return self._dbusobjects[path].local_get_value() | |
155 | |
156 def __setitem__(self, path, newvalue): | |
157 self._dbusobjects[path].local_set_value(newvalue) | |
158 | |
159 def __delitem__(self, path): | |
160 self._dbusobjects[path].__del__() # Invalidates and then removes the object path | |
161 assert path not in self._dbusobjects | |
162 | |
163 def __contains__(self, path): | |
164 return path in self._dbusobjects | |
165 | |
166 def __enter__(self): | |
167 l = ServiceContext(self) | |
168 self._ratelimiters.append(l) | |
169 return l | |
170 | |
171 def __exit__(self, *exc): | |
172 # pop off the top one and flush it. If with statements are nested | |
173 # then each exit flushes its own part. | |
174 if self._ratelimiters: | |
175 self._ratelimiters.pop().flush() | |
176 | |
177 class ServiceContext(object): | |
178 def __init__(self, parent): | |
179 self.parent = parent | |
180 self.changes = {} | |
181 | |
182 def __getitem__(self, path): | |
183 return self.parent[path] | |
184 | |
185 def __setitem__(self, path, newvalue): | |
186 c = self.parent._dbusobjects[path]._local_set_value(newvalue) | |
187 if c is not None: | |
188 self.changes[path] = c | |
189 | |
190 def flush(self): | |
191 if self.changes: | |
192 self.parent._dbusnodes['/'].ItemsChanged(self.changes) | |
193 | |
194 class TrackerDict(defaultdict): | |
195 """ Same as defaultdict, but passes the key to default_factory. """ | |
196 def __missing__(self, key): | |
197 self[key] = x = self.default_factory(key) | |
198 return x | |
199 | |
200 class VeDbusRootTracker(object): | |
201 """ This tracks the root of a dbus path and listens for PropertiesChanged | |
202 signals. When a signal arrives, parse it and unpack the key/value changes | |
203 into traditional events, then pass it to the original eventCallback | |
204 method. """ | |
205 def __init__(self, bus, serviceName): | |
206 self.importers = defaultdict(weakref.WeakSet) | |
207 self.serviceName = serviceName | |
208 self._match = bus.get_object(serviceName, '/', introspect=False).connect_to_signal( | |
209 "ItemsChanged", weak_functor(self._items_changed_handler)) | |
210 | |
211 def __del__(self): | |
212 self._match.remove() | |
213 self._match = None | |
214 | |
215 def add(self, i): | |
216 self.importers[i.path].add(i) | |
217 | |
218 def _items_changed_handler(self, items): | |
219 if not isinstance(items, dict): | |
220 return | |
221 | |
222 for path, changes in items.items(): | |
223 try: | |
224 v = changes['Value'] | |
225 except KeyError: | |
226 continue | |
227 | |
228 try: | |
229 t = changes['Text'] | |
230 except KeyError: | |
231 t = str(unwrap_dbus_value(v)) | |
232 | |
233 for i in self.importers.get(path, ()): | |
234 i._properties_changed_handler({'Value': v, 'Text': t}) | |
235 | |
236 """ | |
237 Importing basics: | |
238 - If when we power up, the D-Bus service does not exist, or it does exist and the path does not | |
239 yet exist, still subscribe to a signal: as soon as it comes online it will send a signal with its | |
240 initial value, which VeDbusItemImport will receive and use to update local cache. And, when set, | |
241 call the eventCallback. | |
242 - If when we power up, save it | |
243 - When using get_value, know that there is no difference between services (or object paths) that don't | |
244 exist and paths that are invalid (= empty array, see above). Both will return None. In case you do | |
245 really want to know ifa path exists or not, use the exists property. | |
246 - When a D-Bus service leaves the D-Bus, it will first invalidate all its values, and send signals | |
247 with that update, and only then leave the D-Bus. (or do we need to subscribe to the NameOwnerChanged- | |
248 signal!?!) To be discussed and make sure. Not really urgent, since all existing code that uses this | |
249 class already subscribes to the NameOwnerChanged signal, and subsequently removes instances of this | |
250 class. | |
251 | |
252 Read when using this class: | |
253 Note that when a service leaves that D-Bus without invalidating all its exported objects first, for | |
254 example because it is killed, VeDbusItemImport doesn't have a clue. So when using VeDbusItemImport, | |
255 make sure to also subscribe to the NamerOwnerChanged signal on bus-level. Or just use dbusmonitor, | |
256 because that takes care of all of that for you. | |
257 """ | |
258 class VeDbusItemImport(object): | |
259 def __new__(cls, bus, serviceName, path, eventCallback=None, createsignal=True): | |
260 instance = object.__new__(cls) | |
261 | |
262 # If signal tracking should be done, also add to root tracker | |
263 if createsignal: | |
264 if "_roots" not in cls.__dict__: | |
265 cls._roots = TrackerDict(lambda k: VeDbusRootTracker(bus, k)) | |
266 | |
267 return instance | |
268 | |
269 ## Constructor | |
270 # @param bus the bus-object (SESSION or SYSTEM). | |
271 # @param serviceName the dbus-service-name (string), for example 'com.victronenergy.battery.ttyO1' | |
272 # @param path the object-path, for example '/Dc/V' | |
273 # @param eventCallback function that you want to be called on a value change | |
274 # @param createSignal only set this to False if you use this function to one time read a value. When | |
275 # leaving it to True, make sure to also subscribe to the NameOwnerChanged signal | |
276 # elsewhere. See also note some 15 lines up. | |
277 def __init__(self, bus, serviceName, path, eventCallback=None, createsignal=True): | |
278 # TODO: is it necessary to store _serviceName and _path? Isn't it | |
279 # stored in the bus_getobjectsomewhere? | |
280 self._serviceName = serviceName | |
281 self._path = path | |
282 self._match = None | |
283 # TODO: _proxy is being used in settingsdevice.py, make a getter for that | |
284 self._proxy = bus.get_object(serviceName, path, introspect=False) | |
285 self.eventCallback = eventCallback | |
286 | |
287 assert eventCallback is None or createsignal == True | |
288 if createsignal: | |
289 self._match = self._proxy.connect_to_signal( | |
290 "PropertiesChanged", weak_functor(self._properties_changed_handler)) | |
291 self._roots[serviceName].add(self) | |
292 | |
293 # store the current value in _cachedvalue. When it doesn't exists set _cachedvalue to | |
294 # None, same as when a value is invalid | |
295 self._cachedvalue = None | |
296 try: | |
297 v = self._proxy.GetValue() | |
298 except dbus.exceptions.DBusException: | |
299 pass | |
300 else: | |
301 self._cachedvalue = unwrap_dbus_value(v) | |
302 | |
303 def __del__(self): | |
304 if self._match is not None: | |
305 self._match.remove() | |
306 self._match = None | |
307 self._proxy = None | |
308 | |
309 def _refreshcachedvalue(self): | |
310 self._cachedvalue = unwrap_dbus_value(self._proxy.GetValue()) | |
311 | |
312 ## Returns the path as a string, for example '/AC/L1/V' | |
313 @property | |
314 def path(self): | |
315 return self._path | |
316 | |
317 ## Returns the dbus service name as a string, for example com.victronenergy.vebus.ttyO1 | |
318 @property | |
319 def serviceName(self): | |
320 return self._serviceName | |
321 | |
322 ## Returns the value of the dbus-item. | |
323 # the type will be a dbus variant, for example dbus.Int32(0, variant_level=1) | |
324 # this is not a property to keep the name consistant with the com.victronenergy.busitem interface | |
325 # returns None when the property is invalid | |
326 def get_value(self): | |
327 return self._cachedvalue | |
328 | |
329 ## Writes a new value to the dbus-item | |
330 def set_value(self, newvalue): | |
331 r = self._proxy.SetValue(wrap_dbus_value(newvalue)) | |
332 | |
333 # instead of just saving the value, go to the dbus and get it. So we have the right type etc. | |
334 if r == 0: | |
335 self._refreshcachedvalue() | |
336 | |
337 return r | |
338 | |
339 ## Resets the item to its default value | |
340 def set_default(self): | |
341 self._proxy.SetDefault() | |
342 self._refreshcachedvalue() | |
343 | |
344 ## Returns the text representation of the value. | |
345 # For example when the value is an enum/int GetText might return the string | |
346 # belonging to that enum value. Another example, for a voltage, GetValue | |
347 # would return a float, 12.0Volt, and GetText could return 12 VDC. | |
348 # | |
349 # Note that this depends on how the dbus-producer has implemented this. | |
350 def get_text(self): | |
351 return self._proxy.GetText() | |
352 | |
353 ## Returns true of object path exists, and false if it doesn't | |
354 @property | |
355 def exists(self): | |
356 # TODO: do some real check instead of this crazy thing. | |
357 r = False | |
358 try: | |
359 r = self._proxy.GetValue() | |
360 r = True | |
361 except dbus.exceptions.DBusException: | |
362 pass | |
363 | |
364 return r | |
365 | |
366 ## callback for the trigger-event. | |
367 # @param eventCallback the event-callback-function. | |
368 @property | |
369 def eventCallback(self): | |
370 return self._eventCallback | |
371 | |
372 @eventCallback.setter | |
373 def eventCallback(self, eventCallback): | |
374 self._eventCallback = eventCallback | |
375 | |
376 ## Is called when the value of the imported bus-item changes. | |
377 # Stores the new value in our local cache, and calls the eventCallback, if set. | |
378 def _properties_changed_handler(self, changes): | |
379 if "Value" in changes: | |
380 changes['Value'] = unwrap_dbus_value(changes['Value']) | |
381 self._cachedvalue = changes['Value'] | |
382 if self._eventCallback: | |
383 # The reason behind this try/except is to prevent errors silently ending up the an error | |
384 # handler in the dbus code. | |
385 try: | |
386 self._eventCallback(self._serviceName, self._path, changes) | |
387 except: | |
388 traceback.print_exc() | |
389 os._exit(1) # sys.exit() is not used, since that also throws an exception | |
390 | |
391 | |
392 class VeDbusTreeExport(dbus.service.Object): | |
393 def __init__(self, bus, objectPath, service): | |
394 dbus.service.Object.__init__(self, bus, objectPath) | |
395 self._service = service | |
396 logging.debug("VeDbusTreeExport %s has been created" % objectPath) | |
397 | |
398 def __del__(self): | |
399 # self._get_path() will raise an exception when retrieved after the call to .remove_from_connection, | |
400 # so we need a copy. | |
401 path = self._get_path() | |
402 if path is None: | |
403 return | |
404 self.remove_from_connection() | |
405 logging.debug("VeDbusTreeExport %s has been removed" % path) | |
406 | |
407 def _get_path(self): | |
408 if len(self._locations) == 0: | |
409 return None | |
410 return self._locations[0][1] | |
411 | |
412 def _get_value_handler(self, path, get_text=False): | |
413 logging.debug("_get_value_handler called for %s" % path) | |
414 r = {} | |
415 px = path | |
416 if not px.endswith('/'): | |
417 px += '/' | |
418 for p, item in self._service._dbusobjects.items(): | |
419 if p.startswith(px): | |
420 v = item.GetText() if get_text else wrap_dbus_value(item.local_get_value()) | |
421 r[p[len(px):]] = v | |
422 logging.debug(r) | |
423 return r | |
424 | |
425 @dbus.service.method('com.victronenergy.BusItem', out_signature='v') | |
426 def GetValue(self): | |
427 value = self._get_value_handler(self._get_path()) | |
428 return dbus.Dictionary(value, signature=dbus.Signature('sv'), variant_level=1) | |
429 | |
430 @dbus.service.method('com.victronenergy.BusItem', out_signature='v') | |
431 def GetText(self): | |
432 return self._get_value_handler(self._get_path(), True) | |
433 | |
434 def local_get_value(self): | |
435 return self._get_value_handler(self.path) | |
436 | |
437 class VeDbusRootExport(VeDbusTreeExport): | |
438 @dbus.service.signal('com.victronenergy.BusItem', signature='a{sa{sv}}') | |
439 def ItemsChanged(self, changes): | |
440 pass | |
441 | |
442 @dbus.service.method('com.victronenergy.BusItem', out_signature='a{sa{sv}}') | |
443 def GetItems(self): | |
444 return { | |
445 path: { | |
446 'Value': wrap_dbus_value(item.local_get_value()), | |
447 'Text': item.GetText() } | |
448 for path, item in self._service._dbusobjects.items() | |
449 } | |
450 | |
451 | |
452 class VeDbusItemExport(dbus.service.Object): | |
453 ## Constructor of VeDbusItemExport | |
454 # | |
455 # Use this object to export (publish), values on the dbus | |
456 # Creates the dbus-object under the given dbus-service-name. | |
457 # @param bus The dbus object. | |
458 # @param objectPath The dbus-object-path. | |
459 # @param value Value to initialize ourselves with, defaults to None which means Invalid | |
460 # @param description String containing a description. Can be called over the dbus with GetDescription() | |
461 # @param writeable what would this do!? :). | |
462 # @param callback Function that will be called when someone else changes the value of this VeBusItem | |
463 # over the dbus. First parameter passed to callback will be our path, second the new | |
464 # value. This callback should return True to accept the change, False to reject it. | |
465 def __init__(self, bus, objectPath, value=None, description=None, writeable=False, | |
466 onchangecallback=None, gettextcallback=None, deletecallback=None): | |
467 dbus.service.Object.__init__(self, bus, objectPath) | |
468 self._onchangecallback = onchangecallback | |
469 self._gettextcallback = gettextcallback | |
470 self._value = value | |
471 self._description = description | |
472 self._writeable = writeable | |
473 self._deletecallback = deletecallback | |
474 | |
475 # To force immediate deregistering of this dbus object, explicitly call __del__(). | |
476 def __del__(self): | |
477 # self._get_path() will raise an exception when retrieved after the | |
478 # call to .remove_from_connection, so we need a copy. | |
479 path = self._get_path() | |
480 if path == None: | |
481 return | |
482 if self._deletecallback is not None: | |
483 self._deletecallback(path) | |
484 self.local_set_value(None) | |
485 self.remove_from_connection() | |
486 logging.debug("VeDbusItemExport %s has been removed" % path) | |
487 | |
488 def _get_path(self): | |
489 if len(self._locations) == 0: | |
490 return None | |
491 return self._locations[0][1] | |
492 | |
493 ## Sets the value. And in case the value is different from what it was, a signal | |
494 # will be emitted to the dbus. This function is to be used in the python code that | |
495 # is using this class to export values to the dbus. | |
496 # set value to None to indicate that it is Invalid | |
497 def local_set_value(self, newvalue): | |
498 changes = self._local_set_value(newvalue) | |
499 if changes is not None: | |
500 self.PropertiesChanged(changes) | |
501 | |
502 def _local_set_value(self, newvalue): | |
503 if self._value == newvalue: | |
504 return None | |
505 | |
506 self._value = newvalue | |
507 return { | |
508 'Value': wrap_dbus_value(newvalue), | |
509 'Text': self.GetText() | |
510 } | |
511 | |
512 def local_get_value(self): | |
513 return self._value | |
514 | |
515 # ==== ALL FUNCTIONS BELOW THIS LINE WILL BE CALLED BY OTHER PROCESSES OVER THE DBUS ==== | |
516 | |
517 ## Dbus exported method SetValue | |
518 # Function is called over the D-Bus by other process. It will first check (via callback) if new | |
519 # value is accepted. And it is, stores it and emits a changed-signal. | |
520 # @param value The new value. | |
521 # @return completion-code When successful a 0 is return, and when not a -1 is returned. | |
522 @dbus.service.method('com.victronenergy.BusItem', in_signature='v', out_signature='i') | |
523 def SetValue(self, newvalue): | |
524 if not self._writeable: | |
525 return 1 # NOT OK | |
526 | |
527 newvalue = unwrap_dbus_value(newvalue) | |
528 | |
529 if newvalue == self._value: | |
530 return 0 # OK | |
531 | |
532 # call the callback given to us, and check if new value is OK. | |
533 if (self._onchangecallback is None or | |
534 (self._onchangecallback is not None and self._onchangecallback(self.__dbus_object_path__, newvalue))): | |
535 | |
536 self.local_set_value(newvalue) | |
537 return 0 # OK | |
538 | |
539 return 2 # NOT OK | |
540 | |
541 ## Dbus exported method GetDescription | |
542 # | |
543 # Returns the a description. | |
544 # @param language A language code (e.g. ISO 639-1 en-US). | |
545 # @param length Lenght of the language string. | |
546 # @return description | |
547 @dbus.service.method('com.victronenergy.BusItem', in_signature='si', out_signature='s') | |
548 def GetDescription(self, language, length): | |
549 return self._description if self._description is not None else 'No description given' | |
550 | |
551 ## Dbus exported method GetValue | |
552 # Returns the value. | |
553 # @return the value when valid, and otherwise an empty array | |
554 @dbus.service.method('com.victronenergy.BusItem', out_signature='v') | |
555 def GetValue(self): | |
556 return wrap_dbus_value(self._value) | |
557 | |
558 ## Dbus exported method GetText | |
559 # Returns the value as string of the dbus-object-path. | |
560 # @return text A text-value. '---' when local value is invalid | |
561 @dbus.service.method('com.victronenergy.BusItem', out_signature='s') | |
562 def GetText(self): | |
563 if self._value is None: | |
564 return '---' | |
565 | |
566 # Default conversion from dbus.Byte will get you a character (so 'T' instead of '84'), so we | |
567 # have to convert to int first. Note that if a dbus.Byte turns up here, it must have come from | |
568 # the application itself, as all data from the D-Bus should have been unwrapped by now. | |
569 if self._gettextcallback is None and type(self._value) == dbus.Byte: | |
570 return str(int(self._value)) | |
571 | |
572 if self._gettextcallback is None and self.__dbus_object_path__ == '/ProductId': | |
573 return "0x%X" % self._value | |
574 | |
575 if self._gettextcallback is None: | |
576 return str(self._value) | |
577 | |
578 return self._gettextcallback(self.__dbus_object_path__, self._value) | |
579 | |
580 ## The signal that indicates that the value has changed. | |
581 # Other processes connected to this BusItem object will have subscribed to the | |
582 # event when they want to track our state. | |
583 @dbus.service.signal('com.victronenergy.BusItem', signature='a{sv}') | |
584 def PropertiesChanged(self, changes): | |
585 pass | |
586 | |
587 ## This class behaves like a regular reference to a class method (eg. self.foo), but keeps a weak reference | |
588 ## to the object which method is to be called. | |
589 ## Use this object to break circular references. | |
590 class weak_functor: | |
591 def __init__(self, f): | |
592 self._r = weakref.ref(f.__self__) | |
593 self._f = weakref.ref(f.__func__) | |
594 | |
595 def __call__(self, *args, **kargs): | |
596 r = self._r() | |
597 f = self._f() | |
598 if r == None or f == None: | |
599 return | |
600 f(r, *args, **kargs) |