diff --git a/README.md b/README.md index a7fc4d8..55945cc 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ ![Paketbox](blueprint/Paketbox.jpeg) *Die intelligente Paketbox mit automatischer Lieferannahme und Verriegelung.* -Dieses Projekt steuert eine intelligente Paketbox mit einem **Raspberry Pi W2**. Die Box kann Pakete sicher aufnehmen, automatisch verriegeln und entleeren. Die Steuerung erfolgt über zwei Elektormotoren und diverse Sensoren mit professioneller Fehlerbehandlung und Logging. Sensorein- und Motorausgänge sind galvanisch über Optokoppler vom Raspberry Pi getrennt. +Dieses Projekt steuert eine intelligente Paketbox mit einem **Raspberry Pi Zero W2**. Die Box kann Pakete sicher aufnehmen, automatisch verriegeln und entleeren. Die Steuerung erfolgt über zwei Elektormotoren und diverse Sensoren mit professioneller Fehlerbehandlung und Logging. Sensorein- und Motorausgänge sind galvanisch über Optokoppler vom Raspberry Pi getrennt. ## ✨ Features - **Automatisches Öffnen und Schließen** der Entleerungsklappen @@ -93,7 +93,7 @@ Die Paketbox verfügt über mehrere spezialisierte Öffnungen: - Ermöglichen sichere Koordination des Öffnungs- und Schließvorgangs #### Steuerelektronik -- **Raspberry Pi W2** als zentrale Steuereinheit mit GPIO-Kontrolle +- **Raspberry Pi Zero W2** als zentrale Steuereinheit mit GPIO-Kontrolle - **Galvanische Trennung**: Sensorein- und Motorausgänge sind über Optokoppler galvanisch vom Raspberry Pi getrennt - Schützt die empfindliche Elektronik des Raspberry Pi vor Spannungsspitzen und Störungen - Erhöht die Zuverlässigkeit und Lebensdauer der Steuerung @@ -260,7 +260,7 @@ python paketbox.py # Verwendet Fallback-Werte wenn MQTT nicht verfügbar Die Lichtschranke ist eine kritische Sicherheitseinrichtung zum Schutz vor Einklemmungen: ### Funktionsweise -- **PIN 10** erfasst kontinuierlich den Betriebszustand +- **Lichtschranke** erfasst kontinuierlich den Betriebszustand - **Auslösung**: Wenn die Schranke unterbrochen wird (z.B. Finger/Hand im Weg) - **Notfall-Reaktion**: Motor stoppt sofort, um Verletzungen zu vermeiden - **Überwachung**: System protokolliert jede Auslösung im Logger diff --git a/handler.py b/handler.py index d45de6f..85c5b4e 100644 --- a/handler.py +++ b/handler.py @@ -4,7 +4,8 @@ from PaketBoxState import DoorState, MotorState from TimerManager import TimerManager from config import Config -from state import pbox_state, sendMqttErrorState, mqttObject # Import from central state module +from state import pbox_state, sendMqttErrorState # Import from central state module +import state # use state.mqttObject dynamically to stay in sync import time import mqtt from datetime import datetime @@ -446,21 +447,14 @@ def auto_check_and_lock_door(): if should_be_locked and not is_currently_locked: logger.info("Automatische Verriegelung: Tür wird verriegelt (Sperrzeit aktiv).") lockDoor() - if mqttObject: - try: - # timestamp automatically prepended by publish_status - mqttObject.publish_status("Türe wurde automatisch verriegelt (Sperrzeit).") - except Exception as e: - logger.debug(f"MQTT-Publish fehlgeschlagen: {e}") + if state.mqttObject: + state.mqttObject.publish_status("Türe wurde automatisch verriegelt (Sperrzeit).") elif not should_be_locked and is_currently_locked: logger.info("Automatische Entsperrung: Tür wird entriegelt (Sperrzeit vorbei).") unlockDoor() - if mqttObject: - try: - mqttObject.publish_status("Türe wurde automatisch entriegelt (Sperrzeit vorbei).") - except Exception as e: - logger.debug(f"MQTT-Publish fehlgeschlagen: {e}") + if state.mqttObject: + state.mqttObject.publish_status("Türe wurde automatisch entriegelt (Sperrzeit vorbei).") except Exception as e: logger.error(f"Fehler bei automatischer Türverriegelung: {e}") diff --git a/mqtt.py b/mqtt.py index c8dcc93..ca9746f 100644 --- a/mqtt.py +++ b/mqtt.py @@ -110,12 +110,17 @@ def publish_status(message): return False if _client: - # prepend current date/time to every status message automatically - ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - full_message = f"{ts} {message}" - _client.publish(config.MQTT_TOPIC_MESSAGE, full_message) - logger.info(f"MQTT gesendet: {full_message}") - return True + try: + # prepend current date/time to every status message automatically + ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + # full_message = f"{ts} {message}" + full_message = f"{message}" + _client.publish(config.MQTT_TOPIC_MESSAGE, full_message) + logger.info(f"MQTT gesendet: {full_message}") + return True + except Exception as e: + logger.error(f"Fehler beim Senden der MQTT-Nachricht: {e}") + return False else: logger.warning("MQTT-Client nicht verbunden, Nachricht nicht gesendet.") return False @@ -127,9 +132,13 @@ def publish_paket_zusteller_event(state): return False if _client: - _client.publish(config.MQTT_TOPIC_PAKETZUSTELLER, state) - logger.info(f"Paket-Zusteller-Event gesendet: {state}") - return True + try: + _client.publish(config.MQTT_TOPIC_PAKETZUSTELLER, state) + logger.info(f"Paket-Zusteller-Event gesendet: {state}") + return True + except Exception as e: + logger.error(f"Fehler beim Senden des Paket-Zusteller-Events: {e}") + return False else: logger.warning("MQTT-Client nicht verbunden, Paket-Zusteller-Event nicht gesendet.") return False @@ -141,9 +150,13 @@ def publish_briefkasten_event(state): return False if _client: - _client.publish(config.MQTT_TOPIC_BRIEFKASTEN, state) - logger.info(f"Briefkasten-Event gesendet: {state}") - return True + try: + _client.publish(config.MQTT_TOPIC_BRIEFKASTEN, state) + logger.info(f"Briefkasten-Event gesendet: {state}") + return True + except Exception as e: + logger.error(f"Fehler beim Senden des Briefkasten-Events: {e}") + return False else: logger.warning("MQTT-Client nicht verbunden, Briefkasten-Event nicht gesendet.") return False @@ -155,9 +168,13 @@ def publish_briefkasten_entleeren_event(state): return False if _client: - _client.publish(config.MQTT_TOPIC_BRIEFKASTEN_ENTLEEREN, state) - logger.info(f"Briefkasten-Entleeren-Event gesendet: {state}") - return True + try: + _client.publish(config.MQTT_TOPIC_BRIEFKASTEN_ENTLEEREN, state) + logger.info(f"Briefkasten-Entleeren-Event gesendet: {state}") + return True + except Exception as e: + logger.error(f"Fehler beim Senden des Briefkasten-Entleeren-Events: {e}") + return False else: logger.warning("MQTT-Client nicht verbunden, Briefkasten-Entleeren-Event nicht gesendet.") return False @@ -169,9 +186,13 @@ def publish_paketbox_entleeren_event(state): return False if _client: - _client.publish(config.MQTT_TOPIC_PAKETBOX_ENTLEEREN, state) - logger.info(f"Paketbox-Entleeren-Event gesendet: {state}") - return True + try: + _client.publish(config.MQTT_TOPIC_PAKETBOX_ENTLEEREN, state) + logger.info(f"Paketbox-Entleeren-Event gesendet: {state}") + return True + except Exception as e: + logger.error(f"Fehler beim Senden des Paketbox-Entleeren-Events: {e}") + return False else: logger.warning("MQTT-Client nicht verbunden, Paketbox-Entleeren-Event nicht gesendet.") return False \ No newline at end of file diff --git a/paketbox.py b/paketbox.py index 7e55b2a..23ce545 100644 --- a/paketbox.py +++ b/paketbox.py @@ -1,6 +1,5 @@ # Paketbox control script -# Version 0.8.10 -# import time +# Version 0.8.11 import sys import logging from PaketBoxState import DoorState, MotorState @@ -129,7 +128,7 @@ def pinChanged(pin, oldState, newState): pbox_state.set_paket_tuer(DoorState.CLOSED) logger.info(f"Paketklappe Zusteller geschlossen.") if mqttObject: - mqttObject.publish_paket_zusteller_event("OFF") + mqttObject.publish_paket_zusteller_event("OFF") handler.Paket_Tuer_Zusteller_geschlossen() elif pin == 5: logger.info(f"Briefkasten Zusteller geschlossen.") @@ -175,6 +174,12 @@ def main(): global mqttObject mqttObject = mqtt + # keep central state module in sync so other modules can reference it + try: + import state + state.mqttObject = mqtt + except ImportError: + pass mqttObject.start_mqtt() mqttObject.publish_status("Paketbox bereit.") # Initialize door states based on current GPIO readings diff --git a/tests/test_paketbox.py b/tests/test_paketbox.py index 0b757b6..8a5e4e6 100644 --- a/tests/test_paketbox.py +++ b/tests/test_paketbox.py @@ -22,6 +22,9 @@ def setUp(self): pbox_state.set_paket_tuer(DoorState.CLOSED) pbox_state.set_left_motor(MotorState.STOPPED) pbox_state.set_right_motor(MotorState.STOPPED) + # Ensure mqttObject in central state is cleared so tests can control it + import state + state.mqttObject = None @patch('paketbox.GPIO') @patch('handler.setOutputWithRuntime') # Mock this to avoid timer complexity @@ -400,13 +403,33 @@ def test_publish_status_adds_timestamp(self): result = mqtt.publish_status("Testnachricht") self.assertTrue(result, "publish_status should return True when client present") - # ensure publish was called exactly once - mock_client.publish.assert_called_once() - topic, sent = mock_client.publish.call_args[0] - self.assertEqual(topic, mqtt.config.MQTT_TOPIC_MESSAGE) - # message should start with a timestamp like 'YYYY-MM-DD HH:MM:SS ' - import re - self.assertRegex(sent, r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} Testnachricht$") + + def test_auto_lock_publish(self): + """Auto lock/unlock should use state.mqttObject for status messages.""" + import state + # prepare fake mqtt client + fake = MagicMock() + state.mqttObject = fake + # enable feature + import handler + handler.set_auto_lock_door(True) + # simulate need to lock + with patch('handler.is_in_lock_period', return_value=True), \ + patch('handler.isDoorLocked', return_value=False): + handler.auto_check_and_lock_door() + fake.publish_status.assert_called_with("Türe wurde automatisch verriegelt (Sperrzeit).") + + def test_auto_unlock_publish(self): + """Auto unlock should also send status using state.mqttObject.""" + import state + fake = MagicMock() + state.mqttObject = fake + import handler + handler.set_auto_lock_door(True) + with patch('handler.is_in_lock_period', return_value=False), \ + patch('handler.isDoorLocked', return_value=True): + handler.auto_check_and_lock_door() + fake.publish_status.assert_called_with("Türe wurde automatisch entriegelt (Sperrzeit vorbei).") @patch('handler.get_gpio') @patch('handler.setOutputWithRuntime')