_noaa_current_summary builds the current period's line with its details, UV and precipitation; _append_noaa_tomorrow appends the Tomorrow period. Both bodies are statement-for-statement the original code (checked by AST comparison), and an old-vs-new run over 30,000 perturbed real NOAA forecasts (fixtures in tests/fixtures/noaa, varied units, budgets, names, temperatures, wind, fetch failures and JSON errors) matched on replies, fetch calls and log calls.
_locate_coordinates, _locate_zipcode and _locate_city each return the error reply (or None), the coordinates and the location prefix; _noaa_forecast_body fetches and formats one forecast type; _with_noaa_alerts builds the default reply and its two-message alert tuple. An unrecognized location_type still geocodes as a city without the city prefix, as before. Old-vs-new over 30,000 randomized cases (fake geocoders, NOAA fetches and alert results, including exceptions and error sentinels) matched on replies, call traces and log calls; four mutations were each caught.
The custom MQTT and WXSIM replies, the no-location fallback chain (sender's position, default_city, bot's position) and the reply tail (two-part alerts, multiday split, single message) move into WeatherCommandMixin. Each command keeps its own WXSIM fetch (_fetch_wxsim) and wx keeps naming fallback positions in its log lines (_fallback_place_name). The no-location MQTT reply still never splits a multiday answer. The execute dispatch goldens are unchanged.
command_manager: _record_transmission passes extra keyword arguments through, so #336's recipient_key (DMs) and channel_idx (channels) reach record_transmission as on dev.
graph_trace_helper: dev's rewrite from #336, with its two repeater-key lookups (immediate neighbor and _unique_repeater_key) going through contacts_repo.unique_recent_repeater_key, whose queries are identical.
get_noaa_weather's Tomorrow part tested period.get('temperature', '') for truthiness, so a forecast of exactly 0 was treated as missing and the part was left out. It now uses _has_temp, as _noaa_period_str already does for Today and Tonight.
After #334 the two blocks were identical, so get_noaa_weather calls one helper for both. Tomorrow keeps its own thresholds inline. An old-vs-new run over 30,000 generated NOAA forecasts (log calls included) shows no difference; a threshold mutation in the helper shows 1,021.
get_noaa_weather's Today/Tonight/Tomorrow lookup moves verbatim into its own method, which returns the three periods and the two night flags the rest of the method reads. The moved statements and the remaining body are AST-identical to the original.
get_noaa_weather built the Tonight part only when _noaa_period_str returned one, but the detail and append steps after it ran either way. With no temperature or short forecast, period_str was unbound and the whole reply became "Error fetching weather data"; after a night-time Today part it still held that part, which was appended a second time. The steps now sit under the same check, as in the Today and Tomorrow blocks.
wx and gwx name the place only when it adds information: a city outside default_state or default_country, or a position. always_show_location (default false) names every place a lookup finds instead, including cities in the bot's own state and ZIP codes.
For a ZIP code, gwx now does a reverse lookup when the option is on (it skipped that since the name was never shown), and wx reuses the reverse lookup it already did for a companion position. A failed lookup leaves the reply unnamed rather than repeating the ZIP code. The prefix comes out of the message budget as before. The option is in the wx settings schema for the web viewer, the example config, the command reference and the changelog.
A codex re-review of the last two commits confirmed their fixes and found three gaps. wx alerts (and gwx alerts) with no location looked up a place called "alerts"; the option now applies to the fallback location like the others. A custom source named like an option (custom.wxsim.hourly or custom.mqtt_weather.hourly) was treated as the option and answered from the default source; a configured name now wins. The full alert list (wx <place> alerts) kept sending after a refused first message and reported success; it now stops and reports the refusal.
- A scheduled {cmd:gwx ...} or {cmd:wx ...} with a forecast and an alert slept out the TX pacing between them during capture; with bot_tx_rate_limit_seconds = 30 that passed the 30 s render timeout and the whole reply was lost. Pacing is skipped when the message has a capture_sink.
- gwx tomorrow ignored its byte budget (a Russian Reykjavik forecast was 99 bytes against 89). It now drops the precipitation amount, gusts, wind, the precipitation chance and the description, in that order. The Open-Meteo goldens give tomorrow runs a roomy budget, since their key-name translator makes the text much longer than real; trimming is tested with real catalogs in test_gwx_tomorrow_budget.
- Custom default sources (bare wx/gwx) still returned True after a refused send.
- An option without a location (wx hourly, gwx tomorrow, wx 5d) showed usage, since the fallback location was resolved before the option was parsed; the option now applies to the fallback, custom default sources included. The docs say how far each provider's forecast reaches.
- wx read its unit settings through get_config_value, whose [Weather] fallback logged a notice to move them into [Wx_Command] on every lookup (14 per reply); _unit_setting reads [Wx_Command], then [Weather], without it.
- NOAA forecasts that mention thunderstorms after sun ("Sunny then Slight Chance Showers And Thunderstorms") still got ☀️; thunderstorms now outrank sun.
- The catalog tests passed with keys removed, since a missing key differs from English; they now require each key and every wind point, with en-GB and Polish allowed to use the English letters.
A space bunny review of this branch's commits found:
- gwx's high-wind warning converted the speed to mph for every catalog, so on a km/h bot it read 49 next to a reply saying 80, and Polish (whose template said km/h) regressed in the merge. The threshold stays 30 mph; the warning shows the speed in the configured unit with a {unit} placeholder in every catalog.
- tomorrow_not_available, tomorrow_error, multiday_not_available and multiday_error were still English in seven catalogs; they are translated, and the catalog test now fails on any weather reply left in English.
- wx kept NOAA's English wind letters and English weekday letters while gwx's are translated, so one German bot showed NE and NO for the same wind. wx now translates both through the catalog (commands.wx.day_abbrev added); English output is byte-for-byte unchanged, which the goldens show (their stub translator gives English for these short labels, via tests.helpers.key_name_translate).
- Polish feels_like was ungrammatical (odczuwalna -> odczuwalnie), Russian's used a nonstandard abbreviation, and Russian gwx's MQTT subscriber error lacked wx's hint.
- The help text (commands.wx.description/usage, commands.gwx.usage) described wx as ZIP-only; it now lists cities, coordinates and the options, under 130 bytes in every language.
- The generated default config, the docs' default_state wording, _fit_tomorrow_parts' docstring and the CHANGELOG's stats claim were inaccurate.
The weather service took [Weather] wind_speed_unit = kn, while wx and gwx rejected it with a warning and used mph, so the same setting gave knots in one and mph in the other. Knots now work in all of them: Open-Meteo is asked for kn, NOAA's mph and km/h values and station gusts are converted, gwx's high-wind warning converts back to mph for its threshold, and the WXSIM parser converts its km/h. The web config's wind unit choices gain m/s and knots.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
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.
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.
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.
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).
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.
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.
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.
A user who raised bot_tx_rate_limit_seconds to 5 noticed wx's weather-then-alerts reply did not seem to honor it. The gap between those two messages was in fact longer than the limit (every send already waits on the bot TX limiter in _check_rate_limits, and wx sleeps limit + 1 s on top), but looking at it turned up two real bugs.
The second part of wx's and gwx's two-part reply, and every part after the first of 'alert all', went through the reply rate limiter (rate_limit_seconds, 10 s by default) a few seconds after the first part had used it, so it was refused: with default settings the alert message was never sent. These now pass skip_user_rate_limit=True, as the other multi-part senders already did. The repeater command's two-part branch gets the same change for consistency.
BotTxRateLimiter.wait_for_tx only checked the time since the last recorded transmission, and record_tx runs after the radio confirms, so concurrent senders (two replies, a reply and a scheduled message) all passed the wait before any of them recorded and transmitted back to back. wait_for_tx now claims the slot under the lock when it returns. A coverage test that monkeypatched can_tx to drive the old loop is rewritten against the new one.
An oversized webhook or channel body was handed to the firmware whole. The device dropped it without an ack, retries stalled the transport, and the radio looked dead. Split to the frame budget, keep links intact, and tag multi-part sends so they can be reassembled.
Reserve the wx location prefix from that same budget, and let NOAA alert polling be turned off without disabling the rest of the weather service.
- Extracted `modules/alert_format.py` as the single NWS alert formatter, replacing four copies of the event-type abbreviation table and two of the time compactor. `!wx alerts` and the proactive `WeatherService` broadcasts now localize from one code path, so a Russian bot no longer answers `!wx alerts` in English while its proactive alerts are Russian.
- Stopped leaking translation key paths into mesh broadcasts. `Translator.translate` returns the dotted key when a lookup misses in both the locale and the English fallback, which is right for development but reached the air in production: an unclassifiable NWS title rendered as `⚪Hazardous services.weather_service.event_types.Unknown`, an unmapped WMO code as `services.weather_service.weather_descriptions.4`, and an oddly-cased `wind_speed_unit` as `services.weather_service.wind_speed_units.KMH`. `alert_format.translate_or()` carries an English default at each site, and `WeatherService` now normalizes and validates its three `[Weather]` unit settings the way `GlobalWxCommand` already did.
- Fixed alert expiry rendering in every locale. The formatter rendered a timestamp to a string and re-parsed its own output with `(\d+)(AM|PM)` against a hardcoded English month list, so translated months took the wrong branch and truncated mid-string. Times now carry parsed parts and render through a per-locale `common.alerts.time_12h` template — the space before AM/PM was correct (Russian writes "6 дня", not "6дня"); the downstream regex was the bug.
- Restored month abbreviation. `_compact_time` iterated over abbreviations and replaced them in the string instead of mapping full names, so English stopped shortening "June 28" and Russian replaced the "Jun" inside "June", leaving a stray Latin "e" (`июнe 28`). Reuses the existing `common.date_time.month_abbreviations` rather than the duplicate `services.weather_service.months` block.
- Made `!gwx` display units follow `[Weather]` config instead of the response language. Visibility switched on `base_language != 'en'`, so `language = ru` with the default `temperature_unit = fahrenheit` printed Fahrenheit beside kilometers, and `en-GB` was forced to miles. Pressure is a locale convention rather than a metric/imperial split, so each catalog names its own via `commands.gwx.pressure_unit` — previously every non-English locale inherited mmHg from the English catalog, whose `pressure_mmhg` string contained Russian text, giving German and French users Cyrillic pressure units.
- Let localized `H`/`L` labels reach a standard install. `config.ini.example` shipped the three `temperature_*_format` keys uncommented with literal `H:`/`L:`, and a config value always beats the new locale-aware default, so a Russian bot built from the documented example still rendered `H:47°C L:33°C`. The example now uses the `{high_label}`/`{low_label}` placeholders, which were documented in the docstring but not in the file.
- Routed high/low labels through the reply's translator. `_format_high_low` passed `bot.translator`, so with `auto_detect_language` on, an English-default bot answering a Russian sender localized the rest of the line but not `H:`/`L:`. Added `BaseCommand.response_translator` for this, replacing `wx_international`'s reach into the private `_response_translator` ContextVar.
- Fixed a byte-budget overrun in `!gwx`. The guard on the extra conditions block compared a character count against a byte-derived budget while the rest of the function used `_count_display_width`; Cyrillic is two bytes per character, so the block was appended after the budget was spent.
- Reverted nine `commands.gwx` English rewordings that were not localization work, including the configuration hint in `mqtt_weather_no_subscriber` — dropping it left a mis-configured operator with no pointer to the two keys they need.
- Fixed the Russian `visibility` string, which said "км" on the miles key — the same locale/config conflation as the code bug, in the data. Shortened the Russian event-type abbreviations, which were full words consuming a quarter of the 130-byte budget at two bytes per character.
- Added `commands.wx.hourly_not_available`, missing from every catalog so `!wx hourly` printed the raw key path. Predates this branch; found while auditing every translation key the weather modules reference.
- Moved alert strings to `common.alerts.*` and wind directions to `common.wind_directions.*`, since a command and a service both read them.
- Addresses #267
- Added `original_content` attribute to `MeshMessage` to store the on-air body of messages, ensuring it remains unchanged during command processing.
- Updated command matching methods across various commands to utilize `_cleaned_content_matches`, which restores original content when necessary, preventing mention stripping from altering the message context.
- Implemented tests to verify that original content is preserved and correctly utilized in command execution and message handling.
- Add optional translator param to format_temperature_high_low()
- Translate temperature labels (H/L) via common.temp_high_label/low_label
- Translate wind directions via services.weather_service.wind_directions
- Fix wind format spacing (direction and speed were concatenated)
- Pass translator from Weather_Service, !wx, and !gwx callers
Correctness:
- {path_distance} was always blank in production. The resolution code that
builds repeater_info dropped latitude/longitude in every branch, so the
calculator could never find a coordinate. My tests passed hand-built
dicts straight to the calculator and never exercised the builder, which
is why they stayed green. Coordinates are now carried through all four
construction sites, and the new tests drive _lookup_repeater_names via
its lookup_func hook so the real builder runs.
- The #80 route guard was defeated two ways in the channel handler. When
the RF data was an uncorrelated fallback, control fell through to the
raw-hex and routing_info fallbacks below, which took the route from the
unrelated packet anyway; the guard had actually made that path
reachable. message.routing_info was also assigned unconditionally, and
the path command reads it. "Not attributable" is now a terminal branch
and the routing_info hand-off checks provenance.
- Same fix was incomplete for DMs: routing_info was captured and turned
into path_info before the provenance check ran, so the later check only
declined to overwrite an already-wrong value. Guarded at the source.
- Rendering could transmit for real. Capture only intercepts
send_response, but advert calls send_advert() directly and
send_response_chunked never checked capture_sink. Chunked sends are now
captured, and rendering is opt-in via BaseCommand.render_safe (default
False) instead of a denylist that cannot be complete. This also closes
the DM-only leak: schedule is not marked safe, so {cmd:schedule} can no
longer broadcast configuration to a channel.
- Multi-part rendered output was rejoined into one oversized send.
Scheduled messages are now split to the RF body budget and sent through
send_channel_messages_chunked, on character boundaries so multi-byte
text is not corrupted.
- _last_path_distance_km is instance state that was only set on success,
so an invalid path request could show the previous request's distance.
Reset at the start of every execute().
- Stale-contact retries were only counted on non-OK results, so timeouts
and exceptions left a contact eligible forever and could recreate the
storm. All failed attempts count now.
Web viewer:
- A failed config reload was reported as a successful save. The API and
UI now distinguish "saved and active" from "saved, restart needed".
- Preview count was unbounded; clamped to 1-20.
- update_ini_values is a read-modify-replace with no locking, so two
concurrent viewer saves could lose one. Serialised behind a lock.
Resolve the feeds.html conflict by keeping the branch's XSS-safe DOM
construction of the feed-details view and re-adding dev's per-feed and
reset-all error buttons via addEventListener instead of inline onclick,
since /feeds is a nonce-CSP page where inline handlers are blocked.
Update tests/test_feed_manager_extended.py::TestPollFeedPosting to patch
the SafeUrlPolicy.validate_async path (this branch's SSRF refactor) rather
than the removed module-level validate_external_url symbol, matching the
idiom already used throughout that test file.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Introduced a shared API for location resolution using `modules.location`, allowing for better handling of place lookups (coordinates, ZIP codes, city names).
- Updated geocoding functions to accept both strings and structured dictionaries, improving flexibility in location queries.
- Refactored existing commands (AQI, Rain, Wx) to utilize the new location resolution methods, streamlining the codebase and enhancing maintainability.
- Removed redundant location handling logic from commands, centralizing functionality in the new location module.
- Added tests to ensure proper classification and resolution of various location formats, including multi-word international cities and ZIP codes with surrounding whitespace.
- Modified `WxCommand` to use its own keywords along with any aliases from `BaseCommand` when delegating to `GlobalWxCommand` for the Open-Meteo weather provider.
- Added a new test fixture for `WxCommand` configured to delegate to `GlobalWxCommand`, ensuring that aliases are preserved and correctly matched in keyword checks.
- Enhanced unit tests to verify the delegation behavior and keyword matching for both standard and aliased commands.