feat: v0.3.7 - Weather with location name and PLZ support

Show city name via reverse geocoding in weather response. Allow
specifying a German postal code with plz:XXXXX argument.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
ppfeiffer 2026-02-16 17:07:22 +01:00
parent 40f8d17a9f
commit fb4425958f
3 changed files with 73 additions and 13 deletions

View file

@ -1,5 +1,14 @@
# Changelog # Changelog
## [0.3.7] - 2026-02-16
### Added
- Weather-Befehl zeigt Ortsnamen via Reverse-Geocoding (Nominatim)
- Optionales Argument `plz:XXXXX` fuer Wetter nach deutscher Postleitzahl
- Geocoding-Methoden `_geocode_plz()` und `_reverse_geocode()` im Bot
### Changed
- Help-Text zeigt `plz:XXXXX` Option beim Weather-Befehl
## [0.3.6] - 2026-02-15 ## [0.3.6] - 2026-02-15
### Added ### Added
- Node-Einstellungen Seite (`/settings`) zeigt Geraet, LoRa, Channels, Position, Power, Bluetooth/Netzwerk - Node-Einstellungen Seite (`/settings`) zeigt Geraet, LoRa, Channels, Position, Power, Bluetooth/Netzwerk

View file

@ -1,4 +1,4 @@
version: "0.3.6" version: "0.3.7"
bot: bot:
name: "MeshDD-Bot" name: "MeshDD-Bot"

View file

@ -280,13 +280,19 @@ class MeshBot:
f"{prefix}info - Bot-Info\n" f"{prefix}info - Bot-Info\n"
f"{prefix}stats - Statistiken\n" f"{prefix}stats - Statistiken\n"
f"{prefix}uptime - Laufzeit\n" f"{prefix}uptime - Laufzeit\n"
f"{prefix}weather - Wetter\n" f"{prefix}weather [plz:XXXXX] - Wetter\n"
f"{prefix}mesh - Mesh-Netzwerk\n" f"{prefix}mesh - Mesh-Netzwerk\n"
f"{prefix}help - Diese Hilfe" f"{prefix}help - Diese Hilfe"
) )
elif cmd == f"{prefix}weather": elif cmd == f"{prefix}weather":
response = await self._get_weather(from_id) args = text.split()[1:]
plz = None
for arg in args:
if arg.lower().startswith("plz:"):
plz = arg[4:]
break
response = await self._get_weather(from_id, plz=plz)
elif cmd == f"{prefix}stats": elif cmd == f"{prefix}stats":
stats = await self.db.get_stats() stats = await self.db.get_stats()
@ -415,14 +421,23 @@ class MeshBot:
return "\n\n".join(parts) return "\n\n".join(parts)
async def _get_weather(self, from_id: str) -> str: async def _get_weather(self, from_id: str, plz: str | None = None) -> str:
lat, lon = None, None
if plz:
coords = await self._geocode_plz(plz)
if coords:
lat, lon = coords
else:
return f"❌ PLZ {plz} nicht gefunden."
if lat is None:
node = await self.db.get_node(from_id) node = await self.db.get_node(from_id)
fallback = False
if node and node.get("lat") and node.get("lon"): if node and node.get("lat") and node.get("lon"):
lat, lon = node["lat"], node["lon"] lat, lon = node["lat"], node["lon"]
else: else:
lat, lon = 51.0504, 13.7373 # Dresden Zentrum lat, lon = 51.0504, 13.7373 # Dresden Zentrum
fallback = True
url = ( url = (
f"https://api.open-meteo.com/v1/forecast?" f"https://api.open-meteo.com/v1/forecast?"
f"latitude={lat}&longitude={lon}" f"latitude={lat}&longitude={lon}"
@ -439,9 +454,10 @@ class MeshBot:
code = current.get("weather_code", 0) code = current.get("weather_code", 0)
weather_icon = self._weather_code_to_icon(code) weather_icon = self._weather_code_to_icon(code)
location = " (Dresden)" if fallback else "" location = await self._reverse_geocode(lat, lon)
loc_str = f" ({location})" if location else ""
return ( return (
f"{weather_icon} Wetter{location}:\n" f"{weather_icon} Wetter{loc_str}:\n"
f"Temp: {temp}°C\n" f"Temp: {temp}°C\n"
f"Feuchte: {humidity}%\n" f"Feuchte: {humidity}%\n"
f"Wind: {wind} km/h" f"Wind: {wind} km/h"
@ -450,6 +466,41 @@ class MeshBot:
logger.exception("Error fetching weather") logger.exception("Error fetching weather")
return "❌ Wetterdaten konnten nicht abgerufen werden." return "❌ Wetterdaten konnten nicht abgerufen werden."
async def _geocode_plz(self, plz: str) -> tuple[float, float] | None:
url = (
f"https://nominatim.openstreetmap.org/search?"
f"postalcode={plz}&country=DE&format=json&limit=1&accept-language=de"
)
try:
loop = asyncio.get_event_loop()
req = urllib.request.Request(url, headers={"User-Agent": "MeshDD-Bot/1.0"})
data = await loop.run_in_executor(None, self._fetch_request, req)
if data and len(data) > 0:
return float(data[0]["lat"]), float(data[0]["lon"])
except Exception:
logger.debug("Geocode PLZ failed for %s", plz)
return None
async def _reverse_geocode(self, lat: float, lon: float) -> str:
url = (
f"https://nominatim.openstreetmap.org/reverse?"
f"lat={lat}&lon={lon}&format=json&zoom=10&accept-language=de"
)
try:
loop = asyncio.get_event_loop()
req = urllib.request.Request(url, headers={"User-Agent": "MeshDD-Bot/1.0"})
data = await loop.run_in_executor(None, self._fetch_request, req)
addr = data.get("address", {})
return addr.get("city") or addr.get("town") or addr.get("village") or addr.get("municipality") or ""
except Exception:
logger.debug("Reverse geocode failed for %s,%s", lat, lon)
return ""
@staticmethod
def _fetch_request(req: urllib.request.Request) -> dict:
with urllib.request.urlopen(req, timeout=10) as resp:
return json.loads(resp.read().decode())
@staticmethod @staticmethod
def _fetch_url(url: str) -> dict: def _fetch_url(url: str) -> dict:
with urllib.request.urlopen(url, timeout=10) as resp: with urllib.request.urlopen(url, timeout=10) as resp: