remote: close file on usteer_init_local_id fread fail
[project/usteer.git] / policy.c
index c528bfa650715a77bce04f508af61de75ea6c43a..8c5d244b35d8735f0918dfd8514d0abea9c5cd11 100644 (file)
--- a/policy.c
+++ b/policy.c
 
 #include "usteer.h"
 #include "node.h"
+#include "event.h"
 
 static bool
-below_assoc_threshold(struct sta_info *si_cur, struct sta_info *si_new)
+below_assoc_threshold(struct usteer_node *node_cur, struct usteer_node *node_new)
 {
-       int n_assoc_cur = si_cur->node->n_assoc;
-       int n_assoc_new = si_new->node->n_assoc;
-       bool ref_5g = si_cur->node->freq > 4000;
-       bool node_5g = si_new->node->freq > 4000;
+       int n_assoc_cur = node_cur->n_assoc;
+       int n_assoc_new = node_new->n_assoc;
+       bool ref_5g = node_cur->freq > 4000;
+       bool node_5g = node_new->freq > 4000;
+
+       if (!config.load_balancing_threshold)
+               return false;
 
        if (ref_5g && !node_5g)
                n_assoc_new += config.band_steering_threshold;
@@ -35,93 +39,122 @@ below_assoc_threshold(struct sta_info *si_cur, struct sta_info *si_new)
 
        n_assoc_new += config.load_balancing_threshold;
 
-       if (n_assoc_new > n_assoc_cur) {
-               MSG_T_STA("band_steering_threshold,load_balancing_threshold",
-                       si_cur->sta->addr, "exeeded (bs=%u, lb=%u)\n",
-                       config.band_steering_threshold,
-                       config.load_balancing_threshold);
-       }
        return n_assoc_new <= n_assoc_cur;
 }
 
 static bool
-better_signal_strength(struct sta_info *si_cur, struct sta_info *si_new)
+better_signal_strength(int signal_cur, int signal_new)
 {
-       const bool is_better = si_new->signal - si_cur->signal
+       const bool is_better = signal_new - signal_cur
                                > (int) config.signal_diff_threshold;
 
        if (!config.signal_diff_threshold)
                return false;
 
-       if (is_better) {
-               MSG_T_STA("signal_diff_threshold", si_cur->sta->addr,
-                       "exceeded (config=%i) (real=%i)\n",
-                       config.signal_diff_threshold,
-                       si_new->signal - si_cur->signal);
-       }
        return is_better;
 }
 
 static bool
-below_load_threshold(struct sta_info *si)
+below_load_threshold(struct usteer_node *node)
 {
-       return si->node->n_assoc >= config.load_kick_min_clients &&
-              si->node->load > config.load_kick_threshold;
+       return node->n_assoc >= config.load_kick_min_clients &&
+              node->load > config.load_kick_threshold;
 }
 
 static bool
-has_better_load(struct sta_info *si_cur, struct sta_info *si_new)
+has_better_load(struct usteer_node *node_cur, struct usteer_node *node_new)
 {
-       return !below_load_threshold(si_cur) && below_load_threshold(si_new);
+       return !below_load_threshold(node_cur) && below_load_threshold(node_new);
 }
 
-static bool
-below_max_assoc(struct sta_info *si)
+bool
+usteer_policy_node_below_max_assoc(struct usteer_node *node)
 {
-       struct usteer_node *node = si->node;
-
        return !node->max_assoc || node->n_assoc < node->max_assoc;
 }
 
 static bool
