From 7c95e135552889bb303e024d79a8593d74b7f980 Mon Sep 17 00:00:00 2001 From: Sonya Yim <91685133+SYCqi@users.noreply.github.com> Date: Sat, 7 Mar 2026 14:25:58 -0500 Subject: [PATCH 1/5] Update main.py Add Azure SQL integration to save occupancy data --- main.py | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/main.py b/main.py index 1dff501..2222200 100644 --- a/main.py +++ b/main.py @@ -7,6 +7,7 @@ import json import os +import pyodbc from collections import Counter from datetime import datetime, timedelta from pathlib import Path @@ -87,6 +88,8 @@ def _append_to_blob(blob_name: str, line: str) -> None: # Configuration DATA_DIR = Path(os.environ.get("THERMAL_DATA_DIR", "thermal_data")) SAVE_DATA = os.environ.get("SAVE_THERMAL_DATA", "true").lower() in ("1", "true", "yes") +SQL_CONNECTION_STRING = os.environ.get("SQL_CONNECTION_STRING", "").strip() +SAVE_TO_SQL = os.environ.get("SAVE_TO_SQL", "true").lower() in ("1", "true", "yes") # Occupancy detection parameters MIN_HUMAN_TEMP = 30.0 @@ -316,6 +319,72 @@ def convert_numpy_types(obj: Any) -> Any: return tuple(convert_numpy_types(x) for x in obj) return obj +def _get_sql_connection(): + """Create Azure SQL connection. Returns None if not configured.""" + if not SQL_CONNECTION_STRING: + return None + try: + return pyodbc.connect(SQL_CONNECTION_STRING, timeout=10) + except Exception as e: + print(f"Azure SQL connection failed: {e}") + return None + + +def save_occupancy_data_sql(occupancy_result: dict, timestamp_iso: Optional[str] = None) -> None: + """Save occupancy estimation to Azure SQL.""" + if not SAVE_TO_SQL: + return + + conn = _get_sql_connection() + if conn is None: + return + + try: + sid = occupancy_result.get("sensor_id") or "unknown" + ts = timestamp_iso or datetime.now().isoformat() + + entry = { + "timestamp": ts, + "sensor_id": sid, + "occupancy": int(occupancy_result["occupancy"]), + "room_temperature": ( + float(occupancy_result["room_temperature"]) + if occupancy_result.get("room_temperature") is not None + else None + ), + "people_clusters": json.dumps( + convert_numpy_types(occupancy_result.get("people_clusters", [])) + ), + "fever_count": int(occupancy_result.get("fever_count", 0)), + "any_fever": bool(occupancy_result.get("any_fever", False)), + } + + cursor = conn.cursor() + cursor.execute( + """ + INSERT INTO occupancy_data + ([timestamp], sensor_id, occupancy, room_temperature, + people_clusters, fever_count, any_fever) + VALUES (?, ?, ?, ?, ?, ?, ?) + """, + entry["timestamp"], + entry["sensor_id"], + entry["occupancy"], + entry["room_temperature"], + entry["people_clusters"], + entry["fever_count"], + 1 if entry["any_fever"] else 0, + ) + conn.commit() + cursor.close() + conn.close() + except Exception as e: + print(f"Error saving occupancy data to Azure SQL: {e}") + try: + conn.close() + except Exception: + pass + def save_thermal_data( compact_data: dict, expanded_data: dict, sensor_id: Optional[str] = None @@ -544,6 +613,7 @@ def receive_thermal_data(data: dict) -> dict: last_update_time_by_sensor[sensor_id] = now_iso save_thermal_data(compact_data, latest_thermal_data, sensor_id) save_occupancy_data(occupancy_result) + save_occupancy_data_sql(occupancy_result, timestamp_iso=now_iso) pixel_count = len(latest_thermal_data.get("pixels", [])) return { "status": "success", From 048601d152d6dbc6451b72c738a81f7b5f60d345 Mon Sep 17 00:00:00 2001 From: Sonya Yim <91685133+SYCqi@users.noreply.github.com> Date: Sat, 7 Mar 2026 14:29:27 -0500 Subject: [PATCH 2/5] Update requirements.txt Add pyodbc dependency for Azure SQL integration --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index e2aeecd..9bb1fd4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,4 @@ numpy>=1.20.0 scipy>=1.7.0 azure-storage-blob>=12.19.0 requests>=2.25.0 +pyodbc>=5.1.0 From 671553822452208613383994e3b4ced16b611251 Mon Sep 17 00:00:00 2001 From: Sonya Yim <91685133+SYCqi@users.noreply.github.com> Date: Sat, 7 Mar 2026 14:33:51 -0500 Subject: [PATCH 3/5] Update Dockerfile Add Azure SQL support and ODBC driver installation - Installed Microsoft ODBC Driver 18 - Added unixodbc dependencies - Combined RUN commands for proper Docker build layering - Prepared backend for Azure SQL integration --- Dockerfile | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 22e00d3..d03ac5b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,11 @@ WORKDIR /app # Install dependencies (no build tools needed for current deps) COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt +RUN apt-get update && apt-get install -y curl gnupg unixodbc unixodbc-dev && \ + curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add - && \ + curl https://packages.microsoft.com/config/debian/12/prod.list > /etc/apt/sources.list.d/mssql-release.list && \ + apt-get update && ACCEPT_EULA=Y apt-get install -y msodbcsql18 && \ + pip install --no-cache-dir -r requirements.txt # Application COPY main.py . From fb8e9594856cbccf9d28b3ae1f0a4bb907169654 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Mar 2026 19:42:01 +0000 Subject: [PATCH 4/5] Initial plan From 443c9e133fd25cbe6fc9a8d977214926cde645a2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Mar 2026 19:45:53 +0000 Subject: [PATCH 5/5] Address review comments: improve Dockerfile, SQL connection caching, lazy import, and error handling Co-authored-by: mandeeps <3266584+mandeeps@users.noreply.github.com> --- Dockerfile | 13 ++++++++----- main.py | 49 +++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 49 insertions(+), 13 deletions(-) diff --git a/Dockerfile b/Dockerfile index d03ac5b..cb7683f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,12 +3,15 @@ FROM python:3.12-slim WORKDIR /app -# Install dependencies (no build tools needed for current deps) +# Install system deps for ODBC/SQL Server (unixODBC, msodbcsql18) and Python requirements COPY requirements.txt . -RUN apt-get update && apt-get install -y curl gnupg unixodbc unixodbc-dev && \ - curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add - && \ - curl https://packages.microsoft.com/config/debian/12/prod.list > /etc/apt/sources.list.d/mssql-release.list && \ - apt-get update && ACCEPT_EULA=Y apt-get install -y msodbcsql18 && \ +RUN apt-get update && apt-get install -y --no-install-recommends curl gnupg ca-certificates unixodbc unixodbc-dev && \ + mkdir -p /etc/apt/keyrings && \ + curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor -o /etc/apt/keyrings/microsoft.gpg && \ + chmod 644 /etc/apt/keyrings/microsoft.gpg && \ + curl https://packages.microsoft.com/config/debian/12/prod.list | sed 's|^deb |deb [signed-by=/etc/apt/keyrings/microsoft.gpg] |' > /etc/apt/sources.list.d/mssql-release.list && \ + apt-get update && ACCEPT_EULA=Y apt-get install -y --no-install-recommends msodbcsql18 && \ + rm -rf /var/lib/apt/lists/* && \ pip install --no-cache-dir -r requirements.txt # Application diff --git a/main.py b/main.py index 2222200..dd53580 100644 --- a/main.py +++ b/main.py @@ -7,7 +7,6 @@ import json import os -import pyodbc from collections import Counter from datetime import datetime, timedelta from pathlib import Path @@ -22,6 +21,10 @@ # _blob_container_client: container client if connected, False if init failed, None if not tried _blob_container_client: Any = None +# Optional Azure SQL connection cache. +# None = not yet attempted, False = permanently disabled (misconfiguration or import error), object = live connection +_sql_connection: Any = None + def _get_blob_container(): """Lazy-init Azure Blob container client from env. Returns None if not configured or init failed.""" @@ -320,18 +323,41 @@ def convert_numpy_types(obj: Any) -> Any: return obj def _get_sql_connection(): - """Create Azure SQL connection. Returns None if not configured.""" + """Return a cached Azure SQL connection. Returns None if not configured or permanently disabled.""" + global _sql_connection + if _sql_connection is False: + return None # permanently disabled after earlier failure + if _sql_connection is not None: + try: + # Lightweight connectivity check at the ODBC driver level (no query sent to server). + _sql_connection.getinfo(2) # SQL_DATA_SOURCE_NAME + return _sql_connection + except Exception: + _sql_connection = None # stale, attempt to reconnect below + if not SQL_CONNECTION_STRING: return None + + try: + import pyodbc # noqa: PLC0415 – intentionally deferred for optional dependency + except ImportError: + print("pyodbc is not installed; Azure SQL saving disabled.") + _sql_connection = False + return None + try: - return pyodbc.connect(SQL_CONNECTION_STRING, timeout=10) + conn = pyodbc.connect(SQL_CONNECTION_STRING, timeout=10) + _sql_connection = conn + return _sql_connection except Exception as e: - print(f"Azure SQL connection failed: {e}") + print(f"Azure SQL connection failed ({type(e).__name__}); SQL saving disabled.") + _sql_connection = False return None def save_occupancy_data_sql(occupancy_result: dict, timestamp_iso: Optional[str] = None) -> None: """Save occupancy estimation to Azure SQL.""" + global _sql_connection if not SAVE_TO_SQL: return @@ -339,6 +365,7 @@ def save_occupancy_data_sql(occupancy_result: dict, timestamp_iso: Optional[str] if conn is None: return + cursor = None try: sid = occupancy_result.get("sensor_id") or "unknown" ts = timestamp_iso or datetime.now().isoformat() @@ -376,14 +403,20 @@ def save_occupancy_data_sql(occupancy_result: dict, timestamp_iso: Optional[str] 1 if entry["any_fever"] else 0, ) conn.commit() - cursor.close() - conn.close() except Exception as e: - print(f"Error saving occupancy data to Azure SQL: {e}") + print(f"Error saving occupancy data to Azure SQL ({type(e).__name__}); will retry on next call.") try: - conn.close() + conn.rollback() except Exception: pass + # Invalidate the cached connection so the next call will reconnect + _sql_connection = None + finally: + if cursor is not None: + try: + cursor.close() + except Exception: + pass def save_thermal_data(