<# .SYNOPSIS PowerShell script to repeatedly download Boltwood weather data from a specified URL and write it to a local file, allowing other programs to read the file while it's being updated. #> param( [Parameter(Mandatory)] [string]$url, # The URL to download [string]$filePath = ".\remotedata.txt", # The local file path [int]$intervalSeconds = 10, # How often to repeat the download [bool]$logOutput = $true, # Whether to output the structured data as a log file in a readable list format [bool]$jsonOutput = $false, # Whether to output the structured data as JSON for other programs [bool]$csvOutput = $true # Whether to output the structured data as CSV for spreadsheet use #[string]$writeMode = $true # "Force" (False) or "Append" (True) mode for the text output file ) # --- SAFETY THRESHOLDS --- # Cloud: 1=Clear, 2=Light Clouds, 3=Very Cloudy $UnsafeCloud = 2 # Wind: 1=Calm, 2=Windy, 3=Very Windy $UnsafeWind = 2 # Rain: 1=Dry, 2=Damp, 3=Rain $UnsafeRain = 2 # Darkness: 1=Dark, 2=Dim, 3=Daylight $UnsafeLight = 2 function Convert-Field { # A helper function to convert a field value to the specified type with error handling and defaults param($Value, $Type, $Default) try { if ($Type -eq "double") { return [double]$Value } if ($Type -eq "int") { return [int]$Value } if ($Type -eq "string") { return [string]$Value } return $Value } catch { Write-Warning "Failed to parse field value '$Value'. Using default '$Default'." return $Default } } function Convert-BoltwoodData { param([string]$content) # The 'Boltwood II' format separates fields with variable spaces. $parts = $content -split '\s+' | Where-Object { $_ -ne "" } try { # Boltwood II cloud sensor standard has 21 fields, but we will map them to a more descriptive object with conversions and defaults $data = [PSCustomObject]@{ Timestamp = "$($parts[0]) $($parts[1])" # --- SENSOR VALUES --- TempScale = Convert-Field $parts[2] "string" "?" # "F" or "C" WindScale = Convert-Field $parts[3] "string" "?" # "M" or "K" SkyTemp = Convert-Field $parts[4] "double" 999 AmbientTemp = Convert-Field $parts[5] "double" 999 SensorTemp = Convert-Field $parts[6] "double" 999 WindSpeed = Convert-Field $parts[7] "double" 0 Humidity = Convert-Field $parts[8] "int" 0 DewPoint = Convert-Field $parts[9] "double" 0 DewHeaterPercentage = Convert-Field $parts[10] "int" 0 # --- RAW FLAGS --- # Index 11 is often a raw rain frequency/bit, Index 12 is Wetness RainFlag = Convert-Field $parts[11] "int" 0 WetFlag = Convert-Field $parts[12] "int" 0 SinceGoodData = Convert-Field $parts[13] "int" 0 # Minutes since good data (0 means good now) DaysSinceLastWrite = Convert-Field $parts[14] "double" 0 # Days since last good data write (0 means good now) # --- SAFETY CONDITIONS (Boltwood Standard) --- # 1=Clear, 2=Cloudy, 3=Very Cloudy CloudCondition = Convert-Field $parts[15] "int" 1 # 1=Calm, 2=Windy, 3=Very Windy WindCondition = Convert-Field $parts[16] "int" 1 # 1=Dry, 2=Damp, 3=Rain RainCondition = Convert-Field $parts[17] "int" 1 # 1=Dark, 2=Dim, 3=Daylight DarknessCondition = Convert-Field $parts[18] "int" 1 RoofCloseRequested = Convert-Field $parts[19] "int" 0 # 0=No, 1=Yes AlertCondition = Convert-Field $parts[20] "int" 0 # 0=No Alert, 1=Alert # --- ALERT LOGIC --- IsRaining = $false IsLight = $false IsUnsafe = $false AlertMessage = "" } # LOGIC CHECKS if ($data.RainCondition -ge $UnsafeRain) { $data.IsRaining = $true $data.IsUnsafe = $true $data.AlertMessage += "[RAIN DETECTED] " } if ($data.DarknessCondition -ge $UnsafeLight) { $data.IsLight = $true $data.IsUnsafe = $true $data.AlertMessage += "[LIGHT] " } if ($data.WindCondition -ge $UnsafeWind) { $data.IsUnsafe = $true $data.AlertMessage += "[HIGH WIND] " } if ($data.CloudCondition -ge $UnsafeCloud) { $data.AlertMessage += "[CLOUDY] " } $data.AlertMessage = $data.AlertMessage.Trim() return $data } catch { Write-Error "Critical error mapping data fields: $_" return $null } } function Get-BoltwoodData { param([string]$url) try { $ProgressPreference = 'SilentlyContinue' $content = Invoke-WebRequest -Uri $url -UseBasicParsing return $content.Content } catch { #Write-Error "Failed to download or parse Boltwood data: $_" return $null } } function Get-FilePath { param([string]$path) # Convert any relative file path to an absolute file path based on current working directory $fullPath = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($PWD.ProviderPath, $path)) # Create directory if it doesn't exist $dir = Split-Path $fullPath if (!(Test-Path $dir)) { New-Item -ItemType Directory -Path $dir | Out-Null } return $fullPath } function Write-BoltwoodDataToFile { param([PSCustomObject]$text, [string]$filePath) # Convert the text to bytes using UTF-8 encoding for Write command below $enc = [system.Text.Encoding]::UTF8 $bytes = $enc.GetBytes($text) # Open the file with specific sharing permissions: # FileMode.Create: Overwrites if exists # FileAccess.Write: We are writing to it # FileShare.Read: ALLOWS other programs to read while we have it open $fileStream = New-Object System.IO.FileStream($filePath, [System.IO.FileMode]::Create, [System.IO.FileAccess]::Write, [System.IO.FileShare]::Read) # Write the downloaded bytes and close try { $fileStream.Write($bytes, 0, $bytes.Length) } finally { # Ensure the file is closed even if writing fails $fileStream.Close() $fileStream.Dispose() } } # Convert any relative file path to an absolute file path based on current working directory $fullPath = Get-FilePath -path $filePath Write-Host "Starting repeating download to $fullPath. Press Ctrl+C to stop." -ForegroundColor Cyan $lastFileTime = [DateTime]::MinValue # Initialize to a very old date so the first download always writes to the file while ($true) { try { # 1. Read the Boltwood data from the URL $text = Get-BoltwoodData -url $url if (-not $text) { Write-Warning "($(Get-Date -Format 'HH:mm:ss')) Warning: No data downloaded; skipping this iteration." continue } # 2. Convert the raw text data into a structured object for easier access and potential alert logic $weatherData = Convert-BoltwoodData -content $text # 3. Only write to the file if the timestamp has changed since last download, to avoid unnecessary writes and allow other programs to read it without conflicts $weatherDateTime = [DateTime]::ParseExact(($weatherData.Timestamp), "yyyy-MM-dd HH:mm:ss.ff", $null) if ($weatherDateTime -eq $lastFileTime) { continue # Skip writing if the timestamp hasn't changed since last download } $lastFileTime = $weatherDateTime # 3. Convert the raw text data into a structured object for easier access and potential alert logic $weatherData = Convert-BoltwoodData -content $text $display = ($weatherData | Format-List | Out-String).Trim() Write-Host $display -ForegroundColor White <# $spaceSeparatedString = ($weatherData.psobject.Properties.Value -join ' ') Write-Host $spaceSeparatedString -ForegroundColor Blue #> # Append to a log file in a readable list format if ($logOutput) { $newFilePath = [System.IO.Path]::ChangeExtension($fullPath, ".log") $weatherData | Format-List | Out-File -FilePath $newFilePath -Force } # Save as a spreadsheet-friendly file if ($csvOutput) { $newFilePath = [System.IO.Path]::ChangeExtension($fullPath, ".csv") $weatherData | Export-Csv -Path $newFilePath -NoTypeInformation -Append } # Save as a JSON file for other programs to read if ($jsonOutput) { $newFilePath = [System.IO.Path]::ChangeExtension($fullPath, ".json") $weatherData | ConvertTo-Json | Out-File -FilePath $newFilePath -Force } # Example: Trigger External Action on Rain if ($weatherData.IsRaining) { Write-Host "CRITICAL: Closing Roof due to Rain!" -ForegroundColor Red # Add command here: e.g., Invoke-RestMethod or Start-Process } elseif ($weatherData.IsUnsafe) { Write-Host "WARNING: Weather Unsafe: $($weatherData.AlertMessage)" -ForegroundColor Yellow } else { Write-Host "Status: Weather is Safe." -ForegroundColor Green } # 4. Write the structured data to the file with proper sharing permissions so other programs can read it while it's being updated Write-Host $text -NoNewline -ForegroundColor Green Write-BoltwoodDataToFile -text $text -filePath $fullPath } catch { Write-Warning "($(Get-Date -Format 'HH:mm:ss')) Error: $($_.Exception.Message)" } # Wait before the next iteration Start-Sleep -Seconds $intervalSeconds }