Matter add Wi-Fi Network Diagnostics cluster (0x0036) (#25126)

* Matter add Wi-Fi Network Diagnostics cluster (0x0036)

Expose BSSID, SecurityType, WiFiVersion, ChannelNumber and RSSI on the
Root endpoint so controllers (Apple Home, Home Assistant) can show which
access point and channel a device uses. BSSID and channel changes are
reported to subscribers when the device roams.

tasmota.wifi() now also reports `channel`, `security` and `phy`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Clamp RSSI

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: s-hadinger <49731213+s-hadinger@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-10-11 17:41:47 +02:00
1 parent bf2afdc7f1
commit bcc37a50ab
4 files changed
+173 -1

No files matched your search

+2
View File
@@ -8,6 +8,8 @@ All notable changes to this project will be documented in this file.
- LVGL option to prefer PSRAM for draw buffers using bit 5 (0x20) of `:B` in display.ini (#25120)
- Matter support for Light RBG+CT (5 channels) and refactoring of lights
- Initial support for ESP32-S31 (#25130)
- Matter Wi-Fi Network Diagnostics cluster (BSSID, security, Wi-Fi version, channel, RSSI)
- Berry `tasmota.wifi()` reports `channel`, `security` and `phy`
### Breaking Changed
@@ -599,6 +599,37 @@ class Matter_Plugin_Root : Matter_Plugin
# - Heap metrics: Memory usage statistics
#################################################################################
#################################################################################
# Matter 1.6.1 Wi-Fi Network Diagnostics Cluster (0x0036)
#################################################################################
# Cluster Revision: 1
# Role: Utility | Scope: Node
#
# Provides Wi-Fi link information (access point, channel, signal strength).
#
# FEATURES: none (PKTCNT and ERRCNT counters are not supported)
#
# ATTRIBUTES:
# ID | Name | Type | Constraint | Quality | Default | Access | Conf
# -------|---------------|------------------|------------|---------|---------|--------|-----
# 0x0000 | BSSID | octstr | 6 | X | null | R V | M
# 0x0001 | SecurityType | SecurityTypeEnum | all | X | null | R V | M
# 0x0002 | WiFiVersion | WiFiVersionEnum | all | X | null | R V | M
# 0x0003 | ChannelNumber | uint16 | all | X | null | R V | M
# 0x0004 | RSSI | int8 | -120 to 0 | X C | null | R V | M
#
# SecurityTypeEnum:
# 0=Unspecified, 1=None, 2=WEP, 3=WPA, 4=WPA2, 5=WPA3
#
# WiFiVersionEnum:
# 0=a, 1=b, 2=g, 3=n, 4=ac, 5=ax, 6=ah
#
# NOTES:
# - All attributes are null when Wi-Fi is not connected
# - WiFiVersion reports the highest PHY mode supported by the access point
# - BSSID and ChannelNumber changes (roaming) are reported to subscribers
#################################################################################
static var TYPE = "root" # name of the plug-in in json
static var DISPLAY_NAME = "Root node" # display name of the plug-in
@@ -615,6 +646,7 @@ class Matter_Plugin_Root : Matter_Plugin
0x0032: [], # Diagnostic Logs Cluster 11.10 p.637
0x0033: [0,1,2,8], # General Diagnostics Cluster 11.11 p.642
0x0034: [], # Software Diagnostics Cluster 11.12 p.654
0x0036: [0,1,2,3,4], # Wi-Fi Network Diagnostics Cluster 11.8 (Matter 1.6.1)
0x0038: [0,1,7], # Time Synchronization 11.16 p.689
0x003C: [0,1,2], # Administrator Commissioning Cluster 11.18 p.725
0x003E: [0,1,2,3,4,5], # Node Operational Credentials Cluster 11.17 p.704
@@ -629,11 +661,15 @@ class Matter_Plugin_Root : Matter_Plugin
0x0046: [0,1,2] # ICD Management Cluster - base SIT mode (no CIP/LITS features)
})
static var TYPES = { 0x0016: 5 } # Root node - Matter 1.6.1 Device Library Rev 5
static var WIFI_SECURITY = { 'open': 1, 'wep': 2, 'wpa': 3, 'wpa2': 4, 'wpa3': 5 } # SecurityTypeEnum
static var WIFI_VERSION = { '11a': 0, '11b': 1, '11g': 2, '11n': 3, '11ac': 4, '11ax': 5 } # WiFiVersionEnum
# static var MATTER_EPOCH_OFFSET = 946684800 # seconds from Unix epoch to 2000-01-01
var _group_key_map_write_staged # request-scoped candidate GroupKeyMap
var _group_key_map_write_failed # prevents partial GroupKeyMap persistence
var _write_exchange_id # identifies a multi-message list transaction
var _write_session # prevents transaction reuse by another CASE peer
var _wifi_bssid # last reported BSSID, to detect roaming
var _wifi_channel # last reported Wi-Fi channel
#############################################################
# Constructor
@@ -644,6 +680,27 @@ class Matter_Plugin_Root : Matter_Plugin
self.publish_event(0x0033, 0x03, 2 #-matter.EVENT_CRITICAL-#, matter.TLV.Matter_TLV_item().set(0x06 #-matter.TLV.U4-#, 1)) # Event BootReason - PowerOnReboot - TODO if we need to refine
end
#############################################################
# Report Wi-Fi Network Diagnostics changes when the device
# roams to another access point or channel
def update_shadow()
var tas_wif = tasmota.wifi()
var up = tas_wif['up']
var bssid = up ? tas_wif.find('bssid') : nil
var channel = up ? tas_wif.find('channel') : nil
if bssid != self._wifi_bssid
self.attribute_updated(0x0036, 0x0000) # BSSID
self.attribute_updated(0x0036, 0x0001) # SecurityType
self.attribute_updated(0x0036, 0x0002) # WiFiVersion
self._wifi_bssid = bssid
end
if channel != self._wifi_channel
self.attribute_updated(0x0036, 0x0003) # ChannelNumber
self._wifi_channel = channel
end
super(self).update_shadow()
end
# Open or resume the GroupKeyMap list-write transaction for this exchange.
# The transaction survives MoreChunkedMessages.
def begin_write_request(msg)
@@ -997,6 +1054,32 @@ class Matter_Plugin_Root : Matter_Plugin
elif cluster == 0x0034 # ========== Software Diagnostics Cluster 11.12 p.654 ==========
# no mandatory attributes - to be added later (maybe)
# ====================================================================================================
elif cluster == 0x0036 # ========== Wi-Fi Network Diagnostics Cluster 11.8 ==========
var tas_wif = tasmota.wifi()
if !tas_wif['up'] tas_wif = {} end # all attributes are null when not connected
if attribute == 0x0000 # ---------- BSSID / octstr ----------
var bssid = tas_wif.find('bssid')
if bssid != nil bssid = bytes().fromhex(string.replace(bssid, ":", "")) end
return tlv_solo.set_or_nil(0x10 #-TLV.B1-#, bssid)
elif attribute == 0x0001 # ---------- SecurityType / enum8 ----------
var security = tas_wif.find('security')
return tlv_solo.set_or_nil(0x04 #-TLV.U1-#, (security != nil) ? self.WIFI_SECURITY.find(security, 0) : nil)
elif attribute == 0x0002 # ---------- WiFiVersion / enum8 ----------
return tlv_solo.set_or_nil(0x04 #-TLV.U1-#, self.WIFI_VERSION.find(tas_wif.find('phy', '')))
elif attribute == 0x0003 # ---------- ChannelNumber / u16 ----------
return tlv_solo.set_or_nil(0x05 #-TLV.U2-#, tas_wif.find('channel'))
elif attribute == 0x0004 # ---------- RSSI / int8 (-120..0) ----------
var rssi = tas_wif.find('rssi')
if rssi != nil
if rssi < -120 rssi = -120
elif rssi > 0 rssi = 0
end
end
return tlv_solo.set_or_nil(0x02 #-TLV.I4-#, rssi)
end
# ====================================================================================================
elif cluster == 0x0038 # ========== Time Synchronization 11.16 p.689 ==========
if attribute == 0x0000 # ---------- UTCTime / epoch_us ----------
+58 -1
View File
@@ -41,12 +41,14 @@ end
matter.get_attribute_name = def (cluster, attribute) return nil end
class TestTasmota
var utc_time, local_time, now
var utc_time, local_time, now, wifi_info
def init()
self.utc_time = 1
self.local_time = 1700000000
self.now = 0
self.wifi_info = {'up': false}
end
def wifi() return self.wifi_info end
def rtc_utc() return self.utc_time end
def rtc(mode) return self.local_time end
def millis() return self.now end
@@ -329,6 +331,61 @@ ctx.attribute = 0x0000
assert(root.read_attribute(session, ctx, TLV.Matter_TLV_item()).typ == 0x14)
print(" Matter epoch conversion for UTC and local time: OK")
# Wi-Fi Network Diagnostics attributes are null when Wi-Fi is not connected.
ctx = matter.Path(0, 0x0036, 0x0000)
for attr : 0..4
ctx.attribute = attr
assert(root.read_attribute(session, ctx, TLV.Matter_TLV_item()).typ == 0x14)
end
tasmota.wifi_info = {'up': true, 'bssid': 'AA:BB:CC:01:02:03', 'channel': 6, 'rssi': -61,
'security': 'wpa2', 'phy': '11ax'}
ctx.attribute = 0x0000
assert(root.read_attribute(session, ctx, TLV.Matter_TLV_item()).val == bytes("AABBCC010203"))
ctx.attribute = 0x0001
assert(root.read_attribute(session, ctx, TLV.Matter_TLV_item()).val == 4)
ctx.attribute = 0x0002
assert(root.read_attribute(session, ctx, TLV.Matter_TLV_item()).val == 5)
ctx.attribute = 0x0003
assert(root.read_attribute(session, ctx, TLV.Matter_TLV_item()).val == 6)
ctx.attribute = 0x0004
var rssi = root.read_attribute(session, ctx, TLV.Matter_TLV_item())
assert(rssi.typ == 0x00 && rssi.val == -61)
assert(rssi.tlv2raw() == bytes("00C3"))
tasmota.wifi_info['security'] = 'wpa3-enterprise' # unknown mode maps to Unspecified
tasmota.wifi_info['phy'] = '' # unknown PHY is null
ctx.attribute = 0x0001
assert(root.read_attribute(session, ctx, TLV.Matter_TLV_item()).val == 0)
ctx.attribute = 0x0002
assert(root.read_attribute(session, ctx, TLV.Matter_TLV_item()).typ == 0x14)
# Roaming to another access point or channel is reported to subscribers.
class TestWifiDevice
var tick, updates
def init() self.tick = 0 self.updates = [] end
def attribute_updated(endpoint, cluster, attribute, fabric_specific)
self.updates.push(attribute)
end
end
var wifi_root = TestRoot()
wifi_root.device = TestWifiDevice()
wifi_root.update_shadow()
assert(wifi_root.device.updates == [0, 1, 2, 3])
wifi_root.device.updates = []
wifi_root.update_shadow()
assert(wifi_root.device.updates == [])
tasmota.wifi_info['bssid'] = 'AA:BB:CC:01:02:04'
wifi_root.update_shadow()
assert(wifi_root.device.updates == [0, 1, 2])
wifi_root.device.updates = []
tasmota.wifi_info['channel'] = 11
wifi_root.update_shadow()
assert(wifi_root.device.updates == [3])
wifi_root.device.updates = []
tasmota.wifi_info = {'up': false}
wifi_root.update_shadow()
assert(wifi_root.device.updates == [0, 1, 2, 3])
print(" Wi-Fi Network Diagnostics attributes and roaming: OK")
ctx = matter.Path(0, 0x003F, 0xFFFB)
var attribute_list = root.read_attribute(session, ctx, TLV.Matter_TLV_item())
var attribute_ids = []
@@ -22,6 +22,7 @@
#include <berry.h>
#include <Wire.h>
#include "esp_wifi.h"
const uint32_t BERRY_MAX_LOGS = 16; // max number of print output recorded when outside of REPL, used to avoid infinite grow of logs
const uint32_t BERRY_MAX_REPL_LOGS = 50; // max number of print output recorded when inside REPL
@@ -283,6 +284,35 @@ extern "C" {
be_map_insert_int(vm, "quality", WifiGetRssiAsQuality(rssi));
be_map_insert_str(vm, "ssid", SettingsTextEscaped(SET_STASSID1 + Settings->sta_active).c_str());
be_map_insert_str(vm, "bssid", WiFi.BSSIDstr().c_str());
be_map_insert_int(vm, "channel", WiFi.channel());
wifi_ap_record_t ap_info;
if (ESP_OK == esp_wifi_sta_get_ap_info(&ap_info)) {
const char * security = "";
switch (ap_info.authmode) {
case WIFI_AUTH_OPEN: security = "open"; break;
case WIFI_AUTH_WEP: security = "wep"; break;
case WIFI_AUTH_WPA_PSK: security = "wpa"; break;
case WIFI_AUTH_WPA2_PSK:
case WIFI_AUTH_WPA_WPA2_PSK:
case WIFI_AUTH_WPA2_ENTERPRISE: security = "wpa2"; break;
case WIFI_AUTH_WPA3_PSK:
case WIFI_AUTH_WPA2_WPA3_PSK:
case WIFI_AUTH_WPA3_ENT_192:
case WIFI_AUTH_WPA3_EXT_PSK:
case WIFI_AUTH_WPA3_EXT_PSK_MIXED_MODE: security = "wpa3"; break;
default: break;
}
be_map_insert_str(vm, "security", security);
// highest PHY mode supported by the access point
const char * phy = "";
if (ap_info.phy_11ax) { phy = "11ax"; }
else if (ap_info.phy_11ac) { phy = "11ac"; }
else if (ap_info.phy_11n) { phy = "11n"; }
else if (ap_info.phy_11g) { phy = "11g"; }
else if (ap_info.phy_11b) { phy = "11b"; }
else if (ap_info.phy_11a) { phy = "11a"; }
be_map_insert_str(vm, "phy", phy);
}
}
}
be_pop(vm, 1);