mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-06-06 07:41:21 +00:00
feat(weather): add default city configuration for wx commands
- Introduced a new configuration option `default_city` in `config.ini.example` for fallback city names when no location is provided. - Updated `wx_command.py` and `wx_international.py` to utilize the `default_city` for location disambiguation, enhancing user experience when companion location is unavailable. - Improved logging to reflect the use of default city or bot location as fallback options.
This commit is contained in:
@@ -656,6 +656,10 @@ weather_provider = noaa
|
||||
# Set to an empty value (weather_model =) to omit model selection and let Open-Meteo auto-select
|
||||
#weather_model =
|
||||
|
||||
# Default city name for empty "wx" / "gwx" commands when companion location is unavailable
|
||||
# Use English city names (examples: Seattle or Seattle, WA)
|
||||
#default_city =
|
||||
|
||||
# Default state for city name disambiguation
|
||||
# When users type "wx seattle", it will search for "seattle, WA, USA"
|
||||
# Use 2-letter state abbreviation (e.g., WA, CA, NY, TX)
|
||||
|
||||
@@ -80,7 +80,8 @@ class GlobalWxCommand(BaseCommand):
|
||||
|
||||
self.weather_model = self._load_weather_model()
|
||||
|
||||
# Get default state and country from config for city disambiguation
|
||||
# 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')
|
||||
|
||||
@@ -460,36 +461,47 @@ class GlobalWxCommand(BaseCommand):
|
||||
parts = [parts[0], location_str]
|
||||
self.logger.info(f"Using companion coordinates: {location_str}")
|
||||
else:
|
||||
# No companion location: optionally use bot's configured coordinates
|
||||
use_bot = self.get_config_value(
|
||||
'Wx_Command',
|
||||
'use_bot_location_when_no_location',
|
||||
fallback=False,
|
||||
value_type='bool',
|
||||
)
|
||||
bot_loc = self._get_bot_location() if use_bot else None
|
||||
if bot_loc:
|
||||
location_str = self._coordinates_to_location_string(bot_loc[0], bot_loc[1])
|
||||
if location_str:
|
||||
parts = [parts[0], location_str]
|
||||
self.logger.info(
|
||||
f"Using bot location (no args): {location_str} "
|
||||
f"({bot_loc[0]}, {bot_loc[1]})"
|
||||
)
|
||||
else:
|
||||
location_str = f"{bot_loc[0]},{bot_loc[1]}"
|
||||
parts = [parts[0], location_str]
|
||||
self.logger.info(f"Using bot coordinates (no args): {location_str}")
|
||||
# No companion location: use default city if configured, then bot location fallback
|
||||
if self.default_city:
|
||||
location_parts = [self.default_city]
|
||||
if self.default_state:
|
||||
location_parts.append(self.default_state)
|
||||
if self.default_country:
|
||||
location_parts.append(self.default_country)
|
||||
location_str = ", ".join(location_parts)
|
||||
parts = [parts[0], location_str]
|
||||
self.logger.info(f"Using default city (no args): {location_str}")
|
||||
else:
|
||||
if use_bot:
|
||||
self.logger.debug(
|
||||
"use_bot_location_when_no_location enabled but bot_latitude/bot_longitude "
|
||||
"not set; showing usage"
|
||||
)
|
||||
# No default city: optionally use bot's configured coordinates
|
||||
use_bot = self.get_config_value(
|
||||
'Wx_Command',
|
||||
'use_bot_location_when_no_location',
|
||||
fallback=False,
|
||||
value_type='bool',
|
||||
)
|
||||
bot_loc = self._get_bot_location() if use_bot else None
|
||||
if bot_loc:
|
||||
location_str = self._coordinates_to_location_string(bot_loc[0], bot_loc[1])
|
||||
if location_str:
|
||||
parts = [parts[0], location_str]
|
||||
self.logger.info(
|
||||
f"Using bot location (no args): {location_str} "
|
||||
f"({bot_loc[0]}, {bot_loc[1]})"
|
||||
)
|
||||
else:
|
||||
location_str = f"{bot_loc[0]},{bot_loc[1]}"
|
||||
parts = [parts[0], location_str]
|
||||
self.logger.info(f"Using bot coordinates (no args): {location_str}")
|
||||
else:
|
||||
self.logger.debug("No companion location found, showing usage")
|
||||
await self.send_response(message, self.translate('commands.gwx.usage'))
|
||||
return True
|
||||
if use_bot:
|
||||
self.logger.debug(
|
||||
"use_bot_location_when_no_location enabled but bot_latitude/bot_longitude "
|
||||
"not set; showing usage"
|
||||
)
|
||||
else:
|
||||
self.logger.debug("No companion/default city location found, showing usage")
|
||||
await self.send_response(message, self.translate('commands.gwx.usage'))
|
||||
return True
|
||||
|
||||
# Check for forecast type options: "tomorrow", Nd (7d, 10d), or plain digit days 2–GWX_MULTIDAY_MAX_DAYS
|
||||
forecast_type = "default"
|
||||
|
||||
@@ -105,7 +105,8 @@ class WxCommand(BaseCommand):
|
||||
self.use_metric = False # Use imperial units by default
|
||||
self.zulu_time = False # Use local time by default
|
||||
|
||||
# Get default state and country from config for city disambiguation
|
||||
# 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')
|
||||
|
||||
@@ -527,34 +528,45 @@ class WxCommand(BaseCommand):
|
||||
else:
|
||||
self.logger.info(f"Using companion coordinates: {location_str}")
|
||||
else:
|
||||
# No companion location: optionally use bot's configured coordinates
|
||||
use_bot = self.get_config_value(
|
||||
'Wx_Command',
|
||||
'use_bot_location_when_no_location',
|
||||
fallback=False,
|
||||
value_type='bool',
|
||||
)
|
||||
bot_loc = self._get_bot_location() if use_bot else None
|
||||
if bot_loc:
|
||||
location_str = f"{bot_loc[0]},{bot_loc[1]}"
|
||||
# No companion location: use default city if configured, then bot location fallback
|
||||
if self.default_city:
|
||||
location_parts = [self.default_city]
|
||||
if self.default_state:
|
||||
location_parts.append(self.default_state)
|
||||
if self.default_country:
|
||||
location_parts.append(self.default_country)
|
||||
location_str = ", ".join(location_parts)
|
||||
parts = [parts[0], location_str]
|
||||
display_name = self._coordinates_to_location_string(bot_loc[0], bot_loc[1])
|
||||
if display_name:
|
||||
self.logger.info(
|
||||
f"Using bot location (no args): {display_name} ({bot_loc[0]}, {bot_loc[1]})"
|
||||
)
|
||||
else:
|
||||
self.logger.info(f"Using bot coordinates (no args): {location_str}")
|
||||
self.logger.info(f"Using default city (no args): {location_str}")
|
||||
else:
|
||||
if use_bot:
|
||||
self.logger.debug(
|
||||
"use_bot_location_when_no_location enabled but bot_latitude/bot_longitude "
|
||||
"not set; showing usage"
|
||||
)
|
||||
# No default city: optionally use bot's configured coordinates
|
||||
use_bot = self.get_config_value(
|
||||
'Wx_Command',
|
||||
'use_bot_location_when_no_location',
|
||||
fallback=False,
|
||||
value_type='bool',
|
||||
)
|
||||
bot_loc = self._get_bot_location() if use_bot else None
|
||||
if bot_loc:
|
||||
location_str = f"{bot_loc[0]},{bot_loc[1]}"
|
||||
parts = [parts[0], location_str]
|
||||
display_name = self._coordinates_to_location_string(bot_loc[0], bot_loc[1])
|
||||
if display_name:
|
||||
self.logger.info(
|
||||
f"Using bot location (no args): {display_name} ({bot_loc[0]}, {bot_loc[1]})"
|
||||
)
|
||||
else:
|
||||
self.logger.info(f"Using bot coordinates (no args): {location_str}")
|
||||
else:
|
||||
self.logger.debug("No companion location found, showing usage")
|
||||
await self.send_response(message, self.translate('commands.wx.usage'))
|
||||
return True
|
||||
if use_bot:
|
||||
self.logger.debug(
|
||||
"use_bot_location_when_no_location enabled but bot_latitude/bot_longitude "
|
||||
"not set; showing usage"
|
||||
)
|
||||
else:
|
||||
self.logger.debug("No companion/default city location found, showing usage")
|
||||
await self.send_response(message, self.translate('commands.wx.usage'))
|
||||
return True
|
||||
|
||||
# Check for "alerts" keyword first (special handling)
|
||||
show_full_alerts = False
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Unit tests for default_city fallback ordering in wx and gwx commands."""
|
||||
|
||||
import asyncio
|
||||
import configparser
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, Mock
|
||||
|
||||
from modules.commands.alternatives.wx_international import GlobalWxCommand
|
||||
from modules.commands.wx_command import WxCommand
|
||||
|
||||
|
||||
def _build_config(default_city: str = "", use_bot: bool = False) -> configparser.ConfigParser:
|
||||
config = configparser.ConfigParser()
|
||||
config.add_section("Weather")
|
||||
config.set("Weather", "weather_provider", "noaa")
|
||||
config.set("Weather", "default_city", default_city)
|
||||
config.set("Weather", "default_state", "WA")
|
||||
config.set("Weather", "default_country", "US")
|
||||
config.add_section("Wx_Command")
|
||||
config.set(
|
||||
"Wx_Command",
|
||||
"use_bot_location_when_no_location",
|
||||
"true" if use_bot else "false",
|
||||
)
|
||||
config.add_section("Bot")
|
||||
config.set("Bot", "bot_tx_rate_limit_seconds", "1.0")
|
||||
return config
|
||||
|
||||
|
||||
def _build_bot(config: configparser.ConfigParser, mock_logger):
|
||||
bot = Mock()
|
||||
bot.logger = mock_logger
|
||||
bot.config = config
|
||||
bot.db_manager = Mock()
|
||||
bot.translator = Mock()
|
||||
bot.translator.translate = Mock(side_effect=lambda key, **kwargs: key)
|
||||
bot.command_manager = Mock()
|
||||
bot.command_manager.monitor_channels = ["general"]
|
||||
bot.command_manager.send_response = AsyncMock(return_value=True)
|
||||
return bot
|
||||
|
||||
|
||||
def _mock_message(content: str) -> SimpleNamespace:
|
||||
return SimpleNamespace(
|
||||
content=content,
|
||||
sender_id="user1",
|
||||
sender_pubkey="pubkey1",
|
||||
channel="general",
|
||||
is_dm=False,
|
||||
)
|
||||
|
||||
|
||||
def _stub_shared_paths(cmd):
|
||||
cmd._get_custom_mqtt_weather_topic = Mock(return_value=None)
|
||||
cmd._get_custom_wxsim_source = Mock(return_value=None)
|
||||
cmd.send_response = AsyncMock(return_value=True)
|
||||
cmd.record_execution = Mock()
|
||||
cmd.translate = Mock(side_effect=lambda key, **kwargs: key)
|
||||
|
||||
|
||||
def test_gwx_empty_prefers_companion_over_default_city(mock_logger):
|
||||
cmd = GlobalWxCommand(_build_bot(_build_config(default_city="Seattle"), mock_logger))
|
||||
_stub_shared_paths(cmd)
|
||||
cmd._get_companion_location = Mock(return_value=(1, 2))
|
||||
cmd._coordinates_to_location_string = Mock(return_value=None)
|
||||
cmd.get_weather_for_location = AsyncMock(return_value="ok")
|
||||
|
||||
asyncio.run(cmd.execute(_mock_message("gwx")))
|
||||
|
||||
assert cmd.get_weather_for_location.await_count == 1
|
||||
assert cmd.get_weather_for_location.await_args.args[0] == "1,2"
|
||||
|
||||
|
||||
def test_gwx_empty_uses_default_city_when_no_companion(mock_logger):
|
||||
cmd = GlobalWxCommand(_build_bot(_build_config(default_city="Seattle"), mock_logger))
|
||||
_stub_shared_paths(cmd)
|
||||
cmd._get_companion_location = Mock(return_value=None)
|
||||
cmd.get_weather_for_location = AsyncMock(return_value="ok")
|
||||
|
||||
asyncio.run(cmd.execute(_mock_message("gwx")))
|
||||
|
||||
assert cmd.get_weather_for_location.await_count == 1
|
||||
assert cmd.get_weather_for_location.await_args.args[0] == "Seattle, WA, US"
|
||||
|
||||
|
||||
def test_gwx_empty_falls_back_to_bot_location_when_default_city_missing(mock_logger):
|
||||
cmd = GlobalWxCommand(_build_bot(_build_config(default_city="", use_bot=True), mock_logger))
|
||||
_stub_shared_paths(cmd)
|
||||
cmd._get_companion_location = Mock(return_value=None)
|
||||
cmd._get_bot_location = Mock(return_value=(47, -122))
|
||||
cmd._coordinates_to_location_string = Mock(return_value=None)
|
||||
cmd.get_weather_for_location = AsyncMock(return_value="ok")
|
||||
|
||||
asyncio.run(cmd.execute(_mock_message("gwx")))
|
||||
|
||||
assert cmd.get_weather_for_location.await_count == 1
|
||||
assert cmd.get_weather_for_location.await_args.args[0] == "47,-122"
|
||||
|
||||
|
||||
def test_gwx_empty_shows_usage_without_any_fallback(mock_logger):
|
||||
cmd = GlobalWxCommand(_build_bot(_build_config(default_city="", use_bot=False), mock_logger))
|
||||
_stub_shared_paths(cmd)
|
||||
cmd._get_companion_location = Mock(return_value=None)
|
||||
cmd.get_weather_for_location = AsyncMock(return_value="ok")
|
||||
|
||||
asyncio.run(cmd.execute(_mock_message("gwx")))
|
||||
|
||||
assert cmd.get_weather_for_location.await_count == 0
|
||||
assert cmd.send_response.await_count == 1
|
||||
assert cmd.send_response.await_args.args[1] == "commands.gwx.usage"
|
||||
|
||||
|
||||
def test_wx_empty_prefers_companion_over_default_city(mock_logger):
|
||||
cmd = WxCommand(_build_bot(_build_config(default_city="Seattle"), mock_logger))
|
||||
_stub_shared_paths(cmd)
|
||||
cmd._get_companion_location = Mock(return_value=(1, 2))
|
||||
cmd._coordinates_to_location_string = Mock(return_value=None)
|
||||
cmd.get_weather_for_location = AsyncMock(return_value="ok")
|
||||
|
||||
asyncio.run(cmd.execute(_mock_message("wx")))
|
||||
|
||||
assert cmd.get_weather_for_location.await_count == 1
|
||||
assert cmd.get_weather_for_location.await_args.args[0] == "1,2"
|
||||
assert cmd.get_weather_for_location.await_args.kwargs["using_companion_location"] is True
|
||||
|
||||
|
||||
def test_wx_empty_uses_default_city_when_no_companion(mock_logger):
|
||||
cmd = WxCommand(_build_bot(_build_config(default_city="Seattle"), mock_logger))
|
||||
_stub_shared_paths(cmd)
|
||||
cmd._get_companion_location = Mock(return_value=None)
|
||||
cmd.get_weather_for_location = AsyncMock(return_value="ok")
|
||||
|
||||
asyncio.run(cmd.execute(_mock_message("wx")))
|
||||
|
||||
assert cmd.get_weather_for_location.await_count == 1
|
||||
assert cmd.get_weather_for_location.await_args.args[0] == "Seattle, WA, US"
|
||||
assert cmd.get_weather_for_location.await_args.kwargs["using_companion_location"] is False
|
||||
|
||||
|
||||
def test_wx_empty_falls_back_to_bot_location_when_default_city_missing(mock_logger):
|
||||
cmd = WxCommand(_build_bot(_build_config(default_city="", use_bot=True), mock_logger))
|
||||
_stub_shared_paths(cmd)
|
||||
cmd._get_companion_location = Mock(return_value=None)
|
||||
cmd._get_bot_location = Mock(return_value=(47, -122))
|
||||
cmd._coordinates_to_location_string = Mock(return_value=None)
|
||||
cmd.get_weather_for_location = AsyncMock(return_value="ok")
|
||||
|
||||
asyncio.run(cmd.execute(_mock_message("wx")))
|
||||
|
||||
assert cmd.get_weather_for_location.await_count == 1
|
||||
assert cmd.get_weather_for_location.await_args.args[0] == "47,-122"
|
||||
assert cmd.get_weather_for_location.await_args.kwargs["using_companion_location"] is False
|
||||
|
||||
|
||||
def test_wx_empty_shows_usage_without_any_fallback(mock_logger):
|
||||
cmd = WxCommand(_build_bot(_build_config(default_city="", use_bot=False), mock_logger))
|
||||
_stub_shared_paths(cmd)
|
||||
cmd._get_companion_location = Mock(return_value=None)
|
||||
cmd.get_weather_for_location = AsyncMock(return_value="ok")
|
||||
|
||||
asyncio.run(cmd.execute(_mock_message("wx")))
|
||||
|
||||
assert cmd.get_weather_for_location.await_count == 0
|
||||
assert cmd.send_response.await_count == 1
|
||||
assert cmd.send_response.await_args.args[1] == "commands.wx.usage"
|
||||
Reference in New Issue
Block a user