From 4a28b6c247afc515dce3828120ef64ddaca27074 Mon Sep 17 00:00:00 2001 From: Kumar Challa Date: Wed, 31 Dec 2025 12:29:31 -0600 Subject: [PATCH 01/21] fix: remove outdated setup image from README --- images/setup.jpg | Bin 131776 -> 0 bytes readme.md | 3 --- 2 files changed, 3 deletions(-) delete mode 100644 images/setup.jpg diff --git a/images/setup.jpg b/images/setup.jpg deleted file mode 100644 index 267e583ebb430823ad1a54f868059a2da068997b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 131776 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�xJi7g}zcQ|j4Frr-#64!Y0h0h13XnJf+T`ev$ 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 LS9}rk_p$#E5xID` diff --git a/readme.md b/readme.md index de94ede..51b6d7b 100644 --- a/readme.md +++ b/readme.md @@ -363,13 +363,10 @@ The container includes an ASCOM Alpaca SafetyMonitor service (Interface Version 3. **Configure Device**: Set name, location, and unsafe conditions 4. **Add to Software**: Configure in N.I.N.A., SGP, TheSkyX, etc. -![Alpaca Setup Page](/images/setup.jpg) ### Supported Software - N.I.N.A. (Nighttime Imaging 'N' Astronomy) -- Sequence Generator Pro -- TheSkyX - Any ASCOM Alpaca-compatible application ![N.I.N.A. Integration](/images/NINA.jpg) From 89256750521aaea7deb53d652be2fe6c017b2bcb Mon Sep 17 00:00:00 2001 From: Kumar Challa Date: Wed, 31 Dec 2025 12:32:59 -0600 Subject: [PATCH 02/21] fix: update comment from ASCOM Connection to Client Connection in setup_device function --- alpaca_safety_monitor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alpaca_safety_monitor.py b/alpaca_safety_monitor.py index 339cd8a..650946c 100644 --- a/alpaca_safety_monitor.py +++ b/alpaca_safety_monitor.py @@ -1754,7 +1754,7 @@ def setup_device(device_number: int):
- +
+
+
Docker Container Uptime
+
{{ container_uptime }}
+
@@ -2574,6 +2579,18 @@ def setup_device(device_number: int): 'confidence': f"{entry['confidence']:.1f}" }) + uptime_delta = datetime.now() - start_time + days = uptime_delta.days + hours, rem = divmod(uptime_delta.seconds, 3600) + minutes, seconds = divmod(rem, 60) + + if days > 0: + container_uptime = f"{days}d {hours}h {minutes}m" + elif hours > 0: + container_uptime = f"{hours}h {minutes}m {seconds}s" + else: + container_uptime = f"{minutes}m {seconds}s" + return render_template_string( html_template, message=message, @@ -2604,6 +2621,7 @@ def setup_device(device_number: int): debounce_to_safe=safety_monitor.alpaca_config.debounce_to_safe_sec, debounce_to_unsafe=safety_monitor.alpaca_config.debounce_to_unsafe_sec, default_threshold=safety_monitor.alpaca_config.default_threshold, + container_uptime=container_uptime, class_thresholds=safety_monitor.alpaca_config.class_thresholds ) From 78336ac470b8a54a869d03f038af2c317f106407 Mon Sep 17 00:00:00 2001 From: Kumar Challa Date: Wed, 31 Dec 2025 12:55:22 -0600 Subject: [PATCH 04/21] fix: layout in the system status section --- alpaca_safety_monitor.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/alpaca_safety_monitor.py b/alpaca_safety_monitor.py index 5a30eae..f4cbabb 100644 --- a/alpaca_safety_monitor.py +++ b/alpaca_safety_monitor.py @@ -1703,8 +1703,7 @@ def setup_device(device_number: int):
+ + From 52f9c016c20926d2b505fd1c2b00dfb985d59535 Mon Sep 17 00:00:00 2001 From: Kumar Challa Date: Thu, 1 Jan 2026 15:44:19 -0600 Subject: [PATCH 10/21] feat: add timeout handling and improved logging --- alpaca/discovery.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/alpaca/discovery.py b/alpaca/discovery.py index f6e66d5..1c1fe36 100644 --- a/alpaca/discovery.py +++ b/alpaca/discovery.py @@ -28,6 +28,9 @@ def start(self): self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + # Set timeout to allow checking self.running periodically + self.socket.settimeout(1.0) + # Bind to all interfaces on discovery port self.socket.bind(('', self.DISCOVERY_PORT)) @@ -35,7 +38,7 @@ def start(self): self.thread = threading.Thread(target=self._discovery_loop, daemon=True) self.thread.start() - logger.info(f"Alpaca Discovery service started on UDP port {self.DISCOVERY_PORT}") + logger.info(f"Alpaca Discovery service started on UDP port {self.DISCOVERY_PORT}, advertising HTTP port {self.alpaca_port}") except Exception as e: logger.error(f"Failed to start discovery service: {e}") logger.warning("Discovery will not be available, but HTTP API will still work") @@ -55,11 +58,16 @@ def _discovery_loop(self): while self.running: try: - # Receive discovery request - data, addr = self.socket.recvfrom(1024) + # Receive discovery request (with timeout to check self.running) + try: + data, addr = self.socket.recvfrom(1024) + except socket.timeout: + continue # Timeout allows us to check self.running + + logger.debug(f"Received UDP packet from {addr[0]}:{addr[1]}: {data}") if data == self.DISCOVERY_MESSAGE: - logger.info(f"Discovery request from {addr[0]}:{addr[1]}") + logger.info(f"Valid Alpaca discovery request from {addr[0]}:{addr[1]}") self._send_discovery_response(addr) else: logger.debug(f"Ignored non-discovery message from {addr}: {data}") @@ -71,14 +79,16 @@ def _discovery_loop(self): def _send_discovery_response(self, addr): """Send discovery response to client""" try: - # Build discovery response per ASCOM spec + # Build discovery response per ASCOM Alpaca Discovery spec + # Must include AlpacaPort at minimum, ServerName is recommended response = { - "AlpacaPort": self.alpaca_port + "AlpacaPort": self.alpaca_port, + "ServerName": "SimpleCloudDetect" } response_json = json.dumps(response).encode('utf-8') self.socket.sendto(response_json, addr) - logger.info(f"Sent discovery response to {addr[0]}:{addr[1]} - Port: {self.alpaca_port}") + logger.info(f"Sent discovery response to {addr[0]}:{addr[1]} - Port: {self.alpaca_port}, ServerName: SimpleCloudDetect") except Exception as e: logger.error(f"Failed to send discovery response: {e}") From dc9310a6fe6642051a04d08c0585101511a061f2 Mon Sep 17 00:00:00 2001 From: Kumar Challa Date: Thu, 1 Jan 2026 16:46:29 -0600 Subject: [PATCH 11/21] feat: remove deprecated management endpoints --- alpaca/routes/api.py | 33 ---------------------------- alpaca/routes/management.py | 43 +++++++++++++++++++++++++++++++++++-- 2 files changed, 41 insertions(+), 35 deletions(-) diff --git a/alpaca/routes/api.py b/alpaca/routes/api.py index debd457..0d6eaca 100644 --- a/alpaca/routes/api.py +++ b/alpaca/routes/api.py @@ -223,36 +223,3 @@ def not_implemented(device_number: int): client_transaction_id=client_tx_id )), 400 -# Management endpoints -@api_bp.route('/management/apiversions', methods=['GET']) -def get_apiversions(): - """Get supported API versions""" - return jsonify({"Value": [1], "ErrorNumber": 0, "ErrorMessage": ""}) - -@api_bp.route('/management/v1/description', methods=['GET']) -def get_management_description(): - """Get server description""" - return jsonify({ - "Value": { - "ServerName": "SimpleCloudDetect Alpaca Server", - "Manufacturer": "SimpleCloudDetect", - "ManufacturerVersion": "2.0", - "Location": monitor.alpaca_config.location - }, - "ErrorNumber": 0, - "ErrorMessage": "" - }) - -@api_bp.route('/management/v1/configureddevices', methods=['GET']) -def get_configureddevices(): - """Get list of configured devices""" - return jsonify({ - "Value": [{ - "DeviceName": monitor.alpaca_config.device_name, - "DeviceType": "SafetyMonitor", - "DeviceNumber": monitor.alpaca_config.device_number, - "UniqueID": f"simpleclouddetect-{monitor.alpaca_config.device_number}" - }], - "ErrorNumber": 0, - "ErrorMessage": "" - }) diff --git a/alpaca/routes/management.py b/alpaca/routes/management.py index ad0098a..573dbfc 100644 --- a/alpaca/routes/management.py +++ b/alpaca/routes/management.py @@ -106,7 +106,7 @@ def setup_device(device_number: int): for (ip, client_id), conn_time in monitor.connected_clients.items(): duration = (get_current_time(monitor.alpaca_config.timezone) - conn_time).total_seconds() client_list.append({ - 'ip': f"{ip} (ID: {client_id})", + 'ip': ip, 'status': 'connected', 'duration': f"{int(duration)}s", 'duration_seconds': duration, @@ -119,7 +119,7 @@ def setup_device(device_number: int): for (ip, client_id), (conn_time, disc_time) in monitor.disconnected_clients.items(): duration = (disc_time - conn_time).total_seconds() client_list.append({ - 'ip': f"{ip} (ID: {client_id})", + 'ip': ip, 'status': 'disconnected', 'duration': f"{int(duration)}s", 'duration_seconds': duration, @@ -171,3 +171,42 @@ def setup_device(device_number: int): default_threshold=monitor.alpaca_config.default_threshold, class_thresholds=monitor.alpaca_config.class_thresholds ) + + +# ASCOM Alpaca Management API Endpoints (must be at root, not under /api prefix) +@mgmt_bp.route('/management/apiversions', methods=['GET']) +def get_apiversions(): + """Get supported API versions""" + return { + "Value": [1], + "ErrorNumber": 0, + "ErrorMessage": "" + } + +@mgmt_bp.route('/management/v1/description', methods=['GET']) +def get_management_description(): + """Get server description""" + return { + "Value": { + "ServerName": "SimpleCloudDetect", + "Manufacturer": "SimpleCloudDetect", + "ManufacturerVersion": "2.0", + "Location": monitor.alpaca_config.location + }, + "ErrorNumber": 0, + "ErrorMessage": "" + } + +@mgmt_bp.route('/management/v1/configureddevices', methods=['GET']) +def get_configureddevices(): + """Get list of configured devices""" + return { + "Value": [{ + "DeviceName": monitor.alpaca_config.device_name, + "DeviceType": "SafetyMonitor", + "DeviceNumber": monitor.alpaca_config.device_number, + "UniqueID": f"simpleclouddetect-safetymonitor-{monitor.alpaca_config.device_number}" + }], + "ErrorNumber": 0, + "ErrorMessage": "" + } From c4c681ee3e260f510c944632d6f3c2a39cdba6c9 Mon Sep 17 00:00:00 2001 From: Kumar Challa Date: Thu, 1 Jan 2026 17:06:24 -0600 Subject: [PATCH 12/21] feat: add timezone handling for client connection timestamps and safety history --- alpaca/routes/management.py | 53 ++++++++++++++++++++++++++++++++----- 1 file changed, 46 insertions(+), 7 deletions(-) diff --git a/alpaca/routes/management.py b/alpaca/routes/management.py index 573dbfc..a18cb6b 100644 --- a/alpaca/routes/management.py +++ b/alpaca/routes/management.py @@ -4,6 +4,7 @@ import os from flask import Blueprint, render_template, request, redirect, url_for from datetime import datetime +from zoneinfo import ZoneInfo import logging from ..device import AlpacaSafetyMonitor from ..config import ALL_CLOUD_CONDITIONS, get_current_time @@ -105,37 +106,75 @@ def setup_device(device_number: int): with monitor.connection_lock: for (ip, client_id), conn_time in monitor.connected_clients.items(): duration = (get_current_time(monitor.alpaca_config.timezone) - conn_time).total_seconds() + + try: + # Convert timestamp to current timezone + tz = ZoneInfo(monitor.alpaca_config.timezone) + local_conn_time = conn_time.astimezone(tz) + conn_time_str = local_conn_time.strftime("%H:%M:%S") + conn_ts = local_conn_time.timestamp() + except Exception: + # Fallback if timezone conversion fails + conn_time_str = conn_time.strftime("%H:%M:%S") + conn_ts = conn_time.timestamp() + client_list.append({ 'ip': ip, 'status': 'connected', 'duration': f"{int(duration)}s", 'duration_seconds': duration, - 'connected_time': conn_time.strftime("%H:%M:%S"), - 'connected_ts': conn_time.timestamp(), + 'connected_time': conn_time_str, + 'connected_ts': conn_ts, 'disconnected_time': '-', 'disconnected_ts': 0 }) for (ip, client_id), (conn_time, disc_time) in monitor.disconnected_clients.items(): duration = (disc_time - conn_time).total_seconds() + + try: + # Convert timestamps to current timezone + tz = ZoneInfo(monitor.alpaca_config.timezone) + local_conn_time = conn_time.astimezone(tz) + local_disc_time = disc_time.astimezone(tz) + conn_time_str = local_conn_time.strftime("%H:%M:%S") + disc_time_str = local_disc_time.strftime("%H:%M:%S") + conn_ts = local_conn_time.timestamp() + disc_ts = local_disc_time.timestamp() + except Exception: + # Fallback if timezone conversion fails + conn_time_str = conn_time.strftime("%H:%M:%S") + disc_time_str = disc_time.strftime("%H:%M:%S") + conn_ts = conn_time.timestamp() + disc_ts = disc_time.timestamp() + client_list.append({ 'ip': ip, 'status': 'disconnected', 'duration': f"{int(duration)}s", 'duration_seconds': duration, - 'connected_time': conn_time.strftime("%H:%M:%S"), - 'connected_ts': conn_time.timestamp(), - 'disconnected_time': disc_time.strftime("%H:%M:%S"), - 'disconnected_ts': disc_time.timestamp() + 'connected_time': conn_time_str, + 'connected_ts': conn_ts, + 'disconnected_time': disc_time_str, + 'disconnected_ts': disc_ts }) # Safety history safety_history = [] with monitor.detection_lock: for entry in list(monitor._safety_history)[-10:]: # Last 10 entries + try: + # Convert timestamp to current timezone + tz = ZoneInfo(monitor.alpaca_config.timezone) + converted_time = entry['timestamp'].astimezone(tz) + time_str = converted_time.strftime("%H:%M:%S") + except Exception: + # Fallback if timezone conversion fails + time_str = entry['timestamp'].strftime("%H:%M:%S") + safety_history.append({ 'is_safe': entry['is_safe'], - 'time': entry['timestamp'].strftime("%H:%M:%S"), + 'time': time_str, 'condition': entry['condition'], 'confidence': round(entry['confidence'], 1) }) From e7e3ddbabcc8b5cd8ad1bf09b7bc714e0d725ef5 Mon Sep 17 00:00:00 2001 From: Kumar Challa Date: Thu, 1 Jan 2026 17:18:42 -0600 Subject: [PATCH 13/21] feat: display safety history in reverse chronological order --- alpaca/routes/management.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/alpaca/routes/management.py b/alpaca/routes/management.py index a18cb6b..beb0455 100644 --- a/alpaca/routes/management.py +++ b/alpaca/routes/management.py @@ -159,10 +159,10 @@ def setup_device(device_number: int): 'disconnected_ts': disc_ts }) - # Safety history + # Safety history (newest first - reverse chronological) safety_history = [] with monitor.detection_lock: - for entry in list(monitor._safety_history)[-10:]: # Last 10 entries + for entry in reversed(list(monitor._safety_history)[-10:]): # Last 10 entries, newest first try: # Convert timestamp to current timezone tz = ZoneInfo(monitor.alpaca_config.timezone) From 483319535580a9f76715744f16c93263ab2951ad Mon Sep 17 00:00:00 2001 From: Kumar Challa Date: Thu, 1 Jan 2026 17:32:12 -0600 Subject: [PATCH 14/21] feat: handle reconnections by removing clients from disconnected list in the UI --- alpaca/device.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/alpaca/device.py b/alpaca/device.py index c7f40e7..f98fa3c 100644 --- a/alpaca/device.py +++ b/alpaca/device.py @@ -307,6 +307,10 @@ def connect(self, client_ip: str, client_id: int): key = (client_ip, client_id) self.connected_clients[key] = get_current_time(self.alpaca_config.timezone) + # Remove from disconnected clients if reconnecting + if key in self.disconnected_clients: + del self.disconnected_clients[key] + if len(self.connected_clients) == 1: self.connected_at = self.connected_clients[key] self.last_connected_at = self.connected_at From 088f6935b8e41654cff084837d47960a5e6e9810 Mon Sep 17 00:00:00 2001 From: Kumar Challa Date: Thu, 1 Jan 2026 18:50:27 -0600 Subject: [PATCH 15/21] feat: improve client list handling by filtering out currently connected IPs from disconnected clients --- alpaca/routes/management.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/alpaca/routes/management.py b/alpaca/routes/management.py index beb0455..d3500f1 100644 --- a/alpaca/routes/management.py +++ b/alpaca/routes/management.py @@ -104,6 +104,9 @@ def setup_device(device_number: int): # Build client list client_list = [] with monitor.connection_lock: + # Get set of currently connected IPs + connected_ips = {ip for (ip, client_id) in monitor.connected_clients.keys()} + for (ip, client_id), conn_time in monitor.connected_clients.items(): duration = (get_current_time(monitor.alpaca_config.timezone) - conn_time).total_seconds() @@ -129,7 +132,11 @@ def setup_device(device_number: int): 'disconnected_ts': 0 }) + # Only show disconnected clients if their IP is not currently connected for (ip, client_id), (conn_time, disc_time) in monitor.disconnected_clients.items(): + if ip in connected_ips: + continue # Skip disconnected entries for IPs that are currently connected + duration = (disc_time - conn_time).total_seconds() try: From 8b332dc0351fed5d06b0c01230b146362f84fe87 Mon Sep 17 00:00:00 2001 From: Kumar Challa Date: Thu, 1 Jan 2026 19:00:44 -0600 Subject: [PATCH 16/21] feat: optimize Docker caching by changing cache mode to min for faster exports --- .github/workflows/build-and-release.yml | 8 ++++---- .github/workflows/snd.yml | 5 +++-- Dockerfile | 20 ++++++++++++-------- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 1eb95e4..1844903 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -175,7 +175,8 @@ jobs: push: true tags: ${{ vars.DOCKERHUB_USERNAME }}/simpleclouddetect:${{ steps.docker_tags.outputs.tag }} cache-from: type=local,src=/tmp/.buildx-cache - cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max + # CHANGED: mode=min speeds up export by only caching final layers, avoiding massive I/O + cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=min platforms: linux/amd64,linux/arm64 - name: Move cache @@ -491,7 +492,7 @@ jobs: const summary = summaryBody.join('\n').trim(); // Create enhanced PR body with AI summary - const marker = ''; + const marker = ''; const summarySection = `${marker}\n\n${summary}\n\n---\n📊 Analyzed **${commitCount}** commit(s) | 🕐 Updated: ${timestamp} | Generated by GitHub Actions\n\n---\n\n`; // Get current PR details @@ -541,5 +542,4 @@ jobs: body: newBody }); - console.log(`✅ Updated PR #${context.issue.number}`); - + console.log(`✅ Updated PR #${context.issue.number}`); \ No newline at end of file diff --git a/.github/workflows/snd.yml b/.github/workflows/snd.yml index 1a14cc5..88f7829 100644 --- a/.github/workflows/snd.yml +++ b/.github/workflows/snd.yml @@ -59,7 +59,8 @@ jobs: push: true tags: ${{ vars.DOCKERHUB_USERNAME }}/simpleclouddetect:snd cache-from: type=local,src=/tmp/.buildx-cache - cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max + # OPTIMIZATION: Changed mode=max to mode=min to speed up export on self-hosted runners + cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=min platforms: linux/amd64,linux/arm64 - name: Move cache @@ -72,4 +73,4 @@ jobs: run: | docker pull ${{ vars.DOCKERHUB_USERNAME }}/simpleclouddetect:snd IMAGE_SIZE=$(docker images --format "{{.Size}}" ${{ vars.DOCKERHUB_USERNAME }}/simpleclouddetect:snd | head -n1) - echo "Docker image size: $IMAGE_SIZE" + echo "Docker image size: $IMAGE_SIZE" \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index d7d6378..85eb95a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,7 +17,8 @@ COPY requirements.txt requirements-arm64.txt ./ # Install dependencies based on architecture ARG TARGETPLATFORM RUN pip install --no-cache-dir --upgrade pip && \ - if [ "$TARGETPLATFORM" = "linux/arm64" ]; then \ + if [ "$TARGETPLATFORM" = "linux/arm64" ]; \ + then \ pip install --no-cache-dir -r requirements-arm64.txt; \ else \ pip install --no-cache-dir -r requirements.txt; \ @@ -38,18 +39,21 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ RUN useradd -m -u 1000 appuser # Copy Python packages and binaries from builder +# NOTE: We generally do NOT need to chown site-packages; read-only access is sufficient for the app. COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages COPY --from=builder /usr/local/bin /usr/local/bin -# Copy application code (this layer changes frequently, so it's last) -COPY convert.py detect.py main.py start_services.sh ./ -COPY alpaca/ ./alpaca/ -COPY templates/ ./templates/ +# Copy application code with ownership set explicitly during copy +# This avoids the expensive "chown -R" layer later +COPY --chown=appuser:appuser convert.py detect.py main.py start_services.sh ./ +COPY --chown=appuser:appuser alpaca/ ./alpaca/ +COPY --chown=appuser:appuser templates/ ./templates/ -# Fix line endings and make the startup script executable and set ownership +# Fix line endings and make the startup script executable +# We only chown the specific script we modified if necessary, not the whole /app recursively RUN dos2unix start_services.sh && \ chmod +x start_services.sh && \ - chown -R appuser:appuser /app + chown appuser:appuser start_services.sh # Switch to non-root user USER appuser @@ -59,4 +63,4 @@ HEALTHCHECK --interval=60s --timeout=10s --start-period=30s --retries=3 \ CMD curl -f http://localhost:${ALPACA_PORT:-11111}/api/v1/safetymonitor/${ALPACA_DEVICE_NUMBER:-0}/connected || exit 1 # Run the startup script to launch unified service -CMD ["./start_services.sh"] +CMD ["./start_services.sh"] \ No newline at end of file From 0a7c0ddcfb63c772562f01719945718e57a554df Mon Sep 17 00:00:00 2001 From: Kumar Challa Date: Thu, 1 Jan 2026 19:15:41 -0600 Subject: [PATCH 17/21] feat: reorder client table columns for improved readability and update status indicators --- templates/setup.html | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/templates/setup.html b/templates/setup.html index 6e2de96..b156dba 100644 --- a/templates/setup.html +++ b/templates/setup.html @@ -808,8 +808,8 @@

☁️ SimpleCloudDetect

- - + + @@ -818,10 +818,10 @@

☁️ SimpleCloudDetect

{% for client in client_list %} - - + From b08e05e799e31d1fc4043f5851ce527cdf571470 Mon Sep 17 00:00:00 2001 From: Kumar Challa Date: Thu, 1 Jan 2026 19:44:41 -0600 Subject: [PATCH 18/21] feat: add persistent config option for docker container --- Dockerfile | 6 ++++++ alpaca/__init__.py | 37 +++++++++++++++++++---------------- alpaca/config.py | 48 ++++++++++++++++++++++++++-------------------- readme.md | 21 +++++++++++++++++++- verify_config.py | 0 5 files changed, 73 insertions(+), 39 deletions(-) create mode 100644 verify_config.py diff --git a/Dockerfile b/Dockerfile index 85eb95a..8bec088 100644 --- a/Dockerfile +++ b/Dockerfile @@ -55,9 +55,15 @@ RUN dos2unix start_services.sh && \ chmod +x start_services.sh && \ chown appuser:appuser start_services.sh +# Create configuration directory +RUN mkdir -p /config && chown appuser:appuser /config + # Switch to non-root user USER appuser +# Set configuration file path +ENV CONFIG_FILE=/config/alpaca_config.json + # FIX: Add healthcheck to ensure container restarts if Python process hangs HEALTHCHECK --interval=60s --timeout=10s --start-period=30s --retries=3 \ CMD curl -f http://localhost:${ALPACA_PORT:-11111}/api/v1/safetymonitor/${ALPACA_DEVICE_NUMBER:-0}/connected || exit 1 diff --git a/alpaca/__init__.py b/alpaca/__init__.py index 6af5f4f..c8631a9 100644 --- a/alpaca/__init__.py +++ b/alpaca/__init__.py @@ -15,23 +15,26 @@ def create_app(): app = Flask(__name__, template_folder='../templates') CORS(app) - # Load configs - alpaca_cfg = AlpacaConfig.load_from_file() - if alpaca_cfg is None: - # Create default config from environment - alpaca_cfg = AlpacaConfig( - port=int(os.getenv('ALPACA_PORT', '11111')), - device_number=int(os.getenv('ALPACA_DEVICE_NUMBER', '0')), - detection_interval=int(os.getenv('DETECT_INTERVAL', '30')), - update_interval=int(os.getenv('ALPACA_UPDATE_INTERVAL', '30')) - ) - alpaca_cfg.save_to_file() - else: - # Override settings from environment if provided - alpaca_cfg.port = int(os.getenv('ALPACA_PORT', str(alpaca_cfg.port))) - alpaca_cfg.device_number = int(os.getenv('ALPACA_DEVICE_NUMBER', str(alpaca_cfg.device_number))) - alpaca_cfg.detection_interval = int(os.getenv('DETECT_INTERVAL', str(alpaca_cfg.detection_interval))) - alpaca_cfg.update_interval = int(os.getenv('ALPACA_UPDATE_INTERVAL', str(alpaca_cfg.update_interval))) + # 1. Initialize configuration from Environment Variables (Base Config) + # This establishes the defaults if no file exists. + alpaca_cfg = AlpacaConfig( + port=int(os.getenv('ALPACA_PORT', '11111')), + device_number=int(os.getenv('ALPACA_DEVICE_NUMBER', '0')), + detection_interval=int(os.getenv('DETECT_INTERVAL', '30')), + update_interval=int(os.getenv('ALPACA_UPDATE_INTERVAL', '30')) + ) + + # 2. Load from file and override environment settings if file exists + # This ensures user settings (in file) take precedence over preconfigured env vars, + # but Env vars are still preserved if the file doesn't specify them. + file_settings = AlpacaConfig.load_settings_from_file() + if file_settings: + # Update our base config with ONLY the values explicitly in the file + for key, value in file_settings.items(): + setattr(alpaca_cfg, key, value) + + # 3. Save the final configuration to ensure the file exists and is current + alpaca_cfg.save_to_file() detect_cfg = DetectConfig.from_env() diff --git a/alpaca/config.py b/alpaca/config.py index 0c8596f..4a7f735 100644 --- a/alpaca/config.py +++ b/alpaca/config.py @@ -60,40 +60,46 @@ class AlpacaConfig: ntp_server: str = field(default_factory=lambda: os.environ.get('NTP_SERVER', 'pool.ntp.org')) timezone: str = field(default_factory=lambda: os.environ.get('TZ', 'UTC')) - def save_to_file(self, filepath: str = "alpaca_config.json"): + @classmethod + def get_config_path(cls) -> str: + """Get configuration file path from environment or default""" + return os.environ.get('CONFIG_FILE', 'alpaca_config.json') + + def save_to_file(self): """Save configuration to JSON file""" + filepath = self.get_config_path() try: + # Create directory if it doesn't exist (for /config volume usage) + os.makedirs(os.path.dirname(os.path.abspath(filepath)), exist_ok=True) + config_dict = asdict(self) with open(filepath, 'w') as f: json.dump(config_dict, f, indent=2) logger.info(f"Configuration saved to {filepath}") except Exception as e: - logger.error(f"Failed to save configuration: {e}") + logger.error(f"Failed to save configuration to {filepath}: {e}") @classmethod - def load_from_file(cls, filepath: str = "alpaca_config.json"): - """Load configuration from JSON file with backward compatibility""" + def load_settings_from_file(cls) -> dict: + """Load configuration dictionary from JSON file, filtering for valid fields""" + filepath = cls.get_config_path() try: if os.path.exists(filepath): with open(filepath, 'r') as f: config_dict = json.load(f) - # Ensure new fields exist with defaults for backward compatibility - if 'default_threshold' not in config_dict: - config_dict['default_threshold'] = 50.0 - if 'class_thresholds' not in config_dict: - config_dict['class_thresholds'] = {} - if 'debounce_to_safe_sec' not in config_dict: - config_dict['debounce_to_safe_sec'] = 60 - if 'debounce_to_unsafe_sec' not in config_dict: - config_dict['debounce_to_unsafe_sec'] = 0 - if 'detection_interval' not in config_dict: - config_dict['detection_interval'] = 30 - if 'image_url' not in config_dict: - config_dict['image_url'] = os.environ.get('IMAGE_URL', '') - - logger.info(f"Configuration loaded from {filepath}") - return cls(**config_dict) + # Filter dictionary to only include valid fields for this dataclass + valid_fields = {f.name for f in cls.__dataclass_fields__.values()} + return {k: v for k, v in config_dict.items() if k in valid_fields} except Exception as e: - logger.error(f"Failed to load configuration: {e}") + logger.error(f"Failed to load configuration from {filepath}: {e}") + return {} + + @classmethod + def load_from_file(cls): + """Load configuration from JSON file with backward compatibility""" + settings = cls.load_settings_from_file() + if settings: + logger.info(f"Configuration loaded from {cls.get_config_path()}") + return cls(**settings) return None diff --git a/readme.md b/readme.md index 51b6d7b..fe215ef 100644 --- a/readme.md +++ b/readme.md @@ -49,6 +49,7 @@ A Machine Learning-based cloud detection system for AllSky cameras with MQTT and ```shell docker run -d --name simple-cloud-detect --network=host \ + -v /path/to/config:/config \ -v /path/to/keras_model.h5:/app/keras_model.h5 \ -v /path/to/labels.txt:/app/labels.txt \ -e IMAGE_URL="http://your-allsky-camera/image.jpg" \ @@ -147,6 +148,16 @@ docker pull chvvkumar/simpleclouddetect:latest > **Note:** For detailed Alpaca configuration, see **[ALPACA_README.md](ALPACA_README.md)** +### Persistent Configuration + +To ensure your settings (device name, thresholds, etc.) are saved across container restarts, mount the `/config` directory: + +```shell +-v /path/to/config:/config +``` + +The application will save `alpaca_config.json` in this directory. If a config file exists, it will take precedence over environment variables for the settings it contains. + ### Raspberry Pi Support **Multi-architecture support:** The Docker images are built for both AMD64 (x86_64) and ARM64 (Raspberry Pi 4/5). Docker will automatically pull the correct image for your platform. @@ -162,6 +173,7 @@ For Raspberry Pi, use the same docker commands. The ARM64 build uses full Tensor **With URL-based image:** ```shell docker run -d --name simple-cloud-detect --network=host \ + -v /path/to/config:/config \ -v /path/to/keras_model.h5:/app/keras_model.h5 \ -v /path/to/labels.txt:/app/labels.txt \ -e IMAGE_URL="http://allskypi5.lan/current/resized/image.jpg" \ @@ -180,6 +192,7 @@ docker run -d --name simple-cloud-detect --network=host \ **With local file:** ```shell docker run -d --name simple-cloud-detect --network=host \ + -v /path/to/config:/config \ -v $HOME/path/to/image.jpg:/tmp/image.jpg \ -v /path/to/keras_model.h5:/app/keras_model.h5 \ -v /path/to/labels.txt:/app/labels.txt \ @@ -196,6 +209,7 @@ docker run -d --name simple-cloud-detect --network=host \ **With URL-based image:** ```shell docker run -d --name simple-cloud-detect --network=host \ + -v /path/to/config:/config \ -v /path/to/keras_model.h5:/app/keras_model.h5 \ -v /path/to/labels.txt:/app/labels.txt \ -e IMAGE_URL="http://allskypi5.lan/current/resized/image.jpg" \ @@ -214,6 +228,7 @@ To use your own trained model and labels: ```shell docker run -d --name simple-cloud-detect --network=host \ + -v /path/to/config:/config \ -v /path/to/your/keras_model.h5:/app/keras_model.h5 \ -v /path/to/your/labels.txt:/app/labels.txt \ -e IMAGE_URL="http://allskypi5.lan/image.jpg" \ @@ -250,6 +265,7 @@ services: - ALPACA_PORT=11111 - ALPACA_UPDATE_INTERVAL=30 volumes: + - /path/to/config:/config - /home/pi/git/simpleCloudDetect/keras_model.h5:/app/keras_model.h5 - /home/pi/git/simpleCloudDetect/labels.txt:/app/labels.txt ``` @@ -273,6 +289,7 @@ services: - DETECT_INTERVAL=60 - VERIFY_SSL=false volumes: + - /path/to/config:/config - /home/pi/git/simpleCloudDetect/keras_model.h5:/app/keras_model.h5 - /home/pi/git/simpleCloudDetect/labels.txt:/app/labels.txt ``` @@ -292,7 +309,8 @@ services: - MQTT_DISCOVERY_MODE=homeassistant - DEVICE_ID=clouddetect_001 volumes: - - '$HOME/path/to/image.jpg:/tmp/image.jpg' + - /path/to/config:/config + - /path/to/image.jpg:/tmp/image.jpg - /home/pi/git/simpleCloudDetect/keras_model.h5:/app/keras_model.h5 - /home/pi/git/simpleCloudDetect/labels.txt:/app/labels.txt ``` @@ -401,6 +419,7 @@ While an example model is included, **training your own model with your camera's **For Docker:** ```shell docker run -d --name simple-cloud-detect --network=host \ + -v /path/to/config:/config \ -v /path/to/your/keras_model.h5:/app/keras_model.h5 \ -v /path/to/your/labels.txt:/app/labels.txt \ # ...other environment variables... diff --git a/verify_config.py b/verify_config.py new file mode 100644 index 0000000..e69de29 From ae726012157676ff6d4774dff8da0b90b30ad462 Mon Sep 17 00:00:00 2001 From: Kumar Challa Date: Thu, 1 Jan 2026 20:00:11 -0600 Subject: [PATCH 19/21] feat: update README with config directory permissions and usage instructions --- readme.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/readme.md b/readme.md index fe215ef..da5411e 100644 --- a/readme.md +++ b/readme.md @@ -156,7 +156,25 @@ To ensure your settings (device name, thresholds, etc.) are saved across contain -v /path/to/config:/config ``` -The application will save `alpaca_config.json` in this directory. If a config file exists, it will take precedence over environment variables for the settings it contains. +The application will save `alpaca_config.json` in this directory. If a config file exists, it will take precedence for environment variables for the settings it contains. + +**Setting Proper Permissions:** + +The config directory must be writable by the container user. To ensure proper permissions: + +```shell +# Create the config directory +mkdir -p /path/to/config + +# Set appropriate permissions (Linux/macOS) +chmod 777 /path/to/config + +# Or set ownership to a specific user (uses current user's UID, recommended for better security) +sudo chown $(id -u):$(id -g) /path/to/config +chmod 755 /path/to/config +``` + +> **Note:** The container runs as user ID 1000 by default. If you encounter permission errors like `[Errno 13] Permission denied: '/config/alpaca_config.json'`, verify that the config directory is writable by the container user. ### Raspberry Pi Support From fc2d405652f52c9be3ee72125e3d9f9ba0b14768 Mon Sep 17 00:00:00 2001 From: Kumar Challa Date: Thu, 1 Jan 2026 20:07:46 -0600 Subject: [PATCH 20/21] feat: enhance configuration saving with detailed permission error logging and update README with config directory setup instructions --- alpaca/config.py | 14 +++++++++++++- readme.md | 7 +++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/alpaca/config.py b/alpaca/config.py index 4a7f735..7c2e119 100644 --- a/alpaca/config.py +++ b/alpaca/config.py @@ -70,12 +70,24 @@ def save_to_file(self): filepath = self.get_config_path() try: # Create directory if it doesn't exist (for /config volume usage) - os.makedirs(os.path.dirname(os.path.abspath(filepath)), exist_ok=True) + dir_path = os.path.dirname(os.path.abspath(filepath)) + os.makedirs(dir_path, exist_ok=True) config_dict = asdict(self) with open(filepath, 'w') as f: json.dump(config_dict, f, indent=2) logger.info(f"Configuration saved to {filepath}") + except PermissionError: + # Enhanced error logging for permission issues + try: + dir_path = os.path.dirname(os.path.abspath(filepath)) + stat_info = os.stat(dir_path) + logger.error(f"Permission denied saving to {filepath}. " + f"Directory '{dir_path}' is owned by UID {stat_info.st_uid} with mode {oct(stat_info.st_mode)[-3:]}. " + f"Container running as UID {os.getuid()}. " + f"Fix with: sudo chown {os.getuid()} {dir_path}") + except Exception: + logger.error(f"Failed to save configuration to {filepath}: [Errno 13] Permission denied") except Exception as e: logger.error(f"Failed to save configuration to {filepath}: {e}") diff --git a/readme.md b/readme.md index da5411e..e8b70f0 100644 --- a/readme.md +++ b/readme.md @@ -47,6 +47,13 @@ A Machine Learning-based cloud detection system for AllSky cameras with MQTT and **Docker (Recommended):** +1. Prepare the configuration directory: +```shell +mkdir -p /path/to/config +sudo chown 1000:1000 /path/to/config +``` + +2. Run the container: ```shell docker run -d --name simple-cloud-detect --network=host \ -v /path/to/config:/config \ From babcdb53ec0cfac149101cd1e69fac0611af3a11 Mon Sep 17 00:00:00 2001 From: Kumar Challa Date: Thu, 1 Jan 2026 20:24:43 -0600 Subject: [PATCH 21/21] feat: enhance tag comparison logic to exclude dev tags on main branch --- .github/workflows/build-and-release.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 1844903..8c5d354 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -232,7 +232,12 @@ jobs: NEW_TAG="${{ needs.build.outputs.tag }}" # Get all matching tags, exclude the new tag if it exists, and get the latest - PREVIOUS_TAG=$(git tag -l "${TAG_PATTERN}" | grep -v "^${NEW_TAG}$" | sort -V | tail -n1) + # For main branch, also exclude dev tags (v-dev-*) to prevent incorrect comparisons + if [ "$BRANCH_NAME" = "main" ]; then + PREVIOUS_TAG=$(git tag -l "${TAG_PATTERN}" | grep -v "^${NEW_TAG}$" | grep -v "^v-.*-" | sort -V | tail -n1) + else + PREVIOUS_TAG=$(git tag -l "${TAG_PATTERN}" | grep -v "^${NEW_TAG}$" | sort -V | tail -n1) + fi if [ -z "$PREVIOUS_TAG" ]; then echo "No previous tag found, showing last 20 commits"
IP ↕Status ↕Status ↕IP ↕ Duration ↕ Connected ↕ Disconnected ↕
{{ client.ip }} - {{ '●' if client.status == 'connected' else '○' }} + + {{ '■' if client.status == 'connected' else '□' }} {{ client.ip }} {{ client.duration }} {{ client.connected_time }} {{ client.disconnected_time }}
-
Current Detection
-
+
Condition
{{ current_condition }}
@@ -1726,7 +1725,7 @@ def setup_device(device_number: int):
{{ last_update }}
-
Docker Container Uptime
+
Container Uptime
{{ container_uptime }}
From 6c07e946bbe5498f4ee7de83f228a8662886ce3d Mon Sep 17 00:00:00 2001 From: Kumar Challa Date: Wed, 31 Dec 2025 13:36:22 -0600 Subject: [PATCH 05/21] fix: update label for connection duration to session duration in setup_device function --- alpaca_safety_monitor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alpaca_safety_monitor.py b/alpaca_safety_monitor.py index f4cbabb..ba12aa3 100644 --- a/alpaca_safety_monitor.py +++ b/alpaca_safety_monitor.py @@ -1771,7 +1771,7 @@ def setup_device(device_number: int):

{% if connection_duration %}

- Duration: {{ connection_duration }} + Session Duration: {{ connection_duration }}

{% endif %} {% if last_connected %} From 2623fe3277fea90bf72afbc40dcfb0a021d2b865 Mon Sep 17 00:00:00 2001 From: Kumar Challa Date: Wed, 31 Dec 2025 13:49:16 -0600 Subject: [PATCH 06/21] feat: track and display last session duration in safety monitor --- alpaca_safety_monitor.py | 38 +++++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/alpaca_safety_monitor.py b/alpaca_safety_monitor.py index ba12aa3..bf7305b 100644 --- a/alpaca_safety_monitor.py +++ b/alpaca_safety_monitor.py @@ -141,6 +141,7 @@ def __init__(self, alpaca_config: AlpacaConfig, detect_config: DetectConfig): self.disconnected_at: Optional[datetime] = None self.last_connected_at: Optional[datetime] = None # Track last successful connection self.client_ip: Optional[str] = None # Track connected client's IP address + self.last_session_duration: Optional[float] = None # Cloud detection state - use single lock for all state self.latest_detection: Optional[Dict[str, Any]] = { @@ -456,6 +457,9 @@ def disconnect(self): # Just mark as disconnected - detection loop keeps running for MQTT/web UI self.connected = False self.disconnected_at = get_current_time(self.alpaca_config.timezone) + if self.connected_at: + duration = (self.disconnected_at - self.connected_at).total_seconds() + self.last_session_duration = duration self.connected_at = None logger.info("ASCOM client disconnected from safety monitor") except Exception as e: @@ -1769,21 +1773,29 @@ def setup_device(device_number: int):

Status: {{ ascom_status }}

- {% if connection_duration %} + {% if client_ip %}

- Session Duration: {{ connection_duration }} + Client: {{ client_ip }}

{% endif %} + {% if ascom_status == 'Connected' %} + {% if connection_duration %} +

+ Duration: {{ connection_duration }} +

+ {% endif %} + {% else %} + {% if last_session_duration %} +

+ Last Session Duration: {{ last_session_duration }} +

+ {% endif %} + {% endif %} {% if last_connected %}

Last Connected: {{ last_connected }}

{% endif %} - {% if client_ip %} -

- Client: {{ client_ip }} -

- {% endif %} {% if last_disconnected %}

Last Disconnected: {{ last_disconnected }} @@ -2510,6 +2522,7 @@ def setup_device(device_number: int): last_update = 'Never' # Check ASCOM connection status + last_session_duration_str = None if safety_monitor.connected: ascom_status = 'Connected' ascom_status_class = 'status-connected' @@ -2531,6 +2544,16 @@ def setup_device(device_number: int): ascom_status = 'Disconnected' ascom_status_class = 'status-disconnected' connection_duration = None + if safety_monitor.last_session_duration is not None: + duration = safety_monitor.last_session_duration + hours, remainder = divmod(int(duration), 3600) + minutes, seconds = divmod(remainder, 60) + if hours > 0: + last_session_duration_str = f"{hours}h {minutes}m {seconds}s" + elif minutes > 0: + last_session_duration_str = f"{minutes}m {seconds}s" + else: + last_session_duration_str = f"{seconds}s" # Format last connected/disconnected timestamps with current timezone last_connected = None @@ -2612,6 +2635,7 @@ def setup_device(device_number: int): ascom_safe_color=ascom_safe_color, safety_history=safety_history, connection_duration=connection_duration, + last_session_duration=last_session_duration_str, client_ip=safety_monitor.client_ip, last_connected=last_connected, last_disconnected=last_disconnected, From 7a1a90dfc0aa9bd4f95cab55fa3b2fc91b5b3197 Mon Sep 17 00:00:00 2001 From: Kumar Challa Date: Thu, 1 Jan 2026 13:57:46 -0600 Subject: [PATCH 07/21] feat: implement multi-client support in safety monitor and update connection status display handling in the UI --- .gitignore | 1 + alpaca_safety_monitor.py | 355 +++++++++++++++++++++++++-------------- verify_multi_client.py | 164 ++++++++++++++++++ 3 files changed, 390 insertions(+), 130 deletions(-) create mode 100644 .gitignore create mode 100644 verify_multi_client.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e736cba --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +docker-compose.yaml \ No newline at end of file diff --git a/alpaca_safety_monitor.py b/alpaca_safety_monitor.py index bf7305b..3c70437 100644 --- a/alpaca_safety_monitor.py +++ b/alpaca_safety_monitor.py @@ -17,7 +17,7 @@ from dataclasses import dataclass, asdict, field from datetime import datetime from zoneinfo import ZoneInfo -from typing import Optional, Dict, Any +from typing import Optional, Dict, Any, Tuple import json import base64 import io @@ -135,12 +135,14 @@ def __init__(self, alpaca_config: AlpacaConfig, detect_config: DetectConfig): self.transaction_lock = threading.Lock() # Device state - self.connected = False + self.connected_clients: Dict[Tuple[str, int], datetime] = {} # (IP, ClientID) -> ConnectionTime + self.disconnected_clients: Dict[Tuple[str, int], Tuple[datetime, datetime]] = {} # (IP, ClientID) -> (ConnectionTime, DisconnectionTime) + self.connection_lock = threading.Lock() self.connecting = False self.connected_at: Optional[datetime] = None self.disconnected_at: Optional[datetime] = None self.last_connected_at: Optional[datetime] = None # Track last successful connection - self.client_ip: Optional[str] = None # Track connected client's IP address + self.client_ip: Optional[str] = None # Track connected client's IP address (deprecated in multi-client) self.last_session_duration: Optional[float] = None # Cloud detection state - use single lock for all state @@ -432,41 +434,57 @@ def _detection_loop(self): logger.info("Detection loop stopped") - def connect(self, client_ip: Optional[str] = None): - """Connect to the device (ASCOM client connection)""" - if self.connected: - return - - try: - # Just mark as connected - detection loop is already running - self.connected = True - self.connected_at = get_current_time(self.alpaca_config.timezone) - self.last_connected_at = self.connected_at - self.client_ip = client_ip - logger.info(f"ASCOM client connected to safety monitor from {client_ip or 'unknown'}") - except Exception as e: - logger.error(f"Failed to connect: {e}") - raise - - def disconnect(self): - """Disconnect from the device (ASCOM client disconnection)""" - if not self.connected: - return - - try: - # Just mark as disconnected - detection loop keeps running for MQTT/web UI - self.connected = False - self.disconnected_at = get_current_time(self.alpaca_config.timezone) - if self.connected_at: - duration = (self.disconnected_at - self.connected_at).total_seconds() - self.last_session_duration = duration - self.connected_at = None - logger.info("ASCOM client disconnected from safety monitor") - except Exception as e: - logger.error(f"Error during disconnect: {e}") - + @property + def is_connected(self) -> bool: + """Check if any clients are connected""" + with self.connection_lock: + return len(self.connected_clients) > 0 + + def connect(self, client_ip: str, client_id: int): + """Connect a client to the device""" + with self.connection_lock: + # Add or update client session + key = (client_ip, client_id) + self.connected_clients[key] = get_current_time(self.alpaca_config.timezone) + + # If this is the first connection, set global connected_at + if len(self.connected_clients) == 1: + self.connected_at = self.connected_clients[key] + self.last_connected_at = self.connected_at + self.disconnected_at = None + + logger.info(f"Client connected: {client_ip} (ID: {client_id}). Total clients: {len(self.connected_clients)}") + + def disconnect(self, client_ip: str, client_id: int): + """Disconnect a client from the device""" + with self.connection_lock: + key = (client_ip, client_id) + if key in self.connected_clients: + # Move to disconnected history + conn_time = self.connected_clients[key] + disc_time = get_current_time(self.alpaca_config.timezone) + self.disconnected_clients[key] = (conn_time, disc_time) + + del self.connected_clients[key] + logger.info(f"Client disconnected: {client_ip} (ID: {client_id}). Total clients: {len(self.connected_clients)}") + + # If no clients left, update global disconnected state + if len(self.connected_clients) == 0: + self.disconnected_at = disc_time + if self.connected_at: + duration = (self.disconnected_at - self.connected_at).total_seconds() + self.last_session_duration = duration + self.connected_at = None + logger.info("All clients disconnected") + else: + logger.warning(f"Attempted to disconnect unknown client: {client_ip} (ID: {client_id})") + def is_safe(self) -> bool: """Determine if conditions are safe based on latest detection""" + # Safety Fail-safe: Always return False if not connected + if not self.is_connected: + return False + with self.detection_lock: return self._stable_safe_state @@ -474,8 +492,8 @@ def get_device_state(self) -> list: """Get current operational state""" # Per ASCOM spec: DeviceState should only include operational properties # For SafetyMonitor, only IsSafe is operational - with self.detection_lock: - return [{"Name": "IsSafe", "Value": self._stable_safe_state if self.connected else False}] + is_safe_val = self.is_safe() + return [{"Name": "IsSafe", "Value": is_safe_val}] def _get_arg(self, key: str, default: Any = None) -> str: """Case-insensitive argument retrieval from request values""" @@ -533,7 +551,8 @@ def get_issafe(device_number: int): # Per ASCOM spec: Always return a value, never an error # If disconnected, return False (unsafe) to protect equipment - is_safe = safety_monitor.is_safe() if safety_monitor.connected else False + # is_safe() now handles the connection check internally + is_safe = safety_monitor.is_safe() return jsonify(safety_monitor.create_response( value=is_safe, @@ -550,7 +569,7 @@ def get_connected(device_number: int): _, client_tx_id = safety_monitor.get_client_params() return jsonify(safety_monitor.create_response( - value=safety_monitor.connected, + value=safety_monitor.is_connected, client_transaction_id=client_tx_id )) @@ -562,7 +581,7 @@ def set_connected(device_number: int): if error_response: return error_response - _, client_tx_id = safety_monitor.get_client_params() + client_id, client_tx_id = safety_monitor.get_client_params() connected_str = safety_monitor._get_arg('Connected', '').strip().lower() if connected_str == 'true': @@ -577,12 +596,11 @@ def set_connected(device_number: int): )), 400 try: - if target_state != safety_monitor.connected: - if target_state: - client_ip = request.remote_addr - safety_monitor.connect(client_ip=client_ip) - else: - safety_monitor.disconnect() + client_ip = request.remote_addr + if target_state: + safety_monitor.connect(client_ip=client_ip, client_id=client_id) + else: + safety_monitor.disconnect(client_ip=client_ip, client_id=client_id) return jsonify(safety_monitor.create_response( client_transaction_id=client_tx_id @@ -617,11 +635,10 @@ def connect_device(device_number: int): if error_response: return error_response - _, client_tx_id = safety_monitor.get_client_params() + client_id, client_tx_id = safety_monitor.get_client_params() try: - if not safety_monitor.connected: - client_ip = request.remote_addr - safety_monitor.connect(client_ip=client_ip) + client_ip = request.remote_addr + safety_monitor.connect(client_ip=client_ip, client_id=client_id) return jsonify(safety_monitor.create_response(client_transaction_id=client_tx_id)) except Exception as e: logger.error(f"Failed to connect: {e}") @@ -639,10 +656,10 @@ def disconnect_device(device_number: int): if error_response: return error_response - _, client_tx_id = safety_monitor.get_client_params() + client_id, client_tx_id = safety_monitor.get_client_params() try: - if safety_monitor.connected: - safety_monitor.disconnect() + client_ip = request.remote_addr + safety_monitor.disconnect(client_ip=client_ip, client_id=client_id) return jsonify(safety_monitor.create_response(client_transaction_id=client_tx_id)) except Exception as e: logger.error(f"Error during disconnect: {e}") @@ -1770,36 +1787,42 @@ def setup_device(device_number: int):

-

- Status: {{ ascom_status }} -

- {% if client_ip %} -

- Client: {{ client_ip }} -

- {% endif %} - {% if ascom_status == 'Connected' %} - {% if connection_duration %} -

- Duration: {{ connection_duration }} +

+

+ Status: {{ ascom_status }}

- {% endif %} - {% else %} - {% if last_session_duration %} -

- Last Session Duration: {{ last_session_duration }} +

+ Active: {{ client_count }}

- {% endif %} - {% endif %} - {% if last_connected %} -

- Last Connected: {{ last_connected }} -

- {% endif %} - {% if last_disconnected %} -

- Last Disconnected: {{ last_disconnected }} -

+
+ + {% if client_list %} +
+ + + + + + + + + + + {% for client in client_list %} + + + + + + + {% endfor %} + +
Status ↕IP ↕Time ↕Dur ↕
+ {{ '●' if client.status == 'connected' else '○' }} + {{ client.ip }} + {{ client.time_label }} {{ client.time_val }} + {{ client.duration }}
+
{% endif %}
@@ -1827,6 +1850,46 @@ def setup_device(device_number: int):
+ + +
+
+ 🛠️ + Configuration Settings +
+ +
+ +
+ +
+
+
+ Image Fetch Interval + How often to download new images from AllSky camera and run AI analysis. Lower values = faster response but higher CPU usage. +
+
+
+ Recommended:
+ • Fast response: 15-30s
+ • Balanced: 30-60s
+ • Resource efficient: 60-120s +
+
+ +
+ ASCOM Update Interval + How often to re-check safety status. Should be ≥ Image Fetch Interval. +
+
+
+ Rule: Set equal to Image Fetch Interval for efficiency. +
+
+ +
+ Safe Wait Time + How long the sky must remain clear before the system reports "Safe". +
+
+
+ Why it matters:
+ Prevents opening the observatory roof during brief breaks in storms or passing clouds. + Set higher (e.g., 300s = 5 min) for unstable weather patterns.

+ Recommended:
+ • Stable climate: 60-120s
+ • Variable weather: 180-300s
+ • Storm-prone areas: 300-600s +
+
+ +
+ Unsafe Wait Time (Debounce to Unsafe) + How long bad weather must persist before the system reports "Unsafe". +
+
+
+ Why it matters:
+ Controls emergency response speed. Setting to 0 triggers immediate roof closure at first sign of danger. + Higher values (10-30s) can filter out brief sensor glitches.

+ Recommended:
+ • Automated roof: 0s (immediate)
+ • Manual operation: 5-10s
+ • Glitch filtering: 10-30s +
+
+ +
+ Confidence Threshold + Minimum AI confidence (%) required to trigger each weather condition. +
+
+
+ 📊 Key Concept:
+ Higher threshold = Less sensitive (AI must be more confident)
+ Lower threshold = More sensitive (AI triggers with less certainty)

+ + How it works:
+ If set to 80% for "Rain", the AI must be 80% confident it's raining to trigger. + Lower = more sensitive (fewer misses, more false alarms). Higher = less sensitive (more misses, fewer false alarms).

+ Tuning tips:
+ • Start at 50% (default)
+ • Lower for critical conditions (Rain: 40-50%)
+ • Higher for uncertain conditions (Overcast: 60-70%)
+ • Adjust based on false alarm rate +
+
+
+
+
+ + +
+ + +
+ +
+
+
+ + +
+

