Updated SMHI API and improved the polygon contains logic
This commit is contained in:
+2
-2
@@ -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
@@ -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¶meters=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
|
||||
|
||||
Binary file not shown.
+22
-58
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user