From ead2703ea9c80bbe8238f855303f436ec01d05f6 Mon Sep 17 00:00:00 2001
From: Janusz Dziedzic <janusz.dziedzic@iopsys.eu>
Date: Mon, 9 Jun 2025 11:26:21 +0200
Subject: [PATCH] acs: introduce improvements

UBUS:
 - add trigger_acs
 - improve trigger_channel_clearing
 - report clearing/acs status in ubus status
 - report status request during ubus call

CMDUS:
 - handle and use CAC completion report
 - handle and use CAC status
 - handle and use oper channel report
 - fix generation of channel selection request

ACS:
 - add CMDUs ACS callbacks - use them for recalc
 - remove dfs_cleanup timer - base on events when recalc
 - fix bsta connected check
 - run clearing when background CAC supported by node
 - run acs recalc when DFS chan available
 - introduce backoff when platform don't report CAC ongoing correctly
 - allow to choose highest bandwidth
 - store and check radio metrics

CONFIG:

Available options:

config channel_plan
        option preclear_dfs '1'
        option acs '1'
        option acs_timeout '3h'
        option acs_highest_bandwidth '0'
	option acs_skip_dfs '0'
	option acs_prevent_cac '1'
	option acs_scan_before_recalc '1'
---
 src/acs.c                 | 1329 ++++++++++++++++++++++++++++++++++---
 src/acs.h                 |   52 +-
 src/cntlr.c               |   36 +-
 src/cntlr.h               |    1 -
 src/cntlr_cmdu.c          |   39 +-
 src/cntlr_cmdu.h          |    9 +-
 src/cntlr_commands.c      |   54 +-
 src/cntlr_commands.h      |   23 +
 src/cntlr_commands_impl.c |  135 +++-
 src/cntlr_commands_impl.h |    1 +
 src/cntlr_map.c           |  164 ++++-
 src/cntlr_tlv.c           |  146 ++--
 src/cntlr_tlv.h           |    3 -
 src/cntlr_ubus.c          |   29 +
 src/cntlr_ubus_dbg.c      |    2 +-
 src/config.c              |  163 ++++-
 src/config.h              |    7 +-
 src/timer.c               |    2 +-
 src/utils/utils.c         |   16 +
 src/utils/utils.h         |    1 +
 src/wifi_dataelements.h   |    4 +
 src/wifi_opclass.c        |  151 ++++-
 src/wifi_opclass.h        |    9 +-
 23 files changed, 2007 insertions(+), 369 deletions(-)

diff --git a/src/acs.c b/src/acs.c
index 6c014f08..6fc7dd82 100644
--- a/src/acs.c
+++ b/src/acs.c
@@ -17,11 +17,18 @@
 
 #include "cntlr.h"
 #include "cntlr_cmdu.h"
+#include "cntlr_map.h"
 #include "utils/debug.h"
+#include "utils/utils.h"
 #include "wifi_dataelements.h"
 #include "wifi_opclass.h"
+#include "timer.h"
+#include "sta.h"
 
+/* Per radio ACS recalc timer */
+static void cntlr_radio_acs_timer(atimer_t *t);
 
+/* Opclass helpers - move it to other place */
 int cntlr_radio_pref_opclass_reset(struct wifi_radio_element *radio)
 {
 	/*
@@ -30,10 +37,29 @@ int cntlr_radio_pref_opclass_reset(struct wifi_radio_element *radio)
 	 */
 	memcpy(&radio->pref_opclass, &radio->supp_opclass, sizeof(radio->pref_opclass));
 	wifi_opclass_set_preferences(&radio->pref_opclass, 15 << 4);
+	timestamp_update(&radio->pref_opclass.entry_time);
 
 	return radio->pref_opclass.num_opclass;
 }
 
+enum wifi_radio_opclass_dfs dfs_state_from_preference(uint8_t preference)
+{
+	uint8_t reas = preference & CHANNEL_PREF_REASON;
+
+	switch(reas) {
+	case CHANNEL_PREF_REASON_DFS_USABLE:
+		return WIFI_RADIO_OPCLASS_CHANNEL_DFS_USABLE;
+	case CHANNEL_PREF_REASON_DFS_AVAILABLE:
+		return WIFI_RADIO_OPCLASS_CHANNEL_DFS_AVAILABLE;
+	case CHANNEL_PREF_REASON_DFS_NOP:
+		return WIFI_RADIO_OPCLASS_CHANNEL_DFS_NOP;
+	default:
+		break;
+	}
+
+	return WIFI_RADIO_OPCLASS_CHANNEL_DFS_NONE;
+}
+
 int cntlr_radio_pref_opclass_add(struct wifi_radio_element *radio, uint8_t classid,
 				 uint8_t channel, uint8_t preference)
 {
@@ -53,6 +79,7 @@ int cntlr_radio_pref_opclass_add(struct wifi_radio_element *radio, uint8_t class
 
 	new_chan.channel = channel;
 	new_chan.preference = preference;
+	new_chan.dfs = dfs_state_from_preference(preference);
 
 	timestamp_update(&opclass->entry_time);
 
@@ -61,12 +88,31 @@ int cntlr_radio_pref_opclass_add(struct wifi_radio_element *radio, uint8_t class
 	if (chan) {
 		chan->channel = channel;
 		chan->preference = preference;
+
+		chan->dfs = dfs_state_from_preference(preference);
+
 		return 0;
 	}
 
 	return wifi_opclass_add_channel(entry, &new_chan);
 }
 
+int cntlr_radio_pref_opclass_set_dfs_status(struct wifi_radio_element *radio, uint8_t classid,
+					    uint8_t channel, enum wifi_radio_opclass_dfs state)
+{
+	struct wifi_radio_opclass_channel *chan;
+	struct wifi_radio_opclass *opclass;
+
+	opclass = &radio->pref_opclass;
+
+	chan = wifi_opclass_get_channel(opclass, classid, channel);
+	if (!chan)
+		return -1;
+
+	chan->dfs = state;
+	return 0;
+}
+
 void cntlr_radio_pref_opclass_dump(struct wifi_radio_element *radio)
 {
 	wifi_opclass_dump(&radio->pref_opclass, "dev_pref_opclass", radio->macaddr);
@@ -105,7 +151,7 @@ uint8_t ctrl_radio_cur_opclass_id(struct wifi_radio_element *radio)
 	uint8_t id = 0;
 	int ret;
 
-	ret = wifi_opclass_get_max_bw(&radio->cur_opclass, NULL, NULL, &id);
+	ret = wifi_opclass_get_max_bw(&radio->cur_opclass, NULL, NULL, &id, NULL);
 	if (ret)
 		return 0;
 	return id;
@@ -116,18 +162,29 @@ uint8_t ctrl_radio_cur_opclass_ctrl_chan(struct wifi_radio_element *radio)
 	uint8_t ctrl_chan = 0;
 	int ret;
 
-	ret = wifi_opclass_get_max_bw(&radio->cur_opclass, &ctrl_chan, NULL, NULL);
+	ret = wifi_opclass_get_max_bw(&radio->cur_opclass, &ctrl_chan, NULL, NULL, NULL);
 	if (ret)
 		return 0;
 	return ctrl_chan;
 }
 
-uint8_t ctrl_radio_cur_opclass_max_bw(struct wifi_radio_element *radio)
+uint8_t ctrl_radio_cur_opclass_chan(struct wifi_radio_element *radio)
+{
+	uint8_t chan = 0;
+	int ret;
+
+	ret = wifi_opclass_get_max_bw(&radio->cur_opclass, NULL, NULL, NULL, &chan);
+	if (ret)
+		return 0;
+	return chan;
+}
+
+uint16_t ctrl_radio_cur_opclass_max_bw(struct wifi_radio_element *radio)
 {
 	uint32_t bw = 0;
 	int ret;
 
-	ret = wifi_opclass_get_max_bw(&radio->cur_opclass, NULL, &bw, NULL);
+	ret = wifi_opclass_get_max_bw(&radio->cur_opclass, NULL, &bw, NULL, NULL);
 	if (ret)
 		return 0;
 	return bw;
@@ -143,42 +200,552 @@ void cntlr_radio_cur_opclass_dump(struct wifi_radio_element *radio)
 	wifi_opclass_dump(&radio->cur_opclass, "dev_cur_opclass", radio->macaddr);
 }
 
-static bool cntlr_radio_pref_opclass_expired(struct wifi_radio_element *radio)
+static bool cntlr_radio_pref_opclass_expired(struct wifi_radio_element *radio, int age)
 {
-	return wifi_opclass_expired(&radio->pref_opclass, 120);
+	return wifi_opclass_expired(&radio->pref_opclass, age);
 }
 
-bool cntlr_node_pref_opclass_expired(struct node *node)
+bool cntlr_node_pref_opclass_expired(struct node *node, int age)
 {
 	struct netif_radio *r = NULL;
 	bool expired = false;
 
 	list_for_each_entry(r, &node->radiolist, list) {
-		expired |= cntlr_radio_pref_opclass_expired(r->radio_el);
+		expired |= cntlr_radio_pref_opclass_expired(r->radio_el, age);
 	}
 
 	return expired;
 }
 
+enum acs_cleanup_status {
+	ACS_CLEANUP_STATUS_SKIPPED,
+	ACS_CLEANUP_STATUS_SELECTED,
+	ACS_CLEANUP_STATUS_REQUESTED,
+	ACS_CLEANUP_STATUS_REJECTED,
+	ACS_CLEANUP_STATUS_RADAR,
+	ACS_CLEANUP_STATUS_ACCEPTED,
+	ACS_CLEANUP_STATUS_DONE,
+	ACS_CLEANUP_STATUS_ALL_CLEAN,
+	ACS_CLEANUP_STATUS_NOT_SUPPORTED,
+	ACS_CLEANUP_STATUS_NO_APS,
+	ACS_CLEANUP_STATUS_ALREADY_ONGOING,
+	ACS_CLEANUP_STATUS_TIMEOUT,
+};
+
+enum acs_recalc_status {
+	ACS_RECALC_STATUS_SKIPPED,
+	ACS_RECALC_STATUS_SCAN_REQUESTED,
+	ACS_RECALC_STATUS_SCAN_DONE,
+	ACS_RECALC_STATUS_BEST_SELECTED,
+	ACS_RECALC_STATUS_BEST_REQUESTED,
+	ACS_RECALC_STATUS_BEST_REJECTED,
+	ACS_RECALC_STATUS_BEST_ACCEPTED,
+	ACS_RECALC_STATUS_BEST_SET,
+	ACS_RECALC_STATUS_BEST_ALREADY_SET,
+	ACS_RECALC_STATUS_INPUT_DATA_INSUFFICIENT,
+	ACS_RECALC_STATUS_BSTA_CONNECTED,
+	ACS_RECALC_STATUS_INVALID_DATA,
+	ACS_RECALC_STATUS_BEST_SET_TIMEOUT,
+	ACS_RECALC_STATUS_BEST_CAC,
+	ACS_RECALC_STATUS_BEST_CAC_RADAR,
+};
+
+struct acs_params {
+	struct timespec entry_time;
+
+	/* Input params */
+	int opclass;			/* recalc for this opclass */
+	int bw;				/* recalc for this bandwidth */
+
+	bool skip_dfs;			/* Skip all DFS channels */
+	bool skip_dfs_not_available;	/* Prevent CAC */
+	bool higest_bandwidth;		/* Use highest possible bandwidth */
+
+	/* Output params - for channel selection request */
+	int best_channel;
+	int best_opclass;
+	int best_bw;
+	int best_pref;
+	uint32_t best_cac_time;
+
+	enum acs_recalc_status status;	/* current recalc state */
+	uint16_t scan_mid;		/* scan request mid */
+	uint16_t mid;			/* channel selection mid */
+
+	bool recalc;			/* run new recalc */
+};
+
+struct acs_cac_data {
+	struct cac_data data;		/* data for CAC CMDU */
+
+	struct timespec entry_time;
+	enum acs_cleanup_status status;	/* current CAC state */
+	uint16_t mid;			/* CAC request mid */
+	uint32_t cac_time;		/* CAC time for current request */
+};
+
+struct acs_radio_metrics_entry {
+	struct timespec time;
+
+	uint8_t anpi;			/* Noise level */
+	uint8_t obss;			/* Other BSS busy factor */
+};
+
+struct acs_radio_metrics {
+	struct timespec time;
+
+	uint8_t anpi;	/* EWMA value */
+	uint8_t obss;	/* EWMA value */
+
+	/* Ring buffer for more data */
+	uint8_t idx;
+	struct acs_radio_metrics_entry entry[16];
+};
+
+/* acs per radio structure */
+struct acs {
+	struct acs_params last_acs;
+	struct acs_cac_data last_cac_data;
+	struct acs_radio_metrics radio_metrics;
+
+	struct controller *cntlr;
+	struct node *node;
+	struct netif_radio *radio;
+
+	atimer_t acs_timer;
+};
+
+/* Called when conroller alloc radio structure */
+void *cntlr_radio_acs_alloc(struct controller *c, struct node *n, struct netif_radio *r)
+{
+	struct acs *acs;
+
+	acs = calloc(1, sizeof(struct acs));
+	if (!acs)
+		return NULL;
+
+	acs->cntlr = c;
+	acs->node = n;
+	acs->radio = r;
+
+	timer_init(&acs->acs_timer, cntlr_radio_acs_timer);
+	return acs;
+}
+
+/* Called before controller destroy radio structure */
+void cntlr_radio_acs_free(void *acs)
+{
+	struct acs *ptr = acs;
+
+	if (!ptr)
+		return;
+
+	timer_del(&ptr->acs_timer);
+	ptr->cntlr = NULL;
+	ptr->node = NULL;
+	ptr->radio = NULL;
+
+	/* Do required cleanup here before free memory */
+	free(ptr);
+}
+
+/* acs/cac/metrics - helpers */
+static struct acs_params *cntlr_radio_get_last_acs(struct netif_radio *r)
+{
+	struct acs *acs;
+
+	acs = r->radio_el->acs;
+	if (!acs)
+		return NULL;
+
+	return &acs->last_acs;
+}
+
+static struct acs_cac_data *cntlr_radio_get_last_cac(struct netif_radio *r)
+{
+	struct acs *acs;
+
+	acs = r->radio_el->acs;
+	if (!acs)
+		return NULL;
+
+	return &acs->last_cac_data;
+}
+
+static struct acs_radio_metrics *cntlr_radio_get_metrics(struct netif_radio *r)
+{
+	struct acs *acs;
+
+	acs = r->radio_el->acs;
+	if (!acs)
+		return NULL;
+
+	return &acs->radio_metrics;
+}
+
+static int cntlr_radio_acs_timer_set(struct netif_radio *r, uint32_t tmo_ms)
+{
+	struct acs *acs;
+
+	acs = r->radio_el->acs;
+	if (!acs)
+		return -1;
+
+	return timer_set(&acs->acs_timer, tmo_ms);
+}
+
+/* JSON status print - for UBUS radio status and trigger_acs/trigger_channel_clearing */
+static char *cntlr_acs_radio_status(enum acs_recalc_status status)
+{
+	switch (status) {
+	case ACS_RECALC_STATUS_SKIPPED:
+		return "skipped";
+	case ACS_RECALC_STATUS_SCAN_REQUESTED:
+		return "scan requested";
+	case ACS_RECALC_STATUS_SCAN_DONE:
+		return "scan done";
+	case ACS_RECALC_STATUS_BEST_SELECTED:
+		return "best_selected";
+	case ACS_RECALC_STATUS_BEST_REQUESTED:
+		return "best_requested";
+	case ACS_RECALC_STATUS_BEST_ACCEPTED:
+		return "best_accepted";
+	case ACS_RECALC_STATUS_BEST_REJECTED:
+		return "best_rejected";
+	case ACS_RECALC_STATUS_BEST_SET:
+		return "best_set";
+	case ACS_RECALC_STATUS_INPUT_DATA_INSUFFICIENT:
+		return "insufficient_data";
+	case ACS_RECALC_STATUS_BEST_ALREADY_SET:
+		return "current_best";
+	case ACS_RECALC_STATUS_BSTA_CONNECTED:
+		return "skipped - bsta connected";
+	case ACS_RECALC_STATUS_INVALID_DATA:
+		return "skipped - invalid data";
+	case ACS_RECALC_STATUS_BEST_SET_TIMEOUT:
+		return "best_set timeout";
+	case ACS_RECALC_STATUS_BEST_CAC:
+		return "best_set CAC ongoing";
+	case ACS_RECALC_STATUS_BEST_CAC_RADAR:
+		return "best_set CAC RADAR hit";
+	default:
+		break;
+	}
+
+	return "recalc_unknown";
+}
+
+void cntlr_acs_radio_info(struct blob_buf *bb, struct netif_radio *r)
+{
+	struct acs_params *acs;
+	void *t, *a;
+
+	acs = cntlr_radio_get_last_acs(r);
+	if (!acs)
+		return;
+
+	blobmsg_add_u32(bb, "acs_request_age", (uint32_t) timestamp_elapsed_sec(&acs->entry_time));
+	a = blobmsg_open_array(bb, "acs_request");
+	t = blobmsg_open_table(bb, "");
+	blobmsg_add_string(bb, "status", cntlr_acs_radio_status(acs->status));
+	blobmsg_add_u32(bb, "channel", acs->best_channel);
+	blobmsg_add_u32(bb, "bandwidth", acs->best_bw);
+
+	if (acs->status == ACS_RECALC_STATUS_BEST_REQUESTED)
+		blobmsg_add_u32(bb, "mid", acs->mid);
+	blobmsg_close_table(bb, t);
+	blobmsg_close_array(bb, a);
+}
+
+void cntlr_acs_node_info(struct node *node, enum wifi_band band, struct blob_buf *bb)
+{
+	struct netif_radio *r = NULL;
+	void *t;
+
+	list_for_each_entry(r, &node->radiolist, list) {
+		struct acs_params *acs;
+
+		acs = cntlr_radio_get_last_acs(r);
+		if (!acs)
+			continue;
+
+		if (band && band != BAND_ANY && band != r->radio_el->band)
+			continue;
+
+		if (acs->status == ACS_RECALC_STATUS_SKIPPED)
+			continue;
+
+		t = blobmsg_open_table(bb, "");
+		blobmsg_add_macaddr(bb, "agent", node->almacaddr);
+		blobmsg_add_macaddr(bb, "radio", r->radio_el->macaddr);
+		blobmsg_add_string(bb, "status", cntlr_acs_radio_status(acs->status));
+		blobmsg_add_u32(bb, "channel", acs->best_channel);
+                blobmsg_add_u32(bb, "bandwidth", acs->best_bw);
+
+		if (acs->status == ACS_RECALC_STATUS_BEST_REQUESTED)
+			blobmsg_add_u32(bb, "mid", acs->mid);
+
+		blobmsg_close_table(bb, t);
+	}
+}
+
+static char *cntlr_acs_radio_cleanup_status(enum acs_cleanup_status status)
+{
+	switch (status) {
+	case ACS_CLEANUP_STATUS_SKIPPED:
+		return "none";
+	case ACS_CLEANUP_STATUS_SELECTED:
+		return "selected";
+	case ACS_CLEANUP_STATUS_REQUESTED:
+		return "requested";
+	case ACS_CLEANUP_STATUS_REJECTED:
+		return "rejected";
+	case ACS_CLEANUP_STATUS_RADAR:
+		return "radar";
+	case ACS_CLEANUP_STATUS_ACCEPTED:
+		return "accepted";
+	case ACS_CLEANUP_STATUS_DONE:
+		return "done";
+	case ACS_CLEANUP_STATUS_ALL_CLEAN:
+		return "all clean";
+	case ACS_CLEANUP_STATUS_NOT_SUPPORTED:
+		return "not supported";
+	case ACS_CLEANUP_STATUS_NO_APS:
+		return "no APs iface";
+	case ACS_CLEANUP_STATUS_ALREADY_ONGOING:
+		return "CAC already ongoing";
+	case ACS_CLEANUP_STATUS_TIMEOUT:
+		return "timeout";
+	default:
+		break;
+	}
+	return "unknown";
+}
+
+void cntlr_dfs_radio_cleanup_info(struct node *n, struct netif_radio *r, struct blob_buf *bb)
+{
+	struct acs_cac_data *cac;
+	void *t;
+
+	cac = cntlr_radio_get_last_cac(r);
+	if (!cac)
+		return;
+
+	t = blobmsg_open_table(bb, "");
+	blobmsg_add_macaddr(bb, "agent", n->almacaddr);
+	blobmsg_add_macaddr(bb, "radio", r->radio_el->macaddr);
+	blobmsg_add_string(bb, "status", cntlr_acs_radio_cleanup_status(cac->status));
+	blobmsg_add_u32(bb, "channel", cac->data.channel);
+	blobmsg_add_u32(bb, "opclass", cac->data.opclass);
+
+	if (cac->status == ACS_CLEANUP_STATUS_REQUESTED)
+		blobmsg_add_u32(bb, "mid", cac->mid);
+	blobmsg_close_table(bb, t);
+}
+
+void cntlr_acs_radio_cleanup_info(struct blob_buf *bb, struct netif_radio *r)
+{
+	struct acs_cac_data *cac;
+	void *t, *a;
+
+	cac = cntlr_radio_get_last_cac(r);
+	if (!cac)
+		return;
+
+	blobmsg_add_u32(bb, "cleanup_request_age", (uint32_t) timestamp_elapsed_sec(&cac->entry_time));
+	a = blobmsg_open_array(bb, "cleanup_request");
+	t = blobmsg_open_table(bb, "");
+	blobmsg_add_string(bb, "status", cntlr_acs_radio_cleanup_status(cac->status));
+	blobmsg_add_u32(bb, "channel", cac->data.channel);
+	blobmsg_add_u32(bb, "opclass", cac->data.opclass);
+
+	if (cac->status == ACS_CLEANUP_STATUS_REQUESTED)
+		blobmsg_add_u32(bb, "mid", cac->mid);
+	blobmsg_close_table(bb, t);
+	blobmsg_close_array(bb, a);
+}
+
+/* CMDU ACS builders - for scan request / preference request */
+static uint16_t cntlr_acs_send_scan_request(struct node *n, struct netif_radio *r)
+{
+	struct wifi_radio_opclass_entry *entry;
+	struct wifi_radio_opclass *opclass;
+	struct scan_req_data req = {};
+	struct cmdu_buff *cmdu;
+	uint16_t mid = 0xffff;
+	int num = 0;
+	int i;
+
+	cntlr_dbg(LOG_CHANNEL, "acs node " MACFMT " " MACFMT " - scan request\n",
+		  MAC2STR(n->almacaddr), MAC2STR(r->radio_el->macaddr));
+
+	opclass = &r->radio_el->pref_opclass;
+
+	if (!opclass->num_opclass)
+		goto out;
+
+	/* Build proper scan request - all 20MHz channels */
+	req.is_fresh_scan = true;
+	req.num_radio = 1;
+	memcpy(req.radios[0].radio_mac, r->radio_el->macaddr, 6);
+
+	for (i = 0; i < opclass->num_opclass; i++) {
+		entry = &opclass->opclass[i];
+
+		if (entry->bandwidth != 20)
+			continue;
+		if (entry->num_channel == 0)
+			continue;
+
+		if (num >= ARRAY_SIZE(req.radios[0].opclasses))
+			break;
+		req.radios[0].opclasses[num].classid = entry->id;
+		num++;
+	}
+
+	req.radios[0].num_opclass = num;
+
+	cmdu = cntlr_gen_channel_scan_request(n->cntlr, n->almacaddr, &req);
+	if (!cmdu)
+		goto out;
+
+	mid = send_cmdu(n->cntlr, cmdu);
+	cmdu_free(cmdu);
+
+out:
+	cntlr_dbg(LOG_CHANNEL, "acs node " MACFMT " " MACFMT " - scan request mid %u\n",
+		  MAC2STR(n->almacaddr), MAC2STR(r->radio_el->macaddr), mid);
+	return mid;
+}
+
+static uint16_t cntlr_acs_send_pref_request(struct node *n)
+{
+	struct cmdu_buff *cmdu;
+	uint16_t mid = 0xffff;
+
+	cntlr_dbg(LOG_CHANNEL, "acs node send pref request " MACFMT "\n", MAC2STR(n->almacaddr));
+	cmdu = cntlr_gen_channel_preference_query(n->cntlr, n->almacaddr);
+	if (!cmdu)
+		return mid;
+
+	mid = send_cmdu(n->cntlr, cmdu);
+	cmdu_free(cmdu);
+
+	return mid;
+}
+
+/* ACS recalc code */
+uint8_t cntrl_acs_radio_ctrl_channel(struct wifi_radio_element *radio, uint8_t opclass, uint8_t channel)
+{
+	struct wifi_radio_opclass_channel *chan;
+
+	chan = wifi_opclass_get_channel(&radio->pref_opclass, opclass, channel);
+	if (!chan)
+		return 0;
+
+	return wifi_get_best_ctrl_channel(&radio->pref_opclass,
+					  chan->ctrl_channels,
+					  ARRAY_SIZE(chan->ctrl_channels));
+}
+
+static bool cntlr_acs_radio_is_bsta_connected(struct controller *cntlr, struct netif_radio *radio)
+{
+	struct netif_iface *iface = NULL;
+
+	list_for_each_entry(iface, &radio->iflist, list) {
+		struct node *n = NULL;
+
+		/* Check if sta iface */
+		if (iface->bss->is_bbss || iface->bss->is_fbss)
+			continue;
+
+		/* Check all nodes */
+		list_for_each_entry(n, &cntlr->nodelist, list) {
+			struct sta *s = NULL;
+
+			if (!n->sta_count)
+				continue;
+
+			list_for_each_entry(s, &n->stalist, list) {
+				if (!s->de_sta)
+					continue;
+				/* skip other band */
+				if (memcmp(iface->bss->bssid, s->de_sta->macaddr, sizeof(iface->bss->bssid)))
+					continue;
+
+				if (s->is_bsta && s->state == STA_ASSOCIATED)
+					return true;
+			}
+		}
+	}
+
+	return false;
+}
+
+static void cntrl_acs_radio_common_opclass(struct controller *cntlr, struct netif_radio *rd,
+					   struct wifi_radio_opclass *opclass)
+{
+	memcpy(opclass, &rd->radio_el->pref_opclass, sizeof(*opclass));
 
-int cntlr_acs_radio_channel_recalc(struct wifi_radio_element *radio, struct acs_params *params)
+	/*
+	 * For future use just in case nodes using different supported
+	 * channels. Prevent switch to channel/opclass not supported
+	 * by leaf(s).
+	 * We could skip this step when ethernet backhaul used.
+	 */
+}
+
+int cntlr_acs_radio_channel_recalc(struct node *node, struct netif_radio *rd, struct acs_params *params)
 {
+	struct wifi_radio_element *radio = rd->radio_el;
+	struct wifi_radio_opclass common_opclass = {};
 	struct acs_params acs_params[64] = {};
-	int acs_params_num = 0;
 	struct wifi_radio_opclass_entry *entry;
 	struct wifi_radio_opclass *opclass;
+	struct acs_params *last_acs;
+	int acs_params_num = 0;
 	int chan, pref, reas;
 	int pref_best = 0;
 	int pref_cur = 0;
+	uint32_t cac_time = 0;
 	int prefered[64] = {0};
 	int i, j, r;
 
-	opclass = &radio->pref_opclass;
-
-	cntlr_dbg(LOG_CHANNEL, "acs radio channel recalc " MACFMT " opclass %d bw %d skip_dfs %d\n",
+	cntlr_trace(LOG_CHANNEL, "acs radio channel recalc " MACFMT " opclass %d bw %d skip_dfs %d\n",
 	    MAC2STR(radio->macaddr),
 	    params->opclass, params->bw, params->skip_dfs);
 
+	last_acs = cntlr_radio_get_last_acs(rd);
+	if (!last_acs)
+		return -1;
+
+	memset(last_acs, 0, sizeof(*last_acs));
+	last_acs->scan_mid = 0xffff;
+
+	if (!radio->pref_opclass.num_opclass) {
+		/* We don't know node preferences yet */
+		cntlr_dbg(LOG_CHANNEL, "acs radio channel recalc " MACFMT " - no pref opclass - skip recalc\n",
+			  MAC2STR(radio->macaddr));
+
+		params->status = ACS_RECALC_STATUS_INPUT_DATA_INSUFFICIENT;
+		goto out;
+	}
+
+	if (cntlr_acs_radio_is_bsta_connected(node->cntlr, rd)) {
+		/*
+		 * Node is leaf with active wifi backhaul. In such leaf should
+		 * simple follow parent node. Skip channel switch request.
+		 */
+		cntlr_dbg(LOG_CHANNEL, "acs radio " MACFMT " - skip switch - bsta connected\n",
+			  MAC2STR(radio->macaddr));
+
+		params->status = ACS_RECALC_STATUS_BSTA_CONNECTED;
+		goto out;
+	}
+
+	cntrl_acs_radio_common_opclass(node->cntlr, rd, &common_opclass);
+	opclass = &common_opclass;
+
 	for (i = 0; i < opclass->num_opclass; i++) {
 		entry = &opclass->opclass[i];
 
@@ -194,8 +761,9 @@ int cntlr_acs_radio_channel_recalc(struct wifi_radio_element *radio, struct acs_
 			chan = entry->channel[j].channel;
 			pref = (entry->channel[j].preference & CHANNEL_PREF_MASK) >> 4;
 			reas = entry->channel[j].preference & CHANNEL_PREF_REASON;
+			cac_time = entry->channel[j].cac_time;
 
-			trace("\tacs check/cmp chan %d pref %d reas %d\n", chan, pref, reas);
+			cntlr_trace(LOG_CHANNEL, "\tacs check/cmp chan %d pref %d reas %d\n", chan, pref, reas);
 
 			/* Always skip disabled channels */
 			if (reas == CHANNEL_PREF_REASON_DFS_NOP)
@@ -203,6 +771,10 @@ int cntlr_acs_radio_channel_recalc(struct wifi_radio_element *radio, struct acs_
 			if (reas == CHANNEL_PREF_REASON_REG_DISALLOWED)
 				continue;
 
+			/* Current channel preference */
+			if (chan == params->best_channel)
+				pref_cur = pref;
+
 			/* Skip DFS channels if requested */
 			if (params->skip_dfs) {
 				if (reas == CHANNEL_PREF_REASON_DFS_AVAILABLE ||
@@ -211,16 +783,18 @@ int cntlr_acs_radio_channel_recalc(struct wifi_radio_element *radio, struct acs_
 			}
 
 			/* Skip non available DFS channels if requested */
-			if (params->skip_dfs_not_available && reas != CHANNEL_PREF_REASON_DFS_AVAILABLE)
+			if (params->skip_dfs_not_available && reas == CHANNEL_PREF_REASON_DFS_USABLE)
+				continue;
+
+			/* If background CAC supported and enabled - wait background CAC complete */
+			if (radio->bgcac_supported &&
+			    reas == CHANNEL_PREF_REASON_DFS_USABLE &&
+			    node->cntlr->cfg.dfs_cleanup)
 				continue;
 
 			if (WARN_ON(acs_params_num >= ARRAY_SIZE(acs_params)))
 				break;
 
-			/* Current channel preference */
-			if (chan == params->best_channel)
-				pref_cur = pref;
-
 			/* Kick best value */
 			if (pref > pref_best)
 				pref_best = pref;
@@ -229,21 +803,30 @@ int cntlr_acs_radio_channel_recalc(struct wifi_radio_element *radio, struct acs_
 			acs_params[acs_params_num].best_opclass = entry->id;
 			acs_params[acs_params_num].best_bw = entry->bandwidth;
 			acs_params[acs_params_num].best_pref = pref;
+			acs_params[acs_params_num].best_cac_time = cac_time;
 
 			acs_params_num++;
 		}
 	}
 
-	if (!pref_best)
-		return -1;
+	if (!pref_best) {
+		cntlr_dbg(LOG_CHANNEL, "acs radio channel recalc " MACFMT " - no pref best - skip recalc\n",
+			  MAC2STR(radio->macaddr));
 
-	cntlr_dbg(LOG_CHANNEL, "acs radio " MACFMT " best pref %d vs current pref %d\n",
+		params->status = ACS_RECALC_STATUS_INPUT_DATA_INSUFFICIENT;
+		goto out;
+	}
+
+	cntlr_trace(LOG_CHANNEL, "acs radio " MACFMT " best pref %d vs current pref %d\n",
 	    MAC2STR(radio->macaddr), pref_best, pref_cur);
 
 	/* If current channel equal to best don't switch */
 	if (pref_cur == pref_best) {
-		cntlr_dbg(LOG_CHANNEL, "acs skip - current channel %d is the best\n", params->best_channel);
-		return -1;
+		cntlr_warn(LOG_CHANNEL, "acs node " MACFMT " " MACFMT " current channel %d is the best\n",
+			   MAC2STR(node->almacaddr), MAC2STR(radio->macaddr), params->best_channel);
+
+		params->status = ACS_RECALC_STATUS_BEST_ALREADY_SET;
+		goto out;
 	}
 
 	/* Get random channel from best performance */
@@ -259,45 +842,39 @@ int cntlr_acs_radio_channel_recalc(struct wifi_radio_element *radio, struct acs_
 		j++;
 	}
 
-	if (WARN_ON(!j))
-		return -1;
+	if (WARN_ON(!j)) {
+		params->status = ACS_RECALC_STATUS_INPUT_DATA_INSUFFICIENT;
+		goto out;
+	}
 
 	srandom(time(NULL));
 	r = random() % j;
 
-	cntlr_dbg(LOG_CHANNEL, "acs radio " MACFMT " table size %d - rand %d, index %d\n",
+	cntlr_trace(LOG_CHANNEL, "acs radio " MACFMT " table size %d - rand %d, index %d\n",
 	    MAC2STR(radio->macaddr), j, r, prefered[r]);
 
-	if (prefered[r] >= acs_params_num)
-		return -1;
+	if (prefered[r] >= acs_params_num) {
+		params->status = ACS_RECALC_STATUS_INPUT_DATA_INSUFFICIENT;
+		goto out;
+	}
 
 	params->best_channel = acs_params[prefered[r]].best_channel;
 	params->best_bw = acs_params[prefered[r]].best_bw;
 	params->best_opclass = acs_params[prefered[r]].best_opclass;
+	params->best_cac_time = acs_params[prefered[r]].best_cac_time;
+	params->best_pref = acs_params[prefered[r]].best_pref;
+	params->status = ACS_RECALC_STATUS_BEST_SELECTED;
+
 
 	cntlr_dbg(LOG_CHANNEL, "acs radio " MACFMT " best chan %d/%d opclass %d\n",
 	    MAC2STR(radio->macaddr),
 	    params->best_channel, params->best_bw, params->best_opclass);
 
-	return 0;
-}
+out:
+	memcpy(last_acs, params, sizeof(*last_acs));
+	timestamp_update(&last_acs->entry_time);
 
-static bool cntlr_acs_radio_is_bsta_connected(struct netif_radio *radio)
-{
-	struct netif_iface *iface = NULL;
-
-	list_for_each_entry(iface, &radio->iflist, list) {
-		/* Check if sta iface connected */
-		if (iface->bss->is_bbss || iface->bss->is_fbss)
-			continue;
-
-		if (hwaddr_is_zero(iface->upstream_bssid))
-			continue;
-
-		return true;
-	}
-
-	return false;
+	return params->status != ACS_RECALC_STATUS_BEST_SELECTED;
 }
 
 static int cntlr_get_current_acs_params(struct wifi_radio_element *radio, struct acs_params *params)
@@ -310,65 +887,201 @@ static int cntlr_get_current_acs_params(struct wifi_radio_element *radio, struct
 	params->opclass = ctrl_radio_cur_opclass_id(radio);
 	params->bw = ctrl_radio_cur_opclass_max_bw(radio);
 
-	params->best_channel = ctrl_radio_cur_opclass_ctrl_chan(radio);
+	params->best_channel = ctrl_radio_cur_opclass_chan(radio);
 	params->best_bw = params->bw;
 	params->best_opclass = params->opclass;
 
 	return 0;
 }
 
-void cntlr_acs_node_channel_recalc(struct node *node, bool skip_dfs)
+void cntlr_acs_node_channel_recalc(struct node *node, enum wifi_band band, uint8_t opclass,
+				   uint32_t bandwidth, bool skip_dfs, bool prevent_cac,
+				   bool highest_bandwidth)
 {
 	struct acs_params cur_acs_params = {};
 	struct acs_params acs_params = {};
+	uint32_t bw = bandwidth;
 	struct netif_radio *r = NULL;
 	int ret;
 
 	acs_params.skip_dfs = skip_dfs;
+	acs_params.skip_dfs_not_available = prevent_cac;
 
-	cntlr_dbg(LOG_CHANNEL, "acs node channel recalc " MACFMT " skip_dfs %d\n",
-	    MAC2STR(node->almacaddr), acs_params.skip_dfs);
+	cntlr_dbg(LOG_CHANNEL, "acs node channel recalc " MACFMT " opclass %u bandwidth %u skip_dfs %d prevent_cac %d higest_bandwidth %d\n",
+	    MAC2STR(node->almacaddr), opclass, bandwidth, acs_params.skip_dfs, acs_params.skip_dfs_not_available, highest_bandwidth);
 
 	list_for_each_entry(r, &node->radiolist, list) {
-		WARN_ON(cntlr_get_current_acs_params(r->radio_el, &cur_acs_params));
+		struct wifi_radio_opclass req_opclass = {};
+		struct acs_params *last_acs;
+		uint8_t ctrl_channel;
+
+		if (band && band != BAND_ANY && band != r->radio_el->band)
+			continue;
+
+		last_acs = cntlr_radio_get_last_acs(r);
+		if (!last_acs)
+			continue;
+
+		memset(&acs_params, 0, sizeof(acs_params));
 
-		/* Use current opclass - TODO: if no opclass check 80/40/20 */
-		acs_params.opclass = cur_acs_params.opclass;
-		acs_params.best_channel = cur_acs_params.best_channel;
+		/* Try best bandwidth */
+		if (highest_bandwidth && !bandwidth && !opclass) {
+			bw = wifi_opclass_highest_bandwidth(&r->radio_el->pref_opclass, prevent_cac);
+			cntlr_dbg(LOG_CHANNEL, "acs node channel recalc " MACFMT " radio " MACFMT " highest bandwidth %d\n",
+				  MAC2STR(node->almacaddr), MAC2STR(r->radio_el->macaddr), bw);
+		}
+
+		if (opclass) {
+			cntlr_get_current_acs_params(r->radio_el, &cur_acs_params);
+			acs_params.opclass = opclass;
+
+			if (cur_acs_params.opclass == opclass) {
+				acs_params.best_channel = cur_acs_params.best_channel;
+				acs_params.best_bw = cur_acs_params.best_bw;
+			}
+		} else if (bw) {
+			cntlr_get_current_acs_params(r->radio_el, &cur_acs_params);
+			acs_params.bw = bw;
 
-		ret = cntlr_acs_radio_channel_recalc(r->radio_el, &acs_params);
+			if (cur_acs_params.bw == bw) {
+				acs_params.best_channel = cur_acs_params.best_channel;
+				acs_params.best_bw = cur_acs_params.best_bw;
+			}
+		} else {
+			if (cntlr_get_current_acs_params(r->radio_el, &cur_acs_params)) {
+				cntlr_dbg(LOG_CHANNEL, "acs node " MACFMT " " MACFMT " - current channel not known\n",
+					  MAC2STR(node->almacaddr), MAC2STR(r->radio_el->macaddr));
+				continue;
+			}
+
+			acs_params.opclass = cur_acs_params.opclass;
+			acs_params.best_channel = cur_acs_params.best_channel;
+			acs_params.best_bw = cur_acs_params.best_bw;
+		}
+
+		ret = cntlr_acs_radio_channel_recalc(node, r, &acs_params);
 		if (WARN_ON(ret))
 			continue;
 
-		cntlr_dbg(LOG_CHANNEL, "acs node " MACFMT " radio " MACFMT " new %d/%d opclass %d vs old %d/%d opclass %d\n",
+		cntlr_trace(LOG_CHANNEL, "acs node " MACFMT " radio " MACFMT " new %d/%d opclass %d vs old %d/%d opclass %d cac_time %u\n",
 		    MAC2STR(node->almacaddr), MAC2STR(r->radio_el->macaddr), acs_params.best_channel,
 		    acs_params.best_bw, acs_params.best_opclass, cur_acs_params.best_channel,
-		    cur_acs_params.bw, cur_acs_params.opclass);
+		    cur_acs_params.bw, cur_acs_params.opclass, acs_params.best_cac_time);
+
+		/* Pick up control channel */
+		if (acs_params.best_bw > 40) {
+			ctrl_channel = cntrl_acs_radio_ctrl_channel(r->radio_el,
+							acs_params.best_opclass,
+							acs_params.best_channel);
+		} else {
+			ctrl_channel = acs_params.best_channel;
+		}
 
-		if (cntlr_acs_radio_is_bsta_connected(r))
+		/* Build opclass we would like to send */
+		memcpy(&req_opclass, &r->radio_el->supp_opclass, sizeof(req_opclass));
+		wifi_opclass_set_preferences(&req_opclass, 0x0);
+
+		if (wifi_radio_opclass_update_channel(&req_opclass,
+						      wifi_band_to_freqband(r->radio_el->band),
+						      ctrl_channel, acs_params.best_bw, 15)) {
+
+			cntlr_dbg(LOG_CHANNEL, "acs node " MACFMT " " MACFMT " - update opclass failed\n",
+				  MAC2STR(node->almacaddr), MAC2STR(r->radio_el->macaddr));
 			continue;
+		}
+
+		wifi_opclass_dump_ex(&req_opclass, "acs_selection", r->radio_el->macaddr, false);
 
-		warn("acs switch to best channel %d/%d\n", acs_params.best_channel, acs_params.best_bw);
 		ret = cntlr_send_channel_selection(node->cntlr, node->almacaddr,
-						   r->radio_el->macaddr,
-						   acs_params.best_channel,
-						   acs_params.best_opclass,
-						   14);
+						   r->radio_el->macaddr, &req_opclass);
+		if (ret == 0xffff) {
+			cntlr_warn(LOG_CHANNEL, "acs node " MACFMT " " MACFMT " send channel switch request failed\n",
+				   MAC2STR(node->almacaddr), MAC2STR(r->radio_el->macaddr));
+			continue;
+		}
 
-		if (ret)
-			warn("acs switch failed\n");
+		cntlr_warn(LOG_CHANNEL, "acs node " MACFMT " " MACFMT " switch to best channel %d/%d ctrl %d mid %u\n",
+			   MAC2STR(node->almacaddr), MAC2STR(r->radio_el->macaddr), acs_params.best_channel,
+			   acs_params.best_bw, ctrl_channel, ret);
+
+		last_acs->mid = ret;
+		last_acs->status = ACS_RECALC_STATUS_BEST_REQUESTED;
+		timestamp_update(&last_acs->entry_time);
 	}
 }
 
-void cntlr_acs_recalc(struct controller *c, bool skip_dfs)
+static void cntlr_radio_acs_timer(atimer_t *t)
+{
+	struct acs *acs = container_of(t, struct acs, acs_timer);
+	struct netif_radio *r = acs->radio;
+	struct controller *c = acs->cntlr;
+	struct node *n = acs->node;
+	struct acs_params *last_acs;
+
+	last_acs = cntlr_radio_get_last_acs(r);
+	if (!last_acs)
+		return;
+
+	if (cntlr_acs_radio_is_bsta_connected(c, r)) {
+		cntlr_dbg(LOG_CHANNEL, "acs timer recalc (bsta connected) " MACFMT " " MACFMT "\n",
+			  MAC2STR(n->almacaddr), MAC2STR(r->radio_el->macaddr));
+		last_acs->status = ACS_RECALC_STATUS_BSTA_CONNECTED;
+		timestamp_update(&last_acs->entry_time);
+		return;
+	}
+
+	if (last_acs->recalc) {
+		cntlr_dbg(LOG_CHANNEL, "acs timer recalc (recalc already set) " MACFMT " " MACFMT "\n",
+			  MAC2STR(n->almacaddr), MAC2STR(r->radio_el->macaddr));
+		return;
+	}
+
+	if (c->cfg.acs_scan_before_recalc == false) {
+		cntlr_dbg(LOG_CHANNEL, "acs timer recalc node (don't scan) " MACFMT " " MACFMT "\n",
+			  MAC2STR(n->almacaddr), MAC2STR(r->radio_el->macaddr));
+
+		last_acs->scan_mid = 0xffff;
+		last_acs->recalc = true;
+
+		/* Send node preference request */
+		cntlr_acs_send_pref_request(n);
+	} else {
+		cntlr_dbg(LOG_CHANNEL, "acs timer recalc node (scan) " MACFMT " " MACFMT "\n",
+			  MAC2STR(n->almacaddr), MAC2STR(r->radio_el->macaddr));
+
+		/* Send scan request */
+		last_acs->recalc = true;
+		last_acs->scan_mid = cntlr_acs_send_scan_request(n, r);
+		if (last_acs->scan_mid != 0xffff) {
+			last_acs->status = ACS_RECALC_STATUS_SCAN_REQUESTED;
+			timestamp_update(&last_acs->entry_time);
+		}
+	}
+}
+
+void cntlr_acs_recalc(struct controller *c)
 {
 	struct node *n = NULL;
+	uint32_t delay = 10;
 
+	/* Handle main timer - prevent scan/switch at same time */
 	list_for_each_entry(n, &c->nodelist, list) {
-		cntlr_acs_node_channel_recalc(n, skip_dfs);
+		struct netif_radio *r = NULL;
+
+		/* Run fresh scan before preference recalc */
+		list_for_each_entry(r, &n->radiolist, list) {
+			cntlr_dbg(LOG_CHANNEL, "acs setup recalc timer %u seconds " MACFMT " " MACFMT "\n",
+				  delay, MAC2STR(n->almacaddr), MAC2STR(r->radio_el->macaddr));
+
+			/* In the future check more params - possible recalc postpond */
+			cntlr_radio_acs_timer_set(r, delay * 1000);
+			delay += 60;
+		}
 	}
 }
 
+
+/* DFS background CAC cleanup */
 static uint8_t cntlr_dfs_cac_method(uint32_t methods)
 {
 	uint8_t cac_method = 0;
@@ -384,7 +1097,27 @@ static uint8_t cntlr_dfs_cac_method(uint32_t methods)
 	return cac_method;
 }
 
-static bool cntlr_dfs_get_usable(struct wifi_radio_opclass_entry *entry, struct cac_data *cac_data)
+static bool cntlr_dfs_cac_ongoing(struct wifi_radio_element *radio)
+{
+	struct wifi_radio_opclass_entry *entry;
+	struct wifi_radio_opclass *opclass;
+	int i, j;
+
+	opclass = &radio->pref_opclass;
+
+	for (i = 0; i < opclass->num_opclass; i++) {
+		entry = &opclass->opclass[i];
+
+		for (j = 0; j < entry->num_channel; j++) {
+			if (entry->channel[j].dfs == WIFI_RADIO_OPCLASS_CHANNEL_DFS_CAC)
+				return true;
+		}
+	}
+
+	return false;
+}
+
+static bool cntlr_dfs_get_usable(struct wifi_radio_opclass_entry *entry, struct acs_cac_data *cac)
 {
 	uint8_t reas;
 	int i;
@@ -395,12 +1128,14 @@ static bool cntlr_dfs_get_usable(struct wifi_radio_opclass_entry *entry, struct
 		if (reas != CHANNEL_PREF_REASON_DFS_USABLE)
 			continue;
 
-		cac_data->cac_method = cntlr_dfs_cac_method(entry->channel[i].cac_methods);
-		if (!cac_data->cac_method)
+		/* Check if background CAC supported */
+		cac->data.cac_method = cntlr_dfs_cac_method(entry->channel[i].cac_methods);
+		if (!cac->data.cac_method)
 			continue;
 
-		cac_data->channel = entry->channel[i].channel;
-		cac_data->opclass = entry->id;
+		cac->data.channel = entry->channel[i].channel;
+		cac->data.opclass = entry->id;
+		cac->cac_time = entry->channel[i].cac_time;
 		return true;
 	}
 
@@ -422,35 +1157,40 @@ static bool cntlr_radio_is_ap_iface(struct netif_radio *radio)
 	return false;
 }
 
-static bool cntlr_dfs_get_cac_data(struct wifi_radio_element *radio, struct cac_data *cac_data)
+static bool cntlr_dfs_get_cac_data(struct wifi_radio_element *radio, struct acs_cac_data *cac, uint16_t bw)
 {
 	struct wifi_radio_opclass_entry *entry;
 	struct wifi_radio_opclass *opclass;
+	uint32_t cur_bw = 0;
+	uint32_t cur_chan = 0;
 	int i;
 
-	/* TODO check dfs_region early - skip non EU */
+	if (!bw) {
+		cur_bw = ctrl_radio_cur_opclass_max_bw(radio);
+		cur_chan = ctrl_radio_cur_opclass_chan(radio);
+	} else {
+		cur_bw = bw;
+		cur_chan = 0;
+	}
+
+	if (!cur_bw)
+		return false;
 
 	opclass = &radio->pref_opclass;
 
 	for (i = 0; i < opclass->num_opclass; i++) {
 		entry = &opclass->opclass[i];
-		uint32_t cur_bw = 0;
-		uint32_t cur_ctrl_chan = 0;
-
-		cur_bw = ctrl_radio_cur_opclass_max_bw(radio);
-		cur_ctrl_chan = ctrl_radio_cur_opclass_ctrl_chan(radio);
 
 		if (entry->bandwidth != cur_bw)
 			continue;
 
-		if (!cntlr_dfs_get_usable(entry, cac_data))
+		if (!cntlr_dfs_get_usable(entry, cac))
 			continue;
 
-		/* TODO check chan/bw - not only control channel */
-		if (cac_data->channel == cur_ctrl_chan)
+		if (cac->data.channel == cur_chan)
 			continue;
 
-		memcpy(cac_data->radio, radio->macaddr, sizeof(cac_data->radio));
+		memcpy(cac->data.radio, radio->macaddr, sizeof(cac->data.radio));
 
 		return true;
 	}
@@ -460,30 +1200,82 @@ static bool cntlr_dfs_get_cac_data(struct wifi_radio_element *radio, struct cac_
 
 void cntlr_dfs_radio_cleanup(struct node *node, struct netif_radio *radio)
 {
-	struct cac_data cac_data = {};
+	struct acs_cac_data cac_data = {};
+	struct acs_cac_data *last_cac_data;
+
+	last_cac_data = cntlr_radio_get_last_cac(radio);
+	if (!last_cac_data)
+		return;
+
+	if (!radio->radio_el->pref_opclass.num_opclass ||
+	    !radio->radio_el->cur_opclass.num_opclass) {
+		/* We don't know preferences and channels DFS state */
+		cntlr_dbg(LOG_CHANNEL, "dfs radio preCAC not yet " MACFMT "\n", MAC2STR(radio->radio_el->macaddr));
+		timestamp_update(&last_cac_data->entry_time);
+		last_cac_data->status = ACS_CLEANUP_STATUS_SKIPPED;
+		return;
+	}
+
+	if (!radio->radio_el->bgcac_supported) {
+		/* Some nodes don't even support it */
+		cntlr_dbg(LOG_CHANNEL, "dfs radio preCAC not supported " MACFMT "\n", MAC2STR(radio->radio_el->macaddr));
+		timestamp_update(&last_cac_data->entry_time);
+		last_cac_data->status = ACS_CLEANUP_STATUS_NOT_SUPPORTED;
+		return;
+	}
 
 	cntlr_dbg(LOG_CHANNEL, "dfs radio preCAC cleanup " MACFMT "\n", MAC2STR(radio->radio_el->macaddr));
 	if (!cntlr_radio_is_ap_iface(radio)) {
+		/* Wait controller get proper AP list */
 		cntlr_dbg(LOG_CHANNEL, "dfs radio preCAC no AP ifaces, skip radio\n");
+		timestamp_update(&last_cac_data->entry_time);
+		last_cac_data->status = ACS_CLEANUP_STATUS_NO_APS;
 		return;
 	}
 
-	if (!cntlr_dfs_get_cac_data(radio->radio_el, &cac_data)) {
-		cntlr_dbg(LOG_CHANNEL, "dfs radio preCAC cleanup no channels left\n");
+	/* So far keep it simple - don't start new CAC request if one ongoing */
+	if (cntlr_dfs_cac_ongoing(radio->radio_el)) {
+		/* Could be device run own background CAC */
+		cntlr_dbg(LOG_CHANNEL, "dfs radio CAC ongoing " MACFMT "\n", MAC2STR(radio->radio_el->macaddr));
+		timestamp_update(&last_cac_data->entry_time);
+		last_cac_data->status = ACS_CLEANUP_STATUS_ALREADY_ONGOING;
 		return;
 	}
 
-	cntlr_dbg(LOG_CHANNEL, "dfs radio preCAC run chan %d opclass %d\n", cac_data.channel, cac_data.opclass);
-	WARN_ON(cntlr_send_cac_req(node->cntlr, node->almacaddr, 1, &cac_data));
+	if (!cntlr_dfs_get_cac_data(radio->radio_el, &cac_data, 0)) {
+		/* First use current bandwidth for clearing. Finally switch to 20MHz for left channels */
+		cntlr_dbg(LOG_CHANNEL, "dfs radio preCAC cleanup no channels left check 20MHz\n");
+		if (!cntlr_dfs_get_cac_data(radio->radio_el, &cac_data, 20)) {
+			cntlr_dbg(LOG_CHANNEL, "dfs radio preCAC cleanup no channels left\n");
+			timestamp_update(&last_cac_data->entry_time);
+			last_cac_data->status = ACS_CLEANUP_STATUS_ALL_CLEAN;
+			return;
+		}
+	}
+
+	cntlr_warn(LOG_CHANNEL, "dfs node " MACFMT " radio " MACFMT " preCAC run chan %d opclass %d\n",
+		   MAC2STR(node->almacaddr), MAC2STR(radio->radio_el->macaddr),
+		   cac_data.data.channel, cac_data.data.opclass);
+	memcpy(last_cac_data, &cac_data, sizeof(*last_cac_data));
+	timestamp_update(&last_cac_data->entry_time);
+	last_cac_data->status = ACS_CLEANUP_STATUS_SELECTED;
+
+	cac_data.mid = cntlr_send_cac_req(node->cntlr, node->almacaddr, 1, &cac_data.data);
+
+	last_cac_data->mid = cac_data.mid;
+	if (cac_data.mid != 0xffff)
+		last_cac_data->status = ACS_CLEANUP_STATUS_REQUESTED;
 }
 
 void cntlr_dfs_node_cleanup(struct node *node)
 {
 	struct netif_radio *radio = NULL;
 
-	cntlr_dbg(LOG_CHANNEL, "dfs node preCAC cleanup " MACFMT "\n", MAC2STR(node->almacaddr));
+	cntlr_trace(LOG_CHANNEL, "dfs node preCAC cleanup " MACFMT "\n", MAC2STR(node->almacaddr));
 
 	list_for_each_entry(radio, &node->radiolist, list) {
+		if (radio->radio_el->band != BAND_5)
+			continue;
 		cntlr_dfs_radio_cleanup(node, radio);
 	}
 }
@@ -496,3 +1288,360 @@ void cntlr_dfs_cleanup(struct controller *c)
 		cntlr_dfs_node_cleanup(n);
 	}
 }
+
+/* CMDU callbacks we expect to receive from nodes */
+void cntlr_acs_channel_sel_response(struct netif_radio *r, uint16_t mid, uint8_t status)
+{
+	struct acs_params *acs;
+
+	cntlr_dbg(LOG_CHANNEL, "new channel selection response radio " MACFMT " mid %u status %u\n",
+		  MAC2STR(r->radio_el->macaddr), mid, status);
+
+	acs = cntlr_radio_get_last_acs(r);
+	if (!acs)
+		return;
+
+	if (acs->status != ACS_RECALC_STATUS_BEST_REQUESTED)
+		return;
+
+	if (acs->mid != mid)
+		return;
+
+	if (status == 0)
+		acs->status = ACS_RECALC_STATUS_BEST_ACCEPTED;
+	else
+		acs->status = ACS_RECALC_STATUS_BEST_REJECTED;
+
+	timestamp_update(&acs->entry_time);
+}
+
+void cntlr_acs_oper_channel_report(struct netif_radio *r)
+{
+	struct acs_params *acs;
+	struct wifi_radio_opclass_entry *entry;
+	struct wifi_radio_opclass *cur;
+	int i, j;
+
+	cntlr_dbg(LOG_CHANNEL, "new oper channel report radio " MACFMT " %u/%u\n",
+		  MAC2STR(r->radio_el->macaddr),
+		  ctrl_radio_cur_opclass_ctrl_chan(r->radio_el),
+		  ctrl_radio_cur_opclass_max_bw(r->radio_el));
+
+	acs = cntlr_radio_get_last_acs(r);
+	if (!acs)
+		return;
+
+	if (acs->status != ACS_RECALC_STATUS_BEST_REQUESTED &&
+	    acs->status != ACS_RECALC_STATUS_BEST_ACCEPTED)
+		return;
+
+	cur = &r->radio_el->cur_opclass;
+	for (i = 0; i < cur->num_opclass; i++) {
+		entry = &cur->opclass[i];
+
+		if (entry->bandwidth != acs->best_bw)
+			continue;
+
+		for (j = 0; j < entry->num_channel; j++) {
+			if (entry->channel[j].channel == acs->best_channel) {
+				acs->status = ACS_RECALC_STATUS_BEST_SET;
+				timestamp_update(&acs->entry_time);
+				return;
+			}
+		}
+	}
+
+	/* timeout check - worst case when continous CAC required */
+	if (timestamp_elapsed_sec(&acs->entry_time) > 30 + acs->best_cac_time) {
+		acs->status = ACS_RECALC_STATUS_BEST_SET_TIMEOUT;
+		timestamp_update(&acs->entry_time);
+	}
+}
+
+void cntlr_acs_cac_completion(struct netif_radio *r, uint8_t classid,
+			      uint8_t channel, uint8_t status)
+{
+	struct acs_params *acs;
+	struct acs_cac_data *cac;
+
+	cntlr_dbg(LOG_CHANNEL, "new cac completion radio " MACFMT " opclass %u channel %u status %u\n",
+		  MAC2STR(r->radio_el->macaddr), classid, channel, status);
+
+	cac = cntlr_radio_get_last_cac(r);
+	if (!cac)
+		return;
+
+	acs = cntlr_radio_get_last_acs(r);
+
+	if (cac->status != ACS_CLEANUP_STATUS_REQUESTED)
+		return;
+
+	if (cac->data.channel != channel)
+		return;
+	if (cac->data.opclass != classid)
+		return;
+
+
+	switch (status) {
+	case CAC_COMP_REPORT_STATUS_SUCCESSFUL:
+		cac->status = ACS_CLEANUP_STATUS_DONE;
+
+		/* Kick ACS recalc - we have new available channel */
+		if (acs && acs->scan_mid == 0xffff)
+			cntlr_radio_acs_timer_set(r, 5 * 1000);
+		break;
+	case CAC_COMP_REPORT_STATUS_CAC_NOT_SUPPORTED:
+	case CAC_COMP_REPORT_STATUS_TOO_BUSY:
+	case CAC_COMP_REPORT_STATUS_NON_CONFORMANT:
+	case CAC_COMP_REPORT_STATUS_OTHER:
+		cac->status = ACS_CLEANUP_STATUS_REJECTED;
+		break;
+	case CAC_COMP_REPORT_STATUS_RADAR_DETECTED:
+		cac->status = ACS_CLEANUP_STATUS_RADAR;
+		break;
+	default:
+		break;
+	}
+}
+
+void cntlr_acs_channel_pref_report(struct node *n, struct netif_radio *r)
+{
+	struct acs_params *acs;
+	struct acs_cac_data *cac;
+
+	cntlr_dbg(LOG_CHANNEL, "new pref report node " MACFMT " radio " MACFMT "\n",
+		  MAC2STR(n->almacaddr), MAC2STR(r->radio_el->macaddr));
+
+	cac = cntlr_radio_get_last_cac(r);
+	if (!cac)
+		return;
+
+	acs = cntlr_radio_get_last_acs(r);
+	if (!acs)
+		return;
+
+	/* ACS status update */
+	if (acs->status == ACS_RECALC_STATUS_BEST_REQUESTED ||
+	    acs->status == ACS_RECALC_STATUS_BEST_ACCEPTED) {
+		struct wifi_radio_opclass_channel *chan;
+
+		chan = wifi_opclass_get_channel(&r->radio_el->pref_opclass,
+						acs->best_opclass,
+						acs->best_channel);
+		if (chan) {
+			switch (chan->dfs) {
+			case WIFI_RADIO_OPCLASS_CHANNEL_DFS_CAC:
+				acs->status = ACS_RECALC_STATUS_BEST_CAC;
+				timestamp_update(&acs->entry_time);
+				break;
+			case WIFI_RADIO_OPCLASS_CHANNEL_DFS_NOP:
+				acs->status = ACS_RECALC_STATUS_BEST_CAC_RADAR;
+				timestamp_update(&acs->entry_time);
+				break;
+			default:
+				break;
+			}
+		}
+	}
+
+	/* Background CAC status update */
+	if (cac->status == ACS_CLEANUP_STATUS_REQUESTED ||
+	    cac->status == ACS_CLEANUP_STATUS_ACCEPTED ||
+	    cac->status == ACS_CLEANUP_STATUS_TIMEOUT) {
+		struct wifi_radio_opclass_channel *chan;
+
+		chan = wifi_opclass_get_channel(&r->radio_el->pref_opclass,
+						cac->data.opclass,
+						cac->data.channel);
+		if (chan) {
+			switch (chan->dfs) {
+			case WIFI_RADIO_OPCLASS_CHANNEL_DFS_AVAILABLE:
+				cac->status = ACS_CLEANUP_STATUS_DONE;
+				break;
+			case WIFI_RADIO_OPCLASS_CHANNEL_DFS_CAC:
+				cac->status = ACS_CLEANUP_STATUS_ACCEPTED;
+				break;
+			case WIFI_RADIO_OPCLASS_CHANNEL_DFS_NOP:
+				cac->status = ACS_CLEANUP_STATUS_RADAR;
+				break;
+			case WIFI_RADIO_OPCLASS_CHANNEL_DFS_USABLE:
+				/* change CAC -> USABLE - abort possible */
+				if (cac->status == ACS_CLEANUP_STATUS_ACCEPTED)
+					cac->status = ACS_CLEANUP_STATUS_REJECTED;
+				if (timestamp_elapsed_sec(&cac->entry_time) > 30 + cac->cac_time)
+					cac->status = ACS_CLEANUP_STATUS_TIMEOUT;
+				break;
+			default:
+				break;
+			}
+		}
+	}
+
+	/* Kick CAC background clearing */
+	if (n->cntlr->cfg.dfs_cleanup &&
+	    r->radio_el->band == BAND_5 &&
+	    r->radio_el->bgcac_supported &&
+	    cac->status != ACS_CLEANUP_STATUS_REQUESTED &&
+	    cac->status != ACS_CLEANUP_STATUS_ACCEPTED) {
+		cntlr_dbg(LOG_CHANNEL, "kick dfs cleanup " MACFMT " radio " MACFMT "\n",
+			  MAC2STR(n->almacaddr), MAC2STR(r->radio_el->macaddr));
+
+		cntlr_dfs_radio_cleanup(n, r);
+	}
+
+	/* Kick ACS recalc code */
+	if (acs->recalc && n->cntlr->cfg.acs && acs->scan_mid == 0xffff) {
+		cntlr_dbg(LOG_CHANNEL, "kick acs reclac " MACFMT " radio " MACFMT "\n",
+			  MAC2STR(n->almacaddr), MAC2STR(r->radio_el->macaddr));
+
+		acs->recalc = false;
+		cntlr_acs_node_channel_recalc(n, r->radio_el->band, 0, 0,
+				n->cntlr->cfg.acs_skip_dfs,
+				n->cntlr->cfg.acs_prevent_cac,
+				n->cntlr->cfg.acs_highest_bandwidth);
+	}
+}
+
+static uint8_t ewma(uint8_t cur, uint8_t prev, uint8_t alpha)
+{
+	uint8_t new;
+
+	if (alpha > 100)
+		alpha = 100;
+
+	new = (alpha * cur)/100 + ((100 - alpha) * prev) / 100;
+
+	return new;
+}
+
+static bool cntlr_acs_radio_add_and_check_metrics(struct netif_radio *r, uint8_t anpi, uint8_t obss)
+{
+	struct acs_radio_metrics *m;
+	struct acs_radio_metrics_entry *last, *next;
+
+	m = cntlr_radio_get_metrics(r);
+	if (!m)
+		return false;
+
+	timestamp_update(&m->time);
+	m->anpi = ewma(anpi, m->anpi, 60);
+	m->obss = ewma(obss, m->obss, 60);
+
+	last = &m->entry[m->idx];
+
+	cntlr_trace(LOG_CHANNEL, "radio ewma metrics " MACFMT " anpi %u obss %u\n",
+		    MAC2STR(r->radio_el->macaddr), m->anpi, m->obss);
+
+	/* Update ring buffer */
+	if (timestamp_invalid(&last->time)) {
+		timestamp_update(&last->time);
+		last->anpi = m->anpi;
+		last->obss = m->obss;
+	}
+
+	/* Save/check data to ring buffer each 300 seconds */
+	if (timestamp_elapsed_sec(&last->time) >= 300) {
+		m->idx++;
+		m->idx %= ARRAY_SIZE(m->entry);
+
+		next = &m->entry[m->idx];
+
+		timestamp_update(&next->time);
+		next->anpi = m->anpi;
+		next->obss = m->obss;
+
+		cntlr_dbg(LOG_CHANNEL, "metrics[%u] radio " MACFMT " anpi %u (%d) obss %u (%d)\n",
+			  m->idx, MAC2STR(r->radio_el->macaddr), next->anpi,
+			  next->anpi - last->anpi, next->obss, next->obss - last->obss);
+
+		/* If other BSS busy increase - worth to check it (100% == 255) */
+		if (next->obss - last->obss > 30)
+			return true;
+
+		/* If noise level worst - worth to check it (0.5 dBm) */
+		if (next->anpi - last->anpi > 20)
+			return true;
+	}
+
+	return false;
+}
+
+void cntlr_acs_radio_metrics(struct node *n, struct netif_radio *r,
+			     uint8_t anpi, uint8_t obss)
+{
+	struct acs_params *acs;
+	struct acs_cac_data *cac;
+
+	cntlr_trace(LOG_CHANNEL, "new radio metrics node " MACFMT " radio " MACFMT " anpi %u busy %u\n",
+		    MAC2STR(n->almacaddr), MAC2STR(r->radio_el->macaddr), anpi, obss);
+
+	cac = cntlr_radio_get_last_cac(r);
+	if (!cac)
+		return;
+
+	acs = cntlr_radio_get_last_acs(r);
+	if (!acs)
+		return;
+
+	/* Kick CAC background clearing - check AP apears */
+	if (n->cntlr->cfg.dfs_cleanup &&
+	    r->radio_el->band == BAND_5 &&
+	    r->radio_el->bgcac_supported &&
+	    cac->status == ACS_CLEANUP_STATUS_NO_APS &&
+	    timestamp_elapsed_sec(&cac->entry_time) >= 30) {
+		cntlr_dbg(LOG_CHANNEL, "kick dfs cleanup (NO_APS before) " MACFMT " radio " MACFMT "\n",
+			  MAC2STR(n->almacaddr), MAC2STR(r->radio_el->macaddr));
+		cntlr_dfs_radio_cleanup(n, r);
+	}
+
+	if (!n->cntlr->cfg.acs)
+		return;
+
+	/* Store data for future use/compare */
+	if (!cntlr_acs_radio_add_and_check_metrics(r, anpi, obss))
+		return;
+
+	/* Smth changed in current channel noise/busy */
+	cntlr_dbg(LOG_CHANNEL, "issue ACS recalc due to metrics (request fresh pref) " MACFMT " radio " MACFMT "\n",
+		  MAC2STR(n->almacaddr), MAC2STR(r->radio_el->macaddr));
+
+	/* Trigger scan/pref_req and get fresh preference report */
+	cntlr_radio_acs_timer_set(r, 5 * 1000);
+}
+
+void cntlr_acs_scan_report(struct node *n, struct netif_radio *r, uint16_t mid)
+{
+	struct acs_params *acs;
+
+	cntlr_dbg(LOG_CHANNEL, "new scan report node " MACFMT " radio " MACFMT " mid %u\n",
+		  MAC2STR(n->almacaddr), MAC2STR(r->radio_el->macaddr), mid);
+
+	acs = cntlr_radio_get_last_acs(r);
+	if (!acs)
+		return;
+
+	if (!n->cntlr->cfg.acs)
+		return;
+
+	/* Scan done for this radio, safe to switch channel */
+	if (acs->recalc && acs->scan_mid != 0xffff) {
+		acs->status = ACS_RECALC_STATUS_SCAN_DONE;
+		timestamp_update(&acs->entry_time);
+	}
+
+	acs->scan_mid = 0xffff;
+
+	/* Request fresh preferences if required */
+	if (cntlr_node_pref_opclass_expired(n, 10) || acs->recalc)
+		/* Preference responose with new data will trigger actions */
+		cntlr_acs_send_pref_request(n);
+}
+
+void cntlr_acs_dev_supp_opclass(struct node *n, struct netif_radio *r)
+{
+	cntlr_trace(LOG_CHANNEL, "new dev supp opclass " MACFMT " radio " MACFMT "\n",
+		    MAC2STR(n->almacaddr), MAC2STR(r->radio_el->macaddr));
+
+	if (r->radio_el->pref_opclass.num_opclass == 0)
+		cntlr_acs_send_pref_request(n);
+}
diff --git a/src/acs.h b/src/acs.h
index 8dc7a024..9cac6fea 100644
--- a/src/acs.h
+++ b/src/acs.h
@@ -13,29 +13,48 @@
 struct controller;
 struct node;
 struct wifi_radio_element;
+struct netif_radio;
+enum wifi_band;
+enum wifi_radio_opclass_dfs;
+struct blob_buf;
 
+/* Alloc free private storage */
+void *cntlr_radio_acs_alloc(struct controller *c, struct node *n, struct netif_radio *r);
+void cntlr_radio_acs_free(void *acs);
 
-struct acs_params {
-	int opclass;
-	int bw;
+/* Format UBUS output */
+void cntlr_acs_node_info(struct node *node, enum wifi_band band, struct blob_buf *bb);
+void cntlr_acs_radio_info(struct blob_buf *bb, struct netif_radio *r);
+void cntlr_dfs_radio_cleanup_info(struct node *n, struct netif_radio *r, struct blob_buf *bb);
+void cntlr_acs_radio_cleanup_info(struct blob_buf *bb, struct netif_radio *r);
 
-	bool skip_dfs;
-	bool skip_dfs_not_available;
+/* CMDU info/event for ACS */
+void cntlr_acs_channel_sel_response(struct netif_radio *r, uint16_t mid, uint8_t status);
+void cntlr_acs_oper_channel_report(struct netif_radio *r);
+void cntlr_acs_cac_completion(struct netif_radio *r, uint8_t classid,
+			      uint8_t channel, uint8_t status);
+void cntlr_acs_channel_pref_report(struct node *n, struct netif_radio *r);
+void cntlr_acs_radio_metrics(struct node *n, struct netif_radio *r,
+			     uint8_t anpi, uint8_t obss);
+void cntlr_acs_scan_report(struct node *n, struct netif_radio *r, uint16_t mid);
+void cntlr_acs_dev_supp_opclass(struct node *n, struct netif_radio *r);
 
-	int best_channel;
-	int best_opclass;
-	int best_bw;
-	int best_pref;
-};
+/* Generic requests */
+void cntlr_acs_recalc(struct controller *c);
+void cntlr_acs_node_channel_recalc(struct node *node, enum wifi_band band,
+				   uint8_t opclass, uint32_t bandwidth,
+				   bool skip_dfs, bool prevent_cac,
+				   bool highest_bandwidth);
 
-int cntlr_acs_radio_channel_recalc(struct wifi_radio_element *radio, struct acs_params *params);
-void cntlr_acs_node_channel_recalc(struct node *node, bool skip_dfs);
-void cntlr_dfs_node_cleanup(struct node *node);
-void cntlr_acs_recalc(struct controller *c, bool skip_dfs);
 void cntlr_dfs_cleanup(struct controller *c);
+void cntlr_dfs_node_cleanup(struct node *node);
+void cntlr_dfs_radio_cleanup(struct node *node, struct netif_radio *radio);
 
+/* Opclass helpers */
 int cntlr_radio_pref_opclass_add(struct wifi_radio_element *radio, uint8_t classid,
 				 uint8_t channel, uint8_t preference);
+int cntlr_radio_pref_opclass_set_dfs_status(struct wifi_radio_element *radio, uint8_t classid,
+					    uint8_t channel, enum wifi_radio_opclass_dfs state);
 int cntlr_radio_pref_opclass_reset(struct wifi_radio_element *radio);
 void cntlr_radio_pref_opclass_dump(struct wifi_radio_element *radio);
 
@@ -43,10 +62,9 @@ int cntlr_radio_cur_opclass_add(struct wifi_radio_element *radio, uint8_t opclas
 				uint8_t channel, uint8_t txpower);
 void cntlr_radio_cur_opclass_reset(struct wifi_radio_element *radio);
 void cntlr_radio_cur_opclass_dump(struct wifi_radio_element *radio);
-bool cntlr_node_pref_opclass_expired(struct node *node);
+bool cntlr_node_pref_opclass_expired(struct node *node, int age);
 void cntlr_radio_pref_opclass_set_pref(struct wifi_radio_element *radio, uint8_t id, uint8_t preference);
 uint8_t ctrl_radio_cur_opclass_id(struct wifi_radio_element *radio);
 uint8_t ctrl_radio_cur_opclass_ctrl_chan(struct wifi_radio_element *radio);
-uint8_t ctrl_radio_cur_opclass_max_bw(struct wifi_radio_element *radio);
-
+uint16_t ctrl_radio_cur_opclass_max_bw(struct wifi_radio_element *radio);
 #endif
diff --git a/src/cntlr.c b/src/cntlr.c
index c0f6f7cc..f40e635b 100644
--- a/src/cntlr.c
+++ b/src/cntlr.c
@@ -639,15 +639,15 @@ struct netif_iface *cntlr_radio_add_iface(struct controller *c,
 
 #if (EASYMESH_VERSION >= 6)
 	/* send channel selection request to propagate puncture bitmap */
-	cntlr_send_channel_selection(c, r->agent->almacaddr, r->radio_el->macaddr,
-				     0, 0, 0);
+	cntlr_send_channel_selection(c, r->agent->almacaddr, r->radio_el->macaddr, NULL);
 #endif
 	mactable_add_entry(c->mac_table, macaddr, MAC_ENTRY_FBSS, (void *)n);
 
 	return n;
 }
 
-static struct wifi_radio_element *cntlr_alloc_wifi_radio(struct controller *c)
+static struct wifi_radio_element *cntlr_alloc_wifi_radio(struct controller *c, struct node *n,
+							 struct netif_radio *r)
 {
 	struct wifi_radio_element *radio_el = NULL;
 
@@ -656,6 +656,7 @@ static struct wifi_radio_element *cntlr_alloc_wifi_radio(struct controller *c)
 		return NULL;
 
 	INIT_LIST_HEAD(&radio_el->scanlist);
+	radio_el->acs = cntlr_radio_acs_alloc(c, n, r);
 
 	return radio_el;
 }
@@ -674,7 +675,7 @@ struct netif_radio *cntlr_node_add_radio(struct controller *c, struct node *n,
 	if (!r)
 		return NULL;
 
-	r->radio_el = cntlr_alloc_wifi_radio(c);
+	r->radio_el = cntlr_alloc_wifi_radio(c, n, r);
 	if (!r->radio_el) {
 		free(r);
 		return NULL;
@@ -688,7 +689,7 @@ struct netif_radio *cntlr_node_add_radio(struct controller *c, struct node *n,
 
 	mactable_add_entry(c->mac_table, macaddr, MAC_ENTRY_RADIO, (void *)r);
 
-	cntlr_send_channel_selection(c, n->almacaddr, macaddr, 0, 0, 0);
+	cntlr_send_channel_selection(c, n->almacaddr, macaddr, NULL);
 
 	p = cntlr_get_radio_policy(&c->cfg, macaddr);
 	if (p)
@@ -851,6 +852,9 @@ static void radio_clean_radio_el(struct netif_radio *r)
 	list_for_each_entry_safe(b, tmp, &r->radio_el->scanlist, list) {
 		cntlr_radio_clean_scanlist_el(b);
 	}
+
+	cntlr_radio_acs_free(r->radio_el->acs);
+	r->radio_el->acs = NULL;
 	free(r->radio_el);
 }
 
@@ -1449,28 +1453,15 @@ static void cntlr_metric_collection(struct controller *c)
 static void cntlr_acs_run(atimer_t *t)
 {
 	struct controller *c = container_of(t, struct controller, acs);
-	bool skip_dfs = false;
 
 	/* Run ACS recalc here */
 	dbg("acs timeout - run recalc\n");
-	cntlr_acs_recalc(c, skip_dfs);
+	cntlr_acs_recalc(c);
 
-	if (c->cfg.acs_timeout)
+	if (c->cfg.acs && c->cfg.acs_timeout)
 		timer_set(&c->acs, c->cfg.acs_timeout * 1000);
 }
 
-static void cntlr_dfs_cleanup_run(atimer_t *t)
-{
-	struct controller *c = container_of(t, struct controller, dfs_cleanup);
-
-	/* Run background CAC here */
-	dbg("dfs bgcac timeout - run cleanup\n");
-	cntlr_dfs_cleanup(c);
-
-	if (c->cfg.dfs_cleanup_timeout)
-		timer_set(&c->dfs_cleanup, c->cfg.dfs_cleanup_timeout * 1000);
-}
-
 static void cntlr_steer_sched_run(atimer_t *t)
 {
 	struct controller *c = container_of(t, struct controller, steer_sched_timer);
@@ -1878,7 +1869,6 @@ void run_controller(void *opts)
 	timer_init(&c->signal_handler, cntlr_signal_periodic_run);
 	timer_init(&c->query_nodes, cntlr_query_nodes);
 	timer_init(&c->acs, cntlr_acs_run);
-	timer_init(&c->dfs_cleanup, cntlr_dfs_cleanup_run);
 	timer_init(&c->steer_sched_timer, cntlr_steer_sched_run);
 
 	timer_set(&c->heartbeat, 1 * 1000);
@@ -1887,10 +1877,8 @@ void run_controller(void *opts)
 	timer_set(&c->signal_handler, 5 * 1000);
 	timer_set(&c->query_nodes, 60 * 1000);
 
-	if (c->cfg.acs_timeout)
+	if (c->cfg.acs && c->cfg.acs_timeout)
 		timer_set(&c->acs, c->cfg.acs_timeout * 1000);
-	if (c->cfg.dfs_cleanup_timeout)
-		timer_set(&c->dfs_cleanup, c->cfg.dfs_cleanup_timeout * 1000);
 
 	c->evh.cb = cntlr_event_handler;
 	ubus_register_event_handler(ctx, &c->evh, "ubus.object.*");
diff --git a/src/cntlr.h b/src/cntlr.h
index bdbf6729..a7ea588a 100644
--- a/src/cntlr.h
+++ b/src/cntlr.h
@@ -291,7 +291,6 @@ struct controller {
 	struct ubus_event_handler evh;
 	atimer_t heartbeat;
 	atimer_t acs;
-	atimer_t dfs_cleanup;
 	int num_nodes;
 	int num_tx_links;
 	int num_rx_links;
diff --git a/src/cntlr_cmdu.c b/src/cntlr_cmdu.c
index 0f8a61ca..34f90be3 100644
--- a/src/cntlr_cmdu.c
+++ b/src/cntlr_cmdu.c
@@ -901,46 +901,36 @@ int cntlr_send_channel_preference_query(struct controller *c, uint8_t *agent)
 	return 0;
 }
 
-int cntlr_send_channel_selection(struct controller *c, uint8_t *agent, uint8_t *radio,
-				 uint8_t channel, uint8_t opclass, uint8_t pref)
+uint16_t cntlr_send_channel_selection(struct controller *c, uint8_t *agent, uint8_t *radio,
+				      struct wifi_radio_opclass *opclass)
 {
-	uint8_t chanlist[1] = {};
+	struct cmdu_buff *cmdu;
+	uint16_t mid;
 #if (EASYMESH_VERSION >= 6)
 	struct node *n;
+	int ret;
 #endif
-	struct cmdu_buff *cmdu;
-	uint16_t mid = 0;
-	int ret = 0;
-
-	chanlist[0] = channel;
 
-	cmdu = cmdu_alloc_simple(CMDU_CHANNEL_SELECTION_REQ, &mid);
+	cmdu = cntlr_gen_channel_sel_request(c, agent, radio, opclass);
 	if (!cmdu)
 		return -1;
 
-	memcpy(cmdu->origin, agent, 6);
-	ret = cntlr_gen_channel_pref(c, cmdu, radio, opclass, channel ?  ARRAY_SIZE(chanlist) : 0, chanlist, pref);
-	if (ret) {
-		cmdu_free(cmdu);
-		return -1;
-	}
-
 #if (EASYMESH_VERSION >= 6)
 	n = cntlr_find_node(c, agent);
 	if (n) {
 		ret = cntlr_gen_eht_operations(c, cmdu, n);
 		if (ret) {
 			cmdu_free(cmdu);
-			return -1;
+			return 0xffff;
 		}
 	}
 #endif
 
 	cmdu_put_eom(cmdu);
-	send_cmdu(c, cmdu);
+	mid = send_cmdu(c, cmdu);
 	cmdu_free(cmdu);
 
-	return 0;
+	return mid;
 }
 
 struct cmdu_buff* cntlr_gen_cac_req(struct controller *c, uint8_t *agent,
@@ -1012,19 +1002,20 @@ int cntlr_send_channel_scan_request(struct controller *c, uint8_t *agent_mac,
 	return 0;
 }
 
-int cntlr_send_cac_req(struct controller *c, uint8_t *agent,
-		       int num_data, struct cac_data *data)
+uint16_t cntlr_send_cac_req(struct controller *c, uint8_t *agent,
+			    int num_data, struct cac_data *data)
 {
 	struct cmdu_buff *cmdu;
+	uint16_t mid = 0xffff;
 
 	cmdu = cntlr_gen_cac_req(c, agent, num_data, data);
 	if (!cmdu)
-		return -1;
+		return mid;
 
-	send_cmdu(c, cmdu);
+	mid = send_cmdu(c, cmdu);
 	cmdu_free(cmdu);
 
-	return 0;
+	return mid;
 }
 
 int cntlr_send_cac_term(struct controller *c, uint8_t *agent,
diff --git a/src/cntlr_cmdu.h b/src/cntlr_cmdu.h
index fbbf88dc..fd1919a7 100644
--- a/src/cntlr_cmdu.h
+++ b/src/cntlr_cmdu.h
@@ -24,6 +24,7 @@ struct sta_error_response;
 struct tlv;
 struct unassoc_sta_metric;
 struct wifi7_radio_capabilities;
+struct wifi_radio_opclass;
 
 struct cmdu_buff *cntlr_gen_ap_autoconfig_renew(struct controller *c,
 		uint8_t *dst);
@@ -104,16 +105,16 @@ struct cmdu_buff *cntlr_gen_client_steer_request(struct controller *c,
 struct cmdu_buff *cntlr_gen_comb_infra_metrics_query(struct controller *c,
 		uint8_t *origin, uint8_t *bssid_mac);
 int cntlr_send_channel_preference_query(struct controller *c, uint8_t *agent);
-int cntlr_send_channel_selection(struct controller *c, uint8_t *agent, uint8_t *radio,
-				 uint8_t channel, uint8_t opclass, uint8_t pref);
+uint16_t cntlr_send_channel_selection(struct controller *c, uint8_t *agent, uint8_t *radio,
+				      struct wifi_radio_opclass *opclass);
 int cntlr_send_channel_scan_request(struct controller *c, uint8_t *agent_mac,
 		struct scan_req_data *data);
 int cntlr_send_client_assoc_ctrl_request(struct controller *c,
 		uint8_t *agent_mac, uint8_t *bssid,
 		uint8_t assoc_cntl_mode, uint16_t assoc_timeout,
 		uint8_t sta_nr, uint8_t *stalist, uint16_t *mid);
-int cntlr_send_cac_req(struct controller *c, uint8_t *agent,
-		       int num_data, struct cac_data *data);
+uint16_t cntlr_send_cac_req(struct controller *c, uint8_t *agent,
+			    int num_data, struct cac_data *data);
 int cntlr_send_cac_term(struct controller *c, uint8_t *agent,
 		        int num_data, struct cac_data *data);
 int cntlr_send_client_steer_request(struct controller *c, uint8_t *agent,
diff --git a/src/cntlr_commands.c b/src/cntlr_commands.c
index deaaa015..ea88ce35 100644
--- a/src/cntlr_commands.c
+++ b/src/cntlr_commands.c
@@ -243,7 +243,7 @@ DEFINE_ATTR(send_channel_sel) = {
 
 DEFINE_ATTR(trigger_channel_clearing) = {
 	[CHANNEL_CLEARING_ATTR_AGENT] = {
-		.optional = 0,
+		.optional = 1,
 		.pol = {
 			.name = "agent",
 			.type = BLOBMSG_TYPE_STRING,
@@ -252,6 +252,57 @@ DEFINE_ATTR(trigger_channel_clearing) = {
 	},
 };
 
+DEFINE_ATTR(trigger_acs) = {
+	[CHANNEL_ACS_ATTR_AGENT] = {
+		.optional = 1,
+		.pol = {
+			.name = "agent",
+			.type = BLOBMSG_TYPE_STRING,
+		},
+		.help = "AL-address of Agent for channel recalc request",
+	},
+	[CHANNEL_ACS_ATTR_BAND] = {
+		.optional = 1,
+		.pol = {
+			.name = "band",
+			.type = BLOBMSG_TYPE_INT32,
+		},
+		.help = "Band for channel recalc request",
+	},
+	[CHANNEL_ACS_ATTR_SKIP_DFS] = {
+		.optional = 1,
+		.pol = {
+			.name = "skip_dfs",
+			.type = BLOBMSG_TYPE_INT32,
+		},
+		.help = "skip DFS channels when recalc",
+	},
+	[CHANNEL_ACS_ATTR_OPCLASS] = {
+		.optional = 1,
+		.pol = {
+			.name = "opclass",
+			.type = BLOBMSG_TYPE_INT32,
+		},
+		.help = "opclass for channel recalc request",
+	},
+	[CHANNEL_ACS_ATTR_BANDWIDTH] = {
+		.optional = 1,
+		.pol = {
+			.name = "bandwidth",
+			.type = BLOBMSG_TYPE_INT32,
+		},
+		.help = "bandwidth for channel recalc request",
+	},
+	[CHANNEL_ACS_ATTR_PREVENT_CAC] = {
+		.optional = 1,
+		.pol = {
+			.name = "prevent_cac",
+			.type = BLOBMSG_TYPE_INT32,
+		},
+		.help = "prevent DFS CAC when recalc",
+	},
+};
+
 DEFINE_ATTR(query_beacon_metrics) = {
 	[BCN_METRICS_ATTR_AGENT] = {
 		.optional = 0,
@@ -985,6 +1036,7 @@ static struct controller_command cntlr_commandlist[] = {
 	CNTLR_CMD(query_beacon_metrics, "Send Beacon Metrics Query to Agent"),
 	CNTLR_CMD(send_channel_sel, "Send Channel Selection Request to Agent"),
 	CNTLR_CMD(trigger_channel_clearing, "Trigger CAC Request in Agent for clearing DFS channels"),
+	CNTLR_CMD(trigger_acs, "Trigger ACS"),
 	CNTLR_CMD(steer, "Send STA steering request to Agent"),
 	CNTLR_CMD(steer_op, "Send STA steering opportunity request to Agent"),
 	CNTLR_CMD(assoc_control, "Send STA/bSTA association control request to Agent"),
diff --git a/src/cntlr_commands.h b/src/cntlr_commands.h
index 3f87e281..fba99d39 100644
--- a/src/cntlr_commands.h
+++ b/src/cntlr_commands.h
@@ -214,6 +214,29 @@ enum channel_clearing_attrs {
 	NUM_ATTRS_CHANNEL_CLEARING,
 };
 
+/**
+ * @enum channel_acs_attrs
+ * @brief ACS Channel attributes
+ *
+ * @ingroup cmdattrs
+ * @CHANNEL_ACS_ATTR_AGENT: Agent's AL-address.
+ * @CHANNEL_ACS_ATTR_BAND: band in Agent.
+ * @CHANNEL_ACS_ATTR_SKIP_DFS: skip DFS channels.
+ * @CHANNEL_ACS_ATTR_OPCLASS: requested opclass.
+ * @CHANNEL_ACS_ATTR_BANDWIDTH: requested bandwidth.
+ * @CHANNEL_ACS_ATTR_PREVENT_CAC: prevent DFS CAC.
+ * @NUM_ATTRS_CHANNEL_ACS: Number of Channel ACS attributes.
+ */
+enum channel_acs_attrs {
+	CHANNEL_ACS_ATTR_AGENT,
+	CHANNEL_ACS_ATTR_BAND,
+	CHANNEL_ACS_ATTR_SKIP_DFS,
+	CHANNEL_ACS_ATTR_OPCLASS,
+	CHANNEL_ACS_ATTR_BANDWIDTH,
+	CHANNEL_ACS_ATTR_PREVENT_CAC,
+	NUM_ATTRS_CHANNEL_ACS,
+};
+
 /**
  * @enum steer_attrs
  * @brief Steer Request attributes
diff --git a/src/cntlr_commands_impl.c b/src/cntlr_commands_impl.c
index 963c2281..f407a646 100644
--- a/src/cntlr_commands_impl.c
+++ b/src/cntlr_commands_impl.c
@@ -1030,22 +1030,6 @@ int COMMAND(send_hld)(void *priv, void *args, void *out)
 	return mid != 0xffff ? 0 : -1;
 }
 
-static uint8_t wifi_band_to_band(enum wifi_band band)
-{
-	switch (band) {
-	case BAND_2:
-		return 2;
-	case BAND_5:
-		return 5;
-	case BAND_6:
-		return 6;
-	default:
-		break;
-	}
-
-	return 0;
-}
-
 int COMMAND(send_channel_sel)(void *priv, void *args, void *out)
 {
 	struct controller *c = (struct controller *)priv;
@@ -1100,18 +1084,18 @@ int COMMAND(send_channel_sel)(void *priv, void *args, void *out)
 	memcpy(&opclass, &r->radio_el->supp_opclass, sizeof(opclass));
 	wifi_opclass_set_preferences(&opclass, 0x0);
 
-	band = wifi_band_to_band(r->radio_el->band);
+	band = wifi_band_to_freqband(r->radio_el->band);
 	if (!band) {
 		err("%s unknown band %u\n", __func__, r->radio_el->band);
 		return -EINVAL;
 	}
 
-	if (wifi_radio_opclass_update_channel(&opclass, band, channel, bandwidth, 15 << 4)) {
+	if (wifi_radio_opclass_update_channel(&opclass, band, channel, bandwidth, 15)) {
 		err("%s: opclass_update_channel(%u, %u, %u) error\n", __func__, band, channel, bandwidth);
 		return -EINVAL;
 	}
 
-	wifi_opclass_dump(&opclass, "chan_selection", radio_mac);
+	wifi_opclass_dump_ex(&opclass, "chan_selection", radio_mac, false);
 
 	cmdu = cntlr_gen_channel_sel_request(c, agent_mac, radio_mac, &opclass);
 	if (!cmdu) {
@@ -1119,6 +1103,7 @@ int COMMAND(send_channel_sel)(void *priv, void *args, void *out)
 		return -1;
 	}
 
+	cmdu_put_eom(cmdu);
 	mid = send_cmdu(c, cmdu);
 	cmdu_free(cmdu);
 
@@ -1130,6 +1115,7 @@ int COMMAND(send_channel_sel)(void *priv, void *args, void *out)
 	return mid != 0xffff ? 0 : -1;
 }
 
+
 int COMMAND(trigger_channel_clearing)(void *priv, void *args, void *out)
 {
 	struct controller *c = (struct controller *)priv;
@@ -1146,20 +1132,101 @@ int COMMAND(trigger_channel_clearing)(void *priv, void *args, void *out)
 		return -EINVAL;
 	}
 
-	strncpy(agent, blobmsg_data(tb[CHANNEL_CLEARING_ATTR_AGENT]), sizeof(agent) - 1);
-	if (!hwaddr_aton(agent, agent_mac))
+	if (tb[CHANNEL_CLEARING_ATTR_AGENT]) {
+		strncpy(agent, blobmsg_data(tb[CHANNEL_CLEARING_ATTR_AGENT]), sizeof(agent) - 1);
+		if (!hwaddr_aton(agent, agent_mac))
+			return -EINVAL;
+	}
+
+	list_for_each_entry(node, &c->nodelist, list) {
+		struct netif_radio *radio = NULL;
+
+		if (!hwaddr_is_zero(agent_mac) && memcmp(agent_mac, node->almacaddr, 6))
+			continue;
+
+		list_for_each_entry(radio, &node->radiolist, list) {
+			if (radio->radio_el->band != BAND_5)
+				continue;
+
+			/* Action here */
+			cntlr_dfs_radio_cleanup(node, radio);
+			cntlr_dfs_radio_cleanup_info(node, radio, bb);
+		}
+	}
+
+	return 0;
+}
+
+int COMMAND(trigger_acs)(void *priv, void *args, void *out)
+{
+	struct controller *c = (struct controller *)priv;
+	struct blob_buf *bb = (struct blob_buf *)out;
+	struct blob_attr *tb[NUM_ATTRS_CHANNEL_ACS];
+	uint8_t agent_mac[6] = { 0 };
+	enum wifi_band band = BAND_ANY;
+	struct node *node = NULL;
+	char agent[18] = { 0 };
+	bool skip_dfs = c->cfg.acs_skip_dfs;
+	bool prevent_cac = c->cfg.acs_prevent_cac;
+	bool highest_bandwidth = c->cfg.acs_highest_bandwidth;
+	uint32_t bandwidth = 0;
+	uint8_t opclass = 0;
+	int ret;
+
+	ret = controller_command_parse("trigger_acs", args, tb);
+	if (ret) {
+		err("%s: Error (ret = %d)\n", __func__, ret);
 		return -EINVAL;
+	}
+
+	if (tb[CHANNEL_ACS_ATTR_SKIP_DFS])
+		skip_dfs = blobmsg_get_u32(tb[CHANNEL_ACS_ATTR_SKIP_DFS]);
+
+	if (tb[CHANNEL_ACS_ATTR_PREVENT_CAC])
+		prevent_cac = blobmsg_get_u32(tb[CHANNEL_ACS_ATTR_PREVENT_CAC]);
+
+	if (tb[CHANNEL_ACS_ATTR_AGENT]) {
+		strncpy(agent, blobmsg_data(tb[CHANNEL_ACS_ATTR_AGENT]), sizeof(agent) - 1);
+		if (!hwaddr_aton(agent, agent_mac))
+			return -EINVAL;
+	}
+
+	if (tb[CHANNEL_ACS_ATTR_BAND]) {
+		switch (blobmsg_get_u32(tb[CHANNEL_ACS_ATTR_BAND])) {
+		case 2:
+			band = BAND_2;
+			break;
+		case 5:
+			band = BAND_5;
+			break;
+		case 6:
+			band = BAND_6;
+			break;
+		default:
+			break;
+		}
+	}
+
+	if (tb[CHANNEL_ACS_ATTR_OPCLASS])
+		opclass = blobmsg_get_u32(tb[CHANNEL_ACS_ATTR_OPCLASS]);
+
+	if (tb[CHANNEL_ACS_ATTR_BANDWIDTH]) {
+		bandwidth = blobmsg_get_u32(tb[CHANNEL_ACS_ATTR_BANDWIDTH]);
+		if (!bandwidth)
+			highest_bandwidth = true;
+	}
 
 	list_for_each_entry(node, &c->nodelist, list) {
 		if (!hwaddr_is_zero(agent_mac) && memcmp(agent_mac, node->almacaddr, 6))
 			continue;
 
 		/* Action here */
-		cntlr_dfs_node_cleanup(node);
+		cntlr_acs_node_channel_recalc(node, band, opclass, bandwidth, skip_dfs, prevent_cac, highest_bandwidth);
+
+		/* Show information about recalc */
+		cntlr_acs_node_info(node, band, bb);
 	}
 
-	/* reply with status ok */
-	blobmsg_add_string(bb, "status", "ok");
 	return 0;
 }
 
@@ -2849,7 +2916,8 @@ out:
 	return mid != 0xffff ? 0 : -1;
 }
 
-static char *cntlr_status_channel_pref_reason(uint8_t reason)
+static char *cntlr_status_channel_pref_reason(uint8_t reason,
+				enum wifi_radio_opclass_dfs status)
 {
 	switch (reason) {
 	case CHANNEL_PREF_REASON_UNSPEC:
@@ -2871,7 +2939,11 @@ static char *cntlr_status_channel_pref_reason(uint8_t reason)
 	case CHANNEL_PREF_REASON_SHARED_BHAUL_PREVENT:
 		return "shared-bhaul-prevent";
 	case CHANNEL_PREF_REASON_DFS_USABLE:
-		return "dfs-usable";
+		/* Two options here, CAC required or CAC ongoing */
+		if (status == WIFI_RADIO_OPCLASS_CHANNEL_DFS_CAC)
+			return "dfs-cac";
+		else
+			return "dfs-usable";
 	case CHANNEL_PREF_REASON_DFS_AVAILABLE:
 		return "dfs-available";
 	case CHANNEL_PREF_REASON_REG_DISALLOWED:
@@ -2886,6 +2958,7 @@ static char *cntlr_status_channel_pref_reason(uint8_t reason)
 void cntlr_status_add_opclass(struct blob_buf *bb, struct wifi_radio_opclass *opclass,
 			      const char *name, int opclass_id)
 {
+	enum wifi_radio_opclass_dfs dfs_status;
 	uint32_t cac_methods, cac_time;
 	void *a, *aa, *t, *tt;
 	uint8_t reas, pref;
@@ -2912,9 +2985,10 @@ void cntlr_status_add_opclass(struct blob_buf *bb, struct wifi_radio_opclass *op
 			if (!strstr(name, "cur")) {
 				pref = (opclass->opclass[j].channel[k].preference & CHANNEL_PREF_MASK) >> 4;
 				reas = opclass->opclass[j].channel[k].preference & CHANNEL_PREF_REASON;
+				dfs_status = opclass->opclass[j].channel[k].dfs;
 
 				blobmsg_add_u32(bb, "preference", pref);
-				blobmsg_add_string(bb, "reason", cntlr_status_channel_pref_reason(reas));
+				blobmsg_add_string(bb, "reason", cntlr_status_channel_pref_reason(reas, dfs_status));
 
 				if (opclass->opclass[j].channel[k].cac_methods) {
 					cac_methods = opclass->opclass[j].channel[k].cac_methods;
@@ -2966,6 +3040,10 @@ static int _cntlr_status(struct controller *c, void *args, void *out, bool full)
 
 			/* Show current/prefered opclasses */
 			cntlr_status_add_opclass(bb, &p->radio_el->cur_opclass, "cur_opclass", 0);
+			cntlr_acs_radio_info(bb, p);
+
+			if (p->radio_el->bgcac_supported)
+				cntlr_acs_radio_cleanup_info(bb, p);
 
 			/* Limit opclass output if possible */
 			cur_opclass_id = ctrl_radio_cur_opclass_id(p->radio_el);
@@ -3096,8 +3174,7 @@ int COMMAND(timers)(void *priv, void *args, void *out)
 	void *t;
 
 	t = blobmsg_open_table(bb, "channel_planning");
-	blobmsg_add_u32(bb, "channel_plan", timer_remaining_ms(&c->acs));
-	blobmsg_add_u32(bb, "allow_bgdfs", timer_remaining_ms(&c->dfs_cleanup));
+	blobmsg_add_u32(bb, "acs_timeout", timer_remaining_ms(&c->acs));
 	blobmsg_close_table(bb, t);
 
 	return 0;
diff --git a/src/cntlr_commands_impl.h b/src/cntlr_commands_impl.h
index b1ae36eb..437db813 100644
--- a/src/cntlr_commands_impl.h
+++ b/src/cntlr_commands_impl.h
@@ -38,6 +38,7 @@ DECLARE_COMMAND(query_channel_pref);
 DECLARE_COMMAND(query_beacon_metrics);
 DECLARE_COMMAND(send_channel_sel);
 DECLARE_COMMAND(trigger_channel_clearing);
+DECLARE_COMMAND(trigger_acs);
 DECLARE_COMMAND(steer);
 DECLARE_COMMAND(steer_op);
 DECLARE_COMMAND(assoc_control);
diff --git a/src/cntlr_map.c b/src/cntlr_map.c
index 4a537ddb..4cd4022c 100644
--- a/src/cntlr_map.c
+++ b/src/cntlr_map.c
@@ -666,7 +666,7 @@ int handle_topology_response(void *cntlr, struct cmdu_buff *cmdu, struct node *n
 	}
 
 	/* Check opclass preferency age */
-	if (cntlr_node_pref_opclass_expired(n)) {
+	if (cntlr_node_pref_opclass_expired(n, 300)) {
 		cntlr_trace(LOG_CHANNEL, "node " MACFMT " pref opclass expired\n", MAC2STR(n->almacaddr));
 		cntlr_send_channel_preference_query(c, n->almacaddr);
 	}
@@ -1520,6 +1520,9 @@ static int cntlr_parse_radio_cac_caps(struct controller *c, struct node *n, stru
 
 					chan->cac_methods |= get_supp_methods(cac->supp_method);
 					chan->cac_time = get_duration_data(cac->duration);
+
+					if (chan->cac_methods && chan->cac_methods != (1 << WIFI_CAC_CONTINUOUS))
+						radio_el->bgcac_supported =  true;
 				}
 			}
 		}
@@ -1634,6 +1637,7 @@ int handle_ap_caps_report(void *cntlr, struct cmdu_buff *cmdu, struct node *n)
 		}
 
 		wifi_opclass_dump(opclass, "dev_supp_opclass", re->macaddr);
+		cntlr_acs_dev_supp_opclass(n, r);
 	}
 
 
@@ -1769,6 +1773,102 @@ int handle_channel_pref_report(void *cntlr, struct cmdu_buff *cmdu, struct node
 		}
 
 		cntlr_radio_pref_opclass_dump(r->radio_el);
+
+		/* Kick ACS code - for 5GHz we will send from CAC status */
+		if (r->radio_el->band != BAND_5)
+			cntlr_acs_channel_pref_report(n, r);
+	}
+
+	idx = 0;
+	while (tv[CHANNEL_PREF_REPORT_CAC_COMPLETION_REPORT_IDX][idx]) {
+		struct tlv *t = (struct tlv *)tv[CHANNEL_PREF_REPORT_CAC_COMPLETION_REPORT_IDX][idx++];
+		uint8_t num_radio;
+		uint8_t num_pairs;
+
+		offset = 0;
+		num_radio = t->data[offset++];
+
+		for (i = 0; i < num_radio; i++) {
+			uint8_t mac[6] = { 0 };
+			uint8_t opclass, channel, status;
+
+			memcpy(mac, &t->data[offset], 6);
+			offset += 6;
+
+			opclass = t->data[offset++];
+			channel = t->data[offset++];
+			status = t->data[offset++];
+
+			num_pairs = t->data[offset++];
+			for (j = 0; j < num_pairs; j++)
+				offset += 2;
+
+			r = cntlr_find_radio(cntlr, mac);
+			if (!r)
+				continue;
+
+			/* Kick ACS code */
+			cntlr_acs_cac_completion(r, opclass, channel, status);
+		}
+
+		break;
+	}
+
+	idx = 0;
+	while (tv[CHANNEL_PREF_REPORT_CAC_STATUS_REPORT_IDX][idx]) {
+		struct tlv *t = (struct tlv *)tv[CHANNEL_PREF_REPORT_CAC_STATUS_REPORT_IDX][idx++];
+		uint8_t channel, opclassid;
+		uint16_t time;
+		uint8_t num;
+
+		r = cntlr_find_radio_in_node_by_band(n->cntlr, n, BAND_5);
+		if (!r)
+			break;
+
+		offset = 0;
+
+		/* CAC completed */
+		num = t->data[offset++];
+		for (i = 0; i < num; i++) {
+			opclassid = t->data[offset++];
+			channel = t->data[offset++];
+			time = BUF_GET_BE16(t->data[offset]);
+			offset += 2;
+
+			cntlr_radio_pref_opclass_set_dfs_status(r->radio_el, opclassid, channel,
+							time ? WIFI_RADIO_OPCLASS_CHANNEL_DFS_AVAILABLE :
+							       WIFI_RADIO_OPCLASS_CHANNEL_DFS_NONE);
+		}
+
+		/* NOP */
+		num = t->data[offset++];
+		for (i = 0; i < num; i++) {
+			opclassid = t->data[offset++];
+			channel = t->data[offset++];
+			time = BUF_GET_BE16(t->data[offset]);
+			offset += 2;
+
+			cntlr_radio_pref_opclass_set_dfs_status(r->radio_el, opclassid, channel,
+							WIFI_RADIO_OPCLASS_CHANNEL_DFS_NOP);
+		}
+
+		/* CAC ongoing */
+		num = t->data[offset++];
+		for (i = 0; i < num; i++) {
+			opclassid = t->data[offset++];
+			channel = t->data[offset++];
+			time = BUF_GET_BE24(t->data[offset]);
+			offset += 3;
+
+			cntlr_radio_pref_opclass_set_dfs_status(r->radio_el, opclassid, channel,
+							WIFI_RADIO_OPCLASS_CHANNEL_DFS_CAC);
+		}
+
+		/* Kick ACS code */
+		cntlr_acs_channel_pref_report(n, r);
+
+		/* Only one allowed */
+		break;
 	}
 
 	return 0;
@@ -1776,8 +1876,40 @@ int handle_channel_pref_report(void *cntlr, struct cmdu_buff *cmdu, struct node
 
 int handle_channel_sel_response(void *cntlr, struct cmdu_buff *cmdu, struct node *n)
 {
-	trace("%s: --->\n", __func__);
-	return 0;
+	int idx = 0;
+	struct controller *c = (struct controller *) cntlr;
+	struct tlv *tv[CHANNEL_SELECTION_RESP_NUM_OF_TLV_TYPES][TLV_MAXNUM] = {0};
+	uint16_t mid;
+
+	cntlr_dbg(LOG_CHANNEL, "%s called\n", __func__);
+
+	if (!map_cmdu_validate_parse(cmdu, tv, ARRAY_SIZE(tv), n->map_profile)) {
+		dbg("%s: map_cmdu_validate_parse ( ..,EMP=%d) failed,  err = (%d) '%s'\n", __func__,
+		    n->map_profile, map_error, map_strerror(map_error));
+		return -1;
+	}
+
+	mid = cmdu_get_mid(cmdu);
+	while (idx < TLV_MAXNUM && tv[CHANNEL_SELECTION_RESP_CHANNEL_SELECTION_IDX][idx]) {
+		struct netif_radio *r;
+		struct tlv_channel_selection_resp *p;
+
+		p = (struct tlv_channel_selection_resp *)
+			tv[CHANNEL_SELECTION_RESP_CHANNEL_SELECTION_IDX][idx++]->data;
+
+		cntlr_dbg(LOG_CHANNEL, "\tmid: %d\n", mid);
+		cntlr_dbg(LOG_CHANNEL, "\tradio_id: " MACFMT "\n", MAC2STR(p->radio));
+		cntlr_dbg(LOG_CHANNEL, "\tresponse_code: %d\n", p->response);
+
+		r = cntlr_find_radio(c, p->radio);
+		if (!r)
+			continue;
+
+		/* kick acs code */
+		cntlr_acs_channel_sel_response(r, mid, p->response);
+       }
+
+       return 0;
 }
 
 int handle_oper_channel_report(void *cntlr, struct cmdu_buff *cmdu, struct node *n)
@@ -1848,6 +1980,9 @@ int handle_oper_channel_report(void *cntlr, struct cmdu_buff *cmdu, struct node
 		}
 
 		cntlr_radio_cur_opclass_dump(r->radio_el);
+
+		/* kick acs code */
+		cntlr_acs_oper_channel_report(r);
 	}
 
 	return 0;
@@ -2059,6 +2194,9 @@ int handle_ap_metrics_response(void *cntlr, struct cmdu_buff *cmdu, struct node
 
 		radio = cntlr_find_radio(c, p->radio);
 		if (radio) {
+			/* Kick ACS code */
+			cntlr_acs_radio_metrics(n, radio, p->noise, p->receive_other);
+
 			radio->radio_el->anpi = p->noise;
 			radio->radio_el->tx_utilization = p->transmit;
 			radio->radio_el->rx_utilization = p->receive_self;
@@ -3041,6 +3179,9 @@ int handle_channel_scan_report(void *cntlr, struct cmdu_buff *cmdu,
 	char timestamp[TIMESTAMP_MAX_LEN] = {0};
 	struct tlv_timestamp *p = NULL;
 	uint8_t *tv_data = NULL;
+	struct netif_radio *r = NULL;
+	uint16_t mid;
+	int i;
 
 	if (!map_cmdu_validate_parse(cmdu, tv, ARRAY_SIZE(tv), n->map_profile)) {
 		dbg("%s: map_cmdu_validate_parse failed,  err = (%d) '%s'\n",
@@ -3058,6 +3199,23 @@ int handle_channel_scan_report(void *cntlr, struct cmdu_buff *cmdu,
 
 	dbg("%s: timestamp = %s\n", __func__, timestamp);
 
+	/* Inform ACS about fresh scan and preference */
+	mid = cmdu_get_mid(cmdu);
+	list_for_each_entry(r, &n->radiolist, list) {
+		for (i = 0; i < num_result; i++) {
+			struct tlv_channel_scan_result *tlv =
+				(struct tlv_channel_scan_result *)tv_scan[i]->data;
+
+			if (!memcmp(r->radio_el->macaddr, tlv->radio, 6))
+				break;
+		}
+
+		if (i == num_result)
+			continue;
+
+		cntlr_acs_scan_report(n, r, mid);
+	}
+
 	return cntlr_radio_update_scanlist(cntlr, timestamp, tv_scan, num_result);
 }
 
diff --git a/src/cntlr_tlv.c b/src/cntlr_tlv.c
index eca7759f..c98b70ae 100644
--- a/src/cntlr_tlv.c
+++ b/src/cntlr_tlv.c
@@ -1357,6 +1357,7 @@ struct cmdu_buff *cntlr_gen_channel_sel_request(struct controller *c,
 {
 	struct wifi_radio_opclass_entry *entry;
 	struct wifi_radio_opclass_channel *channel;
+	struct wifi_radio_opclass empty = {};
 	int ret, offset = 0;
 	struct cmdu_buff *cmdu;
 	struct netif_radio *radio;
@@ -1391,6 +1392,12 @@ struct cmdu_buff *cntlr_gen_channel_sel_request(struct controller *c,
 	memcpy(&t->data[offset], radio_id, 6);	/* radio id */
 	offset += 6;
 
+	if (!opclass) {
+		memcpy(&empty, &radio->radio_el->supp_opclass, sizeof(empty));
+		wifi_opclass_set_preferences(&empty, 0x0);
+		opclass = &empty;
+	}
+
 	/* Now update prefered */
         opclass_num_offset = offset;
         t->data[offset++] = 0;                                                  /* m */
@@ -1410,125 +1417,60 @@ struct cmdu_buff *cntlr_gen_channel_sel_request(struct controller *c,
                         continue;
                 }
 
-		/* Group disabled channels - assume other with pref=15 */
-                t->data[offset++] = entry->id;
-                channel_offset = offset;
-                t->data[offset++] = 0;                                  /* k */
-
-                for (j = 0; j < entry->num_channel; j++) {
-                        channel = &entry->channel[j];
-
-			if (((channel->preference & CHANNEL_PREF_MASK) >> 4) != 0)
-				continue;
-
-                        t->data[offset++] = channel->channel;
-			channel_num++;
-                }
-
-                t->data[offset++] = 0;
-                t->data[channel_offset] = channel_num;
-                opclass_num++;
-        }
-
-        t->data[opclass_num_offset] = opclass_num;                              /* m */
-	t->len = offset;
-	ret = cmdu_put_tlv(cmdu, t);
-	if (ret) {
-		dbg("%s: error: cmdu_put_tlv()\n", __func__);
-		cmdu_free(cmdu);
-		return NULL;
-	}
+		if (wifi_opclass_same_preference(opclass, &preference)) {
+			/* Group disabled channels - assume other with pref=15 */
+			t->data[offset++] = entry->id;
+			channel_offset = offset;
+			t->data[offset++] = 0;                                  /* k */
 
-	cmdu_put_eom(cmdu);
-	return cmdu;
-}
+			for (j = 0; j < entry->num_channel; j++) {
+				channel = &entry->channel[j];
 
-int cntlr_gen_channel_pref(struct controller *c, struct cmdu_buff *frm,
-		uint8_t *radio_id, uint8_t class_id, uint8_t channel_nr,
-		const uint8_t *chanlist, uint8_t pref)
-{
-	int ret, offset = 0;
-	struct wifi_radio_opclass opclass = {};
-	struct wifi_radio_opclass_entry *entry;
-	struct wifi_radio_opclass_channel *channel;
-	struct netif_radio *radio;
-	struct tlv *t;
-	int opclass_num_offset;
-	int opclass_num;
-	int i, j;
-
-	/* Find radio and supported opclasses */
-	radio = cntlr_find_radio(c, radio_id);
-	if (!radio)
-		return -1;
-
-	if (!radio->radio_el)
-		return -1;
-
-	if (!radio->radio_el->supp_opclass.num_opclass && channel_nr)
-		return -1;
+				if (((channel->preference & CHANNEL_PREF_MASK) >> 4) != 0)
+					continue;
 
-	/* Build opclass we would like to send */
-	memcpy(&opclass, &radio->radio_el->supp_opclass, sizeof(opclass));
-	wifi_opclass_set_preferences(&opclass, 0x0);
-
-	/* Update requested preference */
-	for (i = 0; i < channel_nr; i++)
-		wifi_opclass_id_set_channel_preferences(&opclass, class_id, chanlist[i], pref << 4);
-
-	wifi_opclass_dump(&opclass, "send_chan_pref", radio->radio_el->macaddr);
-
-	t = cmdu_reserve_tlv(frm, 1024);
-	if (!t)
-		return -1;
-
-	/* Prefer all supported - don't include any opclass */
-	if (!channel_nr)
-		opclass.num_opclass = 0;
-
-	t->type = MAP_TLV_CHANNEL_PREFERENCE;
-
-	memcpy(&t->data[offset], radio_id, 6);	/* radio id */
-	offset += 6;
+				t->data[offset++] = channel->channel;
+				channel_num++;
+			}
 
-	/* Now update prefered */
-        opclass_num_offset = offset;
-        t->data[offset++] = 0;                                                  /* m */
+			t->data[offset++] = 0;
+			t->data[channel_offset] = channel_num;
+			opclass_num++;
+			continue;
+		}
 
-	opclass_num = 0;
-        for (i = 0; i < opclass.num_opclass; i++) {
-                entry = &opclass.opclass[i];
-                uint8_t preference;
+		/* Setup whole opclass disabled */
+		t->data[offset++] = entry->id;
+		t->data[offset++] = 0;						/* k */
+		t->data[offset++] = 0;
+		opclass_num++;
 
-                if (wifi_opclass_id_same_preference(&opclass, entry->id, &preference)) {
-                        t->data[offset++] = entry->id;
-                        t->data[offset++] = 0;                                  /* k */
-                        t->data[offset++] = preference;
-                        opclass_num++;
-                        continue;
-                }
+		/* Next unlock required channels */
+		for (j = 0; j < entry->num_channel; j++) {
+			channel = &entry->channel[j];
 
-                for (j = 0; j < entry->num_channel; j++) {
-                        channel = &entry->channel[j];
+			if (((channel->preference & CHANNEL_PREF_MASK) >> 4) == 0)
+				continue;
 
-                        t->data[offset++] = entry->id;
-                        t->data[offset++] = 1;                                  /* k */
-                        t->data[offset++] = channel->channel;
-                        t->data[offset++] = channel->preference;
+			t->data[offset++] = entry->id;
+			t->data[offset++] = 1;                                  /* k */
+			t->data[offset++] = channel->channel;
+			t->data[offset++] = channel->preference;
 
-                        opclass_num++;
-                }
+			opclass_num++;
+		}
         }
 
         t->data[opclass_num_offset] = opclass_num;                              /* m */
 	t->len = offset;
-	ret = cmdu_put_tlv(frm, t);
+	ret = cmdu_put_tlv(cmdu, t);
 	if (ret) {
 		dbg("%s: error: cmdu_put_tlv()\n", __func__);
-		return -1;
+		cmdu_free(cmdu);
+		return NULL;
 	}
 
-	return 0;
+	return cmdu;
 }
 
 int cntlr_gen_txpower_limit(struct controller *c, struct cmdu_buff *frm,
diff --git a/src/cntlr_tlv.h b/src/cntlr_tlv.h
index 68acf3af..0f8a4473 100644
--- a/src/cntlr_tlv.h
+++ b/src/cntlr_tlv.h
@@ -114,9 +114,6 @@ int cnltr_gen_searched_service(struct controller *c, struct cmdu_buff *frm,
 		uint8_t service);
 int agent_gen_tlv_error_code(struct controller *c,
 		struct cmdu_buff *cmdu, uint8_t *macaddr, uint8_t reason_code);
-int cntlr_gen_channel_pref(struct controller *c, struct cmdu_buff *frm,
-		uint8_t *radio_id, uint8_t class_id, uint8_t channel_nr,
-		const uint8_t *chanlist, uint8_t pref);
 int cntlr_gen_txpower_limit(struct controller *c, struct cmdu_buff *frm,
 		uint8_t *radio_id, uint8_t txpower_limit);
 int cntlr_gen_cac_tlv(struct controller *c, struct cmdu_buff *frm,
diff --git a/src/cntlr_ubus.c b/src/cntlr_ubus.c
index d035e25c..a6e1854a 100644
--- a/src/cntlr_ubus.c
+++ b/src/cntlr_ubus.c
@@ -409,6 +409,34 @@ out:
 	return ret;
 }
 
+int cntlr_ubus_trigger_acs(struct ubus_context *ctx,
+			   struct ubus_object *obj,
+			   struct ubus_request_data *req,
+			   const char *method,
+			   struct blob_attr *msg)
+{
+	//TRACE_ENTER();
+	struct controller *c = container_of(obj, struct controller, obj);
+	struct blob_buf bb;
+	int ret;
+
+	memset(&bb, 0, sizeof(bb));
+	blob_buf_init(&bb, 0);
+
+	ret = COMMAND(trigger_acs)(c, msg, &bb);
+	if (ret) {
+		if (ret == -EINVAL)
+			ret = UBUS_STATUS_INVALID_ARGUMENT;
+
+		goto out;
+	}
+
+	ubus_send_reply(ctx, req, bb.head);
+out:
+	blob_buf_free(&bb);
+	return ret;
+}
+
 int cntlr_ubus_steer(struct ubus_context *ctx,
 		     struct ubus_object *obj,
 		     struct ubus_request_data *req,
@@ -1128,6 +1156,7 @@ int cntlr_publish_object(struct controller *c, const char *objname)
 		{ "query_beacon_metrics", cntlr_ubus_query_beacon_metrics },
 		{ "send_channel_sel", cntlr_ubus_send_channel_sel },
 		{ "trigger_channel_clearing", cntlr_ubus_trigger_channel_clearing },
+		{ "trigger_acs", cntlr_ubus_trigger_acs },
 		{ "steer", cntlr_ubus_steer },
 		{ "steer_op", cntlr_ubus_steer_op },
 		{ "assoc_control", cntlr_ubus_assoc_control },
diff --git a/src/cntlr_ubus_dbg.c b/src/cntlr_ubus_dbg.c
index e66a91b0..236446d0 100644
--- a/src/cntlr_ubus_dbg.c
+++ b/src/cntlr_ubus_dbg.c
@@ -214,7 +214,7 @@ static int cntlr_dbg_csr(struct ubus_context *ctx,
 		struct netif_radio *r = NULL;
 
 		list_for_each_entry(r, &node->radiolist, list) {
-			cntlr_send_channel_selection(c, node->almacaddr, r->radio_el->macaddr, 0, 0, 0);
+			cntlr_send_channel_selection(c, node->almacaddr, r->radio_el->macaddr, NULL);
 		}
 	}
 
diff --git a/src/config.c b/src/config.c
index 34696ba3..4325d949 100644
--- a/src/config.c
+++ b/src/config.c
@@ -721,8 +721,6 @@ static int cntlr_config_get_base(struct controller_config *c,
 		CNTLR_RESEND_NUM,
 		CNTLR_BCN_METRICS_MAX_NUM,
 		CNTLR_INITIAL_CHANNEL_SCAN,
-		CNTLR_CHANNEL_PLAN_TIMEOUT,
-		CNTLR_BGDFS_TIMEOUT,
 		CNTLR_STALE_STA_TIMEOUT,
 		CNTLR_PRIMARY_VID,
 		CNTLR_DEFAULT_PCP,
@@ -741,8 +739,6 @@ static int cntlr_config_get_base(struct controller_config *c,
 		[CNTLR_RESEND_NUM] = { .name = "resend_num", .type = UCI_TYPE_STRING },
 		[CNTLR_BCN_METRICS_MAX_NUM] = { .name = "bcn_metrics_max_num", .type = UCI_TYPE_STRING },
 		[CNTLR_INITIAL_CHANNEL_SCAN] = { .name = "initial_channel_scan", .type = UCI_TYPE_STRING },
-		[CNTLR_CHANNEL_PLAN_TIMEOUT] = { .name = "channel_plan", .type = UCI_TYPE_STRING },
-		[CNTLR_BGDFS_TIMEOUT] = { .name = "allow_bgdfs", .type = UCI_TYPE_STRING },
 		[CNTLR_STALE_STA_TIMEOUT] = { .name = "stale_sta_timeout", .type = UCI_TYPE_STRING },
 		[CNTLR_PRIMARY_VID] = { .name = "primary_vid", .type = UCI_TYPE_STRING },
 		[CNTLR_DEFAULT_PCP] = { .name = "default_pcp", .type = UCI_TYPE_STRING },
@@ -855,35 +851,6 @@ static int cntlr_config_get_base(struct controller_config *c,
 		c->initial_channel_scan = (enbl == 1) ? true : false;
 	}
 
-	if (tb[CNTLR_CHANNEL_PLAN_TIMEOUT]) {
-		const char *val = tb[CNTLR_CHANNEL_PLAN_TIMEOUT]->v.string;
-
-		errno = 0;
-		c->acs_timeout = strtol(val, &endptr, 10);
-		if (errno || *endptr != '\0') {
-			cntlr_dbg(LOG_MISC, "|%s:%d| Error parsing channel_plan value: %s\n",
-					__func__, __LINE__, val);
-			return -1;
-		}
-		/* TODO agree conf param - by default run each 3 hours */
-		if (c->acs_timeout > 0 && c->acs_timeout < 180)
-			c->acs_timeout = 3600 * 3;
-	}
-
-	if (tb[CNTLR_BGDFS_TIMEOUT]) {
-		const char *val = tb[CNTLR_BGDFS_TIMEOUT]->v.string;
-
-		errno = 0;
-		c->dfs_cleanup_timeout = strtol(val, &endptr, 10);
-		if (errno || *endptr != '\0') {
-			cntlr_dbg(LOG_MISC, "|%s:%d| Error parsing allow_bgdfs value: %s\n",
-					__func__, __LINE__, val);
-			return -1;
-		}
-		if (c->dfs_cleanup_timeout > 0 && c->dfs_cleanup_timeout < 120)
-			c->dfs_cleanup_timeout = 120;
-	}
-
 #define DEFAULT_STALE_STA_TIMEOUT (30 * 86400) /* Default 30days */
 
 	if (tb[CNTLR_STALE_STA_TIMEOUT]) {
@@ -1197,6 +1164,134 @@ static int cntlr_config_get_steer_policy(struct controller_config *cc,
 	return 0;
 }
 
+static int cntlr_config_get_channel_plan(struct controller_config *c,
+					 struct uci_section *s)
+{
+	enum {
+		CNTLR_CHANNEL_PLAN_ACS,
+		CNTLR_CHANNEL_PLAN_ACS_RECALC_TIMEOUT,
+		CNTLR_CHANNEL_PLAN_ACS_SKIP_DFS,
+		CNTLR_CHANNEL_PLAN_ACS_PREVENT_CAC,
+		CNTLR_CHANNEL_PLAN_ACS_HIGHEST_BANDWIDTH,
+		CNTLR_CHANNEL_PLAN_ACS_SCAN_BEFORE_RECALC,
+		CNTLR_CHANNEL_PLAN_BGCAC,
+		NUM_CNTLR_CHANNEL_PLAN_ATTRS
+	};
+	const struct uci_parse_option opts[NUM_CNTLR_CHANNEL_PLAN_ATTRS] = {
+		[CNTLR_CHANNEL_PLAN_ACS] = { .name = "acs", .type = UCI_TYPE_STRING },
+		[CNTLR_CHANNEL_PLAN_ACS_RECALC_TIMEOUT] = { .name = "acs_timeout", .type = UCI_TYPE_STRING },
+		[CNTLR_CHANNEL_PLAN_ACS_SKIP_DFS] = { .name = "acs_skip_dfs", .type = UCI_TYPE_STRING },
+		[CNTLR_CHANNEL_PLAN_ACS_PREVENT_CAC] = { .name = "acs_prevent_cac", .type = UCI_TYPE_STRING },
+		[CNTLR_CHANNEL_PLAN_ACS_HIGHEST_BANDWIDTH] = { .name = "acs_highest_bandwidth", .type = UCI_TYPE_STRING },
+		[CNTLR_CHANNEL_PLAN_ACS_SCAN_BEFORE_RECALC] = { .name = "acs_scan_before_recalc", .type = UCI_TYPE_STRING },
+		[CNTLR_CHANNEL_PLAN_BGCAC] = { .name = "preclear_dfs", .type = UCI_TYPE_STRING },
+	};
+	struct uci_option *tb[NUM_CNTLR_CHANNEL_PLAN_ATTRS];
+	char *endptr = NULL;
+
+	uci_parse_section(s, opts, NUM_CNTLR_CHANNEL_PLAN_ATTRS, tb);
+
+	if (tb[CNTLR_CHANNEL_PLAN_ACS]) {
+		const char *val = tb[CNTLR_CHANNEL_PLAN_ACS]->v.string;
+
+		errno = 0;
+		c->acs = strtol(val, &endptr, 10);
+		if (errno || *endptr != '\0') {
+			cntlr_dbg(LOG_MISC, "|%s:%d| Error parsing channel_plan acs value: %s\n",
+					__func__, __LINE__, val);
+			return -1;
+		}
+	} else {
+		c->acs = false;
+	}
+
+	if (tb[CNTLR_CHANNEL_PLAN_ACS_RECALC_TIMEOUT]) {
+		const char *val = tb[CNTLR_CHANNEL_PLAN_ACS_RECALC_TIMEOUT]->v.string;
+
+		c->acs_timeout = time_str_to_sec(val);
+		if (c->acs_timeout == -1) {
+			cntlr_dbg(LOG_MISC, "|%s:%d| Error parsing channel_plan acs_timeout value: %s\n",
+					__func__, __LINE__, val);
+			return -1;
+		}
+	} else {
+		c->acs_timeout = 0;
+	}
+
+	if (tb[CNTLR_CHANNEL_PLAN_ACS_SKIP_DFS]) {
+		const char *val = tb[CNTLR_CHANNEL_PLAN_ACS_SKIP_DFS]->v.string;
+
+		errno = 0;
+		c->acs_skip_dfs = strtol(val, &endptr, 10);
+		if (errno || *endptr != '\0') {
+			cntlr_dbg(LOG_MISC, "|%s:%d| Error parsing channel_plan skip_dfs value: %s\n",
+					__func__, __LINE__, val);
+			return -1;
+		}
+	} else {
+		c->acs_skip_dfs = false;
+	}
+
+	if (tb[CNTLR_CHANNEL_PLAN_ACS_PREVENT_CAC]) {
+		const char *val = tb[CNTLR_CHANNEL_PLAN_ACS_PREVENT_CAC]->v.string;
+
+		errno = 0;
+		c->acs_prevent_cac = strtol(val, &endptr, 10);
+		if (errno || *endptr != '\0') {
+			cntlr_dbg(LOG_MISC, "|%s:%d| Error parsing channel_plan prevent_cac value: %s\n",
+					__func__, __LINE__, val);
+			return -1;
+		}
+	} else {
+		c->acs_prevent_cac = true;
+	}
+
+	if (tb[CNTLR_CHANNEL_PLAN_ACS_HIGHEST_BANDWIDTH]) {
+		const char *val = tb[CNTLR_CHANNEL_PLAN_ACS_HIGHEST_BANDWIDTH]->v.string;
+
+		errno = 0;
+		c->acs_highest_bandwidth = strtol(val, &endptr, 10);
+		if (errno || *endptr != '\0') {
+			cntlr_dbg(LOG_MISC, "|%s:%d| Error parsing channel_plan higest_bandwidth value: %s\n",
+					__func__, __LINE__, val);
+			return -1;
+		}
+	} else {
+		/* Use configured bandwidth */
+		c->acs_highest_bandwidth = false;
+	}
+
+	if (tb[CNTLR_CHANNEL_PLAN_ACS_SCAN_BEFORE_RECALC]) {
+		const char *val = tb[CNTLR_CHANNEL_PLAN_ACS_SCAN_BEFORE_RECALC]->v.string;
+
+		errno = 0;
+		c->acs_scan_before_recalc = strtol(val, &endptr, 10);
+		if (errno || *endptr != '\0') {
+			cntlr_dbg(LOG_MISC, "|%s:%d| Error parsing channel_plan scan_before_recalc value: %s\n",
+					__func__, __LINE__, val);
+			return -1;
+		}
+	} else {
+		c->acs_scan_before_recalc = true;
+	}
+
+	if (tb[CNTLR_CHANNEL_PLAN_BGCAC]) {
+		const char *val = tb[CNTLR_CHANNEL_PLAN_BGCAC]->v.string;
+
+		errno = 0;
+		c->dfs_cleanup = !!strtol(val, &endptr, 10);
+		if (errno || *endptr != '\0') {
+			cntlr_dbg(LOG_MISC, "|%s:%d| Error parsing preclear_dfs value: %s\n",
+					__func__, __LINE__, val);
+			return -1;
+		}
+	} else {
+		c->dfs_cleanup = false;
+	}
+
+	return 0;
+}
+
 static int cntlr_config_get_credentials(struct controller_config *c,
 						struct uci_section *s)
 {
@@ -3194,6 +3289,8 @@ uint32_t cntlr_config_reload(struct controller_config *cfg)
 			cntlr_config_get_steer_policy(cfg, s);
 			cntlr_load_steer_modules(c);
 			cntlr_reorder_steer_modules(c);
+		} else if (!strcmp(s->type, "channel_plan")) {
+			cntlr_config_get_channel_plan(cfg, s);
 		} else if (!strcmp(s->type, "ap")) {
 			cntlr_config_get_credentials(cfg, s);
 		} else if (!strcmp(s->type, "node")) {
diff --git a/src/config.h b/src/config.h
index cb45fb50..759b2903 100644
--- a/src/config.h
+++ b/src/config.h
@@ -258,8 +258,13 @@ struct controller_config {
 	bool initial_channel_scan;
 	int num_bss;
 	int num_apolicy;
+	bool acs;
 	int acs_timeout;
-	int dfs_cleanup_timeout;
+	bool acs_skip_dfs;
+	bool acs_prevent_cac;
+	bool acs_highest_bandwidth;
+	bool acs_scan_before_recalc;
+	bool dfs_cleanup;
 	int stale_sta_timeout;
 	unsigned int primary_vid;
 	unsigned int default_pcp;
diff --git a/src/timer.c b/src/timer.c
index 04baf847..e8476b2f 100644
--- a/src/timer.c
+++ b/src/timer.c
@@ -76,7 +76,7 @@ int timer_remaining_ms(atimer_t *t)
 	clock_getnow(&now);
 	timersub(&t->expires, &now, &res);
 
-	return res.tv_sec + res.tv_usec / 1000;
+	return res.tv_sec  * 1000 + res.tv_usec / 1000;
 }
 
 void timestamp_reset(struct timespec *ts)
diff --git a/src/utils/utils.c b/src/utils/utils.c
index 3985011c..8d012680 100644
--- a/src/utils/utils.c
+++ b/src/utils/utils.c
@@ -590,6 +590,22 @@ const char *wifi_freqband_to_str(uint32_t band)
 	return NULL;
 }
 
+uint8_t wifi_band_to_freqband(enum wifi_band band)
+{
+	switch (band) {
+	case BAND_2:
+		return 2;
+	case BAND_5:
+		return 5;
+	case BAND_6:
+		return 6;
+	default:
+		break;
+	}
+
+	return 0;
+}
+
 int time_str_to_sec(const char *val)
 {
         if (!val || *val == '\0') {
diff --git a/src/utils/utils.h b/src/utils/utils.h
index 2468d368..08798ad2 100644
--- a/src/utils/utils.h
+++ b/src/utils/utils.h
@@ -160,6 +160,7 @@ int rcpi_to_rssi(uint8_t rcpi);
 bool is_vid_valid(uint16_t vid);
 uint16_t wifi_sec_to_auth_types(enum wifi_security sec);
 const char *wifi_freqband_to_str(uint32_t band);
+uint8_t wifi_band_to_freqband(enum wifi_band band);
 int time_str_to_sec(const char *val);
 
 #define blobmsg_add_macaddr(b, f, v)	\
diff --git a/src/wifi_dataelements.h b/src/wifi_dataelements.h
index 20c53ef4..0ede77b7 100644
--- a/src/wifi_dataelements.h
+++ b/src/wifi_dataelements.h
@@ -636,6 +636,7 @@ struct wifi_radio_element {
 	uint8_t tx_utilization;
 	uint8_t rx_utilization;
 	uint8_t other_utilization;
+
 	uint8_t tx_streams;
 	uint8_t rx_streams;
 	char country_code[4];
@@ -672,6 +673,9 @@ struct wifi_radio_element {
 
 	struct list_head fbss_akmlist;		/* fBSS AKM list */
 	struct list_head bbss_akmlist;		/* bBSS AKM list */
+
+	bool bgcac_supported;
+	void *acs;
 };
 
 struct wifi_default_8021q {
diff --git a/src/wifi_opclass.c b/src/wifi_opclass.c
index af46639f..3ac47d72 100644
--- a/src/wifi_opclass.c
+++ b/src/wifi_opclass.c
@@ -214,7 +214,7 @@ static const struct wifi_radio_opclass e4 = {
 				{ .channel = 153, .ctrl_channels = {153}},
 				{ .channel = 157, .ctrl_channels = {157}},
 				{ .channel = 161, .ctrl_channels = {161}},
-				{ .channel = 164, .ctrl_channels = {164}},
+				{ .channel = 165, .ctrl_channels = {165}},
 				{ .channel = 169, .ctrl_channels = {169}},
 				{ .channel = 173, .ctrl_channels = {173}},
 				{ .channel = 173, .ctrl_channels = {173}},
@@ -616,7 +616,7 @@ void wifi_opclass_reset(struct wifi_radio_opclass *opclass)
 	memset(opclass->opclass, 0, sizeof(opclass->opclass));
 }
 
-void wifi_opclass_dump(struct wifi_radio_opclass *opclass, const char *name, uint8_t *radio)
+void wifi_opclass_dump_ex(struct wifi_radio_opclass *opclass, const char *name, uint8_t *radio, bool full)
 {
 	struct wifi_radio_opclass_entry *entry;
 	char radio_str[18] = {};
@@ -628,15 +628,26 @@ void wifi_opclass_dump(struct wifi_radio_opclass *opclass, const char *name, uin
 	cntlr_dbg(LOG_CHANNEL, ">>> %s %s opclass num: %d\n", radio_str, name ? name : "", opclass->num_opclass);
 	for (i = 0; i < opclass->num_opclass; i++) {
 		entry = &opclass->opclass[i];
-		cntlr_dbg(LOG_CHANNEL, "opclass: %u\n", entry->id);
+
+		if (full)
+			cntlr_trace(LOG_CHANNEL, "opclass: %u\n", entry->id);
 		for (j = 0; j < entry->num_channel; j++) {
-			cntlr_dbg(LOG_CHANNEL, "\tchan %u pref %u reason %u\n",
+			if (!full && !((entry->channel[j].preference & CHANNEL_PREF_MASK) >> 4))
+				continue;
+			if (!full)
+				cntlr_trace(LOG_CHANNEL, "opclass: %u\n", entry->id);
+			cntlr_trace(LOG_CHANNEL, "\tchan %u pref %u reason %u\n",
 			    entry->channel[j].channel,
 			    (entry->channel[j].preference & CHANNEL_PREF_MASK) >> 4,
 			    entry->channel[j].preference & CHANNEL_PREF_REASON);
 		}
 	}
-	cntlr_dbg(LOG_CHANNEL, "<<<\n");
+	cntlr_trace(LOG_CHANNEL, "<<<\n");
+}
+
+void wifi_opclass_dump(struct wifi_radio_opclass *opclass, const char *name, uint8_t *radio)
+{
+	return wifi_opclass_dump_ex(opclass, name, radio, true);
 }
 
 uint8_t wifi_opclass_get_id(struct wifi_radio_opclass *opclass, uint8_t channel, int bandwidth)
@@ -680,7 +691,7 @@ _wifi_opclass_get_higest_preference(struct wifi_radio_opclass *opclass,
 				    int req_bandwidth,
 				    uint8_t *opclass_id,
 				    uint8_t *channel,
-				    uint8_t *bandwidth)
+				    uint16_t *bandwidth)
 {
 	struct wifi_radio_opclass_entry *entry;
 	struct wifi_radio_opclass_channel *chan;
@@ -726,7 +737,7 @@ _wifi_opclass_get_higest_preference(struct wifi_radio_opclass *opclass,
 	return best;
 }
 
-static uint8_t wifi_get_best_ctrl_channel(struct wifi_radio_opclass *opclass, const uint8_t *channels, int channels_num)
+uint8_t wifi_get_best_ctrl_channel(struct wifi_radio_opclass *opclass, const uint8_t *channels, int channels_num)
 {
 	struct wifi_radio_opclass_entry *entry;
 	struct wifi_radio_opclass_channel *chan;
@@ -767,13 +778,14 @@ int wifi_opclass_get_higest_preference(struct wifi_radio_opclass *opclass, int b
 				       uint8_t *opclass_id, uint8_t *channel)
 {
 	struct wifi_radio_opclass_channel *best;
-	uint8_t bw;
+	uint16_t bw;
 
 	best = _wifi_opclass_get_higest_preference(opclass, bandwidth, opclass_id, channel, &bw);
 	if (!best)
 		return -1;
 
 	switch (bw) {
+	case 320:
 	case 160:
 	case 80:
 		*channel = wifi_get_best_ctrl_channel(opclass, best->ctrl_channels, ARRAY_SIZE(best->ctrl_channels));
@@ -836,6 +848,33 @@ bool wifi_opclass_id_same_preference(struct wifi_radio_opclass *opclass, uint8_t
 	return true;
 }
 
+bool wifi_opclass_same_preference(struct wifi_radio_opclass *opclass, uint8_t *preferences)
+{
+	struct wifi_radio_opclass_entry *entry;
+	struct wifi_radio_opclass_channel *chan;
+	uint8_t pref = 0;
+	int i, j;
+
+	for (i = 0; i < opclass->num_opclass; i++) {
+		entry = &opclass->opclass[i];
+
+		for (j = 0; j < entry->num_channel; j++) {
+			chan = &entry->channel[j];
+
+			if (((chan->preference & CHANNEL_PREF_MASK) >> 4) == 0)
+				continue;
+
+			if (pref && pref != ((chan->preference & CHANNEL_PREF_MASK) >> 4))
+				return false;
+
+			pref = (chan->preference & CHANNEL_PREF_MASK) >> 4;
+		}
+	}
+
+	*preferences = pref;
+	return true;
+}
+
 bool wifi_opclass_max_preference(uint8_t preference)
 {
 	uint8_t pref;
@@ -1388,36 +1427,24 @@ bool wifi_opclass_has_channel(uint8_t id, uint8_t channel)
 }
 
 int wifi_radio_opclass_update_channel(struct wifi_radio_opclass *opclass, uint8_t band,
-				      uint32_t channel, uint32_t bw, uint32_t pref)
+				      uint32_t channel, uint32_t bw, uint8_t pref)
 {
 	struct wifi_radio_opclass_channel *channel_entry;
-	int bws[] = {20, 40, 80, 160, 320};
+	int bws[] = {320, 160, 80, 40, 20};
 	int i;
 
-	if (!bw) {
-		for (i = 0; i < ARRAY_SIZE(bws); i++) {
-			channel_entry = wifi_opclass_find_opclass_channel(opclass, band, channel, bws[i]);
-			if (!channel_entry)
-				continue;
-
-			channel_entry->preference = pref;
-		}
-
-		return 0;
-	}
-
-	channel_entry = wifi_opclass_find_opclass_channel(opclass, band, channel, bw);
-	if (!channel_entry)
-		return -1;
-
-	channel_entry->preference = pref;
+	for (i = 0; i < ARRAY_SIZE(bws); i++) {
+		if (bw && bw < bws[i])
+			continue;
 
-	/* Setup also 20MHz channel - so agent will know ctrl channel */
-	if (bw >= 80) {
-		channel_entry = wifi_opclass_find_opclass_channel(opclass, band, channel, 20);
+		channel_entry = wifi_opclass_find_opclass_channel(opclass, band, channel, bws[i]);
 		if (!channel_entry)
-			return -1;
-		channel_entry->preference = pref;
+			continue;
+
+		if (pref > 1)
+			channel_entry->preference = (pref--) << 4;
+		else
+			channel_entry->preference = pref << 4;
 	}
 
 	return 0;
@@ -1426,11 +1453,13 @@ int wifi_radio_opclass_update_channel(struct wifi_radio_opclass *opclass, uint8_
 int wifi_opclass_get_max_bw(struct wifi_radio_opclass *opclass,
 			    uint8_t *ctrl_channel,
 			    uint32_t *bw,
-			    uint8_t *id)
+			    uint8_t *id,
+			    uint8_t *channel)
 {
 	struct wifi_radio_opclass_entry *entry, *max = NULL;
 	uint32_t max_bw = 0;
 	uint8_t ctrl_chan = 0;
+	uint8_t chan = 0;
 	int i;
 
 	for (i = 0; i < opclass->num_opclass; i++) {
@@ -1443,6 +1472,7 @@ int wifi_opclass_get_max_bw(struct wifi_radio_opclass *opclass,
 		if (entry->bandwidth > max_bw) {
 			max_bw = entry->bandwidth;
 			max = entry;
+			chan = entry->channel[0].channel;
 		}
 	}
 
@@ -1457,5 +1487,60 @@ int wifi_opclass_get_max_bw(struct wifi_radio_opclass *opclass,
 	if (ctrl_channel)
 		*ctrl_channel = ctrl_chan;
 
+	if (channel)
+		*channel = chan;
+
 	return 0;
 }
+
+uint32_t wifi_opclass_highest_bandwidth(struct wifi_radio_opclass *opclass, bool available_only)
+{
+	struct wifi_radio_opclass_entry *entry;
+	struct wifi_radio_opclass_channel *chan;
+	uint32_t bandwidth = 0;
+	uint8_t pref = 0;
+	int i, j;
+
+	for (i = 0; i < opclass->num_opclass; i++) {
+		entry = &opclass->opclass[i];
+
+		if (!entry->num_channel)
+			continue;
+
+		for (j = 0; j < entry->num_channel; j++) {
+			chan = &entry->channel[j];
+
+			/* Not supported */
+			pref = (chan->preference & CHANNEL_PREF_MASK) >> 4;
+			if (!pref)
+				continue;
+
+			if (entry->band != 5)
+				break;
+
+			if (chan->dfs == WIFI_RADIO_OPCLASS_CHANNEL_DFS_NONE)
+				break;
+
+			/* Check if we pass CAC */
+			if (available_only &&
+			    chan->dfs != WIFI_RADIO_OPCLASS_CHANNEL_DFS_AVAILABLE)
+				continue;
+
+			/* Skip NOP */
+			if (chan->dfs == WIFI_RADIO_OPCLASS_CHANNEL_DFS_NOP ||
+			    chan->dfs == WIFI_RADIO_OPCLASS_CHANNEL_DFS_CAC)
+				continue;
+
+			break;
+		}
+
+		/* Not found */
+		if (j == entry->num_channel)
+			continue;
+
+		if (entry->bandwidth > bandwidth)
+			bandwidth = entry->bandwidth;
+	}
+
+	return bandwidth;
+}
diff --git a/src/wifi_opclass.h b/src/wifi_opclass.h
index 29220902..ca2366f4 100644
--- a/src/wifi_opclass.h
+++ b/src/wifi_opclass.h
@@ -23,11 +23,13 @@ int wifi_opclass_add_entry(struct wifi_radio_opclass *opclass, struct wifi_radio
 bool wifi_opclass_expired(struct wifi_radio_opclass *opclass, uint32_t seconds);
 void wifi_opclass_reset(struct wifi_radio_opclass *opclass);
 void wifi_opclass_dump(struct wifi_radio_opclass *opclass, const char *name, uint8_t *radio);
+void wifi_opclass_dump_ex(struct wifi_radio_opclass *opclass, const char *name, uint8_t *radio, bool full);
 
 uint8_t wifi_opclass_get_id(struct wifi_radio_opclass *opclass, uint8_t channel, int bandwidth);
 void wifi_opclass_set_preferences(struct wifi_radio_opclass *opclass, uint8_t preference);
 int wifi_opclass_get_higest_preference(struct wifi_radio_opclass *opclass, int bandwith,
 				       uint8_t *opclass_id, uint8_t *channel);
+uint8_t wifi_get_best_ctrl_channel(struct wifi_radio_opclass *opclass, const uint8_t *channels, int channels_num);
 bool wifi_opclass_id_supported(struct wifi_radio_opclass *opclass, uint8_t id);
 uint8_t wifi_opclass_num_supported(struct wifi_radio_opclass *opclass);
 bool wifi_opclass_cac_required(struct wifi_radio_opclass *opclass,
@@ -54,6 +56,7 @@ uint8_t wifi_opclass_find_id_from_channel(struct wifi_radio_opclass *opclass,
 					  int ctrl_channel,
 					  int bandwidth);
 bool wifi_opclass_id_same_preference(struct wifi_radio_opclass *opclass, uint8_t id, uint8_t *pref);
+bool wifi_opclass_same_preference(struct wifi_radio_opclass *opclass, uint8_t *preferences);
 bool wifi_opclass_max_preference(uint8_t preference);
 bool wifi_opclass_is_channel_dfs_available(struct wifi_radio_opclass_channel *chan);
 bool wifi_opclass_is_channel_dfs_nop(struct wifi_radio_opclass_channel *chan);
@@ -67,9 +70,11 @@ struct wifi_radio_opclass_channel *wifi_opclass_get_channel(struct wifi_radio_op
 struct wifi_radio_opclass_channel *wifi_opclass_get_ctrl_channel(struct wifi_radio_opclass *opclass, uint8_t id, uint8_t channel);
 bool wifi_opclass_has_channel(uint8_t id, uint8_t channel);
 int wifi_radio_opclass_update_channel(struct wifi_radio_opclass *opclass, uint8_t band,
-				      uint32_t channel, uint32_t bw, uint32_t pref);
+				      uint32_t channel, uint32_t bw, uint8_t pref);
 int wifi_opclass_get_max_bw(struct wifi_radio_opclass *opclass,
 			    uint8_t *ctrl_channel,
 			    uint32_t *bw,
-			    uint8_t *id);
+			    uint8_t *id,
+			    uint8_t *channel);
+uint32_t wifi_opclass_highest_bandwidth(struct wifi_radio_opclass *opclass, bool available_only);
 #endif /* _WIFI_RADIO_OPCLASS_H_ */
-- 
GitLab