feat: Add multi-device support (M4)
- Support multiple machines in parallel - Each device uses unique topic_prefix - Add 3 integration tests for multi-device scenarios - Update config.yaml.example with 2-machine setup - All 8 integration tests passing - Test scenarios: parallel sessions, independent timeouts, different thresholds
This commit is contained in:
parent
aeb8e5660b
commit
bb3e492d30
|
|
@ -122,6 +122,6 @@ python tests/tools/shelly_simulator.py --scenario timeout
|
|||
- [x] M0-M3: MQTT + Parser + Session Detection
|
||||
- [x] Unit + Integration Tests
|
||||
- [x] Timeout Detection mit check_timeouts()
|
||||
- [ ] M4: Multi-Device Support
|
||||
- [x] M4: Multi-Device Support (2+ Maschinen parallel)
|
||||
- [ ] M5: Reconnect + Error Handling
|
||||
- [ ] M6-M10: Odoo Integration
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ mqtt:
|
|||
# Add more topics as needed
|
||||
|
||||
# Device Configuration
|
||||
# Multi-Device Support: Jedes Shelly PM Mini G3 Gerät = 1 Maschine
|
||||
# Unterscheide Geräte durch verschiedene topic_prefix in MQTT Topics
|
||||
devices:
|
||||
- shelly_id: "shellypmminig3-48f6eeb73a1c"
|
||||
machine_name: "Shaper Origin"
|
||||
|
|
@ -37,15 +39,18 @@ devices:
|
|||
|
||||
enabled: true
|
||||
|
||||
# Example for additional device
|
||||
# - shelly_id: "shellypmminig3-xxxxxxxxxxxx"
|
||||
# machine_name: "CNC Mill"
|
||||
# machine_id: "cnc-mill-01"
|
||||
# device_type: "shelly_pm_mini_g3"
|
||||
# power_threshold: 100
|
||||
# start_debounce_s: 5
|
||||
# stop_debounce_s: 20
|
||||
# enabled: true
|
||||
# Zweite Maschine - anderes Shelly-Gerät, andere Thresholds
|
||||
# Wichtig: Jedes Shelly muss eigenen topic_prefix haben!
|
||||
- shelly_id: "shellypmminig3-48f6eeb73a2d"
|
||||
machine_name: "CNC Fräse"
|
||||
machine_id: "cnc-mill-01"
|
||||
device_type: "shelly_pm_mini_g3"
|
||||
standby_threshold_w: 30 # Höherer Standby-Wert für CNC
|
||||
working_threshold_w: 150 # Spindel braucht mehr Power
|
||||
start_debounce_s: 3
|
||||
stop_debounce_s: 15
|
||||
message_timeout_s: 20
|
||||
enabled: true
|
||||
|
||||
# Logging Configuration
|
||||
logging:
|
||||
|
|
|
|||
|
|
@ -73,10 +73,10 @@ mqtt:
|
|||
client_id: "ows_iot_bridge_pytest"
|
||||
keepalive: 60
|
||||
topics:
|
||||
- "{mqtt_config['topic_prefix']}/#"
|
||||
|
||||
- "{mqtt_config['topic_prefix']}-m1/#"
|
||||
- "{mqtt_config['topic_prefix']}-m2/#"
|
||||
devices:
|
||||
- topic_prefix: "{mqtt_config['topic_prefix']}"
|
||||
- topic_prefix: "{mqtt_config['topic_prefix']}-m1"
|
||||
machine_name: "PyTest Machine"
|
||||
machine_id: "pytest-machine-01"
|
||||
device_type: "shelly_pm_mini_g3"
|
||||
|
|
@ -86,6 +86,16 @@ devices:
|
|||
stop_debounce_s: 15
|
||||
message_timeout_s: 20
|
||||
enabled: true
|
||||
- topic_prefix: "{mqtt_config['topic_prefix']}-m2"
|
||||
machine_name: "PyTest CNC"
|
||||
machine_id: "pytest-machine-02"
|
||||
device_type: "shelly_pm_mini_g3"
|
||||
standby_threshold_w: 30
|
||||
working_threshold_w: 150
|
||||
start_debounce_s: 3
|
||||
stop_debounce_s: 15
|
||||
message_timeout_s: 20
|
||||
enabled: true
|
||||
|
||||
logging:
|
||||
level: "INFO"
|
||||
|
|
@ -264,7 +274,7 @@ class TestMQTTIntegration:
|
|||
|
||||
def test_session_start_standby(self, bridge_process, mqtt_sender, mqtt_config, test_storage_files):
|
||||
"""Session Start → STANDBY"""
|
||||
topic = f"{mqtt_config['topic_prefix']}/status/pm1:0"
|
||||
topic = f"{mqtt_config['topic_prefix']}-m1/status/pm1:0"
|
||||
sessions_file = test_storage_files['sessions']
|
||||
|
||||
# Anzahl Sessions vor dem Test
|
||||
|
|
@ -295,7 +305,7 @@ class TestMQTTIntegration:
|
|||
|
||||
def test_session_end_with_stop_debounce(self, bridge_process, mqtt_sender, mqtt_config, test_storage_files):
|
||||
"""Session Ende mit stop_debounce (15s unter 20W)"""
|
||||
topic = f"{mqtt_config['topic_prefix']}/status/pm1:0"
|
||||
topic = f"{mqtt_config['topic_prefix']}-m1/status/pm1:0"
|
||||
sessions_file = test_storage_files['sessions']
|
||||
|
||||
initial_count = len(read_sessions(sessions_file))
|
||||
|
|
@ -340,7 +350,7 @@ class TestMQTTIntegration:
|
|||
|
||||
def test_standby_to_working_transition(self, bridge_process, mqtt_sender, mqtt_config, test_storage_files):
|
||||
"""STANDBY → WORKING Transition"""
|
||||
topic = f"{mqtt_config['topic_prefix']}/status/pm1:0"
|
||||
topic = f"{mqtt_config['topic_prefix']}-m1/status/pm1:0"
|
||||
sessions_file = test_storage_files['sessions']
|
||||
|
||||
# Session im STANDBY starten
|
||||
|
|
@ -387,7 +397,7 @@ class TestMQTTIntegration:
|
|||
|
||||
def test_timeout_detection(self, bridge_process, mqtt_sender, mqtt_config, test_storage_files):
|
||||
"""Timeout Detection (20s keine Messages)"""
|
||||
topic = f"{mqtt_config['topic_prefix']}/status/pm1:0"
|
||||
topic = f"{mqtt_config['topic_prefix']}-m1/status/pm1:0"
|
||||
sessions_file = test_storage_files['sessions']
|
||||
|
||||
initial_count = len(read_sessions(sessions_file))
|
||||
|
|
@ -412,6 +422,128 @@ class TestMQTTIntegration:
|
|||
timeout_sessions = [s for s in sessions if s.get('end_reason') == 'timeout']
|
||||
|
||||
assert len(timeout_sessions) > 0, f"Keine timeout Session gefunden. Sessions: {json.dumps(sessions[-3:], indent=2)}"
|
||||
|
||||
def test_multi_device_parallel_sessions(self, bridge_process, mqtt_sender, mqtt_config, test_storage_files):
|
||||
"""Zwei Maschinen starten parallel Sessions"""
|
||||
topic_machine1 = f"{mqtt_config['topic_prefix']}-m1/status/pm1:0"
|
||||
topic_machine2 = f"{mqtt_config['topic_prefix']}-m2/status/pm1:0"
|
||||
sessions_file = test_storage_files['sessions']
|
||||
|
||||
initial_count = len(read_sessions(sessions_file))
|
||||
|
||||
print("\n🔄 Starte parallele Sessions auf 2 Maschinen...")
|
||||
|
||||
# Maschine 1: Session starten (20W threshold)
|
||||
send_shelly_message(mqtt_sender, topic_machine1, 50)
|
||||
time.sleep(0.5)
|
||||
|
||||
# Maschine 2: Session starten (30W threshold)
|
||||
send_shelly_message(mqtt_sender, topic_machine2, 80)
|
||||
time.sleep(0.5)
|
||||
|
||||
# Beide Maschinen halten
|
||||
for _ in range(4):
|
||||
send_shelly_message(mqtt_sender, topic_machine1, 60)
|
||||
send_shelly_message(mqtt_sender, topic_machine2, 100)
|
||||
time.sleep(1)
|
||||
|
||||
# Warten auf Sessions
|
||||
time.sleep(3)
|
||||
|
||||
# Beide Maschinen sollten jetzt laufende Sessions haben
|
||||
sessions = read_sessions(sessions_file)
|
||||
new_sessions = [s for s in sessions if len(sessions) > initial_count][-2:] # Letzte 2 neue Sessions
|
||||
running_sessions = [s for s in new_sessions if s.get('status') == 'running']
|
||||
|
||||
assert len(running_sessions) == 2, f"Erwartet exakt 2 neue laufende Sessions, gefunden: {len(running_sessions)}"
|
||||
|
||||
# Prüfen dass beide machine_ids vorhanden sind
|
||||
machine_ids = [s['machine_id'] for s in running_sessions]
|
||||
assert 'pytest-machine-01' in machine_ids, "Maschine 1 Session fehlt"
|
||||
assert 'pytest-machine-02' in machine_ids, "Maschine 2 Session fehlt"
|
||||
|
||||
print(f"✅ 2 parallele Sessions erfolgreich: {machine_ids}")
|
||||
|
||||
def test_multi_device_independent_timeouts(self, bridge_process, mqtt_sender, mqtt_config, test_storage_files):
|
||||
"""Maschine 1 Timeout, Maschine 2 läuft weiter"""
|
||||
topic_machine1 = f"{mqtt_config['topic_prefix']}-m1/status/pm1:0"
|
||||
topic_machine2 = f"{mqtt_config['topic_prefix']}-m2/status/pm1:0"
|
||||
sessions_file = test_storage_files['sessions']
|
||||
|
||||
print("\n⏱️ Teste unabhängige Timeouts...")
|
||||
|
||||
# Beide Maschinen starten
|
||||
send_shelly_message(mqtt_sender, topic_machine1, 50)
|
||||
send_shelly_message(mqtt_sender, topic_machine2, 80)
|
||||
time.sleep(1)
|
||||
|
||||
for _ in range(3):
|
||||
send_shelly_message(mqtt_sender, topic_machine1, 50)
|
||||
send_shelly_message(mqtt_sender, topic_machine2, 80)
|
||||
time.sleep(1)
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
# Maschine 1: Letzte Message, dann Timeout
|
||||
send_shelly_message(mqtt_sender, topic_machine1, 50)
|
||||
|
||||
# Maschine 2: Läuft weiter
|
||||
for _ in range(26): # 26 Sekunden
|
||||
send_shelly_message(mqtt_sender, topic_machine2, 80)
|
||||
time.sleep(1)
|
||||
|
||||
# Prüfen
|
||||
sessions = read_sessions(sessions_file)
|
||||
|
||||
# Maschine 1 sollte timeout haben
|
||||
m1_sessions = [s for s in sessions if s['machine_id'] == 'pytest-machine-01']
|
||||
timeout_session = [s for s in m1_sessions if s.get('end_reason') == 'timeout']
|
||||
assert len(timeout_session) > 0, "Maschine 1 sollte Timeout-Session haben"
|
||||
|
||||
# Maschine 2 sollte noch laufen
|
||||
m2_sessions = [s for s in sessions if s['machine_id'] == 'pytest-machine-02']
|
||||
running_session = [s for s in m2_sessions if s.get('status') == 'running']
|
||||
assert len(running_session) > 0, "Maschine 2 sollte noch laufen"
|
||||
|
||||
print("✅ Unabhängige Timeouts funktionieren")
|
||||
|
||||
def test_multi_device_different_thresholds(self, bridge_process, mqtt_sender, mqtt_config, test_storage_files):
|
||||
"""Unterschiedliche Power-Thresholds pro Maschine"""
|
||||
topic_machine1 = f"{mqtt_config['topic_prefix']}-m1/status/pm1:0"
|
||||
topic_machine2 = f"{mqtt_config['topic_prefix']}-m2/status/pm1:0"
|
||||
sessions_file = test_storage_files['sessions']
|
||||
|
||||
# Cleanup: Alte Sessions löschen für sauberen Test
|
||||
if sessions_file.exists():
|
||||
sessions_file.unlink()
|
||||
|
||||
print("\n🔧 Teste unterschiedliche Thresholds...")
|
||||
|
||||
# Maschine 1: 25W (über 20W threshold) → sollte starten
|
||||
for _ in range(4):
|
||||
send_shelly_message(mqtt_sender, topic_machine1, 25)
|
||||
time.sleep(1)
|
||||
|
||||
time.sleep(3)
|
||||
|
||||
# Maschine 2: 25W (unter 30W threshold) → sollte NICHT starten
|
||||
for _ in range(4):
|
||||
send_shelly_message(mqtt_sender, topic_machine2, 25)
|
||||
time.sleep(1)
|
||||
|
||||
time.sleep(3)
|
||||
|
||||
sessions = read_sessions(sessions_file)
|
||||
|
||||
# Maschine 1 sollte Session haben
|
||||
m1_sessions = [s for s in sessions if s['machine_id'] == 'pytest-machine-01' and s.get('status') == 'running']
|
||||
assert len(m1_sessions) > 0, "Maschine 1 sollte bei 25W Session haben (threshold 20W)"
|
||||
|
||||
# Maschine 2 sollte KEINE Session haben
|
||||
m2_sessions = [s for s in sessions if s['machine_id'] == 'pytest-machine-02' and s.get('status') == 'running']
|
||||
assert len(m2_sessions) == 0, "Maschine 2 sollte bei 25W KEINE Session haben (threshold 30W)"
|
||||
|
||||
print("✅ Unterschiedliche Thresholds funktionieren")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user