diff --git a/constants.py b/constants.py index 15d67ad..284379e 100644 --- a/constants.py +++ b/constants.py @@ -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")) diff --git a/environment/smhi.py b/environment/smhi.py index d8433e3..44144e9 100644 --- a/environment/smhi.py +++ b/environment/smhi.py @@ -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 diff --git a/requirements.txt b/requirements.txt index d180f4b..648a17d 100644 Binary files a/requirements.txt and b/requirements.txt differ diff --git a/tools/__init__.py b/tools/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tools/geometry.py b/tools/geometry.py index 5d2f8cc..67bdabb 100644 --- a/tools/geometry.py +++ b/tools/geometry.py @@ -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: