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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
18 changes: 6 additions & 12 deletions handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}")
Expand Down
57 changes: 39 additions & 18 deletions mqtt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
11 changes: 8 additions & 3 deletions paketbox.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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.")
Expand Down Expand Up @@ -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
Expand Down
37 changes: 30 additions & 7 deletions tests/test_paketbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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')
Expand Down