WiFi Client Health Score Calculation
Purpose
This document outlines the methodology for calculating an aggregated health score for each WiFi connected client to assess their overall user experience. This scoring system leverages data collected from the TR-181 data model, which provides a standardized framework for managing and monitoring devices.
The health score provides:
-
Quantifiable measure of client performance
-
Priority guidance for support engineers
-
Basis for tailored solutions and recommendations
Limitations and Assumptions
|
This WiFi connected client’s health feature is designed for:
|
Parameters in Use
RSSI (Received Signal Strength Indicator)
| Data Model | Parameter Path |
|---|---|
AccessPoint |
|
DataElements |
|
MultiAP |
|
For Satellites (backhaul signal):
| Data Model | Parameter Path |
|---|---|
DataElements |
|
MultiAP |
|
RCPI Conversion: See RCPI to RSSI Conversion section.
SNR (Signal-to-Noise Ratio)
Direct Parameter:
Device.WiFi.AccessPoint.{i}.AssociatedDevice.{i}.SNR
Calculated (if direct not available):
| Data Model | Calculation |
|---|---|
AccessPoint |
|
DataElements |
|
MultiAP |
|
For Satellites: Determine connection point via mesh flow, then use parent radio’s Noise:
Device.WiFi.DataElements.Network.Device.{i}.MultiAPDevice.Backhaul.Stats.SignalStrength
- Device.WiFi.DataElements.Network.Device.{y}.Radio.{y}.Noise
Note: If Noise value is positive (RCPI format), apply conversion: Noise [dBm] = Noise/2 - 110
Operating Standard
Direct Parameter:
Device.WiFi.AccessPoint.{i}.AssociatedDevice.{i}.OperatingStandard
Detection via Capabilities (DataElements):
Determine operating standard by checking the highest non-empty capability parameter:
def detect_operating_standard(sta):
# Check WiFi 6 (802.11ax)
if sta.WiFi6Capabilities and any_param_non_empty(sta.WiFi6Capabilities):
return "802.11ax"
# Check WiFi 5 (802.11ac)
if sta.VHTCapabilities and is_non_empty(sta.VHTCapabilities):
return "802.11ac"
# Check WiFi 4 (802.11n)
if sta.HTCapabilities and is_non_empty(sta.HTCapabilities):
return "802.11n"
# Legacy standards based on frequency band
return "802.11a/g" # 802.11a for 5GHz, 802.11g for 2.4GHz
Capability Parameter Paths:
Device.WiFi.DataElements.Network.Device.{i}.Radio.{i}.BSS.{i}.STA.{i}.WiFi6Capabilities.
Device.WiFi.DataElements.Network.Device.{i}.Radio.{i}.BSS.{i}.STA.{i}.VHTCapabilities
Device.WiFi.DataElements.Network.Device.{i}.Radio.{i}.BSS.{i}.STA.{i}.HTCapabilities
Traffic Statistics
| Metric | AccessPoint Path | DataElements Path | MultiAP Path |
|---|---|---|---|
Bytes Sent |
|
|
|
Bytes Received |
|
|
|
Packets Sent |
|
|
|
Packets Received |
|
|
|
Errors Sent |
|
|
|
Errors Received |
|
|
|
Retransmissions |
|
|
|
For Satellites (DataElements):
Device.WiFi.DataElements.Network.Device.{i}.MultiAPDevice.Backhaul.Stats.PacketsSent
Device.WiFi.DataElements.Network.Device.{i}.MultiAPDevice.Backhaul.Stats.PacketsReceived
Device.WiFi.DataElements.Network.Device.{i}.MultiAPDevice.Backhaul.Stats.ErrorsSent
Device.WiFi.DataElements.Network.Device.{i}.MultiAPDevice.Backhaul.Stats.ErrorsReceived
Data Rates
| Metric | Parameter Paths |
|---|---|
Last Data Uplink Rate |
|
Last Data Downlink Rate |
|
Max Supported Uplink Rate |
|
Max Supported Downlink Rate |
|
Note: Max Supported rates only available in AccessPoint path. Exclude rate scores for other paths.
Steering History
| Data Model | Parameter Path |
|---|---|
DataElements |
|
MultiAP |
|
Counting Rules:
-
Count number of SteeringHistory instances for each device
-
Only include instances with
SteeringHistory.{i}.Timeno older than 24 hours
def count_recent_steering_events(steering_history, current_time):
count = 0
for event in steering_history:
if (current_time - event.Time) <= timedelta(hours=24):
count += 1
return count
RCPI to RSSI Conversion
If SignalStrength is a positive value (RCPI format), convert to RSSI:
RSSI [dBm] = RCPI / 2 - 110
RCPI Value Handling:
| RCPI Value | RSSI Result | Action |
|---|---|---|
0 |
-110 dBm |
Use as RSSI |
1-220 |
-109.5 to 0 dBm |
Apply formula, round mathematically |
221-255 |
N/A |
No RSSI available (exclude from calculation) |
Rounding Example:
RCPI = 65
RSSI = 65 / 2 - 110
RSSI = 32.5 - 110
RSSI = -77.5 dBm
Rounded: -78 dBm (mathematical rounding)
Health Score Calculation
Step 1: Normalize Each Metric
Normalize each metric to a scale of 0 to 1 for comparability.
1. RSSI Normalization
Based on typical range (-100 dBm to -20 dBm):
Normalized RSSI = (RSSI + 100) / 80
Clamp to [0, 1].
3. Operating Standard Score
Fixed scores based on WiFi standard:
| Operating Standard | Score |
|---|---|
802.11be (WiFi 7) |
1.0 |
802.11ax (WiFi 6) |
1.0 |
802.11ac (WiFi 5) |
0.8 |
802.11n (WiFi 4) |
0.5 |
802.11g or below |
0.3 |
4. Error Score
Error Score = 1 - (TotalErrors / TotalPackets)
Clamp to [0, 1].
Where:
TotalErrors = TotalErrors_current - TotalErrors_previous
TotalPackets = TotalPackets_current - TotalPackets_previous
Note: If no previous values available, use 0.
5. Retransmission Score
Retransmission Score = 1 - (TotalRetransmissions / TotalPacketsReceived)
Clamp to [0, 1].
Where:
TotalRetransmissions = Retrans_current - Retrans_previous
TotalPacketsReceived = PacketsReceived_current - PacketsReceived_previous
Note: If no previous values available, use 0.
Step 2: Assign Weights
| Metric | Weight |
|---|---|
RSSI |
20% |
SNR |
20% |
Operating Standard |
15% |
Error Score |
15% |
Retransmission Score |
10% |
Downlink Rate Score |
10% |
Uplink Rate Score |
5% |
Steering Stability Score |
5% |
Total |
100% |
Example Calculation
Client Data:
| Metric | Value |
|---|---|
RSSI |
-55 dBm |
SNR |
32 dB |
Operating Standard |
802.11ac |
Total Errors (delta) |
30 |
Total Packets (delta) |
1500 |
Retransmissions (delta) |
20 |
Last Downlink Rate |
400 Mbps |
Max Downlink Rate |
867 Mbps |
Last Uplink Rate |
300 Mbps |
Max Uplink Rate |
867 Mbps |
Steering Events (24h) |
2 |
Normalized Metrics:
RSSI: (-55 + 100) / 80 = 0.563
SNR: 32 / 80 = 0.4
Operating Std: 0.8 (802.11ac)
Error Score: 1 - (30 / 1500) = 0.98
Retrans Score: 1 - (20 / 1500) = 0.987
DL Rate Score: 400 / 867 = 0.461
UL Rate Score: 300 / 867 = 0.346
Steering Score: 1 - (2 / 20) = 0.9
Aggregated Score:
Score = (0.563 × 0.20) + (0.4 × 0.20) + (0.8 × 0.15) + (0.98 × 0.15)
+ (0.987 × 0.10) + (0.461 × 0.10) + (0.346 × 0.05) + (0.9 × 0.05)
Score = 0.1126 + 0.08 + 0.12 + 0.147 + 0.0987 + 0.0461 + 0.0173 + 0.045
Score = 0.6667 → 0.7 (rounded)
Health Score = 0.7 × 100 = 70
Score Interpretation
| Score Range | Status | Color | Description |
|---|---|---|---|
70-100 |
Good |
Green |
Excellent performance |
40-69 |
Average |
Yellow |
Good performance; minor issues may exist |
0-39 |
Low |
Red |
Poor performance; immediate action needed |
Weight Redistribution (Missing Parameters)
When metrics are unavailable, redistribute weights proportionally.
Rule:
-
Remove the weight of every missing metric
-
Scale remaining weights proportionally so they sum to 100%
Formula:
New Weight = (Baseline Weight / Sum of Available Weights) × 100
Example: SNR Missing
| Metric | Available? | Baseline | New Weight |
|---|---|---|---|
RSSI |
Yes |
20% |
20 / 80 × 100 = 25.0% |
SNR |
No |
20% |
— |
Operating Std |
Yes |
15% |
15 / 80 × 100 = 18.7% |
Error Score |
Yes |
15% |
15 / 80 × 100 = 18.7% |
Retrans Score |
Yes |
10% |
10 / 80 × 100 = 12.5% |
DL Rate Score |
Yes |
10% |
10 / 80 × 100 = 12.5% |
UL Rate Score |
Yes |
5% |
5 / 80 × 100 = 6.2% |
Steering Score |
Yes |
5% |
5 / 80 × 100 = 6.2% |
Total |
80% |
~100% |
Note: Round new weights down (drop decimals) to ensure sum doesn’t exceed 100%.
RSSI-Only Fallback
When no other KPIs are available (no SNR, no Operating Standard, no Steering History, no Data Rates), display only RSSI-based health score.
Implementation Reference
class ClientHealthCalculator:
WEIGHTS = {
'rssi': 0.20,
'snr': 0.20,
'operating_standard': 0.15,
'error_score': 0.15,
'retransmission_score': 0.10,
'dl_rate_score': 0.10,
'ul_rate_score': 0.05,
'steering_score': 0.05
}
STANDARD_SCORES = {
'802.11be': 1.0,
'802.11ax': 1.0,
'802.11ac': 0.8,
'802.11n': 0.5,
'802.11g': 0.3,
'802.11a': 0.3,
'legacy': 0.3
}
def calculate_health_score(self, client_metrics):
available_metrics = {}
available_weights = {}
# Normalize RSSI
if client_metrics.rssi is not None:
normalized_rssi = self._clamp((client_metrics.rssi + 100) / 80)
available_metrics['rssi'] = normalized_rssi
available_weights['rssi'] = self.WEIGHTS['rssi']
# Normalize SNR
if client_metrics.snr is not None:
normalized_snr = self._clamp(client_metrics.snr / 80)
available_metrics['snr'] = normalized_snr
available_weights['snr'] = self.WEIGHTS['snr']
# Operating Standard
if client_metrics.operating_standard:
std_score = self.STANDARD_SCORES.get(
client_metrics.operating_standard, 0.3)
available_metrics['operating_standard'] = std_score
available_weights['operating_standard'] = self.WEIGHTS['operating_standard']
# Error Score
if client_metrics.total_packets > 0:
error_score = self._clamp(
1 - (client_metrics.total_errors / client_metrics.total_packets))
available_metrics['error_score'] = error_score
available_weights['error_score'] = self.WEIGHTS['error_score']
# Retransmission Score
if client_metrics.packets_received > 0:
retrans_score = self._clamp(
1 - (client_metrics.retransmissions / client_metrics.packets_received))
available_metrics['retransmission_score'] = retrans_score
available_weights['retransmission_score'] = self.WEIGHTS['retransmission_score']
# Data Rate Scores (AccessPoint path only)
if client_metrics.max_dl_rate and client_metrics.max_dl_rate > 0:
dl_score = self._clamp(
client_metrics.last_dl_rate / client_metrics.max_dl_rate)
available_metrics['dl_rate_score'] = dl_score
available_weights['dl_rate_score'] = self.WEIGHTS['dl_rate_score']
if client_metrics.max_ul_rate and client_metrics.max_ul_rate > 0:
ul_score = self._clamp(
client_metrics.last_ul_rate / client_metrics.max_ul_rate)
available_metrics['ul_rate_score'] = ul_score
available_weights['ul_rate_score'] = self.WEIGHTS['ul_rate_score']
# Steering Score
if client_metrics.steering_events is not None:
steering_score = self._clamp(
1 - (client_metrics.steering_events / 20))
available_metrics['steering_score'] = steering_score
available_weights['steering_score'] = self.WEIGHTS['steering_score']
# Check for RSSI-only fallback
if len(available_metrics) == 1 and 'rssi' in available_metrics:
return self._rssi_only_score(client_metrics.rssi)
if not available_metrics:
return None
# Redistribute weights
total_weight = sum(available_weights.values())
normalized_weights = {
k: v / total_weight for k, v in available_weights.items()
}
# Calculate aggregated score
aggregated_score = sum(
available_metrics[k] * normalized_weights[k]
for k in available_metrics
)
# Round to 1 decimal place
return round(aggregated_score * 100, 0)
def _rssi_only_score(self, rssi):
return max(0, min(100, 100 + rssi))
def _clamp(self, value, min_val=0, max_val=1):
return max(min_val, min(max_val, value))
def get_status(self, health_score, rssi_only=False):
if rssi_only:
if health_score >= 40:
return 'Good', 'green'
elif health_score >= 21:
return 'Average', 'yellow'
else:
return 'Low', 'red'
else:
if health_score >= 70:
return 'Good', 'green'
elif health_score >= 40:
return 'Average', 'yellow'
else:
return 'Low', 'red'
Related Documentation
-
Backhaul Information - RSSI/RCPI conversion details