WifiConnectionAPI.psm1

<#
 .Synopsis
  Wifi
 .Description
  Manage Wifi connections
 
 .Parameter Get-RadioState
 .Parameter Set-RadioState
 .Parameter Get-WifiDeviceInformation
 .Parameter Get-WiFiAvailableAdapter
 .Parameter Get-WifiConnectionProfile
 .Parameter Remove-WifiConnectionProfile
 .Parameter Get-WifiCurrentConection
 .Parameter Get-WifiAvailableNetwork
 .Parameter Search-WifiNetworks
 .Parameter Disconnect-WifiNetwork
 .Parameter Connect-WiFiNetwork
#>


ImportSystemModules
Add-Type -AssemblyName System.Runtime.WindowsRuntime

# Zoek de juiste overloads voor zowel Operations (met resultaat) als Actions (zonder resultaat)
$asTaskGeneric = [System.WindowsRuntimeSystemExtensions].GetMethods() | Where-Object { 
    $_.Name -eq 'AsTask' -and $_.GetParameters().Count -eq 1 
}

$asTaskOperation = $asTaskGeneric | Where-Object { $_.GetParameters()[0].ParameterType.Name -eq 'IAsyncOperation`1' }
$asTaskAction    = $asTaskGeneric | Where-Object { $_.GetParameters()[0].ParameterType.Name -eq 'IAsyncAction' }

Function Await($WinRtTask, $ResultType) {
  if ($ResultType) {
    # Voor FindAllAdaptersAsync (IAsyncOperation)
    $asTask = $asTaskOperation.MakeGenericMethod($ResultType)
    $netTask = $asTask.Invoke($null, @($WinRtTask))
  } else {
    # Voor ScanAsync (IAsyncAction)
    $netTask = $asTaskAction.Invoke($null, @($WinRtTask))
  }

  $netTask.Wait(-1) | Out-Null
  return $netTask.Result
}

[Windows.Devices.Radios.Radio,Windows.System.Devices,ContentType=WindowsRuntime] | Out-Null
[Windows.Devices.Radios.RadioState,Windows.System.Devices,ContentType=WindowsRuntime] | Out-Null
[Windows.Devices.Radios.RadioAccessStatus,Windows.System.Devices,ContentType=WindowsRuntime] | Out-Null

Function Get-RadioState {
<#
.SYNOPSIS
  Retrieves all available radio adapters (WiFi, Bluetooth, Mobile) and displays their current status.
 
.DESCRIPTION
  This function uses the Windows Runtime (WinRT) API to access the system's radio hardware directly.
  The result is a list of Radio objects that can be piped directly into Set-RadioState.
 
.EXAMPLE
  Get-RadioState
  Displays all radios and whether they are 'On' or 'Off'.
 
.EXAMPLE
  Get-RadioState | Where-Object Kind -eq 'WiFi' | Set-RadioState -SetState Off
  Specifically searches for the WiFi radio and turns it off.
 
.OUTPUTS
  Windows.Devices.Radios.Radio
#>

  [outputType([Windows.Devices.Radios.Radio])]
  param()
  Process {
    $accessStatus = Await ([Windows.Devices.Radios.Radio]::RequestAccessAsync()) ([Windows.Devices.Radios.RadioAccessStatus])
  
    if($accessStatus -eq "Allowed"){
      Await ([Windows.Devices.Radios.Radio]::GetRadiosAsync()) ([System.Collections.Generic.IReadOnlyList[Windows.Devices.Radios.Radio]])
    } else {
      Write-Warning "Geen toegang tot de radio-hardware. Status: $accessStatus" 
    }
  }
}

Function Set-RadioState{
<#
.SYNOPSIS
  Turns a specific radio on or off.
 
.DESCRIPTION
  Accepts Radio objects (typically from the pipeline of Get-RadioState) and
  attempts to change the physical status to 'On' or 'Off'.
 
.PARAMETER SetState
  The desired target state. Use 'On' or 'Off'. (Supports autocomplete).
 
.PARAMETER Radios
  The Radio object to be modified. Accepts input via the pipeline.
 
.EXAMPLE
  Get-RadioState | Set-RadioState -SetState Off
  Get-RadioState | Where-Object Name -eq 'Wi-Fi' | Set-RadioState -SetState On
  Attempts to turn off all radios on the system.
 
.NOTES
  Some adapters (such as Mobile Broadband) may refuse the status change
  if they are managed elsewhere by the system. In such cases, the function will
  issue a warning rather than crashing.
#>

  param(
    [Parameter(Mandatory=$true)][ArgumentCompleter({
      param($commandName,$parameterName,$wordToComplete,$commandAst,$fakeBoundParameters)
        $possibleValues=@{Values=@([Windows.Devices.Radios.RadioState]::On,[Windows.Devices.Radios.RadioState]::Off)}
      if($fakeBoundParameters.ContainsKey('Type')){
        $possibleValues[$fakeBoundParameters.Type]| Where-object {$_ -like "$wordToComplete*"}
      }else{
        $possibleValues.Values|Foreach-Object{$_}
      }
    })]$SetState,
    [parameter(Mandatory=$true,ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)][Windows.Devices.Radios.Radio][alias("Radio")][array]$Radios
  )
  process{
    $Radios.Gettype()
    Await ($Radios.SetStateAsync($SetState)) ([Windows.Devices.Radios.RadioAccessStatus])
  }
}

