Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 24 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
@@ -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)

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
```
Expand All @@ -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
```
Expand All @@ -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`
Expand Down Expand Up @@ -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/)
Expand Down
214 changes: 189 additions & 25 deletions WeatherPiTFT.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'))
Expand All @@ -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:
Expand All @@ -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")

Expand Down Expand Up @@ -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'&current={",".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,
Expand Down
13 changes: 6 additions & 7 deletions example.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down