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.
This commit is contained in:
agessaman committed 2026-10-01 21:27:56 -07:00
1 parent 5601a126dd
commit 70972d1d4d
5 files changed
+93 -3

No files matched your search

@@ -29,6 +29,9 @@ from ...utils import ( # noqa: F401 format_temperature_high_low and get_nomina
from ...weather_common import WeatherCommandMixin, load_open_meteo_model
from ..base_command import BaseCommand
# Kept for code that checked them; these imports used to be optional.
WXSIM_PARSER_AVAILABLE = True
# Multiday: plain digits, 7day/7-day, or suffix form 7d/10d (min 2, max below). Open-Meteo allows up to 16 forecast days.
GWX_MULTIDAY_MAX_DAYS = 16
@@ -99,6 +102,9 @@ class GlobalWxCommand(WeatherCommandMixin, BaseCommand):
self.logger.warning(f"Invalid precipitation_unit '{self.precipitation_unit}', using 'inch'")
self.precipitation_unit = 'inch'
# Initialize geocoder (will use rate-limited helpers for actual calls)
self.geolocator = get_nominatim_geocoder()
# Get database manager for geocoding cache
self.db_manager = bot.db_manager
+16 -2
View File
@@ -47,8 +47,6 @@ class SolarforecastCommand(BaseCommand):
# Read-only informational output; safe for scheduled {cmd:...} rendering.
render_safe = True
name = "solarforecast"
honors_skip_channel_check = False
enabled_attr = "solarforecast_enabled"
keywords = ['solarforecast', 'sf']
description = "Get solar panel production forecast (usage: sf <location|repeater_name|coordinates|zipcode> [panel_size] [azimuth, 0=south] [angle])"
category = "solar"
@@ -99,9 +97,25 @@ class SolarforecastCommand(BaseCommand):
# Get default state from config for city disambiguation
self.default_state = self.bot.config.get('Weather', 'default_state', fallback='')
# Initialize geocoder (will use rate-limited helpers for actual calls)
self.geolocator = get_nominatim_geocoder()
# Get database manager for geocoding cache
self.db_manager = bot.db_manager
def can_execute(self, message: MeshMessage, skip_channel_check: bool = False) -> bool:
"""Check if this command can be executed with the given message.
Args:
message: The message triggering the command.
Returns:
bool: True if command is enabled and checks pass, False otherwise.
"""
if not self.solarforecast_enabled:
return False
return super().can_execute(message)
def get_help_text(self) -> str:
return self.translate('commands.solarforecast.usage')
+13 -1
View File
@@ -30,7 +30,7 @@ from ..utils import (
format_temperature_high_low,
geocode_city_sync,
geocode_zipcode_sync,
get_nominatim_geocoder, # noqa: F401
get_nominatim_geocoder,
normalize_us_state,
)
from ..weather_common import WeatherCommandMixin
@@ -38,6 +38,10 @@ from .alternatives.wx_international import GlobalWxCommand
from .base_command import BaseCommand
from .rain_command import nws_http_means_no_coverage
# Kept for code that checked them; these imports used to be optional.
WX_INTERNATIONAL_AVAILABLE = True
WXSIM_PARSER_AVAILABLE = True
# Multiday: plain digits (e.g. 7), 7day/7-day, or suffix form 7d/10d (min 2, max below).
WX_MULTIDAY_MAX_DAYS = 16
@@ -239,12 +243,20 @@ class WxCommand(WeatherCommandMixin, BaseCommand):
# Only initialize NOAA-specific attributes if not delegating
if self.delegate_command is None:
self.url_timeout = 8 # seconds (reduced from 10 for faster failure detection)
self.forecast_duration = 3 # days
self.num_wx_alerts = 2 # number of alerts to show
self.use_metric = False # Use imperial units by default
self.zulu_time = False # Use local time by default
# Get default location/state/country from config for fallback/disambiguation
self.default_city = self.bot.config.get('Weather', 'default_city', fallback='').strip()
self.default_state = self.bot.config.get('Weather', 'default_state', fallback='')
self.default_country = self.bot.config.get('Weather', 'default_country', fallback='US')
# Initialize geocoder (will use rate-limited helpers for actual calls)
# Keep geolocator for backwards compatibility, but prefer rate-limited helpers
self.geolocator = get_nominatim_geocoder()
# Get database manager for geocoding cache
self.db_manager = bot.db_manager
+5
View File
@@ -112,6 +112,11 @@ class WeatherCommandMixin:
text, err = mqtt_weather_display_for_topic(topic, cache, fmt)
if text is not None:
return f"{location_name}: {text}" if location_name else text
return self._mqtt_weather_error_key(err)
def _mqtt_weather_error_key(self, err: Optional[str]) -> str:
"""The translated reply for an MQTT weather lookup error (overridable)."""
ns = self.translation_ns
if err == "no_cache":
return self.translate(f"{ns}.mqtt_weather_no_subscriber")
if err in ("no_data", "empty_payload", "empty_after_sanitize"):
+53
View File
@@ -0,0 +1,53 @@
"""Names and hooks local plugins may rely on survive the weather refactor."""
import configparser
from types import SimpleNamespace
from unittest.mock import Mock
import modules.commands.alternatives.wx_international as wx_international
import modules.commands.wx_command as wx_command
from modules.commands.alternatives.wx_international import GlobalWxCommand
from modules.commands.solarforecast_command import SolarforecastCommand
from modules.commands.wx_command import WxCommand
def _bot(sections=None):
config = configparser.ConfigParser()
config.read_dict({"Weather": {"weather_provider": "noaa"}, "Wx_Command": {}, "Bot": {}, **(sections or {})})
bot = Mock()
bot.config = config
bot.translator.translate = Mock(side_effect=lambda key, **kwargs: key)
return bot
def test_wx_keeps_its_attributes():
wx = WxCommand(_bot())
assert (wx.forecast_duration, wx.num_wx_alerts, wx.use_metric, wx.zulu_time) == (3, 2, False, False)
assert wx.geolocator is not None
def test_gwx_and_solarforecast_keep_geolocator():
assert GlobalWxCommand(_bot()).geolocator is not None
assert SolarforecastCommand(_bot()).geolocator is not None
def test_availability_flags_still_exist():
assert wx_command.WX_INTERNATIONAL_AVAILABLE is True
assert wx_command.WXSIM_PARSER_AVAILABLE is True
assert wx_international.WXSIM_PARSER_AVAILABLE is True
def test_mqtt_error_text_can_be_overridden():
class LocalWx(WxCommand):
def _mqtt_weather_error_key(self, err):
return f"local:{err}"
wx = LocalWx(_bot())
wx.bot.mqtt_weather_cache = None # no subscriber -> "no_cache"
assert wx._mqtt_weather_line("weather/topic", "default", None) == "local:no_cache"
def test_solarforecast_honours_its_enabled_setting():
cmd = SolarforecastCommand(_bot({"Solarforecast_Command": {"enabled": "false"}}))
message = SimpleNamespace(content="sf Seattle", channel="general", is_dm=True, sender_id="u", sender_pubkey="pk")
assert cmd.can_execute(message) is False