From 1dcd553ada0c465879e44d70d22ee9b0e4fd4c94 Mon Sep 17 00:00:00 2001 From: nesquikm Date: Tue, 23 Sep 2025 16:08:31 +0400 Subject: [PATCH] Migrate from Weatherbit to Open-Meteo API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace Weatherbit integration with Open-Meteo free weather API - Add comprehensive WMO weather code to icon mapping (92 conditions) - Update configuration format (lat/lon instead of postal code) - Remove API key requirement - Open-Meteo is completely free - Update README with Open-Meteo setup instructions - Add weather descriptions for all WMO weather codes - Improve error handling for API responses - Update example.config.json for new API format Benefits: ✅ No API key required ✅ Unlimited calls for non-commercial use ✅ Better reliability (no auth failures) ✅ Global coverage with high-resolution models ✅ Free forever --- README.md | 42 +++++---- WeatherPiTFT.py | 214 ++++++++++++++++++++++++++++++++++++++------ example.config.json | 13 ++- 3 files changed, 219 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 7f530c7..7628201 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # WeatherPi_TFT +> **🆕 NEW: Now powered by Open-Meteo!** +> WeatherPi_TFT has been migrated from Weatherbit to **[Open-Meteo](https://open-meteo.com/)** - a free, open-source weather API. +> ✅ **No API key required** | ✅ **Unlimited calls** | ✅ **Global coverage** | ✅ **Free forever** + Working on @pimoroni HyperPixel4 Display and a Raspberry Pi 3 B+ ![Hardware](https://user-images.githubusercontent.com/753562/85412425-d9e3cd00-b569-11ea-9a87-57c09a9328d0.jpg) @@ -216,14 +220,15 @@ sudo mount -a sudo reboot ``` -while your Pi reboots grep your API key ... +while your Pi reboots, let's configure the weather API... -### get an api key from weatherbit.io +### Open-Meteo Weather API (No API key needed!) -* go to [weatherbit.io](https://www.weatherbit.io/) -* and register to get an API key +* WeatherPi_TFT now uses **[Open-Meteo](https://open-meteo.com/)** - a free, open-source weather API +* No registration or API key required - completely free for non-commercial use +* Global weather coverage with high-resolution forecasts -### add API key and other options to the config file +### add location and other options to the config file when your Pi has rebooted @@ -266,14 +271,14 @@ otherwise set it to `false` * `SHOW_API_STATS` show how many API calls are left over (resets every midnight UTC) * `MOUSE` enable/disable mouse pointer - needed for local development, better leave it disabled -#### configure weatherbit.io settings +#### configure Open-Meteo settings -* replace `xxxxxxxxxxxxxxxxxxxxxxxxx` in `"WEATHERBIT_IO_KEY": "xxxxxxxxxxxxxxxxxxxxxxxxx"` with your own API key -* replace `en` in `"WEATHERBIT_LANGUAGE": "en"` with your preferred language -* replace `de` in `"WEATHERBIT_COUNTRY": "de"` with your country -* replace `10178` in `"WEATHERBIT_POSTALCODE": 10178` with your zip code / postal code (this example-location zip code -is from berlin city, germany) -* for language-support, units, etc please refer to -> **[weatherbit API Docs](https://www.weatherbit.io/api)** +* set `"OPENMETEO_LAT": 52.5200` to your location's latitude (example: Berlin, Germany) +* set `"OPENMETEO_LON": 13.4050` to your location's longitude +* optionally change `"OPENMETEO_LANGUAGE": "en"` to your preferred language +* set `"OPENMETEO_DAYS": 4` for the number of forecast days (max 16) +* **No API key needed!** Open-Meteo is completely free +* for detailed API documentation refer to -> **[Open-Meteo API Docs](https://open-meteo.com/en/docs)** #### localise hardcoded strings and ISO settings ``` @@ -287,9 +292,9 @@ is from berlin city, germany) ``` * change `"ISO"` and `"METRIC"` according to your needs * `"METRIC": true` will get all weather data units as metric system, change to `false` if you prefer imperial styled units - * `°C` will be `°F` and `km/h` will be `mph - * will also change the request parameter `untis` for the api request - (see [weatherbit API Docs](https://www.weatherbit.io/api) for more details) + * `°C` will be `°F` and `km/h` will be `mph` + * will also change the units parameter for the API request + * Open-Meteo supports both metric and imperial units #### timer options ``` @@ -298,8 +303,8 @@ is from berlin city, germany) "RELOAD": 30 }, ``` -* the `UPDATE` timer defines how often the API will be called in seconds - 7min will give you enough API calls over the day -* `RELOAD` defines who often the information on the display will be updated +* the `UPDATE` timer defines how often the API will be called in seconds - with Open-Meteo you can set this much lower if needed (no rate limits!) +* `RELOAD` defines how often the information on the display will be updated ### theme file and theme options set your theme file [darcula.theme, light.theme or example.theme] in `config.json` @@ -412,7 +417,8 @@ sudo update-rc.d PiButtons defaults * [squix78](https://github.com/squix78) for his [esp8266 weather station color](https://github.com/squix78/esp8266-weather-station-color) which inspired me to make it in python for a raspberry and another weather api * [adafruit](https://github.com/adafruit) for [hardware](https://www.adafruit.com/) and [tutorials](https://learn.adafruit.com/) -* [weatherbit.io](https://www.weatherbit.io/) for weather api and [documentation](https://www.weatherbit.io/api) +* [Open-Meteo](https://open-meteo.com/) for the free, open-source weather API and excellent [documentation](https://open-meteo.com/en/docs) +* [weatherbit.io](https://www.weatherbit.io/) for the original weather API (now migrated to Open-Meteo) * weather icons: [@erikflowers](https://github.com/erikflowers) [weather-icons](https://github.com/erikflowers/weather-icons), making them colorful was my work * statusbar icons: [google](https://github.com/google) [material-design-icons](https://github.com/google/material-design-icons) * default font: [google - roboto](https://fonts.google.com/) diff --git a/WeatherPiTFT.py b/WeatherPiTFT.py index 4d2dc7f..132dd98 100755 --- a/WeatherPiTFT.py +++ b/WeatherPiTFT.py @@ -72,13 +72,13 @@ theme_settings = open(PATH + theme_config).read() theme = json.loads(theme_settings) -SERVER = config['WEATHERBIT_URL'] +SERVER = config['OPENMETEO_URL'] HEADERS = {} -WEATHERBIT_COUNTRY = config['WEATHERBIT_COUNTRY'] -WEATHERBIT_LANG = config['WEATHERBIT_LANGUAGE'] -WEATHERBIT_POSTALCODE = config['WEATHERBIT_POSTALCODE'] -WEATHERBIT_HOURS = config['WEATHERBIT_HOURS'] -WEATHERBIT_DAYS = config['WEATHERBIT_DAYS'] +OPENMETEO_LANG = config['OPENMETEO_LANGUAGE'] +OPENMETEO_LAT = config['OPENMETEO_LAT'] +OPENMETEO_LON = config['OPENMETEO_LON'] +OPENMETEO_DAYS = config['OPENMETEO_DAYS'] +OPENMETEO_TIMEZONE = config['OPENMETEO_TIMEZONE'] METRIC = config['LOCALE']['METRIC'] locale.setlocale(locale.LC_ALL, (config['LOCALE']['ISO'], 'UTF-8')) @@ -91,11 +91,10 @@ # or to create your own custom test data for your own dashboard views) if config['ENV'] == 'DEV': SERVER = config['MOCKSERVER_URL'] - WEATHERBIT_IO_KEY = config['WEATHERBIT_DEV_KEY'] HEADERS = {'X-Api-Key': f'{config["MOCKSERVER_API_KEY"]}'} elif config['ENV'] == 'STAGE': - WEATHERBIT_IO_KEY = config['WEATHERBIT_DEV_KEY'] + pass elif config['ENV'] == 'Pi': if config['DISPLAY']['FRAMEBUFFER'] is not False: @@ -104,7 +103,6 @@ os.environ["SDL_VIDEODRIVER"] = "fbcon" LOG_PATH = '/mnt/ramdisk/' - WEATHERBIT_IO_KEY = config['WEATHERBIT_IO_KEY'] logger.info(f"STARTING IN {config['ENV']} MODE") @@ -529,27 +527,193 @@ def update_json(): CONNECTION = pygame.time.get_ticks() + 1500 # 1.5 seconds - try: + def wmo_code_to_icon(wmo_code, is_day): + """Convert WMO weather code to Weatherbit icon format""" + day_night = 'd' if is_day else 'n' + + # Clear sky + if wmo_code == 0: + return f'c01{day_night}' + # Mainly clear, partly cloudy, overcast + elif wmo_code == 1: + return f'c02{day_night}' + elif wmo_code == 2: + return f'c03{day_night}' + elif wmo_code == 3: + return f'c04{day_night}' + # Fog + elif wmo_code in [45, 48]: + return f'f01{day_night}' + # Drizzle + elif wmo_code in [51, 53, 55]: + return f'd01{day_night}' + # Freezing drizzle + elif wmo_code in [56, 57]: + return f'd02{day_night}' + # Rain + elif wmo_code == 61: + return f'r01{day_night}' + elif wmo_code == 63: + return f'r02{day_night}' + elif wmo_code == 65: + return f'r03{day_night}' + # Freezing rain + elif wmo_code in [66, 67]: + return f'r04{day_night}' + # Snow + elif wmo_code == 71: + return f's01{day_night}' + elif wmo_code == 73: + return f's02{day_night}' + elif wmo_code == 75: + return f's03{day_night}' + # Snow grains + elif wmo_code == 77: + return f's04{day_night}' + # Rain showers + elif wmo_code == 80: + return f'r04{day_night}' + elif wmo_code == 81: + return f'r05{day_night}' + elif wmo_code == 82: + return f'r06{day_night}' + # Snow showers + elif wmo_code == 85: + return f's05{day_night}' + elif wmo_code == 86: + return f's06{day_night}' + # Thunderstorm + elif wmo_code == 95: + return f't01{day_night}' + elif wmo_code in [96, 99]: + return f't02{day_night}' + else: + return f'unknown' + + def wmo_code_to_description(wmo_code): + """Convert WMO weather code to human-readable description""" + descriptions = { + 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 snowfall", + 73: "Moderate snowfall", + 75: "Heavy snowfall", + 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" + } + return descriptions.get(wmo_code, "Unknown weather") + + def wind_deg_to_dir(deg): + """Convert wind degrees to cardinal direction""" + directions = ["N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", + "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW"] + return directions[int((deg + 11.25) / 22.5) % 16] + + def transform_openmeteo_data(meteo_data): + """Transform Open-Meteo response to match expected Weatherbit structure""" + + # Transform current weather data + current = meteo_data['current'] + current_data = { + 'data': [{ + 'temp': current['temperature_2m'], + 'weather': { + 'description': wmo_code_to_description(current['weather_code']), + 'icon': wmo_code_to_icon(current['weather_code'], current['is_day']) + }, + 'wind_spd': current['wind_speed_10m'], + 'wind_cdir': wind_deg_to_dir(current['wind_direction_10m']), + 'wind_dir': current['wind_direction_10m'], + 'ts': int(datetime.datetime.fromisoformat(current['time']).timestamp()) + }] + } + + # Transform daily forecast data + daily_data = { + 'data': [] + } - current_endpoint = f'{SERVER}/current' - daily_endpoint = f'{SERVER}/forecast/daily' - stats_endpoint = f'{SERVER}/subscription/usage' - units = 'M' if METRIC else 'I' + for i in range(min(len(meteo_data['daily']['time']), OPENMETEO_DAYS)): + day_data = { + 'datetime': meteo_data['daily']['time'][i], + 'ts': int(datetime.datetime.fromisoformat(meteo_data['daily']['time'][i]).timestamp()), + 'low_temp': meteo_data['daily']['temperature_2m_min'][i], + 'high_temp': meteo_data['daily']['temperature_2m_max'][i], + 'weather': { + 'description': wmo_code_to_description(meteo_data['daily']['weather_code'][i]), + 'icon': wmo_code_to_icon(meteo_data['daily']['weather_code'][i], True) # Default to day icon + }, + 'pop': meteo_data['daily']['precipitation_probability_max'][i], + 'precip': meteo_data['daily']['precipitation_sum'][i], + 'snow': meteo_data['daily']['snowfall_sum'][i], + 'sunrise_ts': int(datetime.datetime.fromisoformat(meteo_data['daily']['sunrise'][i]).timestamp()), + 'sunset_ts': int(datetime.datetime.fromisoformat(meteo_data['daily']['sunset'][i]).timestamp()) + } + daily_data['data'].append(day_data) + + # Create mock stats data (Open-Meteo doesn't provide usage stats) + stats_data = { + 'calls_remaining': 999999 # No API limits for free tier + } - logger.info(f'connecting to server: {SERVER}') + return current_data, daily_data, stats_data - options = str(f'&postal_code={WEATHERBIT_POSTALCODE}' - f'&country={WEATHERBIT_COUNTRY}' - f'&lang={WEATHERBIT_LANG}' - f'&units={units}') + try: - current_request_url = str(f'{current_endpoint}?key={WEATHERBIT_IO_KEY}{options}') - daily_request_url = str(f'{daily_endpoint}?key={WEATHERBIT_IO_KEY}{options}&days={WEATHERBIT_DAYS}') - stats_request_url = str(f'{stats_endpoint}?key={WEATHERBIT_IO_KEY}') + logger.info(f'connecting to server: {SERVER}') - current_data = requests.get(current_request_url, headers=HEADERS).json() - daily_data = requests.get(daily_request_url, headers=HEADERS).json() - stats_data = requests.get(stats_request_url, headers=HEADERS).json() + # Build Open-Meteo API request parameters + current_params = [ + 'temperature_2m', 'weather_code', 'wind_speed_10m', + 'wind_direction_10m', 'is_day' + ] + + daily_params = [ + 'weather_code', 'temperature_2m_max', 'temperature_2m_min', + 'sunrise', 'sunset', 'precipitation_sum', 'snowfall_sum', + 'precipitation_probability_max' + ] + + options = str(f'latitude={OPENMETEO_LAT}' + f'&longitude={OPENMETEO_LON}' + f'¤t={",".join(current_params)}' + f'&daily={",".join(daily_params)}' + f'&timezone={OPENMETEO_TIMEZONE}' + f'&forecast_days={OPENMETEO_DAYS}') + + request_url = str(f'{SERVER}?{options}') + + response = requests.get(request_url, headers=HEADERS) + weather_data = response.json() + + # Check for API errors + if 'error' in weather_data: + logger.error(f"Open-Meteo API error: {weather_data.get('reason', 'Unknown error')}") + raise Exception(f"API Error: {weather_data.get('reason', 'Unknown error')}") + + # Transform Open-Meteo data to match expected Weatherbit structure + current_data, daily_data, stats_data = transform_openmeteo_data(weather_data) data = { 'current': current_data, diff --git a/example.config.json b/example.config.json index cbbc74d..92876ec 100644 --- a/example.config.json +++ b/example.config.json @@ -11,13 +11,12 @@ "SHOW_API_STATS": true, "MOUSE": false }, - "WEATHERBIT_URL": "https://api.weatherbit.io/v2.0", - "WEATHERBIT_IO_KEY": "xxxxxxxxxxxxxxxxxxxxxxxxx", - "WEATHERBIT_COUNTRY": "de", - "WEATHERBIT_LANGUAGE": "en", - "WEATHERBIT_POSTALCODE": 10178, - "WEATHERBIT_HOURS": 1, - "WEATHERBIT_DAYS": 4, + "OPENMETEO_URL": "https://api.open-meteo.com/v1/forecast", + "OPENMETEO_LANGUAGE": "en", + "OPENMETEO_LAT": 52.5200, + "OPENMETEO_LON": 13.4050, + "OPENMETEO_DAYS": 4, + "OPENMETEO_TIMEZONE": "auto", "LOCALE": { "ISO": "en_GB", "RAIN_STR": "Rain",