Send-StreamDeck.ps1

function Send-StreamDeck {
    <#
    .Synopsis
        Sends messages to a StreamDeck
    .Description
        Sends messages to a StreamDeck.
        
        This function will often be used within StreamDeck plugins.
    .Link
        Receive-StreamDeck
    .Link
        https://developer.elgato.com/documentation/stream-deck/sdk/events-sent/
    #>

    [CmdletBinding(SupportsShouldProcess,DefaultParameterSetName='EventName')]
    param(
    # The name of the event
    [Parameter(Mandatory,Position=0,ValueFromPipelineByPropertyName,ParameterSetName='EventName')]
    [Alias('Event')]
    [string]
    $EventName,

    # The event payload.
    [Parameter(Position=2,ParameterSetName='EventName',ValueFromPipelineByPropertyName)]
    [PSObject]
    $Payload,

    # If set, will send a showOk event to the Stream Deck application.
    # This will temporarily show an OK checkmark icon on the image displayed by an instance of an action.
    [Parameter(Mandatory,ParameterSetName='showOK',ValueFromPipelineByPropertyName)]
    [switch]
    $ShowOK,

    # If set, will send a showOk event to the Stream Deck application.
    # This will temporarily show an alert icon on the image displayed by an instance of an action.
    [Parameter(Mandatory,ParameterSetName='showAlert',ValueFromPipelineByPropertyName)]
    [switch]
    $ShowAlert,

    # If set, will send an openURL event to the Stream Deck application.
    # This will temporarily show an alert icon on the image displayed by an instance of an action.
    [Parameter(Mandatory,ParameterSetName='openURL',ValueFromPipelineByPropertyName)]
    [uri]
    $OpenURL,

    # If set, will send an openURL event to the Stream Deck application.
    # This will temporarily show an alert icon on the image displayed by an instance of an action.
    [Parameter(Mandatory,ParameterSetName='logMessage',ValueFromPipelineByPropertyName)]
    [string]
    $LogMessage,

    # If provided will send a showImage event to the Stream Deck application using the contents of the file in ImagePath
    [Parameter(Mandatory,ParameterSetName='setImage',ValueFromPipelineByPropertyName)]
    [Alias('Fullname')]
    [string]
    $ImagePath,

    # The state index of an image or title. Defaults to zero.
    [Parameter(ParameterSetName='setTitle',ValueFromPipelineByPropertyName)]
    [Parameter(ParameterSetName='setImage',ValueFromPipelineByPropertyName)]
    [int]
    $State = 0,

    # The target of a title or image change. Valid values are
    [Parameter(ParameterSetName='setTitle',ValueFromPipelineByPropertyName)]
    [Parameter(ParameterSetName='setImage',ValueFromPipelineByPropertyName)]
    [ValidateSet('both','hardware', 'software')]
    [string]
    $EventTarget = 'both',

    # The event context.
    # If not provided, the global variable STREAMDECK_CONTEXT will be used
    [Parameter(Position=3,ValueFromPipelineByPropertyName)]
    [string]
    $Context,

    # The maximum amount of time to wait for a WebSocket to open. By default, 30 seconds.
    [Parameter(ValueFromPipelineByPropertyName)]
    [Timespan]
    $WaitFor      = '00:00:30',

    # The interval to wait while receiving a message. By default, 11 milliseconds.
    [TimeSpan]$WaitInterval = '00:00:00.011',

    # The web socket.
    # If not provided, the global variable STREAMDECK_WEBSOCKET will be used.
    [Parameter(Position=4,ValueFromPipelineByPropertyName)]
    [Net.WebSockets.ClientWebSocket]
    $Websocket,

    # The web socket.
    # If not provided, the global variable STREAMDECK_WEBSOCKET will be used.
    [Parameter(Position=4,ValueFromPipelineByPropertyName)]
    [int]
    $Port,

    # The plugin UUID. This is used in plugin registration.
    [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='registerPlugin')]
    [string]
    $PluginUUID
    )
    process {
        # If no -WebSocket was provided, use the $global:STREAMDECK_WEBSOCKET
        if (-not $Websocket -and $Global:STREAMDECK_WEBSOCKET) {
            $Websocket = $Global:STREAMDECK_WEBSOCKET 
        }

        # A number of different parameter sets are named to reflect the EventName StreamDeck expects.
        # If we find an argument that hints that this is true
        if ($ShowOK -or $ShowAlert -or $OpenURL -or $LogMessage -or $ImagePath) {
            $EventName = $PSCmdlet.ParameterSetName # set -EventName to the $psCmdlet.ParameterSetName.
        }

        if ($OpenURL) { # If we're going to -OpenURL,
            $Payload = @{url="$OpenURL"} # the payload is a single property, URL.
        }

        if ($LogMessage) { # If we're going to -LogMessage
            $Payload = @{message=$LogMessage} # the payload is a single property, message.
        }
        
        if ($ImagePath) { # If we're going to send an image,
            # find the file
            $resolvedPath = $ExecutionContext.SessionState.Path.GetResolvedPSPathFromPSPath($ImagePath) 
            if (-not $resolvedPath) { return } 
            $resolvedItem = Get-Item -LiteralPath $resolvedPath 
            if (-not $resolvedItem) { return } 
            # and check that it actually is an image.
            $imageExtensions = '.svg','.png','.jpg','.gif','.bmp'
            if ($resolvedItem.Extension -notin $imageExtensions) {
                Write-Error "-ImagePath '$ImagePath' has an invalid extension. Valid extensions are: $imageExtensions"
                return
            }
            # Ensure that -EventTarget is lowercase and create a payload with the image data.
            $eventTarget = $EventTarget.ToLower()
            if ($resolvedItem.Extension -eq '.svg') { # If the image was an SVG,
                $Payload = [Ordered]@{
                    image = "data:image/svg+xml;charset=utf8,$( # it will be inline UTF8 text
                        [IO.File]::ReadAllText($resolvedItem.FullName, [Text.Encoding]::UTF8)
                    )"

                    target = $eventTarget
                    state  = $State
                }
            } else {
                $Payload = [Ordered]@{ # Otherwise, it's base64 binary.
                    image = "data:image/$($resolvedItem.Extension.TrimStart('.'));base64,$(
                        [Convert]::ToBase64String([IO.File]::ReadAllBytes($resolvedItem.FullName))
                    )"

                    target = $eventTarget
                    state  = $State
                }
            }
        }

        # If no one provided a -Context but $Global:STREAMDECK_CONTEXT is set,
        if ((-not $Context) -and $Global:STREAMDECK_CONTEXT) { 
            $Context = $Global:STREAMDECK_CONTEXT # use that.
        }
        
        $WebSocketPayload = @{
            event   = $EventName
            context = $Context
            payload = $Payload
        }        

        if (-not $WebSocketPayload.payload -or $WebSocketPayload.payload.count -eq 0) { # If the payload was blank
            $WebSocketPayload.Remove('payload') # remove it
        }
        if (-not $WebSocketPayload.context) { # If the context was blank,
            $WebSocketPayload.Remove('context') # remove it.
        }
        if ($PluginUUID) { # If we've got a plugin UUID,
            $WebSocketPayload.uuid = $PluginUUID # set .uuid
        }
        if ($WhatIfPreference) { # If -WhatIf was passed
            return $WebSocketPayload # return the payload.
        }
        
        # If -Confirm was passed, and they chose not to send this payload, return.
        if (-not $PSCmdlet.ShouldProcess("Send $($WebSocketPayload | ConvertTo-json)")) {  return  }

        # If we don't have a context or a pluginUUID:
        if (-not $Context -and -not $PluginUUID) {
            Write-Error "Must provide -Context" -ErrorId Context.Missing -Category InvalidArgument # error out.
            return
        }

        if (-not $Websocket ){ # If we don't have a websocket:
            Write-Error "Must provide a -WebSocket" -ErrorId WebSocket.Missing -Category ConnectionError # error out.
            return
        }
        
                
        $PayloadJson  = $WebSocketPayload | ConvertTo-Json -Depth 100   # Construct the payload
        $SendSegment  = [ArraySegment[Byte]]::new([Text.Encoding]::UTF8.GetBytes($PayloadJson))        
        $SendTask     = $Websocket.SendAsync($SendSegment, 'Binary', $true, 
                            [Threading.CancellationToken]::new($false)) # send it
        while (!$SendTask.IsCompleted) { # and wait for it to be sent.
            Start-Sleep -Milliseconds $WaitInterval.TotalMilliseconds
        }
    }
}