On 14May2020 08:28, Christopher de Vidal <cbdevidal....@gmail.com> wrote:
Help please? Creating an MQTT-to-Firestore bridge and I know a decorator
would help but I'm stumped how to create one. I've used decorators before
but not with arguments.
The Firestore collection.on_snapshot() method invokes a callback and sends
it three parameters (collection_snapshot, changes, and read_time). I need
the callback to also know the name of the collection so that I can publish
to the equivalent MQTT topic name. I had thought to add a fourth parameter
and I believe a decorator is the right approach but am stumped how to add
that fourth parameter. How would I do this with the code below?
To start with, I'm not convinced a decorator is a good approach here.
I'd use a small class.
Maybe you could provide an example of how you think the code would look
_with_ a decorator (ignoring the implementation of the decorator itself)
so that we can see what you'd like to use?
The thing about a decorator is that in normal use you use it to define a
new named function; each function would tend to be different in what it
does, otherwise you'd only have one function.
It sounds like you want a named function per collection name, but with
the _same_ inner code (on_snapshot). But decorators, being applied to
multiple functions, are usually for situations where the inner code
varies and the decorator is just making a shim to call it in a
particular way. You've got _one_ function and just want to attach it to
multiple collection names, kind of the inverse.
To clear my mind, I'll lay out the class approach (untested). Then if I
think you can do this with a decorator I'll try to sketch one.
class MQTTAdaptor:
def __init__(self, fbdb, collection_name):
self.fbdb = fbdb
self.collection_name = collection_name
self.fbref = None
self.subscribe()
def subscribe(self):
assert self.fbref is None
self.fbref = db.collection(collection_name)
self.fbref.on_snapshot(self.on_snapshot)
def unsubscribe(self):
self.fbref.unsubscribe()
self.fbref = None
def on_snapshot(self, col_snapshot, changes, read_time):
col_name = self.collection_name
data = {}
for doc in col_snapshot:
serial = doc.id
contents = load_json(doc.to_dict()['value'])
data[serial] = contents
for change in changes:
serial = change.document.id
mqtt_topic = col_name + '/' + serial
contents = data[serial]
if change.type.name in ['ADDED', 'MODIFIED']:
mqtt.publish(mqtt_topic, contents)
elif change.type.name == 'REMOVED':
mqtt.publish(mqtt_topic, None)
else:
warning("unhandled change type: %r" % change.type.name)
adaptors = []
for collection_name in 'cpu_temp', 'door_status':
adaptors.append(MQTTAdaptor, db, collection_name)
.... run for a while ...
for adaptor in adaptors:
adaptor.unsubscribe()
I've broken out the subscribe/unsubscribe as standalone methods just in
case you want to resubscribe an adaptor later (eg turn them on and off).
So here we've got a little class that keeps the state (the subscription
ref and the collection name) and has its own FB style on_snapshot which
passes stuff on to MQTT.
If you want to do this with a decorator you've got a small problem: it
is easy to make a shim like your on_snapshot callback, but if you want
to do a nice unsubscribe at the end thend you need to keep the ref
around somewhere. The class above provides a place to keep that.
With a decorator you need it to know where to store that ref. You can
use a global registry (ugh) or you could make one (just a dict) and pass
it to the decorator as well. We'll use a global and just use it in the
decorator directly, since we'll use "db" the same way.
# the registry
adaptors = {}
@adapt('cpu_temp')
def cpu_temp_on_snapshot(collection_name, col_snapshot, changes, read_time):
... your existing code here ...
and then to subscribe:
cpu_temp_col_ref = db.collection('cpu_temp')
cpu_temp_col_watch = cpu_temp_col_ref.on_snapshot(cpu_temp_on_snapshot)
but then for the door_status you want the same function repeated:
@adapt('door_status')
def door_on_snapshot(collection_name, col_snapshot, changes, read_time):
... your existing code here ...
and the same longhand subscription.
You can see this isn't any better - you're writing out on_snapshot
longhand every time. Now, a decorator just accepts a function as its
argument and returns a new function to be used in its place. So we could
define on_snapshot once and decorate it:
def named_snapshot(collection_name, col_snapshot, changes, read_time):
... the egneral function code here again ...
cpu_temp_on_snapshot = adapt('cpu_temp')(named_snapshot)
door_on_snapshot = adapt('door_status')(named_snapshot)
but (a) it doesn't look so much like a decorator any more and (b) you
still have to to the explicit subscription.
However we could have the decorator do the subscription _and_ record the
fbref for the unsubscription later. So not quite so bad.
So, how do you do the decorator-with-an-argument?
A decorator takes _one_ argument, a function, and returns a new
function. The syntax:
@foo
def bah(...):
...
is just a pretty shorthand for:
# original "bah"
def bah(...):
....
# switch out "bah" for a new function using the original "bah"
bah = foo(bah)
However, you're allowed to write:
@foo2(some-arguments...)
def bah(...):
...
What's going on there? The expression:
foo2(some-arguments...)
_itself_ returns a decorator. Which then decorates "bah".
So, making an @adapt decorator for your purpose... [... hack hack ...]
Well I can't write one of any use. I just wrote an @adapt decorator, but
it never calls the function it is passed when I do that (because the
mqtt "on_snapshot()" function already exists and it calls that instead).
So there's no use for a decorator :-(
Cheers,
Cameron Simpson <c...@cskk.id.au>
--
https://mail.python.org/mailman/listinfo/python-list