mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-06-21 15:01:40 +00:00
ffa5595fff
Builds on the merged rain/snow nowcast (#193): ten enhancements plus end-to-end, proactive, and live-smoke test coverage. All new behavior is config-gated or additive, so existing deployments are unaffected by default. Enhancements - Precip amount estimate "(est 0.2 in)" on the command and the proactive push; snow shown as real depth (Open-Meteo snowfall, cm); freezing rain tagged "in ice". - Bare country / US state resolves to its capital with a heads-up (self-contained modules/region_capitals.py; no pycountry/us dependency). - join_location() dedupes "Spain, Spain" / city-states. - !snow alias + neutral !nowcast; each looks for its own precip family across the window, else falls back with a "No snow, but rain ..." cross-type line. - Keyword-aware help (help rain / help snow). - Precip probability shown "(..., 70%)"; the proactive incoming alert is gated to >= [Weather_Service] rain_nowcast_min_probability (default 50). - Rain<->snow changeover line when the window holds both families. - Borderline temperature tag (30-38F). - Short-lived series cache shared by the command and the proactive poll. Tests - test_rain_command_e2e.py: drives RainCommand.execute() end to end and asserts the exact rendered reply across dry/incoming/raining, snow depth, cross-type mismatch, changeover, ice, temp tag, region capitals, config toggles, DM budget, and keyword-aware help. - test_rain_proactive_e2e.py: drives Weather_Service._check_rain_nowcast() for the probability gate, snow depth, ending notice, and once-per-episode dedup. - test_rain_live_smoke.py: opt-in (RAIN_LIVE_SMOKE=1) live Open-Meteo check for upstream schema drift; skipped in CI. - Shared scaffolding in tests/unit/_rain_harness.py. New config: [Rain_Command] show_amount/amount_unit/show_probability/show_temp/ cache_seconds/zip_city_lookup; [Weather_Service] rain_nowcast_min_probability/ rain_nowcast_cache_seconds/rain_nowcast_show_amount/rain_nowcast_amount_unit. ruff + mypy clean.
152 lines
9.1 KiB
Python
152 lines
9.1 KiB
Python
"""Capital-city lookup for bare country / US-state inputs to the rain command.
|
|
|
|
Self-contained — neither ``pycountry`` nor ``us`` is installed in production, so
|
|
this carries its own data. When a user gives only a region name (e.g. ``!rain
|
|
france``), the rain command resolves to that region's capital and appends a
|
|
short heads-up. Keyed by lowercased name; the value is ``(capital, region)``
|
|
used to build a geocoder query like ``"Paris, France"`` or ``"Austin, TX"``.
|
|
|
|
Lives outside ``modules/commands/`` so the plugin loader (which globs
|
|
``commands/*.py`` for command classes) never tries to load it as a command.
|
|
"""
|
|
from typing import Optional
|
|
|
|
# Short warning appended when a bare region defaults to its capital. ~71 bytes —
|
|
# fits the 160-byte channel budget alongside any nowcast line (verified).
|
|
REGION_DEFAULT_NOTE = "⚠️ no city given — showing capital; try a city for detail"
|
|
|
|
# US state name -> (capital, abbreviation). Keyed by the full state name only;
|
|
# bare 2-letter abbreviations are too ambiguous ("in", "or", "la", "me") to map.
|
|
US_STATE_CAPITALS = {
|
|
"alabama": ("Montgomery", "AL"), "alaska": ("Juneau", "AK"),
|
|
"arizona": ("Phoenix", "AZ"), "arkansas": ("Little Rock", "AR"),
|
|
"california": ("Sacramento", "CA"), "colorado": ("Denver", "CO"),
|
|
"connecticut": ("Hartford", "CT"), "delaware": ("Dover", "DE"),
|
|
"florida": ("Tallahassee", "FL"), "georgia": ("Atlanta", "GA"),
|
|
"hawaii": ("Honolulu", "HI"), "idaho": ("Boise", "ID"),
|
|
"illinois": ("Springfield", "IL"), "indiana": ("Indianapolis", "IN"),
|
|
"iowa": ("Des Moines", "IA"), "kansas": ("Topeka", "KS"),
|
|
"kentucky": ("Frankfort", "KY"), "louisiana": ("Baton Rouge", "LA"),
|
|
"maine": ("Augusta", "ME"), "maryland": ("Annapolis", "MD"),
|
|
"massachusetts": ("Boston", "MA"), "michigan": ("Lansing", "MI"),
|
|
"minnesota": ("Saint Paul", "MN"), "mississippi": ("Jackson", "MS"),
|
|
"missouri": ("Jefferson City", "MO"), "montana": ("Helena", "MT"),
|
|
"nebraska": ("Lincoln", "NE"), "nevada": ("Carson City", "NV"),
|
|
"new hampshire": ("Concord", "NH"), "new jersey": ("Trenton", "NJ"),
|
|
"new mexico": ("Santa Fe", "NM"), "new york": ("Albany", "NY"),
|
|
"north carolina": ("Raleigh", "NC"), "north dakota": ("Bismarck", "ND"),
|
|
"ohio": ("Columbus", "OH"), "oklahoma": ("Oklahoma City", "OK"),
|
|
"oregon": ("Salem", "OR"), "pennsylvania": ("Harrisburg", "PA"),
|
|
"rhode island": ("Providence", "RI"), "south carolina": ("Columbia", "SC"),
|
|
"south dakota": ("Pierre", "SD"), "tennessee": ("Nashville", "TN"),
|
|
"texas": ("Austin", "TX"), "utah": ("Salt Lake City", "UT"),
|
|
"vermont": ("Montpelier", "VT"), "virginia": ("Richmond", "VA"),
|
|
"washington": ("Olympia", "WA"), "west virginia": ("Charleston", "WV"),
|
|
"wisconsin": ("Madison", "WI"), "wyoming": ("Cheyenne", "WY"),
|
|
}
|
|
|
|
# Bare state names that almost always mean the city, not the state. Nominatim
|
|
# resolves these to the city (NYC, Washington DC) on its own, so let them.
|
|
STATE_AS_CITY = {"new york", "washington"}
|
|
|
|
# Country / common-alias -> (capital, country name for the geocoder query).
|
|
WORLD_CAPITALS = {
|
|
# North & Central America
|
|
"united states": ("Washington", "United States"), "usa": ("Washington", "United States"),
|
|
"america": ("Washington", "United States"), "canada": ("Ottawa", "Canada"),
|
|
"mexico": ("Mexico City", "Mexico"), "guatemala": ("Guatemala City", "Guatemala"),
|
|
"cuba": ("Havana", "Cuba"), "jamaica": ("Kingston", "Jamaica"),
|
|
"haiti": ("Port-au-Prince", "Haiti"), "dominican republic": ("Santo Domingo", "Dominican Republic"),
|
|
"panama": ("Panama City", "Panama"), "costa rica": ("San Jose", "Costa Rica"),
|
|
"honduras": ("Tegucigalpa", "Honduras"), "el salvador": ("San Salvador", "El Salvador"),
|
|
"nicaragua": ("Managua", "Nicaragua"), "belize": ("Belmopan", "Belize"),
|
|
# South America
|
|
"colombia": ("Bogota", "Colombia"), "venezuela": ("Caracas", "Venezuela"),
|
|
"ecuador": ("Quito", "Ecuador"), "peru": ("Lima", "Peru"),
|
|
"brazil": ("Brasilia", "Brazil"), "bolivia": ("La Paz", "Bolivia"),
|
|
"chile": ("Santiago", "Chile"), "argentina": ("Buenos Aires", "Argentina"),
|
|
"uruguay": ("Montevideo", "Uruguay"), "paraguay": ("Asuncion", "Paraguay"),
|
|
# Europe
|
|
"france": ("Paris", "France"), "germany": ("Berlin", "Germany"),
|
|
"italy": ("Rome", "Italy"), "spain": ("Madrid", "Spain"),
|
|
"portugal": ("Lisbon", "Portugal"), "united kingdom": ("London", "United Kingdom"),
|
|
"uk": ("London", "United Kingdom"), "britain": ("London", "United Kingdom"),
|
|
"great britain": ("London", "United Kingdom"), "england": ("London", "United Kingdom"),
|
|
"scotland": ("Edinburgh", "United Kingdom"), "wales": ("Cardiff", "United Kingdom"),
|
|
"ireland": ("Dublin", "Ireland"), "netherlands": ("Amsterdam", "Netherlands"),
|
|
"holland": ("Amsterdam", "Netherlands"), "belgium": ("Brussels", "Belgium"),
|
|
"luxembourg": ("Luxembourg", "Luxembourg"), "switzerland": ("Bern", "Switzerland"),
|
|
"austria": ("Vienna", "Austria"), "poland": ("Warsaw", "Poland"),
|
|
"czechia": ("Prague", "Czechia"), "czech republic": ("Prague", "Czechia"),
|
|
"slovakia": ("Bratislava", "Slovakia"), "hungary": ("Budapest", "Hungary"),
|
|
"romania": ("Bucharest", "Romania"), "bulgaria": ("Sofia", "Bulgaria"),
|
|
"greece": ("Athens", "Greece"), "sweden": ("Stockholm", "Sweden"),
|
|
"norway": ("Oslo", "Norway"), "denmark": ("Copenhagen", "Denmark"),
|
|
"finland": ("Helsinki", "Finland"), "iceland": ("Reykjavik", "Iceland"),
|
|
"russia": ("Moscow", "Russia"), "ukraine": ("Kyiv", "Ukraine"),
|
|
"belarus": ("Minsk", "Belarus"), "croatia": ("Zagreb", "Croatia"),
|
|
"serbia": ("Belgrade", "Serbia"), "slovenia": ("Ljubljana", "Slovenia"),
|
|
"albania": ("Tirana", "Albania"), "estonia": ("Tallinn", "Estonia"),
|
|
"latvia": ("Riga", "Latvia"), "lithuania": ("Vilnius", "Lithuania"),
|
|
"monaco": ("Monaco", "Monaco"), "malta": ("Valletta", "Malta"),
|
|
"cyprus": ("Nicosia", "Cyprus"),
|
|
# Middle East & Central Asia
|
|
"turkey": ("Ankara", "Turkey"), "turkiye": ("Ankara", "Turkey"),
|
|
"israel": ("Jerusalem", "Israel"), "jordan": ("Amman", "Jordan"),
|
|
"lebanon": ("Beirut", "Lebanon"), "syria": ("Damascus", "Syria"),
|
|
"iraq": ("Baghdad", "Iraq"), "iran": ("Tehran", "Iran"),
|
|
"saudi arabia": ("Riyadh", "Saudi Arabia"), "uae": ("Abu Dhabi", "United Arab Emirates"),
|
|
"united arab emirates": ("Abu Dhabi", "United Arab Emirates"), "qatar": ("Doha", "Qatar"),
|
|
"kuwait": ("Kuwait City", "Kuwait"), "oman": ("Muscat", "Oman"),
|
|
"yemen": ("Sanaa", "Yemen"), "kazakhstan": ("Astana", "Kazakhstan"),
|
|
"uzbekistan": ("Tashkent", "Uzbekistan"), "afghanistan": ("Kabul", "Afghanistan"),
|
|
# South & East Asia
|
|
"china": ("Beijing", "China"), "japan": ("Tokyo", "Japan"),
|
|
"south korea": ("Seoul", "South Korea"), "korea": ("Seoul", "South Korea"),
|
|
"north korea": ("Pyongyang", "North Korea"), "india": ("New Delhi", "India"),
|
|
"pakistan": ("Islamabad", "Pakistan"), "bangladesh": ("Dhaka", "Bangladesh"),
|
|
"sri lanka": ("Colombo", "Sri Lanka"), "nepal": ("Kathmandu", "Nepal"),
|
|
"thailand": ("Bangkok", "Thailand"), "vietnam": ("Hanoi", "Vietnam"),
|
|
"cambodia": ("Phnom Penh", "Cambodia"), "laos": ("Vientiane", "Laos"),
|
|
"myanmar": ("Naypyidaw", "Myanmar"), "burma": ("Naypyidaw", "Myanmar"),
|
|
"malaysia": ("Kuala Lumpur", "Malaysia"), "singapore": ("Singapore", "Singapore"),
|
|
"indonesia": ("Jakarta", "Indonesia"), "philippines": ("Manila", "Philippines"),
|
|
"mongolia": ("Ulaanbaatar", "Mongolia"), "taiwan": ("Taipei", "Taiwan"),
|
|
# Africa
|
|
"egypt": ("Cairo", "Egypt"), "morocco": ("Rabat", "Morocco"),
|
|
"algeria": ("Algiers", "Algeria"), "tunisia": ("Tunis", "Tunisia"),
|
|
"libya": ("Tripoli", "Libya"), "nigeria": ("Abuja", "Nigeria"),
|
|
"ghana": ("Accra", "Ghana"), "kenya": ("Nairobi", "Kenya"),
|
|
"ethiopia": ("Addis Ababa", "Ethiopia"), "tanzania": ("Dodoma", "Tanzania"),
|
|
"uganda": ("Kampala", "Uganda"), "south africa": ("Pretoria", "South Africa"),
|
|
"zimbabwe": ("Harare", "Zimbabwe"), "zambia": ("Lusaka", "Zambia"),
|
|
"angola": ("Luanda", "Angola"), "senegal": ("Dakar", "Senegal"),
|
|
"cameroon": ("Yaounde", "Cameroon"), "sudan": ("Khartoum", "Sudan"),
|
|
# Oceania
|
|
"australia": ("Canberra", "Australia"), "new zealand": ("Wellington", "New Zealand"),
|
|
"fiji": ("Suva", "Fiji"), "papua new guinea": ("Port Moresby", "Papua New Guinea"),
|
|
}
|
|
|
|
|
|
def region_capital_query(location: Optional[str]) -> Optional[str]:
|
|
"""Return a ``"Capital, Region"`` geocoder query for a bare country or US
|
|
state, else ``None``.
|
|
|
|
Case-insensitive and whitespace-normalized. A comma in the input means the
|
|
user already qualified a city ("Paris, France"), so it's not a bare region.
|
|
City-dominant state names (New York, Washington) are excluded so they keep
|
|
resolving to the city.
|
|
"""
|
|
if not location:
|
|
return None
|
|
key = " ".join(location.strip().lower().split())
|
|
if not key or "," in location:
|
|
return None
|
|
if key in US_STATE_CAPITALS and key not in STATE_AS_CITY:
|
|
capital, abbr = US_STATE_CAPITALS[key]
|
|
return f"{capital}, {abbr}"
|
|
if key in WORLD_CAPITALS:
|
|
capital, country = WORLD_CAPITALS[key]
|
|
return f"{capital}, {country}"
|
|
return None
|