whaleygeek / pyenergenie

A python interface to the Energenie line of products
MIT License
82 stars 51 forks source link

Discovery of devices #56

Closed whaleygeek closed 8 years ago

whaleygeek commented 8 years ago

Auto-discovery of devices that regularly transmit, would be a useful feature. That way you could plug in a MiHome Adaptor plus and it would start sending it's 10 second reports, and a user app could say 'I've got a new device here I don't know about, do you want to add it to your app/registry'.

whaleygeek commented 8 years ago

Comment taken from #18 from Mark:

The only bit I'm unclear on is discovery? Would I register a callback to be called when a new device is discovered and then call a factory method to get a new one? That would be nice then I could use isinstanceof() to find out what kind of energenie it was.

whaleygeek commented 8 years ago

I think the answer to this is yes.

I've yet to decide where messages for unknown devices will be routed when they come in, but they will bubble up from the air_interface (radio) as they arrive, and something will have to route them to the appropriate device class instance. That something object (might be the registry, or a message router inside the registry perhaps) could then call your callback and you could decide what to do about it.

instanceof() is a good idea, you would know by then that it is a MIHO005 and a class instance could be auto created based on that knowledge and the newly discovered device_id. You could then add it to the registry by just calling add() and giving it a name.

Also you can use duck typing - anything that has a switch will have a turn_on() method, so you could make your binding to the device a little looser, rather than saying "it's a MIHO005 so it must have a switch" you could use the knowledge of it having a turn_on() - or use the (still to be decided, but I'm warming to it, method has_switch()).

I think I will probably separate the discovery from the registration - i.e. just because you know there is a device, and have a device class instance that can communicate with it, you may or may not want it added to the registry, so I'll leave that 'link' to be made in the callback (it's just a single method call anyway)

whaleygeek commented 8 years ago

Note to self, based on Mark's suggestions, it's probably something like this:

# register a callback with something to call new_device() when a non registered device is sent
# (probably means it *is* the registry, or *consults* the registry to know it is unknown)

def unknown_device(device):
    if device.has_switch():
        print("Auto adding new device: %s" % device)
        # might need a way to auto generate names (or async consult user for a name)
        registry.add(device, generate_name(device))
        if device instanceof ENER005:
            print("yippee, it's an AdaptorPlus!!")
            # let's be a bit devilish and turn it on
            device.turn_on()

registry.when_unknown_device(unknown_device)
whaleygeek commented 8 years ago

I've started designing the discovery service, as it fits in nicely with the OO work I'm doing at the moment.

One thing that is clear is that there are choices as to how the discovery workflow would look, so I'm planning to add a few design patterns that the app writer can choose from.

Specifically, one very natural design flow is that you hold the join button on the front which initiates a join request message. When the rx message router sees this message from an unknown device, it passes it to the app callback to decide whether to accept/reject. On accept, the library code can then send back a join_ack to stop the join light flashing. It can also easily auto-create the device in the registry, create a runtime instance of it, and connect it up to the message router so that it 'just works' from then on.

Other flows are possible with the design, such as app-managed registration if required.

The file energenie/Registry.py has ongoing work on a Discovery class that will provide this functionality. I might put some default choices in the energenie/init.py so that in most cases, the app write doesn't have to do much wiring up, they just have to register a callback for 'new device confirmation'.

whaleygeek commented 8 years ago

Discovery works on the new device_classes branch.

There are 5 automatic modes you can choose from, or you can provide your own:

none: ignore data from unknown devices auto: on first report from an unknown device, add it to the registry ask: on first report from an unknown device, ask the user if they want it registered join: on join_req (hold button) from a device, add it to the registry joinask: on join_req (hold button) from a device, ask user if they want it registered

auto class instance creation in the registry is also working. So if you auto/join your device will automatically appear in the registry, and registry.devices() can be iterated to perform operations on all registered devices.

There is an app callback mechanism in energenie.fsk_router.

Use fsk_router.when_incoming(callback) to register a handler for any message from any MiHome device. Signature of the callback is fn(address, message) where address is a tuple of (mfrid, productid, deviceid) and message is a Message() wrapped payload with a whole range of useful accessor methods. For example, use this to generate a raw log of all received messages.

Use fsk_router.when_unknown(callback) to register a handler for any message from an unknown device - this is the single hook point that the Discovery() hierarchy of behaviours use to implement auto, ask and join semantics. The callback signature is fn(address, message) - again, address is a tuple of (mfrid, productid, deviceid) and message is a Message() wrapped payload.

The Discovery() class hierarchy in Registry.py shows how you can auto register an unknown device in the registry, and auto route messages to it with the fsk_router. There are 5 design patterns there, with helper methods like discovery_none() discovery_autojoin() that you can use to choose one of the default design patterns.

The default if you don't fiddle with anything in your app, is that it will ignore messages from unknown devices, but if you hold the button for a few seconds so it sends in a JOIN_REQ, it will auto create a device class instance to manage it, auto add this device to the registry, auto add a route to it so that all received messages from it are now routed to that device class instance, and it will send a JOIN_ACK to the device which will stop the join light from flashing.

From then on, energenie.registry.devices() can be iterated at any time to perform operations on all devices (you could use the d.has_switch() to work out if it has a switch or not, or use d.isinstance(energenie.Devices.MIHO005) if you want to check if it is a specific device.

I won't be merging this back to master until I have completed the following tasks:

whaleygeek commented 8 years ago

All of this is now implemented and basically working on the device_classes branch. Pending testing on real hardware and merging back to master, this works.

There is a new example 'discover_mihome.py' that shows 4 different types of standard behaviour for discovery that you can choose from. Also the setup_tool.py has a discovery option that you can use to discover devices and seed your registry.kvs file.

Note that as requested, there is a 'ask first' or an 'auto' feature. You can also configure discovery to work from first received message (any message, such as a report), or join message only (holding the button down for a few seconds to send a join request). Note that with the join message, it sends a join_ack back to the device to turn the light off, once the device has been added to the registry. If you use joinask and reply 'no', then the join_ack will not be send back, and the light on the front of the device will stay on.

whaleygeek commented 8 years ago

Discovery is now implemented and working in the latest code just merged to master from the device_classes branch/sprint.

Discovery is automatic. You can choose one of 5 different behaviours at startup by calling any of these after energenie.init()

energenie.discovery_none() - discovery is turned off
energenie.discovery_auto() - automatically add any unknown device to the registry
energenie.discovery_ask(fn) - call fn() on any unknown message, to ask for adding to registry
energenie.discovery_autojoin() - automatically add to registry anything that sends in a join_req
energenie.discovery_askjoin(fn) - call fn() on any join_req, to ask for adding to registry

fn() returns True to accept, False to reject.

If you want to roll your own discovery mechanism, look at the file energenie/init.py and Registry.py (with all the Discovery behaviours) and implement your own.

There is a simple hand-rolled discovery demo in file discover_mihome.py that shows how to knit in a custom ask function to one of the standard behaviours (and how to make that standard behaviour current)