[Windows.Devices.WiFi.WiFiAdapter,Windows.Devices.Wifi,ContentType=WindowsRunTime]|Out-Null
[Windows.Devices.Enumeration.DeviceInformation,Windows.Devices.Enumeration,ContentType=WindowsRunTime]|Out-Null
[Windows.Security.Credentials.PasswordCredential,Windows.Security.Credentials,ContentType=WindowsRunTime]|Out-Null
[Windows.Networking.Connectivity.NetworkInformation,Windows.Networking.Connectivity,Contenttype=WindowsRuntime]| Out-Null

Function Get-WifiDeviceInformation{
  [OutputType([Windows.Devices.Enumeration.DeviceInformationCollection])]
  Param()
  Process{
    Await([Windows.Devices.Enumeration.DeviceInformation]::FindAllAsync([Windows.Devices.WiFi.WiFiAdapter]::GetDeviceSelector()))([Windows.Devices.Enumeration.DeviceInformationCollection])
  }
}

Function Get-WiFiAvailableAdapter{
  [outputtype([Windows.Devices.WiFi.WiFiAdapter])]
  param()
  Process{
    Await([Windows.Devices.WiFi.WiFiAdapter]::FindAllAdaptersAsync())([System.Collections.Generic.IReadOnlyList[Windows.Devices.WiFi.WiFiAdapter]])
  }
}

Function Search-WifiNetworks{
  [OutputType([Windows.Devices.Wifi.WifiAvailableNetwork])]
  param()
  Process{
    $WifiAdapter=Get-WiFiAvailableAdapter
    Write-Progress -Activity "WiFi" -Status "Scanning WiFi Networks"
    [Void](Await($WiFiAdapter.ScanAsync())($null))
    Write-Progress -Activity "WiFi" -Completed
    $script:WiFiLastScanTime = Get-Date
    return $WifiAdapter.NetworkReport.AvAilableNetworks
  }
}

Function Get-WifiConnectionProfile{
  [outputType([Windows.Networking.Connectivity.NetworkInformation])]
  param()
  Process{
    [Windows.Networking.Connectivity.NetworkInformation]::GetConnectionProfiles()|Where-Object{$_.IsWlanConnectionProfile}
  }
}

Function Remove-WifiConnectionProfile{
  param(
    [parameter(Mandatory=$true,ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)][Windows.Networking.Connectivity.ConnectionProfile]
    [alias("Profile")]
    [array]$Profiles
  )
  process{
    if($Profiles.CanDelete){
      Write-host ("Delete Profile {0}" -f $Profiles.ProfileName)
       $Profiles.TryDeleteAsync() | Out-Null
    }else{
      Write-host ("Can't Delete Profile {0}" -f $Profiles.ProfileName)
    }
  } 
}

Function Get-WifiCurrentConection{
  [outputtype([Windows.Networking.Connectivity.ConnectionProfile])]
  param()
  Process{
    await((Get-WiFiAvailableAdapter).NetworkAdapter.GetConnectedProfileAsync()) ([Windows.Networking.Connectivity.ConnectionProfile])
  }
}

Function Disconnect-WifiNetwork {
  param(
    [Parameter(Mandatory=$false)]
    [int[]]$Index
  )
  Process {
    $Adapters = Get-WiFiAvailableAdapter

    if ($null -eq $Adapters -or $Adapters.Count -eq 0) {
      Write-Warning "No WiFi-adapters found to disconect"
      return
    }

    if ($PSBoundParameters.ContainsKey('Index')) {
      # Looping through the index given
      foreach ($i in $Index) {
        if ($i -ge 0 -and $i -lt $Adapters.Count) {
          $adapterName = (Get-NetAdapter | Where-Object DeviceID -match $Adapters[$i].NetworkAdapter.NetworkAdapterId).InterfaceDescription
          Write-host "Disconnected connection on adapter index: $i : $adapterName" -ForegroundColor Cyan
          $Adapters[$i].Disconnect()
        } else {
          Write-Warning "Index $i skipped: does not exist (available: 0 to $($Adapters.Count - 1))."
        }
      }
    } else {
      # DEFAULT: Alles platgoooien
      Write-Host "Disconnecting on ALL ($($Adapters.Count)) adapters..." -ForegroundColor Yellow
      $Adapters.Disconnect()
    }
  }
}

