Skip to content
This repository has been archived by the owner on May 7, 2020. It is now read-only.

Improve Tradfri binding #4299

Merged
merged 1 commit into from Sep 25, 2017
Merged

Improve Tradfri binding #4299

merged 1 commit into from Sep 25, 2017

Conversation

ivivanov-bg
Copy link
Contributor

Send command to the bulb only if there is actually a change on the value for brightness or color temperature

Signed-off-by: Ivaylo Ivanov ivivanov.bg@gmail.com

@@ -226,7 +236,12 @@ private void handleBrightnessCommand(Command command) {

private void handleColorTemperatureCommand(Command command) {
if (command instanceof PercentType) {
setColorTemperature((PercentType) command);
PercentType newValue = (PercentType) command;
if (!state.getColorTemperature().equals(newValue)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we had sent some other Color command meanwhile, is it guaranteed that the state.getColorTemperature() is changed? What value does it e.g. take if I send a "green" to the bulb?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The state is re-created on each message received from the bridge so - it should be up-to-date.

For changing color - I think this is not supported (yet) by the binding - only color temperature can be set

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ivivanov-bg Color is supported now: #4271

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, Point taken, will check how it behaves tomorrow - but are you sure that the RGB light supports setting color temperature

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes

@kaikreuzer
Copy link
Contributor

Did you come across any real issue in this context? So far, we hardly do such checks in any of our bindings - it should always be valid to send out a command, even if we believe that it might not have any effect (but in the end it should be up to the device to decide on that).

@ivivanov-bg
Copy link
Contributor Author

ivivanov-bg commented Sep 19, 2017

Yes - I did - if you send 2 commands (one for brightness and one for color temperature) in a short time (less than a second) - the second command is ignored by the bulb no matter if the first command actually change anything.

So - this change will prevent flooding the bulb with unnecessary commands and will improve it's responsiveness

@sjsf
Copy link
Contributor

sjsf commented Sep 19, 2017

Don't you assume with this change that the binding is the only one in the universe interacting with the bulb? How is this protected against race conditions with e.g. the native app?

@ivivanov-bg
Copy link
Contributor Author

Messages are send by the bridge asynchronously no matter where does the change come from - if you change any value using the remote control, the IKEA app or the binding - the COAP message is always transmitted and the binding always receives it (and updates it's sate and items)

@sjsf
Copy link
Contributor

sjsf commented Sep 19, 2017

But there is a time delay. So with your change the binding might swallow incorrectly a command.

I don't think this is the right way to compensate for a limitation on hardware side...

@ivivanov-bg
Copy link
Contributor Author

The binding is the 'driver' to the hardware - so if there are any hardware limitations - the binding is the place where this should be handled.

eg. - if it's known that the device can not handle more than 1 command per second - the binding should not allow any user (application) to send more than 1 command each second. It should either ignore the request, or cache them in a queue, or whatever else but for sure the commands should not arrive to the device.

(at least this is my point of view for the bindings)

@wborn
Copy link
Contributor

wborn commented Sep 19, 2017

The LIFX Binding uses a throttling mechanism with locking/unlocking to make sure at most one message is sent to a light within a certain interval. This results in the command being queued instead of being ignored.

@hreichert
Copy link
Contributor

Something like a queue sounds better.
A queue would maybe also solve the following problem:

  • Group (in ESH, not a Tradfri-Group) with 10 color_temperature channels from the Tradfri binding
  • Send command PercentType=50 to the group
  • Binding sends out 10 commands to the gateway
  • 1 to 10 lights change their color temperature, but mostly not all, or even better: gateway hangs..

@ivivanov-bg
Copy link
Contributor Author

In this case I think it would be even better to use the protocol in synchronious way when sending commands.

@hreichert
Copy link
Contributor

Related problem:
https://community.openhab.org/t/ikea-tradfri-gateway/26135/82
This also happens to me, workaround was to use a Thread:sleep in my rules.

@ivivanov-bg
Copy link
Contributor Author

Related problem:
https://community.openhab.org/t/ikea-tradfri-gateway/26135/82
This also happens to me, workaround was to use a Thread:sleep in my rules.

Exactly this I think should be handled in the binding and not in each application which uses it.

Updated the PR to use a queue and send commands with a 200 milliseconds delay after each other.
(the delay was chosen by some tests I run manually - but it might also be increased)

@hreichert
Copy link
Contributor

Just for discussion:
Would it be a good idea to make the delay more dynamic? Lets say, by adding a nullable attribute to PayloadCallbackPair?
If sending updates to brightness or color_temperature channels and to multiple bulbs, 200ms would be enough. But in setColor, were I previously delayed the color-sending and brightness-sending by 1500ms, the 200ms would be too less and the brightess-setting would interrupt the color-fading.
Or maybe a per-device (read: per-light) throttling would be even better, like @wborn mentioned.

@ivivanov-bg
Copy link
Contributor Author

Lets say, by adding a nullable attribute to PayloadCallbackPair?

I think this is a good idea - this way it will be per command.

Currently each light handler has it's own coap handler - so it is already per light, but having it per command will be more flexible

BTW - I tested also with the RGB bulb and didn't notice any issues with the 200ms delay. Both color and brightness were set properly

@hreichert
Copy link
Contributor

I see, that's cool. And yes, today color+brightness setting works for me even with 200ms, but with freshly rebooted gateway. Nevertheless a dynamic delay would be great for further improvements, lets say, a configurable DIMMER_TRANSITION_TIME, which then would need longer calculated delays.

@hreichert
Copy link
Contributor

If I see this correctly, would that start a thread with a while (true) loop for every device?

@ivivanov-bg
Copy link
Contributor Author

Yes - that's right. But the blocking queue will ensure that the thread is not running if there is no data to be processed

@ivivanov-bg
Copy link
Contributor Author

It might be possible to have only one loop (in the bridge handler), but this will require quite big refactoring as each bulb should not directly send coap messages, but ask the bridge to do it instead

@hreichert
Copy link
Contributor

Indeed. Sorry, should have read the code more carefully.
In conclusion, I think that this PR is now a great improvement to the stability and usability of the Tradfri binding, cause this fixes many problems that otherwise needed workarounds.

logger = LoggerFactory.getLogger(getClass());

commandsQueue = new LinkedBlockingQueue<>();
commandExecutorThread = new Thread(() -> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you create a new Thread on your own? According to our guidelines this is not allowed. Please use the scheduler and its submit method to have it run immediately.

Also if this is now the second binding after the Lifx one that implements such a throttle mechanism, whether it might be better to offer a general throttling implementation that can be used by both bindings?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you create a new Thread on your own?

Using scheduler is suitable for one time jobs or short actions (in a way fire and forget). The idea of this thread is to be a long running operation (start it in the initialization of the handler and stops it on dispose).

The scheduler will also introduce some limitations.

  1. you can't check the state (if it's running or not)
  2. You can't restart it (unless you submit a new job - which will have new instance of the command queue)
  3. You can't wait for it to finish

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping a thread occupied with nothing for most of the time is not a good idea though. Especially not on constrained hardware.

How about something like this:

private final Queue<Command> queue = //...
private Future<?> job;

void sendCommand(Command cmd) {
    queue.add(cmd);
    if (job == null || job.isDone()) {
        job = scheduler.submit(() -> {
            while (!queue.isEmpty()) {
                Command command = queue.poll();
                
                // do send the command...
                
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
    }
}

void destroy() {
    if (job != null) {
        job.cancel(true);
    }
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we use a blocking queue instead of that thread.sleep usage?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about something like this

I guess it is also possible but the if statement: job == null || job.isDone() is not atomic which means that in some rare cases we might skip running the job and the command will not be sent.

Shouldn't we use a blocking queue instead of that thread.sleep usage?

The thread.sleep has nothing to do with the blocking, it's used to allow the device to process the command before sending a new one.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we use a blocking queue

If you can make the queue block for at least 200ms...? I don't think this works though. But maybe I didn't fully get your idea...

but the if statement: job == null || job.isDone() is not atomic

Of course not, you are right! The above code was meant to illustrate the mechnism ("something like this"), not the final implementation. Otherwise I would have created a PR myself, right? Please add proper synchronization, logging and whatever else you see is necessary in order to make this robust and stable.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@wborn since you mentioned that the Lifx uses a throttling mechanism and @kaikreuzer
mentioned that @kgoderis had to use such a mechanism for the Tesla binding because the web API locks you out if you fire too many requests at once, do you guys think that we should come up with a general solution and provide it in the framework? Do you think it would be possible to generalize all use cases that we currently have?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I've also been thinking about having a general solution in the framework.

The LIFX lights can be addressed via unicast and multicast (UDP broadcast). The LIFX throttling implementation makes sure that a lock is obtained on all lights or single lights. What I don't like about the implementation is that it uses Thread.sleep. But since the packet interval is just 50ms it this is not really an issue.

The Nest API also has rate limits which deny access to the API when you overload it with too many requests.

Perhaps the framework can provide some kind of mechanism where you can create groups (and possibly sub-groups) with objects (devices). These groups then have properties like time based limits and allow for operations (jobs) that take into account the defined group limits.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The details of such a generic throttling mechanism can be a little tricky...

There are easy ones, e.g. the minimal delay which could be made configurable.

Then there is to distinguish whether the throttling should happen per bridge or per thing - still doable if it is completely agnostic to what it actually does, this would have to be handed over by Runnables.

Then you will have to distinguish whether whatever-the-throttles-is-instructed-to-do should really happen delayed (if a delay is required at all) or rather some of the executions should be skipped. While delaying might make sense in some cases (e.g. if I switch on a group of lights, I want every light to be on in the end, even if that's not possible at the same time) while sometimes it is rather not wanted (e.g. if I toggle a lamp a couple of times quickly, I might not want it to blink for the next minute but rather as soon as possible reach the final state). Unless I want to morse to my neighbors, then of course the blinking is intended 😉

Just skipping the congested executions also might not be a suitable solution, because maybe only some command equal out each other. Others might be important.

This all might still be doable with configurable implementations of different optimization strategies and callbacks for doing the "equalling out" calculations.

But all that seems a little overdone compared to the few lines of code that are similar in LIFX and Trådfri. I would recommend waiting a little, collecting other use-cases before suggesting to implement a generic solution. Especially since I really consider this a bug in the Trådfri gateway that ideally should be fixed there - don't get me wrong, I'm kind of okay with having the workaround here, but that's what it is. I would not want to establish this as "best practice" or anything.

@ivivanov-bg
Copy link
Contributor Author

ivivanov-bg commented Sep 22, 2017

Especially since I really consider this a bug in the Trådfri gateway that ideally should be fixed there - don't get me wrong, I'm kind of okay with having the workaround here, but that's what it is. I >would not want to establish this as "best practice" or anything.

I totally agree that this should be a temporary workaround until the FW of the bridge is fixed.
That's why I wanted to do it as simple as possible and with as small overhead as possible - so I used a plain thread.

Usually I also prefer the pool executors but in this case I think it will only introduce overhead as the thread will be occupate

Please add proper synchronization, logging and ...

I can't think of a way to do what you suggest (to get new thread of the pool when one command is send and when the second command arrives to reuse the same thread if it's not yet finished). That's because the state of the thread is not under control. Further more - there is chance that two (or more) threads are retrieved from the pool at the same time - then there is no way to ensure the required delay between two commands

Signed-off-by: Ivaylo Ivanov <ivivanov.bg@gmail.com>
@ivivanov-bg
Copy link
Contributor Author

I think I figured it out how to synchronize the threads. Now the scheduler is used in kind of fire and forget way and if there is no command - there will be no blocking thread waiting for commands.

(hopefully @SJKA will be happy with this implementation :))

Copy link
Contributor

@triller-telekom triller-telekom left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks much better now, Thanks.

@kaikreuzer kaikreuzer merged commit c6b4126 into eclipse-archived:master Sep 25, 2017
@kaikreuzer kaikreuzer added this to the 0.9.0 milestone Nov 30, 2017
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants