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()
|
load_dotenv()
|
||||||
|
|
||||||
# --- Network & Credentials ---
|
# --- 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}"
|
SENSOR_HTTP = f"http://{SENSOR_IP}"
|
||||||
|
|
||||||
MQTT_USERNAME = os.getenv("MQTT_USERNAME", "weather_station")
|
MQTT_USERNAME = os.getenv("MQTT_USERNAME", "weather_station")
|
||||||
MQTT_PASSWORD = os.getenv("MQTT_PASSWORD", "SensorData12345!")
|
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 ---
|
# --- Location ---
|
||||||
LAT = float(os.getenv("LATITUDE", "56.2006"))
|
LAT = float(os.getenv("LATITUDE", "56.2006"))
|
||||||
|
|||||||
+41
-10
@@ -1,7 +1,7 @@
|
|||||||
# environment/smhi.py
|
# environment/smhi.py
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from tools.geometry import Coordinate, LineString, MultiPolygon, Polygon
|
from tools.geometry import Coordinate, LineString, MultiPolygon, Polygon
|
||||||
@@ -12,7 +12,7 @@ logger = logging.getLogger(__name__)
|
|||||||
class SMHI:
|
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"
|
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 = (
|
alerts_api = (
|
||||||
"https://opendata-download-warnings.smhi.se/ibww/api/version/1/warning.json"
|
"https://opendata-download-warnings.smhi.se/ibww/api/version/1/warning.json"
|
||||||
)
|
)
|
||||||
@@ -68,8 +68,31 @@ class SMHI:
|
|||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
|
|
||||||
area_list = []
|
area_list = []
|
||||||
|
now_utc = datetime.now(timezone.utc)
|
||||||
|
|
||||||
for warning in resp.json():
|
for warning in resp.json():
|
||||||
for areas in warning["warningAreas"]:
|
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
|
is_in_area = False
|
||||||
if areas["area"]["type"] == "FeatureCollection":
|
if areas["area"]["type"] == "FeatureCollection":
|
||||||
for features in areas["area"]["features"]:
|
for features in areas["area"]["features"]:
|
||||||
@@ -89,7 +112,7 @@ class SMHI:
|
|||||||
area_list.append(
|
area_list.append(
|
||||||
{
|
{
|
||||||
"areaName": areas["areaName"]["sv"],
|
"areaName": areas["areaName"]["sv"],
|
||||||
"type": areas["warningLevel"]["code"],
|
"type": warning_level,
|
||||||
"description": areas["eventDescription"]["code"],
|
"description": areas["eventDescription"]["code"],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -107,29 +130,37 @@ class SMHI:
|
|||||||
|
|
||||||
def _get_forecast(self) -> bool:
|
def _get_forecast(self) -> bool:
|
||||||
try:
|
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(
|
resp = self._session.get(
|
||||||
self.forecast_api.format(lon=self.location.long, lat=self.location.lat),
|
full_url,
|
||||||
timeout=10,
|
timeout=10,
|
||||||
)
|
)
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
|
|
||||||
data = resp.json()["timeSeries"][1]
|
w_data = resp.json()["timeSeries"][0]["data"]
|
||||||
w_data = {x["name"]: x["values"][0] for x in data["parameters"]}
|
|
||||||
|
|
||||||
self._data["forecast"] = {
|
self._data["forecast"] = {
|
||||||
"data": {
|
"data": {
|
||||||
"wind": {
|
"wind": {
|
||||||
"deg": w_data["wd"],
|
"deg": w_data["wind_from_direction"],
|
||||||
"speed": round(w_data["ws"], 1),
|
"speed": round(w_data["wind_speed"], 1),
|
||||||
"gust": round(w_data["gust"], 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(),
|
"valid_time": datetime.now(),
|
||||||
}
|
}
|
||||||
return True
|
return True
|
||||||
except requests.RequestException as e:
|
except requests.RequestException as e:
|
||||||
logger.error(f"Network error getting SMHI Forecast: {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:
|
except Exception as e:
|
||||||
logger.error(f"Parsing error for SMHI Forecast: {e}")
|
logger.error(f"Parsing error for SMHI Forecast: {e}")
|
||||||
return False
|
return False
|
||||||
|
|||||||
Binary file not shown.
+22
-58
@@ -1,6 +1,7 @@
|
|||||||
# tools/geometry.py
|
# tools/geometry.py
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
import math
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -68,8 +69,8 @@ class LineString:
|
|||||||
return min(line.distance_to_point(point) for line in self.lineList)
|
return min(line.distance_to_point(point) for line in self.lineList)
|
||||||
|
|
||||||
def is_intersecting(self, point: Point) -> bool:
|
def is_intersecting(self, point: Point) -> bool:
|
||||||
"""Checks if a given point lies exactly on the LineString."""
|
"""Checks if a given point lies exactly on the LineString (with tolerance)."""
|
||||||
return self.distance_to(point) == 0.0
|
return math.isclose(self.distance_to(point), 0.0, abs_tol=1e-9)
|
||||||
|
|
||||||
|
|
||||||
class Polygon:
|
class Polygon:
|
||||||
@@ -78,72 +79,35 @@ class Polygon:
|
|||||||
def __init__(self, points: list[list[float]]) -> None:
|
def __init__(self, points: list[list[float]]) -> None:
|
||||||
self.listPoints: list[Point] = [Point(p[0], p[1]) for p in points]
|
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:
|
def contains(self, p: Point) -> bool:
|
||||||
"""
|
"""
|
||||||
Determines if a point is strictly inside the Polygon using the Ray-Casting algorithm.
|
Determines if a point is inside the Polygon using the Even-Odd rule.
|
||||||
Draws a horizontal line to the right of the point and counts edge intersections.
|
Includes an explicit check for points lying exactly on the boundary edges.
|
||||||
"""
|
"""
|
||||||
n = len(self.listPoints)
|
n = len(self.listPoints)
|
||||||
if n < 3:
|
if n < 3:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Create a horizontal ray starting from the point and going infinitely right
|
inside = False
|
||||||
exline = Line(p, Point(99999.0, p.y))
|
j = n - 1
|
||||||
count = 0
|
|
||||||
|
|
||||||
for i in range(n):
|
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):
|
cross_product = (p.y - pi.y) * (pj.x - pi.x) - (p.x - pi.x) * (pj.y - pi.y)
|
||||||
# If the point is collinear with the side, check if it's strictly on the side
|
if math.isclose(cross_product, 0.0, abs_tol=1e-9):
|
||||||
if self.direction(side.p1, p, side.p2) == 0:
|
if min(pi.x, pj.x) <= p.x <= max(pi.x, pj.x) and min(
|
||||||
return self.on_line(side, p)
|
pi.y, pj.y
|
||||||
count += 1
|
) <= p.y <= max(pi.y, pj.y):
|
||||||
|
return True
|
||||||
|
|
||||||
# If the number of intersections is odd, the point is inside the polygon
|
if ((pi.y > p.y) != (pj.y > p.y)) and (
|
||||||
return bool(count & 1)
|
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:
|
class MultiPolygon:
|
||||||
|
|||||||
Reference in New Issue
Block a user