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:

  • TR-181 supported devices

  • USP (TR-369) supported devices

  • Devices with BULKDATA capabilities properly working

Parameters in Use

RSSI (Received Signal Strength Indicator)

Data Model Parameter Path

AccessPoint

Device.WiFi.AccessPoint.{i}.AssociatedDevice.{i}.SignalStrength

DataElements

Device.WiFi.DataElements.Network.Device.{i}.Radio.{i}.BSS.{i}.STA.{i}.SignalStrength

MultiAP

Device.WiFi.MultiAP.APDevice.{i}.Radio.{i}.AP.{i}.AssociatedDevice.{i}.SignalStrength

For Satellites (backhaul signal):

Data Model Parameter Path

DataElements

Device.WiFi.DataElements.Network.Device.{i}.MultiAPDevice.Backhaul.Stats.SignalStrength

MultiAP

Device.WiFi.MultiAP.APDevice.{i}.BackhaulSignalStrength

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

AssociatedDevice.SignalStrength - Device.WiFi.Radio.{i}.Stats.Noise

DataElements

STA.SignalStrength - Device.WiFi.DataElements.Network.Device.{i}.Radio.{i}.Noise

MultiAP

AssociatedDevice.SignalStrength - AssociatedDevice.Noise

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

…​AssociatedDevice.{i}.Stats.BytesSent

…​STA.{i}.BytesSent

…​AssociatedDevice.{i}.Stats.BytesSent

Bytes Received

…​AssociatedDevice.{i}.Stats.BytesReceived

…​STA.{i}.BytesReceived

…​AssociatedDevice.{i}.Stats.BytesReceived

Packets Sent

…​AssociatedDevice.{i}.Stats.PacketsSent

…​STA.{i}.PacketsSent

…​AssociatedDevice.{i}.Stats.PacketsSent

Packets Received

…​AssociatedDevice.{i}.Stats.PacketsReceived

…​STA.{i}.PacketsReceived

…​AssociatedDevice.{i}.Stats.PacketsReceived

Errors Sent

…​AssociatedDevice.{i}.Stats.ErrorsSent

…​STA.{i}.ErrorsSent

…​AssociatedDevice.{i}.Stats.ErrorsSent

Errors Received

…​AssociatedDevice.{i}.Stats.ErrorsReceived

…​STA.{i}.ErrorsReceived

…​AssociatedDevice.{i}.Stats.ErrorsReceived

Retransmissions

…​AssociatedDevice.{i}.Stats.RetransCount

…​STA.{i}.RetransCount

…​AssociatedDevice.{i}.Stats.RetransCount

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

Device.WiFi.AccessPoint.{i}.AssociatedDevice.{i}.LastDataUplinkRate Device.WiFi.DataElements…​STA.{i}.LastDataUplinkRate Device.WiFi.MultiAP…​AssociatedDevice.{i}.LastDataUplinkRate

Last Data Downlink Rate

Device.WiFi.AccessPoint.{i}.AssociatedDevice.{i}.LastDataDownlinkRate Device.WiFi.DataElements…​STA.{i}.LastDataDownlinkRate Device.WiFi.MultiAP…​AssociatedDevice.{i}.LastDataDownlinkRate

Max Supported Uplink Rate

Device.WiFi.AccessPoint.{i}.AssociatedDevice.{i}.MaxSupportedDataUplinkRate

Max Supported Downlink Rate

Device.WiFi.AccessPoint.{i}.AssociatedDevice.{i}.MaxSupportedDataDownlinkRate

Note: Max Supported rates only available in AccessPoint path. Exclude rate scores for other paths.

Steering History

Data Model Parameter Path

DataElements

Device.WiFi.DataElements.Network.Device.{i}.Radio.{i}.BSS.{i}.STA.{i}.MultiAPSTA.SteeringHistory.{i}.

MultiAP

Device.WiFi.MultiAP.APDevice.{i}.Radio.{i}.AP.{i}.AssociatedDevice.{i}.SteeringHistory.{i}.

Counting Rules:

  1. Count number of SteeringHistory instances for each device

  2. Only include instances with SteeringHistory.{i}.Time no 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].

2. SNR Normalization

Based on range (0 dB to 80 dB):

Normalized SNR = SNR / 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.

6. Data Rate Scores

Downlink Rate Score = LastDownlinkRate / MaxSupportedDownlinkRate
Uplink Rate Score = LastUplinkRate / MaxSupportedUplinkRate

Clamp to [0, 1].

Note: Only applicable for Device.WiFi.AccessPoint.{i}.AssociatedDevice.{i} path. Exclude for other data models.

7. Steering Stability Score

Steering Stability Score = 1 - (SteeringEvents / MaxAllowedEvents)

Where MaxAllowedEvents = 20.

Clamp to [0, 1]: - 1.0 = No steering events - 0.0 = Excessive steering (20+ events in 24 hours)

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%

Step 3: Calculate Aggregated Score

Aggregated Score = Σ (Normalized Metric × Weight)

Precision Rules:

  • Intermediate values: Round to 6 decimal places maximum

  • Final score: Round to 1 decimal place

Step 4: Convert to Health Score

Health Score = Aggregated Score × 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:

  1. Remove the weight of every missing metric

  2. 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.

RSSI-Only Health Score Formula

Health Score = 100 - (RSSI × -1)
Health Score = 100 + RSSI

Examples:

RSSI = -55 dBm → Health Score = 100 + (-55) = 45
RSSI = -70 dBm → Health Score = 100 + (-70) = 30
RSSI = -40 dBm → Health Score = 100 + (-40) = 60

RSSI-Only Interpretation

Health Score Range Status Color

40-100

Good

Green

21-39

Average

Yellow

0-20

Low

Red

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'