From 57030bc86522b9ffd58a6b73e558616b733d842e Mon Sep 17 00:00:00 2001 From: Mikael Roos Date: Sun, 23 Aug 2026 23:54:03 +0200 Subject: [PATCH 01/15] Add wind and short-term forecast to marvinWeather MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fetches wind direction/speed (SMHI metobs parameters 3/4) alongside the existing temperature and present-weather observation, and appends a short forecast for the coming hours using SMHI's new snow1g point forecast API (the old pmp3g forecast API was shut down 2026-03-31). Also drops the "Inget signifikant väder observerat" sentence from the output since it never added information - the observation sentence is now only shown when there's an actual, meaningful observation. --- irc2phpbb/data/marvin_strings.json | 34 +++++++++ irc2phpbb/marvin_actions.py | 76 +++++++++++++++---- .../resources/weather/codes_significant.json | 12 +++ tests/resources/weather/forecast.json | 34 +++++++++ .../weather/station_significant.json | 40 ++++++++++ tests/resources/weather/winddirection.json | 40 ++++++++++ tests/resources/weather/windspeed.json | 40 ++++++++++ tests/test_weather.py | 29 ++++++- 8 files changed, 288 insertions(+), 17 deletions(-) create mode 100644 tests/resources/weather/codes_significant.json create mode 100644 tests/resources/weather/forecast.json create mode 100644 tests/resources/weather/station_significant.json create mode 100644 tests/resources/weather/winddirection.json create mode 100644 tests/resources/weather/windspeed.json diff --git a/irc2phpbb/data/marvin_strings.json b/irc2phpbb/data/marvin_strings.json index 9797511..317fab5 100644 --- a/irc2phpbb/data/marvin_strings.json +++ b/irc2phpbb/data/marvin_strings.json @@ -208,6 +208,40 @@ "station_url": "https://opendata-download-metobs.smhi.se/api/version/1.0/parameter/13/station/65090/period/latest-hour/data.json", "current_weather_url": "https://opendata-download-metobs.smhi.se/api/version/1.0/parameter/1/station/65090/period/latest-hour/data.json", "weather_codes_url": "https://opendata-download-metobs.smhi.se/api/version/1.0/parameter/13/codes.json", + "wind_direction_url": "https://opendata-download-metobs.smhi.se/api/version/1.0/parameter/3/station/65090/period/latest-hour/data.json", + "wind_speed_url": "https://opendata-download-metobs.smhi.se/api/version/1.0/parameter/4/station/65090/period/latest-hour/data.json", + "forecast_url": "https://opendata-download-metfcst.smhi.se/api/category/snow1g/version/1/geotype/point/lon/15.589/lat/56.15/data.json?parameters=air_temperature,symbol_code×eries=3", + "no_significant_weather": "Inget signifikant väder observerat", + "compass": ["N", "NO", "O", "SO", "S", "SV", "V", "NV"], + "symbols": { + "1": "klar himmel", + "2": "nästan klar himmel", + "3": "växlande molnighet", + "4": "halvklart", + "5": "molnigt", + "6": "mulet", + "7": "dimma", + "8": "lätta regnskurar", + "9": "måttliga regnskurar", + "10": "kraftiga regnskurar", + "11": "åskväder", + "12": "lätta skurar av snöblandat regn", + "13": "måttliga skurar av snöblandat regn", + "14": "kraftiga skurar av snöblandat regn", + "15": "lätta snöbyar", + "16": "måttliga snöbyar", + "17": "kraftiga snöbyar", + "18": "lätt regn", + "19": "måttligt regn", + "20": "kraftigt regn", + "21": "åska", + "22": "lätt snöblandat regn", + "23": "måttligt snöblandat regn", + "24": "kraftigt snöblandat regn", + "25": "lätt snöfall", + "26": "måttligt snöfall", + "27": "kraftigt snöfall" + }, "failed": "Något har hänt med SMHI, de svarar inte." }, diff --git a/irc2phpbb/marvin_actions.py b/irc2phpbb/marvin_actions.py index c16920a..97a6399 100644 --- a/irc2phpbb/marvin_actions.py +++ b/irc2phpbb/marvin_actions.py @@ -289,30 +289,62 @@ def marvinSun(row): return msg +def windDirectionToCompass(degrees): + """ + Convert a wind direction in degrees to a compass direction (N, NO, O, ...). + """ + compass = getString("smhi")["compass"] + index = round(degrees / (360 / len(compass))) % len(compass) + return compass[index] + + +def getCurrentWeather(): + """ + Fetch the current temperature, wind and weather observation for Karlskrona. + """ + station_req = requests.get(getString("smhi", "station_url"), timeout=5) + weather_code: int = int(station_req.json().get("value")[0].get("value")) + + weather_codes_req = requests.get(getString("smhi", "weather_codes_url"), timeout=5) + weather_codes_arr: list = weather_codes_req.json().get("entry") + + current_weather_req = requests.get(getString("smhi", "current_weather_url"), timeout=5) + temperature: str = current_weather_req.json().get("value")[0].get("value") + + wind_direction_req = requests.get(getString("smhi", "wind_direction_url"), timeout=5) + wind_direction: float = float(wind_direction_req.json().get("value")[0].get("value")) + + wind_speed_req = requests.get(getString("smhi", "wind_speed_url"), timeout=5) + wind_speed: str = wind_speed_req.json().get("value")[0].get("value") + + observation = "" + for code in weather_codes_arr: + if code.get("key") == weather_code: + observation = code.get("value") + + return temperature, wind_speed, windDirectionToCompass(wind_direction), observation + + def marvinWeather(row): """ Check what the weather prognosis looks like. """ msg = "" if any(r in row for r in ["väder", "vädret", "prognos", "prognosen", "smhi"]): - temperature = "" - observation = "" - try: - station_req = requests.get(getString("smhi", "station_url"), timeout=5) - weather_code: int = int(station_req.json().get("value")[0].get("value")) + temperature, wind_speed, compass_direction, observation = getCurrentWeather() - weather_codes_req = requests.get(getString("smhi", "weather_codes_url"), timeout=5) - weather_codes_arr: list = weather_codes_req.json().get("entry") + parts = [f"Karlskrona just nu: {temperature} °C, vind {wind_speed} m/s " + f"från {compass_direction}."] - current_weather_req = requests.get(getString("smhi", "current_weather_url"), timeout=5) - temperature: str = current_weather_req.json().get("value")[0].get("value") + if observation and observation != getString("smhi", "no_significant_weather"): + parts.append(f"{observation}.") - for code in weather_codes_arr: - if code.get("key") == weather_code: - observation = code.get("value") + forecast = getWeatherForecast() + if forecast: + parts.append(f"Kommande timmar: {forecast}.") - msg = f"Karlskrona just nu: {temperature} °C. {observation}." + msg = " ".join(parts) except Exception as e: LOG.error("Failed to get weather: %s", e) @@ -321,6 +353,24 @@ def marvinWeather(row): return msg +def getWeatherForecast(): + """ + Get a short summary of the weather forecast for the coming hours. + """ + symbols = getString("smhi")["symbols"] + forecast_req = requests.get(getString("smhi", "forecast_url"), timeout=5) + time_series = forecast_req.json().get("timeSeries") + + steps = [] + for step in time_series: + data = step.get("data") + temperature = data.get("air_temperature") + symbol = symbols.get(str(data.get("symbol_code"))) + steps.append(f"{temperature}°C {symbol}") + + return ", ".join(steps) + + def marvinStrip(row): """ Get a comic strip. diff --git a/tests/resources/weather/codes_significant.json b/tests/resources/weather/codes_significant.json new file mode 100644 index 0000000..d516f8c --- /dev/null +++ b/tests/resources/weather/codes_significant.json @@ -0,0 +1,12 @@ +{ + "entry": [ + { + "key": 68, + "value": "Lätt regn" + }, + { + "key": 100, + "value": "Inget signifikant väder observerat" + } + ] +} diff --git a/tests/resources/weather/forecast.json b/tests/resources/weather/forecast.json new file mode 100644 index 0000000..30b6bfe --- /dev/null +++ b/tests/resources/weather/forecast.json @@ -0,0 +1,34 @@ +{ + "createdTime": "2026-08-23T21:15:00Z", + "referenceTime": "2026-08-23T21:15:00Z", + "geometry": { + "type": "Point", + "coordinates": [[15.589, 56.15]] + }, + "timeSeries": [ + { + "time": "2026-08-23T22:00:00Z", + "intervalParametersStartTime": "2026-08-23T21:00:00Z", + "data": { + "air_temperature": 15.8, + "symbol_code": 4 + } + }, + { + "time": "2026-08-23T23:00:00Z", + "intervalParametersStartTime": "2026-08-23T22:00:00Z", + "data": { + "air_temperature": 15.2, + "symbol_code": 5 + } + }, + { + "time": "2026-08-24T00:00:00Z", + "intervalParametersStartTime": "2026-08-23T23:00:00Z", + "data": { + "air_temperature": 14.6, + "symbol_code": 6 + } + } + ] +} diff --git a/tests/resources/weather/station_significant.json b/tests/resources/weather/station_significant.json new file mode 100644 index 0000000..225c728 --- /dev/null +++ b/tests/resources/weather/station_significant.json @@ -0,0 +1,40 @@ +{ + "updated": 1729620000000, + "parameter": { + "key": "13", + "name": "Rådande väder", + "summary": "momentanvärde, 1 gång/tim resp 8 gånger/dygn", + "unit": "kod" + }, + "station": { + "key": "65090", + "name": "Karlskrona-Söderstjerna", + "owner": "Försvarsmakten", + "ownerCategory": "CLIMATE", + "measuringStations": "CORE", + "height": 2.0 + }, + "period": { + "key": "latest-hour", + "from": 1729616401000, + "to": 1729620000000, + "summary": "Data från senaste timmen", + "sampling": "Ej angivet" + }, + "position": [ + { + "from": 1267401600000, + "to": 1729621220000, + "height": 2.4, + "latitude": 56.15, + "longitude": 15.589 + } + ], + "value": [ + { + "date": 1729620000000, + "value": "68", + "quality": "G" + } + ] +} diff --git a/tests/resources/weather/winddirection.json b/tests/resources/weather/winddirection.json new file mode 100644 index 0000000..52e9e0f --- /dev/null +++ b/tests/resources/weather/winddirection.json @@ -0,0 +1,40 @@ +{ + "updated": 1787346000000, + "parameter": { + "key": "3", + "name": "Vindriktning", + "summary": "medelvärde 10 min, 1 gång/tim", + "unit": "grader" + }, + "station": { + "key": "65090", + "name": "Karlskrona-Söderstjerna", + "owner": "Försvarsmakten", + "ownerCategory": "CLIMATE", + "measuringStations": "CORE", + "height": 10.0 + }, + "period": { + "key": "latest-hour", + "from": 1787342401000, + "to": 1787346000000, + "summary": "Data från senaste timmen", + "sampling": "10 minuter" + }, + "position": [ + { + "from": 1267401600000, + "to": 1787347212000, + "height": 10.0, + "latitude": 56.15, + "longitude": 15.589 + } + ], + "value": [ + { + "date": 1787346000000, + "value": "225", + "quality": "G" + } + ] +} diff --git a/tests/resources/weather/windspeed.json b/tests/resources/weather/windspeed.json new file mode 100644 index 0000000..8509b33 --- /dev/null +++ b/tests/resources/weather/windspeed.json @@ -0,0 +1,40 @@ +{ + "updated": 1787346000000, + "parameter": { + "key": "4", + "name": "Vindhastighet", + "summary": "medelvärde 10 min, 1 gång/tim", + "unit": "meter per sekund" + }, + "station": { + "key": "65090", + "name": "Karlskrona-Söderstjerna", + "owner": "Försvarsmakten", + "ownerCategory": "CLIMATE", + "measuringStations": "CORE", + "height": 10.0 + }, + "period": { + "key": "latest-hour", + "from": 1787342401000, + "to": 1787346000000, + "summary": "Data från senaste timmen", + "sampling": "10 minuter" + }, + "position": [ + { + "from": 1267401600000, + "to": 1787347212000, + "height": 10.0, + "latitude": 56.15, + "longitude": 15.589 + } + ], + "value": [ + { + "date": 1787346000000, + "value": "3.2", + "quality": "G" + } + ] +} diff --git a/tests/test_weather.py b/tests/test_weather.py index 6e74304..0238b2d 100644 --- a/tests/test_weather.py +++ b/tests/test_weather.py @@ -20,13 +20,16 @@ def testWeatherRequest(self): self.executeAction(marvin_actions.marvinWeather, "väder") for url in ["https://opendata-download-metobs.smhi.se/api/version/1.0/parameter/13/station/65090/period/latest-hour/data.json", "https://opendata-download-metobs.smhi.se/api/version/1.0/parameter/13/codes.json", - "https://opendata-download-metobs.smhi.se/api/version/1.0/parameter/1/station/65090/period/latest-hour/data.json"]: + "https://opendata-download-metobs.smhi.se/api/version/1.0/parameter/1/station/65090/period/latest-hour/data.json", + "https://opendata-download-metobs.smhi.se/api/version/1.0/parameter/3/station/65090/period/latest-hour/data.json", + "https://opendata-download-metobs.smhi.se/api/version/1.0/parameter/4/station/65090/period/latest-hour/data.json"]: self.assertTrue(mock.call(url, timeout=5) in r.get.call_args_list) def testWeatherResponse(self): - """Test that marvin properly parses weather responses""" + """Test that marvin properly parses weather responses, and skips the observation + sentence when nothing significant was observed""" responses = [] - for responseFile in ["station.json", "codes.json", "weather.json"]: + for responseFile in ["station.json", "codes.json", "weather.json", "winddirection.json", "windspeed.json", "forecast.json"]: path = os.path.join(os.path.dirname(__file__), "resources", "weather", responseFile) with open(path, "r", encoding="UTF-8") as f: response = requests.models.Response() @@ -35,5 +38,23 @@ def testWeatherResponse(self): with mock.patch("irc2phpbb.marvin_actions.requests") as r: r.get.side_effect = responses - expected = "Karlskrona just nu: 16.6 °C. Inget signifikant väder observerat." + expected = ("Karlskrona just nu: 16.6 °C, vind 3.2 m/s från SV. " + "Kommande timmar: 15.8°C halvklart, 15.2°C molnigt, 14.6°C mulet.") + self.assertActionOutput(marvin_actions.marvinWeather, "väder", expected) + + def testWeatherResponseWithSignificantWeather(self): + """Test that the observation sentence is kept when there actually is significant weather""" + responses = [] + for responseFile in ["station_significant.json", "codes_significant.json", "weather.json", + "winddirection.json", "windspeed.json", "forecast.json"]: + path = os.path.join(os.path.dirname(__file__), "resources", "weather", responseFile) + with open(path, "r", encoding="UTF-8") as f: + response = requests.models.Response() + response._content = str.encode(json.dumps(json.load(f))) + responses.append(response) + + with mock.patch("irc2phpbb.marvin_actions.requests") as r: + r.get.side_effect = responses + expected = ("Karlskrona just nu: 16.6 °C, vind 3.2 m/s från SV. Lätt regn. " + "Kommande timmar: 15.8°C halvklart, 15.2°C molnigt, 14.6°C mulet.") self.assertActionOutput(marvin_actions.marvinWeather, "väder", expected) From ded63a45fdb88ebbb1365818cb1987d9c04417d5 Mon Sep 17 00:00:00 2001 From: Mikael Roos Date: Mon, 24 Aug 2026 00:06:05 +0200 Subject: [PATCH 02/15] Add clock times and wind to SMHI forecast steps Each forecast step now shows a local (Europe/Stockholm) clock time and wind direction/speed, instead of just temperature and weather symbol. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Ax5CKcNH5hwbYNJoRGLyAC --- irc2phpbb/data/marvin_strings.json | 2 +- irc2phpbb/marvin_actions.py | 8 +++++++- tests/resources/weather/forecast.json | 12 +++++++++--- tests/test_weather.py | 8 ++++++-- 4 files changed, 23 insertions(+), 7 deletions(-) diff --git a/irc2phpbb/data/marvin_strings.json b/irc2phpbb/data/marvin_strings.json index 317fab5..2b6a407 100644 --- a/irc2phpbb/data/marvin_strings.json +++ b/irc2phpbb/data/marvin_strings.json @@ -210,7 +210,7 @@ "weather_codes_url": "https://opendata-download-metobs.smhi.se/api/version/1.0/parameter/13/codes.json", "wind_direction_url": "https://opendata-download-metobs.smhi.se/api/version/1.0/parameter/3/station/65090/period/latest-hour/data.json", "wind_speed_url": "https://opendata-download-metobs.smhi.se/api/version/1.0/parameter/4/station/65090/period/latest-hour/data.json", - "forecast_url": "https://opendata-download-metfcst.smhi.se/api/category/snow1g/version/1/geotype/point/lon/15.589/lat/56.15/data.json?parameters=air_temperature,symbol_code×eries=3", + "forecast_url": "https://opendata-download-metfcst.smhi.se/api/category/snow1g/version/1/geotype/point/lon/15.589/lat/56.15/data.json?parameters=air_temperature,symbol_code,wind_from_direction,wind_speed×eries=3", "no_significant_weather": "Inget signifikant väder observerat", "compass": ["N", "NO", "O", "SO", "S", "SV", "V", "NV"], "symbols": { diff --git a/irc2phpbb/marvin_actions.py b/irc2phpbb/marvin_actions.py index 97a6399..dfe6b8e 100644 --- a/irc2phpbb/marvin_actions.py +++ b/irc2phpbb/marvin_actions.py @@ -11,6 +11,7 @@ import logging import random import re +import zoneinfo from importlib import resources as impresources @@ -364,9 +365,14 @@ def getWeatherForecast(): steps = [] for step in time_series: data = step.get("data") + time = datetime.datetime.fromisoformat(step.get("time")) + local_time = time.astimezone(zoneinfo.ZoneInfo("Europe/Stockholm")) temperature = data.get("air_temperature") symbol = symbols.get(str(data.get("symbol_code"))) - steps.append(f"{temperature}°C {symbol}") + wind_speed = data.get("wind_speed") + wind_direction = windDirectionToCompass(data.get("wind_from_direction")) + steps.append(f"{local_time:%H:%M} {temperature}°C {symbol}, " + f"vind {wind_speed} m/s från {wind_direction}") return ", ".join(steps) diff --git a/tests/resources/weather/forecast.json b/tests/resources/weather/forecast.json index 30b6bfe..595e3d5 100644 --- a/tests/resources/weather/forecast.json +++ b/tests/resources/weather/forecast.json @@ -11,7 +11,9 @@ "intervalParametersStartTime": "2026-08-23T21:00:00Z", "data": { "air_temperature": 15.8, - "symbol_code": 4 + "symbol_code": 4, + "wind_from_direction": 340, + "wind_speed": 2.6 } }, { @@ -19,7 +21,9 @@ "intervalParametersStartTime": "2026-08-23T22:00:00Z", "data": { "air_temperature": 15.2, - "symbol_code": 5 + "symbol_code": 5, + "wind_from_direction": 320, + "wind_speed": 3.1 } }, { @@ -27,7 +31,9 @@ "intervalParametersStartTime": "2026-08-23T23:00:00Z", "data": { "air_temperature": 14.6, - "symbol_code": 6 + "symbol_code": 6, + "wind_from_direction": 300, + "wind_speed": 3.4 } } ] diff --git a/tests/test_weather.py b/tests/test_weather.py index 0238b2d..104bff3 100644 --- a/tests/test_weather.py +++ b/tests/test_weather.py @@ -39,7 +39,9 @@ def testWeatherResponse(self): with mock.patch("irc2phpbb.marvin_actions.requests") as r: r.get.side_effect = responses expected = ("Karlskrona just nu: 16.6 °C, vind 3.2 m/s från SV. " - "Kommande timmar: 15.8°C halvklart, 15.2°C molnigt, 14.6°C mulet.") + "Kommande timmar: 00:00 15.8°C halvklart, vind 2.6 m/s från N, " + "01:00 15.2°C molnigt, vind 3.1 m/s från NV, " + "02:00 14.6°C mulet, vind 3.4 m/s från NV.") self.assertActionOutput(marvin_actions.marvinWeather, "väder", expected) def testWeatherResponseWithSignificantWeather(self): @@ -56,5 +58,7 @@ def testWeatherResponseWithSignificantWeather(self): with mock.patch("irc2phpbb.marvin_actions.requests") as r: r.get.side_effect = responses expected = ("Karlskrona just nu: 16.6 °C, vind 3.2 m/s från SV. Lätt regn. " - "Kommande timmar: 15.8°C halvklart, 15.2°C molnigt, 14.6°C mulet.") + "Kommande timmar: 00:00 15.8°C halvklart, vind 2.6 m/s från N, " + "01:00 15.2°C molnigt, vind 3.1 m/s från NV, " + "02:00 14.6°C mulet, vind 3.4 m/s från NV.") self.assertActionOutput(marvin_actions.marvinWeather, "väder", expected) From 7e33d071c573024381a991660f2175b031ee60ea Mon Sep 17 00:00:00 2001 From: Mikael Roos Date: Mon, 24 Aug 2026 00:12:05 +0200 Subject: [PATCH 03/15] Shorten wind text in forecast steps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop "vind"/"från" from each step ("2.6 m/s N" instead of "vind 2.6 m/s från N") to reduce the length of an already data-dense line. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Ax5CKcNH5hwbYNJoRGLyAC --- irc2phpbb/marvin_actions.py | 2 +- tests/test_weather.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/irc2phpbb/marvin_actions.py b/irc2phpbb/marvin_actions.py index dfe6b8e..6c5937c 100644 --- a/irc2phpbb/marvin_actions.py +++ b/irc2phpbb/marvin_actions.py @@ -372,7 +372,7 @@ def getWeatherForecast(): wind_speed = data.get("wind_speed") wind_direction = windDirectionToCompass(data.get("wind_from_direction")) steps.append(f"{local_time:%H:%M} {temperature}°C {symbol}, " - f"vind {wind_speed} m/s från {wind_direction}") + f"{wind_speed} m/s {wind_direction}") return ", ".join(steps) diff --git a/tests/test_weather.py b/tests/test_weather.py index 104bff3..1adf26a 100644 --- a/tests/test_weather.py +++ b/tests/test_weather.py @@ -39,9 +39,9 @@ def testWeatherResponse(self): with mock.patch("irc2phpbb.marvin_actions.requests") as r: r.get.side_effect = responses expected = ("Karlskrona just nu: 16.6 °C, vind 3.2 m/s från SV. " - "Kommande timmar: 00:00 15.8°C halvklart, vind 2.6 m/s från N, " - "01:00 15.2°C molnigt, vind 3.1 m/s från NV, " - "02:00 14.6°C mulet, vind 3.4 m/s från NV.") + "Kommande timmar: 00:00 15.8°C halvklart, 2.6 m/s N, " + "01:00 15.2°C molnigt, 3.1 m/s NV, " + "02:00 14.6°C mulet, 3.4 m/s NV.") self.assertActionOutput(marvin_actions.marvinWeather, "väder", expected) def testWeatherResponseWithSignificantWeather(self): @@ -58,7 +58,7 @@ def testWeatherResponseWithSignificantWeather(self): with mock.patch("irc2phpbb.marvin_actions.requests") as r: r.get.side_effect = responses expected = ("Karlskrona just nu: 16.6 °C, vind 3.2 m/s från SV. Lätt regn. " - "Kommande timmar: 00:00 15.8°C halvklart, vind 2.6 m/s från N, " - "01:00 15.2°C molnigt, vind 3.1 m/s från NV, " - "02:00 14.6°C mulet, vind 3.4 m/s från NV.") + "Kommande timmar: 00:00 15.8°C halvklart, 2.6 m/s N, " + "01:00 15.2°C molnigt, 3.1 m/s NV, " + "02:00 14.6°C mulet, 3.4 m/s NV.") self.assertActionOutput(marvin_actions.marvinWeather, "väder", expected) From 5a2984c6a0f15c93d976b19990ab065da259c3c5 Mon Sep 17 00:00:00 2001 From: Mikael Roos Date: Mon, 24 Aug 2026 00:14:27 +0200 Subject: [PATCH 04/15] Shorten SMHI weather symbol names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Trim redundant words ("regnskurar" -> "skurar", "molnighet" -> "molnväxling", drop trailing "regn" from sleet terms) so forecast lines take up less space. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Ax5CKcNH5hwbYNJoRGLyAC --- irc2phpbb/data/marvin_strings.json | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/irc2phpbb/data/marvin_strings.json b/irc2phpbb/data/marvin_strings.json index 2b6a407..d87e857 100644 --- a/irc2phpbb/data/marvin_strings.json +++ b/irc2phpbb/data/marvin_strings.json @@ -214,20 +214,20 @@ "no_significant_weather": "Inget signifikant väder observerat", "compass": ["N", "NO", "O", "SO", "S", "SV", "V", "NV"], "symbols": { - "1": "klar himmel", - "2": "nästan klar himmel", - "3": "växlande molnighet", + "1": "klart", + "2": "nästan klart", + "3": "molnväxling", "4": "halvklart", "5": "molnigt", "6": "mulet", "7": "dimma", - "8": "lätta regnskurar", - "9": "måttliga regnskurar", - "10": "kraftiga regnskurar", + "8": "lätta skurar", + "9": "måttliga skurar", + "10": "kraftiga skurar", "11": "åskväder", - "12": "lätta skurar av snöblandat regn", - "13": "måttliga skurar av snöblandat regn", - "14": "kraftiga skurar av snöblandat regn", + "12": "lätta snöblandade skurar", + "13": "måttliga snöblandade skurar", + "14": "kraftiga snöblandade skurar", "15": "lätta snöbyar", "16": "måttliga snöbyar", "17": "kraftiga snöbyar", @@ -235,9 +235,9 @@ "19": "måttligt regn", "20": "kraftigt regn", "21": "åska", - "22": "lätt snöblandat regn", - "23": "måttligt snöblandat regn", - "24": "kraftigt snöblandat regn", + "22": "lätt snöblandat", + "23": "måttligt snöblandat", + "24": "kraftigt snöblandat", "25": "lätt snöfall", "26": "måttligt snöfall", "27": "kraftigt snöfall" From 4bb7dafda8d695a467b7f3d207c010daa3359c6e Mon Sep 17 00:00:00 2001 From: Mikael Roos Date: Mon, 24 Aug 2026 00:15:15 +0200 Subject: [PATCH 05/15] Drop "just nu" from current weather line The current weather line already only ever shows a single-moment observation, so "just nu" was redundant. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Ax5CKcNH5hwbYNJoRGLyAC --- irc2phpbb/marvin_actions.py | 2 +- tests/test_weather.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/irc2phpbb/marvin_actions.py b/irc2phpbb/marvin_actions.py index 6c5937c..18d6a49 100644 --- a/irc2phpbb/marvin_actions.py +++ b/irc2phpbb/marvin_actions.py @@ -335,7 +335,7 @@ def marvinWeather(row): try: temperature, wind_speed, compass_direction, observation = getCurrentWeather() - parts = [f"Karlskrona just nu: {temperature} °C, vind {wind_speed} m/s " + parts = [f"Karlskrona: {temperature} °C, vind {wind_speed} m/s " f"från {compass_direction}."] if observation and observation != getString("smhi", "no_significant_weather"): diff --git a/tests/test_weather.py b/tests/test_weather.py index 1adf26a..bcb3370 100644 --- a/tests/test_weather.py +++ b/tests/test_weather.py @@ -38,7 +38,7 @@ def testWeatherResponse(self): with mock.patch("irc2phpbb.marvin_actions.requests") as r: r.get.side_effect = responses - expected = ("Karlskrona just nu: 16.6 °C, vind 3.2 m/s från SV. " + expected = ("Karlskrona: 16.6 °C, vind 3.2 m/s från SV. " "Kommande timmar: 00:00 15.8°C halvklart, 2.6 m/s N, " "01:00 15.2°C molnigt, 3.1 m/s NV, " "02:00 14.6°C mulet, 3.4 m/s NV.") @@ -57,7 +57,7 @@ def testWeatherResponseWithSignificantWeather(self): with mock.patch("irc2phpbb.marvin_actions.requests") as r: r.get.side_effect = responses - expected = ("Karlskrona just nu: 16.6 °C, vind 3.2 m/s från SV. Lätt regn. " + expected = ("Karlskrona: 16.6 °C, vind 3.2 m/s från SV. Lätt regn. " "Kommande timmar: 00:00 15.8°C halvklart, 2.6 m/s N, " "01:00 15.2°C molnigt, 3.1 m/s NV, " "02:00 14.6°C mulet, 3.4 m/s NV.") From 8d8a7b267a0f1463cb1a2c7169d387206eb4c5ee Mon Sep 17 00:00:00 2001 From: Mikael Roos Date: Mon, 24 Aug 2026 00:17:05 +0200 Subject: [PATCH 06/15] Show forecast at +4h/+8h instead of every hour, shorten current wind MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two well-spaced points give a better overview than three consecutive hours. Also drop "vind"/"från" from the current weather line's wind text, matching the shorter format already used in the forecast steps. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Ax5CKcNH5hwbYNJoRGLyAC --- irc2phpbb/data/marvin_strings.json | 2 +- irc2phpbb/marvin_actions.py | 9 +++-- tests/resources/weather/forecast.json | 54 ++++++++++++++++++++++++++- tests/test_weather.py | 14 +++---- 4 files changed, 65 insertions(+), 14 deletions(-) diff --git a/irc2phpbb/data/marvin_strings.json b/irc2phpbb/data/marvin_strings.json index d87e857..0ba6a1a 100644 --- a/irc2phpbb/data/marvin_strings.json +++ b/irc2phpbb/data/marvin_strings.json @@ -210,7 +210,7 @@ "weather_codes_url": "https://opendata-download-metobs.smhi.se/api/version/1.0/parameter/13/codes.json", "wind_direction_url": "https://opendata-download-metobs.smhi.se/api/version/1.0/parameter/3/station/65090/period/latest-hour/data.json", "wind_speed_url": "https://opendata-download-metobs.smhi.se/api/version/1.0/parameter/4/station/65090/period/latest-hour/data.json", - "forecast_url": "https://opendata-download-metfcst.smhi.se/api/category/snow1g/version/1/geotype/point/lon/15.589/lat/56.15/data.json?parameters=air_temperature,symbol_code,wind_from_direction,wind_speed×eries=3", + "forecast_url": "https://opendata-download-metfcst.smhi.se/api/category/snow1g/version/1/geotype/point/lon/15.589/lat/56.15/data.json?parameters=air_temperature,symbol_code,wind_from_direction,wind_speed×eries=8", "no_significant_weather": "Inget signifikant väder observerat", "compass": ["N", "NO", "O", "SO", "S", "SV", "V", "NV"], "symbols": { diff --git a/irc2phpbb/marvin_actions.py b/irc2phpbb/marvin_actions.py index 18d6a49..a388a06 100644 --- a/irc2phpbb/marvin_actions.py +++ b/irc2phpbb/marvin_actions.py @@ -335,8 +335,7 @@ def marvinWeather(row): try: temperature, wind_speed, compass_direction, observation = getCurrentWeather() - parts = [f"Karlskrona: {temperature} °C, vind {wind_speed} m/s " - f"från {compass_direction}."] + parts = [f"Karlskrona: {temperature} °C, {wind_speed} m/s {compass_direction}."] if observation and observation != getString("smhi", "no_significant_weather"): parts.append(f"{observation}.") @@ -362,8 +361,12 @@ def getWeatherForecast(): forecast_req = requests.get(getString("smhi", "forecast_url"), timeout=5) time_series = forecast_req.json().get("timeSeries") + # Pick two points a few hours apart instead of showing every hour. + hours_ahead = [3, 7] + selected_steps = [time_series[hour] for hour in hours_ahead if hour < len(time_series)] + steps = [] - for step in time_series: + for step in selected_steps: data = step.get("data") time = datetime.datetime.fromisoformat(step.get("time")) local_time = time.astimezone(zoneinfo.ZoneInfo("Europe/Stockholm")) diff --git a/tests/resources/weather/forecast.json b/tests/resources/weather/forecast.json index 595e3d5..07d9a5e 100644 --- a/tests/resources/weather/forecast.json +++ b/tests/resources/weather/forecast.json @@ -19,6 +19,26 @@ { "time": "2026-08-23T23:00:00Z", "intervalParametersStartTime": "2026-08-23T22:00:00Z", + "data": { + "air_temperature": 15.5, + "symbol_code": 4, + "wind_from_direction": 335, + "wind_speed": 2.8 + } + }, + { + "time": "2026-08-24T00:00:00Z", + "intervalParametersStartTime": "2026-08-23T23:00:00Z", + "data": { + "air_temperature": 15.3, + "symbol_code": 4, + "wind_from_direction": 330, + "wind_speed": 3.0 + } + }, + { + "time": "2026-08-24T01:00:00Z", + "intervalParametersStartTime": "2026-08-24T00:00:00Z", "data": { "air_temperature": 15.2, "symbol_code": 5, @@ -27,8 +47,38 @@ } }, { - "time": "2026-08-24T00:00:00Z", - "intervalParametersStartTime": "2026-08-23T23:00:00Z", + "time": "2026-08-24T02:00:00Z", + "intervalParametersStartTime": "2026-08-24T01:00:00Z", + "data": { + "air_temperature": 15.0, + "symbol_code": 5, + "wind_from_direction": 315, + "wind_speed": 3.2 + } + }, + { + "time": "2026-08-24T03:00:00Z", + "intervalParametersStartTime": "2026-08-24T02:00:00Z", + "data": { + "air_temperature": 14.8, + "symbol_code": 6, + "wind_from_direction": 310, + "wind_speed": 3.3 + } + }, + { + "time": "2026-08-24T04:00:00Z", + "intervalParametersStartTime": "2026-08-24T03:00:00Z", + "data": { + "air_temperature": 14.7, + "symbol_code": 6, + "wind_from_direction": 305, + "wind_speed": 3.3 + } + }, + { + "time": "2026-08-24T05:00:00Z", + "intervalParametersStartTime": "2026-08-24T04:00:00Z", "data": { "air_temperature": 14.6, "symbol_code": 6, diff --git a/tests/test_weather.py b/tests/test_weather.py index bcb3370..3a4bcbf 100644 --- a/tests/test_weather.py +++ b/tests/test_weather.py @@ -38,10 +38,9 @@ def testWeatherResponse(self): with mock.patch("irc2phpbb.marvin_actions.requests") as r: r.get.side_effect = responses - expected = ("Karlskrona: 16.6 °C, vind 3.2 m/s från SV. " - "Kommande timmar: 00:00 15.8°C halvklart, 2.6 m/s N, " - "01:00 15.2°C molnigt, 3.1 m/s NV, " - "02:00 14.6°C mulet, 3.4 m/s NV.") + expected = ("Karlskrona: 16.6 °C, 3.2 m/s SV. " + "Kommande timmar: 03:00 15.2°C molnigt, 3.1 m/s NV, " + "07:00 14.6°C mulet, 3.4 m/s NV.") self.assertActionOutput(marvin_actions.marvinWeather, "väder", expected) def testWeatherResponseWithSignificantWeather(self): @@ -57,8 +56,7 @@ def testWeatherResponseWithSignificantWeather(self): with mock.patch("irc2phpbb.marvin_actions.requests") as r: r.get.side_effect = responses - expected = ("Karlskrona: 16.6 °C, vind 3.2 m/s från SV. Lätt regn. " - "Kommande timmar: 00:00 15.8°C halvklart, 2.6 m/s N, " - "01:00 15.2°C molnigt, 3.1 m/s NV, " - "02:00 14.6°C mulet, 3.4 m/s NV.") + expected = ("Karlskrona: 16.6 °C, 3.2 m/s SV. Lätt regn. " + "Kommande timmar: 03:00 15.2°C molnigt, 3.1 m/s NV, " + "07:00 14.6°C mulet, 3.4 m/s NV.") self.assertActionOutput(marvin_actions.marvinWeather, "väder", expected) From 5374962798eb70f1dea2bde246b400cd359d6736 Mon Sep 17 00:00:00 2001 From: Mikael Roos Date: Mon, 24 Aug 2026 00:19:11 +0200 Subject: [PATCH 07/15] Drop "Kommande timmar" label, separate forecast steps with " - " --- irc2phpbb/marvin_actions.py | 4 ++-- tests/test_weather.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/irc2phpbb/marvin_actions.py b/irc2phpbb/marvin_actions.py index a388a06..c465c28 100644 --- a/irc2phpbb/marvin_actions.py +++ b/irc2phpbb/marvin_actions.py @@ -342,7 +342,7 @@ def marvinWeather(row): forecast = getWeatherForecast() if forecast: - parts.append(f"Kommande timmar: {forecast}.") + parts.append(f"{forecast}.") msg = " ".join(parts) @@ -377,7 +377,7 @@ def getWeatherForecast(): steps.append(f"{local_time:%H:%M} {temperature}°C {symbol}, " f"{wind_speed} m/s {wind_direction}") - return ", ".join(steps) + return " - ".join(steps) def marvinStrip(row): diff --git a/tests/test_weather.py b/tests/test_weather.py index 3a4bcbf..4dfed25 100644 --- a/tests/test_weather.py +++ b/tests/test_weather.py @@ -39,7 +39,7 @@ def testWeatherResponse(self): with mock.patch("irc2phpbb.marvin_actions.requests") as r: r.get.side_effect = responses expected = ("Karlskrona: 16.6 °C, 3.2 m/s SV. " - "Kommande timmar: 03:00 15.2°C molnigt, 3.1 m/s NV, " + "03:00 15.2°C molnigt, 3.1 m/s NV - " "07:00 14.6°C mulet, 3.4 m/s NV.") self.assertActionOutput(marvin_actions.marvinWeather, "väder", expected) @@ -57,6 +57,6 @@ def testWeatherResponseWithSignificantWeather(self): with mock.patch("irc2phpbb.marvin_actions.requests") as r: r.get.side_effect = responses expected = ("Karlskrona: 16.6 °C, 3.2 m/s SV. Lätt regn. " - "Kommande timmar: 03:00 15.2°C molnigt, 3.1 m/s NV, " + "03:00 15.2°C molnigt, 3.1 m/s NV - " "07:00 14.6°C mulet, 3.4 m/s NV.") self.assertActionOutput(marvin_actions.marvinWeather, "väder", expected) From 1a0baa4df055c4b72af677c8e61e0b2667465aaa Mon Sep 17 00:00:00 2001 From: Mikael Roos Date: Mon, 24 Aug 2026 00:21:33 +0200 Subject: [PATCH 08/15] Use an arrow to separate current weather from the forecast MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A "→" between the current conditions and the forecast signals that what follows is upcoming weather, replacing the plain "." there. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Ax5CKcNH5hwbYNJoRGLyAC --- irc2phpbb/marvin_actions.py | 11 +++++------ tests/test_weather.py | 4 ++-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/irc2phpbb/marvin_actions.py b/irc2phpbb/marvin_actions.py index c465c28..13a073a 100644 --- a/irc2phpbb/marvin_actions.py +++ b/irc2phpbb/marvin_actions.py @@ -335,16 +335,15 @@ def marvinWeather(row): try: temperature, wind_speed, compass_direction, observation = getCurrentWeather() - parts = [f"Karlskrona: {temperature} °C, {wind_speed} m/s {compass_direction}."] + parts = [f"Karlskrona: {temperature} °C, {wind_speed} m/s {compass_direction}"] if observation and observation != getString("smhi", "no_significant_weather"): - parts.append(f"{observation}.") + parts.append(observation) - forecast = getWeatherForecast() - if forecast: - parts.append(f"{forecast}.") + current = ". ".join(parts) - msg = " ".join(parts) + forecast = getWeatherForecast() + msg = f"{current} → {forecast}." if forecast else f"{current}." except Exception as e: LOG.error("Failed to get weather: %s", e) diff --git a/tests/test_weather.py b/tests/test_weather.py index 4dfed25..a5daabd 100644 --- a/tests/test_weather.py +++ b/tests/test_weather.py @@ -38,7 +38,7 @@ def testWeatherResponse(self): with mock.patch("irc2phpbb.marvin_actions.requests") as r: r.get.side_effect = responses - expected = ("Karlskrona: 16.6 °C, 3.2 m/s SV. " + expected = ("Karlskrona: 16.6 °C, 3.2 m/s SV → " "03:00 15.2°C molnigt, 3.1 m/s NV - " "07:00 14.6°C mulet, 3.4 m/s NV.") self.assertActionOutput(marvin_actions.marvinWeather, "väder", expected) @@ -56,7 +56,7 @@ def testWeatherResponseWithSignificantWeather(self): with mock.patch("irc2phpbb.marvin_actions.requests") as r: r.get.side_effect = responses - expected = ("Karlskrona: 16.6 °C, 3.2 m/s SV. Lätt regn. " + expected = ("Karlskrona: 16.6 °C, 3.2 m/s SV. Lätt regn → " "03:00 15.2°C molnigt, 3.1 m/s NV - " "07:00 14.6°C mulet, 3.4 m/s NV.") self.assertActionOutput(marvin_actions.marvinWeather, "väder", expected) From d502f3a21ed02ce3620f6258536931ca53a56e80 Mon Sep 17 00:00:00 2001 From: Mikael Roos Date: Mon, 24 Aug 2026 00:22:58 +0200 Subject: [PATCH 09/15] Remove commas from weather output The commas between temperature/wind and symbol/wind didn't add anything, so drop them and rely on spacing alone. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Ax5CKcNH5hwbYNJoRGLyAC --- irc2phpbb/marvin_actions.py | 4 ++-- tests/test_weather.py | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/irc2phpbb/marvin_actions.py b/irc2phpbb/marvin_actions.py index 13a073a..b5f6c2d 100644 --- a/irc2phpbb/marvin_actions.py +++ b/irc2phpbb/marvin_actions.py @@ -335,7 +335,7 @@ def marvinWeather(row): try: temperature, wind_speed, compass_direction, observation = getCurrentWeather() - parts = [f"Karlskrona: {temperature} °C, {wind_speed} m/s {compass_direction}"] + parts = [f"Karlskrona: {temperature} °C {wind_speed} m/s {compass_direction}"] if observation and observation != getString("smhi", "no_significant_weather"): parts.append(observation) @@ -373,7 +373,7 @@ def getWeatherForecast(): symbol = symbols.get(str(data.get("symbol_code"))) wind_speed = data.get("wind_speed") wind_direction = windDirectionToCompass(data.get("wind_from_direction")) - steps.append(f"{local_time:%H:%M} {temperature}°C {symbol}, " + steps.append(f"{local_time:%H:%M} {temperature}°C {symbol} " f"{wind_speed} m/s {wind_direction}") return " - ".join(steps) diff --git a/tests/test_weather.py b/tests/test_weather.py index a5daabd..95a69eb 100644 --- a/tests/test_weather.py +++ b/tests/test_weather.py @@ -38,9 +38,9 @@ def testWeatherResponse(self): with mock.patch("irc2phpbb.marvin_actions.requests") as r: r.get.side_effect = responses - expected = ("Karlskrona: 16.6 °C, 3.2 m/s SV → " - "03:00 15.2°C molnigt, 3.1 m/s NV - " - "07:00 14.6°C mulet, 3.4 m/s NV.") + expected = ("Karlskrona: 16.6 °C 3.2 m/s SV → " + "03:00 15.2°C molnigt 3.1 m/s NV - " + "07:00 14.6°C mulet 3.4 m/s NV.") self.assertActionOutput(marvin_actions.marvinWeather, "väder", expected) def testWeatherResponseWithSignificantWeather(self): @@ -56,7 +56,7 @@ def testWeatherResponseWithSignificantWeather(self): with mock.patch("irc2phpbb.marvin_actions.requests") as r: r.get.side_effect = responses - expected = ("Karlskrona: 16.6 °C, 3.2 m/s SV. Lätt regn → " - "03:00 15.2°C molnigt, 3.1 m/s NV - " - "07:00 14.6°C mulet, 3.4 m/s NV.") + expected = ("Karlskrona: 16.6 °C 3.2 m/s SV. Lätt regn → " + "03:00 15.2°C molnigt 3.1 m/s NV - " + "07:00 14.6°C mulet 3.4 m/s NV.") self.assertActionOutput(marvin_actions.marvinWeather, "väder", expected) From ec4e18d6cd468370e669e60171db93b0d1542462 Mon Sep 17 00:00:00 2001 From: Mikael Roos Date: Mon, 24 Aug 2026 00:25:17 +0200 Subject: [PATCH 10/15] Trim more punctuation from weather output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop the colon after "Karlskrona", the space before the degree sign, and the "C" in "°C" (Celsius is implied for a Swedish audience). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Ax5CKcNH5hwbYNJoRGLyAC --- irc2phpbb/marvin_actions.py | 4 ++-- tests/test_weather.py | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/irc2phpbb/marvin_actions.py b/irc2phpbb/marvin_actions.py index b5f6c2d..84f4c6d 100644 --- a/irc2phpbb/marvin_actions.py +++ b/irc2phpbb/marvin_actions.py @@ -335,7 +335,7 @@ def marvinWeather(row): try: temperature, wind_speed, compass_direction, observation = getCurrentWeather() - parts = [f"Karlskrona: {temperature} °C {wind_speed} m/s {compass_direction}"] + parts = [f"Karlskrona {temperature}° {wind_speed} m/s {compass_direction}"] if observation and observation != getString("smhi", "no_significant_weather"): parts.append(observation) @@ -373,7 +373,7 @@ def getWeatherForecast(): symbol = symbols.get(str(data.get("symbol_code"))) wind_speed = data.get("wind_speed") wind_direction = windDirectionToCompass(data.get("wind_from_direction")) - steps.append(f"{local_time:%H:%M} {temperature}°C {symbol} " + steps.append(f"{local_time:%H:%M} {temperature}° {symbol} " f"{wind_speed} m/s {wind_direction}") return " - ".join(steps) diff --git a/tests/test_weather.py b/tests/test_weather.py index 95a69eb..75185b6 100644 --- a/tests/test_weather.py +++ b/tests/test_weather.py @@ -38,9 +38,9 @@ def testWeatherResponse(self): with mock.patch("irc2phpbb.marvin_actions.requests") as r: r.get.side_effect = responses - expected = ("Karlskrona: 16.6 °C 3.2 m/s SV → " - "03:00 15.2°C molnigt 3.1 m/s NV - " - "07:00 14.6°C mulet 3.4 m/s NV.") + expected = ("Karlskrona 16.6° 3.2 m/s SV → " + "03:00 15.2° molnigt 3.1 m/s NV - " + "07:00 14.6° mulet 3.4 m/s NV.") self.assertActionOutput(marvin_actions.marvinWeather, "väder", expected) def testWeatherResponseWithSignificantWeather(self): @@ -56,7 +56,7 @@ def testWeatherResponseWithSignificantWeather(self): with mock.patch("irc2phpbb.marvin_actions.requests") as r: r.get.side_effect = responses - expected = ("Karlskrona: 16.6 °C 3.2 m/s SV. Lätt regn → " - "03:00 15.2°C molnigt 3.1 m/s NV - " - "07:00 14.6°C mulet 3.4 m/s NV.") + expected = ("Karlskrona 16.6° 3.2 m/s SV. Lätt regn → " + "03:00 15.2° molnigt 3.1 m/s NV - " + "07:00 14.6° mulet 3.4 m/s NV.") self.assertActionOutput(marvin_actions.marvinWeather, "väder", expected) From 8662642ebc89db8d022076fb5c98c11067c6801d Mon Sep 17 00:00:00 2001 From: Mikael Roos Date: Mon, 24 Aug 2026 00:26:42 +0200 Subject: [PATCH 11/15] Replace compass letters with arrow characters for wind direction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit N/NO/O/... become ↑/↗/→/... — a compact compass-rose arrow instead of a two-letter abbreviation. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Ax5CKcNH5hwbYNJoRGLyAC --- irc2phpbb/data/marvin_strings.json | 2 +- irc2phpbb/marvin_actions.py | 2 +- tests/test_weather.py | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/irc2phpbb/data/marvin_strings.json b/irc2phpbb/data/marvin_strings.json index 0ba6a1a..e7ccd5a 100644 --- a/irc2phpbb/data/marvin_strings.json +++ b/irc2phpbb/data/marvin_strings.json @@ -212,7 +212,7 @@ "wind_speed_url": "https://opendata-download-metobs.smhi.se/api/version/1.0/parameter/4/station/65090/period/latest-hour/data.json", "forecast_url": "https://opendata-download-metfcst.smhi.se/api/category/snow1g/version/1/geotype/point/lon/15.589/lat/56.15/data.json?parameters=air_temperature,symbol_code,wind_from_direction,wind_speed×eries=8", "no_significant_weather": "Inget signifikant väder observerat", - "compass": ["N", "NO", "O", "SO", "S", "SV", "V", "NV"], + "compass": ["↑", "↗", "→", "↘", "↓", "↙", "←", "↖"], "symbols": { "1": "klart", "2": "nästan klart", diff --git a/irc2phpbb/marvin_actions.py b/irc2phpbb/marvin_actions.py index 84f4c6d..fcd922f 100644 --- a/irc2phpbb/marvin_actions.py +++ b/irc2phpbb/marvin_actions.py @@ -292,7 +292,7 @@ def marvinSun(row): def windDirectionToCompass(degrees): """ - Convert a wind direction in degrees to a compass direction (N, NO, O, ...). + Convert a wind direction in degrees to a compass arrow (↑, ↗, →, ...). """ compass = getString("smhi")["compass"] index = round(degrees / (360 / len(compass))) % len(compass) diff --git a/tests/test_weather.py b/tests/test_weather.py index 75185b6..f74c762 100644 --- a/tests/test_weather.py +++ b/tests/test_weather.py @@ -38,9 +38,9 @@ def testWeatherResponse(self): with mock.patch("irc2phpbb.marvin_actions.requests") as r: r.get.side_effect = responses - expected = ("Karlskrona 16.6° 3.2 m/s SV → " - "03:00 15.2° molnigt 3.1 m/s NV - " - "07:00 14.6° mulet 3.4 m/s NV.") + expected = ("Karlskrona 16.6° 3.2 m/s ↙ → " + "03:00 15.2° molnigt 3.1 m/s ↖ - " + "07:00 14.6° mulet 3.4 m/s ↖.") self.assertActionOutput(marvin_actions.marvinWeather, "väder", expected) def testWeatherResponseWithSignificantWeather(self): @@ -56,7 +56,7 @@ def testWeatherResponseWithSignificantWeather(self): with mock.patch("irc2phpbb.marvin_actions.requests") as r: r.get.side_effect = responses - expected = ("Karlskrona 16.6° 3.2 m/s SV. Lätt regn → " - "03:00 15.2° molnigt 3.1 m/s NV - " - "07:00 14.6° mulet 3.4 m/s NV.") + expected = ("Karlskrona 16.6° 3.2 m/s ↙. Lätt regn → " + "03:00 15.2° molnigt 3.1 m/s ↖ - " + "07:00 14.6° mulet 3.4 m/s ↖.") self.assertActionOutput(marvin_actions.marvinWeather, "väder", expected) From 60d09e26ef2b2a1573c855adfe034671f16df0bb Mon Sep 17 00:00:00 2001 From: Mikael Roos Date: Mon, 24 Aug 2026 00:30:05 +0200 Subject: [PATCH 12/15] =?UTF-8?q?Use=20"-"=20instead=20of=20"=E2=86=92"=20?= =?UTF-8?q?before=20the=20forecast?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "→" separator clashed visually with the compass arrows now used for wind direction (e.g. an actual east wind renders as "→" too), so switch back to the plain dash already used between forecast steps. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Ax5CKcNH5hwbYNJoRGLyAC --- irc2phpbb/marvin_actions.py | 2 +- tests/test_weather.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/irc2phpbb/marvin_actions.py b/irc2phpbb/marvin_actions.py index fcd922f..1ef6717 100644 --- a/irc2phpbb/marvin_actions.py +++ b/irc2phpbb/marvin_actions.py @@ -343,7 +343,7 @@ def marvinWeather(row): current = ". ".join(parts) forecast = getWeatherForecast() - msg = f"{current} → {forecast}." if forecast else f"{current}." + msg = f"{current} - {forecast}." if forecast else f"{current}." except Exception as e: LOG.error("Failed to get weather: %s", e) diff --git a/tests/test_weather.py b/tests/test_weather.py index f74c762..fef03d0 100644 --- a/tests/test_weather.py +++ b/tests/test_weather.py @@ -38,7 +38,7 @@ def testWeatherResponse(self): with mock.patch("irc2phpbb.marvin_actions.requests") as r: r.get.side_effect = responses - expected = ("Karlskrona 16.6° 3.2 m/s ↙ → " + expected = ("Karlskrona 16.6° 3.2 m/s ↙ - " "03:00 15.2° molnigt 3.1 m/s ↖ - " "07:00 14.6° mulet 3.4 m/s ↖.") self.assertActionOutput(marvin_actions.marvinWeather, "väder", expected) @@ -56,7 +56,7 @@ def testWeatherResponseWithSignificantWeather(self): with mock.patch("irc2phpbb.marvin_actions.requests") as r: r.get.side_effect = responses - expected = ("Karlskrona 16.6° 3.2 m/s ↙. Lätt regn → " + expected = ("Karlskrona 16.6° 3.2 m/s ↙. Lätt regn - " "03:00 15.2° molnigt 3.1 m/s ↖ - " "07:00 14.6° mulet 3.4 m/s ↖.") self.assertActionOutput(marvin_actions.marvinWeather, "väder", expected) From 7add615e645d00608ed8fd7c3b0febcfe44bc76c Mon Sep 17 00:00:00 2001 From: Mikael Roos Date: Mon, 24 Aug 2026 00:31:25 +0200 Subject: [PATCH 13/15] =?UTF-8?q?Use=20"=C2=B7"=20instead=20of=20"-"=20as?= =?UTF-8?q?=20separator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A middle dot reads more cleanly than a plain dash and avoids any resemblance to a minus sign. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Ax5CKcNH5hwbYNJoRGLyAC --- irc2phpbb/marvin_actions.py | 4 ++-- tests/test_weather.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/irc2phpbb/marvin_actions.py b/irc2phpbb/marvin_actions.py index 1ef6717..eaf4522 100644 --- a/irc2phpbb/marvin_actions.py +++ b/irc2phpbb/marvin_actions.py @@ -343,7 +343,7 @@ def marvinWeather(row): current = ". ".join(parts) forecast = getWeatherForecast() - msg = f"{current} - {forecast}." if forecast else f"{current}." + msg = f"{current} · {forecast}." if forecast else f"{current}." except Exception as e: LOG.error("Failed to get weather: %s", e) @@ -376,7 +376,7 @@ def getWeatherForecast(): steps.append(f"{local_time:%H:%M} {temperature}° {symbol} " f"{wind_speed} m/s {wind_direction}") - return " - ".join(steps) + return " · ".join(steps) def marvinStrip(row): diff --git a/tests/test_weather.py b/tests/test_weather.py index fef03d0..f7b70b9 100644 --- a/tests/test_weather.py +++ b/tests/test_weather.py @@ -38,8 +38,8 @@ def testWeatherResponse(self): with mock.patch("irc2phpbb.marvin_actions.requests") as r: r.get.side_effect = responses - expected = ("Karlskrona 16.6° 3.2 m/s ↙ - " - "03:00 15.2° molnigt 3.1 m/s ↖ - " + expected = ("Karlskrona 16.6° 3.2 m/s ↙ · " + "03:00 15.2° molnigt 3.1 m/s ↖ · " "07:00 14.6° mulet 3.4 m/s ↖.") self.assertActionOutput(marvin_actions.marvinWeather, "väder", expected) @@ -56,7 +56,7 @@ def testWeatherResponseWithSignificantWeather(self): with mock.patch("irc2phpbb.marvin_actions.requests") as r: r.get.side_effect = responses - expected = ("Karlskrona 16.6° 3.2 m/s ↙. Lätt regn - " - "03:00 15.2° molnigt 3.1 m/s ↖ - " + expected = ("Karlskrona 16.6° 3.2 m/s ↙. Lätt regn · " + "03:00 15.2° molnigt 3.1 m/s ↖ · " "07:00 14.6° mulet 3.4 m/s ↖.") self.assertActionOutput(marvin_actions.marvinWeather, "väder", expected) From 9a692ec2a88e9eb1ae49cd736a3121baa5682731 Mon Sep 17 00:00:00 2001 From: Mikael Roos Date: Mon, 24 Aug 2026 00:34:14 +0200 Subject: [PATCH 14/15] Clean up getWeatherForecast: clearer naming, hoist ZoneInfo lookup Rename hours_ahead to step_indices since these are positions in the timeSeries list, not literal hour offsets. Build the Europe/Stockholm ZoneInfo once outside the loop instead of on every iteration. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Ax5CKcNH5hwbYNJoRGLyAC --- irc2phpbb/marvin_actions.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/irc2phpbb/marvin_actions.py b/irc2phpbb/marvin_actions.py index eaf4522..9fe270b 100644 --- a/irc2phpbb/marvin_actions.py +++ b/irc2phpbb/marvin_actions.py @@ -361,14 +361,14 @@ def getWeatherForecast(): time_series = forecast_req.json().get("timeSeries") # Pick two points a few hours apart instead of showing every hour. - hours_ahead = [3, 7] - selected_steps = [time_series[hour] for hour in hours_ahead if hour < len(time_series)] + step_indices = [3, 7] + selected_steps = [time_series[i] for i in step_indices if i < len(time_series)] + stockholm = zoneinfo.ZoneInfo("Europe/Stockholm") steps = [] for step in selected_steps: data = step.get("data") - time = datetime.datetime.fromisoformat(step.get("time")) - local_time = time.astimezone(zoneinfo.ZoneInfo("Europe/Stockholm")) + local_time = datetime.datetime.fromisoformat(step.get("time")).astimezone(stockholm) temperature = data.get("air_temperature") symbol = symbols.get(str(data.get("symbol_code"))) wind_speed = data.get("wind_speed") From e637267df5510b739112a6373c35c60462310638 Mon Sep 17 00:00:00 2001 From: Mikael Roos Date: Mon, 24 Aug 2026 00:41:55 +0200 Subject: [PATCH 15/15] Include the current sky symbol in the current weather line MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The metobs "rådande väder" code only reports active precipitation/fog/ thunder, not general sky/cloud condition, so "klart"/"molnigt" etc. never showed up in the current-weather line. Reuse the forecast API's symbol for the nearest hour (time_series[0]) to fill that gap. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Ax5CKcNH5hwbYNJoRGLyAC --- irc2phpbb/marvin_actions.py | 12 ++++++++---- tests/test_weather.py | 4 ++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/irc2phpbb/marvin_actions.py b/irc2phpbb/marvin_actions.py index 9fe270b..d515810 100644 --- a/irc2phpbb/marvin_actions.py +++ b/irc2phpbb/marvin_actions.py @@ -334,15 +334,16 @@ def marvinWeather(row): if any(r in row for r in ["väder", "vädret", "prognos", "prognosen", "smhi"]): try: temperature, wind_speed, compass_direction, observation = getCurrentWeather() + current_symbol, forecast = getWeatherForecast() - parts = [f"Karlskrona {temperature}° {wind_speed} m/s {compass_direction}"] + parts = [f"Karlskrona {temperature}° {current_symbol} " + f"{wind_speed} m/s {compass_direction}"] if observation and observation != getString("smhi", "no_significant_weather"): parts.append(observation) current = ". ".join(parts) - forecast = getWeatherForecast() msg = f"{current} · {forecast}." if forecast else f"{current}." except Exception as e: @@ -354,12 +355,15 @@ def marvinWeather(row): def getWeatherForecast(): """ - Get a short summary of the weather forecast for the coming hours. + Get the current sky symbol plus a short summary of the weather forecast + for the coming hours. """ symbols = getString("smhi")["symbols"] forecast_req = requests.get(getString("smhi", "forecast_url"), timeout=5) time_series = forecast_req.json().get("timeSeries") + current_symbol = symbols.get(str(time_series[0].get("data").get("symbol_code"))) + # Pick two points a few hours apart instead of showing every hour. step_indices = [3, 7] selected_steps = [time_series[i] for i in step_indices if i < len(time_series)] @@ -376,7 +380,7 @@ def getWeatherForecast(): steps.append(f"{local_time:%H:%M} {temperature}° {symbol} " f"{wind_speed} m/s {wind_direction}") - return " · ".join(steps) + return current_symbol, " · ".join(steps) def marvinStrip(row): diff --git a/tests/test_weather.py b/tests/test_weather.py index f7b70b9..1d7e00f 100644 --- a/tests/test_weather.py +++ b/tests/test_weather.py @@ -38,7 +38,7 @@ def testWeatherResponse(self): with mock.patch("irc2phpbb.marvin_actions.requests") as r: r.get.side_effect = responses - expected = ("Karlskrona 16.6° 3.2 m/s ↙ · " + expected = ("Karlskrona 16.6° halvklart 3.2 m/s ↙ · " "03:00 15.2° molnigt 3.1 m/s ↖ · " "07:00 14.6° mulet 3.4 m/s ↖.") self.assertActionOutput(marvin_actions.marvinWeather, "väder", expected) @@ -56,7 +56,7 @@ def testWeatherResponseWithSignificantWeather(self): with mock.patch("irc2phpbb.marvin_actions.requests") as r: r.get.side_effect = responses - expected = ("Karlskrona 16.6° 3.2 m/s ↙. Lätt regn · " + expected = ("Karlskrona 16.6° halvklart 3.2 m/s ↙. Lätt regn · " "03:00 15.2° molnigt 3.1 m/s ↖ · " "07:00 14.6° mulet 3.4 m/s ↖.") self.assertActionOutput(marvin_actions.marvinWeather, "väder", expected)