Updated SMHI API and improved the polygon contains logic

This commit is contained in:
2026-04-18 12:37:37 +02:00 Verified
parent 49e52d9613
commit 131b74daf9
5 changed files with 65 additions and 70 deletions
+2 -2
View File
@@ -7,12 +7,12 @@ from tools.geometry import Coordinate
load_dotenv()
# --- Network & Credentials ---
SENSOR_IP = os.getenv("SENSOR_IP", "192.168.0.188")
SENSOR_IP = os.getenv("SENSOR_IP", "10.11.4.78")
SENSOR_HTTP = f"http://{SENSOR_IP}"
MQTT_USERNAME = os.getenv("MQTT_USERNAME", "weather_station")
MQTT_PASSWORD = os.getenv("MQTT_PASSWORD", "SensorData12345!")
MQTT_FALLBACK_IP = os.getenv("MQTT_FALLBACK_IP", "192.168.0.168")
MQTT_FALLBACK_IP = os.getenv("MQTT_FALLBACK_IP", "10.11.0.33")
# --- Location ---
LAT = float(os.getenv("LATITUDE", "56.2006"))
+41 -10
View File
@@ -1,7 +1,7 @@
# environment/smhi.py
import logging
from datetime import datetime
from datetime import datetime, timezone
import requests
from tools.geometry import Coordinate, LineString, MultiPolygon, Polygon
@@ -12,7 +12,7 @@ logger = logging.getLogger(__name__)
class SMHI:
fire_warning_api = "https://opendata-download-metfcst.smhi.se/api/category/fwif1g/version/1/daily/geotype/point/lon/{lon}/lat/{lat}/data.json"
forecast_api = "https://opendata-download-metfcst.smhi.se/api/category/pmp3g/version/2/geotype/point/lon/{lon}/lat/{lat}/data.json"
forecast_api = "https://opendata-download-metfcst.smhi.se/api/category/snow1g/version/1/geotype/point/lon/{lon}/lat/{lat}/data.json"
alerts_api = (
"https://opendata-download-warnings.smhi.se/ibww/api/version/1/warning.json"
)
@@ -68,8 +68,31 @@ class SMHI:
resp.raise_for_status()
area_list = []
now_utc = datetime.now(timezone.utc)
for warning in resp.json():
for areas in warning["warningAreas"]:
warning_level = areas["warningLevel"]["code"]
if warning_level == "MESSAGE":
continue
start_str = areas.get("approximateStart")
if start_str:
start_time = datetime.fromisoformat(
start_str.replace("Z", "+00:00")
)
if now_utc < start_time:
continue
end_str = areas.get("approximateEnd")
if end_str:
end_time = datetime.fromisoformat(
end_str.replace("Z", "+00:00")
)
if now_utc > end_time:
continue
is_in_area = False
if areas["area"]["type"] == "FeatureCollection":
for features in areas["area"]["features"]:
@@ -89,7 +112,7 @@ class SMHI:
area_list.append(
{
"areaName": areas["areaName"]["sv"],
"type": areas["warningLevel"]["code"],
"type": warning_level,
"description": areas["eventDescription"]["code"],
}
)
@@ -107,29 +130,37 @@ class SMHI:
def _get_forecast(self) -> bool:
try:
base_url = self.forecast_api.format(
lon=self.location.long, lat=self.location.lat
)
query_string = "?timeseries=1&parameters=wind_from_direction,wind_speed,wind_speed_of_gust,symbol_code"
full_url = base_url + query_string
resp = self._session.get(
self.forecast_api.format(lon=self.location.long, lat=self.location.lat),
full_url,
timeout=10,
)
resp.raise_for_status()
data = resp.json()["timeSeries"][1]
w_data = {x["name"]: x["values"][0] for x in data["parameters"]}
w_data = resp.json()["timeSeries"][0]["data"]
self._data["forecast"] = {
"data": {
"wind": {
"deg": w_data["wd"],
"speed": round(w_data["ws"], 1),
"gust": round(w_data["gust"], 1),
"deg": w_data["wind_from_direction"],
"speed": round(w_data["wind_speed"], 1),
"gust": round(w_data["wind_speed_of_gust"], 1),
},
"weather_symbol": w_data["Wsymb2"] - 1,
"weather_symbol": w_data["symbol_code"] - 1,
},
"valid_time": datetime.now(),
}
return True
except requests.RequestException as e:
logger.error(f"Network error getting SMHI Forecast: {e}")
except KeyError as e:
logger.error(f"Missing expected key in SMHI Forecast data: {e}")
except Exception as e:
logger.error(f"Parsing error for SMHI Forecast: {e}")
return False
BIN
View File
Binary file not shown.
View File
+22 -58
View File
@@ -1,6 +1,7 @@
# tools/geometry.py
from dataclasses import dataclass
import math
@dataclass
@@ -68,8 +69,8 @@ class LineString:
return min(line.distance_to_point(point) for line in self.lineList)
def is_intersecting(self, point: Point) -> bool:
"""Checks if a given point lies exactly on the LineString."""
return self.distance_to(point) == 0.0
"""Checks if a given point lies exactly on the LineString (with tolerance)."""
return math.isclose(self.distance_to(point), 0.0, abs_tol=1e-9)
class Polygon:
@@ -78,72 +79,35 @@ class Polygon:
def __init__(self, points: list[list[float]]) -> None:
self.listPoints: list[Point] = [Point(p[0], p[1]) for p in points]
def on_line(self, l1: Line, p: Point) -> bool:
"""Checks if collinear point 'p' lies strictly on the line segment 'l1'."""
return min(l1.p1.x, l1.p2.x) <= p.x <= max(l1.p1.x, l1.p2.x) and min(
l1.p1.y, l1.p2.y
) <= p.y <= max(l1.p1.y, l1.p2.y)
def direction(self, a: Point, b: Point, c: Point) -> int:
"""
Finds the orientation of an ordered triplet (a, b, c).
Returns:
0 : Collinear
1 : Clockwise
2 : Counterclockwise
"""
val = (b.y - a.y) * (c.x - b.x) - (b.x - a.x) * (c.y - b.y)
if val == 0:
return 0
return 2 if val < 0 else 1
def is_intersect(self, l1: Line, l2: Line) -> bool:
"""Checks if line segment l1 intersects with line segment l2."""
dir1 = self.direction(l1.p1, l1.p2, l2.p1)
dir2 = self.direction(l1.p1, l1.p2, l2.p2)
dir3 = self.direction(l2.p1, l2.p2, l1.p1)
dir4 = self.direction(l2.p1, l2.p2, l1.p2)
# General case intersection
if dir1 != dir2 and dir3 != dir4:
return True
# Special collinear cases
if dir1 == 0 and self.on_line(l1, l2.p1):
return True
if dir2 == 0 and self.on_line(l1, l2.p2):
return True
if dir3 == 0 and self.on_line(l2, l1.p1):
return True
if dir4 == 0 and self.on_line(l2, l1.p2):
return True
return False
def contains(self, p: Point) -> bool:
"""
Determines if a point is strictly inside the Polygon using the Ray-Casting algorithm.
Draws a horizontal line to the right of the point and counts edge intersections.
Determines if a point is inside the Polygon using the Even-Odd rule.
Includes an explicit check for points lying exactly on the boundary edges.
"""
n = len(self.listPoints)
if n < 3:
return False
# Create a horizontal ray starting from the point and going infinitely right
exline = Line(p, Point(99999.0, p.y))
count = 0
inside = False
j = n - 1
for i in range(n):
side = Line(self.listPoints[i], self.listPoints[(i + 1) % n])
pi = self.listPoints[i]
pj = self.listPoints[j]
if self.is_intersect(side, exline):
# If the point is collinear with the side, check if it's strictly on the side
if self.direction(side.p1, p, side.p2) == 0:
return self.on_line(side, p)
count += 1
cross_product = (p.y - pi.y) * (pj.x - pi.x) - (p.x - pi.x) * (pj.y - pi.y)
if math.isclose(cross_product, 0.0, abs_tol=1e-9):
if min(pi.x, pj.x) <= p.x <= max(pi.x, pj.x) and min(
pi.y, pj.y
) <= p.y <= max(pi.y, pj.y):
return True
# If the number of intersections is odd, the point is inside the polygon
return bool(count & 1)
if ((pi.y > p.y) != (pj.y > p.y)) and (
p.x < (pj.x - pi.x) * (p.y - pi.y) / (pj.y - pi.y) + pi.x
):
inside = not inside
j = i
return inside
class MultiPolygon: