Skip to content
Draft
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
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
12 changes: 12 additions & 0 deletions src/zamg/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__ = [
Expand All @@ -18,4 +25,9 @@
"ZamgStationNotFoundError",
"ZamgStationUnknownError",
"ZamgData",
"SYMBOL_CONDITION",
"SYMBOL_TEXT_DE",
"SYMBOL_TEXT_EN",
"symbol_to_condition",
"symbol_to_text",
]
154 changes: 154 additions & 0 deletions src/zamg/symbols.py
Original file line number Diff line number Diff line change
@@ -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)))
20 changes: 20 additions & 0 deletions src/zamg/zamg.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__}"

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down
39 changes: 39 additions & 0 deletions tests/test_zamg.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
ZamgStationNotFoundError,
ZamgStationUnknownError,
)
from src.zamg.symbols import symbol_to_condition, symbol_to_text
from src.zamg.zamg import ZamgData


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