Skip to content

Commit

Permalink
Add setting to debounce MQTT state updates
Browse files Browse the repository at this point in the history
Adds a setting that delays state updates from being sent until an update hasn't occurred for a configurable threshold

Implements #615
  • Loading branch information
annego15 authored Apr 18, 2020
1 parent 9f18389 commit 27a1a6a
Show file tree
Hide file tree
Showing 10 changed files with 78 additions and 9 deletions.
4 changes: 2 additions & 2 deletions dist/index.html.gz.h

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions docs/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -889,6 +889,10 @@ components:
type: integer
description: Controls how many miliseconds must pass between MQTT state updates. Set to 0 to disable throttling.
default: 500
mqtt_debounce_delay:
type: integer
description: Controls how much time has to pass after the last status update was queued.
default: 500
packet_repeat_throttle_threshold:
type: integer
description:
Expand Down
13 changes: 6 additions & 7 deletions lib/MQTT/BulbStateUpdater.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ BulbStateUpdater::BulbStateUpdater(Settings& settings, MqttClient& mqttClient, G
mqttClient(mqttClient),
stateStore(stateStore),
lastFlush(0),
lastQueue(0),
enabled(true)
{ }

Expand All @@ -17,12 +18,10 @@ void BulbStateUpdater::disable() {
}

void BulbStateUpdater::enqueueUpdate(BulbId bulbId, GroupState& groupState) {
// If can flush immediately, do so (avoids lookup of group state later).
if (canFlush()) {
flushGroup(bulbId, groupState);
} else {
staleGroups.push(bulbId);
}
staleGroups.push(bulbId);
//Remember time, when queue was added for debounce delay
lastQueue = millis();

}

void BulbStateUpdater::loop() {
Expand Down Expand Up @@ -56,5 +55,5 @@ inline void BulbStateUpdater::flushGroup(BulbId bulbId, GroupState& state) {
}

inline bool BulbStateUpdater::canFlush() const {
return enabled && (millis() > (lastFlush + settings.mqttStateRateLimit));
return enabled && (millis() > (lastFlush + settings.mqttStateRateLimit) && millis() > (lastQueue + settings.mqttDebounceDelay));
}
1 change: 1 addition & 0 deletions lib/MQTT/BulbStateUpdater.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class BulbStateUpdater {
GroupStateStore& stateStore;
CircularBuffer<BulbId, MILIGHT_MAX_STALE_MQTT_GROUPS> staleGroups;
unsigned long lastFlush;
unsigned long lastQueue;
bool enabled;

inline void flushGroup(BulbId bulbId, GroupState& state);
Expand Down
2 changes: 2 additions & 0 deletions lib/Settings/Settings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ void Settings::patch(JsonObject parsedSettings) {
this->setIfPresent(parsedSettings, "listen_repeats", listenRepeats);
this->setIfPresent(parsedSettings, "state_flush_interval", stateFlushInterval);
this->setIfPresent(parsedSettings, "mqtt_state_rate_limit", mqttStateRateLimit);
this->setIfPresent(parsedSettings, "mqtt_debounce_delay", mqttDebounceDelay);
this->setIfPresent(parsedSettings, "packet_repeat_throttle_threshold", packetRepeatThrottleThreshold);
this->setIfPresent(parsedSettings, "packet_repeat_throttle_sensitivity", packetRepeatThrottleSensitivity);
this->setIfPresent(parsedSettings, "packet_repeat_minimum", packetRepeatMinimum);
Expand Down Expand Up @@ -271,6 +272,7 @@ void Settings::serialize(Print& stream, const bool prettyPrint) {
root["listen_repeats"] = this->listenRepeats;
root["state_flush_interval"] = this->stateFlushInterval;
root["mqtt_state_rate_limit"] = this->mqttStateRateLimit;
root["mqtt_debounce_delay"] = this->mqttDebounceDelay;
root["packet_repeat_throttle_sensitivity"] = this->packetRepeatThrottleSensitivity;
root["packet_repeat_throttle_threshold"] = this->packetRepeatThrottleThreshold;
root["packet_repeat_minimum"] = this->packetRepeatMinimum;
Expand Down
2 changes: 2 additions & 0 deletions lib/Settings/Settings.h
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ class Settings {
simpleMqttClientStatus(false),
stateFlushInterval(10000),
mqttStateRateLimit(500),
mqttDebounceDelay(500),
packetRepeatThrottleThreshold(200),
packetRepeatThrottleSensitivity(0),
packetRepeatMinimum(3),
Expand Down Expand Up @@ -168,6 +169,7 @@ class Settings {
bool simpleMqttClientStatus;
size_t stateFlushInterval;
size_t mqttStateRateLimit;
size_t mqttDebounceDelay;
size_t packetRepeatThrottleThreshold;
size_t packetRepeatThrottleSensitivity;
size_t packetRepeatMinimum;
Expand Down
1 change: 1 addition & 0 deletions test/remote/settings.json.example
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"listen_repeats": 3,
"state_flush_interval": 2000,
"mqtt_state_rate_limit": 1000,
"mqtt_debounce_delay": 0,
"packet_repeat_throttle_sensitivity": 0,
"packet_repeat_throttle_threshold": 200,
"packet_repeat_minimum": 3,
Expand Down
52 changes: 52 additions & 0 deletions test/remote/spec/mqtt_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,58 @@
expect(update_timestamp_gaps.length).to be >= 3
expect((avg - 0.5).abs).to be < 0.15, "Should be within margin of error of rate limit"
end

it 'should respect the update debouce interval' do
@client.put(
'/settings',
mqtt_debounce_delay: 1000,
packet_repeats: 1
)

start_time = Time.now

@mqtt_client.on_state(@id_params) do |id, message|
true
end

# Set initial state
@client.patch_state({status: 'ON', level: 0}, @id_params)
@mqtt_client.wait_for_listeners

expect(Time.now - start_time).to be >= 1
end

it 'should only send one state update for many commands if debounce interval is enabled' do
@client.put(
'/settings',
mqtt_update_topic_pattern: '',
mqtt_debounce_delay: 1000,
packet_repeats: 1
)

# Set initial state
@client.patch_state({status: 'ON', level: 0}, @id_params)

num_updates = 10
seen_updates = 0
last_level_value = 0

@mqtt_client.on_state(@id_params) do |id, message|
seen_updates += 1
last_level_value = message['level']
last_level_value == num_updates
end

(1..num_updates).each do |i|
@mqtt_client.patch_state(@id_params, level: i)
sleep 0.5
end

@mqtt_client.wait_for_listeners

expect(seen_updates).to eq(1)
expect(last_level_value).to eq(num_updates)
end
end

context ':device_id token for command topic' do
Expand Down
2 changes: 2 additions & 0 deletions test/remote/spec/udp_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@

context 'on/off commands' do
it 'should result in state changes' do
@client.delete_state(@id_params)
@udp_client.group(@id_params[:group_id]).on

# Wait for packet to be processed
Expand All @@ -66,6 +67,7 @@
end

it 'should result in an MQTT update' do
@client.delete_state(@id_params)
desired_state = {
'status' => 'ON',
'level' => 48
Expand Down
6 changes: 6 additions & 0 deletions web/src/js/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,12 @@ var UI_FIELDS = [ {
help: "Minimum number of milliseconds between MQTT updates of bulb state (defaults to 500)",
type: "string",
tab: "tab-mqtt"
}, {
tag: "mqtt_debounce_delay",
friendly: "MQTT debounce delay",
help: "Minimum number of milliseconds delay for MQTT state updates after change (defaults to 500)",
type: "string",
tab: "tab-mqtt"
}, {
tag: "packet_repeat_throttle_threshold",
friendly: "Packet repeat throttle threshold",
Expand Down

0 comments on commit 27a1a6a

Please sign in to comment.