mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-10-28 08:42:25 +00:00
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:
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
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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
|
||||
Reference in new issue
Block a user