diff --git a/.gitignore b/.gitignore index e736cba..74169ae 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,33 @@ -docker-compose.yaml \ No newline at end of file +docker-compose.yaml + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python + +# Virtual environments +.venv/ +venv/ +ENV/ +env/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Testing +.pytest_cache/ +.coverage +htmlcov/ + +# Logs +*.log \ No newline at end of file diff --git a/detect.py b/detect.py index 36d9ae0..39e68c0 100755 --- a/detect.py +++ b/detect.py @@ -31,9 +31,9 @@ class Config: image_url: str model_path: str label_path: str - broker: str + broker: Optional[str] port: int - topic: str + topic: Optional[str] detect_interval: int mqtt_username: Optional[str] mqtt_password: Optional[str] @@ -50,15 +50,19 @@ def from_env(cls) -> 'Config': discovery_mode = os.getenv('MQTT_DISCOVERY_MODE', 'legacy').lower() # Base required vars for all modes - required_vars = ['IMAGE_URL', 'MQTT_BROKER', 'DETECT_INTERVAL'] + required_vars = ['IMAGE_URL', 'DETECT_INTERVAL'] - # Legacy mode requires MQTT_TOPIC - if discovery_mode == 'legacy': - required_vars.append('MQTT_TOPIC') - # HA discovery mode requires DEVICE_ID - elif discovery_mode == 'homeassistant': - if not os.getenv('DEVICE_ID'): - raise ValueError("DEVICE_ID is required when MQTT_DISCOVERY_MODE is 'homeassistant'") + broker = os.getenv('MQTT_BROKER') + + # Validate MQTT settings only if broker is configured + if broker: + # Legacy mode requires MQTT_TOPIC + if discovery_mode == 'legacy' and not os.getenv('MQTT_TOPIC'): + required_vars.append('MQTT_TOPIC') + # HA discovery mode requires DEVICE_ID + elif discovery_mode == 'homeassistant': + if not os.getenv('DEVICE_ID'): + raise ValueError("DEVICE_ID is required when MQTT_DISCOVERY_MODE is 'homeassistant'") missing = [var for var in required_vars if not os.getenv(var)] if missing: @@ -68,17 +72,17 @@ def from_env(cls) -> 'Config': image_url=os.environ['IMAGE_URL'], model_path=os.getenv('MODEL_PATH', 'keras_model.h5'), label_path=os.getenv('LABEL_PATH', 'labels.txt'), - broker=os.environ['MQTT_BROKER'], - port=int(os.getenv('MQTT_PORT', '1883')), - topic=os.getenv('MQTT_TOPIC', ''), + broker=broker or None, + port=int(os.getenv('MQTT_PORT') or '1883'), + topic=os.getenv('MQTT_TOPIC') or None, detect_interval=int(os.environ['DETECT_INTERVAL']), - mqtt_username=os.getenv('MQTT_USERNAME'), - mqtt_password=os.getenv('MQTT_PASSWORD'), + mqtt_username=os.getenv('MQTT_USERNAME') or None, + mqtt_password=os.getenv('MQTT_PASSWORD') or None, verify_ssl=os.getenv('VERIFY_SSL', 'false').lower() == 'true', mqtt_discovery_mode=discovery_mode, mqtt_discovery_prefix=os.getenv('MQTT_DISCOVERY_PREFIX', 'homeassistant'), device_name=os.getenv('DEVICE_NAME', 'Cloud Detector'), - device_id=os.getenv('DEVICE_ID') + device_id=os.getenv('DEVICE_ID') or None ) class HADiscoveryManager: @@ -190,7 +194,7 @@ def __init__(self, config: Config, mqtt_client=None): self.ha_discovery = None # Initialize HA discovery if enabled - if self.config.mqtt_discovery_mode == 'homeassistant': + if self.mqtt_client and self.config.mqtt_discovery_mode == 'homeassistant': self.ha_discovery = HADiscoveryManager(self.config, self.mqtt_client) self.ha_discovery.publish_discovery_configs() @@ -332,6 +336,9 @@ def detect(self, return_image: bool = False) -> dict: def publish_result(self, result: dict): """Publish detection result to MQTT""" + if not self.mqtt_client: + return + try: if self.config.mqtt_discovery_mode == 'homeassistant': # Use HA discovery publishing diff --git a/tests/test_config_optional_mqtt.py b/tests/test_config_optional_mqtt.py new file mode 100644 index 0000000..44bec32 --- /dev/null +++ b/tests/test_config_optional_mqtt.py @@ -0,0 +1,70 @@ +import os +import unittest +from unittest.mock import patch +from detect import Config + +class TestConfigOptionalMqtt(unittest.TestCase): + + def setUp(self): + # Clear relevant environment variables before each test + self.env_vars = { + 'IMAGE_URL': 'http://example.com/image.jpg', + 'DETECT_INTERVAL': '60', + 'MQTT_BROKER': '', + 'MQTT_PORT': '', + 'MQTT_TOPIC': '', + 'MQTT_USERNAME': '', + 'MQTT_PASSWORD': '', + 'MQTT_DISCOVERY_MODE': '', + 'DEVICE_ID': '' + } + self.patcher = patch.dict(os.environ, self.env_vars) + self.patcher.start() + + def tearDown(self): + self.patcher.stop() + + def test_config_no_mqtt(self): + """Test that Config can be initialized without MQTT settings.""" + # Ensure MQTT variables are unset/empty + if 'MQTT_BROKER' in os.environ: + del os.environ['MQTT_BROKER'] + + config = Config.from_env() + + self.assertIsNone(config.broker) + self.assertIsNone(config.topic) + self.assertEqual(config.image_url, 'http://example.com/image.jpg') + + def test_config_with_mqtt(self): + """Test that Config is initialized correctly with MQTT settings.""" + os.environ['MQTT_BROKER'] = 'mqtt.example.com' + os.environ['MQTT_TOPIC'] = 'test/topic' + + config = Config.from_env() + + self.assertEqual(config.broker, 'mqtt.example.com') + self.assertEqual(config.topic, 'test/topic') + + def test_config_legacy_mode_missing_topic(self): + """Test that legacy mode requires topic only if broker is set.""" + os.environ['MQTT_BROKER'] = 'mqtt.example.com' + os.environ['MQTT_DISCOVERY_MODE'] = 'legacy' + # MQTT_TOPIC is missing + + with self.assertRaises(ValueError) as cm: + Config.from_env() + self.assertIn("Missing required environment variables", str(cm.exception)) + + def test_config_ha_mode_missing_device_id(self): + """Test that HA mode requires DEVICE_ID only if broker is set.""" + os.environ['MQTT_BROKER'] = 'mqtt.example.com' + os.environ['MQTT_DISCOVERY_MODE'] = 'homeassistant' + # DEVICE_ID is missing + + with self.assertRaises(ValueError) as cm: + Config.from_env() + self.assertIn("DEVICE_ID is required", str(cm.exception)) + +if __name__ == '__main__': + unittest.main()