diff --git a/Build.ps1 b/Build.ps1 index 62dd068..c03fc62 100644 --- a/Build.ps1 +++ b/Build.ps1 @@ -1,4 +1,4 @@ -#usage -----> powershell -ExecutionPolicy Bypass -Command "& {. .\Build.ps1; Dist -major 1 -minor 1 -patch 0}" +#usage -----> powershell -ExecutionPolicy Bypass -Command "& {. .\Build.ps1; Dist -major 1 -minor 2 -patch 0}" $msbuild = "C:\Program Files\Microsoft Visual Studio\2022\Community\Msbuild\Current\Bin\MSBuild.exe" function Add-RMSkinFooter { diff --git a/README.md b/README.md index 9ccdeb7..acb1061 100644 --- a/README.md +++ b/README.md @@ -1,433 +1,477 @@ # WeatherX Plugin for Rainmeter + [![License: MIT](https://img.shields.io/badge/License-MIT-lightgrey.svg)]() -[![Version](https://img.shields.io/badge/version-1.0.1-blue.svg)]() +[![Version](https://img.shields.io/badge/version-1.1.0-blue.svg)]() [![Platform](https://img.shields.io/badge/platform-Windows-lightblue.svg)]() -A comprehensive weather plugin for Rainmeter that provides current weather conditions, forecasts, and detailed meteorological data using the Open-Meteo API. - -## Preview +A powerful Rainmeter plugin that provides real-time weather data, 7-day forecasts, hourly predictions, and location information using the free Open-Meteo API. -![WeatherX Skin Preview](https://github.com/NSTechBytes/WeatherX/blob/main/.github/preview.png) +## ✨ Features -## Features +- 🌡️ **Current Weather** - Temperature, humidity, wind, pressure, UV index +- 📅 **7-Day Forecast** - Daily high/low temperatures and conditions +- ⏰ **48-Hour Forecast** - Detailed hourly predictions +- 🌍 **Reverse Geocoding** - Automatic city/state/country detection +- ☀️ **Solar Data** - Radiation, sunrise/sunset times +- 🌙 **Day/Night Detection** - Perfect for dynamic themes +- 📊 **Multiple Units** - Metric or Imperial +- 🚀 **No API Key Required** - Free and unlimited -- **Real-time Weather Data**: Current temperature, humidity, wind conditions, and more -- **7-Day Forecasts**: Daily temperature ranges and weather conditions -- **48-Hour Forecasts**: Detailed hourly weather predictions -- **Solar Radiation Data**: UV index, solar radiation, direct and diffuse radiation -- **Comprehensive Metrics**: Apparent temperature, dew point, visibility, cloud cover -- **Day/Night Detection**: Automatic day/night status for dynamic theming -- **Numeric Weather Codes**: Both descriptive text and numeric codes for weather conditions -- **Multiple Units**: Support for metric and imperial units -- **Automatic Updates**: Configurable update intervals -- **Error Handling**: Built-in debugging and status reporting +## 📦 Installation -## Installation +### Quick Install (Recommended) +1. Download `WeatherX_v*.rmskin` from [Releases](https://github.com/NSTechBytes/WeatherX/releases) +2. Double-click to install +3. Update your coordinates in the skin +4. Done! 🎉 -### Option 1: RMSKIN Package (Recommended) -1. Download the latest `WeatherX_v*.rmskin` file from [Releases](https://github.com/NSTechBytes/WeatherX/releases) -2. Double-click the `.rmskin` file to automatically install in Rainmeter -3. Load the WeatherX skin from Rainmeter's skin browser -4. Configure your coordinates in the skin variables +### Manual Install +1. Download the plugin ZIP from [Releases](https://github.com/NSTechBytes/WeatherX/releases) +2. Copy `WeatherX.dll` to `%AppData%\Rainmeter\Plugins\` + - Use `x64` folder for 64-bit or `x32` for 32-bit +3. Refresh Rainmeter -### Option 2: Manual Installation -1. Download the plugin ZIP file from [Releases](https://github.com/NSTechBytes/WeatherX/releases) -2. Extract the appropriate DLL file (`x64` or `x32`) to your Rainmeter plugins folder -3. Place the skin files in your Rainmeter skins directory -4. Configure your coordinates and refresh Rainmeter +## 🚀 Quick Start Guide -## Configuration +### Step 1: Basic Setup -### Required Variables +Create a parent measure that handles all weather data: ```ini -[Variables] -Latitude=29.696489 ; Your location's latitude -Longitude=72.549843 ; Your location's longitude -Units=metric ; "metric" or "imperial" -UpdateInterval=300 ; Update frequency in seconds -``` +[Rainmeter] +Update=1000 -## Measure Options - -### DataType Options - -The plugin supports numerous `DataType` options for different weather measurements: - -#### Current Weather Data - -| DataType | Description | Return Type | Units (Metric/Imperial) | -|----------|-------------|-------------|-------------------------| -| `CurrentTemp` | Current temperature | Number | °C / °F | -| `CurrentCondition` | Current weather description | String | Text description | -| `CurrentHumidity` | Relative humidity | Number | % | -| `CurrentWindSpeed` | Wind speed | Number | km/h / mph | -| `CurrentWindDirection` | Wind direction in degrees | Number | 0-360° | -| `CurrentWindDirectionText` | Wind direction as text | String | N, NE, E, etc. | -| `CurrentPressure` | Atmospheric pressure | Number | hPa | -| `CurrentApparentTemp` | Feels-like temperature | Number | °C / °F | -| `CurrentDewPoint` | Dew point temperature | Number | °C / °F | -| `CurrentCloudCover` | Cloud coverage percentage | Number | % | -| `CurrentWindGusts` | Wind gust speed | Number | km/h / mph | -| `CurrentUvIndex` | UV index value | Number | 0-11+ | -| `CurrentSolarRadiation` | Solar radiation | Number | W/m² | -| `CurrentDirectRadiation` | Direct solar radiation | Number | W/m² | -| `CurrentDiffuseRadiation` | Diffuse solar radiation | Number | W/m² | -| `CurrentIsDay` | Day/night status | Number | 1.0 = Day, 0.0 = Night | -| `CurrentIsDayText` | Day/night status as text | String | "Day" or "Night" | -| `CurrentWeatherCode` | Weather code as number | Number | 0, 1, 2, 45, 61, etc. | -| `CurrentWeatherCodeText` | Weather code as text | String | "0", "1", "2", "45", "61", etc. | - -#### Daily Forecast Data - -| DataType | Description | Return Type | Additional Parameter | -|----------|-------------|-------------|----------------------| -| `ForecastTempMax` | Daily maximum temperature | Number | `ForecastDay=0-6` | -| `ForecastTempMin` | Daily minimum temperature | Number | `ForecastDay=0-6` | -| `ForecastCondition` | Daily weather condition | String | `ForecastDay=0-6` | -| `ForecastApparentTempMax` | Daily max apparent temperature | Number | `ForecastDay=0-6` | -| `ForecastApparentTempMin` | Daily min apparent temperature | Number | `ForecastDay=0-6` | -| `ForecastWindSpeed` | Daily max wind speed | Number | `ForecastDay=0-6` | -| `ForecastUvIndex` | Daily max UV index | Number | `ForecastDay=0-6` | -| `ForecastSunrise` | Sunrise time (hour value) | Number | `ForecastDay=0-6` | -| `ForecastSunset` | Sunset time (hour value) | Number | `ForecastDay=0-6` | -| `ForecastSunriseText` | Sunrise time as text | String | `ForecastDay=0-6` | -| `ForecastSunsetText` | Sunset time as text | String | `ForecastDay=0-6` | -| `ForecastWeatherCode` | Weather code as number | Number | `ForecastDay=0-6` | -| `ForecastWeatherCodeText` | Weather code as text | String | `ForecastDay=0-6` | - -#### Hourly Forecast Data - -| DataType | Description | Return Type | Additional Parameter | -|----------|-------------|-------------|----------------------| -| `HourlyTemp` | Hourly temperature | Number | `HourOffset=0-47` | -| `HourlyCondition` | Hourly weather condition | String | `HourOffset=0-47` | -| `HourlyHumidity` | Hourly humidity | Number | `HourOffset=0-47` | -| `HourlyWindSpeed` | Hourly wind speed | Number | `HourOffset=0-47` | -| `HourlyApparentTemp` | Hourly apparent temperature | Number | `HourOffset=0-47` | -| `HourlyCloudCover` | Hourly cloud coverage | Number | `HourOffset=0-47` | -| `HourlyVisibility` | Hourly visibility | Number | `HourOffset=0-47` | -| `HourlySolarRadiation` | Hourly solar radiation | Number | `HourOffset=0-47` | -| `HourlyDirectRadiation` | Hourly direct radiation | Number | `HourOffset=0-47` | -| `HourlyDiffuseRadiation` | Hourly diffuse radiation | Number | `HourOffset=0-47` | -| `HourlyTime` | Hourly timestamp | String | `HourOffset=0-47` | -| `HourlyWeatherCode` | Weather code as number | Number | `HourOffset=0-47` | -| `HourlyWeatherCodeText` | Weather code as text | String | `HourOffset=0-47` | - -#### Special Data Types - -| DataType | Description | Return Type | Purpose | -|----------|-------------|-------------|---------| -| `UvIndexText` | UV index description | String | "Low", "Moderate", "High", etc. | -| `NextHoursSummary` | Summary of next 6 hours | String | Quick forecast overview | -| `Status` | Plugin status | String | "Ready", "Updating...", "Error" | -| `DebugError` | Last error message | String | Troubleshooting | -| `DebugUrl` | API URL being used | String | Debugging | -| `DebugSolarRadiation` | Solar radiation debug info | String | Detailed solar data | -| `DebugCloudCover` | Cloud cover debug info | String | Cloud coverage details | - -## Usage Examples - -### Basic Temperature Display +[Variables] +Latitude=40.7128 ; New York coordinates +Longitude=-74.0060 +Units=metric ; "metric" or "imperial" +UpdateInterval=600 ; Update every 10 minutes -```ini -[MeasureCurrentTemp] +[mWeatherParent] Measure=Plugin Plugin=WeatherX.dll -DataType=CurrentTemp Latitude=#Latitude# Longitude=#Longitude# Units=#Units# UpdateInterval=#UpdateInterval# - -[MeterTemp] -Meter=String -MeasureName=MeasureCurrentTemp -Text=%1° -FontSize=24 +DataType=CurrentTemp ``` -### Daily Forecast +### Step 2: Add Child Measures + +Use child measures to get specific weather data: ```ini -[MeasureTomorrowMax] +[mTemperature] Measure=Plugin Plugin=WeatherX.dll -DataType=ForecastTempMax -ForecastDay=1 -Latitude=#Latitude# -Longitude=#Longitude# -Units=#Units# -UpdateInterval=#UpdateInterval# +ParentName=mWeatherParent +DataType=CurrentTemp -[MeasureTomorrowMin] +[mCondition] Measure=Plugin Plugin=WeatherX.dll -DataType=ForecastTempMin -ForecastDay=1 -Latitude=#Latitude# -Longitude=#Longitude# -Units=#Units# -UpdateInterval=#UpdateInterval# +ParentName=mWeatherParent +DataType=CurrentCondition + +[mHumidity] +Measure=Plugin +Plugin=WeatherX.dll +ParentName=mWeatherParent +DataType=CurrentHumidity ``` -### Hourly Forecast +### Step 3: Display the Data + +```ini +[MeterWeather] +Meter=String +MeasureName=mTemperature +MeasureName2=mCondition +MeasureName3=mHumidity +X=10 +Y=10 +FontSize=12 +FontColor=255,255,255 +Text="Temp: %1°#CRLF#Condition: %2#CRLF#Humidity: %3%" +``` +### How to Get Your Coordinates + +1. Go to [Google Maps](https://maps.google.com) +2. Right-click your location +3. Click the coordinates to copy them +4. Paste into your `Latitude` and `Longitude` variables + +## 📊 Available Data Types + +### Current Weather + +| DataType | Description | Example Value | +|----------|-------------|---------------| +| `CurrentTemp` | Current temperature | 22.5 | +| `CurrentCondition` | Weather description | "Clear Sky" | +| `CurrentHumidity` | Relative humidity | 65 | +| `CurrentWindSpeed` | Wind speed | 15.2 | +| `CurrentWindDirection` | Wind direction degrees | 180 | +| `CurrentWindDirectionText` | Wind direction | "S" | +| `CurrentPressure` | Atmospheric pressure | 1013.2 | +| `CurrentApparentTemp` | Feels-like temperature | 24.1 | +| `CurrentDewPoint` | Dew point | 16.3 | +| `CurrentCloudCover` | Cloud coverage % | 25 | +| `CurrentWindGusts` | Wind gusts speed | 22.5 | +| `CurrentUvIndex` | UV index | 5.2 | +| `CurrentIsDay` | Day=1, Night=0 | 1.0 | +| `CurrentIsDayText` | Day/Night as text | "Day" | +| `CurrentWeatherCode` | Weather code number | 0 | +| `CurrentWeatherCodeText` | Weather code as text | "0" | +| `CurrentSolarRadiation` | Solar radiation W/m² | 450.5 | + +### Daily Forecast (0-6 days) + +Add `ForecastDay=0` (today) to `ForecastDay=6` (6 days ahead) + +| DataType | Description | +|----------|-------------| +| `ForecastTempMax` | Daily maximum temperature | +| `ForecastTempMin` | Daily minimum temperature | +| `ForecastCondition` | Daily weather condition | +| `ForecastApparentTempMax` | Max feels-like temperature | +| `ForecastApparentTempMin` | Min feels-like temperature | +| `ForecastWindSpeed` | Max wind speed | +| `ForecastUvIndex` | Max UV index | +| `ForecastSunrise` | Sunrise hour (numeric) | +| `ForecastSunset` | Sunset hour (numeric) | +| `ForecastSunriseText` | Sunrise time "HH:mm" | +| `ForecastSunsetText` | Sunset time "HH:mm" | +| `ForecastWeatherCode` | Weather code number | +| `ForecastWeatherCodeText` | Weather code as text | + +**Example:** ```ini -[MeasureNext3Hours] +[mTomorrowMax] Measure=Plugin Plugin=WeatherX.dll +ParentName=mWeatherParent +DataType=ForecastTempMax +ForecastDay=1 +``` + +### Hourly Forecast (0-47 hours) + +Add `HourOffset=0` (current hour) to `HourOffset=47` (47 hours ahead) + +| DataType | Description | +|----------|-------------| +| `HourlyTemp` | Hourly temperature | +| `HourlyCondition` | Hourly weather condition | +| `HourlyHumidity` | Hourly humidity | +| `HourlyWindSpeed` | Hourly wind speed | +| `HourlyApparentTemp` | Hourly feels-like temp | +| `HourlyCloudCover` | Hourly cloud coverage | +| `HourlyVisibility` | Hourly visibility | +| `HourlyWeatherCode` | Weather code number | +| `HourlyWeatherCodeText` | Weather code as text | +| `HourlyTime` | Hour timestamp | +| `HourlySolarRadiation` | Solar radiation W/m² | + +**Example:** +```ini +[mNext3Hours] +Measure=Plugin +Plugin=WeatherX.dll +ParentName=mWeatherParent DataType=HourlyTemp HourOffset=3 -Latitude=#Latitude# -Longitude=#Longitude# -Units=#Units# -UpdateInterval=#UpdateInterval# ``` -### Wind Information +### Location Data (Reverse Geocoding) +Enable with `EnableReverseGeocode=1` in parent measure: + +| DataType | Description | Example | +|----------|-------------|----------| +| `LocationCity` | City name | "New York" | +| `LocationState` | State/Province | "New York" | +| `LocationStateCode` | State code | "NY" | +| `LocationCountry` | Country name | "United States" | +| `LocationCountryCode` | Country code | "US" | +| `LocationContinent` | Continent name | "North America" | +| `LocationContinentCode` | Continent code | "NA" | +| `LocationFull` | Full location string | "New York, NY, United States" | + +**Example:** ```ini -[MeasureWindSpeed] +[mWeatherParent] Measure=Plugin Plugin=WeatherX.dll -DataType=CurrentWindSpeed Latitude=#Latitude# Longitude=#Longitude# -Units=#Units# -UpdateInterval=#UpdateInterval# +EnableReverseGeocode=1 +DataType=CurrentTemp -[MeasureWindDirection] +[mCity] Measure=Plugin Plugin=WeatherX.dll -DataType=CurrentWindDirectionText -Latitude=#Latitude# -Longitude=#Longitude# -Units=#Units# -UpdateInterval=#UpdateInterval# - -[MeterWind] -Meter=String -MeasureName=MeasureWindSpeed -MeasureName2=MeasureWindDirection -Text=Wind: %1 %2 +ParentName=mWeatherParent +DataType=LocationCity ``` -### Day/Night Detection +### Special Data Types + +| DataType | Description | +|----------|-------------| +| `UvIndexText` | UV description: "Low", "Moderate", "High", etc. | +| `NextHoursSummary` | Summary of next 6 hours | +| `Status` | Plugin status: "Ready", "Updating...", "Error" | +| `DebugError` | Last error message | +| `DebugUrl` | API URL being used | +| `DebugLastUpdate` | Last update time | +| `DebugNextUpdate` | Time until next update | + + +## 🌦️ Weather Codes Reference + +| Code | Description | +|------|-------------| +| 0 | Clear Sky | +| 1 | Mainly Clear | +| 2 | Partly Cloudy | +| 3 | Overcast | +| 45 | Fog | +| 48 | Depositing Rime Fog | +| 51 | Light Drizzle | +| 53 | Moderate Drizzle | +| 55 | Dense Drizzle | +| 61 | Slight Rain | +| 63 | Moderate Rain | +| 65 | Heavy Rain | +| 71 | Slight Snow | +| 73 | Moderate Snow | +| 75 | Heavy Snow | +| 80 | Slight Rain Showers | +| 81 | Moderate Rain Showers | +| 82 | Violent Rain Showers | +| 95 | Thunderstorm | +| 96 | Thunderstorm with Hail | + +## 🚀 Advanced Examples + +### Complete Weather Widget ```ini -[MeasureDayNight] +[Rainmeter] +Update=1000 + +[Variables] +Latitude=40.7128 +Longitude=-74.0060 +Units=imperial +UpdateInterval=600 + +; Parent measure +[mWeatherParent] Measure=Plugin Plugin=WeatherX.dll -DataType=CurrentIsDay Latitude=#Latitude# Longitude=#Longitude# Units=#Units# UpdateInterval=#UpdateInterval# +EnableReverseGeocode=1 +DataType=CurrentTemp -[MeasureDayNightText] +; Current weather +[mTemp] Measure=Plugin Plugin=WeatherX.dll -DataType=CurrentIsDayText -Latitude=#Latitude# -Longitude=#Longitude# -Units=#Units# -UpdateInterval=#UpdateInterval# +ParentName=mWeatherParent +DataType=CurrentTemp + +[mCondition] +Measure=Plugin +Plugin=WeatherX.dll +ParentName=mWeatherParent +DataType=CurrentCondition + +[mHumidity] +Measure=Plugin +Plugin=WeatherX.dll +ParentName=mWeatherParent +DataType=CurrentHumidity + +[mWindSpeed] +Measure=Plugin +Plugin=WeatherX.dll +ParentName=mWeatherParent +DataType=CurrentWindSpeed + +[mWindDir] +Measure=Plugin +Plugin=WeatherX.dll +ParentName=mWeatherParent +DataType=CurrentWindDirectionText + +; Location +[mCity] +Measure=Plugin +Plugin=WeatherX.dll +ParentName=mWeatherParent +DataType=LocationCity + +; Display meters +[MeterLocation] +Meter=String +MeasureName=mCity +X=10 +Y=10 +FontSize=14 +FontWeight=700 +FontColor=255,255,255 +Text="%1" -[MeterDayNight] +[MeterTemp] +Meter=String +MeasureName=mTemp +X=10 +Y=35 +FontSize=48 +FontColor=255,255,255 +Text="%1°" + +[MeterCondition] Meter=String -MeasureName=MeasureDayNightText -Text=Time: %1 +MeasureName=mCondition +X=10 +Y=90 +FontSize=16 +FontColor=200,200,200 +Text="%1" + +[MeterDetails] +Meter=String +MeasureName=mHumidity +MeasureName2=mWindSpeed +MeasureName3=mWindDir +X=10 +Y=115 +FontSize=12 +FontColor=180,180,180 +Text="Humidity: %1% | Wind: %2 %3" ``` -### Weather Code Usage +### 7-Day Forecast ```ini -[MeasureWeatherCode] +; Tomorrow's forecast +[mTomorrowHigh] Measure=Plugin Plugin=WeatherX.dll -DataType=CurrentWeatherCode -Latitude=#Latitude# -Longitude=#Longitude# -Units=#Units# -UpdateInterval=#UpdateInterval# +ParentName=mWeatherParent +DataType=ForecastTempMax +ForecastDay=1 -[MeasureWeatherCodeText] +[mTomorrowLow] Measure=Plugin Plugin=WeatherX.dll -DataType=CurrentWeatherCodeText -Latitude=#Latitude# -Longitude=#Longitude# -Units=#Units# -UpdateInterval=#UpdateInterval# +ParentName=mWeatherParent +DataType=ForecastTempMin +ForecastDay=1 -[MeterWeatherCode] +[mTomorrowCondition] +Measure=Plugin +Plugin=WeatherX.dll +ParentName=mWeatherParent +DataType=ForecastCondition +ForecastDay=1 + +[MeterTomorrow] Meter=String -MeasureName=MeasureWeatherCode -MeasureName2=MeasureWeatherCodeText -Text=Code: %1 (%2) +MeasureName=mTomorrowHigh +MeasureName2=mTomorrowLow +MeasureName3=mTomorrowCondition +X=10 +Y=150 +FontSize=11 +Text="Tomorrow: %1°/%2° - %3" ``` -### Dynamic Theming Example +### Dynamic Day/Night Backgrounds ```ini -[MeasureDayNight] +[mIsDay] Measure=Plugin Plugin=WeatherX.dll +ParentName=mWeatherParent DataType=CurrentIsDay -Latitude=#Latitude# -Longitude=#Longitude# -Units=#Units# -UpdateInterval=#UpdateInterval# -[MeasureWeatherCode] +[mWeatherCode] Measure=Plugin Plugin=WeatherX.dll +ParentName=mWeatherParent DataType=CurrentWeatherCode -Latitude=#Latitude# -Longitude=#Longitude# -Units=#Units# -UpdateInterval=#UpdateInterval# -[MeterBackground] +[MeterBG] Meter=Image -; Dynamic background based on day/night and weather -ImageName=#@#Images\Weather\%1_%2.png -MeasureName=MeasureDayNight -MeasureName2=MeasureWeatherCode -; This would use images like: 1_0.png (day_clear), 0_61.png (night_rain), etc. +; Use different images based on time and weather +; Image naming: day_0.jpg (day, clear), night_61.jpg (night, rain) +ImageName=#@#Backgrounds\[mIsDay]_[mWeatherCode].jpg +W=400 +H=300 ``` -## Parameters - -### Required Parameters - -- **`Latitude`**: Your location's latitude (-90 to 90) -- **`Longitude`**: Your location's longitude (-180 to 180) -- **`DataType`**: The type of weather data to retrieve (see table above) - -### Optional Parameters - -- **`Units`**: Unit system ("metric" or "imperial", default: "metric") -- **`UpdateInterval`**: Update frequency in seconds (default: 600) -- **`ForecastDay`**: Day offset for forecast data (0-6, default: 0) -- **`HourOffset`**: Hour offset for hourly data (0-47, default: 0) -- **`Timezone`**: Timezone identifier (default: "auto") - -## Data Source - -This plugin uses the [Open-Meteo API](https://open-meteo.com/), which provides: -- Free weather data -- No API key required -- Global coverage -- High accuracy -- Multiple forecast models - -## Error Handling - -The plugin includes comprehensive error handling: -- Network timeout protection -- HTTPS/HTTP fallback -- Invalid coordinate detection -- JSON parsing error recovery -- Status reporting via `Status` DataType - -## Debugging - -Use these DataTypes for troubleshooting: -- `Status`: Current plugin status -- `DebugError`: Last error message -- `DebugUrl`: API URL being called -- `DebugSolarRadiation`: Solar data details -- `DebugCloudCover`: Cloud cover information - -## Units - -### Metric System (default) -- Temperature: Celsius (°C) -- Wind Speed: km/h -- Pressure: hPa -- Visibility: km -- Radiation: W/m² - -### Imperial System -- Temperature: Fahrenheit (°F) -- Wind Speed: mph -- Pressure: hPa (unchanged) -- Visibility: km (unchanged) -- Radiation: W/m² (unchanged) - -## Weather Codes - -The plugin translates numeric weather codes into descriptive text: - -| Code | Description | -| ---- | ----------------------------- | -| 0 | Clear Sky | -| 1 | Mainly Clear | -| 2 | Partly Cloudy | -| 3 | Overcast | -| 45 | Fog | -| 48 | Depositing Rime Fog | -| 51 | Light Drizzle | -| 53 | Moderate Drizzle | -| 55 | Dense Drizzle | -| 56 | Light Freezing Drizzle | -| 57 | Dense Freezing Drizzle | -| 61 | Slight Rain | -| 63 | Moderate Rain | -| 65 | Heavy Rain | -| 66 | Light Freezing Rain | -| 67 | Heavy Freezing Rain | -| 71 | Slight Snow | -| 73 | Moderate Snow | -| 75 | Heavy Snow | -| 77 | Snow Grains | -| 80 | Slight Rain Showers | -| 81 | Moderate Rain Showers | -| 82 | Violent Rain Showers | -| 85 | Slight Snow Showers | -| 86 | Heavy Snow Showers | -| 95 | Thunderstorm | -| 96 | Thunderstorm with Slight Hail | -| 99 | Thunderstorm with Heavy Hail | - -## Performance - -- Automatic HTTP fallback for network issues -- Efficient JSON parsing without external dependencies -- Configurable update intervals to minimize API calls -- Memory-efficient data storage -- Non-blocking asynchronous updates - -## Building from Source - -### Prerequisites -- Visual Studio 2022 with C++ development tools -- .NET Framework 4.7.2 or later -- PowerShell (for build script) - -### Build Instructions -1. Clone the repository -2. Open PowerShell in the project directory -3. Run the build script: - ```powershell - powershell -ExecutionPolicy Bypass -Command ". .\Build.ps1; Dist -major 1 -minor 0 -patch 0" - ``` -4. Find the built files in the `dist/` folder: - - `WeatherX_v*.rmskin` - Complete skin package - - `WeatherX_v*_x64_x86_dll.zip` - Plugin DLLs only - -## Requirements - -- Rainmeter 4.5 or later -- .NET Framework 4.7.2 or later -- Internet connection - -## License - -MIT - -## Contributing - -Contributions are welcome! Please feel free to submit issues and enhancement requests. +## 🔧 Troubleshooting + +### Plugin Not Loading +- Ensure you're using the correct DLL version (x64 vs x32) +- Check Rainmeter log for error messages +- Verify .NET Framework 4.8 is installed + +### No Data Showing +- Check your coordinates are correct (use Google Maps) +- Verify internet connection +- Check `DebugError` DataType for error messages +- Ensure `UpdateInterval` is not too short (minimum 60 seconds recommended) + +### Location Not Updating +- Make sure `EnableReverseGeocode=1` is set in parent measure +- Location updates when coordinates change +- Check Rainmeter log for geocoding errors + +### Debug Example + +```ini +[mDebugStatus] +Measure=Plugin +Plugin=WeatherX.dll +ParentName=mWeatherParent +DataType=Status + +[mDebugError] +Measure=Plugin +Plugin=WeatherX.dll +ParentName=mWeatherParent +DataType=DebugError + +[MeterDebug] +Meter=String +MeasureName=mDebugStatus +MeasureName2=mDebugError +Text="Status: %1#CRLF#Error: %2" +``` +## 📝 License + +MIT License - Free to use and modify + +## 👏 Credits + +- Weather data: [Open-Meteo API](https://open-meteo.com/) +- Reverse geocoding: [BigDataCloud API](https://www.bigdatacloud.com/) +- Plugin framework: [Rainmeter](https://www.rainmeter.net/) + +## 🐛 Issues & Support + +Found a bug or have a feature request? [Open an issue](https://github.com/NSTechBytes/WeatherX/issues) + +--- + +Made with ❤️ for the Rainmeter community diff --git a/Resources/Skins/WeatherX/FullExample.ini b/Resources/Skins/WeatherX/FullExample.ini new file mode 100644 index 0000000..dba8061 Binary files /dev/null and b/Resources/Skins/WeatherX/FullExample.ini differ diff --git a/Resources/Skins/WeatherX/Main.ini b/Resources/Skins/WeatherX/Main.ini index e4247cd..8467e58 100644 Binary files a/Resources/Skins/WeatherX/Main.ini and b/Resources/Skins/WeatherX/Main.ini differ diff --git a/Resources/Skins/WeatherX/Old-Main.ini b/Resources/Skins/WeatherX/Old-Main.ini new file mode 100644 index 0000000..4a91584 Binary files /dev/null and b/Resources/Skins/WeatherX/Old-Main.ini differ diff --git a/Resources/Skins/WeatherX/README.txt b/Resources/Skins/WeatherX/README.txt new file mode 100644 index 0000000..d15dfaf --- /dev/null +++ b/Resources/Skins/WeatherX/README.txt @@ -0,0 +1,208 @@ +================================================================================ + WEATHERX PLUGIN FOR RAINMETER - USER GUIDE +================================================================================ + +VERSION: 1.1.0 +AUTHOR: NSTechBytes +LICENSE: MIT +GITHUB: https://github.com/NSTechBytes/WeatherX + +================================================================================ + GETTING STARTED +================================================================================ + +1. FIND YOUR COORDINATES + ------------------------ + - Go to https://maps.google.com + - Right-click your location + - Click the coordinates to copy + - Example: 40.7128, -74.0060 + (First = Latitude, Second = Longitude) + +2. EDIT THE SKIN + -------------- + - Open any .ini file in this folder + - Find the [Variables] section + - Update Latitude and Longitude + - Choose Units (metric or imperial) + - Save and refresh the skin + +3. EXAMPLE SKINS INCLUDED + ----------------------- + Simple.ini - Minimal weather display (RECOMMENDED FOR BEGINNERS) + Main.ini - Full-featured weather widget with location + ReverseGeocode.ini - Demonstrates location detection + FullExample.ini - Shows all available features + +================================================================================ + BASIC STRUCTURE +================================================================================ + +PARENT MEASURE (Required - One per location) +--------------------------------------------- +Makes the API call and fetches weather data: + +[mWeatherParent] +Measure=Plugin +Plugin=WeatherX.dll +Latitude=40.7128 +Longitude=-74.0060 +Units=metric +UpdateInterval=600 +DataType=CurrentTemp + +CHILD MEASURES (Get specific data) +----------------------------------- +Don't make API calls - retrieve data from parent: + +[mTemp] +Measure=Plugin +Plugin=WeatherX.dll +ParentName=mWeatherParent ; Links to parent +DataType=CurrentTemp ; What data to get + +DISPLAY METER +------------- +[MeterTemp] +Meter=String +MeasureName=mTemp +Text="%1°" + +================================================================================ + POPULAR DATA TYPES +================================================================================ + +CURRENT WEATHER +--------------- +CurrentTemp - Current temperature +CurrentCondition - Weather description +CurrentHumidity - Humidity percentage +CurrentWindSpeed - Wind speed +CurrentWindDirectionText - Wind direction (N, NE, E, etc.) +CurrentApparentTemp - Feels-like temperature +CurrentUvIndex - UV index +CurrentIsDay - 1.0 = day, 0.0 = night + +DAILY FORECAST (Add ForecastDay=0 to 6) +---------------------------------------- +ForecastTempMax - High temperature +ForecastTempMin - Low temperature +ForecastCondition - Weather condition +ForecastSunriseText - Sunrise time +ForecastSunsetText - Sunset time + +Example: +DataType=ForecastTempMax +ForecastDay=1 ; Tomorrow + +HOURLY FORECAST (Add HourOffset=0 to 47) +----------------------------------------- +HourlyTemp - Temperature +HourlyCondition - Weather condition +HourlyHumidity - Humidity + +Example: +DataType=HourlyTemp +HourOffset=3 ; 3 hours from now + +LOCATION (Add EnableReverseGeocode=1 to parent) +------------------------------------------------ +LocationCity - City name +LocationState - State/Province +LocationCountry - Country name +LocationFull - Full location string + +================================================================================ + UNITS +================================================================================ + +METRIC (default) +---------------- +Temperature: Celsius (°C) +Wind Speed: km/h +Pressure: hPa + +IMPERIAL +-------- +Temperature: Fahrenheit (°F) +Wind Speed: mph +Pressure: hPa + +Set in parent measure: +Units=metric or Units=imperial + +================================================================================ + TROUBLESHOOTING +================================================================================ + +NO DATA SHOWING? +---------------- +✓ Check coordinates are correct +✓ Verify internet connection +✓ Check Rainmeter log (Ctrl+G) +✓ Use DataType=DebugError to see errors + +PLUGIN NOT LOADING? +------------------- +✓ Using correct DLL version (x64 vs x32)? +✓ .NET Framework 4.8 installed? +✓ Try restarting Rainmeter + +LOCATION NOT SHOWING? +--------------------- +✓ Add EnableReverseGeocode=1 to parent measure +✓ Wait for first update (default 10 minutes) + +================================================================================ + TIPS & BEST PRACTICES +================================================================================ + +1. Use ONE parent measure per location + Create child measures for each data point + +2. Update Interval + Don't set below 60 seconds (respect the free API) + Default 600 seconds (10 minutes) is recommended + +3. Testing + Use DataType=Status to check if plugin is working + Use DebugError to see any error messages + +4. Multiple Locations + Create separate parent measures with different names + +5. Dynamic Themes + Use CurrentIsDay to change colors/images for day/night + Use CurrentWeatherCode for weather-specific themes + +================================================================================ + WEATHER CODES +================================================================================ + +0 = Clear Sky 61 = Slight Rain +1 = Mainly Clear 63 = Moderate Rain +2 = Partly Cloudy 65 = Heavy Rain +3 = Overcast 71 = Slight Snow +45 = Fog 73 = Moderate Snow +51 = Light Drizzle 75 = Heavy Snow +53 = Moderate Drizzle 80 = Rain Showers +55 = Dense Drizzle 95 = Thunderstorm + +================================================================================ + COMPLETE DOCUMENTATION +================================================================================ + +For complete documentation, examples, and troubleshooting: +- README.md - Full documentation +- QUICKSTART.md - Step-by-step beginner guide +- GitHub: https://github.com/NSTechBytes/WeatherX + +================================================================================ + SUPPORT +================================================================================ + +Found a bug? Have a question? +Open an issue: https://github.com/NSTechBytes/WeatherX/issues + +Made with ❤️ for the Rainmeter community +================================================================================ diff --git a/Resources/Skins/WeatherX/ReverseGeocode.ini b/Resources/Skins/WeatherX/ReverseGeocode.ini new file mode 100644 index 0000000..7663ed3 Binary files /dev/null and b/Resources/Skins/WeatherX/ReverseGeocode.ini differ diff --git a/Resources/Skins/WeatherX/Simple.ini b/Resources/Skins/WeatherX/Simple.ini new file mode 100644 index 0000000..94efd61 Binary files /dev/null and b/Resources/Skins/WeatherX/Simple.ini differ diff --git a/Resources/skin_definition.json b/Resources/skin_definition.json index d541394..823fc21 100644 --- a/Resources/skin_definition.json +++ b/Resources/skin_definition.json @@ -1,6 +1,6 @@ { "skinDir": ".\\Resources\\Skins", - "version": "1.1.0", + "version": "1.2.0", "minimumVersion": "4.5", "author": "nstechbytes", "variableFiles": "", @@ -17,5 +17,5 @@ "load": "WeatherX\\Main.ini", "headerImage": ".\\Resources\\banner.bmp", "configPrefix": "WeatherX", - "output": ".\\dist\\WeatherX_v1.1.0.rmskin" + "output": ".\\dist\\WeatherX_v1.2.0.rmskin" } diff --git a/WeatherX/AssemblyInfo.cs b/WeatherX/AssemblyInfo.cs index eb1d1d6..e700635 100644 --- a/WeatherX/AssemblyInfo.cs +++ b/WeatherX/AssemblyInfo.cs @@ -2,7 +2,7 @@ using System.Runtime.CompilerServices; [assembly: AssemblyCopyright("© 2025 - nstechbytes 🇵🇰")] -[assembly: AssemblyVersion("1.1.0.0")] +[assembly: AssemblyVersion("1.2.0.0")] // Do not change the entries below! #if X64 diff --git a/WeatherX/ParseHelper.cs b/WeatherX/ParseHelper.cs index 71eb03d..22aade3 100644 --- a/WeatherX/ParseHelper.cs +++ b/WeatherX/ParseHelper.cs @@ -44,6 +44,60 @@ public static double ParseJsonValue(string json, string section, string key) } } + public static string ParseJsonStringValue(string json, string key) + { + try + { + // Find the key in the JSON + string searchKey = $"\"{key}\""; + int keyStart = json.IndexOf(searchKey); + if (keyStart == -1) return ""; + + // Find the colon after the key + int colonPos = json.IndexOf(":", keyStart); + if (colonPos == -1) return ""; + + // Find the start of the value (skip whitespace) + int valueStart = colonPos + 1; + while (valueStart < json.Length && char.IsWhiteSpace(json[valueStart])) + { + valueStart++; + } + + // Check if the value is a string (starts with quote) + if (valueStart >= json.Length || json[valueStart] != '"') + return ""; + + // Find the end quote (handle escaped quotes) + int valueEnd = valueStart + 1; + while (valueEnd < json.Length) + { + if (json[valueEnd] == '"' && json[valueEnd - 1] != '\\') + { + break; + } + valueEnd++; + } + + if (valueEnd >= json.Length) return ""; + + // Extract the string value (without quotes) + string value = json.Substring(valueStart + 1, valueEnd - valueStart - 1); + + // Handle null or empty values + if (string.IsNullOrEmpty(value) || value.Equals("null", StringComparison.OrdinalIgnoreCase)) + { + return ""; + } + + return value; + } + catch + { + return ""; + } + } + public static string ExtractHourlySection(string json, int hourlyStart) { int braceStart = json.IndexOf("{", hourlyStart); diff --git a/WeatherX/Plugin.cs b/WeatherX/Plugin.cs index 41d76af..5319bbe 100644 --- a/WeatherX/Plugin.cs +++ b/WeatherX/Plugin.cs @@ -9,12 +9,32 @@ public static class Plugin [DllExport] public static void Initialize(ref IntPtr data, IntPtr rm) { - data = GCHandle.ToIntPtr(GCHandle.Alloc(new Measure())); + Rainmeter.API api = new Rainmeter.API(rm); + + string parentName = api.ReadString("ParentName", ""); + Measure measure; + + if (String.IsNullOrEmpty(parentName)) + { + // This is a parent measure + measure = new ParentMeasure(); + api.Log(API.LogType.Debug, "WeatherX: Initialized as Parent measure"); + } + else + { + // This is a child measure + measure = new ChildMeasure(); + api.Log(API.LogType.Debug, $"WeatherX: Initialized as Child measure with ParentName={parentName}"); + } + + data = GCHandle.ToIntPtr(GCHandle.Alloc(measure)); } [DllExport] public static void Finalize(IntPtr data) { + Measure measure = (Measure)GCHandle.FromIntPtr(data).Target; + measure.Dispose(); GCHandle.FromIntPtr(data).Free(); if (StringBuffer != IntPtr.Zero) diff --git a/WeatherX/WeatherX.cs b/WeatherX/WeatherX.cs index 859da55..e1d117e 100644 --- a/WeatherX/WeatherX.cs +++ b/WeatherX/WeatherX.cs @@ -18,85 +18,73 @@ protected override WebRequest GetWebRequest(Uri address) } } -internal class Measure +// Shared weather data cache that all measures can access +internal static class WeatherDataCache { - private double latitude, longitude; - private string dataType; - private int forecastDay; - private int hourOffset; - private int updateInterval; - private DateTime lastUpdate; - private API api; - private string units; - private string timezone; - - private double currentTemp = 0.0; - private string currentCondition = "Unknown"; - private double currentHumidity = 0.0; - private double currentWindSpeed = 0.0; - private double currentPressure = 0.0; - private double currentApparentTemp = 0.0; - private double currentDewPoint = 0.0; - private double currentCloudCover = 0.0; - private double currentWindDirection = 0.0; - private double currentWindGusts = 0.0; - - private double currentSolarRadiation = 0.0; - private double currentDirectRadiation = 0.0; - private double currentDiffuseRadiation = 0.0; - private double currentIsDay = 0.0; - private double currentWeatherCode = 0.0; - - private double todayUvIndex = 0.0; - - private double[] forecastTempMax = new double[7]; - private double[] forecastTempMin = new double[7]; - private double[] forecastApparentTempMax = new double[7]; - private double[] forecastApparentTempMin = new double[7]; - private string[] forecastConditions = new string[7]; - private double[] forecastWeatherCode = new double[7]; - private double[] forecastWindSpeedMax = new double[7]; - private double[] forecastUvIndexMax = new double[7]; - private string[] forecastSunrise = new string[7]; - private string[] forecastSunset = new string[7]; - - private double[] hourlyTemp = new double[48]; - private double[] hourlyHumidity = new double[48]; - private double[] hourlyWindSpeed = new double[48]; - private double[] hourlyApparentTemp = new double[48]; - private double[] hourlyCloudCover = new double[48]; - private double[] hourlyVisibility = new double[48]; - private int[] hourlyWeatherCode = new int[48]; - private string[] hourlyTime = new string[48]; - - private double[] hourlySolarRadiation = new double[48]; - private double[] hourlyDirectRadiation = new double[48]; - private double[] hourlyDiffuseRadiation = new double[48]; - - private string lastError = ""; - private string lastApiUrl = ""; - private volatile bool isUpdating = false; - private bool initialUpdateScheduled = false; - private Timer updateTimer; - - internal Measure() + public static double currentTemp = 0.0; + public static string currentCondition = "Unknown"; + public static double currentHumidity = 0.0; + public static double currentWindSpeed = 0.0; + public static double currentPressure = 0.0; + public static double currentApparentTemp = 0.0; + public static double currentDewPoint = 0.0; + public static double currentCloudCover = 0.0; + public static double currentWindDirection = 0.0; + public static double currentWindGusts = 0.0; + public static double currentSolarRadiation = 0.0; + public static double currentDirectRadiation = 0.0; + public static double currentDiffuseRadiation = 0.0; + public static double currentIsDay = 0.0; + public static double currentWeatherCode = 0.0; + public static double todayUvIndex = 0.0; + + public static double[] forecastTempMax = new double[7]; + public static double[] forecastTempMin = new double[7]; + public static double[] forecastApparentTempMax = new double[7]; + public static double[] forecastApparentTempMin = new double[7]; + public static string[] forecastConditions = new string[7]; + public static double[] forecastWeatherCode = new double[7]; + public static double[] forecastWindSpeedMax = new double[7]; + public static double[] forecastUvIndexMax = new double[7]; + public static string[] forecastSunrise = new string[7]; + public static string[] forecastSunset = new string[7]; + + public static double[] hourlyTemp = new double[48]; + public static double[] hourlyHumidity = new double[48]; + public static double[] hourlyWindSpeed = new double[48]; + public static double[] hourlyApparentTemp = new double[48]; + public static double[] hourlyCloudCover = new double[48]; + public static double[] hourlyVisibility = new double[48]; + public static int[] hourlyWeatherCode = new int[48]; + public static string[] hourlyTime = new string[48]; + public static double[] hourlySolarRadiation = new double[48]; + public static double[] hourlyDirectRadiation = new double[48]; + public static double[] hourlyDiffuseRadiation = new double[48]; + + public static DateTime lastUpdate = DateTime.MinValue; + public static string lastError = ""; + public static string lastApiUrl = ""; + + // Reverse geocoding data from BigDataCloud API + public static string locationContinent = ""; + public static string locationContinentCode = ""; + public static string locationCountryName = ""; + public static string locationCountryCode = ""; + public static string locationPrincipalSubdivision = ""; + public static string locationPrincipalSubdivisionCode = ""; + public static string locationCity = ""; + public static DateTime lastGeocodeUpdate = DateTime.MinValue; + public static double lastGeocodedLatitude = 0.0; + public static double lastGeocodedLongitude = 0.0; + public static bool hasGeocodedBefore = false; + + static WeatherDataCache() { - latitude = 0.0; - longitude = 0.0; - dataType = "CurrentTemp"; - forecastDay = 0; - hourOffset = 0; - updateInterval = 600; - lastUpdate = DateTime.MinValue; - units = "metric"; - timezone = "auto"; - InitializeArrays(); } - private void InitializeArrays() + public static void InitializeArrays() { - for (int i = 0; i < 7; i++) { forecastTempMax[i] = 0.0; @@ -126,8 +114,59 @@ private void InitializeArrays() hourlyDiffuseRadiation[i] = 0.0; } } +} + +internal class Measure +{ + protected double latitude, longitude; + protected string dataType; + protected int forecastDay; + protected int hourOffset; + protected int updateInterval; + protected API api; + protected string units; + protected string timezone; + protected string language; + protected bool enableReverseGeocode; + + protected static DateTime lastGlobalApiCall = DateTime.MinValue; + protected static readonly object apiCallLock = new object(); + protected static volatile bool isUpdating = false; + protected const int MIN_API_CALL_INTERVAL = 5; + protected static int activeMeasures = 0; - internal void Reload(Rainmeter.API api, ref double maxValue) + internal Measure() + { + latitude = 0.0; + longitude = 0.0; + dataType = "CurrentTemp"; + forecastDay = 0; + hourOffset = 0; + updateInterval = 600; + units = "metric"; + timezone = "auto"; + language = "en"; + enableReverseGeocode = false; + + lock (apiCallLock) + { + activeMeasures++; + } + } + + ~Measure() + { + lock (apiCallLock) + { + activeMeasures--; + } + } + + internal virtual void Dispose() + { + } + + internal virtual void Reload(Rainmeter.API api, ref double maxValue) { this.api = api; @@ -139,15 +178,17 @@ internal void Reload(Rainmeter.API api, ref double maxValue) updateInterval = api.ReadInt("UpdateInterval", 600); units = api.ReadString("Units", "metric").ToLower(); timezone = api.ReadString("Timezone", "auto"); + language = api.ReadString("Language", "en").ToLower(); + enableReverseGeocode = api.ReadInt("EnableReverseGeocode", 0) == 1; if (latitude < -90 || latitude > 90 || longitude < -180 || longitude > 180) { - lastError = "Invalid coordinates"; + WeatherDataCache.lastError = "Invalid coordinates"; api.Log(API.LogType.Error, $"WeatherX: Invalid coordinates Lat={latitude}, Lon={longitude}"); } else { - lastError = ""; + WeatherDataCache.lastError = ""; } if (forecastDay < 0 || forecastDay > 6) @@ -165,25 +206,82 @@ internal void Reload(Rainmeter.API api, ref double maxValue) units = "metric"; } - lastUpdate = DateTime.MinValue; - initialUpdateScheduled = false; - isUpdating = false; - - updateTimer?.Dispose(); - api.Log(API.LogType.Debug, $"WeatherX: Initialized with Lat={latitude}, Lon={longitude}, DataType={dataType}, Units={units}, HourOffset={hourOffset}"); + + // Always force update on reload/refresh + lock (apiCallLock) + { + // Reset the last update time to trigger immediate update + WeatherDataCache.lastUpdate = DateTime.MinValue; + + // Reset geocode cache if coordinates changed or on first load + if (enableReverseGeocode) + { + bool coordsChanged = !WeatherDataCache.hasGeocodedBefore || + Math.Abs(WeatherDataCache.lastGeocodedLatitude - latitude) > 0.0001 || + Math.Abs(WeatherDataCache.lastGeocodedLongitude - longitude) > 0.0001; + + if (coordsChanged) + { + WeatherDataCache.lastGeocodeUpdate = DateTime.MinValue; + api.Log(API.LogType.Debug, $"WeatherX: Coordinates changed - will refresh reverse geocode data"); + } + } + } + api.Log(API.LogType.Debug, "WeatherX: Reload detected - forcing data refresh"); } - internal double Update() + internal virtual double Update() { - if (!initialUpdateScheduled && string.IsNullOrEmpty(lastError)) + // Check if an update is needed based on updateInterval + double timeSinceLastUpdate = DateTime.Now.Subtract(WeatherDataCache.lastUpdate).TotalSeconds; + bool needsUpdate = (WeatherDataCache.lastUpdate == DateTime.MinValue) || (timeSinceLastUpdate >= updateInterval); + + if (needsUpdate && !isUpdating) { - initialUpdateScheduled = true; - updateTimer = new Timer(async _ => await UpdateWeatherDataAsync(), null, 2000, Timeout.Infinite); + lock (apiCallLock) + { + // Double-check inside lock to prevent race conditions + timeSinceLastUpdate = DateTime.Now.Subtract(WeatherDataCache.lastUpdate).TotalSeconds; + needsUpdate = (WeatherDataCache.lastUpdate == DateTime.MinValue) || (timeSinceLastUpdate >= updateInterval); + + if (needsUpdate && !isUpdating) + { + double timeSinceLastGlobalCall = DateTime.Now.Subtract(lastGlobalApiCall).TotalSeconds; + + // Ensure minimum interval between API calls to prevent rate limiting + if (timeSinceLastGlobalCall >= MIN_API_CALL_INTERVAL) + { + isUpdating = true; + lastGlobalApiCall = DateTime.Now; + api?.Log(API.LogType.Debug, + $"WeatherX: Triggering update - Time since last: {timeSinceLastUpdate:F1}s, Interval: {updateInterval}s"); + Task.Run(async () => await UpdateWeatherDataAsync()); + + // Update reverse geocode if enabled and not updated yet + if (enableReverseGeocode && WeatherDataCache.lastGeocodeUpdate == DateTime.MinValue) + { + Task.Run(async () => await UpdateReverseGeocodeAsync()); + } + } + else + { + double waitTime = MIN_API_CALL_INTERVAL - timeSinceLastGlobalCall; + api?.Log(API.LogType.Debug, + $"WeatherX: Rate limit protection - wait {waitTime:F1}s more (last call: {timeSinceLastGlobalCall:F1}s ago, min: {MIN_API_CALL_INTERVAL}s)"); + } + } + } } - else if (DateTime.Now.Subtract(lastUpdate).TotalSeconds >= updateInterval && !isUpdating) + else if (!needsUpdate && WeatherDataCache.lastUpdate != DateTime.MinValue) { - Task.Run(async () => await UpdateWeatherDataAsync()); + // Log occasionally to show interval is working (every 60 seconds) + if (DateTime.Now.Second == 0) + { + double timeRemaining = updateInterval - timeSinceLastUpdate; + api?.Log(API.LogType.Debug, + $"WeatherX: Next update in {timeRemaining:F0}s (Interval: {updateInterval}s, Last update: {timeSinceLastUpdate:F0}s ago)"); + } } return GetNumericValue(); @@ -191,14 +289,10 @@ internal double Update() private async Task UpdateWeatherDataAsync() { - if (isUpdating) return; - - isUpdating = true; - try { - lastApiUrl = BuildApiUrl(); - api?.Log(API.LogType.Debug, $"WeatherX: Making API call to: {lastApiUrl}"); + WeatherDataCache.lastApiUrl = BuildApiUrl(); + api?.Log(API.LogType.Debug, $"WeatherX: Making API call to: {WeatherDataCache.lastApiUrl}"); System.Net.ServicePointManager.SecurityProtocol = System.Net.SecurityProtocolType.Tls12 | @@ -212,61 +306,104 @@ private async Task UpdateWeatherDataAsync() client.Encoding = Encoding.UTF8; client.Timeout = 15000; - string response = await client.DownloadStringTaskAsync(lastApiUrl); + string response = await client.DownloadStringTaskAsync(WeatherDataCache.lastApiUrl); ParseWeatherData(response); - lastError = ""; - lastUpdate = DateTime.Now; + WeatherDataCache.lastError = ""; + WeatherDataCache.lastUpdate = DateTime.Now; + + api?.Log(API.LogType.Debug, + $"WeatherX: Data updated successfully at {WeatherDataCache.lastUpdate:HH:mm:ss} | Next update in {updateInterval}s"); } } - catch (WebException ex) + catch (WebException wex) { - if (lastApiUrl.StartsWith("https://")) + if (wex.Response is HttpWebResponse response && response.StatusCode == (HttpStatusCode)429) { - try - { - api?.Log(API.LogType.Warning, $"WeatherX: HTTPS failed, trying HTTP fallback"); - string httpUrl = lastApiUrl.Replace("https://", "http://"); - - using (TimeoutWebClient client = new TimeoutWebClient()) - { - client.Headers.Add("User-Agent", "WeatherX-Rainmeter-Plugin/1.0"); - client.Headers.Add("Accept", "application/json"); - client.Encoding = Encoding.UTF8; - client.Timeout = 15000; - - string response = await client.DownloadStringTaskAsync(httpUrl); - ParseWeatherData(response); - - lastError = ""; - lastUpdate = DateTime.Now; - api?.Log(API.LogType.Debug, $"WeatherX: HTTP fallback successful. Temp: {currentTemp}°"); - return; - } - } - catch (Exception fallbackEx) - { - api?.Log(API.LogType.Error, $"WeatherX: HTTP fallback also failed: {fallbackEx.Message}"); - } + WeatherDataCache.lastError = "Rate limit exceeded - please wait"; + api?.Log(API.LogType.Warning, $"WeatherX: API rate limit hit (429). Waiting before next attempt."); + WeatherDataCache.lastUpdate = DateTime.Now; + } + else + { + WeatherDataCache.lastError = $"Error: {wex.Message}"; + api?.Log(API.LogType.Error, $"WeatherX: {WeatherDataCache.lastError}"); } - - lastError = $"Network Error: {ex.Message}"; - api?.Log(API.LogType.Error, $"WeatherX: {lastError}"); } catch (Exception ex) { - lastError = $"Error: {ex.Message}"; - api?.Log(API.LogType.Error, $"WeatherX: {lastError}"); + WeatherDataCache.lastError = $"Error: {ex.Message}"; + api?.Log(API.LogType.Error, $"WeatherX: {WeatherDataCache.lastError}"); } finally { isUpdating = false; + } + } + + private async Task UpdateReverseGeocodeAsync() + { + try + { + string geocodeUrl = $"https://api.bigdatacloud.net/data/reverse-geocode-client?" + + $"latitude={latitude.ToString(CultureInfo.InvariantCulture)}&" + + $"longitude={longitude.ToString(CultureInfo.InvariantCulture)}&" + + $"localityLanguage={language}"; - if (updateTimer != null) + api?.Log(API.LogType.Debug, $"WeatherX: Fetching reverse geocode from: {geocodeUrl}"); + + using (TimeoutWebClient client = new TimeoutWebClient()) { - updateTimer.Change(updateInterval * 1000, Timeout.Infinite); + client.Headers.Add("User-Agent", "WeatherX-Rainmeter-Plugin/1.0"); + client.Headers.Add("Accept", "application/json"); + client.Encoding = Encoding.UTF8; + client.Timeout = 10000; + + string response = await client.DownloadStringTaskAsync(geocodeUrl); + ParseReverseGeocodeData(response); + + // Store the coordinates we just geocoded + WeatherDataCache.lastGeocodedLatitude = latitude; + WeatherDataCache.lastGeocodedLongitude = longitude; + WeatherDataCache.hasGeocodedBefore = true; + WeatherDataCache.lastGeocodeUpdate = DateTime.Now; + api?.Log(API.LogType.Debug, + $"WeatherX: Reverse geocode updated - City: {WeatherDataCache.locationCity}, Country: {WeatherDataCache.locationCountryName}"); } } + catch (Exception ex) + { + api?.Log(API.LogType.Error, $"WeatherX: Reverse geocode error: {ex.Message}"); + } + } + + private void ParseReverseGeocodeData(string jsonResponse) + { + try + { + // Parse continent + WeatherDataCache.locationContinent = ParseHelper.ParseJsonStringValue(jsonResponse, "continent"); + WeatherDataCache.locationContinentCode = ParseHelper.ParseJsonStringValue(jsonResponse, "continentCode"); + + // Parse country + WeatherDataCache.locationCountryName = ParseHelper.ParseJsonStringValue(jsonResponse, "countryName"); + WeatherDataCache.locationCountryCode = ParseHelper.ParseJsonStringValue(jsonResponse, "countryCode"); + + // Parse subdivision (state/province) + WeatherDataCache.locationPrincipalSubdivision = ParseHelper.ParseJsonStringValue(jsonResponse, "principalSubdivision"); + WeatherDataCache.locationPrincipalSubdivisionCode = ParseHelper.ParseJsonStringValue(jsonResponse, "principalSubdivisionCode"); + + // Parse city + WeatherDataCache.locationCity = ParseHelper.ParseJsonStringValue(jsonResponse, "city"); + + api?.Log(API.LogType.Debug, + $"WeatherX: Parsed location - {WeatherDataCache.locationCity}, {WeatherDataCache.locationPrincipalSubdivision}, {WeatherDataCache.locationCountryName}"); + } + catch (Exception ex) + { + api?.Log(API.LogType.Error, $"WeatherX: Reverse geocode parsing error: {ex.Message}"); + throw; + } } private string BuildApiUrl() @@ -277,14 +414,11 @@ private string BuildApiUrl() return $"https://api.open-meteo.com/v1/forecast?" + $"latitude={latitude.ToString(CultureInfo.InvariantCulture)}&" + $"longitude={longitude.ToString(CultureInfo.InvariantCulture)}&" + - $"current=temperature_2m,relative_humidity_2m,weather_code,surface_pressure,wind_speed_10m," + $"apparent_temperature,dew_point_2m,wind_direction_10m,wind_gusts_10m,is_day&" + - $"hourly=temperature_2m,relative_humidity_2m,wind_speed_10m," + $"apparent_temperature,cloud_cover,visibility,weather_code," + $"shortwave_radiation,direct_radiation,diffuse_radiation&" + - $"daily=weather_code,temperature_2m_max,temperature_2m_min,apparent_temperature_max," + $"apparent_temperature_min,wind_speed_10m_max,uv_index_max," + $"sunrise,sunset,shortwave_radiation_sum&" + @@ -302,10 +436,9 @@ public void ParseWeatherData(string jsonResponse) ParseCurrentWeather(jsonResponse); ParseDailyForecasts(jsonResponse); ParseHourlyData(jsonResponse); - UpdateCurrentFromHourly(); - api?.Log(API.LogType.Debug, $"WeatherX: Comprehensive parsing complete - Temp: {currentTemp}°, Condition: {currentCondition}"); + api?.Log(API.LogType.Debug, $"WeatherX: Parsing complete - Temp: {WeatherDataCache.currentTemp}°, Condition: {WeatherDataCache.currentCondition}"); } catch (Exception ex) { @@ -316,34 +449,32 @@ public void ParseWeatherData(string jsonResponse) private void ParseCurrentWeather(string json) { - - currentTemp = ParseHelper.ParseJsonValue(json, "\"current\"", "\"temperature_2m\""); - currentHumidity = ParseHelper.ParseJsonValue(json, "\"current\"", "\"relative_humidity_2m\""); - currentWindSpeed = ParseHelper.ParseJsonValue(json, "\"current\"", "\"wind_speed_10m\""); - currentPressure = ParseHelper.ParseJsonValue(json, "\"current\"", "\"surface_pressure\""); - - currentApparentTemp = ParseHelper.ParseJsonValue(json, "\"current\"", "\"apparent_temperature\""); - currentDewPoint = ParseHelper.ParseJsonValue(json, "\"current\"", "\"dew_point_2m\""); - currentWindDirection = ParseHelper.ParseJsonValue(json, "\"current\"", "\"wind_direction_10m\""); - currentWindGusts = ParseHelper.ParseJsonValue(json, "\"current\"", "\"wind_gusts_10m\""); - - currentWeatherCode = ParseHelper.ParseJsonValue(json, "\"current\"", "\"weather_code\""); - currentIsDay = ParseHelper.ParseJsonValue(json, "\"current\"", "\"is_day\""); - - int weatherCode = (int)currentWeatherCode; - currentCondition = CommonHelper.GetWeatherDescription(weatherCode); + WeatherDataCache.currentTemp = ParseHelper.ParseJsonValue(json, "\"current\"", "\"temperature_2m\""); + WeatherDataCache.currentHumidity = ParseHelper.ParseJsonValue(json, "\"current\"", "\"relative_humidity_2m\""); + WeatherDataCache.currentWindSpeed = ParseHelper.ParseJsonValue(json, "\"current\"", "\"wind_speed_10m\""); + WeatherDataCache.currentPressure = ParseHelper.ParseJsonValue(json, "\"current\"", "\"surface_pressure\""); + WeatherDataCache.currentApparentTemp = ParseHelper.ParseJsonValue(json, "\"current\"", "\"apparent_temperature\""); + WeatherDataCache.currentDewPoint = ParseHelper.ParseJsonValue(json, "\"current\"", "\"dew_point_2m\""); + WeatherDataCache.currentWindDirection = ParseHelper.ParseJsonValue(json, "\"current\"", "\"wind_direction_10m\""); + WeatherDataCache.currentWindGusts = ParseHelper.ParseJsonValue(json, "\"current\"", "\"wind_gusts_10m\""); + WeatherDataCache.currentWeatherCode = ParseHelper.ParseJsonValue(json, "\"current\"", "\"weather_code\""); + WeatherDataCache.currentIsDay = ParseHelper.ParseJsonValue(json, "\"current\"", "\"is_day\""); + + int weatherCode = (int)WeatherDataCache.currentWeatherCode; + WeatherDataCache.currentCondition = CommonHelper.GetWeatherDescription(weatherCode); + + api?.Log(API.LogType.Debug, $"WeatherX: Current parsed - Temp={WeatherDataCache.currentTemp}, Humidity={WeatherDataCache.currentHumidity}"); } private void UpdateCurrentFromHourly() { - int currentHourIndex = DateTime.Now.Hour; if (currentHourIndex < 48) { - currentCloudCover = hourlyCloudCover[currentHourIndex]; - currentSolarRadiation = hourlySolarRadiation[currentHourIndex]; - currentDirectRadiation = hourlyDirectRadiation[currentHourIndex]; - currentDiffuseRadiation = hourlyDiffuseRadiation[currentHourIndex]; + WeatherDataCache.currentCloudCover = WeatherDataCache.hourlyCloudCover[currentHourIndex]; + WeatherDataCache.currentSolarRadiation = WeatherDataCache.hourlySolarRadiation[currentHourIndex]; + WeatherDataCache.currentDirectRadiation = WeatherDataCache.hourlyDirectRadiation[currentHourIndex]; + WeatherDataCache.currentDiffuseRadiation = WeatherDataCache.hourlyDiffuseRadiation[currentHourIndex]; } } @@ -360,29 +491,29 @@ private void ParseDailyForecasts(string json) string dailySection = ParseHelper.ExtractDailySection(json, dailyStart); - ParseHelper.ParseArrayValuesInSection(dailySection, "\"temperature_2m_max\"", forecastTempMax); - ParseHelper.ParseArrayValuesInSection(dailySection, "\"temperature_2m_min\"", forecastTempMin); - ParseHelper.ParseArrayValuesInSection(dailySection, "\"apparent_temperature_max\"", forecastApparentTempMax); - ParseHelper.ParseArrayValuesInSection(dailySection, "\"apparent_temperature_min\"", forecastApparentTempMin); - ParseHelper.ParseArrayValuesInSection(dailySection, "\"wind_speed_10m_max\"", forecastWindSpeedMax); - ParseHelper.ParseArrayValuesInSection(dailySection, "\"uv_index_max\"", forecastUvIndexMax); + ParseHelper.ParseArrayValuesInSection(dailySection, "\"temperature_2m_max\"", WeatherDataCache.forecastTempMax); + ParseHelper.ParseArrayValuesInSection(dailySection, "\"temperature_2m_min\"", WeatherDataCache.forecastTempMin); + ParseHelper.ParseArrayValuesInSection(dailySection, "\"apparent_temperature_max\"", WeatherDataCache.forecastApparentTempMax); + ParseHelper.ParseArrayValuesInSection(dailySection, "\"apparent_temperature_min\"", WeatherDataCache.forecastApparentTempMin); + ParseHelper.ParseArrayValuesInSection(dailySection, "\"wind_speed_10m_max\"", WeatherDataCache.forecastWindSpeedMax); + ParseHelper.ParseArrayValuesInSection(dailySection, "\"uv_index_max\"", WeatherDataCache.forecastUvIndexMax); - ParseHelper.ParseStringArrayInSection(dailySection, "\"sunrise\"", forecastSunrise); - ParseHelper.ParseStringArrayInSection(dailySection, "\"sunset\"", forecastSunset); + ParseHelper.ParseStringArrayInSection(dailySection, "\"sunrise\"", WeatherDataCache.forecastSunrise); + ParseHelper.ParseStringArrayInSection(dailySection, "\"sunset\"", WeatherDataCache.forecastSunset); - ParseHelper.ParseArrayValuesInSection(dailySection, "\"weather_code\"", forecastWeatherCode); + ParseHelper.ParseArrayValuesInSection(dailySection, "\"weather_code\"", WeatherDataCache.forecastWeatherCode); for (int i = 0; i < 7; i++) { - forecastConditions[i] = CommonHelper.GetWeatherDescription((int)forecastWeatherCode[i]); + WeatherDataCache.forecastConditions[i] = CommonHelper.GetWeatherDescription((int)WeatherDataCache.forecastWeatherCode[i]); } - if (forecastUvIndexMax.Length > 0) + if (WeatherDataCache.forecastUvIndexMax.Length > 0) { - todayUvIndex = forecastUvIndexMax[0]; + WeatherDataCache.todayUvIndex = WeatherDataCache.forecastUvIndexMax[0]; } - api?.Log(API.LogType.Debug, $"WeatherX: Daily forecasts parsed - Day 0: Max={forecastTempMax[0]}°, UV={forecastUvIndexMax[0]}"); + api?.Log(API.LogType.Debug, $"WeatherX: Daily forecasts parsed - Day 0: Max={WeatherDataCache.forecastTempMax[0]}°"); } catch (Exception ex) { @@ -399,28 +530,26 @@ private void ParseHourlyData(string json) string hourlySection = ParseHelper.ExtractHourlySection(json, hourlyStart); - ParseHelper.ParseArrayValuesInSection(hourlySection, "\"temperature_2m\"", hourlyTemp, 48); - ParseHelper.ParseArrayValuesInSection(hourlySection, "\"relative_humidity_2m\"", hourlyHumidity, 48); - ParseHelper.ParseArrayValuesInSection(hourlySection, "\"wind_speed_10m\"", hourlyWindSpeed, 48); - ParseHelper.ParseArrayValuesInSection(hourlySection, "\"apparent_temperature\"", hourlyApparentTemp, 48); - ParseHelper.ParseArrayValuesInSection(hourlySection, "\"cloud_cover\"", hourlyCloudCover, 48); - ParseHelper.ParseArrayValuesInSection(hourlySection, "\"visibility\"", hourlyVisibility, 48); - - ParseHelper.ParseArrayValuesInSection(hourlySection, "\"shortwave_radiation\"", hourlySolarRadiation, 48); - ParseHelper.ParseArrayValuesInSection(hourlySection, "\"direct_radiation\"", hourlyDirectRadiation, 48); - ParseHelper.ParseArrayValuesInSection(hourlySection, "\"diffuse_radiation\"", hourlyDiffuseRadiation, 48); + ParseHelper.ParseArrayValuesInSection(hourlySection, "\"temperature_2m\"", WeatherDataCache.hourlyTemp, 48); + ParseHelper.ParseArrayValuesInSection(hourlySection, "\"relative_humidity_2m\"", WeatherDataCache.hourlyHumidity, 48); + ParseHelper.ParseArrayValuesInSection(hourlySection, "\"wind_speed_10m\"", WeatherDataCache.hourlyWindSpeed, 48); + ParseHelper.ParseArrayValuesInSection(hourlySection, "\"apparent_temperature\"", WeatherDataCache.hourlyApparentTemp, 48); + ParseHelper.ParseArrayValuesInSection(hourlySection, "\"cloud_cover\"", WeatherDataCache.hourlyCloudCover, 48); + ParseHelper.ParseArrayValuesInSection(hourlySection, "\"visibility\"", WeatherDataCache.hourlyVisibility, 48); + ParseHelper.ParseArrayValuesInSection(hourlySection, "\"shortwave_radiation\"", WeatherDataCache.hourlySolarRadiation, 48); + ParseHelper.ParseArrayValuesInSection(hourlySection, "\"direct_radiation\"", WeatherDataCache.hourlyDirectRadiation, 48); + ParseHelper.ParseArrayValuesInSection(hourlySection, "\"diffuse_radiation\"", WeatherDataCache.hourlyDiffuseRadiation, 48); double[] weatherCodes = new double[48]; ParseHelper.ParseArrayValuesInSection(hourlySection, "\"weather_code\"", weatherCodes, 48); for (int i = 0; i < 48; i++) { - hourlyWeatherCode[i] = (int)weatherCodes[i]; + WeatherDataCache.hourlyWeatherCode[i] = (int)weatherCodes[i]; } - ParseHelper.ParseTimeArray(hourlySection, hourlyTime); - - api?.Log(API.LogType.Debug, $"WeatherX: Hourly data parsed successfully (no precipitation)"); + ParseHelper.ParseTimeArray(hourlySection, WeatherDataCache.hourlyTime); + api?.Log(API.LogType.Debug, $"WeatherX: Hourly data parsed successfully"); } catch (Exception ex) { @@ -428,151 +557,180 @@ private void ParseHourlyData(string json) } } - private double GetNumericValue() + protected double GetNumericValue() { int targetHour = CommonHelper.GetTargetHourIndex(hourOffset); switch (dataType.ToLower()) { - case "currenttemp": - return currentTemp; + return WeatherDataCache.currentTemp; case "currenthumidity": - return currentHumidity; + return WeatherDataCache.currentHumidity; case "currentwindspeed": - return currentWindSpeed; + return WeatherDataCache.currentWindSpeed; case "currentpressure": - return currentPressure; + return WeatherDataCache.currentPressure; case "currentapparenttemp": - return currentApparentTemp; + return WeatherDataCache.currentApparentTemp; case "currentdewpoint": - return currentDewPoint; + return WeatherDataCache.currentDewPoint; case "currentcloudcover": - return currentCloudCover; + return WeatherDataCache.currentCloudCover; case "currentwinddirection": - return currentWindDirection; + return WeatherDataCache.currentWindDirection; case "currentwindgusts": - return currentWindGusts; + return WeatherDataCache.currentWindGusts; case "currentuvindex": - return todayUvIndex; + return WeatherDataCache.todayUvIndex; case "currentisday": - return currentIsDay; + return WeatherDataCache.currentIsDay; case "currentweathercode": - return currentWeatherCode; + return WeatherDataCache.currentWeatherCode; case "currentsolarradiation": - return currentSolarRadiation; + return WeatherDataCache.currentSolarRadiation; case "currentdirectradiation": - return currentDirectRadiation; + return WeatherDataCache.currentDirectRadiation; case "currentdiffuseradiation": - return currentDiffuseRadiation; + return WeatherDataCache.currentDiffuseRadiation; case "forecasttemp": case "forecasttempmax": - return forecastDay < forecastTempMax.Length ? forecastTempMax[forecastDay] : 0.0; + return forecastDay < WeatherDataCache.forecastTempMax.Length ? WeatherDataCache.forecastTempMax[forecastDay] : 0.0; case "forecasttempmin": - return forecastDay < forecastTempMin.Length ? forecastTempMin[forecastDay] : 0.0; + return forecastDay < WeatherDataCache.forecastTempMin.Length ? WeatherDataCache.forecastTempMin[forecastDay] : 0.0; case "forecastapparenttempmax": - return forecastDay < forecastApparentTempMax.Length ? forecastApparentTempMax[forecastDay] : 0.0; + return forecastDay < WeatherDataCache.forecastApparentTempMax.Length ? WeatherDataCache.forecastApparentTempMax[forecastDay] : 0.0; case "forecastapparenttempmin": - return forecastDay < forecastApparentTempMin.Length ? forecastApparentTempMin[forecastDay] : 0.0; + return forecastDay < WeatherDataCache.forecastApparentTempMin.Length ? WeatherDataCache.forecastApparentTempMin[forecastDay] : 0.0; case "forecastwindspeed": - return forecastDay < forecastWindSpeedMax.Length ? forecastWindSpeedMax[forecastDay] : 0.0; + return forecastDay < WeatherDataCache.forecastWindSpeedMax.Length ? WeatherDataCache.forecastWindSpeedMax[forecastDay] : 0.0; case "forecastuvindex": - return forecastDay < forecastUvIndexMax.Length ? forecastUvIndexMax[forecastDay] : 0.0; + return forecastDay < WeatherDataCache.forecastUvIndexMax.Length ? WeatherDataCache.forecastUvIndexMax[forecastDay] : 0.0; case "forecastweathercode": - return forecastDay < forecastWeatherCode.Length ? forecastWeatherCode[forecastDay] : 0.0; + return forecastDay < WeatherDataCache.forecastWeatherCode.Length ? WeatherDataCache.forecastWeatherCode[forecastDay] : 0.0; case "forecastsunrise": - return forecastDay < forecastSunrise.Length ? CommonHelper.ConvertIso8601ToHour(forecastSunrise[forecastDay]) : 0.0; + return forecastDay < WeatherDataCache.forecastSunrise.Length ? CommonHelper.ConvertIso8601ToHour(WeatherDataCache.forecastSunrise[forecastDay]) : 0.0; case "forecastsunset": - return forecastDay < forecastSunset.Length ? CommonHelper.ConvertIso8601ToHour(forecastSunset[forecastDay]) : 0.0; + return forecastDay < WeatherDataCache.forecastSunset.Length ? CommonHelper.ConvertIso8601ToHour(WeatherDataCache.forecastSunset[forecastDay]) : 0.0; case "hourlytemp": - return targetHour < hourlyTemp.Length ? hourlyTemp[targetHour] : 0.0; + return targetHour < WeatherDataCache.hourlyTemp.Length ? WeatherDataCache.hourlyTemp[targetHour] : 0.0; case "hourlyhumidity": - return targetHour < hourlyHumidity.Length ? hourlyHumidity[targetHour] : 0.0; + return targetHour < WeatherDataCache.hourlyHumidity.Length ? WeatherDataCache.hourlyHumidity[targetHour] : 0.0; case "hourlywindspeed": - return targetHour < hourlyWindSpeed.Length ? hourlyWindSpeed[targetHour] : 0.0; + return targetHour < WeatherDataCache.hourlyWindSpeed.Length ? WeatherDataCache.hourlyWindSpeed[targetHour] : 0.0; case "hourlyapparenttemp": - return targetHour < hourlyApparentTemp.Length ? hourlyApparentTemp[targetHour] : 0.0; + return targetHour < WeatherDataCache.hourlyApparentTemp.Length ? WeatherDataCache.hourlyApparentTemp[targetHour] : 0.0; case "hourlycloudcover": - return targetHour < hourlyCloudCover.Length ? hourlyCloudCover[targetHour] : 0.0; + return targetHour < WeatherDataCache.hourlyCloudCover.Length ? WeatherDataCache.hourlyCloudCover[targetHour] : 0.0; case "hourlyvisibility": - return targetHour < hourlyVisibility.Length ? hourlyVisibility[targetHour] : 0.0; + return targetHour < WeatherDataCache.hourlyVisibility.Length ? WeatherDataCache.hourlyVisibility[targetHour] : 0.0; case "hourlyweathercode": - return targetHour < hourlyWeatherCode.Length ? hourlyWeatherCode[targetHour] : 0.0; + return targetHour < WeatherDataCache.hourlyWeatherCode.Length ? WeatherDataCache.hourlyWeatherCode[targetHour] : 0.0; case "hourlysolarradiation": - return targetHour < hourlySolarRadiation.Length ? hourlySolarRadiation[targetHour] : 0.0; + return targetHour < WeatherDataCache.hourlySolarRadiation.Length ? WeatherDataCache.hourlySolarRadiation[targetHour] : 0.0; case "hourlydirectradiation": - return targetHour < hourlyDirectRadiation.Length ? hourlyDirectRadiation[targetHour] : 0.0; + return targetHour < WeatherDataCache.hourlyDirectRadiation.Length ? WeatherDataCache.hourlyDirectRadiation[targetHour] : 0.0; case "hourlydiffuseradiation": - return targetHour < hourlyDiffuseRadiation.Length ? hourlyDiffuseRadiation[targetHour] : 0.0; + return targetHour < WeatherDataCache.hourlyDiffuseRadiation.Length ? WeatherDataCache.hourlyDiffuseRadiation[targetHour] : 0.0; case "currenthourtemp": - return hourlyTemp[DateTime.Now.Hour % 48]; + return WeatherDataCache.hourlyTemp[DateTime.Now.Hour % 48]; case "currenthourhumidity": - return hourlyHumidity[DateTime.Now.Hour % 48]; + return WeatherDataCache.hourlyHumidity[DateTime.Now.Hour % 48]; case "currenthourwindspeed": - return hourlyWindSpeed[DateTime.Now.Hour % 48]; + return WeatherDataCache.hourlyWindSpeed[DateTime.Now.Hour % 48]; default: return 0.0; } } - internal string GetStringValue() + internal virtual string GetStringValue() { int targetHour = CommonHelper.GetTargetHourIndex(hourOffset); switch (dataType.ToLower()) { case "currentcondition": - return currentCondition; + return WeatherDataCache.currentCondition; case "currentweathercodetext": - return currentWeatherCode.ToString("F0"); + return WeatherDataCache.currentWeatherCode.ToString("F0"); case "currentisdaytext": - return currentIsDay > 0 ? "Day" : "Night"; + return WeatherDataCache.currentIsDay > 0 ? "Day" : "Night"; case "forecastcondition": - return forecastDay < forecastConditions.Length ? forecastConditions[forecastDay] : "Unknown"; + return forecastDay < WeatherDataCache.forecastConditions.Length ? WeatherDataCache.forecastConditions[forecastDay] : "Unknown"; case "forecastweathercodetext": - return forecastDay < forecastWeatherCode.Length ? forecastWeatherCode[forecastDay].ToString("F0") : "0"; + return forecastDay < WeatherDataCache.forecastWeatherCode.Length ? WeatherDataCache.forecastWeatherCode[forecastDay].ToString("F0") : "0"; case "hourlycondition": - return targetHour < hourlyWeatherCode.Length ? CommonHelper.GetWeatherDescription(hourlyWeatherCode[targetHour]) : "Unknown"; + return targetHour < WeatherDataCache.hourlyWeatherCode.Length ? CommonHelper.GetWeatherDescription(WeatherDataCache.hourlyWeatherCode[targetHour]) : "Unknown"; case "hourlyweathercodetext": - return targetHour < hourlyWeatherCode.Length ? hourlyWeatherCode[targetHour].ToString("F0") : "0"; + return targetHour < WeatherDataCache.hourlyWeatherCode.Length ? WeatherDataCache.hourlyWeatherCode[targetHour].ToString("F0") : "0"; case "currentwinddirectiontext": - return CommonHelper.GetWindDirectionText(currentWindDirection); + return CommonHelper.GetWindDirectionText(WeatherDataCache.currentWindDirection); case "debugerror": - return string.IsNullOrEmpty(lastError) ? "No Error" : lastError; + return string.IsNullOrEmpty(WeatherDataCache.lastError) ? "No Error" : WeatherDataCache.lastError; case "debugurl": - return lastApiUrl; + return WeatherDataCache.lastApiUrl; case "debugdaily": - return $"Day{forecastDay}: Max={forecastTempMax[forecastDay]:F1}°, Min={forecastTempMin[forecastDay]:F1}°, {forecastConditions[forecastDay]}"; + return $"Day{forecastDay}: Max={WeatherDataCache.forecastTempMax[forecastDay]:F1}°, Min={WeatherDataCache.forecastTempMin[forecastDay]:F1}°, {WeatherDataCache.forecastConditions[forecastDay]}"; case "debughourly": - return $"Hour+{hourOffset}: Temp={hourlyTemp[targetHour]:F1}°, {CommonHelper.GetWeatherDescription(hourlyWeatherCode[targetHour])}"; + return $"Hour+{hourOffset}: Temp={WeatherDataCache.hourlyTemp[targetHour]:F1}°, {CommonHelper.GetWeatherDescription(WeatherDataCache.hourlyWeatherCode[targetHour])}"; + case "debuglastupdate": + return WeatherDataCache.lastUpdate == DateTime.MinValue ? "Never" : WeatherDataCache.lastUpdate.ToString("HH:mm:ss"); + case "debugnextupdate": + if (WeatherDataCache.lastUpdate == DateTime.MinValue) + return "On next cycle"; + double timeRemaining = updateInterval - DateTime.Now.Subtract(WeatherDataCache.lastUpdate).TotalSeconds; + return timeRemaining > 0 ? $"In {timeRemaining:F0}s" : "Now"; + case "debugupdateinterval": + return $"{updateInterval}s"; case "status": - return isUpdating ? "Updating..." : (lastError.Length > 0 ? "Error" : "Ready"); + return isUpdating ? "Updating..." : (WeatherDataCache.lastError.Length > 0 ? "Error" : "Ready"); case "forecastsunrisetext": - return forecastDay < forecastSunrise.Length ? CommonHelper.ConvertIso8601ToTime(forecastSunrise[forecastDay]) : "N/A"; + return forecastDay < WeatherDataCache.forecastSunrise.Length ? CommonHelper.ConvertIso8601ToTime(WeatherDataCache.forecastSunrise[forecastDay]) : "N/A"; case "forecastsunsettext": - return forecastDay < forecastSunset.Length ? CommonHelper.ConvertIso8601ToTime(forecastSunset[forecastDay]) : "N/A"; + return forecastDay < WeatherDataCache.forecastSunset.Length ? CommonHelper.ConvertIso8601ToTime(WeatherDataCache.forecastSunset[forecastDay]) : "N/A"; case "hourlytime": - return targetHour < hourlyTime.Length ? hourlyTime[targetHour] : ""; + return targetHour < WeatherDataCache.hourlyTime.Length ? WeatherDataCache.hourlyTime[targetHour] : ""; case "currenthourlytime": { int currentHourIndex = DateTime.Now.Hour % 48; - return currentHourIndex < hourlyTime.Length ? hourlyTime[currentHourIndex] : ""; + return currentHourIndex < WeatherDataCache.hourlyTime.Length ? WeatherDataCache.hourlyTime[currentHourIndex] : ""; } case "uvindextext": - return CommonHelper.GetUvIndexDescription(todayUvIndex); + return CommonHelper.GetUvIndexDescription(WeatherDataCache.todayUvIndex); case "nexthourssummary": return GetNextHoursSummary(); case "debugsolarradiation": - return $"Solar: {currentSolarRadiation:F1} W/m² | Direct: {currentDirectRadiation:F1} | Diffuse: {currentDiffuseRadiation:F1}"; + return $"Solar: {WeatherDataCache.currentSolarRadiation:F1} W/m² | Direct: {WeatherDataCache.currentDirectRadiation:F1} | Diffuse: {WeatherDataCache.currentDiffuseRadiation:F1}"; case "debugcloudcover": - return $"Current Clouds: {currentCloudCover:F0}% | Next hour: {(targetHour < hourlyCloudCover.Length ? hourlyCloudCover[targetHour].ToString("F0") + "%" : "N/A")}"; + return $"Current Clouds: {WeatherDataCache.currentCloudCover:F0}% | Next hour: {(targetHour < WeatherDataCache.hourlyCloudCover.Length ? WeatherDataCache.hourlyCloudCover[targetHour].ToString("F0") + "%" : "N/A")}"; + + // Reverse geocoding data types + case "locationcontinent": + return WeatherDataCache.locationContinent; + case "locationcontinentcode": + return WeatherDataCache.locationContinentCode; + case "locationcountryname": + case "locationcountry": + return WeatherDataCache.locationCountryName; + case "locationcountrycode": + return WeatherDataCache.locationCountryCode; + case "locationprincipalsubdivision": + case "locationstate": + return WeatherDataCache.locationPrincipalSubdivision; + case "locationprincipalsubdivisioncode": + case "locationstatecode": + return WeatherDataCache.locationPrincipalSubdivisionCode; + case "locationcity": + return WeatherDataCache.locationCity; + case "locationfull": + return BuildFullLocationString(); default: double value = GetNumericValue(); @@ -580,7 +738,7 @@ internal string GetStringValue() } } - private string GetNextHoursSummary() + protected string GetNextHoursSummary() { var summary = new StringBuilder(); int currentHour = DateTime.Now.Hour; @@ -588,17 +746,17 @@ private string GetNextHoursSummary() for (int i = 1; i <= Math.Min(6, 47 - currentHour); i++) { int hourIndex = currentHour + i; - if (hourIndex < hourlyTemp.Length) + if (hourIndex < WeatherDataCache.hourlyTemp.Length) { - string time = hourIndex < hourlyTime.Length ? - CommonHelper.ConvertIso8601ToTime(hourlyTime[hourIndex]) : + string time = hourIndex < WeatherDataCache.hourlyTime.Length ? + CommonHelper.ConvertIso8601ToTime(WeatherDataCache.hourlyTime[hourIndex]) : DateTime.Now.AddHours(i).ToString("HH:mm"); - summary.Append($"{time}: {hourlyTemp[hourIndex]:F0}°"); + summary.Append($"{time}: {WeatherDataCache.hourlyTemp[hourIndex]:F0}°"); - if (hourIndex < hourlyWeatherCode.Length) + if (hourIndex < WeatherDataCache.hourlyWeatherCode.Length) { - string condition = CommonHelper.GetWeatherDescription(hourlyWeatherCode[hourIndex]); + string condition = CommonHelper.GetWeatherDescription(WeatherDataCache.hourlyWeatherCode[hourIndex]); if (!condition.Equals("Clear Sky", StringComparison.OrdinalIgnoreCase)) { summary.Append($" ({condition})"); @@ -615,8 +773,168 @@ private string GetNextHoursSummary() return summary.ToString(); } - ~Measure() + private string BuildFullLocationString() + { + var parts = new System.Collections.Generic.List(); + + if (!string.IsNullOrEmpty(WeatherDataCache.locationCity)) + parts.Add(WeatherDataCache.locationCity); + + if (!string.IsNullOrEmpty(WeatherDataCache.locationPrincipalSubdivision)) + parts.Add(WeatherDataCache.locationPrincipalSubdivision); + + if (!string.IsNullOrEmpty(WeatherDataCache.locationCountryName)) + parts.Add(WeatherDataCache.locationCountryName); + + return parts.Count > 0 ? string.Join(", ", parts) : "Unknown Location"; + } +} + +// Parent Measure - handles API calls and data fetching +internal class ParentMeasure : Measure +{ + // Static list to track all parent measures + internal static System.Collections.Generic.List ParentMeasures = new System.Collections.Generic.List(); + + internal string Name; + internal IntPtr Skin; + + internal ParentMeasure() + { + ParentMeasures.Add(this); + } + + internal override void Dispose() + { + ParentMeasures.Remove(this); + } + + internal override void Reload(Rainmeter.API api, ref double maxValue) + { + base.Reload(api, ref maxValue); + + Name = api.GetMeasureName(); + Skin = api.GetSkin(); + + api.Log(API.LogType.Debug, $"WeatherX Parent: '{Name}' initialized in skin {Skin}"); + } + + internal override double Update() + { + return base.Update(); + } + + internal override string GetStringValue() + { + return base.GetStringValue(); + } + + // Method for child measures to get values + internal double GetValue(string type, int day, int hour) + { + string oldDataType = dataType; + int oldForecastDay = forecastDay; + int oldHourOffset = hourOffset; + + dataType = type; + forecastDay = day; + hourOffset = hour; + + double result = GetNumericValue(); + + dataType = oldDataType; + forecastDay = oldForecastDay; + hourOffset = oldHourOffset; + + return result; + } + + internal string GetStringValueFor(string type, int day, int hour) + { + string oldDataType = dataType; + int oldForecastDay = forecastDay; + int oldHourOffset = hourOffset; + + dataType = type; + forecastDay = day; + hourOffset = hour; + + string result = base.GetStringValue(); + + dataType = oldDataType; + forecastDay = oldForecastDay; + hourOffset = oldHourOffset; + + return result; + } +} + +// Child Measure - retrieves data from parent +internal class ChildMeasure : Measure +{ + private ParentMeasure parentMeasure = null; + + internal override void Dispose() + { + base.Dispose(); + } + + internal override void Reload(Rainmeter.API api, ref double maxValue) + { + this.api = api; + + string parentName = api.ReadString("ParentName", ""); + IntPtr skin = api.GetSkin(); + + dataType = api.ReadString("DataType", "CurrentTemp"); + forecastDay = api.ReadInt("ForecastDay", 0); + hourOffset = api.ReadInt("HourOffset", 0); + + if (forecastDay < 0 || forecastDay > 6) + { + forecastDay = 0; + } + + if (hourOffset < 0 || hourOffset > 47) + { + hourOffset = 0; + } + + // Find parent using name AND skin handle + parentMeasure = null; + foreach (ParentMeasure parent in ParentMeasure.ParentMeasures) + { + if (parent.Skin.Equals(skin) && parent.Name.Equals(parentName)) + { + parentMeasure = parent; + api.Log(API.LogType.Debug, $"WeatherX Child: Found parent '{parentName}' for DataType='{dataType}'"); + break; + } + } + + if (parentMeasure == null) + { + api.Log(API.LogType.Error, $"WeatherX Child: ParentName='{parentName}' not found"); + } + } + + internal override double Update() + { + if (parentMeasure != null) + { + return parentMeasure.GetValue(dataType, forecastDay, hourOffset); + } + + return 0.0; + } + + internal override string GetStringValue() { - updateTimer?.Dispose(); + if (parentMeasure != null) + { + return parentMeasure.GetStringValueFor(dataType, forecastDay, hourOffset); + } + + return "No Parent"; } } \ No newline at end of file