How Timing Works

+ +
+ +
+
+
📷
+
+
FETCH IMAGE
+
Every 60s
+
+
+
+ Downloads latest image from AllSky camera and runs AI analysis to detect weather conditions +
+
+ + +
+
+
🔄
+
+
ASCOM UPDATE
+
Every 30s
+
+
+
+ Re-checks safety status using latest detection result and applies debounce logic +
+
+ + +
+
+
+
+
SAFE WAIT TIME
+
60s
+
+
+
+ How long sky must stay clear before reporting "Safe" - prevents premature roof opening +
+
+
+ + +
+
+
+
+
UNSAFE WAIT TIME
+
Immediate
+
+
+
+ How long bad weather must persist before reporting "Unsafe" - 0 = instant roof closure +
+
+
+
+ + +
+
💡 EXAMPLE SCENARIO
+
+
+
+ +
+
+ + +

How often to fetch & analyze new images from AllSky camera

+
+ +
+ + +

How often to check safety status

+
+
+ +
+
+ + +

Delay before reporting safe conditions

+
+ +
+ + +

Delay before reporting unsafe conditions (0 = immediate)

+
+
+ + + + +
+
+
+
+ + +
+ +
+
+
+

+ Mark each weather condition as Safe or Unsafe for observatory operations. +

+ + +
+

Safe Conditions

+
+ {% for condition in safe_conditions %} +
+
+
{{ condition }}
+
+ + +
+
+
+ + + % + + +
+
+
+
+
{{ class_thresholds.get(condition, default_threshold) }}%
+
+
+
+ ✓ Triggers >{{ class_thresholds.get(condition, default_threshold) }}% +
+
+
+ {% endfor %} +
+
+ + +
+

Unsafe Conditions

+
+ {% for condition in unsafe_conditions %} +
+
+
{{ condition }}
+
+ + +
+
+
+ + + % + + +
+
+
+
+
{{ class_thresholds.get(condition, default_threshold) }}%
+
+
+
+ ✓ Triggers >{{ class_thresholds.get(condition, default_threshold) }}% +
+
+
+ {% endfor %} +
+
+ + + +
+
+
+
+ + + +
+