Classes/PerformanceCounter.ps1

using namespace System.Collections.Generic

class PerformanceCounter {
    [string]    $counterID
    [string]    $counterSetType
    [string]    $counterInstance
    [string]    $CounterPath
    [string]    $Title
    [string]    $Type
    [string]    $Format
    [int]       $conversionFactor
    [int]       $conversionExponent
    [string]    $Unit
    [hashtable] $graphConfiguration
    [List[int]] $HistoricalData
    [hashtable] $ColorMap
    [hashtable] $Statistics
    [bool]      $IsAvailable
    [string]    $LastError
    [datetime]  $LastUpdate


    # Constructor
    PerformanceCounter([string]$counterID, [string]$counterSetType, [string]$counterInstance, [string]$title, [string]$Type, [string]$Format, [string]$unit, [int]$conversionFactor, [int]$conversionExponent, [psobject]$colorMap, [psobject]$graphConfiguration) {
        $this.counterID             = $counterID
        $this.counterSetType        = $counterSetType
        $this.counterInstance       = $counterInstance
        $this.CounterPath           = $this.GetCounterPath($counterID, $counterSetType, $counterInstance)
        $this.Title                 = $title
        $this.Type                  = $Type
        $this.Format                = $Format
        $this.Unit                  = $unit
        $this.conversionFactor      = $conversionFactor
        $this.conversionExponent    = $conversionExponent
        $this.ColorMap              = $this.SetColorMap($colorMap)
        $this.GraphConfiguration    = $this.SetGraphConfig($graphConfiguration)
        $this.HistoricalData        = [List[int]]::new()
        $this.Statistics            = @{}
        $this.IsAvailable           = $false
        $this.LastError             = ""
    }

    [hashtable] SetColorMap([psobject]$colorMap) {

        $returnObject = @{}
        foreach ( $property in $colorMap.PSObject.Properties ) {
            $returnObject[[int]$property.Name] = $property.Value
        }
        return $returnObject

    }

    [hashtable] SetGraphConfig([psobject]$graphConfiguration) {

        $returnObject = @{}
        foreach ( $property in $graphConfiguration.PSObject.Properties ) {

                if ( $property.Name -eq "colors" -and $property.Value ) {
                    $Colors = @{}
                    foreach ( $colorProperty in $property.Value.PSObject.Properties ) {
                        $Colors[$colorProperty.Name] = $colorProperty.Value
                    }

                    $returnObject["Colors"] = $Colors

                } else {

                    # override min values for specific properties
                    if ( $property.Name -eq "Samples" -and $property.Value -lt 70 ) {
                        $returnObject[$property.Name] = 70
                    } elseif ( $property.Name -eq "yAxisMaxRows" -and $property.Value -lt 10 ) {
                        $returnObject[$property.Name] = 10
                    } else {
                        $returnObject[$property.Name] = $property.Value
                    }

                }
            }
        return $returnObject

    }

    [string] GetCounterPath([string]$counterID, [string]$counterSetType, [string]$counterInstance) {

        [string] $returnObject = ""

        if ( -not $counterID ) {
            throw "Counter ID cannot be null or empty."
        }

        try {

            $setID          = $counterID.Split('-')[0]
            $pathID         = $counterID.Split('-')[1]

            $setName        = Get-PerformanceCounterLocalName -Id $setID    -ErrorAction Stop
            $pathName       = Get-PerformanceCounterLocalName -Id $pathID   -ErrorAction Stop

            if ( $counterSetType -eq 'SingleInstance'  ) {
                $returnObject = "\$($setName)\$($pathName)"
            } elseif ( $counterSetType -eq 'MultiInstance' ) {
                $returnObject = "\$setName($counterInstance)\$pathName"
            } else {
                throw "Unknown counter set type: $counterSetType"
            }

            return $returnObject

        } catch {
            Throw "Error getting counter path for ID '$counterID': $($_.Exception.Message)"
        }

    }

    # Test if counter is available
    [bool] TestAvailability() {

        try {
            $null               = Get-Counter -Counter $this.CounterPath -MaxSamples 1 -ErrorAction Stop
            $this.IsAvailable   = $true
            $this.LastError     = ""
            return $true
        } catch {
            $this.IsAvailable   = $false
            $this.LastError     = $_.Exception.Message
            return $false
        }

    }

    # Add new data point
    [void] AddDataPoint([int]$value, [int]$maxDataPoints = 100) {

        $this.HistoricalData.Add($value)
        $this.LastUpdate = Get-Date

        # Limit historical data size, drop oldest point
        while ( $this.HistoricalData.Count -gt $maxDataPoints ) {
            $this.HistoricalData.RemoveAt(0)
        }

        # Update statistics
        $this.UpdateStatistics()

    }

    # Update statistics
    [void] UpdateStatistics() {

        if ( $this.HistoricalData.Count -eq 0 ) { return }

        $data = $this.HistoricalData.ToArray()

        $this.Statistics = @{
            Current = $data[-1]
            Minimum = ($data | Measure-Object -Minimum).Minimum
            Maximum = ($data | Measure-Object -Maximum).Maximum
            Average = [Math]::Round(($data | Measure-Object -Average).Average, 1)
            Count   = $data.Count
            Last5   = if ($data.Count -ge 5) { $data[-5..-1] } else { $data }
        }

    }

    # Get current value from performance counter
    [int] GetCurrentValue() {

        if ( -not $this.IsAvailable ) {
            throw "Counter '$($this.Title)' is not available: $($this.LastError)"
        }

        try {

            $sample = (Get-Counter -Counter $this.CounterPath -MaxSamples 1).CounterSamples
            $value  = $sample.CookedValue

            # Convert Units
            $value = [Math]::Round($value / [Math]::Pow($this.conversionFactor, $this.conversionExponent))

            return $value


        } catch {
            $this.LastError = $_.Exception.Message
            throw "Error reading counter '$($this.Title)': $($_.Exception.Message)"
        }

    }

    # Get color for a specific value
    [string] GetColorForValue([int]$value) {

        if ( $this.ColorMap.Count -eq 0 ) { return "White" }

        # map color by thresholds
        $sortedThresholds = $this.ColorMap.Keys | Sort-Object -Descending

        foreach ( $threshold in $sortedThresholds ) {
            if ( $value -ge $threshold ) {
                return $this.ColorMap[$threshold]
            }
        }

        return "White"  # Default color

    }

    # Get formatted title with unit
    [string] GetFormattedTitle() {

        if ( [string]::IsNullOrEmpty($this.Unit) ) { return $this.Title }
        return "$($this.Title) ($($this.Unit))"

    }

    # Get data for graphing (padded to target width)
    [int[]] GetGraphData([int]$targetWidth) {

        $dataCount = $this.HistoricalData.Count
        if ( $dataCount -eq 0 ) { return @() }

        # Take the last targetWidth points
        if ( $dataCount -ge $targetWidth ) {
            return $this.HistoricalData.GetRange($dataCount - $targetWidth, $targetWidth).ToArray()

        # Pad with zeros at the beginning
        } else {
            $padding = @(0) * ($targetWidth - $dataCount)
            return $padding + $this.HistoricalData.ToArray()
        }
    }

    # ToString override for debugging
    [string] ToString() {
        return "PerformanceCounter: $($this.Title) - Available: $($this.IsAvailable) - Data Points: $($this.HistoricalData.Count)"
    }
}