PSBuienradar.psm1
|
#Region './PSBuienradar.prefix.ps1' -1 <# Code inserted in this file will be placed at the top of the .PSM1 file generated by ModuleBuilder #> <# Delete this file if not needed for your module #> <# Delete these comments if you don't want them in your module #> #EndRegion './PSBuienradar.prefix.ps1' 4 #Region './Private/ConvertRainIntensity.ps1' -1 function ConvertRainIntensity { <# .SYNOPSIS Converts a BuienRadar rain intensity value to a human-friendly description .DESCRIPTION Internal helper that maps BuienRadar rain intensity values (0-255) to a human-readable text description with survival-oriented humour and emoji. The BuienRadar scale is roughly logarithmic: 0 = No rain 1-77 = Very light rain (drizzle) 78-109 = Light rain 110-140 = Moderate rain 141-255 = Heavy rain (SURVIVAL MODE) .PARAMETER Value The rain intensity value as returned by BuienRadar (0-255) .EXAMPLE ConvertRainIntensity -Value 0 Returns: "No rain ☀️" .EXAMPLE ConvertRainIntensity -Value 120 Returns: "Moderate rain 🌧️" .OUTPUTS System.String .NOTES Private function - not exported from module #> [CmdletBinding()] [OutputType([string])] param ( [Parameter(Mandatory)] [ValidateRange(0, 255)] [int] $Value ) if ($Value -eq 0) { return 'No rain ☀️' } if ($Value -le 77) { return 'Very light rain (drizzle) 🌂' } if ($Value -le 109) { return 'Light rain 🌦️' } if ($Value -le 140) { return 'Moderate rain 🌧️' } return 'Heavy rain 🌧️ -- SURVIVAL MODE ACTIVATED' } #EndRegion './Private/ConvertRainIntensity.ps1' 60 #Region './Private/GetDistanceBetweenCoordinates.ps1' -1 function GetDistanceBetweenCoordinates { <# .SYNOPSIS Calculates the distance between two geographic coordinates .DESCRIPTION Internal helper that uses the Haversine formula to calculate the great-circle distance in kilometres between two latitude/longitude points. .PARAMETER Latitude1 Latitude of the first point in decimal degrees .PARAMETER Longitude1 Longitude of the first point in decimal degrees .PARAMETER Latitude2 Latitude of the second point in decimal degrees .PARAMETER Longitude2 Longitude of the second point in decimal degrees .EXAMPLE GetDistanceBetweenCoordinates -Latitude1 52.37 -Longitude1 4.90 -Latitude2 51.92 -Longitude2 4.48 Returns the distance in km between Amsterdam and Rotterdam. .OUTPUTS System.Double .NOTES Private function - not exported from module #> [CmdletBinding()] [OutputType([double])] param ( [Parameter(Mandatory)] [ValidateRange(-90.0, 90.0)] [double] $Latitude1, [Parameter(Mandatory)] [ValidateRange(-180.0, 180.0)] [double] $Longitude1, [Parameter(Mandatory)] [ValidateRange(-90.0, 90.0)] [double] $Latitude2, [Parameter(Mandatory)] [ValidateRange(-180.0, 180.0)] [double] $Longitude2 ) $earthRadiusKm = 6371.0 $deltaLat = ($Latitude2 - $Latitude1) * [Math]::PI / 180.0 $deltaLon = ($Longitude2 - $Longitude1) * [Math]::PI / 180.0 $lat1Rad = $Latitude1 * [Math]::PI / 180.0 $lat2Rad = $Latitude2 * [Math]::PI / 180.0 $a = [Math]::Sin($deltaLat / 2) * [Math]::Sin($deltaLat / 2) + [Math]::Cos($lat1Rad) * [Math]::Cos($lat2Rad) * [Math]::Sin($deltaLon / 2) * [Math]::Sin($deltaLon / 2) $c = 2 * [Math]::Atan2([Math]::Sqrt($a), [Math]::Sqrt(1 - $a)) return $earthRadiusKm * $c } #EndRegion './Private/GetDistanceBetweenCoordinates.ps1' 72 #Region './Private/GetSurvivalAdvice.ps1' -1 function GetSurvivalAdvice { <# .SYNOPSIS Generates humorous Dutch weather survival advice based on conditions .DESCRIPTION Internal helper that combines rain intensity and wind speed to produce a context-aware, slightly humorous survival tip for the Dutch weather. .PARAMETER Intensity The current BuienRadar rain intensity value (0-255). .PARAMETER Station The nearest BuienRadar station measurement object. Used to access wind speed for additional survival commentary. .EXAMPLE GetSurvivalAdvice -Intensity 0 -Station $station Returns: "Congratulations, it's not raining (yet). You might survive today." .OUTPUTS System.String .NOTES Private function - not exported from module #> [CmdletBinding()] [OutputType([string])] param ( [Parameter(Mandatory)] [ValidateRange(0, 255)] [int] $Intensity, [Parameter(Mandatory)] [ValidateNotNull()] [object] $Station ) $windSpeed = if ($Station.PSObject.Properties['windspeed'] -and $null -ne $Station.windspeed) { [double]$Station.windspeed } else { 0.0 } $windWarning = if ($windSpeed -ge 10.0) { " Wind is strong enough to question your life choices ($windSpeed m/s). Hold on to your bicycle." } elseif ($windSpeed -ge 7.0) { " Also, it's quite breezy -- your umbrella may have other plans." } else { '' } $rainAdvice = if ($Intensity -eq 0) { "Congratulations, it's not raining (yet). You might survive today." } elseif ($Intensity -le 77) { 'There is some drizzle. Your hair will notice even if you pretend not to.' } elseif ($Intensity -le 109) { 'Take an umbrella. Seriously. This is not a drill.' } elseif ($Intensity -le 140) { 'Moderate rain incoming. The Dutch call this "a bit wet". Outsiders call it "miserable".' } else { 'HEAVY RAIN -- SURVIVAL MODE ACTIVATED. Find higher ground, a stroopwafel, and the will to carry on. 🌧️' } return "$rainAdvice$windWarning" } #EndRegion './Private/GetSurvivalAdvice.ps1' 64 #Region './Public/Get-GeoLocation.ps1' -1 function Get-GeoLocation { <# .SYNOPSIS Converts a public IP address to geographic coordinates and city name .DESCRIPTION Uses the ip-api.com free GeoIP API to resolve an IP address to latitude, longitude, and city name. The result is used to find nearby BuienRadar weather stations and fetch rain forecasts. .PARAMETER IP The public IP address to look up. Accepts IPv4 addresses. .EXAMPLE Get-GeoLocation -IP '203.0.113.42' Returns a PSCustomObject with Latitude, Longitude, and City. .EXAMPLE Get-PublicIP | Get-GeoLocation Chains public-IP detection with geo-location lookup. .INPUTS System.String .OUTPUTS PSCustomObject .NOTES Uses http://ip-api.com/json/{ip}. The free tier supports up to 45 requests per minute from the same IP address. #> [CmdletBinding()] [OutputType([PSCustomObject])] param ( [Parameter( Mandatory, ValueFromPipeline, Position = 0 )] [ValidateNotNullOrEmpty()] [string] $IP ) process { try { Write-Verbose "$($MyInvocation.MyCommand): Looking up geo-location for IP: $IP" $uri = "http://ip-api.com/json/$IP" $response = Invoke-RestMethod -Uri $uri -ErrorAction Stop if ($response.status -ne 'success') { throw "Geo-location lookup failed for '$IP': $($response.message)" } $location = [PSCustomObject]@{ IP = $IP Latitude = [double]$response.lat Longitude = [double]$response.lon City = $response.city Country = $response.country } $location.PSObject.TypeNames.Insert(0, 'BuienRadar.GeoLocation') Write-Verbose "$($MyInvocation.MyCommand): Resolved to $($location.City), Lat=$($location.Latitude), Lon=$($location.Longitude)" return $location } catch { Write-Verbose "$($MyInvocation.MyCommand) failed for '$IP': $_" throw $_ } } } #EndRegion './Public/Get-GeoLocation.ps1' 74 #Region './Public/Get-NearestWeatherStation.ps1' -1 function Get-NearestWeatherStation { <# .SYNOPSIS Finds the nearest BuienRadar weather station to the given coordinates .DESCRIPTION Accepts an array of BuienRadar station measurement objects (as returned by the /2.0/feed/json endpoint) and returns the station closest to the supplied latitude and longitude, calculated using the Haversine formula. .PARAMETER Latitude The latitude of the reference point in decimal degrees. .PARAMETER Longitude The longitude of the reference point in decimal degrees. .PARAMETER Stations An array of BuienRadar station measurement objects. Each object must expose at least a 'lat' and 'lon' property. Typically sourced from the 'actual.stationmeasurements' array of the BuienRadar JSON feed. .EXAMPLE $feed = Invoke-RestMethod -Uri 'https://data.buienradar.nl/2.0/feed/json' Get-NearestWeatherStation -Latitude 52.37 -Longitude 4.90 -Stations $feed.actual.stationmeasurements Returns the weather station closest to Amsterdam. .INPUTS None .OUTPUTS PSCustomObject .NOTES Requires BuienRadar station objects with 'lat' and 'lon' properties. #> [CmdletBinding()] [OutputType([PSCustomObject])] param ( [Parameter(Mandatory)] [ValidateRange(-90.0, 90.0)] [double] $Latitude, [Parameter(Mandatory)] [ValidateRange(-180.0, 180.0)] [double] $Longitude, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [object[]] $Stations ) try { Write-Verbose "$($MyInvocation.MyCommand): Finding nearest station to Lat=$Latitude, Lon=$Longitude from $($Stations.Count) stations" $nearest = $null $shortestDistance = [double]::MaxValue foreach ($station in $Stations) { if ($null -eq $station.lat -or $null -eq $station.lon) { Write-Verbose "$($MyInvocation.MyCommand): Skipping station with missing coordinates: $($station.stationname)" continue } $distance = GetDistanceBetweenCoordinates ` -Latitude1 $Latitude ` -Longitude1 $Longitude ` -Latitude2 ([double]$station.lat) ` -Longitude2 ([double]$station.lon) if ($distance -lt $shortestDistance) { $shortestDistance = $distance $nearest = $station } } if ($null -eq $nearest) { throw 'No valid weather stations found in the provided data.' } Write-Verbose "$($MyInvocation.MyCommand): Nearest station is '$($nearest.stationname)' at $([Math]::Round($shortestDistance, 1)) km" return $nearest } catch { Write-Verbose "$($MyInvocation.MyCommand) failed for Lat=$Latitude, Lon=${Longitude}: $_" throw $_ } } #EndRegion './Public/Get-NearestWeatherStation.ps1' 91 #Region './Public/Get-PublicIP.ps1' -1 function Get-PublicIP { <# .SYNOPSIS Returns the current user's public IP address .DESCRIPTION Queries the ipify.org API to determine the public IP address of the machine running this command. Useful as the first step in a geo-location workflow. .EXAMPLE Get-PublicIP Returns the public IP address as a string, e.g. "203.0.113.42". .INPUTS None .OUTPUTS System.String .NOTES Requires internet access to reach https://api.ipify.org. #> [CmdletBinding()] [OutputType([string])] param () try { Write-Verbose "$($MyInvocation.MyCommand): Querying ipify.org for public IP" $response = Invoke-RestMethod -Uri 'https://api.ipify.org?format=json' -ErrorAction Stop $ip = $response.ip if ([string]::IsNullOrWhiteSpace($ip)) { throw 'ipify.org returned an empty IP address.' } Write-Verbose "$($MyInvocation.MyCommand): Detected public IP: $ip" return $ip } catch { Write-Verbose "$($MyInvocation.MyCommand) failed: $_" throw $_ } } #EndRegion './Public/Get-PublicIP.ps1' 44 #Region './Public/Get-RainForecast.ps1' -1 function Get-RainForecast { <# .SYNOPSIS Retrieves the rain forecast for the next two hours from BuienRadar .DESCRIPTION Queries the BuienRadar gpsgadget rain-text endpoint for the given latitude and longitude and parses the response into a collection of time-stamped rain intensity objects. Each forecast entry contains: - Time : The forecast time (HH:mm) - Intensity : Raw BuienRadar rain value (0-255) - Description : Human-friendly rain description with emoji The rain intensity scale is roughly logarithmic: 0 = No rain 1-77 = Very light rain (drizzle) 78-109 = Light rain 110-140 = Moderate rain 141-255 = Heavy rain (SURVIVAL MODE) .PARAMETER Latitude The latitude of the location to fetch the rain forecast for. .PARAMETER Longitude The longitude of the location to fetch the rain forecast for. .EXAMPLE Get-RainForecast -Latitude 52.37 -Longitude 4.90 Returns the next two-hour rain forecast for Amsterdam. .INPUTS None .OUTPUTS PSCustomObject[] .NOTES Uses https://gpsgadget.buienradar.nl/data/raintext?lat={lat}&lon={lon}. Data is updated approximately every 5 minutes. #> [CmdletBinding()] [OutputType([PSCustomObject[]])] param ( [Parameter(Mandatory)] [ValidateRange(-90.0, 90.0)] [double] $Latitude, [Parameter(Mandatory)] [ValidateRange(-180.0, 180.0)] [double] $Longitude ) try { $lat = [string]::Format([System.Globalization.CultureInfo]::InvariantCulture, '{0:F2}', $Latitude) $lon = [string]::Format([System.Globalization.CultureInfo]::InvariantCulture, '{0:F2}', $Longitude) $uri = "https://gpsgadget.buienradar.nl/data/raintext?lat=$lat&lon=$lon" Write-Verbose "$($MyInvocation.MyCommand): Fetching rain forecast from $uri" $raw = Invoke-RestMethod -Uri $uri -ErrorAction Stop if ([string]::IsNullOrWhiteSpace($raw)) { throw 'BuienRadar returned an empty rain forecast response.' } $lines = $raw -split "`n" | Where-Object { $_ -match '\S' } $forecast = foreach ($line in $lines) { $parts = $line.Trim() -split '\|' if ($parts.Count -ne 2) { Write-Verbose "$($MyInvocation.MyCommand): Skipping malformed line: $line" continue } $intensityValue = [int]$parts[0] $entry = [PSCustomObject]@{ Time = $parts[1].Trim() Intensity = $intensityValue Description = ConvertRainIntensity -Value $intensityValue } $entry.PSObject.TypeNames.Insert(0, 'BuienRadar.RainForecastEntry') $entry } Write-Verbose "$($MyInvocation.MyCommand): Parsed $(@($forecast).Count) forecast entries" return $forecast } catch { Write-Verbose "$($MyInvocation.MyCommand) failed for Lat=$Latitude, Lon=${Longitude}: $_" throw $_ } } #EndRegion './Public/Get-RainForecast.ps1' 95 #Region './Public/Get-WeatherSurvivalAdvice.ps1' -1 function Get-WeatherSurvivalAdvice { <# .SYNOPSIS Returns a humorous Dutch weather survival report for your current location .DESCRIPTION Orchestrates the full BuienRadar weather survival pipeline: 1. Detects the user's public IP address 2. Resolves the IP to geographic coordinates and city name 3. Fetches the two-hour rain forecast from BuienRadar 4. Downloads the BuienRadar weather feed and finds the nearest station 5. Assembles a custom PSCustomObject with weather data and survival advice Survival advice is generated based on current conditions: - Heavy rain -> urgent evacuation humour - Moderate rain -> umbrella reminders - Light rain -> mild caution - No rain -> cautiously optimistic encouragement .EXAMPLE Get-WeatherSurvivalAdvice Detects location automatically and returns the survival report. .EXAMPLE $report = Get-WeatherSurvivalAdvice $report.SurvivalAdvice Retrieves only the survival advice text. .INPUTS None .OUTPUTS PSCustomObject .NOTES Requires internet access to the following endpoints: - https://api.ipify.org - http://ip-api.com - https://gpsgadget.buienradar.nl - https://data.buienradar.nl #> [CmdletBinding()] [OutputType([PSCustomObject])] param () try { Write-Verbose "$($MyInvocation.MyCommand): Starting Dutch weather survival assessment" # Step 1: Public IP Write-Verbose "$($MyInvocation.MyCommand): Step 1 - Detecting public IP" $publicIP = Get-PublicIP # Step 2: Geo-location Write-Verbose "$($MyInvocation.MyCommand): Step 2 - Resolving geo-location for $publicIP" $geoLocation = Get-GeoLocation -IP $publicIP # Step 3: Rain forecast Write-Verbose "$($MyInvocation.MyCommand): Step 3 - Fetching rain forecast" $rainForecast = Get-RainForecast -Latitude $geoLocation.Latitude -Longitude $geoLocation.Longitude # Step 4: Nearest weather station Write-Verbose "$($MyInvocation.MyCommand): Step 4 - Fetching BuienRadar weather feed" $feed = Invoke-RestMethod -Uri 'https://data.buienradar.nl/2.0/feed/json' -ErrorAction Stop $nearestStation = Get-NearestWeatherStation ` -Latitude $geoLocation.Latitude ` -Longitude $geoLocation.Longitude ` -Stations $feed.actual.stationmeasurements # Step 5: Determine current rain severity from first non-null forecast entry $currentIntensity = if ($null -ne $rainForecast -and @($rainForecast).Count -gt 0) { @($rainForecast)[0].Intensity } else { 0 } $survivalAdvice = GetSurvivalAdvice -Intensity $currentIntensity -Station $nearestStation # Step 6: Build report $report = [PSCustomObject]@{ Location = "$($geoLocation.City), $($geoLocation.Country)" Latitude = $geoLocation.Latitude Longitude = $geoLocation.Longitude StationName = $nearestStation.stationname Temperature = $nearestStation.temperature WindSpeed = $nearestStation.windspeed WindDirection = $nearestStation.winddirection Humidity = $nearestStation.humidity RainForecast = $rainForecast CurrentRain = if ($null -ne $rainForecast -and @($rainForecast).Count -gt 0) { @($rainForecast)[0].Description } else { 'No rain data available' } SurvivalAdvice = $survivalAdvice } $report.PSObject.TypeNames.Insert(0, 'BuienRadar.SurvivalReport') Write-Verbose "$($MyInvocation.MyCommand): Survival report assembled for $($report.Location)" return $report } catch { Write-Verbose "$($MyInvocation.MyCommand) failed: $_" throw $_ } } #EndRegion './Public/Get-WeatherSurvivalAdvice.ps1' 107 #Region './PSBuienradar.suffix.ps1' -1 <# Code inserted in this file will be placed at the bottom of the .PSM1 file generated by ModuleBuilder #> <# Delete this file if not needed for your module #> <# Delete these comments if you don't want them in your module #> #EndRegion './PSBuienradar.suffix.ps1' 4 |