-is_better_candidate(struct sta_info *si_cur, struct sta_info *si_new)
+over_min_signal(struct usteer_node *node, int signal)
 {
-       if (!below_max_assoc(si_new))
+       if (config.min_snr && signal < usteer_snr_to_signal(node, config.min_snr))
+               return false;
+
+       if (config.roam_trigger_snr && signal < usteer_snr_to_signal(node, config.roam_trigger_snr))
                return false;
+       
+       return true;
+}
+
+static uint32_t
+is_better_candidate(struct sta_info *si_cur, struct sta_info *si_new)
+{
+       struct usteer_node *current_node = si_cur->node;
+       struct usteer_node *new_node = si_new->node;
+       int current_signal = si_cur->signal;
+       int new_signal = si_new->signal;
+       uint32_t reasons = 0;
+
+       if (!usteer_policy_node_below_max_assoc(new_node))
+               return 0;
 
-       return below_assoc_threshold(si_cur, si_new) ||
-              better_signal_strength(si_cur, si_new) ||
-              has_better_load(si_cur, si_new);
+       if (!over_min_signal(new_node, new_signal))
+               return 0;
+
+       if (below_assoc_threshold(current_node, new_node) &&
+           !below_assoc_threshold(new_node, current_node))
+               reasons |= (1 << UEV_SELECT_REASON_NUM_ASSOC);
+
+       if (better_signal_strength(current_signal, new_signal))
+               reasons |= (1 << UEV_SELECT_REASON_SIGNAL);
+
+       if (has_better_load(current_node, new_node) &&
+               !has_better_load(current_node, new_node))
+               reasons |= (1 << UEV_SELECT_REASON_LOAD);
+
+       return reasons;
 }
 
 static struct sta_info *
-find_better_candidate(struct sta_info *si_ref)
+find_better_candidate(struct sta_info *si_ref, struct uevent *ev, uint32_t required_criteria, uint64_t max_age)
 {
-       struct sta_info *si;
+       struct sta_info *si, *candidate = NULL;
        struct sta *sta = si_ref->sta;
+       uint32_t reasons;
 
        list_for_each_entry(si, &sta->nodes, list) {
                if (si == si_ref)
                        continue;
 
-               if (current_time - si->seen > config.seen_policy_timeout) {
-                       MSG_T_STA("seen_policy_timeout", si->sta->addr,
-                               "timeout exceeded (%u)\n", config.seen_policy_timeout);
+               if (current_time - si->seen > config.seen_policy_timeout)
                        continue;
-               }
 
                if (strcmp(si->node->ssid, si_ref->node->ssid) != 0)
                        continue;
 
-               if (is_better_candidate(si_ref, si) &&
-                   !is_better_candidate(si, si_ref))
-                       return si;
+               if (max_age && max_age < current_time - si->seen)
+                       continue;
+
+               reasons = is_better_candidate(si_ref, si);
+               if (!reasons)
+                       continue;
+
+               if (!(reasons & required_criteria))
+                       continue;
+
+               if (ev) {
+                       ev->si_other = si;
+                       ev->select_reasons = reasons;
+               }
+
+               if (!candidate || si->signal > candidate->signal)
+                       candidate = si;
        }
-       return NULL;
+
+       return candidate;
 }
 
