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

Reply via email to