Register-ArgumentCompleter -CommandName 'Disconnect-WifiNetwork' -ParameterName 'Index' -ScriptBlock {
  param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
  $adapters = Get-WiFiAvailableAdapter
  for ($i = 0; $i -lt $adapters.Count; $i++) {
    $id = $adapters[$i].NetworkAdapter.NetworkAdapterId
    $name = (Get-NetAdapter | Where-Object DeviceID -match $id).InterfaceDescription
    [System.Management.Automation.CompletionResult]::new($i, $i, 'ParameterValue', "Adapter $i : $name")
  }
}

Function Get-WifiAvailableNetworks{
  [OutputType([Windows.Devices.Wifi.WifiAvailableNetwork])]
  param()
  Process{
    return (Get-WiFiAvailableAdapter).NetworkReport.AvailableNetworks
  }
}

Function Connect-WiFiNetwork {
<#
.SYNOPSIS
  Connects one, multiple, or all WiFi adapters to a specific network (SSID).
 
.DESCRIPTION
  This function uses the WinRT API to connect. It supports simultaneously targeting specific adapters by their index. If a network is already known to Windows (profile exists), the password is not required.
 
.PARAMETER Ssid
  The name of the WiFi network. Use the TAB key for suggestions of available nearby networks.
 
.PARAMETER Password
  The password for the network. This is a dynamic parameter that uses SecureString (unless -AsPlainText is specified). Not required if a stored profile already exists.
 
.PARAMETER Index
  The index of the adapter(s) (e.g., 0, 1). Leave this blank to attempt the connection on ALL available WiFi adapters simultaneously.
 
.PARAMETER User
  Optional username for enterprise networks (802.1X).
 
.PARAMETER Bssid
  Force connection to a specific Access Point based on the MAC address.
 
.EXAMPLE
  Connect-WiFiNetwork -Ssid "MyHomeNetwork" -Index 0
  Connects the first adapter to the specified network.
 
.EXAMPLE
  Connect-WiFiNetwork -Ssid "Open_WiFi"
  Attempts to connect all WiFi adapters on the system to an open network.
 
.EXAMPLE
  Connect-WiFiNetwork -Ssid "Company_WiFi" -Password (Read-Host -AsSecureString) -Index 0, 2
  Connects adapter 0 and 2 to a secured network with manual password input.
 
.NOTES
  The function checks each adapter to ensure the SSID is visible before initiating the connection.
#>

  [CmdletBinding()]
  param(
    [Parameter(Mandatory=$true)]$Ssid, 
    [string]$Bssid,
    [string]$User,
    [Switch]$AsPlainText,
    [Switch]$Hidden,
    [int[]]$Index
  )
  DynamicParam {
    # Kies het type op basis van de switch
    $parameterType = if ($AsPlainText) { [string] } else { [securestring] }

    # Maak de collectie voor attributen (zoals Mandatory)
    $attributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
    $attribute = New-Object System.Management.Automation.ParameterAttribute
    $attribute.Mandatory = $false
    $attributeCollection.Add($attribute)

    # Definieer de parameter 'Password' runtime
    $dynParam = New-Object System.Management.Automation.RuntimeDefinedParameter('Password', $parameterType, $attributeCollection)

    # Voeg toe aan de dictionary die PowerShell terug verwacht
    $paramDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
    $paramDictionary.Add('Password', $dynParam)

    return $paramDictionary
  }
  Process{
    $PasswordValue = $PSBoundParameters['Password']
    $SsidValue = $PSBoundParameters['Ssid']

    # 1. Haal de adapters op
    $AllAdapters = Get-WiFiAvailableAdapter
    
    # 2. Bepaal op welke adapters we actie ondernemen
    $TargetAdapters = if ($PSBoundParameters.ContainsKey('Index')) {
      foreach($i in $Index) { $AllAdapters[$i] }
    } else {
      $AllAdapters # Standaard: Probeer op ALLE adapters te verbinden
    }

    $Credentials=[Windows.Security.Credentials.PasswordCredential]::new()
    if($null -ne $passwordValue){
      if($AsPlainText){
        $Credentials.Password=$passwordValue
      }else{
        $Credentials.Password=([System.Net.NetworkCredential]::new([string]::empty,$passwordValue)).Password
      }
    }
    if([bool]$User){ 
      $Credentials.UserName=$User
    }

  $TargetAdapters | Foreach-Object {  
    # Haal de menselijke naam op voor de huidige adapter in de loop
    $currentId = $_.NetworkAdapter.NetworkAdapterId
    $adapterName = (Get-NetAdapter | Where-Object DeviceID -match $currentId).InterfaceDescription
  
    Write-Host "Verbinden via adapter: $adapterName" -ForegroundColor Cyan

    if(-not $Bssid){
      $Network=$_.NetworkReport.AvailableNetworks|Where-Object{$_.Ssid -match $SsidValue}| Sort-Object NetworkRssiInDecibelMilliwatts -Descending | Select-Object -First 1
    }else{
      $Network=$_.NetworkReport.AvailableNetworks|Where-Object{$_.BSsid -match $BSsid}
    }

    $_.Disconnect()
    $Result = Await($_.ConnectAsync($Network,[Windows.Devices.WiFi.WiFiReconnectionKind]::Automatic,$Credentials,$null))([Windows.Devices.WiFi.WiFiConnectionResult])
    
    # Optioneel: Laat direct weten of het gelukt is voor deze specifieke kaart
    $statusColor = "Yellow"
    if ($Result.ConnectionStatus -eq "Success") { $statusColor = "Green" }
    Write-Host "Status voor $adapterName : $($Result.ConnectionStatus)" -ForegroundColor $statusColor
      [PSCustomObject]@{
        ConnectionStatus = $Result.ConnectionStatus  # NIET hernoemen!
        AdapterName      = $adapterName
        Ssid             = $SsidValue
        Index            = [array]::IndexOf($AllAdapters, $_)
        Bssid            = $Network.Bssid
      }
    }
  }
}

