Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,33 @@
docker-compose.yaml
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
41 changes: 24 additions & 17 deletions detect.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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
Expand Down
70 changes: 70 additions & 0 deletions tests/test_config_optional_mqtt.py
Original file line number Diff line number Diff line change
@@ -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()
Loading