Commit Graph
1108 Commits
Author SHA1 Message Date
agessaman db07f8ca6f feat(i18n): fill the weather catalog gaps in every bundled language
Nine catalogs lacked 30 of the keys wx and gwx use, so a German or Spanish bot answered with English weekday letters (M, T, W, Th), English "feels" and English MQTT and "not available" errors, and gwx and the weather service named wind directions in English. They now have translations, including the 16 wind points in local letters (Polish keeps the international ones, which the English catalog supplies). en-GB gets the English text so every catalog is complete, and Russian gets the two new hourly/custom-source replies. A test keeps each catalog covering these keys.
2026-10-01 23:30:48 -07:00
agessaman dbe4ac7028 feat(gwx): name the place only when it adds information, as wx does
gwx prefixed every reply with the resolved place. It now follows wx's policy: a city is named when it resolved to another country than [Weather] default_country or, in the US, to another state than default_state (or no default_state is set); coordinates are named when the reverse lookup found a place; a ZIP code is not named. A failed reverse lookup for coordinates used to turn the input into a label like "47.60620, -122.33210" (the longitude standing in for a country); such replies now have no label. The ZIP path drops its reverse-geocoding request, whose only use was the label.
2026-10-01 23:23:14 -07:00
agessaman 3d86b70899 fix(wx): fit NOAA's tomorrow forecast into its byte budget
format_tomorrow_forecast took max_length and never used it, so two long periods (Miami's "Chance Showers And Thunderstorms" day and night) came to 132 bytes against a 100-118 byte budget and the send path split the reply in two. It now tries the full reply, then without the night period's wind, without any wind, the first period alone, and that period without wind. Measured over the captured NOAA forecasts with the real English catalog at budgets 100-235, nothing overruns now (15 of 384 did); gwx and wx's default reply already fit. The regenerated goldens differ only in tomorrow keys, whose key-name translations are longer than real text.
2026-10-01 23:17:47 -07:00
agessaman a2f0d4c22c fix(weather): a refused first message ends the reply
Follow-up parts of a wx or gwx reply skip the per-user rate limit, since the limiter already let the first part through. When it had not (rate-limited, or the send failed), the alert after the forecast and the later parts of a multi-day forecast still went out on their own. Now a refused first part ends the reply. The multi-day sender also sent a forecast that fits one message only after dropping blank lines as a continuation, skipping the limit; the first message now always goes through it. execute returns whether the reply was sent, which the stats and web viewer record.
2026-10-01 23:12:47 -07:00
agessaman da6e699008 fix(wx): read NOAA's precipitation, gust and below-zero wording
NOAA writes "Chance of precipitation is 60%." and "with gusts as high as 25 mph", and none of the precipitation or gust patterns allowed either, so the 🌦️ and 💨 details never appeared for real forecasts (the captured Miami, Anchorage and Denver forecasts all carry them). High/low extraction took only unsigned numbers and rejected Fahrenheit values under 20, so "Low around -5." or a high of 14 lost the high/low. Both now cover NOAA's range, -65 to 130 °F or -55 to 55 °C. The regenerated goldens differ only by the newly read details, which at tight budgets now take the place of pressure.
2026-10-01 23:08:02 -07:00
agessaman 43c3006072 Merge remote-tracking branch 'origin/refactor/weather-port' into feat/wx-gwx-convergence 2026-10-01 23:00:39 -07:00
agessaman 66176a173b refactor(weather): keep wx and gwx's companion-location DEBUG lines
The codex review of this port also found that the shared companion lookup dropped the three DEBUG lines wx and gwx logged on dev (no sender key, no row for the key, the position found). get_companion_lat_lon takes a trace flag that wx and gwx pass; rain and aurora, which never logged these, stay quiet. With trace, a bot without a db_manager logs the lookup error at the caller's level again instead of returning silently.
2026-10-01 23:00:14 -07:00
agessaman 45f2512acb Merge remote-tracking branch 'origin/feat/gwx-render-safe' into feat/wx-gwx-convergence
# Conflicts:
#	CHANGELOG.md
2026-10-01 22:53:29 -07:00
agessaman 4895da3f77 Merge remote-tracking branch 'origin/fix/weather-wind-emoji' into feat/wx-gwx-convergence
Goldens conflicted; I regenerated them, and against this branch they differ only in NOAA emojis, gwx wind labels and the Phoenix width budgets the merged branch corrected. gwx's new hourly test expected the old truncating conversion (202.5° as SW); with nearest-point directions it is SSW.
2026-10-01 22:50:34 -07:00
agessaman bce38407f2 test(gwx): check that a rendered gwx reply keeps its warning part 2026-10-01 22:47:02 -07:00
agessaman e9122b2dfd fix(weather): let rain and snow outrank cloud in NOAA emojis; skip non-finite wind degrees
A review of this branch found three things. Moving the partly/mostly cloudy check ahead of rain gave forecasts like "Chance Rain Showers then Mostly Cloudy" (ten of the 79 forecasts it sampled from live NWS points) a sun-behind-cloud emoji; rain and snow now come before every cloud check, so they get 🌦️ or ❄️. A NaN or infinite wind direction made gwx's new nearest-point conversion raise; it is now left out like a null one. The Phoenix daily-width test's budgets were written for a reply one byte shorter (SW is now WSW), so they no longer pinned <= at equality; they are 330/343 now and the regenerated golden shows each segment fitting exactly at its budget.
2026-10-01 22:46:47 -07:00
agessaman 37aabe5641 Merge remote-tracking branch 'origin/feat/gwx-hourly' into feat/wx-gwx-convergence
Conflicts: NOAA hourly wind keeps this branch's full 16-point direction and fix/noaa-configured-units' speed conversion (a test pins both); gwx's hourly forecast returns (reply, {}) like the other forecast types since fix/gwx-warnings-structured, and _open_meteo_current keeps fix/gwx-missing-current's Optional return.
2026-10-01 22:42:38 -07:00
agessaman 4ab2a9ae48 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.
2026-10-01 22:32:56 -07:00
agessaman f4aaa7556f Merge remote-tracking branch 'origin/fix/gwx-missing-current' into feat/wx-gwx-convergence
The distorted-London golden conflicted; I regenerated it, and it differs from this branch only in the no_current and null_temp keys the merged change means to move.
2026-10-01 22:29:46 -07:00
agessaman f597205ce4 Merge remote-tracking branch 'origin/fix/noaa-configured-units' into feat/wx-gwx-convergence
The multi-day formatter from fix/weather-multiday-days still tested the period temperature for truthiness, so with both branches a 0° day (common in Celsius) dropped out of the multi-day forecast again. It now uses _has_temp like the older formatter.
2026-10-01 22:26:35 -07:00
agessaman fcb10eaa7e Merge remote-tracking branch 'origin/fix/weather-multiday-days' into feat/wx-gwx-convergence
# Conflicts:
#	CHANGELOG.md
2026-10-01 22:26:08 -07:00
agessaman 988c0caeb3 Merge remote-tracking branch 'origin/feat/gwx-keep-coordinates' into feat/wx-gwx-convergence
# Conflicts:
#	CHANGELOG.md
2026-10-01 22:22:34 -07:00
agessaman bdd9ea92cb Merge remote-tracking branch 'origin/fix/nws-alerts-per-location' into feat/wx-gwx-convergence 2026-10-01 22:22:27 -07:00
agessaman 3964a74301 docs(changelog): say which part of the units change reaches default-unit bots 2026-10-01 22:21:53 -07:00
agessaman 9b9a0a0605 fix(weather): recognize Open-Meteo's heavy-thunderstorm code 97
A review pointed out that Open-Meteo documents WMO code 97 as a heavy thunderstorm, and neither gwx nor the weather service knew it: the reply showed an unknown condition with the generic emoji and gwx gave no thunderstorm warning. It now has a description in every language, the ⛈️ emoji and the thunderstorm warning.
2026-10-01 22:21:48 -07:00
agessaman cf91210394 fix(geocoding): keep a zero coordinate on the state-qualified city lookups
A review found two more paths in geocode_city_sync still testing coordinates for truthiness: the lookup with a state parsed from the query and the [Weather] default_state fallback. A place on the equator or prime meridian found there (or cached) was thrown away, so the reply fell through to later lookups or failed.
2026-10-01 22:20:44 -07:00
agessaman 2260ac0353 fix(gwx): leave out an absent wind direction instead of reporting north
A review found that an Open-Meteo response with no wind_direction_10m key still defaulted the direction to 0°, so the reply read ⬆️N; a null direction was already left out. Both now give the wind speed with no direction.
2026-10-01 22:20:07 -07:00
agessaman f893c7f00d feat(gwx): allow gwx in scheduled {cmd:...} messages
gwx was not render_safe, so {cmd:gwx ...} expanded to nothing, while {cmd:wx ...} already ran gwx through delegation when weather_provider = openmeteo. gwx sends every reply, including the two-part warning, through send_response, which is what render_safe requires. Tests render gwx (plain and two-part replies) through the real CommandManager capture and check nothing is transmitted.
2026-10-01 22:13:06 -07:00
agessaman 0e8ba9bb61 fix(weather): nearest-point wind directions in gwx, and NOAA emoji precedence
gwx's _degrees_to_direction picked the 8-point sector starting below the angle (22.5° → NE, 241° → SW, negative angles → N); it now picks the nearest of the 16 points the catalog already translates, with the arrow wx uses, via _COMPASS_16/_ARROWS_8 moved to weather_common so both commands share them. wx's get_weather_emoji tested 'cloudy' before 'partly cloudy'/'mostly cloudy' and rain before thunderstorms, so those got ☁️ and 🌦️; thunderstorms (and 't-storm') now come right after heavy rain, and the partly/mostly cloudy check before plain cloudy. Golden moves: NOAA emojis for thunderstorm and partly/mostly cloudy periods (Seattle hourly fits one more hour, ⛅ being shorter than ☁️), and gwx wind labels where the nearest point differs.
2026-10-01 22:08:37 -07:00
agessaman 45894262c6 fix(wx): convert station gusts by NOAA's declared unit, keep 0° periods, read km/h gusts from forecast text
A codex review of this PR found three problems. Station observation gusts come in km/h (unitCode wmoUnit:km_h-1) and were multiplied as m/s, so a 36 km/h gust showed as 80 mph (an existing bug in the default mode) and as 129 km/h or 36 m/s in the new ones; gusts and visibility now follow their unitCode. Four 'if temp' guards treated 0° as missing, which Celsius makes common; they test for a missing value instead. The forecast-text gust fallback only knew mph and did not convert; it now reads mph or km/h and converts to the configured unit.
2026-10-01 22:03:43 -07:00
agessaman 151cf0b78a fix(wx): take today from the location's clock, and date undated periods from their neighbors
A codex review found two holes in using the first NOAA period's date as today: at 1 AM the first period can still be the night that began the evening before, so tomorrow and multi-day started a day early; and one period without startTime switched the forecast to date grouping and then dropped that period (multi-day showed Friday night's low as Friday). Today is now the current instant in the UTC offset NOAA writes on its periods; an undated period takes its neighbor's date (same date for a night after a day, else the next). With the characterization clock at Thursday afternoon the drop-period variants start on Friday again, as before this PR; against the base branch only Miami's seven-day forecast (a seventh date) and its already-started hourly entry differ for NOAA.
2026-10-01 21:59:50 -07:00
agessaman 65ca8ff164 fix(geocoding): accept a zero coordinate throughout city geocoding
A codex review of this PR found that geocode_city_sync (and its async and step-generator forms) still tested coordinates for truthiness in ten places, so a country-qualified lookup that found a place on the equator or prime meridian was discarded and the fallback queries tried instead; wx's own None check never saw it. They now test for None. The new test drives geocode_city_sync itself with only the first lookup succeeding, which fails before this change. The CHANGELOG entry's lookup count is corrected.
2026-10-01 21:55:26 -07:00
agessaman 65eb3492fa feat(gwx): hourly forecasts; custom sources say when an option is unavailable
gwx advertised hourly but parsed it as part of the place name. It now requests Open-Meteo's hourly precipitation probability alongside the hourly fields it already got and formats the hours after Open-Meteo's location-local current time in wx's compact one-reply style, sharing the hour label, description shortening and byte packing through WeatherCommandMixin. Hourly lines drop the wind arrow (both commands) so the arrow's six bytes do not push an hour out of the reply; wx's NOAA hourly now keeps the full 16-point letters instead of the first two. WXSIM sources answer hourly and alerts with source_option_not_available instead of their default conditions; wx no longer skips an MQTT alias for alerts (the alias then got geocoded as a city); gwx answers 'alerts' the same way. WXSIM highs and lows of exactly 0° are no longer treated as missing, and gwx warns about a stale WXSIM forecast as wx does. Only noaa_wx_hourly_seattle moved (WNW/NNW instead of WN/NN).
2026-10-01 21:51:34 -07:00
agessaman 71f7f93e26 fix(gwx): judge extreme-weather warnings from Open-Meteo's numbers, not the reply text
_check_extreme_conditions regex-searched the finished, translated reply: it only saw temperatures followed by °F (so -10°F read as 10 and Celsius replies never warned), took the number after a direction label as the wind speed and called it mph whatever the unit, and lost it when the direction letters were translated. The check now gets the current conditions from the same Open-Meteo request (via _get_open_meteo_weather_with_conditions; get_open_meteo_weather keeps its signature) and uses the weather codes the description match stood for (65/82 heavy rain, 95/96/99 thunderstorms, 75/85/86 snow), and the temperature and sustained wind converted to °F and mph for the unchanged thresholds. Warning text and the multi_message shape are unchanged.
2026-10-01 21:46:41 -07:00
agessaman a4513d17c9 fix(gwx): leave out missing current fields instead of inventing values
_open_meteo_current defaulted every missing current field to 0, so a response without current conditions read as clear skies, 0° and 0% humidity, and called int() on present-but-null fields, which failed the whole reply. A missing current temperature now returns None (the caller replies with error_fetching and logs a warning); null or absent humidity, wind, gusts, feels-like and weather code are each left out. The no_current and null_temp distortion goldens move accordingly; nothing else does.
2026-10-01 21:42:44 -07:00
agessaman 883bad9334 feat(wx): follow the configured temperature and wind units for NOAA
NOAA replies were always in °F and mph, and the station observations were converted to °F, miles and mph regardless of [Weather] temperature_unit and wind_speed_unit, which gwx honored. With Celsius configured, wx now requests NOAA's SI units (units=si, which also makes the forecast text it extracts highs and lows from metric), shows visibility in km and dew point in °C; wind speeds and observed gusts are converted to the configured mph/kmh/ms. Units resolve as the WXSIM path already did: [Wx_Command] if set, else [Weather], invalid values fall back to fahrenheit/mph. Defaults are unchanged and no golden moved. config.ini.example comments out [Wx_Command]'s copies of the unit keys, which silently pinned wx to Fahrenheit when an operator switched [Weather] to Celsius.
2026-10-01 21:37:52 -07:00
agessaman 987df9b124 fix(weather): scope NWS 'no coverage' to the point that got it, not the whole bot
A no-coverage reply (400/404) from the NWS alerts API set a single flag on wx and on the weather service that skipped every later alert request for the rest of the run, so one wx request for a city abroad silently removed alerts from every US reply after it. NWSNoCoverageCache remembers no-coverage per point rounded to two decimals, for 24 hours, at most 256 points (least recently used dropped first); a successful response clears its point, and the warning is logged once per point. The weather service polls one position, so for it the change means a retry after 24 hours instead of never.
2026-10-01 21:31:38 -07:00
agessaman 70972d1d4d refactor(weather): keep solarforecast's enabled check and the names local plugins may use
A codex review of this port found solarforecast relying on the enabled_attr hook, which dev's BaseCommand does not have, so [Solarforecast_Command] enabled = false stopped working; its can_execute override is back as on dev. It also found names an earlier refactor commit had dropped as unused, which local plugins could still read: WxCommand.forecast_duration, num_wx_alerts, use_metric, zulu_time and geolocator, geolocator on gwx and solarforecast, the WXSIM_PARSER_AVAILABLE and WX_INTERNATIONAL_AVAILABLE flags, and the _mqtt_weather_error_key hook (now a WeatherCommandMixin method that _mqtt_weather_line calls through self, so an override takes effect again). tests/unit/test_weather_compat_surface.py pins all of them.
2026-10-01 21:27:56 -07:00
agessaman 8874c54a67 fix(weather): date forecasts by the location's own times, not the bot's clock
wx took "today" from the bot's clock: on a server running UTC, or answering for a city in another zone, tomorrow and multi-day started a day late, and hourly compared NOAA's local wall-clock times with the bot's after dropping their offsets, keeping hours that had passed. wx now takes today from the forecast's first period date and compares hourly times as absolute times; multi-day groups NOAA periods by their start date, which also keeps holiday-named periods ("Columbus Day") the weekday-name grouping skipped. Name-based grouping remains the fallback when periods have no startTime. gwx chose Today/Tonight and the multi-day weekday labels from the bot's clock; it now uses Open-Meteo's location-local current time and daily dates. gwx also asked for N days counting today and then skipped today, so 7d showed six days; it now asks for N+1 (capped at Open-Meteo's 16). The NOAA characterization clock gains its time zone, so aware comparisons do not depend on the machine running the tests; the regenerated goldens move only where these fixes apply (drop-period variants whose first period is a later day, Miami's already-started 6 PM hour, gwx Today/Tonight and weekday labels, and gwx's forecast_days request parameter).
2026-10-01 21:24:03 -07:00
agessaman 94e6387eba feat(weather): forecast the sender's own point in gwx, and accept zero coordinates
gwx with no location reverse-geocoded the companion's (or bot's) coordinates to a town name and then forecast that name: three Nominatim lookups, and a forecast for wherever the geocoder put the town rather than for the sender. It now passes the coordinates through, as wx does; the reply's label still comes from the one reverse lookup in geocode_location. Both commands write those coordinates in fixed decimals (`_coordinates_query`), since str() of a small float gives "1e-05", which the coordinate pattern does not match. The companion and repeater position queries in modules.location skipped any zero latitude or longitude; only 0,0 is the no-position sentinel, so they now exclude just that. wx's city lookup used truthiness and rejected a geocoded zero coordinate.
2026-10-01 21:17:16 -07:00
agessaman 5601a126dd refactor(weather): port the behavior-neutral weather refactor from refactor/maintainability
This brings the weather slices of my long-running refactor branch to dev ahead of converging wx and gwx, so the convergence PRs build on one copy of the code instead of rewriting it twice.

