diff --git a/CHANGELOG.md b/CHANGELOG.md index 30f251a..a9bd791 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # 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 ### Added - Node-Einstellungen Seite (`/settings`) zeigt Geraet, LoRa, Channels, Position, Power, Bluetooth/Netzwerk diff --git a/config.yaml b/config.yaml index 74029b8..398066d 100644 --- a/config.yaml +++ b/config.yaml @@ -1,4 +1,4 @@ -version: "0.3.6" +version: "0.3.7" bot: name: "MeshDD-Bot" diff --git a/meshbot/bot.py b/meshbot/bot.py index 461cd43..e185544 100644 --- a/meshbot/bot.py +++ b/meshbot/bot.py @@ -280,13 +280,19 @@ class MeshBot: f"{prefix}info - Bot-Info\n" f"{prefix}stats - Statistiken\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}help - Diese Hilfe" ) 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": stats = await self.db.get_stats() @@ -415,14 +421,23 @@ class MeshBot: return "\n\n".join(parts) - async def _get_weather(self, from_id: str) -> str: - node = await self.db.get_node(from_id) - fallback = False - if node and node.get("lat") and node.get("lon"): - lat, lon = node["lat"], node["lon"] - else: - lat, lon = 51.0504, 13.7373 # Dresden Zentrum - fallback = True + 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) + if node and node.get("lat") and node.get("lon"): + lat, lon = node["lat"], node["lon"] + else: + lat, lon = 51.0504, 13.7373 # Dresden Zentrum + url = ( f"https://api.open-meteo.com/v1/forecast?" f"latitude={lat}&longitude={lon}" @@ -439,9 +454,10 @@ class MeshBot: code = current.get("weather_code", 0) 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 ( - f"{weather_icon} Wetter{location}:\n" + f"{weather_icon} Wetter{loc_str}:\n" f"Temp: {temp}°C\n" f"Feuchte: {humidity}%\n" f"Wind: {wind} km/h" @@ -450,6 +466,41 @@ class MeshBot: logger.exception("Error fetching weather") 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 def _fetch_url(url: str) -> dict: with urllib.request.urlopen(url, timeout=10) as resp: