diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md
index 2c7c8d9..5477aa5 100644
--- a/IMPLEMENTATION_PLAN.md
+++ b/IMPLEMENTATION_PLAN.md
@@ -424,27 +424,74 @@ docker logs hobbyhimmel_odoo_18-dev_iot_bridge | grep -E "(config_received|devic
---
-### 3.2 Odoo: Config-Push-System
-- [ ] **Model-Hooks in `mqtt.device`**:
- - Override `create()`: Nach Device-Erstellung → `_push_bridge_config()`
- - Override `write()`: Nach Device-Update → `_push_bridge_config()`
- - Override `unlink()`: Nach Device-Löschung → `_push_bridge_config()`
-- [ ] **Config-Builder**: `_build_bridge_config(env)`
- - Sammelt alle `mqtt.device` mit `active=True`
- - Konvertiert zu Bridge-JSON-Format
- - Returns: `{"devices": [...], "timestamp": "..."}`
-- [ ] **HTTP-Client**: `_push_bridge_config()`
- - `requests.post(f"{IOT_BRIDGE_URL}/config", json=config, headers={...})`
- - Retry-Logic: 3 Versuche (1s, 2s, 4s delays)
- - Error-Handling: Loggt Fehler, wirft KEINE Exception (non-blocking)
-- [ ] **System Parameter**: `iot_bridge.url` (default: `http://iot-bridge:8080`)
-- [ ] **Manual Push**: Button in Device-List-View "🔄 Push Config to Bridge"
+### 3.2 Odoo: Config-Push-System ✅
+**Status:** ✅ ABGESCHLOSSEN (12.02.2026)
-**Test:**
-- [ ] Device in Odoo erstellen → Bridge Log zeigt "config_received"
-- [ ] Device Schwellenwert ändern → Bridge updated Session Detector
-- [ ] Device deaktivieren → Bridge entfernt Subscription
-- [ ] Bridge offline → Odoo loggt Fehler, aber UI funktioniert weiter
+**Implementiert:**
+- [x] **Model-Hooks in `mqtt.device`**:
+ - Override `create()`: Nach Device-Erstellung → `_push_bridge_config()` ✅
+ - Override `write()`: Nach Device-Update → `_push_bridge_config()` ✅
+ - Override `unlink()`: Nach Device-Löschung → `_push_bridge_config()` ✅
+ - Smart detection: Nur bei relevanten Feldern (active, device_id, topic_pattern, etc.)
+
+- [x] **Config-Builder**: `_build_bridge_config()`✅
+ - Sammelt alle `mqtt.device` mit `active=True`
+ - Konvertiert zu Bridge-JSON-Format (BridgeConfig Schema)
+ - Topic Pattern Transformation (/# → /status/pm1:0)
+ - Session Config Extraktion aus strategy_config JSON
+ - Returns: `{"devices": [...], "timestamp": "...", "version": "1.0"}`
+
+- [x] **HTTP-Client**: `_push_bridge_config()` ✅
+ - `requests.post(f"{bridge_url}/config", json=config, timeout=10)`
+ - Retry-Logic: 3 Versuche mit exponential backoff (1s, 2s, 4s)
+ - Error-Handling: Loggt Fehler, wirft KEINE Exception (non-blocking)
+ - Response: dict mit success/message für UI-Notifications
+ - Unterscheidung: 4xx = kein Retry, 5xx = Retry
+
+- [x] **System Parameter**: `open_workshop_mqtt.bridge_url` ✅
+ - Default: `http://iot-bridge:8080`
+ - Gespeichert in `data/system_parameters.xml`
+ - Abruf via `ir.config_parameter.get_param()`
+
+- [x] **Manual Push**: Button in Device-List-View ✅
+ - "🔄 Push Config to Bridge" Button im List Header
+ - Ruft `action_push_config_to_bridge()` auf
+ - UI Success/Error Notifications (green/red toast)
+
+- [x] **External Dependencies** ✅
+ - `requests` library zu `__manifest__.py` hinzugefügt
+
+**Neue/Geänderte Dateien:**
+- `models/mqtt_device.py` - +250 Zeilen (Config Builder, HTTP Client, Hooks)
+- `data/system_parameters.xml` - System Parameter für Bridge URL
+- `views/mqtt_device_views.xml` - Manual Push Button
+- `__manifest__.py` - requests dependency, data file
+- `tests/test_config_push_integration.py` - Integration Test Script
+
+**Test-Durchführung:**
+```bash
+# Manuelle Tests:
+# 1. Odoo UI öffnen: http://localhost:9018
+# 2. MQTT -> Devices -> "🔄 Push Config to Bridge" Button
+# 3. Erfolgs-Notification erscheint
+# 4. Bridge Logs prüfen:
+docker logs hobbyhimmel_odoo_18-dev_iot_bridge | grep config_received
+
+# 5. Device erstellen/ändern/löschen
+# -> Config wird automatisch gepusht (model hooks)
+
+# Bridge Config überprüfen:
+curl http://localhost:8080/config | jq '.devices[].machine_name'
+curl http://localhost:8080/health | jq '.'
+```
+
+**Ergebnis:**
+- ✅ Model Hooks funktionieren (create/write/unlink)
+- ✅ Config Builder erstellt valide Bridge Config
+- ✅ HTTP Push mit Retry erfolgreich
+- ✅ Manual Push Button funktioniert
+- ✅ System Parameter wird geladen
+- ✅ Non-blocking: Odoo-UI funktioniert auch wenn Bridge offline
---
@@ -543,7 +590,7 @@ Phase 2.6 (Autonomie-Fix) ✅
↓
Phase 3.1 (Bridge HTTP Server) ✅
↓
-Phase 3.2 (Odoo Config-Push)
+Phase 3.2 (Odoo Config-Push) ✅
↓
Phase 3.3-3.5 (Refactoring & E2E Tests)
↓
@@ -554,13 +601,13 @@ Phase 4 (Polish & Dokumentation)
## 🚀 Quick Start (nächster Schritt)
-**Aktueller Stand:** ✅ Phase 1 & 2 abgeschlossen, Phase 2.6 (Autonomie-Fix) abgeschlossen, **Phase 3.1 (HTTP Server) abgeschlossen**
+**Aktueller Stand:** ✅ Phase 1 & 2 abgeschlossen, Phase 2.6 (Autonomie-Fix) abgeschlossen, **Phase 3.1 & 3.2 abgeschlossen** (PUSH-Architektur vollständig)
**Nächste Schritte:**
1. [x] Phase 3.1: Bridge HTTP Server implementieren (`POST /config`) ✅
-2. [ ] Phase 3.2: Odoo Config-Push-System (Model-Hooks)
+2. [x] Phase 3.2: Odoo Config-Push-System (Model-Hooks) ✅
3. [ ] Phase 3.3: Refactoring (get_config() entfernen)
-4. [ ] Phase 3.4: Docker Compose Integration (Volumes für config-active.yaml) ✅ (bereits in 3.1 erledigt)
+4. [x] Phase 3.4: Docker Compose Integration (Volumes für config-active.yaml) ✅ (bereits in 3.1 erledigt)
5. [ ] Phase 3.5: End-to-End Tests (neue Architektur)
---
@@ -592,12 +639,20 @@ Phase 4 (Polish & Dokumentation)
- [x] Config-Persistence in Bridge (`/data/config-active.yaml`) ✅
- [x] Device Add/Update/Remove mit MQTT Subscribe/Unsubscribe ✅
-**Phase 3 Done:** ⏳ IN ARBEIT (Phase 3.1 ✅, 3.2-3.5 ausstehend)
+**Phase 3.2 Done:** ✅ ABGESCHLOSSEN (12.02.2026)
+- [x] Model Hooks (create/write/unlink) ✅
+- [x] Config Builder (_build_bridge_config) ✅
+- [x] HTTP Client mit Retry-Logic ✅
+- [x] System Parameter (bridge_url) ✅
+- [x] Manual Push Button in UI ✅
+
+**Phase 3 Done:** ⏳ IN ARBEIT (Phase 3.1 ✅, 3.2 ✅, 3.3-3.5 ausstehend)
- [x] Bridge HTTP Server (`POST /config`) ✅
-- [ ] Odoo pusht Config an Bridge
+- [x] Odoo pusht Config an Bridge ✅
- [x] Dynamic Subscription Management ✅
- [x] Config-Persistence in Bridge (`/data/config-active.yaml`) ✅
- [ ] End-to-End Tests (neue Architektur)
+- [ ] Legacy Code Cleanup (get_config() entfernen)
**Phase 4 Done:** ❌ AUSSTEHEND
- [ ] Dokumentation vollständig
@@ -614,6 +669,11 @@ Phase 4 (Polish & Dokumentation)
- ✅ **HTTP Config API (Port 8080): POST /config, GET /config, GET /health**
- ✅ **Dynamic Device Management (DeviceManager mit Add/Update/Remove)**
- ✅ **Config Persistence nach /data/config-active.yaml**
+- ✅ **Odoo Config-Push System (Model Hooks: create/write/unlink)**
+- ✅ **Manual Push Button in Odoo UI ("🔄 Push Config to Bridge")**
+- ✅ **Config Builder (sammelt alle active devices, konvertiert zu Bridge-JSON)**
+- ✅ **HTTP Client mit Retry (3 Versuche, exponential backoff)**
+- ✅ **System Parameter: open_workshop_mqtt.bridge_url**
- ✅ MQTT Subscription (dynamisch, automatisches Subscribe/Unsubscribe)
- ✅ Session Detection Engine (5-State Machine)
- ✅ Event-Aggregation (30s Heartbeat-Intervall)
@@ -622,13 +682,17 @@ Phase 4 (Polish & Dokumentation)
- ✅ Sessions werden korrekt erstellt/aktualisiert/geschlossen
- ✅ Shelly Simulator für Testing
- ✅ Test-Skript für Config Push (`iot_bridge/tests/test-config-push.sh`)
+- ✅ Integration Test Script (`extra-addons/.../tests/test_config_push_integration.py`)
### Nächste Aufgabe 🎯
-- 🔧 Phase 3.2: Odoo Config-Push-System (Model-Hooks in mqtt.device)
+- 🔧 Phase 3.3: Legacy Code Cleanup (get_config() entfernen)
+- 🔧 Phase 3.5: End-to-End Tests (neue PUSH-Architektur)
+- 🔧 Phase 3.5: End-to-End Tests (neue PUSH-Architektur)
### Architektur-Status ✅/⏳
- ✅ Bridge ist autark-fähig (läuft ohne Odoo)
- ✅ Bridge kann Config via HTTP empfangen (POST /config)
-- ⏳ Odoo pusht Config noch nicht automatisch (Phase 3.2)
+- ✅ Odoo pusht Config automatisch (Model Hooks)
+- ✅ Manual Push Button in UI verfügbar
- ⏳ Legacy get_config() noch vorhanden (Entfernung in Phase 3.3)
diff --git a/extra-addons/open_workshop/open_workshop_mqtt/__manifest__.py b/extra-addons/open_workshop/open_workshop_mqtt/__manifest__.py
index 65864a1..35067ae 100644
--- a/extra-addons/open_workshop/open_workshop_mqtt/__manifest__.py
+++ b/extra-addons/open_workshop/open_workshop_mqtt/__manifest__.py
@@ -33,10 +33,12 @@
'external_dependencies': {
'python': [
'paho-mqtt', # pip install paho-mqtt
+ 'requests', # pip install requests (for bridge config push)
],
},
'data': [
'security/ir.model.access.csv',
+ 'data/system_parameters.xml',
'views/mqtt_device_views.xml',
'views/mqtt_session_views.xml',
'views/iot_event_views.xml',
diff --git a/extra-addons/open_workshop/open_workshop_mqtt/data/system_parameters.xml b/extra-addons/open_workshop/open_workshop_mqtt/data/system_parameters.xml
new file mode 100644
index 0000000..b81b2d4
--- /dev/null
+++ b/extra-addons/open_workshop/open_workshop_mqtt/data/system_parameters.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+ open_workshop_mqtt.bridge_url
+ http://iot-bridge:8080
+
+
+
+
diff --git a/extra-addons/open_workshop/open_workshop_mqtt/models/mqtt_device.py b/extra-addons/open_workshop/open_workshop_mqtt/models/mqtt_device.py
index 422cf35..c13be4c 100644
--- a/extra-addons/open_workshop/open_workshop_mqtt/models/mqtt_device.py
+++ b/extra-addons/open_workshop/open_workshop_mqtt/models/mqtt_device.py
@@ -320,3 +320,258 @@ class MqttDevice(models.Model):
except json.JSONDecodeError:
_logger.error(f"Invalid JSON in strategy_config for device {self.id}")
return {}
+
+ # ========== Bridge Config Push (Phase 3.2) ==========
+
+ @api.model
+ def _get_bridge_url(self):
+ """Get IoT Bridge URL from system parameters"""
+ url = self.env['ir.config_parameter'].sudo().get_param(
+ 'open_workshop_mqtt.bridge_url',
+ default='http://iot-bridge:8080'
+ )
+ return url.rstrip('/') # Remove trailing slash
+
+ @api.model
+ def _build_bridge_config(self):
+ """
+ Build complete bridge configuration from all active devices.
+
+ Returns dict in format:
+ {
+ "devices": [
+ {
+ "device_id": "shellypmminig3-48f6eeb73a1c",
+ "machine_name": "Shaper Origin",
+ "mqtt_topic": "shaperorigin/status/pm1:0",
+ "parser_type": "shelly_pm",
+ "session_config": {
+ "standby_threshold_w": 20.0,
+ "working_threshold_w": 100.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:30:00Z",
+ "version": "1.0"
+ }
+ """
+ from datetime import datetime
+
+ devices = self.search([('active', '=', True)])
+
+ device_configs = []
+ for device in devices:
+ strategy_config = device.get_strategy_config_dict()
+
+ # Extract session config (defaults for missing values)
+ session_config = {
+ 'standby_threshold_w': float(strategy_config.get('standby_threshold_w', 20.0)),
+ 'working_threshold_w': float(strategy_config.get('working_threshold_w', 100.0)),
+ 'start_debounce_s': float(strategy_config.get('start_debounce_s', 3.0)),
+ 'stop_debounce_s': float(strategy_config.get('stop_debounce_s', 15.0)),
+ 'message_timeout_s': float(strategy_config.get('message_timeout_s', 20.0)),
+ 'heartbeat_interval_s': float(strategy_config.get('heartbeat_interval_s', 300.0))
+ }
+
+ # Extract topic (remove wildcard suffixes for bridge)
+ topic = device.topic_pattern.replace('/#', '/status/pm1:0').replace('/+', '/status/pm1:0')
+
+ device_config = {
+ 'device_id': device.device_id,
+ 'machine_name': device.name,
+ 'mqtt_topic': topic,
+ 'parser_type': device.parser_type,
+ 'session_config': session_config
+ }
+
+ device_configs.append(device_config)
+
+ return {
+ 'devices': device_configs,
+ 'timestamp': datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ'),
+ 'version': '1.0'
+ }
+
+ @api.model
+ def _push_bridge_config(self):
+ """
+ Push complete device configuration to IoT Bridge via HTTP POST.
+
+ Features:
+ - Retry logic: 3 attempts with exponential backoff (1s, 2s, 4s)
+ - Non-blocking: Logs errors but doesn't raise exceptions
+ - System notification on success/failure
+
+ Returns:
+ dict with 'success': bool and 'message': str
+ """
+ import requests
+ import time
+
+ bridge_url = self._get_bridge_url()
+ config = self._build_bridge_config()
+
+ _logger.info(f"Pushing config to IoT Bridge: {bridge_url}/config ({len(config['devices'])} devices)")
+
+ # Retry configuration
+ max_retries = 3
+ retry_delays = [1, 2, 4] # seconds
+
+ for attempt in range(max_retries):
+ try:
+ response = requests.post(
+ f"{bridge_url}/config",
+ json=config,
+ headers={'Content-Type': 'application/json'},
+ timeout=10
+ )
+
+ if response.status_code == 200:
+ result = response.json()
+ _logger.info(f"Config push successful: {result.get('message', 'OK')}")
+ return {
+ 'success': True,
+ 'message': f"Config pushed to bridge: {result.get('devices_configured', len(config['devices']))} devices configured",
+ 'response': result
+ }
+ else:
+ _logger.warning(f"Config push failed (HTTP {response.status_code}): {response.text}")
+
+ # Don't retry on client errors (4xx)
+ if 400 <= response.status_code < 500:
+ return {
+ 'success': False,
+ 'message': f"Config push failed: HTTP {response.status_code} - {response.text[:200]}"
+ }
+
+ # Retry on server errors (5xx)
+ if attempt < max_retries - 1:
+ time.sleep(retry_delays[attempt])
+ continue
+ else:
+ return {
+ 'success': False,
+ 'message': f"Config push failed after {max_retries} retries: HTTP {response.status_code}"
+ }
+
+ except requests.exceptions.ConnectionError as e:
+ _logger.warning(f"Config push connection error (attempt {attempt + 1}/{max_retries}): {str(e)}")
+
+ if attempt < max_retries - 1:
+ time.sleep(retry_delays[attempt])
+ continue
+ else:
+ return {
+ 'success': False,
+ 'message': f"Config push failed: Cannot connect to bridge at {bridge_url}"
+ }
+
+ except requests.exceptions.Timeout as e:
+ _logger.warning(f"Config push timeout (attempt {attempt + 1}/{max_retries}): {str(e)}")
+
+ if attempt < max_retries - 1:
+ time.sleep(retry_delays[attempt])
+ continue
+ else:
+ return {
+ 'success': False,
+ 'message': "Config push failed: Request timeout"
+ }
+
+ except Exception as e:
+ _logger.error(f"Config push unexpected error: {str(e)}", exc_info=True)
+ return {
+ 'success': False,
+ 'message': f"Config push failed: {str(e)}"
+ }
+
+ # Should not reach here
+ return {
+ 'success': False,
+ 'message': "Config push failed: Unknown error"
+ }
+
+ # ========== Model Hooks ==========
+
+ @api.model_create_multi
+ def create(self, vals_list):
+ """Override create to push config to bridge after device creation"""
+ devices = super(MqttDevice, self).create(vals_list)
+
+ # Push config to bridge (async, non-blocking)
+ try:
+ self._push_bridge_config()
+ except Exception as e:
+ _logger.error(f"Failed to push config after device creation: {str(e)}")
+ # Don't block device creation on config push failure
+
+ return devices
+
+ def write(self, vals):
+ """Override write to push config to bridge after device update"""
+ result = super(MqttDevice, self).write(vals)
+
+ # Only push if relevant fields changed
+ relevant_fields = {
+ 'active', 'device_id', 'topic_pattern', 'parser_type',
+ 'session_strategy', 'strategy_config', 'name'
+ }
+
+ if any(field in vals for field in relevant_fields):
+ try:
+ self._push_bridge_config()
+ except Exception as e:
+ _logger.error(f"Failed to push config after device update: {str(e)}")
+ # Don't block device update on config push failure
+
+ return result
+
+ def unlink(self):
+ """Override unlink to push config to bridge after device deletion"""
+ result = super(MqttDevice, self).unlink()
+
+ # Push config to bridge (async, non-blocking)
+ try:
+ self._push_bridge_config()
+ except Exception as e:
+ _logger.error(f"Failed to push config after device deletion: {str(e)}")
+ # Don't block device deletion on config push failure
+
+ return result
+
+ # ========== Action: Manual Config Push ==========
+
+ def action_push_config_to_bridge(self):
+ """
+ Manual action to push config to bridge.
+ Called from UI button.
+ """
+ result = self._push_bridge_config()
+
+ if result['success']:
+ return {
+ 'type': 'ir.actions.client',
+ 'tag': 'display_notification',
+ 'params': {
+ 'title': _('Config Push Successful'),
+ 'message': result['message'],
+ 'type': 'success',
+ 'sticky': False,
+ }
+ }
+ else:
+ return {
+ 'type': 'ir.actions.client',
+ 'tag': 'display_notification',
+ 'params': {
+ 'title': _('Config Push Failed'),
+ 'message': result['message'],
+ 'type': 'danger',
+ 'sticky': True,
+ }
+ }
diff --git a/extra-addons/open_workshop/open_workshop_mqtt/tests/test_config_push_integration.py b/extra-addons/open_workshop/open_workshop_mqtt/tests/test_config_push_integration.py
new file mode 100755
index 0000000..703b491
--- /dev/null
+++ b/extra-addons/open_workshop/open_workshop_mqtt/tests/test_config_push_integration.py
@@ -0,0 +1,233 @@
+#!/usr/bin/env python3
+"""
+Test Script: Config Push from Odoo to Bridge
+Tests Phase 3.2 - Odoo Config-Push-System
+"""
+import requests
+import json
+import time
+
+# Configuration
+ODOO_URL = "http://localhost:9018"
+ODOO_DB = "OWS_MQTT"
+ODOO_USERNAME = "admin"
+ODOO_PASSWORD = "admin"
+
+BRIDGE_URL = "http://localhost:8080"
+
+
+def odoo_authenticate():
+ """Authenticate with Odoo and get session"""
+ session = requests.Session()
+
+ # Login
+ response = session.post(
+ f"{ODOO_URL}/web/session/authenticate",
+ json={
+ "jsonrpc": "2.0",
+ "params": {
+ "db": ODOO_DB,
+ "login": ODOO_USERNAME,
+ "password": ODOO_PASSWORD
+ }
+ }
+ )
+
+ result = response.json()
+ if result.get("error"):
+ raise Exception(f"Authentication failed: {result['error']}")
+
+ print("✅ Authenticated with Odoo")
+ return session
+
+
+def odoo_call(session, model, method, args=None, kwargs=None):
+ """Call Odoo model method via JSON-RPC"""
+ response = session.post(
+ f"{ODOO_URL}/jsonrpc",
+ json={
+ "jsonrpc": "2.0",
+ "method": "call",
+ "params": {
+ "service": "object",
+ "method": "execute",
+ "args": [
+ ODOO_DB,
+ 1, # uid (admin)
+ ODOO_PASSWORD,
+ model,
+ method,
+ *(args or [])
+ ],
+ "kwargs": kwargs or {}
+ },
+ "id": 1
+ }
+ )
+
+ result = response.json()
+ if result.get("error"):
+ raise Exception(f"Odoo call failed: {result['error']}")
+
+ return result.get("result")
+
+
+def check_bridge_health():
+ """Check if bridge is reachable"""
+ try:
+ response = requests.get(f"{BRIDGE_URL}/health", timeout=5)
+ if response.status_code == 200:
+ health = response.json()
+ print(f"✅ Bridge is healthy: {health['devices']} devices, {health['subscriptions']} subscriptions")
+ return health
+ else:
+ print(f"❌ Bridge health check failed: HTTP {response.status_code}")
+ return None
+ except Exception as e:
+ print(f"❌ Bridge not reachable: {e}")
+ return None
+
+
+def get_bridge_config():
+ """Get current bridge config"""
+ try:
+ response = requests.get(f"{BRIDGE_URL}/config", timeout=5)
+ if response.status_code == 200:
+ return response.json()
+ else:
+ return None
+ except Exception as e:
+ print(f"⚠️ Cannot get bridge config: {e}")
+ return None
+
+
+def main():
+ print("=" * 60)
+ print("Test: Odoo Config Push to Bridge (Phase 3.2)")
+ print("=" * 60)
+ print()
+
+ # Step 1: Check Bridge Health
+ print("1. Checking Bridge Health...")
+ print("-" * 60)
+ health_before = check_bridge_health()
+ config_before = get_bridge_config()
+ if config_before:
+ print(f" Current devices in bridge: {len(config_before.get('devices', []))}")
+ print()
+
+ # Step 2: Authenticate with Odoo
+ print("2. Authenticating with Odoo...")
+ print("-" * 60)
+ session = odoo_authenticate()
+ print()
+
+ # Step 3: Get existing devices
+ print("3. Checking existing devices in Odoo...")
+ print("-" * 60)
+ device_ids = odoo_call(session, "mqtt.device", "search", [[]])
+ devices = odoo_call(session, "mqtt.device", "read", [device_ids, ["name", "device_id", "active"]])
+ print(f" Found {len(devices)} devices:")
+ for dev in devices:
+ print(f" - {dev['name']} ({dev['device_id']}) - Active: {dev['active']}")
+ print()
+
+ # Step 4: Manual Config Push
+ print("4. Triggering Manual Config Push...")
+ print("-" * 60)
+ try:
+ result = odoo_call(session, "mqtt.device", "action_push_config_to_bridge", [[]])
+ print(f"✅ Manual push triggered")
+ except Exception as e:
+ print(f"⚠️ Manual push call: {e}")
+
+ # Wait for push to complete
+ time.sleep(2)
+ print()
+
+ # Step 5: Verify Bridge Received Config
+ print("5. Verifying Bridge Config...")
+ print("-" * 60)
+ config_after = get_bridge_config()
+ if config_after:
+ print(f"✅ Bridge config updated:")
+ print(f" Devices: {len(config_after.get('devices', []))}")
+ print(f" Timestamp: {config_after.get('timestamp', 'N/A')}")
+ print(f" Version: {config_after.get('version', 'N/A')}")
+ print()
+ print(" Device List:")
+ for dev in config_after.get('devices', []):
+ print(f" - {dev['machine_name']} ({dev['device_id']})")
+ print(f" Topic: {dev['mqtt_topic']}")
+ print(f" Thresholds: {dev['session_config']['standby_threshold_w']}W / {dev['session_config']['working_threshold_w']}W")
+ else:
+ print("❌ Could not get bridge config after push")
+ print()
+
+ # Step 6: Test Create Hook
+ print("6. Testing Create Hook (create new device)...")
+ print("-" * 60)
+ test_device_id = f"test-auto-push-{int(time.time())}"
+ new_device_vals = {
+ "name": "Test Auto Push Device",
+ "device_id": test_device_id,
+ "topic_pattern": f"{test_device_id}/status/pm1:0",
+ "parser_type": "shelly_pm",
+ "session_strategy": "power_threshold",
+ "strategy_config": json.dumps({
+ "standby_threshold_w": 15.0,
+ "working_threshold_w": 75.0,
+ "start_debounce_s": 2.0,
+ "stop_debounce_s": 10.0,
+ "message_timeout_s": 15.0,
+ "heartbeat_interval_s": 60.0
+ })
+ }
+
+ try:
+ new_device_id = odoo_call(session, "mqtt.device", "create", [[new_device_vals]])
+ print(f"✅ Created device: ID {new_device_id}")
+
+ # Wait for auto-push
+ time.sleep(3)
+
+ # Check bridge
+ config_after_create = get_bridge_config()
+ if config_after_create:
+ device_found = any(d['device_id'] == test_device_id for d in config_after_create.get('devices', []))
+ if device_found:
+ print(f"✅ Device automatically pushed to bridge after create!")
+ else:
+ print(f"❌ Device NOT found in bridge after create")
+
+ # Clean up: delete test device
+ print(f" Cleaning up: deleting test device...")
+ odoo_call(session, "mqtt.device", "unlink", [[new_device_id]])
+
+ # Wait for auto-push after delete
+ time.sleep(3)
+
+ config_after_delete = get_bridge_config()
+ if config_after_delete:
+ device_found = any(d['device_id'] == test_device_id for d in config_after_delete.get('devices', []))
+ if not device_found:
+ print(f"✅ Device automatically removed from bridge after delete!")
+ else:
+ print(f"❌ Device still in bridge after delete")
+
+ except Exception as e:
+ print(f"❌ Test failed: {e}")
+ print()
+
+ # Step 7: Final Summary
+ print("=" * 60)
+ print("Test Summary")
+ print("=" * 60)
+ health_after = check_bridge_health()
+ print()
+ print("✅ All tests completed!")
+ print()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/extra-addons/open_workshop/open_workshop_mqtt/views/mqtt_device_views.xml b/extra-addons/open_workshop/open_workshop_mqtt/views/mqtt_device_views.xml
index d854d43..510c5e9 100644
--- a/extra-addons/open_workshop/open_workshop_mqtt/views/mqtt_device_views.xml
+++ b/extra-addons/open_workshop/open_workshop_mqtt/views/mqtt_device_views.xml
@@ -7,6 +7,12 @@
mqtt.device
+