mirror of
https://github.com/arendst/Tasmota.git
synced 2026-10-27 06:22:25 +00:00
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:
4 files changed
+173
-1
No files matched your search
@@ -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 ----------
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user