- `modules/weather_common.py` (WeatherCommandMixin): code wx and gwx shared by copy (MQTT and WXSIM source lookup, multi-day sending, high/low formatting, the forecast-option parser, the Open-Meteo model loader).
- `modules/nws_alerts.py`: one NWS alert entry parser for wx and the weather service, keeping each caller's own Special Weather Statement rules.
- `modules/http_retry.py`: one retrying session builder for the weather HTTP clients.
- wx: get_noaa_weather, the hourly, tomorrow and multi-day formatters, _add_period_details and the extract_* readers broken up or made table-driven; NOAA fetches go through `_noaa_fetch`.
- gwx: one fetch path; `_open_meteo_current` and `_open_meteo_daily_tail` split out of get_open_meteo_weather.
- solarforecast: `_format_forecast` built from one timestamp parser and one day builder (418 → 155 lines).
- aurora, rain, wx, gwx: companion and bot location lookups through `modules.location` (which gains optional logging arguments).

The changes outside these files that the refactor branch made (the base command's enabled_attr hook, the service base's run_periodic and _cancel_tasks) stay out: aurora and rain keep their can_execute overrides and the weather service its own loops, exactly as on dev.

Tests: characterization goldens over real captured NOAA, NWS, Open-Meteo and forecast.solar responses (tests/characterization, fixtures in tests/fixtures), 50 tests and several hundred cases, all of which also pass against dev's unmodified weather code. Each refactor step was additionally checked on the branch with an old-vs-new differential run (tens of thousands of generated inputs, including log calls and exceptions) and reviewed by codex.
2026-10-01 21:11:23 -07:00
Adam Gessaman 8d28ac637e Merge pull request #321 from agessaman/fix/gwx-null-forecast-days
fix(gwx): end multi-day forecasts at Open-Meteo's null days instead of failing
2026-10-01 20:12:47 -07:00
agessaman 9b04791dff fix(gwx): end multi-day forecasts at Open-Meteo's null days instead of failing
Open-Meteo always returns the requested number of days but fills the ones past the model's horizon with nulls: the last one or two with the default model, nine of sixteen with icon_seamless. format_multiday_forecast called int() on them, so the whole reply became the multi-day error (gwx 15d for London and Reykjavik on 2026-10-01; gwx 7d under icon_seamless). It now stops at the first day without a high and low. The tomorrow forecast and the default reply compared a null precipitation probability with 30, which raised too; a null probability now just leaves the precipitation out, and a tomorrow with null temperatures reports that it is not available.
2026-10-01 19:27:49 -07:00
Adam Gessaman 6dfd02711a Merge pull request #320 from agessaman/fix/wx-wind-direction
fix(wx): show NOAA's 16-point wind directions instead of truncating them
2026-10-01 18:40:58 -07:00
agessaman 09cf1f3e64 Merge remote-tracking branch 'origin/dev' into fix/wx-wind-direction 2026-10-01 18:40:49 -07:00
Adam Gessaman dc11733aa0 Merge pull request #319 from agessaman/fix/bot-tx-spacing
fix(send): stop dropping follow-up reply parts, and space concurrent transmissions
2026-10-01 18:40:48 -07:00
agessaman b7783ba443 Merge remote-tracking branch 'origin/dev' into fix/bot-tx-spacing
# Conflicts:
#	CHANGELOG.md
2026-10-01 18:40:39 -07:00
Adam Gessaman c430c06dca Merge pull request #317 from agessaman/fix/dm-ack-timeout-nan
fix(dm): treat a NaN dm_min_ack_timeout as unset
2026-10-01 18:40:38 -07:00
agessaman 9ffdd49b7f Merge remote-tracking branch 'origin/dev' into fix/dm-ack-timeout-nan 2026-10-01 18:40:29 -07:00
Adam Gessaman c42e515d0c Merge pull request #314 from agessaman/fix/atomic-migrations
fix(db): apply migrations atomically
2026-10-01 18:40:27 -07:00
agessaman 6971a9faf8 Merge remote-tracking branch 'origin/dev' into fix/atomic-migrations
# Conflicts:
#	CHANGELOG.md
2026-10-01 18:40:14 -07:00
Adam Gessaman 1c5e82ab79 Merge pull request #311 from agessaman/docs/region-scopes
docs: add a region scopes page
2026-10-01 18:40:05 -07:00
Adam Gessaman e8f3cbe401 Merge pull request #309 from agessaman/fix/region-warning-channel-scope
fix(region-warnings): send channel warnings at the bot's configured scope
2026-10-01 18:40:01 -07:00
agessaman 55e5f0eda3 fix(wx): show NOAA's 16-point wind directions instead of truncating them
abbreviate_wind_direction matched full words (NORTHWEST, NORTH, ...), but NOAA's forecast windDirection is always an abbreviation like WNW, SSE or N. Every value fell through to the fallback, which kept the first two characters behind a generic wind emoji, so replies read '💨WN2' or '💨SS1' and even plain N and NE lost their arrow. The 16 compass abbreviations now return the nearest 8-point arrow plus the abbreviation; full words and other values behave as before. Found while pinning wx's formatting on real NOAA responses for the maintainability refactor.
2026-10-01 15:53:04 -07:00
agessaman 4d16764f1e Merge remote-tracking branch 'origin/dev' into fix/dm-ack-timeout-nan
# Conflicts:
#	CHANGELOG.md
2026-10-01 15:46:26 -07:00