-static int
-snr_to_signal(struct usteer_node *node, int snr)
+int
+usteer_snr_to_signal(struct usteer_node *node, int snr)
 {
        int noise = -95;
 
@@ -137,53 +170,82 @@ snr_to_signal(struct usteer_node *node, int snr)
 bool
 usteer_check_request(struct sta_info *si, enum usteer_event_type type)
 {
-       struct sta_info *si_new;
+       struct uevent ev = {
+               .si_cur = si,
+       };
        int min_signal;
+       bool ret = true;
+
+       if (type == EVENT_TYPE_PROBE && !config.probe_steering)
+               goto out;
 
        if (type == EVENT_TYPE_AUTH)
-               return true;
+               goto out;
 
-       if (si->stats[type].blocked_cur >= config.max_retry_band) {
-               MSG_T_STA("max_retry_band", si->sta->addr,
-                       "max retry (%u) exceeded\n", config.max_retry_band);
-               return true;
+       if (type == EVENT_TYPE_ASSOC) {
+               /* Check if assoc request has lower signal than min_signal.
+                * If this is the case, block assoc even when assoc steering is enabled.
+                *
+                * Otherwise, the client potentially ends up in a assoc - kick loop.
+                */
+               if (config.min_snr && si->signal < usteer_snr_to_signal(si->node, config.min_snr)) {
+                       ev.reason = UEV_REASON_LOW_SIGNAL;
+                       ev.threshold.cur = si->signal;
+                       ev.threshold.ref = usteer_snr_to_signal(si->node, config.min_snr);
+                       ret = false;
+                       goto out;
+               } else if (!config.assoc_steering) {
+                       goto out;
+               }
        }
 
-       min_signal = snr_to_signal(si->node, config.min_connect_snr);
+       min_signal = usteer_snr_to_signal(si->node, config.min_connect_snr);
        if (si->signal < min_signal) {
-               if (type != EVENT_TYPE_PROBE || config.debug_level >= MSG_DEBUG)
-                       MSG(VERBOSE, "Ignoring %s request from "MAC_ADDR_FMT" due to low signal (%d < %d)\n",
-                           event_types[type], MAC_ADDR_DATA(si->sta->addr),
-                           si->signal, min_signal);
-               MSG_T_STA("min_connect_snr", si->sta->addr,
-                       "snr to low (config=%i) (real=%i)\n",
-                       min_signal, si->signal);
-               return false;
+               ev.reason = UEV_REASON_LOW_SIGNAL;
+               ev.threshold.cur = si->signal;
+               ev.threshold.ref = min_signal;
+               ret = false;
+               goto out;
        }
 
        if (current_time - si->created < config.initial_connect_delay) {
-               if (type != EVENT_TYPE_PROBE || config.debug_level >= MSG_DEBUG)
-                       MSG(VERBOSE, "Ignoring %s request from "MAC_ADDR_FMT" during initial connect delay\n",
-                           event_types[type], MAC_ADDR_DATA(si->sta->addr));
-               MSG_T_STA("initial_connect_delay", si->sta->addr,
-                       "is below delay (%u)\n", config.initial_connect_delay);
-               return false;
+               ev.reason = UEV_REASON_CONNECT_DELAY;
+               ev.threshold.cur = current_time - si->created;
+               ev.threshold.ref = config.initial_connect_delay;
+               ret = false;
+               goto out;
        }
 
-       si_new = find_better_candidate(si);
-       if (!si_new)
-               return true;
+       if (!find_better_candidate(si, &ev, UEV_SELECT_REASON_ALL, 0))
+               goto out;
 
-       if (type != EVENT_TYPE_PROBE || config.debug_level >= MSG_DEBUG)
-               MSG(VERBOSE, "Ignoring %s request from "MAC_ADDR_FMT", "
-                       "node (local/remote): %s/%s, "
-                       "signal=%d/%d, n_assoc=%d/%d\n", event_types[type],
-                       MAC_ADDR_DATA(si->sta->addr),
-                       usteer_node_name(si->node), usteer_node_name(si_new->node),
-                       si->signal, si_new->signal,
-                       si->node->n_assoc, si_new->node->n_assoc);
+       ev.reason = UEV_REASON_BETTER_CANDIDATE;
+       ev.node_cur = si->node;
+       ret = false;
 
-       return false;
+out:
+       switch (type) {
+       case EVENT_TYPE_PROBE:
+               ev.type = ret ? UEV_PROBE_REQ_ACCEPT : UEV_PROBE_REQ_DENY;
+               break;
+       case EVENT_TYPE_ASSOC:
+               ev.type = ret ? UEV_ASSOC_REQ_ACCEPT : UEV_ASSOC_REQ_DENY;
+               break;
+       case EVENT_TYPE_AUTH:
+               ev.type = ret ? UEV_AUTH_REQ_ACCEPT : UEV_AUTH_REQ_DENY;
+               break;
+       default:
+               break;
+       }
+
+       if (!ret && si->stats[type].blocked_cur >= config.max_retry_band) {
+               ev.reason = UEV_REASON_RETRY_EXCEEDED;
+               ev.threshold.cur = si->stats[type].blocked_cur;
+               ev.threshold.ref = config.max_retry_band;
+       }
+       usteer_event(&ev);
+
+       return ret;
 }
 
 static bool
@@ -199,109 +261,164 @@ is_more_kickable(struct sta_info *si_cur, struct sta_info *si_new)
 }
 
 static void
-usteer_roam_set_state(struct sta_info *si, enum roam_trigger_state state)
+usteer_roam_set_state(struct sta_info *si, enum roam_trigger_state state,
+                     struct uevent *ev)
 {
-       static const char * const state_names[] = {
-#define _S(n) [ROAM_TRIGGER_##n] = #n,
-               __roam_trigger_states
-#undef _S
-       };
+       /* NOP in case we remain idle */
+       if (si->roam_state == state && si->roam_state == ROAM_TRIGGER_IDLE) {
+               si->roam_tries = 0;
+               return;
+       }
 
        si->roam_event = current_time;
 
        if (si->roam_state == state) {
-               if (si->roam_state == ROAM_TRIGGER_IDLE) {
-                       si->roam_tries = 0;
-                       return;
-               }
-
                si->roam_tries++;
        } else {
                si->roam_tries = 0;
        }
 
        si->roam_state = state;
+       usteer_event(ev);
+}
+
+static void
+usteer_roam_sm_start_scan(struct sta_info *si, struct uevent *ev)
+{
+       /* Start scanning in case we are not timeout-constrained or timeout has expired */
+       if (!config.roam_scan_timeout ||
+           current_time > si->roam_scan_timeout_start + config.roam_scan_timeout) {
+               usteer_roam_set_state(si, ROAM_TRIGGER_SCAN, ev);
+               return;
+       }
+
+       /* We are currently in scan timeout / cooldown.
+        * Check if we are in ROAM_TRIGGER_IDLE state. Enter this state if not.
+        */
+       if (si->roam_state == ROAM_TRIGGER_IDLE)
+               return;
 
-       MSG(VERBOSE, "Roam trigger SM for client "MAC_ADDR_FMT": state=%s, tries=%d, signal=%d\n",
-           MAC_ADDR_DATA(si->sta->addr), state_names[state], si->roam_tries, si->signal);
+       /* Enter idle state */
+       usteer_roam_set_state(si, ROAM_TRIGGER_IDLE, ev);
 }
 
-static bool
-usteer_roam_trigger_sm(struct sta_info *si)
+static struct sta_info *
+usteer_roam_sm_found_better_node(struct sta_info *si, struct uevent *ev, enum roam_trigger_state next_state)
 {
-       struct sta_info *si_new;
-       int min_signal;
+       uint64_t max_age = 2 * config.roam_scan_interval;
+       struct sta_info *candidate;
 
-       min_signal = snr_to_signal(si->node, config.roam_trigger_snr);
+       if (max_age > current_time - si->roam_scan_start)
+               max_age = current_time - si->roam_scan_start;
+
+       candidate = find_better_candidate(si, ev, (1 << UEV_SELECT_REASON_SIGNAL), max_age);
+       if (candidate)
+               usteer_roam_set_state(si, next_state, ev);
+
+       return candidate;
+}
+
+static bool
+usteer_roam_trigger_sm(struct usteer_local_node *ln, struct sta_info *si)
+{
+       struct sta_info *candidate;
+       struct uevent ev = {
+               .si_cur = si,
+       };
 
        switch (si->roam_state) {
        case ROAM_TRIGGER_SCAN:
-               if (current_time - si->roam_event < config.roam_scan_interval)
+               if (!si->roam_tries) {
+                       si->roam_scan_start = current_time;
+               }
+
+               /* Check if we've found a better node regardless of the scan-interval */
+               if (usteer_roam_sm_found_better_node(si, &ev, ROAM_TRIGGER_SCAN_DONE))
                        break;
 
-               if (find_better_candidate(si) ||
-                   si->roam_scan_done > si->roam_event) {
-                       usteer_roam_set_state(si, ROAM_TRIGGER_SCAN_DONE);
+               /* Only scan every scan-interval */
+               if (current_time - si->roam_event < config.roam_scan_interval)
                        break;
-               }
 
-               if (config.roam_scan_tries &&
-                   si->roam_tries >= config.roam_scan_tries) {
-                       usteer_roam_set_state(si, ROAM_TRIGGER_WAIT_KICK);
+               /* Check if no node was found within roam_scan_tries tries */
+               if (config.roam_scan_tries && si->roam_tries >= config.roam_scan_tries) {
+                       if (!config.roam_scan_timeout) {
+                               /* Prepare to kick client */
+                               usteer_roam_set_state(si, ROAM_TRIGGER_SCAN_DONE, &ev);
+                       } else {
+                               /* Kick in scan timeout */
+                               si->roam_scan_timeout_start = current_time;
+                               usteer_roam_set_state(si, ROAM_TRIGGER_IDLE, &ev);
+                       }
                        break;
                }
 
+               /* Send beacon-request to client */
                usteer_ubus_trigger_client_scan(si);
-               usteer_roam_set_state(si, ROAM_TRIGGER_SCAN);
+               usteer_roam_sm_start_scan(si, &ev);
                break;
 
        case ROAM_TRIGGER_IDLE:
-               if (find_better_candidate(si)) {
-                       usteer_roam_set_state(si, ROAM_TRIGGER_SCAN_DONE);
-                       break;
-               }
-
-               usteer_roam_set_state(si, ROAM_TRIGGER_SCAN);
+               usteer_roam_sm_start_scan(si, &ev);
                break;
 
        case ROAM_TRIGGER_SCAN_DONE:
-               /* Check for stale scan results, kick back to SCAN state if necessary */
-               if (current_time - si->roam_scan_done > 2 * config.roam_scan_interval) {
-                       usteer_roam_set_state(si, ROAM_TRIGGER_SCAN);
+               candidate = usteer_roam_sm_found_better_node(si, &ev, ROAM_TRIGGER_SCAN_DONE);
+               /* Kick back in case no better node is found */
+               if (!candidate) {
+                       usteer_roam_set_state(si, ROAM_TRIGGER_IDLE, &ev);
                        break;
                }
 
-               si_new = find_better_candidate(si);
-               if (!si_new)
-                       break;
-
-               usteer_roam_set_state(si, ROAM_TRIGGER_WAIT_KICK);
+               usteer_ubus_bss_transition_request(si, 1, false, false, 100, candidate->node);
+               si->kick_time = current_time + config.roam_kick_delay;
+               usteer_roam_set_state(si, ROAM_TRIGGER_IDLE, &ev);
                break;
+       }
 
-       case ROAM_TRIGGER_WAIT_KICK:
-               if (si->signal > min_signal)
-                       break;
+       return false;
+}
 
-               usteer_roam_set_state(si, ROAM_TRIGGER_NOTIFY_KICK);
-               usteer_ubus_notify_client_disassoc(si);
-               break;
-       case ROAM_TRIGGER_NOTIFY_KICK:
-               if (current_time - si->roam_event < config.roam_kick_delay * 100)
-                       break;
+bool usteer_policy_can_perform_roam(struct sta_info *si)
+{
+       /* Only trigger for connected STAs */
+       if (si->connected != STA_CONNECTED)
+               return false;
 
-               usteer_roam_set_state(si, ROAM_TRIGGER_KICK);
-               break;
-       case ROAM_TRIGGER_KICK:
-               usteer_ubus_kick_client(si);
-               usteer_roam_set_state(si, ROAM_TRIGGER_IDLE);
-               return true;
-       }
+       /* Skip on pending kick */
+       if (si->kick_time)
+               return false;
 
-       return false;
+       /* Skip on rejected transition */
+       if (si->bss_transition_response.status_code && current_time - si->bss_transition_response.timestamp < config.steer_reject_timeout)
+               return false;
+
+       /* Skip on previous kick attempt */
+       if (current_time - si->roam_kick < config.roam_trigger_interval)
+               return false;
+
+       /* Skip if connection is established shorter than the trigger-interval */
+       if (current_time - si->connected_since < config.roam_trigger_interval)
+               return false;
+       
+       return true;
+}
+
+static bool
+usteer_local_node_roam_sm_active(struct sta_info *si, int min_signal)
+{
+       if (!usteer_policy_can_perform_roam(si))
+               return false;
+
+       /* Signal has to be below scan / roam threshold */
+       if (si->signal >= min_signal)
+               return false;
+
+       return true;
 }
 
 static void
-usteer_local_node_roam_check(struct usteer_local_node *ln)
+usteer_local_node_roam_check(struct usteer_local_node *ln, struct uevent *ev)
 {
        struct sta_info *si;
        int min_signal;
@@ -314,12 +431,11 @@ usteer_local_node_roam_check(struct usteer_local_node *ln)
                return;
 
        usteer_update_time();
-       min_signal = snr_to_signal(&ln->node, min_signal);
+       min_signal = usteer_snr_to_signal(&ln->node, min_signal);
 
        list_for_each_entry(si, &ln->node.sta_info, node_list) {
-               if (!si->connected || si->signal >= min_signal ||
-                   current_time - si->roam_kick < config.roam_trigger_interval) {
-                       usteer_roam_set_state(si, ROAM_TRIGGER_IDLE);
+               if (!usteer_local_node_roam_sm_active(si, min_signal)) {
+                       usteer_roam_set_state(si, ROAM_TRIGGER_IDLE, ev);
                        continue;
                }
 
@@ -327,7 +443,7 @@ usteer_local_node_roam_check(struct usteer_local_node *ln)
                 * If the state machine kicked a client, other clients should wait
                 * until the next turn
                 */
-               if (usteer_roam_trigger_sm(si))
+               if (usteer_roam_trigger_sm(ln, si))
                        return;
        }
 }
@@ -335,83 +451,98 @@ usteer_local_node_roam_check(struct usteer_local_node *ln)
 static void
 usteer_local_node_snr_kick(struct usteer_local_node *ln)
 {
+       unsigned int min_count = DIV_ROUND_UP(config.min_snr_kick_delay, config.local_sta_update);
+       struct uevent ev = {
+               .node_local = &ln->node,
+       };
        struct sta_info *si;
        int min_signal;
 
        if (!config.min_snr)
                return;
 
-       min_signal = snr_to_signal(&ln->node, config.min_snr);
+       min_signal = usteer_snr_to_signal(&ln->node, config.min_snr);
+       ev.threshold.ref = min_signal;
 
        list_for_each_entry(si, &ln->node.sta_info, node_list) {
-               if (!si->connected)
+               if (si->connected != STA_CONNECTED)
                        continue;
 
-               if (si->signal >= min_signal)
+               if (si->signal >= min_signal) {
+                       si->below_min_snr = 0;
                        continue;
+               } else {
+                       si->below_min_snr++;
+               }
 
-               si->kick_count++;
+               if (si->below_min_snr <= min_count)
+                       continue;
 
-               MSG(VERBOSE, "Kicking client "MAC_ADDR_FMT" due to low SNR, signal=%d\n",
-                       MAC_ADDR_DATA(si->sta->addr), si->signal);
+               ev.type = UEV_SIGNAL_KICK;
+               ev.threshold.cur = si->signal;
+               ev.count = si->kick_count;
+               usteer_event(&ev);
 
                usteer_ubus_kick_client(si);
                return;
        }
 }
 
-void
-usteer_local_node_kick(struct usteer_local_node *ln)
+static void
+usteer_local_node_load_kick(struct usteer_local_node *ln)
 {
        struct usteer_node *node = &ln->node;
        struct sta_info *kick1 = NULL, *kick2 = NULL;
        struct sta_info *candidate = NULL;
        struct sta_info *si;
-
-       usteer_local_node_roam_check(ln);
-       usteer_local_node_snr_kick(ln);
+       struct uevent ev = {
+               .node_local = &ln->node,
+       };
+       unsigned int min_count = DIV_ROUND_UP(config.load_kick_delay, config.local_sta_update);
 
        if (!config.load_kick_enabled || !config.load_kick_threshold ||
            !config.load_kick_delay)
                return;
 
        if (node->load < config.load_kick_threshold) {
-               MSG_T("load_kick_threshold",
-                       "is below load for this node (config=%i) (real=%i)\n",
-                       config.load_kick_threshold, node->load);
+               if (!ln->load_thr_count)
+                       return;
+
                ln->load_thr_count = 0;
-               return;
+               ev.type = UEV_LOAD_KICK_RESET;
+               ev.threshold.cur = node->load;
+               ev.threshold.ref = config.load_kick_threshold;
+               goto out;
        }
 
-       if (++ln->load_thr_count <=
-           DIV_ROUND_UP(config.load_kick_delay, config.local_sta_update)) {
-               MSG_T("load_kick_delay", "delay kicking (config=%i)\n",
-                       config.load_kick_delay);
-               return;
-       }
+       if (++ln->load_thr_count <= min_count) {
+               if (ln->load_thr_count > 1)
+                       return;
 
-       MSG(VERBOSE, "AP load threshold exceeded on %s (%d), try to kick a client\n",
-           usteer_node_name(node), node->load);
+               ev.type = UEV_LOAD_KICK_TRIGGER;
+               ev.threshold.cur = node->load;
+               ev.threshold.ref = config.load_kick_threshold;
+               goto out;
+       }
 
        ln->load_thr_count = 0;
        if (node->n_assoc < config.load_kick_min_clients) {
-               MSG_T("load_kick_min_clients",
-                       "min limit reached, stop kicking clients on this node "
-                       "(n_assoc=%i) (config=%i)\n",
-                       node->n_assoc, config.load_kick_min_clients);
-               return;
+               ev.type = UEV_LOAD_KICK_MIN_CLIENTS;
+               ev.threshold.cur = node->n_assoc;
+               ev.threshold.ref = config.load_kick_min_clients;
+               goto out;
        }
 
        list_for_each_entry(si, &ln->node.sta_info, node_list) {
                struct sta_info *tmp;
 
-               if (!si->connected)
+               if (si->connected != STA_CONNECTED)
                        continue;
 
                if (is_more_kickable(kick1, si))
                        kick1 = si;
 
-               tmp = find_better_candidate(si);
+               tmp = find_better_candidate(si, NULL, (1 << UEV_SELECT_REASON_LOAD), 0);
                if (!tmp)
                        continue;
 
@@ -421,16 +552,50 @@ usteer_local_node_kick(struct usteer_local_node *ln)
                }
        }
 
-       if (!kick1)
-               return;
+       if (!kick1) {
+               ev.type = UEV_LOAD_KICK_NO_CLIENT;
+               goto out;
+       }
 
        if (kick2)
                kick1 = kick2;
 
-       MSG(VERBOSE, "Kicking client "MAC_ADDR_FMT", signal=%d, better_candidate=%s\n",
-           MAC_ADDR_DATA(kick1->sta->addr), kick1->signal,
-               candidate ? usteer_node_name(candidate->node) : "(none)");
-
        kick1->kick_count++;
+
+       ev.type = UEV_LOAD_KICK_CLIENT;
+       ev.si_cur = kick1;
+       ev.si_other = candidate;
+       ev.count = kick1->kick_count;
+
        usteer_ubus_kick_client(kick1);
+
+out:
+       usteer_event(&ev);
+}
+
+static void
+usteer_local_node_perform_kick(struct usteer_local_node *ln)
+{
+       struct sta_info *si;
+
+       list_for_each_entry(si, &ln->node.sta_info, node_list) {
+               if (!si->kick_time || si->kick_time > current_time)
+                       continue;
+
+               usteer_ubus_kick_client(si);
+       }
+}
+
+void
+usteer_local_node_kick(struct usteer_local_node *ln)
+{
+       struct uevent ev = {
+               .node_local = &ln->node,
+       };
+
+       usteer_local_node_perform_kick(ln);
+
+       usteer_local_node_snr_kick(ln);
+       usteer_local_node_load_kick(ln);
+       usteer_local_node_roam_check(ln, &ev);
 }