Open tmrobert8 opened 6 years ago
I agree with the points you complain about. There should be some better option provided by the framework. Without having a look at the implementation I cannot judge if the approach fits the most common needs.
Should a demonstration be implemented for e.g. the Hue binding so we have a visible example how the implementation looks like and check them for some drawbacks?
@maggu2810 @triller-telekom Sorry it took me awhile to get back to you on this. I don't know much about hue so I posted an example on my own binding (NEEO).
Background The NEEO Brain is a hub that contains zero to many rooms, each room contains zero to many devices. After discovering the brain, I'd like the rooms to immediately be discovered (and placed in the inbox) and when adding a room, I'd like the devices to immediately be discovered.
Sample implementation ThingDiscoveryParticipant is the interface that a binding would need to implement and will be notified when a thing should be evaluated
ThingDiscoveryService is the implementation of the discovery service that will monitor things and call the ThingDiscoveryParticipant
NeeoRoomDiscovery is the NEEO room discovery implementation of ThingDiscoveryService NeeoDeviceDiscovery is the same implementation but for devices
This is fairly straightforward. The ThingDiscoveryService adds a listener to the ThingRegistry and when a new thing is added, calls the ThingDiscoveryParticipant with the thing. The ThingDiscoveryParticipant will return null (or an empty collection) if no action should be taken on the thing or a collection of discovery results if one or more inbox items should be placed.
Please note I've already tested this on my development box and it works great.
Design Issues
Bridge vs Thing In my small world, I'd only need to be looking at bridges (not things). However, I err'd on the side of caution and assume others may need to discover things based on a thing (rather than a bridge). I'm not sure what everyone thinks - but if you feel strongly that only bridges should be evaluated - it would be a simple process to filter for bridges only (as opposed to things) and of course rename the implementation. We could actually support both I guess by having BridgeDiscoveryParticipant and a ThingDiscoveryParticipant depending on what someone wants - but that may be to complex and either one or the other we should go with. Thoughts?
Online vs Any You'll notice my implementation does not care if the thing is online or not. Both the NEEO and my sony binding don't have to be online to discover child things. But again, I don't know if that's my narrow view. If we think an online status is important - I can see the following options: a. Leave the status up to the implementation. In other words, don't address it and assume the implementor of ThingDiscoveryParticipant will handle whatever logic they need. b. Maybe add a 'isOnlineOnly' to the interface so the implementation writer simply needs to return a boolean to have their implementation called with only online things (and presumably when a thing changes to online). Would require alot more work but could be helpful if most implementations would need an online status c. Only call implementations when the thing goes online. Makes sense to only add stuff to the inbox if the thing is online. Same work as B but no added interface method and the assumption to only call when online.
Between the options - I'd only thing A or C would be the best. Either leave it up to the writer or assume most would only want online status and force it. Thoughts?
Lastly - not real happy with the names but those are easy to change! There's also some smaller implementation issues and documentation issues - but those would be addressed in the eventual PR
Thank you for thinking about a new concept to simplify the discovery process!
A couple of points that I noticed while thinking about it:
Thing
s cannot have other Thing
s as a child, that's the purpose of Bridge
s. A Bridge
is defined as a Thing
which acts a gateway to other Thing
s, i.e. it provides the physical access in terms of a USB stick or the credentials to a webservice. So we only need this for Bridge
s.discoverThings(participant, thing);
you should check if the participant
supports the type of the thing
(well bridge after you have changed is as mentioned above). So maybe you should rename public Set<ThingTypeUID> getSupportedThingTypeUIDs();
in the Participant
to public boolean supportsThingType(ThingType type);
so its easier to check. The HandlerFactory already do it this way too.ThingRegistry
like this: thingRegistry.getAll()
you could use thingRegistry.stream()
and filter for Bridge
s directly.ThingRegistry
is set, because you have defined the registry as a mandatory static reference. So after all static references are satisfied activate()
will be called and your service is activated. At this point the participants might appear.Exception
directly. catch the specific ones.Regarding the naming: How about ChildDiscoveryService
for the service running in the framework and ChildDiscoveryServiceParticipant
for the interface? Because what you discover are children of a Bridge
, well technically you discover Thing
s or ´Bridges, but only those which exist as a child of a
Bridge`.
I would be happy to see a PR here at the smarthome project which includes my points from above so we can discuss your idea further. its easier to discuss and comment in the code.
Will do those suggestions - the only comment I had was for 1. That's really the heart of my question - do you think there is a need to ONLY discover child things (in which case you look at bridges only) or would there be a need to discover neighbor things. In other words, do you think there would ever be a case where a thing could discover another thing under the same bridge (begs the question why evaluating the bridge didn't find it)?
If you don't think so (and I can't think of any examples), then bridge filtering is the way to go. I just don't want to artificially put a limitation because I can't foresee a situation where a thing could potentially discover another thing under the same bridge. The argument would be again, why didn't you find the additional thing when you discovered the bridge.
Do you think that would ever be a possibility?
BTW - as for 5 - that was a straight copy for the UpnpDiscoveryService. I think they are catching exception because they are trapping any participant from preventing any other participant from executing (isolating). There are no defined exceptions to catch. Let me know what you want to do (we either ignore catching exceptions, continue to catch the generic exception or create a defined DiscoveryException to catch [which won't address isolation but could be useful for writers]).
I think the case that a thing
discovers a "neighbour thing" on the same Bridge
is artificial and also speaks against the concept of Bridge
s and child Thing
s. So we only need the discovery service for Bridge
s.
Good catch that the UpnpDiscoveryService
is also doing it wrong :)
But let's focus on getting your discovery service right first. In general for framework code that calls arbitrary external code, such the one of a participant from a binding, should use our SafeCaller
infrastructure. This guards not only against Exception
s but also against jobs which run too long.
One other important thing which is missing in your approach is the "reverse direction". For now you have solved the issue that in the HandlerFactory you have to create a DiscoveryService
and pass over the BridgeHandler
. This is one direction.
What I mean by "reverse direction" is that during the normal communication of the BridgeHandler
with the real physical entity it might notice that a new device exists. For example in Philips Hue you can send a command to turn on a bulb to the Hue-gateway and it returns not only that the bulb is on, but also a list of devices which are connected to it. This "reverse direction" is a bit tricky though...
So what is missing in your approach is that the BridgeHandler
can notify your participant about a newly discovered device. For sure you could define an interface so the participant can register itself on the ThingHandler
, but how will that look like? What is the data structure that you pass tot he listerner? It is very binding specific on which kind of data the handler has and wants to pass to the participant. All I can think about it a HashMap<String, Object>
to pass any data. However, this is prune to (type) errors :( In the current implementation we do not have this general interface and thus bindings specify their own like https://github.com/eclipse/smarthome/blob/master/extensions/binding/org.eclipse.smarthome.binding.hue/src/main/java/org/eclipse/smarthome/binding/hue/handler/LightStatusListener.java
Also a registerListener()
and unregisterListener()
on the BridgeHandler
interface it not nice since not all bindings have a participant and would still have to implement those methods. A better way to solve this dilemma would be to have these methods encapsulated in a BridgeHandlerCallback
, similar to the ThingHandlerCallback
which we now pass tot he handlers. So a binding developer can simply call callback.deviceDiscovered(myMap)
.
One other point which I just stumbled across is the name Participant
which somehow collides with the UpnpDiscoveryParticipant
which has the method DiscoveryResult createResult(RemoteDevice device)
that just converts the data from RemoteDevice
into a DiscoveryResult
which is a matter of milliseconds, versus your proposed Participant
which might even trigger a long running scan. So the ChildDiscoveryServiceParticipant
has to be renamed somehow to maybe ChildDiscoveryServiceModule
? Suggestions for a better name are welcome :)
@tilmankamp Thanks - I'll take the comments and create a PR for them - we can discuss further at that point. Feel free to post any other thoughts between now and then and I'll see if we can include them in the initial PR
Thanks for creating the PR: However the "reverse direction" issue is still to discuss. I would like to hear the opinion of the committers @kaikreuzer @maggu2810 @sjka or @htreu about this:
So what is missing in your approach is that the BridgeHandler can notify your participant about a newly discovered device. For sure you could define an interface so the participant can register itself on the ThingHandler, but how will that look like? What is the data structure that you pass tot he listerner? It is very binding specific on which kind of data the handler has and wants to pass to the participant. All I can think about it a HashMap<String, Object> to pass any data. However, this is prune to (type) errors :(
Just a thought (and not sure if it's good). What if we change the signature (not suggesting these names - just for illustration) to
void bridgeDiscovered(bridge, discoveryCallback)
where discoveryCallback looks like
public interface DiscoveryCallback { void thingDiscovered(DiscoveryResult); }
That way if it's one way only - you just simply call the 'thingDiscovered' method for each result and in the case of your hue (which I haven't look at the code) - you'd do something like (psuedo code):
public void bridgeDiscovered(bridge, discoveryCallback) {
foreach(thing discovered) {
discoveryCallback.thingDiscovered(myResult)
if lightBridge (or whatever) then
lightBridge.addListener(new listener() {
void onLightAdded(whatever) {
discoveryCallback.thingDiscovered(myNewThing);
}
});
}
That way the thing or thinghandler has no knowledge of discovery, all the discovery items are actually in the discovery implementaion and your discovery implementation can add whatever listener is needed and have a way to callback to add discovery results?
Let me know if my poor attempt at psuedo code is unclear!
Tim
@tmrobert8 Thank you for this suggestion, but I am trying to follow your pseudo code but I guess there is something missing, which is why I do not understand it so far. Let me summarize what I understand:
We do have the following classes/interfaces involved:
ChildDiscoveryServiceModuleImpl implements ChildDiscoveryServiceModule:
public void bridgeDiscovered(bridge, discoveryCallback) {
foreach(CHILD discovered) {
discoveryCallback.thingDiscovered(myResult)
if lightBridge (or whatever) then
lightBridge.addListener(new listener() {
void onLightAdded(whatever) {
discoveryCallback.thingDiscovered(myNewThing);
}
});
}
ChildDiscoveryService
//if a thing was added to the thingRegistry
public void added(Thing thing) {
if(! (Thing instanceof Bridge) {return;}
for (ChildDiscoveryServiceModule module : modules) {
if(module.supportsBridgeThingType(thing.getUID()) {
module.bridgeDiscovered(thing, CALLBACK);
}
}
}
My question is: Where does the CALLBACK come from? Maybe I have also mixed up where you want to place which method... Maybe you can sketch which method should be placed i which interface/class.
We do have the interface ChildDiscoveryServiceModule
, to be implemented by the binding and the class ChildDiscoveryService
running as a service inside the framework.
Was pretty poor psuedo code I know. Here's actual code (non-tested and could be prettied up but will get the point):
Child stuff (still need to rename the directory from thing to child):
My very quick Hue implementation (no idea if it works, can absolutely be recoded in a better fashion and probably has some issues (like potentially adding the listener more than once) but hopefully get's the general approach right to talk about): https://github.com/tmrobert8/smarthome/blob/ThingDiscovery/extensions/binding/org.eclipse.smarthome.binding.hue/src/main/java/org/eclipse/smarthome/binding/hue/internal/discovery/HueLightChildDiscoveryImpl.java
Thank you for implementing a proof of concept.
Unfortunately I found a big issue in it: Thing
s which are added to the ThingRegistry
must not have a handler yet! I did not know that before trying your code either. Background of this is that the ThingRegistry
has the "metadata" about a Thing
and the ThingManager
creates a handler for them later.
So by the time you get notified that a Thing
was added to the ThingRegistry
, i.e. through a click on the approve button in the inbox of paperui, you do not have a handler on the Thing
yet.
So unfortunately there seems to be an architectural issue in this new discovery idea.
For your Neeo binding I would thus suggest to implement it like it is done in the hue binding, i.e. create a discovery service from the handlerfactory and pass the bridgehandler to it and register the discovery service as a listener on your handler in case it finds something. This way we will get it merged soon, because this discussion here might take longer :(
However, we can further pursue the idea of improving the state of the art discovery process because it is for sure not optimal.
Does it make sense to you?
Sorry, I didn't follow this discussion in detail yet, but it feels a bit as if you are creating an alternative discovery service framework, which has many overlaps. E.g. the ChildDiscoveryCallback is pretty similar to the methods that AbstractDiscoveryService provides.
Taking a step back, all that you are requesting is actually to be able to declare a certain bridge handler being a DiscoveryService (i.e. giving it the possibility to call methods like the ones in ChildDiscoveryCallback). If a bridge handler were an OSGi service, this could be done straight away, but unfortunately that isn't the case. The downside of making the bridge handler being a discovery service at the same time would be worse separation of concerns as one class would deal with both aspects. But if we want to avoid this, we do not have any other chance than going for additional listener interfaces and imho this is the part that brings the most (for the developer unexpected) complexity that you are trying to solve - your current approach cannot simplify this part at all. I am therefore not really clear on the right way for simplification; if we change something fundamentally here (through new framework services/infrastructure etc.), it should imho really have a clear benefit and not just make stuff slightly simpler/different...
@triller-telekom Didn't realize that myself and you are correct - would be a fatal flaw to this design (even though in practice a handler is assigned since the neeo one is working just fine - but I'm sure there is circumstances that will break that). BTW - going to switch the neeo stuff to the 'hue' way shortly to get it off our hands) but I agree we need to discuss this further because I feel like alot of the points @kaikreuzer makes applies to the current situation as well.
@kaikreuzer The implementation is built on the discovery service (just using the thing registry as a source). Was not an alternative or parallel design. However - as @triller-telekom pointed out - the approach can't be used because a handler isn't applied
Here's the two major issues I have with the current 'hue' way of doing things:
Clearly the thing/bridge level is not correct and but it seems the ThingHandler level is correct. What if we created a ThingHandlerRegistry (as an OSGI service) that will track the thinghandlers as they are created/disposed and provides a listener for changes. We can then implement something similar (which is an abstractservicediscovery implementation) to what we are talking about but using the ThingHandlerRegistry as a source? Just thinking out loud here...
I wouldn't call it out of scope. The HandlerFactory is currently THE place to deal with OSGi services - be it requiring some that need to be passed to handlers (like e.g. the shared httpClient) or be it registering services, because a binding wants to offer them (like a discovery service for a certain bridge where we know that we can do discovery).
Not really out of scope either. As you say, the thing handler contains the code for the technical communication with the device. So naturally, it is the thing handler, who has all the means of figuring out if there is anything new available and often this is pretty intertwined with the "normal" communication it is doing (like e.g. for hue or for bindings that receive push events from the device). The current setup tries to nonetheless keep the discovery code separated from the handler code by simply providing the handler to the discovery service, but not introducing any dependency from the handler to the discovery at all.
A ThingHandlerRegistry does not make much sense. Nobody should be interested in accessing handlers and in their lifecycle directly. We have the ThingManagerImpl that does the handling of all the handlers already. Imho the handler factory is just the right place as it is right now: You have the exact moment when the handler gets created and you even know that for this specific handler that is being created, you want to also register a discovery service (which is rather the exception than the norm). So the best approach might actually be to simplify the way this is done in the handler factory. E.g. by letting the bridge handler implement a specific marker interface and the BaseThingHandlerFactory then automatically deals with the creation of a suitable discovery service for it or something.... Also just thinking out loud here.
Marker interface is an interesting suggestion. Off the top of my head (and with TERRIBLE names)...
Let's say we had some marker interface:
public interface ThingHandlerDiscoverySource {}
That a thing handler can implement:
public class MyThingHandler extends BaseThingHandlerFactory implements ThingHandlerDiscoverySource { ... }
Now let's say we had a paired interface:
public interface ThingHandlerDiscoveryProvider<T extends ThingHandlerDiscoverySource> {
void discover(T handler, DiscoveryCallback callback)
}
the callback would simply be (like above - allowing you to discover stuff immediately or based on a listener for something on the handler):
public interface DiscoveryCallback {
void thingDiscovered(DiscoverResult result);
void thingRemoved(ThingUID thingUID);
}
The discovery implementation would be like
public class MyThingHandlerDiscovery implements ThingHandlerDiscoveryProvider<MyThingHandler> {
public void discover(MyThingHandler handler, DiscoveryCallback callback) {
do discovery stuff based on handler and use callback to give results
}
}
Now - to link them up. The BaseThingHandlerFactory would, after creating the thing handler...
if (handler instanceof ThingHandlerDiscoverySource) {
... do discovery stuff
}
(note: the remove handler would then do the removal stuff
do discovery stuff Yes - I wrote that vaguely because here is where we get into some decisions before we can talk implementation details
I'm really not sure if I explained myself well here - if you kinda agree with the approach but would like more details - maybe I can mock this up to look at for further discussion
or am I off on what you are suggesting?
I have the feeling that we have a simple solution consisting in enhancing the BaseThingHandlerFactory
with a new method protected @Nullable AbstractDiscoveryService createDiscoveryService(ThingHandler thingHandler)
. By default, the method will return null. The bindings wanting a discovery service attached to a thing will override this method in their handler factory and instantiate the discovery service. Then the updated BaseThingHandlerFactory will deal with service registration and service activation.
So this will mainly simplify the stuff with registration that all bindings have currently to implement in their handler factory.
I can propose the change with its use in the Hue binding for example.
My solution was really cool because it simplifies the binding code but after finishing to implement it, I discovered that it introduces a dependency cycle between o.e.s.config.discovery and o.e.s.core.thing because I need to use AbstractDiscoveryService
in BaseThingHandlerFactory
.
Is there an easy way to avoid this cycle ?
I guess we do not want a dependency from o.e.s.core.thing onto o.e.s.config.discovery. The BaseThingHandlerFactory
may provide a new method registerThingDiscovery
and a new interface ThingDiscovery
which is implemented by AbstractDiscoveryService
. In registerThingDiscovery
the service could be registered with bundleContext.registerService("org.eclipse.smarthome.config.discovery.DiscoveryService", ...
but that looks like a hack to me.
Problem Discovery works great right now for bridges but discovering things within the bridge is a bit complicated. The consensus design pattern seems to be something like (with alot of variations):
While it works - there's three downsides to this approach:
What I'd do Rather than complaining, thought I'd try to offer a starting point to the discussion. This is a starting point only - not a full implementation. Would rather hear if its a viable alternative before hashing out any details...
What I'd like to do is create a "BridgeDiscoveryParticipant" (or something better named!) that is similar to Mdns/UpnpDiscoveryParticipant but whose SOURCE is the ThingRegistry itself (rather than MDNS/UPNP).
Basically it would something like:
The BridgeDiscoveryService implementation would:
The discovery service of course would have to deal with all the other management type of issues - I just wanted to talk about the overall approach at this point.
Benefits
What do you all think?
Tim