mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-10-23 22:32:29 +00:00
Merge remote-tracking branch 'origin/fix/gwx-warnings-structured' into feat/wx-gwx-convergence
fix/gwx-missing-current returns the fetch error as a bare string when the current temperature is missing, and this branch moved that code into a method whose callers unpack (reply, current conditions). Git merged it without a conflict; the merged early return now returns the pair, so the reply is the fetch error again instead of an unpacking error.
This commit is contained in:
commit
4ab2a9ae48
15 files changed
+254
-45
No files matched your search
@@ -37,6 +37,7 @@ semantic versioning.
|
||||
- Weather forecasts are dated by the location, not by the bot's clock. On a bot whose clock is in another time zone (a server on UTC, or a bot answering for a distant city), `wx`'s tomorrow and multi-day forecasts could start a day late, `wx`'s hourly forecast could list an hour that had already passed, and `gwx` labeled the current period Today or Tonight, and the multi-day lines' weekdays, by the bot's time. Both commands now take today's date and the time of day from the forecast itself (NOAA's period start times, Open-Meteo's local times). `wx`'s multi-day forecast also keeps holiday-named NOAA periods ("Columbus Day"), which it used to skip, and `gwx 7d` shows seven days instead of six: it asked Open-Meteo for seven days counting today and then left today out.
|
||||
- `wx`'s gust reading from the nearest weather station is no longer about 3.6 times too high. NOAA reports station gusts in km/h, and `wx` converted them as if they were meters per second, so a 36 km/h (22 mph) gust showed as `💨80`. Gusts are now converted from the unit NOAA declares with them. A forecast period at exactly 0° is also no longer dropped from tomorrow's and the multi-day forecast (it was taken for a missing temperature, which `temperature_unit = celsius` makes common).
|
||||
- `gwx` no longer invents weather when Open-Meteo leaves out current conditions. A response without them was shown as clear skies at 0° and 0% humidity, and a single null field (humidity, wind, gusts, feels-like) failed the whole reply. A missing current temperature now gets the usual "could not fetch" reply, and other missing current fields are simply left out of it.
|
||||
- `gwx`'s extreme-weather warnings now work in Celsius, in metric wind units and in other languages. They were found by searching the finished, translated reply text: a temperature was only recognized when followed by °F (so `-10°F` read as 10, and Celsius replies never warned), the wind number was read off the direction label whatever its unit and called mph, and translated direction letters hid it entirely. The warnings now use Open-Meteo's own numbers and weather codes from the same request, converted to °F and mph for the existing thresholds (95°F, 20°F, 30 mph sustained wind), with the same warning text. Open-Meteo's heavy-thunderstorm code (97) was missing from gwx and the weather service: it now shows as "Heavy T-Storm" with ⛈️ and warns like the other thunderstorm codes, instead of an unknown condition.
|
||||
- `gwx` multi-day forecasts no longer fail when the forecast runs out early. Open-Meteo returns 16 days but fills the ones past the model's horizon with nulls (the last one or two by default, nine with `weather_model = icon_seamless`), and the first null day made the whole reply the multi-day error, so `gwx 15d` failed for London and Reykjavik and `gwx 7d` failed under shorter-range models. The forecast now ends at the last day with data. A null precipitation probability no longer breaks the tomorrow forecast or the default reply either, and a tomorrow with null temperatures reports that it is not available.
|
||||
- `wx` shows wind directions properly. NOAA reports them as 16-point abbreviations (`WNW`, `SSE`), which `wx` only recognized as full words, so they fell through to a fallback that kept the first two letters and no arrow: `💨WN2` for a 2 mph west-northwest wind. They now keep all their letters with the nearest arrow (`↖️WNW2`).
|
||||
- A service plugin that does not start because it is not configured (Discord or Telegram with no channels, the map uploader with no key) is no longer restarted every 5 seconds with a warning each time. The health check restarted any service that was not running, and the restart counted as a success, which cleared its backoff. A service that does not start, or is still unhealthy after a restart, now waits out `service_restart_backoff_seconds` before the next try, so one that was only waiting for the radio still recovers. A restarted packet capture or map uploader service also gets its background work back (`stop()` left a shutdown flag set that `start()` never cleared), and the map uploader keeps logging to the bot's log file after a restart. The DARC MoWaS service no longer stops the whole bot when its port is taken (werkzeug called `sys.exit`), and no longer reports itself healthy when it could not listen. Restarts also no longer subscribe a service to radio events a second time: services never unsubscribed in `stop()`, so after a restart packet capture published every packet twice and the Discord, Telegram and prefix-collision services handled every event twice.
|
||||
|
||||
@@ -545,12 +545,13 @@ class GlobalWxCommand(WeatherCommandMixin, BaseCommand):
|
||||
|
||||
# Get weather forecast from Open-Meteo based on type
|
||||
# Pass location_prefix_len so weather formatting can account for it
|
||||
current = {}
|
||||
if forecast_type == "tomorrow":
|
||||
weather_text = self.get_open_meteo_weather(lat, lon, forecast_type="tomorrow", message=message, location_prefix_len=location_prefix_len)
|
||||
elif forecast_type == "multiday":
|
||||
weather_text = self.get_open_meteo_weather(lat, lon, forecast_type="multiday", num_days=num_days, message=message, location_prefix_len=location_prefix_len)
|
||||
else:
|
||||
weather_text = self.get_open_meteo_weather(lat, lon, message=message, location_prefix_len=location_prefix_len)
|
||||
weather_text, current = self._get_open_meteo_weather_with_conditions(lat, lon, message=message, location_prefix_len=location_prefix_len)
|
||||
|
||||
# Check if it's an error (translated error message)
|
||||
error_fetching = self.translate('commands.gwx.error_fetching')
|
||||
@@ -559,7 +560,7 @@ class GlobalWxCommand(WeatherCommandMixin, BaseCommand):
|
||||
|
||||
# Check for severe weather warnings (only for default forecast type)
|
||||
if forecast_type == "default":
|
||||
alert_text = self._check_extreme_conditions(weather_text)
|
||||
alert_text = self._check_extreme_conditions(current)
|
||||
|
||||
if alert_text:
|
||||
# Return multi-message format
|
||||
@@ -818,6 +819,13 @@ class GlobalWxCommand(WeatherCommandMixin, BaseCommand):
|
||||
Returns:
|
||||
str: Formatted weather string or error message.
|
||||
"""
|
||||
weather, _ = self._get_open_meteo_weather_with_conditions(
|
||||
lat, lon, forecast_type, num_days, message, location_prefix_len
|
||||
)
|
||||
return weather
|
||||
|
||||
def _get_open_meteo_weather_with_conditions(self, lat: float, lon: float, forecast_type: str = "default", num_days: int = 7, message: MeshMessage = None, location_prefix_len: int = 0) -> tuple[str, dict]:
|
||||
"""Return the reply and current conditions from the same Open-Meteo request."""
|
||||
# Get max message length dynamically, then subtract location prefix length
|
||||
max_length = self.get_max_message_length(message) if message else 130
|
||||
max_length = max_length - location_prefix_len # Account for location prefix
|
||||
@@ -854,28 +862,28 @@ class GlobalWxCommand(WeatherCommandMixin, BaseCommand):
|
||||
|
||||
if not response.ok:
|
||||
self.logger.warning(f"Error fetching weather from Open-Meteo: {response.status_code}")
|
||||
return self.translate('commands.gwx.error_fetching')
|
||||
return self.translate('commands.gwx.error_fetching'), {}
|
||||
|
||||
data = response.json()
|
||||
|
||||
if forecast_type == "tomorrow":
|
||||
return self.format_tomorrow_forecast(data)
|
||||
return self.format_tomorrow_forecast(data), {}
|
||||
if forecast_type == "multiday":
|
||||
return self.format_multiday_forecast(data, num_days)
|
||||
return self.format_multiday_forecast(data, num_days), {}
|
||||
|
||||
temp_symbol = "°F" if self.temperature_unit == 'fahrenheit' else "°C"
|
||||
weather = self._open_meteo_current(data, max_length, temp_symbol)
|
||||
if weather is None:
|
||||
return self.translate('commands.gwx.error_fetching')
|
||||
return self.translate('commands.gwx.error_fetching'), {}
|
||||
# Forecast high/low for today (the current conditions already name the period).
|
||||
daily = data.get('daily', {})
|
||||
if daily:
|
||||
weather = self._open_meteo_daily_tail(weather, daily, max_length, temp_symbol)
|
||||
return weather
|
||||
return weather, data.get('current', {})
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error fetching Open-Meteo weather: {e}")
|
||||
return self.translate('commands.gwx.error_fetching')
|
||||
return self.translate('commands.gwx.error_fetching'), {}
|
||||
|
||||
def _open_meteo_current(self, data: dict, max_length: int, temp_symbol: str) -> Optional[str]:
|
||||
"""Current conditions for the default gwx reply, with extra conditions when they fit.
|
||||
@@ -1280,6 +1288,7 @@ class GlobalWxCommand(WeatherCommandMixin, BaseCommand):
|
||||
86: "Snow Showers",
|
||||
95: "Thunderstorm",
|
||||
96: "T-Storm w/Hail",
|
||||
97: "Heavy T-Storm",
|
||||
99: "Severe T-Storm"
|
||||
}
|
||||
return weather_codes.get(code, self.translate('commands.gwx.weather_descriptions.unknown'))
|
||||
@@ -1323,66 +1332,48 @@ class GlobalWxCommand(WeatherCommandMixin, BaseCommand):
|
||||
86: "🌨️", # Snow Showers
|
||||
95: "⛈️", # Thunderstorm
|
||||
96: "⛈️", # Thunderstorm with Hail
|
||||
97: "⛈️", # Heavy Thunderstorm
|
||||
99: "⛈️" # Severe Thunderstorm
|
||||
}
|
||||
|
||||
return emoji_map.get(code, "🌤️")
|
||||
|
||||
def _check_extreme_conditions(self, weather_text: str) -> Optional[str]:
|
||||
def _check_extreme_conditions(self, current: dict) -> Optional[str]:
|
||||
"""Check for extreme weather conditions that warrant warnings.
|
||||
|
||||
Args:
|
||||
weather_text: The formatted weather text to check.
|
||||
current: Open-Meteo current conditions in the configured units.
|
||||
|
||||
Returns:
|
||||
Optional[str]: Warning text if conditions found, None otherwise.
|
||||
"""
|
||||
warnings = []
|
||||
|
||||
# Extract temperature from weather text
|
||||
temp_match = re.search(r'(\d+)°F', weather_text)
|
||||
if temp_match:
|
||||
temp = int(temp_match.group(1))
|
||||
# Keep the thresholds in Fahrenheit and mph, regardless of reply units.
|
||||
temp = current.get('temperature_2m')
|
||||
if temp is not None:
|
||||
if self.temperature_unit == 'celsius':
|
||||
temp = temp * 9 / 5 + 32
|
||||
if temp >= 95:
|
||||
warnings.append(self.translate('commands.gwx.warnings.extreme_heat'))
|
||||
elif temp <= 20:
|
||||
warnings.append(self.translate('commands.gwx.warnings.extreme_cold'))
|
||||
|
||||
# Check for severe weather indicators
|
||||
# Note: We check for English strings here since weather descriptions might be in English
|
||||
# In a fully localized version, we'd need to check translated strings too
|
||||
heavy_rain_en = "Heavy Rain"
|
||||
heavy_showers_en = "Heavy Showers"
|
||||
thunderstorm_en = "Thunderstorm"
|
||||
t_storm_en = "T-Storm"
|
||||
heavy_snow_en = "Heavy Snow"
|
||||
snow_showers_en = "Snow Showers"
|
||||
|
||||
# Also get translated versions for checking
|
||||
heavy_rain_trans = self.translate('commands.gwx.weather_descriptions.65')
|
||||
heavy_showers_trans = self.translate('commands.gwx.weather_descriptions.82')
|
||||
thunderstorm_trans = self.translate('commands.gwx.weather_descriptions.95')
|
||||
t_storm_trans = self.translate('commands.gwx.weather_descriptions.96')
|
||||
heavy_snow_trans = self.translate('commands.gwx.weather_descriptions.75')
|
||||
snow_showers_trans = self.translate('commands.gwx.weather_descriptions.86')
|
||||
|
||||
if (heavy_rain_en in weather_text or heavy_showers_en in weather_text or
|
||||
heavy_rain_trans in weather_text or heavy_showers_trans in weather_text):
|
||||
code = current.get('weather_code')
|
||||
if code in (65, 82):
|
||||
warnings.append(self.translate('commands.gwx.warnings.heavy_rain'))
|
||||
|
||||
if (thunderstorm_en in weather_text or t_storm_en in weather_text or
|
||||
thunderstorm_trans in weather_text or t_storm_trans in weather_text):
|
||||
if code in (95, 96, 97, 99):
|
||||
warnings.append(self.translate('commands.gwx.warnings.thunderstorms'))
|
||||
|
||||
if (heavy_snow_en in weather_text or snow_showers_en in weather_text or
|
||||
heavy_snow_trans in weather_text or snow_showers_trans in weather_text):
|
||||
# The old "Snow Showers" match also covered light snow showers (85).
|
||||
if code in (75, 85, 86):
|
||||
warnings.append(self.translate('commands.gwx.warnings.heavy_snow'))
|
||||
|
||||
# Check for high winds
|
||||
wind_match = re.search(r'[NESW]{1,2}(\d+)', weather_text)
|
||||
if wind_match:
|
||||
wind_speed = int(wind_match.group(1))
|
||||
if wind_speed >= 30:
|
||||
warnings.append(self.translate('commands.gwx.warnings.high_winds', wind_speed=wind_speed))
|
||||
# Sustained wind only, as before; the warning's number is in mph like its threshold.
|
||||
wind_factor = {'mph': 1, 'kmh': MI_TO_KM, 'ms': 0.44704}[self.wind_speed_unit]
|
||||
wind_speed = (current.get('wind_speed_10m') or 0) / wind_factor
|
||||
if wind_speed >= 30:
|
||||
warnings.append(self.translate('commands.gwx.warnings.high_winds', wind_speed=int(wind_speed)))
|
||||
|
||||
return " | ".join(warnings) if warnings else None
|
||||
@@ -931,7 +931,7 @@ class WeatherService(BaseServicePlugin):
|
||||
return "🌧️"
|
||||
elif code in [71, 73, 75, 77, 85, 86]:
|
||||
return "❄️"
|
||||
elif code in [95, 96, 99]:
|
||||
elif code in [95, 96, 97, 99]:
|
||||
return "⛈️"
|
||||
else:
|
||||
return "🌤️"
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
"""gwx warnings use Open-Meteo values, independently of reply units and wording."""
|
||||
|
||||
import configparser
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from modules.commands.alternatives.wx_international import GlobalWxCommand
|
||||
from modules.i18n import Translator
|
||||
|
||||
MODULE = "modules.commands.alternatives.wx_international"
|
||||
|
||||
|
||||
def _gwx(temperature_unit="fahrenheit", wind_unit="mph", language="en"):
|
||||
config = configparser.ConfigParser()
|
||||
config.read_dict({"Weather": {
|
||||
"temperature_unit": temperature_unit,
|
||||
"wind_speed_unit": wind_unit,
|
||||
}, "Gwx_Command": {}, "Bot": {}})
|
||||
bot = Mock()
|
||||
bot.config = config
|
||||
bot.translator = Translator(language)
|
||||
cmd = GlobalWxCommand(bot)
|
||||
cmd.geocode_location = Mock(return_value=(1.0, 2.0, {}, None))
|
||||
cmd._format_location_display = Mock(return_value="Test City")
|
||||
cmd.get_max_message_length = Mock(return_value=158)
|
||||
return cmd
|
||||
|
||||
|
||||
def _data(cmd, **current):
|
||||
temp = 25.0 if cmd.temperature_unit == "celsius" else 77.0
|
||||
return {
|
||||
"current": {
|
||||
"temperature_2m": temp,
|
||||
"apparent_temperature": temp,
|
||||
"relative_humidity_2m": 50,
|
||||
"wind_direction_10m": 0,
|
||||
"wind_speed_10m": 0,
|
||||
"wind_gusts_10m": 0,
|
||||
"weather_code": 0,
|
||||
**current,
|
||||
},
|
||||
"daily": {
|
||||
"temperature_2m_max": [temp, temp],
|
||||
"temperature_2m_min": [temp, temp],
|
||||
"weather_code": [0, 0],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _run(cmd, data, forecast_type="default", ok=True):
|
||||
response = Mock(ok=ok, status_code=503 if not ok else 200)
|
||||
response.json.return_value = data
|
||||
with patch(f"{MODULE}.requests.get", return_value=response) as get:
|
||||
result = cmd._get_weather_for_location_sync(
|
||||
"Test City", forecast_type=forecast_type, num_days=2, message=Mock()
|
||||
)
|
||||
get.assert_called_once()
|
||||
assert get.call_args.kwargs["params"]["temperature_unit"] == cmd.temperature_unit
|
||||
assert get.call_args.kwargs["params"]["wind_speed_unit"] == cmd.wind_speed_unit
|
||||
cmd.logger.error.assert_not_called()
|
||||
return result
|
||||
|
||||
|
||||
def _assert_warning(result, expected):
|
||||
if expected is None:
|
||||
assert isinstance(result, str)
|
||||
assert result.startswith("Test City: ")
|
||||
else:
|
||||
assert isinstance(result, tuple)
|
||||
assert len(result) == 3
|
||||
assert result[0] == "multi_message"
|
||||
assert result[1].startswith("Test City: ")
|
||||
assert result[2] == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize("temperature_unit", ["fahrenheit", "celsius"])
|
||||
@pytest.mark.parametrize("fahrenheit, expected", [
|
||||
(94.9, None), (95, "⚠️ Extreme heat"), (95.1, "⚠️ Extreme heat"),
|
||||
(20.1, None), (20, "⚠️ Extreme cold"), (19.9, "⚠️ Extreme cold"),
|
||||
(-1, "⚠️ Extreme cold"), (-100, "⚠️ Extreme cold"),
|
||||
])
|
||||
def test_temperature_thresholds(temperature_unit, fahrenheit, expected):
|
||||
cmd = _gwx(temperature_unit=temperature_unit)
|
||||
temp = fahrenheit if temperature_unit == "fahrenheit" else (fahrenheit - 32) * 5 / 9
|
||||
result = _run(cmd, _data(cmd, temperature_2m=temp, apparent_temperature=temp))
|
||||
_assert_warning(result, expected)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("wind_unit, factor", [("mph", 1), ("kmh", 1.609344), ("ms", 0.44704)])
|
||||
@pytest.mark.parametrize("mph, expected", [
|
||||
(29.9, None), (30, "⚠️ High winds (30 mph)"),
|
||||
(30.1, "⚠️ High winds (30 mph)"), (40, "⚠️ High winds (40 mph)"),
|
||||
])
|
||||
def test_wind_thresholds(wind_unit, factor, mph, expected):
|
||||
cmd = _gwx(wind_unit=wind_unit)
|
||||
result = _run(cmd, _data(cmd, wind_speed_10m=mph * factor))
|
||||
_assert_warning(result, expected)
|
||||
|
||||
|
||||
def test_40_kmh_is_below_the_high_wind_threshold():
|
||||
cmd = _gwx(wind_unit="kmh")
|
||||
_assert_warning(_run(cmd, _data(cmd, wind_speed_10m=40)), None)
|
||||
|
||||
|
||||
def test_sustained_wind_warning_keeps_its_speed_even_with_stronger_gusts():
|
||||
cmd = _gwx()
|
||||
_assert_warning(_run(cmd, _data(cmd, wind_speed_10m=30, wind_gusts_10m=45)),
|
||||
"⚠️ High winds (30 mph)")
|
||||
|
||||
|
||||
def test_gusts_alone_do_not_warn():
|
||||
# Gusts of 30+ mph are common on ordinary windy days; the warning is about sustained wind.
|
||||
cmd = _gwx()
|
||||
_assert_warning(_run(cmd, _data(cmd, wind_speed_10m=10, wind_gusts_10m=45)), None)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("code, expected", [
|
||||
(65, "⚠️ Heavy rain"), (82, "⚠️ Heavy rain"),
|
||||
(95, "⚠️ Thunderstorms"), (96, "⚠️ Thunderstorms"), (97, "⚠️ Thunderstorms"), (99, "⚠️ Thunderstorms"),
|
||||
(75, "⚠️ Heavy snow"), (85, "⚠️ Heavy snow"), (86, "⚠️ Heavy snow"),
|
||||
*[(code, None) for code in (0, 1, 2, 3, 45, 48, 51, 53, 55, 56, 57, 61, 63, 66, 67, 71, 73, 77, 80, 81, 999)],
|
||||
])
|
||||
def test_wmo_warning_categories(code, expected):
|
||||
cmd = _gwx()
|
||||
_assert_warning(_run(cmd, _data(cmd, weather_code=code)), expected)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("code, warning", [
|
||||
(65, "heavy_rain"), (82, "heavy_rain"),
|
||||
(95, "thunderstorms"), (96, "thunderstorms"), (97, "thunderstorms"), (99, "thunderstorms"),
|
||||
(75, "heavy_snow"), (85, "heavy_snow"), (86, "heavy_snow"),
|
||||
])
|
||||
def test_russian_directions_and_conditions(code, warning):
|
||||
cmd = _gwx(temperature_unit="celsius", wind_unit="kmh", language="ru")
|
||||
result = _run(cmd, _data(cmd, weather_code=code, wind_speed_10m=48.28032))
|
||||
expected = " | ".join([
|
||||
cmd.translate(f"commands.gwx.warnings.{warning}"),
|
||||
cmd.translate("commands.gwx.warnings.high_winds", wind_speed=30),
|
||||
])
|
||||
_assert_warning(result, expected)
|
||||
assert "С48" in result[1]
|
||||
|
||||
|
||||
def test_warning_words_in_a_description_do_not_create_warnings():
|
||||
cmd = _gwx()
|
||||
cmd.bot.translator.translations["commands"]["gwx"]["weather_descriptions"]["0"] = (
|
||||
"Heavy Rain Thunderstorm Heavy Snow"
|
||||
)
|
||||
_assert_warning(_run(cmd, _data(cmd)), None)
|
||||
|
||||
|
||||
def test_warning_does_not_depend_on_a_rendered_description():
|
||||
cmd = _gwx()
|
||||
cmd._get_weather_description = Mock(return_value="Wet")
|
||||
_assert_warning(_run(cmd, _data(cmd, weather_code=65)), "⚠️ Heavy rain")
|
||||
|
||||
|
||||
def test_daily_extremes_do_not_replace_current_conditions():
|
||||
cmd = _gwx()
|
||||
data = _data(cmd)
|
||||
data["daily"].update(
|
||||
temperature_2m_max=[110, 110], temperature_2m_min=[-100, -100],
|
||||
weather_code=[99, 99], wind_speed_10m_max=[100, 100], wind_gusts_10m_max=[120, 120],
|
||||
)
|
||||
_assert_warning(_run(cmd, data), None)
|
||||
|
||||
|
||||
def test_combined_warnings_keep_the_existing_order_and_wording():
|
||||
cmd = _gwx()
|
||||
result = _run(cmd, _data(cmd, temperature_2m=100, apparent_temperature=100,
|
||||
weather_code=95, wind_speed_10m=40))
|
||||
_assert_warning(result, "⚠️ Extreme heat | ⚠️ Thunderstorms | ⚠️ High winds (40 mph)")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("forecast_type", ["tomorrow", "multiday"])
|
||||
def test_forecast_only_replies_do_not_add_current_warnings(forecast_type):
|
||||
cmd = _gwx()
|
||||
result = _run(cmd, _data(cmd, temperature_2m=100, weather_code=99, wind_speed_10m=50),
|
||||
forecast_type=forecast_type)
|
||||
_assert_warning(result, None)
|
||||
|
||||
|
||||
def test_fetch_error_still_returns_the_api_error():
|
||||
cmd = _gwx()
|
||||
assert _run(cmd, {}, ok=False) == cmd.translate("commands.gwx.error_fetching_api")
|
||||
|
||||
|
||||
def test_public_weather_getter_still_returns_text():
|
||||
cmd = _gwx()
|
||||
response = Mock(ok=True)
|
||||
response.json.return_value = _data(cmd, temperature_2m=100, weather_code=99, wind_speed_10m=50)
|
||||
with patch(f"{MODULE}.requests.get", return_value=response) as get:
|
||||
text = cmd.get_open_meteo_weather(1.0, 2.0)
|
||||
assert isinstance(text, str)
|
||||
assert "100°F" in text
|
||||
get.assert_called_once()
|
||||
|
||||
|
||||
def test_heavy_thunderstorm_code_has_its_own_description_and_emoji():
|
||||
# Open-Meteo documents WMO 97 as a heavy thunderstorm.
|
||||
cmd = _gwx()
|
||||
assert cmd._get_weather_description(97) == "Heavy T-Storm"
|
||||
assert cmd._get_weather_emoji(97) == "⛈️"
|
||||
@@ -255,6 +255,7 @@
|
||||
"86": "Schneeschauer",
|
||||
"95": "Gewitter",
|
||||
"96": "Gewitter m/Hagel",
|
||||
"97": "Starkes Gewitter",
|
||||
"99": "Schweres Gewitter",
|
||||
"unknown": "Unbekannt"
|
||||
},
|
||||
|
||||
@@ -233,6 +233,7 @@
|
||||
"86": "Snow Showers",
|
||||
"95": "Thunderstorm",
|
||||
"96": "T-Storm w/Hail",
|
||||
"97": "Heavy T-Storm",
|
||||
"99": "Severe T-Storm",
|
||||
"unknown": "Unknown"
|
||||
},
|
||||
|
||||
@@ -300,6 +300,7 @@
|
||||
"86": "Snow Showers",
|
||||
"95": "Thunderstorm",
|
||||
"96": "T-Storm w/Hail",
|
||||
"97": "Heavy T-Storm",
|
||||
"99": "Severe T-Storm",
|
||||
"unknown": "Unknown"
|
||||
},
|
||||
@@ -1527,6 +1528,7 @@
|
||||
"86": "Heavy Snow Showers",
|
||||
"95": "Thunderstorm",
|
||||
"96": "Thunderstorm w/Hail",
|
||||
"97": "Heavy Thunderstorm",
|
||||
"99": "Severe Thunderstorm"
|
||||
},
|
||||
"wind_speed_units": {
|
||||
|
||||
@@ -246,6 +246,7 @@
|
||||
"86": "Chubascos de Nieve",
|
||||
"95": "Tormenta Eléctrica",
|
||||
"96": "Tormenta c/Granizo",
|
||||
"97": "Tormenta Fuerte",
|
||||
"99": "Tormenta Severa",
|
||||
"unknown": "Desconocido"
|
||||
},
|
||||
|
||||
@@ -307,6 +307,7 @@
|
||||
"86": "Averses de neige",
|
||||
"95": "Orage",
|
||||
"96": "Orage avec grêle",
|
||||
"97": "Orage fort",
|
||||
"99": "Orage violent",
|
||||
"unknown": "Inconnu"
|
||||
},
|
||||
|
||||
@@ -235,6 +235,7 @@
|
||||
"86": "Averses de neige",
|
||||
"95": "Orage",
|
||||
"96": "Orage avec grêle",
|
||||
"97": "Orage fort",
|
||||
"99": "Orage violent",
|
||||
"unknown": "Inconnu"
|
||||
},
|
||||
|
||||
@@ -234,6 +234,7 @@
|
||||
"86": "Sneeuwbuien",
|
||||
"95": "Onweer",
|
||||
"96": "Onweer m/hagel",
|
||||
"97": "Hevig onweer",
|
||||
"99": "Zwaar onweer",
|
||||
"unknown": "Onbekend"
|
||||
},
|
||||
|
||||
@@ -254,6 +254,7 @@
|
||||
"86": "Przelotne opady śniegu",
|
||||
"95": "Burza",
|
||||
"96": "Burza z gradem",
|
||||
"97": "Silna burza",
|
||||
"99": "Silna burza z gradem",
|
||||
"unknown": "Unknown"
|
||||
},
|
||||
|
||||
@@ -232,6 +232,7 @@
|
||||
"86": "Pancadas de Neve",
|
||||
"95": "Tempestade",
|
||||
"96": "Tempestade c/Granizo",
|
||||
"97": "Tempestade Forte",
|
||||
"99": "Tempestade Severa",
|
||||
"unknown": "Desconhecido"
|
||||
},
|
||||
|
||||
@@ -231,6 +231,7 @@
|
||||
"86": "Pancadas de Neve",
|
||||
"95": "Tempestade",
|
||||
"96": "Tempestade c/Granizo",
|
||||
"97": "Tempestade Forte",
|
||||
"99": "Tempestade Severa",
|
||||
"unknown": "Desconhecido"
|
||||
},
|
||||
|
||||
@@ -299,6 +299,7 @@
|
||||
"86": "Снеж.ливень",
|
||||
"95": "Гроза",
|
||||
"96": "Гроза+град",
|
||||
"97": "Сильная гроза",
|
||||
"99": "Сильн.гроза",
|
||||
"unknown": "Неизвестно"
|
||||
},
|
||||
@@ -919,6 +920,7 @@
|
||||
"86": "Снеж.ливень",
|
||||
"95": "Гроза",
|
||||
"96": "Гроза+град",
|
||||
"97": "Сильная гроза",
|
||||
"99": "Сильн.гроза"
|
||||
},
|
||||
"wind_speed_units": {
|
||||
|
||||
Reference in new issue
Block a user