#region Connect-OmadaController Function Connect-OmadaController { <# .SYNOPSIS Establishes a connection context for an Omada controller, preferring the controller interface and degrading to the Open API. .DESCRIPTION Two transports exist and they are not equivalent. The controller interface, reached under /{omadacId}/api/v2, is what the Omada web interface itself consumes. It exposes the complete data set including switch uplinks and downlinks, per port LLDP neighbour detail, VLAN definitions with their gateway subnets, and the real SSID configuration. It is authenticated with a controller login and is not a published contract, so it can change between controller releases. The Open API, reached under /openapi/v1, is a published and versioned contract authenticated with a client identifier and secret. It is stable but narrower. It has no uplink information at all and does not expose SSID configuration, which forces SSIDs to be inferred from active client sessions. This function therefore prefers the controller interface and falls back to the Open API, so that the richer topology is used when it is available and collection still succeeds when it is not. The returned context records which mode was selected and carries whichever credentials were successfully established, so a caller can degrade a single request rather than the whole run. .PARAMETER BaseURL The base URL of the Omada controller. .PARAMETER Credential The controller login used for the preferred transport. When omitted, the Open API is used directly. .PARAMETER ClientID The Open API client identifier used for the fallback transport. .PARAMETER ClientSecret The Open API client secret used for the fallback transport. .PARAMETER SkipCertificateCheck Ignores server certificate validation errors. Usually required because controllers present a self signed certificate. .EXAMPLE $OmadaContext = Connect-OmadaController -BaseURL $BaseURL -Credential $Credential -ClientID $ClientID -ClientSecret $ClientSecret -SkipCertificateCheck .NOTES Author: Grace Solutions Version: 2026.08.02.0000 #> [CmdletBinding()] Param ( [Parameter(Mandatory=$True)] [ValidateNotNullOrEmpty()] [System.URI]$BaseURL, [Parameter(Mandatory=$False)] [AllowNull()] [System.Management.Automation.PSCredential]$Credential, [Parameter(Mandatory=$False)] [AllowEmptyString()] [String]$ClientID = '', [Parameter(Mandatory=$False)] [AllowEmptyString()] [String]$ClientSecret = '', [Parameter(Mandatory=$False)] [Switch]$SkipCertificateCheck ) Try { $BaseAddress = "$("$($BaseURL.AbsoluteUri)".TrimEnd('/'))" #region The controller identifier is unauthenticated and is required by both transports $ControllerInformationResponse = Invoke-RestAPIRequest -URI ([System.URI]"$($BaseAddress)/api/info") -Method 'GET' -SkipCertificateCheck:($SkipCertificateCheck.IsPresent) Switch ($ControllerInformationResponse.IsSuccess) { {($_ -eq $False)} { Throw "The Omada controller information could not be retrieved from `"$($BaseAddress)`". [Status Code: $($ControllerInformationResponse.StatusCode)] [$($ControllerInformationResponse.ErrorMessage)]" } } $OmadacID = "$($ControllerInformationResponse.Content.result.omadacId)" Switch ([String]::IsNullOrWhiteSpace($OmadacID)) { {($_ -eq $True)} { Throw "The Omada controller at `"$($BaseAddress)`" did not return a controller identifier." } } #endregion $ContextProperties = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' $ContextProperties.BaseAddress = "$($BaseAddress)" $ContextProperties.OmadacID = "$($OmadacID)" $ContextProperties.ControllerVersion = "$($ControllerInformationResponse.Content.result.controllerVer)" $ContextProperties.SkipCertificateCheck = $SkipCertificateCheck.IsPresent $ContextProperties.Mode = 'None' $ContextProperties.ControllerHeader = $Null $ContextProperties.OpenAPIHeader = $Null #region Preferred transport, which is the controller interface Switch ($Null -ine $Credential) { {($_ -eq $True)} { $LoginBody = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' $LoginBody.username = "$($Credential.UserName)" $LoginBody.password = "$($Credential.GetNetworkCredential().Password)" $LoginResponse = Invoke-RestAPIRequest -URI ([System.URI]"$($BaseAddress)/$($OmadacID)/api/v2/login") -Method 'POST' -Body ($LoginBody) -SkipCertificateCheck:($SkipCertificateCheck.IsPresent) -AcceptableStatusCodeList @('*') Switch (("$($LoginResponse.Content.errorCode)" -eq '0') -and ([String]::IsNullOrWhiteSpace("$($LoginResponse.Content.result.token)") -eq $False)) { {($_ -eq $True)} { #region The shared client does not maintain a cookie container, so the session cookie is replayed by hand $SessionCookieList = New-Object -TypeName 'System.Collections.Generic.List[String]' ForEach ($SetCookieValue In "$($LoginResponse.ResponseHeader['Set-Cookie'])".Split(';,', [System.StringSplitOptions]::RemoveEmptyEntries)) { Switch ("$($SetCookieValue)".Trim() -imatch '^(TPOMADA_SESSIONID|TPEAP_SESSIONID|JSESSIONID)=') { {($_ -eq $True)} { $SessionCookieList.Add("$($SetCookieValue)".Trim()) } } } #endregion $ControllerHeader = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' $ControllerHeader.'Csrf-Token' = "$($LoginResponse.Content.result.token)" $ControllerHeader.Accept = 'application/json' Switch ($SessionCookieList.Count -gt 0) { {($_ -eq $True)} { $ControllerHeader.Cookie = "$($SessionCookieList -Join '; ')" } } $ContextProperties.ControllerHeader = $ControllerHeader $ContextProperties.Mode = 'Controller' Write-Verbose -Message "[$([System.DateTime]::UtcNow.ToString('yyyy/MM/dd HH:mm:ss.FFF'))] - [INFO] - Signed in to the Omada controller interface as `"$($Credential.UserName)`". [Controller Version: $($ContextProperties.ControllerVersion)]" } {($_ -eq $False)} { Write-Warning -Message "[$([System.DateTime]::UtcNow.ToString('yyyy/MM/dd HH:mm:ss.FFF'))] - [WARNING] - The Omada controller sign in failed, so collection will fall back to the Open API. [Error Code: $($LoginResponse.Content.errorCode)] [$($LoginResponse.Content.msg)]" } } } } #endregion #region Fallback transport, which is the Open API Switch (([String]::IsNullOrWhiteSpace($ClientID) -eq $False) -and ([String]::IsNullOrWhiteSpace($ClientSecret) -eq $False)) { {($_ -eq $True)} { $TokenRequestBody = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' $TokenRequestBody.omadacId = "$($OmadacID)" $TokenRequestBody.client_id = "$($ClientID)" $TokenRequestBody.client_secret = "$($ClientSecret)" $TokenResponse = Invoke-RestAPIRequest -URI ([System.URI]"$($BaseAddress)/openapi/authorize/token?grant_type=client_credentials") -Method 'POST' -Body ($TokenRequestBody) -SkipCertificateCheck:($SkipCertificateCheck.IsPresent) -AcceptableStatusCodeList @('*') Switch (("$($TokenResponse.Content.errorCode)" -eq '0') -and ([String]::IsNullOrWhiteSpace("$($TokenResponse.Content.result.accessToken)") -eq $False)) { {($_ -eq $True)} { $OpenAPIHeader = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' $OpenAPIHeader.Authorization = "AccessToken=$($TokenResponse.Content.result.accessToken)" $OpenAPIHeader.Accept = 'application/json' $ContextProperties.OpenAPIHeader = $OpenAPIHeader Switch ($ContextProperties.Mode -ieq 'None') { {($_ -eq $True)} { $ContextProperties.Mode = 'OpenAPI' } } Write-Verbose -Message "[$([System.DateTime]::UtcNow.ToString('yyyy/MM/dd HH:mm:ss.FFF'))] - [INFO] - Acquired an Omada Open API access token, which remains available as a fallback." } {($_ -eq $False)} { Write-Warning -Message "[$([System.DateTime]::UtcNow.ToString('yyyy/MM/dd HH:mm:ss.FFF'))] - [WARNING] - An Omada Open API access token could not be acquired. [Error Code: $($TokenResponse.Content.errorCode)] [$($TokenResponse.Content.msg)]" } } } } #endregion Switch ($ContextProperties.Mode -ieq 'None') { {($_ -eq $True)} { Throw "No usable Omada transport could be established against `"$($BaseAddress)`". Supply either a controller login or an Open API client identifier and secret." } } Write-Verbose -Message "[$([System.DateTime]::UtcNow.ToString('yyyy/MM/dd HH:mm:ss.FFF'))] - [INFO] - Omada collection will use the $(Switch ($ContextProperties.Mode) {{($_ -ieq 'Controller')} {'controller interface, with the Open API held in reserve'} Default {'Open API'}})." Write-Output -InputObject (New-Object -TypeName 'System.Management.Automation.PSObject' -Property ($ContextProperties)) } Catch { Throw } } #endregion