functions/Connect-TibberWebSocket.ps1

function Connect-TibberWebSocket {
    <#
    .Synopsis
        Create a new GraphQL over WebSocket connection.
    .Description
        Calling this function will return a connection object for the established WebSocket connection.
        The object returned is intended to be used with other functions for communication with the endpoint.
    .Example
        $connection = Connect-TibberWebSocket
        Write-Host "New connection created: $($connection.WebSocket.State)"
    .Link
        https://docs.microsoft.com/en-us/dotnet/api/system.net.websockets.clientwebsocket
    .Link
        https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md
    .Link
        https://developer.tibber.com/docs/guides/calling-api
    #>

    param (
        # Specifies the URI for the request.
        [Parameter(ValueFromPipelineByPropertyName)]
        [Alias('URL')]
        [Uri] $URI = 'wss://api.tibber.com/v1-beta/gql/subscriptions',

        # Specifies the access token to use for the communication.
        [Parameter(ValueFromPipelineByPropertyName)]
        [Alias('PAT', 'AccessToken', 'Token')]
        [string] $PersonalAccessToken = $(
            if ($script:TibberAccessTokenCache) {
                $script:TibberAccessTokenCache
            }
            elseif ($env:TIBBER_ACCESS_TOKEN) {
                $env:TIBBER_ACCESS_TOKEN
            }
            else {
                '5K4MVS-OjfWhK_4yrjOlFe1F6kJXPVf7eQYggo8ebAE' # demo token
            }
        ),

        # Specifies the number of retry attempts if WebSocket initialization fails.
        [ValidateRange(0, [int]::MaxValue)]
        [Alias('Retries')]
        [int] $RetryCount = 5,

        # Specifies for how long in seconds we should wait between retries.
        [ValidateRange(0, [int]::MaxValue)]
        [Alias('RetryWaitTime', 'Timeout')]
        [int] $RetryWaitTimeInSeconds = 5
    )

    process {
        $retryCounter = $RetryCount
        while ($retryCounter-- -ge 0) {
            # If this is a retry, release used resources
            if ($webSocket) {
                $webSocket.Dispose()
                $cancellationTokenSource.Dispose()
            }

            # Setup WebSocket for communication
            $webSocket = New-Object Net.WebSockets.ClientWebSocket
            $webSocket.Options.AddSubProtocol('graphql-transport-ws')
            $cancellationTokenSource = New-Object Threading.CancellationTokenSource
            $cancellationToken = $cancellationTokenSource.Token
            $recvBuffer = New-Object ArraySegment[byte] -ArgumentList @(, $([byte[]] @(, 0) * 16384))

            # Connect WebSocket
            $result = $webSocket.ConnectAsync($URI, $cancellationToken)
            while (-Not $result.IsCompleted) {
                Start-Sleep -Milliseconds 10
            }
            Write-Verbose "WebSocket connected to $URI"

            # Init WebSocket
            $command = @{
                type    = 'connection_init'
                payload = @{
                    token = $PersonalAccessToken
                }
            } | ConvertTo-Json -Depth 10
            Write-Debug -Message ("Invoking GraphQL over WebSocket command: " + $URI)
            Write-Debug -Message "GraphQL over WebSocket command:"
            Write-Debug -Message $command
            $sendCommand = New-Object ArraySegment[byte] -ArgumentList @(, $([Text.Encoding]::ASCII.GetBytes($command)))
            $result = $webSocket.SendAsync($sendCommand, [Net.WebSockets.WebSocketMessageType]::Text, $true, $cancellationToken)
            while (-Not $result.IsCompleted) {
                Start-Sleep -Milliseconds 10
            }
            Write-Debug -Message "WebSocket status:"
            Write-Debug -Message ($webSocket | Select-Object * | Out-String)
            Write-Debug -Message "WebSocket operation result:"
            Write-Debug -Message ($result | Select-Object * | Out-String)
            if ($result.Result.CloseStatus) {
                throw "Send failed: $($result.Result.CloseStatusDescription) [$($result.Result.CloseStatus)]"
            }
            Write-Verbose "Init message sent"

            # WebSocket init acknowledgement
            $result = $webSocket.ReceiveAsync($recvBuffer, $cancellationToken)
            while (-Not $result.IsCompleted) {
                Start-Sleep -Milliseconds 10
            }
            Write-Debug -Message "WebSocket status:"
            Write-Debug -Message ($webSocket | Select-Object * | Out-String)
            Write-Debug -Message "WebSocket operation result:"
            Write-Debug -Message ($result | Select-Object * | Out-String)
            if ($result.Result.CloseStatus) {
                $errorMessage = "Receive failed: $($result.Result.CloseStatusDescription) [$($result.Result.CloseStatus)]"
                if ($retryCounter -gt 0) {
                    Write-Verbose "$errorMessage"
                    Write-Verbose "Retrying in $RetryWaitTimeInSeconds seconds, $retryCounter attempts left"
                    Start-Sleep -Seconds $RetryWaitTimeInSeconds
                    continue
                }
                throw $errorMessage
            }
            $response = [Text.Encoding]::ASCII.GetString($recvBuffer.Array, 0, $result.Result.Count)
            Write-Verbose "Init response: $response"

            # Output connection object
            return [PSCustomObject]@{
                URI                     = $URI
                WebSocket               = $webSocket
                CancellationTokenSource = $cancellationTokenSource
                RecvBuffer              = $recvBuffer
                ConnectionAttempts      = $RetryCount - $retryCounter
            }
        }
    }
}