| .. | ||
| tests | ||
| .dockerignore | ||
| API.md | ||
| config_server.py | ||
| config.dev.yaml | ||
| config.example.yaml | ||
| config.py | ||
| config.yaml | ||
| device_manager.py | ||
| device_status_monitor.py | ||
| Dockerfile | ||
| event_queue.py | ||
| logger_setup.py | ||
| main.py | ||
| mqtt_client.py | ||
| odoo_client.py | ||
| README.md | ||
| requirements.txt | ||
| session_detector.py | ||
| shelly_parser.py | ||
IoT MQTT Bridge for Odoo
Autonomous MQTT-to-Odoo Integration Service with PUSH Configuration Architecture
🎯 Overview
The IoT Bridge is a standalone Python microservice that bridges MQTT-based IoT devices (e.g., Shelly PM, Tasmota) with Odoo 18. It provides:
- Real-time MQTT monitoring with auto-reconnect
- Intelligent session detection via state machine
- Event normalization for multiple device types
- Autonomous operation with retry queues and config persistence
- PUSH-based configuration from Odoo
- RESTful HTTP API for health checks and config management
🏗️ Architecture
System Overview
┌─────────────┐ ┌──────────────────┐ ┌────────────────┐
│ IoT Device │ MQTT Messages │ IoT Bridge │ HTTP Events │ Odoo 18 │
│ (Shelly PM)│ ──────────────────> │ (Autonomous) │ ──────────────> │ (ERP/MES) │
│ │ │ │ <────────────────│ │
│ │ │ - MQTT Client │ Config Push │ - REST API │
│ │ │ - Session │ POST /config │ - Model Hooks │
│ │ │ Detector │ │ - UI Controls │
│ │ │ - Event Queue │ │ │
│ │ │ - HTTP Server │ │ │
└─────────────┘ └────────┬─────────┘ └────────────────┘
│
│ Subscribe
▼
┌──────────────────┐
│ Mosquitto Broker │
│ (MQTT Server) │
└──────────────────┘
Data Flow
- Device → Bridge: IoT devices publish MQTT messages (e.g.,
shellypmminig3-xxx/status/pm1:0) - Bridge Processing:
- Parse device-specific payload (Shelly, Tasmota, etc.)
- Run through session detection state machine
- Aggregate events (heartbeat interval: 300s)
- Queue for delivery with retry logic
- Bridge → Odoo: POST events to
/ows/iot/eventREST endpoint - Odoo → Bridge: Push device configuration updates via POST
/config
PUSH Configuration Architecture
OLD (deprecated): Bridge pulled config from Odoo via GET /ows/iot/config
NEW (current): Odoo pushes config to Bridge via POST /config
Benefits:
- ✅ No periodic polling overhead
- ✅ Instant config updates when devices change
- ✅ Bridge runs autonomously with persisted config
- ✅ Automatic triggers via Odoo model hooks (create/write/unlink)
📁 Project Structure
iot_bridge/
├── main.py # Main entry point & startup orchestration
├── config_server.py # FastAPI HTTP server for config mgmt
├── device_manager.py # Dynamic device & subscription manager
├── mqtt_client.py # MQTT client with auto-reconnect
├── session_detector.py # 5-state session detection state machine
├── event_queue.py # Retry queue for Odoo events
├── odoo_client.py # HTTP client for Odoo REST API
├── shelly_parser.py # Shelly PM parser
├── config.py # Config schema & YAML loader
├── logger_setup.py # Structured logging (JSON)
├── requirements.txt # Python dependencies
├── Dockerfile # Multi-stage production build
├── config.yaml # Initial config (optional)
├── config.example.yaml # Config template
│
├── tests/
│ ├── test_e2e_push_architecture.py # End-to-end automated tests
│ ├── test-config-push.sh # Manual config push test
│ ├── integration/
│ │ ├── test_bridge_integration.py # Bridge integration tests
│ │ └── test_retry_logic.py # Retry mechanism tests
│ ├── unit/
│ │ ├── test_event_queue.py # Event queue unit tests
│ │ └── test_session_detector.py # Session detector unit tests
│ └── tools/
│ └── shelly_simulator.py # MQTT device simulator
│
└── logs/ # Log files (if file logging enabled)
🚀 Quick Start
Prerequisites
- Docker & Docker Compose
- Python 3.10+ (for local development)
- Mosquitto MQTT Broker (included in docker-compose)
- Odoo 18 with
open_workshop_mqttmodule installed
1. Configuration
The bridge supports two config sources (in priority order):
- Persisted Config (
/data/config-active.yaml) - Auto-created by config push - Initial Config (
config.yaml) - Optional fallback for first startup
Environment Variables:
# MQTT Broker
MQTT_BROKER=mosquitto # MQTT broker hostname
MQTT_PORT=1883 # MQTT broker port
MQTT_USERNAME= # Optional MQTT auth
MQTT_PASSWORD= # Optional MQTT auth
MQTT_CLIENT_ID=iot_bridge # MQTT client identifier
MQTT_USE_TLS=false # Enable TLS for MQTT
# Bridge HTTP Server
BRIDGE_PORT=8080 # HTTP API port
BRIDGE_API_TOKEN= # Optional Bearer token for auth
# Odoo Connection (for event pushing)
ODOO_BASE_URL=http://odoo-dev:8069 # Odoo base URL
ODOO_DATABASE=odoo # Odoo database name
ODOO_USERNAME=lotz.matthias@gmail.com # Odoo user
ODOO_API_KEY= # Odoo API key (if using API auth)
# Logging
LOG_LEVEL=INFO # DEBUG, INFO, WARNING, ERROR
LOG_FORMAT=json # json or text
LOG_FILE= # Optional log file path
2. Docker Compose Deployment
version: '3.8'
services:
iot-bridge:
build: ./iot_bridge
container_name: iot_bridge
restart: unless-stopped
ports:
- "8080:8080" # HTTP config API
volumes:
- bridge-data:/data # Config persistence
environment:
- MQTT_BROKER=mosquitto
- MQTT_PORT=1883
- BRIDGE_PORT=8080
- ODOO_BASE_URL=http://odoo-dev:8069
- ODOO_DATABASE=odoo
- ODOO_USERNAME=lotz.matthias@gmail.com
networks:
- odoo18-nw
depends_on:
- mosquitto
- odoo-dev
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
mosquitto:
image: eclipse-mosquitto:2
ports:
- "1883:1883"
volumes:
- ./mosquitto/mosquitto.conf:/mosquitto/config/mosquitto.conf
networks:
- odoo18-nw
volumes:
bridge-data: # Persists config-active.yaml
networks:
odoo18-nw:
driver: bridge
3. Start Services
docker compose up -d
4. Verify Bridge Health
curl http://localhost:8080/health
# Expected output:
# {
# "status": "ok",
# "devices": 2,
# "subscriptions": 2,
# "last_config_update": "2026-02-12T19:40:18Z"
# }
🔌 HTTP API
The bridge exposes a FastAPI-based HTTP server for configuration management.
Endpoints
GET /health
Health check endpoint.
Response:
{
"status": "ok",
"devices": 2,
"subscriptions": 2,
"last_config_update": "2026-02-12T19:40:18.123456"
}
POST /config
Receive configuration push from Odoo. Automatically called by Odoo model hooks (create/write/unlink on mqtt.device).
Authentication: Optional Bearer token via BRIDGE_API_TOKEN env var
Request Body:
{
"devices": [
{
"device_id": "shelly-pm-workshop",
"machine_name": "CNC Mill (Workshop)",
"mqtt_topic": "shellypmminig3-48f6eeb73a1c/status/pm1:0",
"parser_type": "shelly_pm",
"session_config": {
"standby_threshold_w": 5.0,
"working_threshold_w": 50.0,
"start_debounce_s": 3.0,
"stop_debounce_s": 15.0,
"message_timeout_s": 20.0,
"heartbeat_interval_s": 300.0
}
}
],
"timestamp": "2026-02-12T19:40:18Z",
"version": "1.0"
}
Response:
{
"status": "success",
"message": "Configuration applied",
"devices_configured": 1,
"timestamp": "2026-02-12T19:40:18.456789"
}
Side Effects:
- Validates config schema (Pydantic)
- Computes device diff (added/updated/removed)
- Updates MQTT subscriptions dynamically
- Persists config to
/data/config-active.yaml - Applies new thresholds to session detectors
GET /config
Retrieve current active configuration.
Response: Same structure as POST /config request
⚙️ Configuration Management
Odoo → Bridge Config Push
The bridge receives config automatically from Odoo when devices are created, updated, or deleted.
Odoo Model Hooks:
# In mqtt.device model:
@api.model_create_multi
def create(self, vals_list):
devices = super().create(vals_list)
self._push_bridge_config() # Auto-push after create
return devices
def write(self, vals):
result = super().write(vals)
if any(field in vals for field in RELEVANT_FIELDS):
self._push_bridge_config() # Auto-push after update
return result
def unlink(self):
result = super().unlink()
self._push_bridge_config() # Auto-push after delete
return result
Manual Push from Odoo UI:
Users can trigger manual config push via the "🔄 Push Config to Bridge" button in the device list view.
Config Persistence
Config is automatically persisted to /data/config-active.yaml when received via POST /config.
Benefits:
- Bridge survives restarts without Odoo connection
- Instant startup with last known config
- Audit trail of config changes (via timestamps)
Config File Location:
- Container:
/data/config-active.yaml - Host (with volume):
bridge-data:/data/config-active.yaml
🔄 Session Detection
The bridge implements a 5-state state machine for intelligent session detection:
States
IDLE → STARTING → STANDBY/WORKING → STOPPING → IDLE
- IDLE: No power draw, machine off
- STARTING: Power increasing, debounce active
- STANDBY: Power above standby threshold but below working
- WORKING: Power above working threshold, active machining
- STOPPING: Power decreasing, stop debounce active
Configuration
session_config:
standby_threshold_w: 5.0 # Min power for session start
working_threshold_w: 50.0 # Min power for working state
start_debounce_s: 3.0 # Delay before session confirmed
stop_debounce_s: 15.0 # Delay before session ends
message_timeout_s: 20.0 # Max time without MQTT messages
heartbeat_interval_s: 300.0 # Interval for aggregated events
Event Types
session_started: Session confirmed after start debouncesession_stopped: Session ended after stop debouncesession_heartbeat: Periodic aggregated update (every 300s)session_timeout: No messages received formessage_timeout_s
🧪 Testing
Automated End-to-End Tests
cd iot_bridge
python3 tests/test_e2e_push_architecture.py
Tests:
- Device Create → Config Push
- Device Update → Config Push
- Manual Push Button
- Device Delete → Config Remove
- Bridge Restart → Config Persistence
Manual Config Push Test
cd iot_bridge/tests
./test-config-push.sh
Shelly Device Simulator
cd iot_bridge/tests/tools
python3 shelly_simulator.py
Simulates Shelly PM device sending power measurements via MQTT.
Recommended explicit invocations:
# Full session (default scenario)
python3 tests/tools/shelly_simulator.py --broker localhost --port 1883 --no-tls --username "" --password "" --scenario full_session
# Online Status Test - Continuous messaging (verify device stays online)
python3 tests/tools/shelly_simulator.py --broker localhost --port 1883 --no-tls --scenario online_test --duration 120
# Offline Detection Test - Message gap triggers timeout
python3 tests/tools/shelly_simulator.py --broker localhost --port 1883 --no-tls --scenario offline_test --offline-duration 60
# Disable TLS (only if your broker uses plain TCP)
python3 shelly_simulator.py --no-tls
Available Scenarios:
full_session: Complete state machine transition (IDLE → STANDBY → WORKING → STOPPING)standby: Continuous low power (20-100W)working: Continuous high power (>100W)online_test: Verify device online status detectionoffline_test: Test offline timeout detection (message gap)
📊 Monitoring & Observability
Structured Logging
The bridge uses structured JSON logging for easy parsing:
{
"timestamp": "2026-02-12T19:40:18.123456Z",
"level": "info",
"event": "mqtt_message_received",
"device_id": "shelly-pm-workshop",
"power_w": 125.5,
"state": "working"
}
Health Checks
Docker health check via HTTP endpoint:
docker inspect --format='{{json .State.Health}}' iot_bridge | jq
Log Files
# View live logs
docker logs -f iot_bridge
# Search for errors
docker logs iot_bridge | grep '"level":"error"'
# View config changes
docker logs iot_bridge | grep 'config_received'
🐛 Troubleshooting
Bridge Not Receiving Config
Symptoms: Health endpoint shows "devices": 0
Solutions:
-
Check Odoo → Bridge connectivity:
docker exec odoo-dev curl -v http://iot-bridge:8080/health -
Verify system parameter in Odoo:
SELECT key, value FROM ir_config_parameter WHERE key = 'open_workshop_mqtt.bridge_url'; -- Should return: http://iot-bridge:8080 -
Check Odoo logs for push errors:
docker logs odoo-dev | grep "push_bridge_config" -
Manually trigger config push from Odoo UI (🔄 button)
MQTT Connection Issues
Symptoms: Logs show mqtt_connection_failed
Solutions:
-
Verify Mosquitto is running:
docker ps | grep mosquitto -
Check MQTT broker logs:
docker logs mosquitto -
Test MQTT connectivity:
docker exec iot_bridge mosquitto_sub -h mosquitto -t '#' -v -
Verify MQTT env vars:
docker exec iot_bridge env | grep MQTT
Events Not Reaching Odoo
Symptoms: Sessions not created in Odoo
Solutions:
-
Check event queue status in logs:
docker logs iot_bridge | grep '"event":"event_queue"' -
Verify Odoo REST API is accessible:
docker exec iot_bridge curl -v http://odoo-dev:8069/ows/iot/event -
Check Odoo API logs:
docker logs odoo-dev | grep "/ows/iot/event" -
Inspect event queue retries:
docker logs iot_bridge | grep '"event":"odoo_send_failed"'
Config Not Persisting After Restart
Symptoms: Bridge starts with 0 devices after restart
Solutions:
-
Check volume mount:
docker inspect iot_bridge | jq '.[0].Mounts' -
Verify
/datadirectory permissions:docker exec iot_bridge ls -la /data/ -
Check for config file:
docker exec iot_bridge cat /data/config-active.yaml -
Review logs for persistence errors:
docker logs iot_bridge | grep '"event":"config_persist"'
Session Detection Not Working
Symptoms: Sessions not starting/stopping correctly
Solutions:
-
Check threshold configuration:
curl http://localhost:8080/config | jq '.devices[].session_config' -
Review session detector logs:
docker logs iot_bridge | grep '"event":"state_transition"' -
Verify power readings:
docker logs iot_bridge | grep '"device_id":"your-device"' | tail -20 -
Adjust debounce times if needed (via Odoo UI)
🔧 Development
Local Setup
# Create virtual environment
python3 -m venv venv
source venv/bin/activate
# Install dependencies
pip install -r requirements.txt
# Run tests
pytest tests/
# Run bridge locally
python3 main.py --config config.yaml
Environment Variables for Development
export MQTT_BROKER=localhost
export MQTT_PORT=1883
export BRIDGE_PORT=8080
export ODOO_BASE_URL=http://localhost:9018
export ODOO_DATABASE=odoo
export LOG_LEVEL=DEBUG
📜 License
See ../LICENSE
📚 Related Documentation
- IMPLEMENTATION_PLAN.md - Project roadmap and architecture decisions
- DEPLOYMENT.md - Production deployment guide
- API.md - HTTP API reference
- Odoo Module Documentation - Odoo side of the integration
- API Schema - Pydantic models for config validation
⭐ Key Features
- ✅ Autonomous Operation: Runs without Odoo connection after initial config
- ✅ PUSH Configuration: Instant updates from Odoo via model hooks
- ✅ Config Persistence: Survives restarts with
/data/config-active.yaml - ✅ Dynamic Subscriptions: Auto-subscribe/unsubscribe based on devices
- ✅ Retry Queue: Exponential backoff for event delivery (max 10 retries)
- ✅ Session Detection: Intelligent 5-state state machine
- ✅ Multi-Device Support: Handles multiple devices simultaneously
- ✅ Structured Logging: JSON logs for easy parsing & monitoring
- ✅ Health Checks: Docker-compatible health endpoint
- ✅ Automated Tests: 5 E2E tests validating entire PUSH architecture