From 6bbbf9855b26d6d204d6b9a388396c47822b5b55 Mon Sep 17 00:00:00 2001 From: Alexander Borg Date: Sun, 7 Sep 2025 12:38:16 +0200 Subject: [PATCH] Fix jwt-token --- health_probe_simulator.py | 11 +- server/index.js | 21 ++ server/routes/deviceHealth.js | 121 ++++++++ server/routes/index.js | 4 + server/services/deviceHealthService.js | 377 +++++++++++++++++++++++++ test_device_health.py | 195 +++++++++++++ 6 files changed, 725 insertions(+), 4 deletions(-) create mode 100644 server/routes/deviceHealth.js create mode 100644 server/services/deviceHealthService.js create mode 100644 test_device_health.py diff --git a/health_probe_simulator.py b/health_probe_simulator.py index 27810a4..18b8969 100644 --- a/health_probe_simulator.py +++ b/health_probe_simulator.py @@ -134,10 +134,12 @@ class DeviceProbeSimulator: 'key': str(device_id) # Use device_id as the key } - print(f"šŸ“” Sending heartbeat: {payload}") + print(f"šŸ“” Sending heartbeat to {self.api_base_url}/detectors") + print(f" Payload: {payload}") + print(f" Headers: {dict(self.session.headers)}") response = self.session.post( - f"{self.api_base_url}/detectors", # Updated to use detectors endpoint + f"{self.api_base_url}/detectors", # Sending to /detectors endpoint json=payload, verify=False, timeout=10 @@ -146,11 +148,12 @@ class DeviceProbeSimulator: if response.status_code in [200, 201]: device_name = device.get('name', f'Device {device_id}') location = device.get('location_description', 'Unknown location') - print(f"šŸ’“ Device {device_id}: Heartbeat sent successfully ({device_name} - {location})") + print(f"āœ… Device {device_id}: Heartbeat sent successfully ({device_name} - {location})") return True else: print(f"āŒ Failed to send heartbeat for Device {device_id}: HTTP {response.status_code}") - print(f" Response: {response.text}") + print(f" Response headers: {dict(response.headers)}") + print(f" Response body: {response.text}") return False except requests.exceptions.Timeout: diff --git a/server/index.js b/server/index.js index 804e974..2a62e6e 100644 --- a/server/index.js +++ b/server/index.js @@ -12,6 +12,8 @@ const { sequelize } = require('./models'); const routes = require('./routes'); const { initializeSocketHandlers } = require('./services/socketService'); const AlertService = require('./services/alertService'); +const DeviceHealthService = require('./services/deviceHealthService'); +const { initializeHealthService } = require('./routes/deviceHealth'); const seedDatabase = require('./seedDatabase'); const errorHandler = require('./middleware/errorHandler'); const { apiDebugMiddleware } = require('./utils/apiDebugLogger'); @@ -152,6 +154,25 @@ async function startServer() { console.log('šŸ”„ Clear alert monitoring: āœ… Started'); + // Initialize Device Health Monitoring Service + const deviceHealthService = new DeviceHealthService(); + initializeHealthService(deviceHealthService); // Make it available to API routes + deviceHealthService.start(); + console.log('šŸ„ Device health monitoring: āœ… Started'); + + // Graceful shutdown for device health service + process.on('SIGTERM', () => { + console.log('SIGTERM received, shutting down gracefully'); + deviceHealthService.stop(); + process.exit(0); + }); + + process.on('SIGINT', () => { + console.log('SIGINT received, shutting down gracefully'); + deviceHealthService.stop(); + process.exit(0); + }); + console.log(`šŸ“Š Health check: http://localhost:${PORT}/health`); console.log(`🌐 API endpoint: http://localhost:${PORT}/api`); console.log('================================================\n'); diff --git a/server/routes/deviceHealth.js b/server/routes/deviceHealth.js new file mode 100644 index 0000000..29bdb47 --- /dev/null +++ b/server/routes/deviceHealth.js @@ -0,0 +1,121 @@ +const express = require('express'); +const router = express.Router(); +const DeviceHealthService = require('../services/deviceHealthService'); + +// Global instance (will be initialized on server start) +let deviceHealthService = null; + +// Initialize the service reference +const initializeHealthService = (service) => { + deviceHealthService = service; +}; + +// Get device health service status +router.get('/status', (req, res) => { + try { + if (!deviceHealthService) { + return res.status(503).json({ + success: false, + message: 'Device health service not initialized' + }); + } + + const status = deviceHealthService.getStatus(); + + res.json({ + success: true, + data: { + ...status, + checkIntervalMinutes: status.checkInterval / 60000, + offlineThresholdMinutes: status.offlineThreshold / 60000 + } + }); + } catch (error) { + console.error('Error getting device health status:', error); + res.status(500).json({ + success: false, + message: 'Failed to get device health status', + error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error' + }); + } +}); + +// Start device health monitoring +router.post('/start', (req, res) => { + try { + if (!deviceHealthService) { + return res.status(503).json({ + success: false, + message: 'Device health service not initialized' + }); + } + + deviceHealthService.start(); + + res.json({ + success: true, + message: 'Device health monitoring started' + }); + } catch (error) { + console.error('Error starting device health service:', error); + res.status(500).json({ + success: false, + message: 'Failed to start device health service', + error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error' + }); + } +}); + +// Stop device health monitoring +router.post('/stop', (req, res) => { + try { + if (!deviceHealthService) { + return res.status(503).json({ + success: false, + message: 'Device health service not initialized' + }); + } + + deviceHealthService.stop(); + + res.json({ + success: true, + message: 'Device health monitoring stopped' + }); + } catch (error) { + console.error('Error stopping device health service:', error); + res.status(500).json({ + success: false, + message: 'Failed to stop device health service', + error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error' + }); + } +}); + +// Trigger manual health check +router.post('/check', async (req, res) => { + try { + if (!deviceHealthService) { + return res.status(503).json({ + success: false, + message: 'Device health service not initialized' + }); + } + + await deviceHealthService.checkDeviceHealth(); + + res.json({ + success: true, + message: 'Manual health check completed' + }); + } catch (error) { + console.error('Error running manual health check:', error); + res.status(500).json({ + success: false, + message: 'Failed to run health check', + error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error' + }); + } +}); + +module.exports = { router, initializeHealthService }; diff --git a/server/routes/index.js b/server/routes/index.js index a9d4176..c381c28 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -7,6 +7,7 @@ const userRoutes = require('./user'); const alertRoutes = require('./alert'); const dashboardRoutes = require('./dashboard'); const healthRoutes = require('./health'); +const { router: deviceHealthRoutes } = require('./deviceHealth'); const debugRoutes = require('./debug'); const detectorsRoutes = require('./detectors'); const detectionsRoutes = require('./detections'); @@ -18,6 +19,7 @@ router.use('/v1/users', userRoutes); router.use('/v1/alerts', alertRoutes); router.use('/v1/dashboard', dashboardRoutes); router.use('/v1/health', healthRoutes); +router.use('/v1/device-health', deviceHealthRoutes); router.use('/v1/detectors', detectorsRoutes); router.use('/v1/detections', detectionsRoutes); router.use('/v1/drone-types', droneTypesRoutes); @@ -28,6 +30,7 @@ router.use('/users', userRoutes); router.use('/alerts', alertRoutes); router.use('/dashboard', dashboardRoutes); router.use('/health', healthRoutes); +router.use('/device-health', deviceHealthRoutes); router.use('/debug', debugRoutes); router.use('/detectors', detectorsRoutes); router.use('/detections', detectionsRoutes); @@ -46,6 +49,7 @@ router.get('/', (req, res) => { alerts: '/api/alerts', dashboard: '/api/dashboard', health: '/api/health', + 'device-health': '/api/device-health', 'drone-types': '/api/drone-types' }, documentation: '/api/docs' diff --git a/server/services/deviceHealthService.js b/server/services/deviceHealthService.js new file mode 100644 index 0000000..bb34ec0 --- /dev/null +++ b/server/services/deviceHealthService.js @@ -0,0 +1,377 @@ +const { Device, AlertRule, AlertLog, Heartbeat } = require('../models'); +const { Op } = require('sequelize'); + +class DeviceHealthService { + constructor() { + this.isRunning = false; + this.checkInterval = 5 * 60 * 1000; // Check every 5 minutes + this.offlineThreshold = 30 * 60 * 1000; // 30 minutes without heartbeat = offline + this.offlineDevices = new Map(); // Track devices that are already reported offline + this.intervalId = null; + } + + start() { + if (this.isRunning) { + console.log('⚔ Device Health Service is already running'); + return; + } + + console.log('⚔ Starting Device Health Monitoring Service'); + console.log(` Check Interval: ${this.checkInterval / 60000} minutes`); + console.log(` Offline Threshold: ${this.offlineThreshold / 60000} minutes`); + + this.isRunning = true; + + // Run initial check + this.checkDeviceHealth(); + + // Schedule periodic checks + this.intervalId = setInterval(() => { + this.checkDeviceHealth(); + }, this.checkInterval); + } + + stop() { + if (!this.isRunning) { + console.log('⚔ Device Health Service is not running'); + return; + } + + console.log('⚔ Stopping Device Health Monitoring Service'); + this.isRunning = false; + + if (this.intervalId) { + clearInterval(this.intervalId); + this.intervalId = null; + } + } + + async checkDeviceHealth() { + try { + console.log('šŸ” Checking device health status...'); + + const now = new Date(); + const activeDevices = await Device.findAll({ + where: { + is_active: true, + is_approved: true + }, + include: [{ + model: Heartbeat, + as: 'heartbeats', + limit: 1, + order: [['received_at', 'DESC']] + }] + }); + + let onlineCount = 0; + let offlineCount = 0; + let newlyOfflineDevices = []; + let recoveredDevices = []; + + for (const device of activeDevices) { + const timeSinceLastHeartbeat = device.last_heartbeat + ? (now - new Date(device.last_heartbeat)) / 1000 + : null; + + const expectedInterval = device.heartbeat_interval || 300; // 5 minutes default + const isOnline = timeSinceLastHeartbeat && timeSinceLastHeartbeat < (expectedInterval * 2); + const isOfflineForAlert = timeSinceLastHeartbeat && timeSinceLastHeartbeat > (this.offlineThreshold / 1000); + + if (isOnline) { + onlineCount++; + + // Check if device was previously offline and is now recovered + if (this.offlineDevices.has(device.id)) { + recoveredDevices.push(device); + this.offlineDevices.delete(device.id); + } + } else if (isOfflineForAlert) { + offlineCount++; + + // Check if this is a newly offline device + if (!this.offlineDevices.has(device.id)) { + newlyOfflineDevices.push(device); + this.offlineDevices.set(device.id, { + device, + offlineSince: new Date(device.last_heartbeat), + alertSent: false + }); + } + } + } + + console.log(`šŸ“Š Device Health Summary: ${onlineCount} online, ${offlineCount} offline`); + + if (newlyOfflineDevices.length > 0) { + console.log(`🚨 Found ${newlyOfflineDevices.length} newly offline devices`); + await this.handleOfflineDevices(newlyOfflineDevices); + } + + if (recoveredDevices.length > 0) { + console.log(`āœ… Found ${recoveredDevices.length} recovered devices`); + await this.handleRecoveredDevices(recoveredDevices); + } + + // Clean up old offline devices that are no longer active/approved + this.cleanupOfflineDevices(activeDevices); + + } catch (error) { + console.error('āŒ Error checking device health:', error); + } + } + + async handleOfflineDevices(offlineDevices) { + for (const device of offlineDevices) { + try { + // Find all active alert rules that monitor device offline events + const deviceOfflineRules = await AlertRule.findAll({ + where: { + is_active: true, + [Op.or]: [ + // Rules specifically for device offline monitoring + { conditions: { device_offline: true } }, + // Rules that include this specific device + { + [Op.and]: [ + { conditions: { device_ids: { [Op.contains]: [device.id] } } }, + { conditions: { device_offline: true } } + ] + } + ] + } + }); + + if (deviceOfflineRules.length === 0) { + console.log(`āš ļø No offline alert rules configured for device ${device.id} (${device.name})`); + continue; + } + + // Trigger alerts for each matching rule + for (const rule of deviceOfflineRules) { + await this.triggerDeviceOfflineAlert(rule, device); + } + + // Mark as alert sent + if (this.offlineDevices.has(device.id)) { + this.offlineDevices.get(device.id).alertSent = true; + } + + } catch (error) { + console.error(`āŒ Error handling offline device ${device.id}:`, error); + } + } + } + + async handleRecoveredDevices(recoveredDevices) { + for (const device of recoveredDevices) { + try { + // Find all active alert rules for this device + const deviceOfflineRules = await AlertRule.findAll({ + where: { + is_active: true, + [Op.or]: [ + { conditions: { device_offline: true } }, + { + [Op.and]: [ + { conditions: { device_ids: { [Op.contains]: [device.id] } } }, + { conditions: { device_offline: true } } + ] + } + ] + } + }); + + // Send recovery notifications + for (const rule of deviceOfflineRules) { + await this.triggerDeviceRecoveryAlert(rule, device); + } + + } catch (error) { + console.error(`āŒ Error handling recovered device ${device.id}:`, error); + } + } + } + + async triggerDeviceOfflineAlert(rule, device) { + try { + const AlertService = require('./alertService'); + const alertService = new AlertService(); + + const timeSinceLastHeartbeat = device.last_heartbeat + ? Math.floor((new Date() - new Date(device.last_heartbeat)) / 1000 / 60) + : 'unknown'; + + // Generate offline alert message + const message = this.generateOfflineMessage(device, timeSinceLastHeartbeat); + + // Send alerts through configured channels + const channels = rule.alert_channels || ['sms']; + + for (const channel of channels) { + try { + switch (channel) { + case 'sms': + if (rule.sms_phone_number) { + await alertService.sendSMSAlert(rule.sms_phone_number, message, rule, null); + console.log(`šŸ“± Device offline SMS alert sent for ${device.name} to ${rule.sms_phone_number}`); + } + break; + + case 'email': + if (rule.email || rule.user?.email) { + await alertService.sendEmailAlert(rule.email || rule.user.email, message, rule, null); + console.log(`šŸ“§ Device offline email alert sent for ${device.name}`); + } + break; + + case 'webhook': + if (rule.webhook_url) { + const webhookData = { + type: 'device_offline', + device: { + id: device.id, + name: device.name, + location: device.location_description, + last_heartbeat: device.last_heartbeat, + time_offline_minutes: timeSinceLastHeartbeat + }, + rule: { + id: rule.id, + name: rule.name + }, + timestamp: new Date().toISOString() + }; + + await alertService.sendWebhookAlert(rule.webhook_url, null, device, rule, webhookData); + console.log(`šŸ”— Device offline webhook alert sent for ${device.name}`); + } + break; + } + } catch (channelError) { + console.error(`āŒ Failed to send ${channel} alert for offline device ${device.id}:`, channelError); + } + } + + } catch (error) { + console.error(`āŒ Error triggering offline alert for device ${device.id}:`, error); + } + } + + async triggerDeviceRecoveryAlert(rule, device) { + try { + const AlertService = require('./alertService'); + const alertService = new AlertService(); + + // Generate recovery alert message + const message = this.generateRecoveryMessage(device); + + // Send recovery alerts through configured channels + const channels = rule.alert_channels || ['sms']; + + for (const channel of channels) { + try { + switch (channel) { + case 'sms': + if (rule.sms_phone_number) { + await alertService.sendSMSAlert(rule.sms_phone_number, message, rule, null); + console.log(`šŸ“± Device recovery SMS alert sent for ${device.name} to ${rule.sms_phone_number}`); + } + break; + + case 'email': + if (rule.email || rule.user?.email) { + await alertService.sendEmailAlert(rule.email || rule.user.email, message, rule, null); + console.log(`šŸ“§ Device recovery email alert sent for ${device.name}`); + } + break; + + case 'webhook': + if (rule.webhook_url) { + const webhookData = { + type: 'device_recovered', + device: { + id: device.id, + name: device.name, + location: device.location_description, + last_heartbeat: device.last_heartbeat + }, + rule: { + id: rule.id, + name: rule.name + }, + timestamp: new Date().toISOString() + }; + + await alertService.sendWebhookAlert(rule.webhook_url, null, device, rule, webhookData); + console.log(`šŸ”— Device recovery webhook alert sent for ${device.name}`); + } + break; + } + } catch (channelError) { + console.error(`āŒ Failed to send ${channel} recovery alert for device ${device.id}:`, channelError); + } + } + + } catch (error) { + console.error(`āŒ Error triggering recovery alert for device ${device.id}:`, error); + } + } + + generateOfflineMessage(device, timeSinceLastHeartbeat) { + const deviceName = device.name || `Device ${device.id}`; + const location = device.location_description || 'Unknown location'; + const lastSeen = device.last_heartbeat + ? new Date(device.last_heartbeat).toLocaleString('sv-SE') + : 'Never'; + + return `🚨 DEVICE OFFLINE ALERT 🚨\n\n` + + `šŸ“ LOCATION: ${location}\n` + + `šŸ”§ DEVICE: ${deviceName}\n` + + `ā° OFFLINE FOR: ${timeSinceLastHeartbeat} minutes\n` + + `šŸ“… LAST SEEN: ${lastSeen}\n\n` + + `āŒ Device has stopped sending heartbeats.\n` + + `šŸ”§ Check device power, network connection, or physical access.\n\n` + + `āš ļø Security monitoring may be compromised in this area.`; + } + + generateRecoveryMessage(device) { + const deviceName = device.name || `Device ${device.id}`; + const location = device.location_description || 'Unknown location'; + const recoveredAt = new Date().toLocaleString('sv-SE'); + + return `āœ… DEVICE RECOVERED āœ…\n\n` + + `šŸ“ LOCATION: ${location}\n` + + `šŸ”§ DEVICE: ${deviceName}\n` + + `ā° RECOVERED AT: ${recoveredAt}\n\n` + + `āœ… Device is now sending heartbeats again.\n` + + `šŸ›”ļø Security monitoring restored for this area.`; + } + + cleanupOfflineDevices(activeDevices) { + const activeDeviceIds = new Set(activeDevices.map(d => d.id)); + + for (const [deviceId] of this.offlineDevices) { + if (!activeDeviceIds.has(deviceId)) { + this.offlineDevices.delete(deviceId); + } + } + } + + getStatus() { + return { + isRunning: this.isRunning, + checkInterval: this.checkInterval, + offlineThreshold: this.offlineThreshold, + offlineDevicesCount: this.offlineDevices.size, + offlineDevices: Array.from(this.offlineDevices.entries()).map(([id, data]) => ({ + deviceId: id, + deviceName: data.device.name, + offlineSince: data.offlineSince, + alertSent: data.alertSent + })) + }; + } +} + +module.exports = DeviceHealthService; diff --git a/test_device_health.py b/test_device_health.py new file mode 100644 index 0000000..81d22bb --- /dev/null +++ b/test_device_health.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python3 +""" +Device Health Monitoring Test Script +Tests the device health monitoring system by checking current device status +and simulating offline/recovery scenarios. +""" + +import requests +import time +import os +from datetime import datetime + +# Disable SSL warnings for self-signed certificates +import warnings +warnings.filterwarnings('ignore', message='Unverified HTTPS request') + +# Configuration from environment variables +API_BASE_URL = os.getenv('API_BASE_URL', 'http://localhost:3002/api') +BASE_PATH = os.getenv('VITE_BASE_PATH', '').rstrip('/') + +# If BASE_PATH is set, construct the full URL +if BASE_PATH and not API_BASE_URL.endswith('/api'): + domain = API_BASE_URL.replace('/api', '').replace('/drones/api', '').replace('/uggla/api', '') + API_BASE_URL = f"{domain}{BASE_PATH}/api" + +def test_device_health_status(): + """Test device health monitoring status""" + print("šŸ” Testing Device Health Monitoring System") + print("=" * 60) + + try: + # Check device health service status + print("šŸ“Š Checking device health service status...") + response = requests.get(f"{API_BASE_URL}/device-health/status", verify=False, timeout=10) + + if response.status_code == 200: + data = response.json() + if data.get('success'): + status = data['data'] + print(f"āœ… Device Health Service Status:") + print(f" Running: {status['isRunning']}") + print(f" Check Interval: {status['checkIntervalMinutes']} minutes") + print(f" Offline Threshold: {status['offlineThresholdMinutes']} minutes") + print(f" Currently Offline Devices: {status['offlineDevicesCount']}") + + if status['offlineDevices']: + print(f" Offline Devices:") + for device in status['offlineDevices']: + print(f" - Device {device['deviceId']}: {device['deviceName']}") + print(f" Offline since: {device['offlineSince']}") + print(f" Alert sent: {device['alertSent']}") + else: + print(f" āœ… All devices are online") + else: + print(f"āŒ Failed to get status: {data.get('message', 'Unknown error')}") + else: + print(f"āŒ HTTP Error: {response.status_code}") + print(f" Response: {response.text}") + + except requests.exceptions.RequestException as e: + print(f"āŒ Network error: {e}") + except Exception as e: + print(f"āŒ Unexpected error: {e}") + +def test_manual_health_check(): + """Trigger a manual health check""" + print("\nšŸ”§ Triggering Manual Health Check") + print("-" * 40) + + try: + response = requests.post(f"{API_BASE_URL}/device-health/check", verify=False, timeout=10) + + if response.status_code == 200: + data = response.json() + if data.get('success'): + print(f"āœ… {data['message']}") + else: + print(f"āŒ Failed: {data.get('message', 'Unknown error')}") + else: + print(f"āŒ HTTP Error: {response.status_code}") + print(f" Response: {response.text}") + + except requests.exceptions.RequestException as e: + print(f"āŒ Network error: {e}") + except Exception as e: + print(f"āŒ Unexpected error: {e}") + +def test_device_list(): + """Check current device list and their status""" + print("\nšŸ“± Checking Device List and Status") + print("-" * 40) + + try: + response = requests.get(f"{API_BASE_URL}/devices", verify=False, timeout=10) + + if response.status_code == 200: + data = response.json() + if data.get('success') and data.get('data'): + devices = data['data'] + print(f"šŸ“Š Found {len(devices)} devices:") + + for device in devices: + status = device.get('stats', {}).get('status', 'unknown') + last_heartbeat = device.get('last_heartbeat') + time_since = device.get('stats', {}).get('time_since_last_heartbeat') + + status_icon = "āœ…" if status == "online" else "āŒ" if status == "offline" else "āš ļø" + + print(f" {status_icon} Device {device['id']}: {device.get('name', 'Unnamed')}") + print(f" Status: {status}") + print(f" Location: {device.get('location_description', 'No location')}") + print(f" Active: {device.get('is_active', False)}") + print(f" Approved: {device.get('is_approved', False)}") + + if last_heartbeat: + print(f" Last Heartbeat: {last_heartbeat}") + else: + print(f" Last Heartbeat: Never") + + if time_since is not None: + print(f" Time Since Last Heartbeat: {time_since:.0f} seconds") + print() + else: + print(f"āŒ No devices found or invalid response") + else: + print(f"āŒ HTTP Error: {response.status_code}") + print(f" Response: {response.text}") + + except requests.exceptions.RequestException as e: + print(f"āŒ Network error: {e}") + except Exception as e: + print(f"āŒ Unexpected error: {e}") + +def test_alert_rules(): + """Check if there are any device offline alert rules configured""" + print("\nāš ļø Checking Device Offline Alert Rules") + print("-" * 40) + + try: + response = requests.get(f"{API_BASE_URL}/alerts/rules", verify=False, timeout=10) + + if response.status_code == 200: + data = response.json() + if data.get('success') and data.get('data'): + rules = data['data'] + offline_rules = [] + + for rule in rules: + conditions = rule.get('conditions', {}) + if conditions.get('device_offline'): + offline_rules.append(rule) + + if offline_rules: + print(f"šŸ“‹ Found {len(offline_rules)} device offline alert rules:") + for rule in offline_rules: + print(f" šŸ“Œ Rule: {rule['name']}") + print(f" Description: {rule.get('description', 'No description')}") + print(f" Active: {rule.get('is_active', False)}") + print(f" Channels: {rule.get('alert_channels', [])}") + if rule.get('sms_phone_number'): + print(f" SMS: {rule['sms_phone_number']}") + print() + else: + print("āš ļø No device offline alert rules found!") + print(" You may want to create alert rules for device offline monitoring.") + else: + print(f"āŒ No alert rules found") + else: + print(f"āŒ HTTP Error: {response.status_code}") + print(f" Response: {response.text}") + + except requests.exceptions.RequestException as e: + print(f"āŒ Network error: {e}") + except Exception as e: + print(f"āŒ Unexpected error: {e}") + +def main(): + print("šŸ„ DEVICE HEALTH MONITORING TEST") + print("=" * 60) + print(f"šŸ“” API URL: {API_BASE_URL}") + print(f"ā° Test Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + print() + + # Run all tests + test_device_health_status() + test_device_list() + test_alert_rules() + test_manual_health_check() + + print("\n" + "=" * 60) + print("šŸ Device Health Monitoring Test Complete") + print("=" * 60) + +if __name__ == "__main__": + main()