Private/Kinds/Printing.ps1

# The Printing Kind: the print spooler, the default printer, and what is stuck in between.
#
# Printing is where "Word is slow" most often turns out not to be Word. Office asks the
# default printer for its capabilities when a document opens, so a default printer on a
# network that is no longer there - the office printer, from home - holds every document
# open for as long as the connection takes to time out. A spooler crashing on a bad driver
# takes every printer with it, and one job stuck at the head of a queue blocks the rest.
#
# Gathered in the Technician's own session, never the Elevated Part: printer connections
# and the default printer belong to the user, and an administrator's session would see a
# different set.

# How often the default printer is connected to. Not a Check Definition parameter, for the
# reason Private/Sampling.ps1 gives about measurements: one connect is not evidence, and a
# Customer may disagree about a threshold, not about how many attempts make a reading.
$script:PrintingConnectAttempts = 3

# Where the print system records what went wrong. Its Admin channel is enabled on every
# Windows and readable without admin rights.
$script:PrintingLog = 'Microsoft-Windows-PrintService/Admin'

# The port a printer shared from a print server is reached through.
$script:PrintingServerPort = 445

function Get-PrintingData {
    [CmdletBinding()]
    [OutputType([psobject])]
    param([hashtable]$Parameters = @{})

    $days  = [int](Get-Parameter $Parameters 'Days' 30)
    $since = (Get-Date).AddDays(-$days)

    $spooler = Get-CimInstance Win32_Service -Filter "Name='Spooler'" -ErrorAction SilentlyContinue |
        Select-Object -First 1

    $printers = @(Get-CimInstance Win32_Printer -ErrorAction SilentlyContinue | ForEach-Object {
        [pscustomobject]@{
            Name                  = "$($_.Name)"
            Default               = [bool]$_.Default
            Network               = [bool]$_.Network
            WorkOffline           = [bool]$_.WorkOffline
            PrinterStatus         = $_.PrinterStatus
            ExtendedPrinterStatus = $_.ExtendedPrinterStatus
            DetectedErrorState    = $_.DetectedErrorState
            PortName              = "$($_.PortName)"
            ServerName            = "$($_.ServerName)"
            ShareName             = "$($_.ShareName)"
            DriverName            = "$($_.DriverName)"
        }
    })

    $ports = @(Get-CimInstance Win32_TCPIPPrinterPort -ErrorAction SilentlyContinue | ForEach-Object {
        [pscustomobject]@{ Name = "$($_.Name)"; HostAddress = "$($_.HostAddress)"; PortNumber = $_.PortNumber }
    })

    $default = $printers | Where-Object { $_.Default } | Select-Object -First 1
    $reach   = $null
    if ($default) { $reach = Measure-PrinterReachability -Printer $default -Port $ports }

    $now  = Get-Date
    $jobs = @(Get-CimInstance Win32_PrintJob -ErrorAction SilentlyContinue | ForEach-Object {
        $age = $null
        if ($_.TimeSubmitted -is [datetime]) { $age = [math]::Round(($now - $_.TimeSubmitted).TotalMinutes, 1) }
        [pscustomobject]@{
            # Win32_PrintJob names a job "<printer>, <id>"; the printer is what matters.
            Printer    = ("$($_.Name)" -replace ',\s*\d+$', '')
            Document   = "$($_.Document)"
            JobStatus  = "$($_.JobStatus)"
            AgeMinutes = $age
        }
    })

    $refused = @{}
    $errors  = @()
    if (Test-EventLogReadable -Log $script:PrintingLog -Unreadable $refused) {
        try {
            # Level 1 and 2 are Critical and Error. The Admin channel logs little else, but
            # the warnings it does log are about drivers being replaced, which is no problem.
            $errors = @(Get-WinEvent -FilterHashtable @{ LogName = $script:PrintingLog; Level = 1, 2; StartTime = $since } -ErrorAction SilentlyContinue |
                ForEach-Object { ConvertTo-EventRow -LogEntry $_ -Tag 'Print error' })
        }
        catch { }   # "No events matched" arrives as an error too; the log was readable.
    }
    $unreadable = $refused.ContainsKey($script:PrintingLog)

    [pscustomobject]@{
        PSTypeName    = 'Gutcheck.Data.Printing'
        Days          = $days
        SpoolerFound  = $null -ne $spooler
        SpoolerState  = "$($spooler.State)"
        SpoolerMode   = "$($spooler.StartMode)"
        Printers      = $printers
        Reachability  = $reach
        Jobs          = $jobs
        Errors        = $errors
        LogUnreadable = $unreadable
    }
}

