From 92deb744556e2fc6c23c0f6f9e0680115e1cfcb3 Mon Sep 17 00:00:00 2001 From: Felix Hochgruber <16101788+felix-hoc@users.noreply.github.com> Date: Mon, 10 Aug 2026 11:55:53 +0200 Subject: [PATCH] Add weather symbol text and condition mapping for forecasts The forecast weather symbol (sy parameter) is only available as a numeric code and the code list is not part of the API metadata. Add the official German descriptions, unofficial English translations and a mapping to Home Assistant weather condition strings, based on https://github.com/Geosphere-Austria/dataset-api-docs/issues/30 get_forecast_current() now includes sy_text and condition, and the trimmed forecast payload gets derived sy_text and condition parameter series alongside the existing rain and wind_speed ones. Co-Authored-By: Claude Fable 5 --- README.md | 16 +++++ src/zamg/__init__.py | 12 ++++ src/zamg/symbols.py | 154 +++++++++++++++++++++++++++++++++++++++++++ src/zamg/zamg.py | 20 ++++++ tests/test_zamg.py | 39 +++++++++++ 5 files changed, 241 insertions(+) create mode 100644 src/zamg/symbols.py diff --git a/README.md b/README.md index 06dacd3..6c761bc 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,22 @@ if __name__ == "__main__": ``` +The forecast contains the GeoSphere Austria weather symbol (`sy` parameter) +as a numeric code. It is automatically translated into a textual description +(`sy_text`) and a [Home Assistant weather condition](https://www.home-assistant.io/integrations/weather/#condition-mapping) +(`condition`). The translation helpers can also be used directly: + +```python +from zamg import symbol_to_condition, symbol_to_text + +print(symbol_to_text(3)) # "Partly cloudy" +print(symbol_to_text(3, lang="de")) # "Wolkig" +print(symbol_to_condition(3)) # "partlycloudy" +``` + +The official (German) symbol code list is documented in +[Geosphere-Austria/dataset-api-docs#30](https://github.com/Geosphere-Austria/dataset-api-docs/issues/30). + ## Contributions are welcome! If you want to contribute to this please read the [Contribution guidelines](https://github.com/killer0071234/python-zamg/blob/master/CONTRIBUTING.md) diff --git a/src/zamg/__init__.py b/src/zamg/__init__.py index d876238..1a5aafb 100644 --- a/src/zamg/__init__.py +++ b/src/zamg/__init__.py @@ -9,6 +9,13 @@ ZamgStationNotFoundError, ZamgStationUnknownError, ) +from .symbols import ( + SYMBOL_CONDITION, + SYMBOL_TEXT_DE, + SYMBOL_TEXT_EN, + symbol_to_condition, + symbol_to_text, +) from .zamg import ZamgData __all__ = [ @@ -18,4 +25,9 @@ "ZamgStationNotFoundError", "ZamgStationUnknownError", "ZamgData", + "SYMBOL_CONDITION", + "SYMBOL_TEXT_DE", + "SYMBOL_TEXT_EN", + "symbol_to_condition", + "symbol_to_text", ] diff --git a/src/zamg/symbols.py b/src/zamg/symbols.py new file mode 100644 index 0000000..f1dc3f7 --- /dev/null +++ b/src/zamg/symbols.py @@ -0,0 +1,154 @@ +"""Weather symbol mappings for GeoSphere Austria forecast data. + +The forecast dataset (nwp-v1-1h-2500m) provides the weather symbol +parameter ``sy`` as a numeric code. The official (German only) code list +is documented in +https://github.com/Geosphere-Austria/dataset-api-docs/issues/30 +""" + +from __future__ import annotations + +SYMBOL_TEXT_DE: dict[int, str] = { + 1: "Wolkenlos", + 2: "Heiter", + 3: "Wolkig", + 4: "Stark bewölkt", + 5: "Bedeckt", + 6: "Bodennebel", + 7: "Hochnebel", + 8: "Leichter Regen", + 9: "Mäßiger Regen", + 10: "Starker Regen", + 11: "Schneeregen", + 12: "Schneeregen", + 13: "Schneeregen", + 14: "Leichter Schneefall", + 15: "Mäßiger Schneefall", + 16: "Starker Schneefall", + 17: "Regenschauer", + 18: "Regenschauer", + 19: "Starker Regenschauer", + 20: "Schneeregenschauer", + 21: "Schneeregenschauer", + 22: "Schneeregenschauer", + 23: "Schneeschauer", + 24: "Schneeschauer", + 25: "Starker Schneeschauer", + 26: "Gewitter", + 27: "Gewitter", + 28: "Starkes Gewitter", + 29: "Gewitter mit Schneeregen", + 30: "Starkes Gewitter mit Schneeregen", + 31: "Gewitter mit Schneefall", + 32: "Starkes Gewitter mit Schneefall", +} +"""Official German symbol descriptions from GeoSphere Austria.""" + +SYMBOL_TEXT_EN: dict[int, str] = { + 1: "Clear", + 2: "Mostly clear", + 3: "Partly cloudy", + 4: "Mostly cloudy", + 5: "Overcast", + 6: "Ground fog", + 7: "High fog", + 8: "Light rain", + 9: "Moderate rain", + 10: "Heavy rain", + 11: "Sleet", + 12: "Sleet", + 13: "Sleet", + 14: "Light snowfall", + 15: "Moderate snowfall", + 16: "Heavy snowfall", + 17: "Rain showers", + 18: "Rain showers", + 19: "Heavy rain showers", + 20: "Sleet showers", + 21: "Sleet showers", + 22: "Sleet showers", + 23: "Snow showers", + 24: "Snow showers", + 25: "Heavy snow showers", + 26: "Thunderstorm", + 27: "Thunderstorm", + 28: "Heavy thunderstorm", + 29: "Thunderstorm with sleet", + 30: "Heavy thunderstorm with sleet", + 31: "Thunderstorm with snowfall", + 32: "Heavy thunderstorm with snowfall", +} +"""Unofficial English translations of the symbol descriptions.""" + +SYMBOL_CONDITION: dict[int, str] = { + 1: "sunny", + 2: "sunny", + 3: "partlycloudy", + 4: "cloudy", + 5: "cloudy", + 6: "fog", + 7: "fog", + 8: "rainy", + 9: "rainy", + 10: "pouring", + 11: "snowy-rainy", + 12: "snowy-rainy", + 13: "snowy-rainy", + 14: "snowy", + 15: "snowy", + 16: "snowy", + 17: "rainy", + 18: "rainy", + 19: "pouring", + 20: "snowy-rainy", + 21: "snowy-rainy", + 22: "snowy-rainy", + 23: "snowy", + 24: "snowy", + 25: "snowy", + 26: "lightning-rainy", + 27: "lightning-rainy", + 28: "lightning-rainy", + 29: "lightning-rainy", + 30: "lightning-rainy", + 31: "lightning-rainy", + 32: "lightning-rainy", +} +"""Symbol codes mapped to Home Assistant weather condition strings. + +A consumer has to change "sunny" to "clear-night" at nighttime itself, +as this library does not know the position of the sun. +""" + + +def symbol_to_text(symbol: float | int | None, lang: str = "en") -> str | None: + """Translate a weather symbol code into a description. + + Args: + symbol: The numeric weather symbol code (``sy`` parameter). + lang: "en" for English (unofficial translation) or + "de" for the official German description. + + Returns: + The description of the weather symbol, + or None for an unknown symbol code. + """ + if symbol is None: + return None + text = SYMBOL_TEXT_DE if lang == "de" else SYMBOL_TEXT_EN + return text.get(int(round(symbol))) + + +def symbol_to_condition(symbol: float | int | None) -> str | None: + """Translate a weather symbol code into a Home Assistant weather condition. + + Args: + symbol: The numeric weather symbol code (``sy`` parameter). + + Returns: + The Home Assistant weather condition string, + or None for an unknown symbol code. + """ + if symbol is None: + return None + return SYMBOL_CONDITION.get(int(round(symbol))) diff --git a/src/zamg/zamg.py b/src/zamg/zamg.py index 8c308a7..afef77d 100644 --- a/src/zamg/zamg.py +++ b/src/zamg/zamg.py @@ -22,6 +22,7 @@ ZamgStationNotFoundError, ZamgStationUnknownError, ) +from .symbols import symbol_to_condition, symbol_to_text CLIENT_AGENT = f"Python/{version_info[0]}.{version_info[1]} +https://github.com/killer0071234/python-zamg python-zamg/{__version__}" @@ -134,6 +135,11 @@ def get_forecast_current( (u10m**2 + v10m**2) ** 0.5 * 3.6, 1 ) # Convert from m/s to km/h result["wind_speed"] = wind_speed + # Translate the weather symbol into a description and condition. + if "sy" in forecast_parameters: + symbol = forecast_parameters["sy"]["data"][index] + result["sy_text"] = symbol_to_text(symbol) + result["condition"] = symbol_to_condition(symbol) return result except (TypeError, ValueError, KeyError, IndexError) as exc: @@ -203,6 +209,20 @@ def _get_forecast_from_now(self, forecast_data: dict | None = None) -> dict: "data": wind_speed_data, } + # Translate the weather symbol into descriptions and conditions. + if "sy" in parameters: + sy_data = parameters["sy"]["data"][index:] + trimmed_parameters["sy_text"] = { + "name": "weather symbol text", + "unit": None, + "data": [symbol_to_text(symbol) for symbol in sy_data], + } + trimmed_parameters["condition"] = { + "name": "weather condition", + "unit": None, + "data": [symbol_to_condition(symbol) for symbol in sy_data], + } + properties["parameters"] = trimmed_parameters trimmed_feature["properties"] = properties trimmed_features.append(trimmed_feature) diff --git a/tests/test_zamg.py b/tests/test_zamg.py index 3a1a2fa..3f92aa6 100644 --- a/tests/test_zamg.py +++ b/tests/test_zamg.py @@ -14,6 +14,7 @@ ZamgStationNotFoundError, ZamgStationUnknownError, ) +from src.zamg.symbols import symbol_to_condition, symbol_to_text from src.zamg.zamg import ZamgData @@ -443,6 +444,8 @@ def test_get_forecast_current() -> None: assert result["rain"] == 0.4 assert result["tcc"] == 0.2 assert result["sy"] == 2.0 + assert result["sy_text"] == "Mostly clear" + assert result["condition"] == "sunny" @pytest.mark.asyncio @@ -500,6 +503,42 @@ async def test_get_forecast_trims_past_data() -> None: 0.3, ] assert result["features"][0]["properties"]["parameters"]["sy"]["data"] == [2.0, 3.0] + assert result["features"][0]["properties"]["parameters"]["sy_text"]["data"] == [ + "Mostly clear", + "Partly cloudy", + ] + assert result["features"][0]["properties"]["parameters"]["condition"]["data"] == [ + "sunny", + "partlycloudy", + ] + + +def test_symbol_to_text() -> None: + """Test translating weather symbol codes to descriptions.""" + + assert symbol_to_text(1) == "Clear" + assert symbol_to_text(26.0) == "Thunderstorm" + assert symbol_to_text(32) == "Heavy thunderstorm with snowfall" + assert symbol_to_text(1, lang="de") == "Wolkenlos" + assert symbol_to_text(26.0, lang="de") == "Gewitter" + assert symbol_to_text(None) is None + assert symbol_to_text(0) is None + assert symbol_to_text(33) is None + + +def test_symbol_to_condition() -> None: + """Test translating weather symbol codes to weather conditions.""" + + assert symbol_to_condition(1) == "sunny" + assert symbol_to_condition(3) == "partlycloudy" + assert symbol_to_condition(6.0) == "fog" + assert symbol_to_condition(10) == "pouring" + assert symbol_to_condition(16) == "snowy" + assert symbol_to_condition(20) == "snowy-rainy" + assert symbol_to_condition(28) == "lightning-rainy" + assert symbol_to_condition(None) is None + assert symbol_to_condition(0) is None + assert symbol_to_condition(33) is None @pytest.fixture