Register-ArgumentCompleter -CommandName 'Connect-WiFiNetwork' -ParameterName 'Ssid' -ScriptBlock {
  param ($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
  
  # FIX: Haal de adapters EERST op zodat we ze kunnen tellen en scannen
  $allAdapters = Get-WiFiAvailableAdapter
  
  # --- SUPER SMART SCAN LOGICA ---
  # We checken de eerste adapter als graadmeter voor de cache
  $networksInCache = ($allAdapters.NetworkReport.AvailableNetworks | Measure-Object).Count
  $cacheIsOld = ($null -eq $script:WiFiLastScanTime -or $script:WiFiLastScanTime -lt (Get-Date).AddMinutes(-5))

  if ($cacheIsOld -or ($networksInCache -le 1)) {
    Write-Progress -Activity "WiFi" -Status "Cache controleren/verversen op alle adapters..."
    foreach($adapter in $allAdapters) {
      [void](Await ($adapter.ScanAsync()) ($null))
    }
    Write-Progress -Activity "wiFi" -Completed
    $script:WiFiLastScanTime = Get-Date
  }
 
  # Bepaal target indices (nu $allAdapters al gevuld is)
  $targetIndices = if ($fakeBoundParameters.ContainsKey('Index')) { $fakeBoundParameters.Index } else { 0..($allAdapters.Count - 1) }
  $givenBssid = $fakeBoundParameters['Bssid']

  # 2. Haal netwerken op (Filtering op BSSID & opschonen)
  $networks = foreach ($i in $targetIndices) {
    $allAdapters[$i].NetworkReport.AvailableNetworks | Where-Object { 
      if ([string]::IsNullOrEmpty($_.Ssid)) { return $false }
        
      if ($givenBssid) {
        $cleanGiven = $givenBssid -replace '[^0-9A-Fa-f]'
        $cleanCurrent = $_.Bssid -replace '[^0-9A-Fa-f]'
        $cleanCurrent -eq $cleanGiven
      } else {
        $true
      }
    }
  }

  # 3. Bouw de lijst: Groepeer op SSID en onthoud het sterkste signaal per naam
  $possibleValues = $networks | Group-Object Ssid | ForEach-Object {
    # Zoek het sterkste exemplaar binnen deze SSID-groep
    $strongest = $_.Group | Sort-Object SignalBars -Descending | Select-Object -First 1
      
    [PSCustomObject]@{
      Ssid       = $strongest.Ssid
      Signal     = $strongest.SignalBars
      Formatted  = if ($strongest.Ssid -match ' ') { "`"$($strongest.Ssid)`"" } else { $strongest.Ssid }
    }
  } | Sort-Object Signal -Descending # Zet de krachtpatsers bovenaan de TAB-lijst!

  # 4. Filteren op wat de gebruiker typt
  $possibleValues | Where-Object { $_.Ssid -like "$wordToComplete*" } | Select-Object -ExpandProperty Formatted
}

Register-ArgumentCompleter -CommandName 'Connect-WiFiNetwork' -ParameterName 'Bssid' -ScriptBlock {
  param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)

  $allAdapters = Get-WiFiAvailableAdapter
  $filterSsid = $fakeBoundParameters['Ssid']
  $showHidden = $fakeBoundParameters.ContainsKey('Hidden')
  
  # --- SMART SCAN LOGICA (Inclusief de 'Empty Cache Guard') ---
  $cacheEmpty = (($allAdapters.NetworkReport.AvailableNetworks | Measure-Object).Count -le 1)
  $cacheOld = ($script:WiFiLastScanTime -lt (Get-Date).AddMinutes(-5))

  if ($cacheOld -or $cacheEmpty) {
    Write-Progress -Activity "WiFi BSSID Discovery" -Status "Updating cache for best signal..."
    $allAdapters | ForEach-Object { [void](Await ($_.ScanAsync()) ($null)) }
    Write-Progress -Activity "WiFi BSSID Discovery" -Completed
    $script:WiFiLastScanTime = Get-Date
  }

  # 1. Verzamel alle data in objecten voor sortering
  $bssidList = $allAdapters.NetworkReport.AvailableNetworks | ForEach-Object {
    # PITFALL FIX: Sla netwerken zonder SSID (leeg of spaties) direct over
    if ([string]::IsNullOrWhiteSpace($_.Ssid) -and -not $showHidden) { return }

    if ([string]::IsNullOrWhiteSpace($filterSsid) -or $_.Ssid -eq $filterSsid) {
      # ... rest van je logica (Mac opschonen, etc.) ...
      
      $cleanMac = if ($_.Bssid -match '^[0-9A-Fa-f]{12}$') {
        ($_.Bssid -replace '..(?!$)', '$&:').ToUpper()
      } else { $_.Bssid }

      if ($cleanMac -like "$wordToComplete*") {
        $freq = $_.ChannelCenterFrequencyInKilohertz
        [PSCustomObject]@{
          Ssid      = $_.Ssid
          Mac       = $cleanMac
          Signal    = $_.SignalBars 
          Rssi      = $_.NetworkRssiInDecibelMilliwatts
          Band      = if($freq -gt 4000000){'5GHz'}else{'2.4GHz'}
          MenuLabel = if ([string]::IsNullOrWhiteSpace($filterSsid)) { "$cleanMac ($($_.Ssid))" } else { $cleanMac }
        }
      }
    }
  }

  # 2. SORTERING: Eerst op Naam, dan op Signaal (Sterkste eerst)
  $bssidList | Sort-Object Signal,Ssid -Descending | ForEach-Object {
    [System.Management.Automation.CompletionResult]::new(
      $_.Mac, 
      $_.MenuLabel, 
      'ParameterValue', 
      "SSID: $($_.Ssid) | Band: $($_.Band) | Signaal: $($_.Signal) bars ($($_.Rssi) dBm)"
    )
  }
}

Register-ArgumentCompleter -CommandName 'Connect-WiFiNetwork' -ParameterName 'Index' -ScriptBlock {
  param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
  $adapters = Get-WiFiAvailableAdapter
  for ($i = 0; $i -lt $adapters.Count; $i++) {
    $name = $adapters[$i].NetworkAdapter.NetworkItem.NetworkId # Of gebruik Get-NetAdapter voor de 'echte' naam
    [System.Management.Automation.CompletionResult]::new($i, $i, 'ParameterValue', "Adapter $i : $name")
  }
}

Export-ModuleMember Get-RadioState
Export-ModuleMember Set-RadioState
Export-ModuleMember Get-WifiDeviceInformation
Export-Modulemember Get-WiFiAvailableAdapter
Export-ModuleMember Get-WifiAvailableNetworks
Export-ModuleMember Search-WifiNetworks
Export-ModuleMember Get-WifiConnectionProfile
Export-Modulemember Remove-WifiConnectionProfile
Export-ModuleMember Get-WifiCurrentConection
Export-ModuleMember Disconnect-WifiNetwork
Export-ModuleMember Connect-WiFiNetwork