function Measure-PrinterReachability {
    <#
    .SYNOPSIS
        Connects to whatever the default printer is reached through, if anything.
    .DESCRIPTION
        A printer shared from a print server is reached through the server, over SMB. A
        printer with its own TCP/IP port is reached directly, on that port. Anything else -
        USB, WSD, a PDF writer - has no address this can connect to, and says so rather
        than reporting a connection nobody made.
    #>

    [CmdletBinding()]
    [OutputType([psobject])]
    param(
        [Parameter(Mandatory)]$Printer,
        [AllowEmptyCollection()][object[]]$Port = @()
    )

    $hostName = $null
    $number   = $null
    $via      = 'None'

    if ($Printer.ServerName) {
        $hostName = $Printer.ServerName.TrimStart('\')
        $number   = $script:PrintingServerPort
        $via      = 'PrintServer'
    }
    else {
        $tcp = @($Port | Where-Object { $_.Name -eq $Printer.PortName }) | Select-Object -First 1
        if ($tcp -and $tcp.HostAddress) {
            $hostName = $tcp.HostAddress
            $number   = [int]$tcp.PortNumber
            $via      = 'TcpPort'
        }
    }

    if (-not $hostName) {
        return [pscustomobject]@{ Via = $via; HostName = $null; Port = $null; Resolved = $null; Attempts = 0; Successes = 0; AverageMs = $null }
    }

    $resolved = $true
    if ($hostName -notmatch '^\d{1,3}(\.\d{1,3}){3}$') {
        $resolved = [bool](Resolve-HostAddress -HostName $hostName).Resolved
    }

    $times = @()
    if ($resolved) {
        $times = @(1..$script:PrintingConnectAttempts | ForEach-Object {
            Measure-TcpConnect -HostName $hostName -Port $number
        } | Where-Object { $null -ne $_ })
    }

    [pscustomobject]@{
        Via       = $via
        HostName  = $hostName
        Port      = $number
        Resolved  = $resolved
        Attempts  = $script:PrintingConnectAttempts
        Successes = $times.Count
        AverageMs = $(if ($times.Count) { [math]::Round(($times | Measure-Object -Average).Average, 1) } else { $null })
    }
}

function ConvertTo-PrintingFinding {
    [CmdletBinding()]
    [OutputType([psobject])]
    param(
        [AllowNull()]$Data,
        [hashtable]$Parameters = @{}
    )

    if (-not (Get-DataProperty $Data 'SpoolerFound')) {
        return New-UnavailableFinding -Category System -Check (Get-Text 'Check.Printing.Spooler') `
            -Hint (Get-Text 'Hint.Printing.SpoolerNotFound')
    }

    if ((Get-DataProperty $Data 'SpoolerState') -ne 'Running') {
        # Nothing below means anything without the spooler: every printer reads as absent.
        return New-Finding -Category System -Check (Get-Text 'Check.Printing.Spooler') -Severity FAIL `
            -Value ((Get-Text 'Value.Printing.SpoolerState') -f (Get-ServiceStateText -State (Get-DataProperty $Data 'SpoolerState')),
                    (ConvertTo-ServiceStartModeText -StartMode (Get-DataProperty $Data 'SpoolerMode'))) `
            -Hint (Get-Text 'Hint.Printing.SpoolerNotRunning')
    }
    New-Finding -Category System -Check (Get-Text 'Check.Printing.Spooler') -Severity OK -Value (Get-Text 'Value.Printing.Running')

    New-DefaultPrinterFinding  -Data $Data -Parameters $Parameters
    New-OfflinePrinterFinding  -Data $Data -Parameters $Parameters
    New-StuckPrintJobFinding   -Data $Data -Parameters $Parameters
    New-PrintErrorFinding      -Data $Data -Parameters $Parameters
}

function Test-PrinterOffline {
    <#
    .SYNOPSIS
        Whether a printer says it cannot print, by any of the three ways Windows says it.
    .DESCRIPTION
        PrinterStatus and ExtendedPrinterStatus 7 are Offline; ExtendedPrinterStatus 9 is
        Error. WorkOffline is the "use printer offline" switch a user or a driver flipped.
    #>

    [CmdletBinding()]
    [OutputType([bool])]
    param([Parameter(Mandatory)]$Printer)

    if (Get-DataProperty $Printer 'WorkOffline') { return $true }
    $status   = ConvertTo-Number (Get-DataProperty $Printer 'PrinterStatus')
    $extended = ConvertTo-Number (Get-DataProperty $Printer 'ExtendedPrinterStatus')
    ($status -eq 7) -or ($extended -eq 7) -or ($extended -eq 9)
}

function New-DefaultPrinterFinding {
    [CmdletBinding()]
    param([AllowNull()]$Data, [hashtable]$Parameters)

    $warnMs = Get-Parameter $Parameters 'PrinterConnectWarnMs' 200
    $failMs = Get-Parameter $Parameters 'PrinterConnectFailMs' ([double]::MaxValue)

    $check   = Get-Text 'Check.Printing.DefaultPrinter'
    $default = @((Get-DataCollection $Data 'Printers') | Where-Object { $_.Default }) | Select-Object -First 1

    if (-not $default) {
        return New-Finding -Category System -Check $check -Severity INFO -Value (Get-Text 'Value.Shared.None') `
            -Hint (Get-Text 'Hint.Printing.NoDefaultPrinter')
    }

    $offline = 'OK'
    if (Test-PrinterOffline -Printer $default) { $offline = 'WARN' }

    $reach = Get-DataProperty $Data 'Reachability'
    $via   = Get-DataProperty $reach 'Via'

    if (-not $reach -or $via -eq 'None') {
        # USB, WSD, a PDF writer: nothing to connect to, so the status is all there is.
        $value = (Get-Text 'Value.Printing.DefaultLocal') -f $default.Name, $default.PortName
        if ($offline -ne 'OK') { $value = '{0} | {1}' -f $value, (Get-Text 'Value.Printing.Reason.Offline') }
        return New-Finding -Category System -Check $check -Severity $offline -Value $value `
            -Hint (Get-Text 'Hint.Printing.DefaultOffline')
    }

    $target    = '{0}:{1}' -f (Get-DataProperty $reach 'HostName'), (Get-DataProperty $reach 'Port')
    $successes = ConvertTo-Number (Get-DataProperty $reach 'Successes')
    $attempts  = ConvertTo-Number (Get-DataProperty $reach 'Attempts')

    if (-not (Get-DataProperty $reach 'Resolved') -or -not $successes) {
        # The case this Check exists for: every document Office opens waits on this.
        return New-Finding -Category System -Check $check -Severity FAIL `
            -Value ((Get-Text 'Value.Printing.DefaultUnreachable') -f $default.Name, $target) `
            -Hint (Get-Text 'Hint.Printing.DefaultUnreachable')
    }

    $average = ConvertTo-Number (Get-DataProperty $reach 'AverageMs')
    $missed  = 'OK'
    if ($successes -lt $attempts) { $missed = 'WARN' }
    $slow = 'OK'
    if ($null -ne $average) { $slow = Get-Severity $average $warnMs $failMs }

    # What is wrong with it, said. The Value used to read "... Verbindung Mittel 1 ms (3/3
    # ok)" under a WARN, and the Hint "slow, unreliable or offline": a Technician could not
    # tell which, and here it was none of the first two.
    $reasons = @()
    $hints   = @()
    if ($offline -ne 'OK') {
        $reasons += Get-Text 'Value.Printing.Reason.Offline'
        $hints   += Get-Text 'Hint.Printing.DefaultOffline'
    }
    if ($missed -ne 'OK') { $reasons += Get-Text 'Value.Printing.Reason.Missed' ($attempts - $successes) $attempts }
    if ($slow -ne 'OK')   { $reasons += Get-Text 'Value.Printing.Reason.Slow' $average }
    if ($missed -ne 'OK' -or $slow -ne 'OK') { $hints += Get-Text 'Hint.Printing.DefaultSlow' }

    $value = (Get-Text 'Value.Printing.DefaultReachable') -f $default.Name, $target, $average, $successes, $attempts
    if ($reasons.Count) { $value = '{0} | {1}' -f $value, ($reasons -join ' | ') }

    New-Finding -Category System -Check $check -Severity (Get-WorstSeverity $offline $missed $slow) `
        -Value $value -Hint ($hints -join ' | ')
}

function New-OfflinePrinterFinding {
    [CmdletBinding()]
    param([AllowNull()]$Data, [hashtable]$Parameters)

    $warn = Get-Parameter $Parameters 'OfflinePrinterWarnAbove' 2
    $fail = Get-Parameter $Parameters 'OfflinePrinterFailAbove' ([double]::MaxValue)

    # The default printer has its own Finding; counting it here too would say it twice.
    $offline = @((Get-DataCollection $Data 'Printers') |
        Where-Object { -not $_.Default -and (Test-PrinterOffline -Printer $_) })

    $check = Get-Text 'Check.Printing.OfflinePrinters'
    if (-not $offline.Count) {
        return New-Finding -Category System -Check $check -Severity OK -Value (Get-Text 'Value.Shared.None')
    }

    # Counted as devices. One printer is often several queues on its print server - one
    # for each tray, one for duplex - whose ports are the printer's address with _1, _2
    # behind it, and four queues of one printer are one printer that reports offline.
    $devices = @($offline | Group-Object { '{0}|{1}' -f "$($_.ServerName)".ToLowerInvariant(), ("$($_.PortName)" -replace '_\d{1,2}$', '').ToLowerInvariant() } |
        ForEach-Object {
            $name = @($_.Group | ForEach-Object { "$($_.Name)" } | Sort-Object { $_.Length }, { $_ })[0]
            [pscustomobject]@{ Name = $name; Queues = $_.Count; OnServer = [bool]"$($_.Group[0].ServerName)" }
        } | Sort-Object Name)

    $severity = Get-Severity $devices.Count $warn $fail
    if ($severity -eq 'OK') { $severity = 'INFO' }

    $named = @($devices | ForEach-Object {
        if ($_.Queues -gt 1) { (Get-Text 'Value.Printing.OfflineQueues') -f $_.Name, $_.Queues } else { $_.Name }
    })

    # "Offline" is what Windows was told, and by whom differs: a print server says it of
    # a printer that does not answer its SNMP query, whether or not the printer prints.
    # That a printer is offline does not make it one nobody uses.
    $meaning = @()
    if (@($devices | Where-Object { $_.OnServer }).Count)      { $meaning += Get-Text 'Meaning.Printing.OfflineOnServer' }
    if (@($devices | Where-Object { -not $_.OnServer }).Count) { $meaning += Get-Text 'Meaning.Printing.OfflineLocal' }

    New-Finding -Category System -Check $check -Severity $severity `
        -Value ((Get-Text 'Value.Printing.OfflinePrinters') -f $devices.Count, ($named -join ', ')) `
        -Meaning ($meaning -join ' | ') -Hint (Get-Text 'Hint.Printing.OfflinePrinters') `
        -Signal 'printer-offline' `
        -Reference @((New-Reference -Signal 'print-job-stuck'), (New-Reference -Section 'Title.Printing.Printers'))
}

function Format-PrintJobAge {
    <#
    .SYNOPSIS
        How long a job has been lying in a queue, in the unit that says it best. Pure.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param([double]$Minutes)

    if ($Minutes -ge 2880) { return (Get-Text 'Value.Printing.Age.Days') -f [math]::Floor($Minutes / 1440) }
    if ($Minutes -ge 120)  { return (Get-Text 'Value.Printing.Age.Hours') -f [math]::Floor($Minutes / 60) }
    (Get-Text 'Value.Printing.Age.Minutes') -f [math]::Floor($Minutes)
}

function New-StuckPrintJobFinding {
    [CmdletBinding()]
    param([AllowNull()]$Data, [hashtable]$Parameters)

    $minutes = Get-Parameter $Parameters 'PrintJobStuckMinutes'  10
    $warn    = Get-Parameter $Parameters 'StuckPrintJobWarnAbove' 0
    $fail    = Get-Parameter $Parameters 'StuckPrintJobFailAbove' 10

    $stuck = @((Get-DataCollection $Data 'Jobs') | Where-Object {
        $age = ConvertTo-Number $_.AgeMinutes
        $null -ne $age -and $age -gt $minutes
    })

    $check = Get-Text 'Check.Printing.StuckJobs'
    if (-not $stuck.Count) {
        return New-Finding -Category System -Check $check -Severity OK -Value (Get-Text 'Value.Shared.None')
    }

    $printers = ($stuck | ForEach-Object { $_.Printer } | Sort-Object -Unique) -join ', '
    $oldest   = ($stuck | ForEach-Object { ConvertTo-Number $_.AgeMinutes } | Measure-Object -Maximum).Maximum

    # A job in a print server's queue is not this machine's to restart a service over.
    $hint = Get-Text 'Hint.Printing.StuckJobs'
    if (@($stuck | Where-Object { "$($_.Printer)".StartsWith('\\') }).Count) { $hint = Get-Text 'Hint.Printing.StuckJobsOnServer' }

    New-Finding -Category System -Check $check -Severity (Get-Severity $stuck.Count $warn $fail) `
        -Value ((Get-Text 'Value.Printing.StuckJobs') -f $stuck.Count, $minutes, $printers, (Format-PrintJobAge -Minutes $oldest)) `
        -Meaning (Get-Text 'Meaning.Printing.StuckJobs') -Hint $hint `
        -Signal 'print-job-stuck' `
        -Reference @((New-Reference -Signal 'printer-offline'), (New-Reference -Section 'Title.Printing.Jobs'))
}

function New-PrintErrorFinding {
    [CmdletBinding()]
    param([AllowNull()]$Data, [hashtable]$Parameters)

    $warn = Get-Parameter $Parameters 'PrintErrorWarnAbove' 10
    $fail = Get-Parameter $Parameters 'PrintErrorFailAbove' ([double]::MaxValue)

    $check = Get-Text 'Check.Printing.Errors'
    if (Get-DataProperty $Data 'LogUnreadable') {
        return New-UnavailableFinding -Category System -Check $check -Hint (Get-Text 'Hint.Printing.LogUnreadable')
    }

    $errors = Get-DataCollection $Data 'Errors'
    $days   = Get-DataProperty $Data 'Days'
    if (-not $errors.Count) {
        return New-Finding -Category System -Check $check -Severity OK -Value ((Get-Text 'Value.Printing.NoErrors') -f $days)
    }

    # The most frequent event id is the lead, and is said with what it means.
    $common = $errors | Group-Object Id | Sort-Object Count -Descending | Select-Object -First 3 |
        ForEach-Object {
            $id = Format-CodeWithMeaning -Code ('ID {0}' -f $_.Name) -Meaning (Get-PrintErrorIdText -Id $_.Name)
            (Get-Text 'Value.Printing.ErrorCount') -f $id, $_.Count
        }

    New-Finding -Category System -Check $check -Severity (Get-Severity $errors.Count $warn $fail) `
        -Value ((Get-Text 'Value.Printing.Errors') -f $errors.Count, $days, ($common -join ', ')) `
        -Hint (Get-Text 'Hint.Printing.Errors')
}

function Get-PrintErrorIdText {
    <#
    .SYNOPSIS
        What an error event of the print service means. Nothing for one not known here,
        which is then shown as the number it is. Pure.
    .DESCRIPTION
        Not a table of Microsoft's: read off the messages the Microsoft-Windows-PrintService
        provider carries for its events ((Get-WinEvent -ListProvider ...).Events), each
        error by its own message. The numbers between them are events that are no error -
        a driver added, a printer published - and have no meaning here, as they are not
        among what is counted.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param([AllowNull()]$Id)

    $number = ConvertTo-Number $Id
    if ($null -eq $number) { return '' }
    switch ([int]$number) {
        808 { return (Get-Text 'Value.Printing.ErrorId.DriverLoad') }
        372 { return (Get-Text 'Value.Printing.ErrorId.DocumentFailed') }
        314 { return (Get-Text 'Value.Printing.ErrorId.Timeout') }
        315 { return (Get-Text 'Value.Printing.ErrorId.ShareFailed') }
        371 { return (Get-Text 'Value.Printing.ErrorId.UnshareFailed') }
        318 { return (Get-Text 'Value.Printing.ErrorId.SettingsReset') }
        319 { return (Get-Text 'Value.Printing.ErrorId.DriverMissing') }
        320 { return (Get-Text 'Value.Printing.ErrorId.PortMissing') }
        { $_ -in 322, 323, 325, 326, 327, 328, 329, 331, 333, 335, 337 } { return (Get-Text 'Value.Printing.ErrorId.DirectoryPublish') }
        { $_ -in 348, 351 } { return (Get-Text 'Value.Printing.ErrorId.DriverIncompatible') }
        350 { return (Get-Text 'Value.Printing.ErrorId.SpoolFileCorrupt') }
        353 { return (Get-Text 'Value.Printing.ErrorId.NoRights') }
        354 { return (Get-Text 'Value.Printing.ErrorId.InitFailed') }
        360 { return (Get-Text 'Value.Printing.ErrorId.ColorProfile') }
        361 { return (Get-Text 'Value.Printing.ErrorId.PortInit') }
        362 { return (Get-Text 'Value.Printing.ErrorId.OwnNameNotResolved') }
        363 { return (Get-Text 'Value.Printing.ErrorId.SpoolerStart') }
        { $_ -in 364, 365, 367 } { return (Get-Text 'Value.Printing.ErrorId.PrintProcessor') }
        366 { return (Get-Text 'Value.Printing.ErrorId.SecurityDescriptor') }
        { $_ -in 368, 369, 370 } { return (Get-Text 'Value.Printing.ErrorId.DriverPackage') }
        373 { return (Get-Text 'Value.Printing.ErrorId.GdiObjects') }
    }
    ''
}

function ConvertTo-PrintingSection {
    [CmdletBinding()]
    [OutputType([psobject])]
    param([AllowNull()]$Data)

    New-Section -Title (Get-Text 'Title.Printing.Printers') -Row @(
        (Get-DataCollection $Data 'Printers') | ForEach-Object {
            $row = [ordered]@{}
            $row[(Get-Text 'Column.Printing.Name')]        = $_.Name
            $row[(Get-Text 'Column.Printing.Default')]     = Get-TriStateText ([bool]$_.Default)
            $row[(Get-Text 'Column.Printing.Network')]     = Get-TriStateText ([bool]$_.Network)
            $row[(Get-Text 'Column.Printing.WorkOffline')] = Get-TriStateText ([bool]$_.WorkOffline)
            # What the two numbers mean, and the numbers: a code is never shown bare.
            $row[(Get-Text 'Column.Printing.Status')]      = Get-PrinterStatusText -PrinterStatus $_.PrinterStatus -ExtendedPrinterStatus $_.ExtendedPrinterStatus
            $row[(Get-Text 'Column.Printing.Port')]        = $_.PortName
            $row[(Get-Text 'Column.Printing.Server')]      = $_.ServerName
            $row[(Get-Text 'Column.Printing.Driver')]      = $_.DriverName
            [pscustomobject]$row
        }
    )
    New-Section -Title (Get-Text 'Title.Printing.Jobs') -Row @(
        (Get-DataCollection $Data 'Jobs') | ForEach-Object {
            $age = ConvertTo-Number (Get-DataProperty $_ 'AgeMinutes')
            $row = [ordered]@{}
            $row[(Get-Text 'Column.Printing.Name')]         = Get-DataProperty $_ 'Printer'
            $row[(Get-Text 'Column.Printing.Job.Document')] = Get-DataProperty $_ 'Document'
            # In Windows' own words and language: the spooler hands it over as text.
            $row[(Get-Text 'Column.Printing.Job.Status')]   = Get-DataProperty $_ 'JobStatus'
            $row[(Get-Text 'Column.Printing.Job.Age')]      = $(if ($null -ne $age) { Format-PrintJobAge -Minutes $age } else { '' })
            [pscustomobject]$row
        }
    )
}

function Get-PrinterStatusText {
    <#
    .SYNOPSIS
        What Windows reports of a printer, in a word, with the two numbers behind it. Pure.
    .DESCRIPTION
        Win32_Printer has two status properties, and the extended one says more: 3 idle,
        4 printing, 7 offline, 8 paused, 9 error and so on, as Microsoft documents them
        for that class. Where the extended one is "unknown" or absent, the plain one
        (3 idle, 4 printing, 7 offline) is what there is.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param([AllowNull()]$PrinterStatus, [AllowNull()]$ExtendedPrinterStatus)

    $plain    = ConvertTo-Number $PrinterStatus
    $extended = ConvertTo-Number $ExtendedPrinterStatus
    $read = $extended
    if ($null -eq $read -or $read -in 1, 2) { if ($null -ne $plain -and $plain -notin 1, 2) { $read = $plain } }

    $text = switch ($read) {
        2  { Get-Text 'Value.Printing.Status.Unknown' }
        3  { Get-Text 'Value.Printing.Status.Idle' }
        4  { Get-Text 'Value.Printing.Status.Printing' }
        5  { Get-Text 'Value.Printing.Status.WarmingUp' }
        6  { Get-Text 'Value.Printing.Status.Stopped' }
        7  { Get-Text 'Value.Printing.Status.Offline' }
        8  { Get-Text 'Value.Printing.Status.Paused' }
        9  { Get-Text 'Value.Printing.Status.Error' }
        10 { Get-Text 'Value.Printing.Status.Busy' }
        11 { Get-Text 'Value.Printing.Status.NotAvailable' }
        12 { Get-Text 'Value.Printing.Status.Waiting' }
        13 { Get-Text 'Value.Printing.Status.Processing' }
        14 { Get-Text 'Value.Printing.Status.Initializing' }
        15 { Get-Text 'Value.Printing.Status.PowerSave' }
        16 { Get-Text 'Value.Printing.Status.PendingDeletion' }
        17 { Get-Text 'Value.Printing.Status.IoActive' }
        18 { Get-Text 'Value.Printing.Status.ManualFeed' }
        default { Get-Text 'Value.Printing.Status.Other' }
    }
    (Get-Text 'Value.Printing.Status') -f $text, ('{0}/{1}' -f $PrinterStatus, $ExtendedPrinterStatus)
}

function ConvertTo-PrintingEvent {
    [CmdletBinding()]
    [OutputType([psobject])]
    param([AllowNull()]$Data)

    (Get-DataCollection $Data 'Errors')
}