diff --git a/docs/LockControlImplementation.md b/docs/LockControlImplementation.md new file mode 100644 index 0000000..6bfccc0 --- /dev/null +++ b/docs/LockControlImplementation.md @@ -0,0 +1,147 @@ +# Remote Lock Control Implementation + +## Overview +This document describes the implementation of a remote lock control feature that allows Home Assistant to control a relay connected to the wallbox's remote lock mechanism via MQTT. + +## Hardware Configuration + +### Pin Assignments + +#### LILYGO T-CAN485 Board +- **Lock Relay Pin**: GPIO18 +- **Logic**: HIGH = Unlocked, LOW = Locked +- **Default State**: Locked (LOW) + +#### Regular ESP32 Board +- **Lock Relay Pin**: GPIO25 +- **Logic**: HIGH = Unlocked, LOW = Locked +- **Default State**: Locked (LOW) + +### Relay Connection +Connect a relay module to the designated GPIO pin: +- VCC → 3.3V or 5V (depending on relay module) +- GND → GND +- IN → GPIO pin (GPIO18 for LILYGO, GPIO25 for ESP32) +- NO/COM → Connected to wallbox remote lock terminals + +## Software Implementation + +### Components Added + +#### 1. Board Configuration Updates +- **Board.h/Board.cpp**: Added `GetPinLockRelay()` method +- **BoardLilygo.cpp**: Configured GPIO18 as lock relay pin +- **BoardESP32.cpp**: Configured GPIO25 as lock relay pin + +#### 2. LockController Component (`src/Components/LockController/`) +- **LockController.h**: Interface for lock control functionality +- **LockController.cpp**: Implementation with singleton pattern +- **Methods**: + - `Init()`: Initialize the lock relay pin + - `SetLockState(bool unlocked)`: Control relay state + - `GetLockState()`: Get current lock state + +#### 3. MQTT Integration Updates +- **Discovery Topic**: `homeassistant/lock/{device_name}/lock_control/config` +- **Command Topic**: `{device_name}/lock_control/command` +- **State Topic**: `{device_name}/lock_control/state` +- **Payloads**: + - Commands: "LOCK" / "UNLOCK" + - States: "LOCKED" / "UNLOCKED" + +#### 4. Main Application Updates +- Added LockController initialization in `setup()` + +## Home Assistant Integration + +The lock control will appear in Home Assistant as: +- **Entity Type**: Lock +- **Entity Name**: "Lock Control" +- **Entity ID**: `lock.{device_name}_lock_control` +- **Device Class**: Lock +- **Commands**: Lock/Unlock buttons +- **State Feedback**: Shows current lock state + +### MQTT Topics + +#### Discovery Configuration Topic: +``` +homeassistant/lock/{device_name}/lock_control/config +``` + +#### Command Topic (Home Assistant → HeidelBridge): +``` +{device_name}/lock_control/command +``` +Payloads: "LOCK" or "UNLOCK" + +#### State Topic (HeidelBridge → Home Assistant): +``` +{device_name}/lock_control/state +``` +Payloads: "LOCKED" or "UNLOCKED" + +## Usage Instructions + +### Setup +1. Connect a relay module to the appropriate GPIO pin +2. Wire the relay output to the wallbox remote lock terminals +3. Deploy the firmware to the ESP32 +4. The lock control will automatically appear in Home Assistant + +### Operation +1. In Home Assistant, find the "Lock Control" entity +2. Use the lock/unlock buttons to control the relay +3. The state will be reflected in real-time +4. Default state is locked on boot + +## Technical Details + +### Relay Logic +- **Active HIGH**: GPIO HIGH = Relay ON = Lock UNLOCKED +- **Active LOW**: GPIO LOW = Relay OFF = Lock LOCKED +- **Default**: Locked state on startup for security + +### Error Handling +- If MQTT connection fails, lock state remains unchanged +- Pin initialization occurs during board setup +- State is maintained in memory and published periodically + +### Logging +- Lock state changes are logged at INFO level +- MQTT command reception logged at TRACE level +- Pin initialization logged at INFO level + +## Safety Considerations + +1. **Default Locked**: System defaults to locked state for security +2. **Power Loss**: On power loss/restart, lock returns to locked state +3. **GPIO Selection**: Chosen pins are safe and don't conflict with existing functionality +4. **Relay Type**: Use appropriate relay module rated for wallbox voltage/current + +## Testing + +To test the lock control feature: +1. Monitor serial output for lock controller initialization +2. Check Home Assistant for the new lock entity +3. Test lock/unlock commands from Home Assistant interface +4. Verify relay operation with multimeter +5. Check MQTT state topic updates + +## Troubleshooting + +### Lock not appearing in Home Assistant +- Check MQTT connection and discovery messages +- Verify Home Assistant MQTT integration is enabled +- Check device name configuration + +### Relay not switching +- Verify GPIO pin wiring +- Check relay module power supply +- Monitor serial logs for state changes +- Test GPIO pin with multimeter + +### Commands not working +- Check MQTT subscription topics +- Verify payload format (LOCK/UNLOCK) +- Monitor MQTT logs in Home Assistant diff --git a/docs/PhysicalLockControlBenefits.md b/docs/PhysicalLockControlBenefits.md new file mode 100644 index 0000000..42f643c --- /dev/null +++ b/docs/PhysicalLockControlBenefits.md @@ -0,0 +1,89 @@ +# Physical Lock Control with Relay Switch + +## Overview + +The HeidelBridge firmware supports an optional physical lock control feature using a relay switch. This provides a hardware-level safety mechanism that physically controls the wallbox's lock mechanism, working alongside the existing Modbus-based current control. + +## What Does the Lock Control Do? + +The physical lock control uses a relay to control the wallbox's **physical lock mechanism**. When activated, it prevents the charging cable from being inserted or removed, and the wallbox will report this lock state via Modbus register 259. + +**Important**: The wallbox itself remains powered and operational - this control only affects the physical locking mechanism, not the power supply. + +## Key Benefits + +### 1. **Fail-Safe Operation** +**This is the main advantage**: If the ESP32 loses power, crashes, or Modbus communication fails, the relay defaults to the **locked state**. + +- **Modbus current limiting**: If communication fails, wallbox may continue with last known settings or default behavior +- **Physical lock relay**: Always defaults to "locked" when ESP32 is disconnected - **guaranteed safe state** + +### 2. **Reliable Lock Control** +- **Hardware-level control**: Direct relay control is more reliable than software-only Modbus commands +- **Immediate response**: No communication delays or potential Modbus timeouts +- **Independent of wallbox firmware**: Works regardless of wallbox software state + +### 3. **True Safety Backup** +When something goes wrong with your home automation system: +- ESP32 loses power → Relay opens → Wallbox locks → No new charging sessions can start +- Network failure → No communication needed for lock to engage +- Software crash → Hardware relay maintains safe state + +## Comparison: Software vs Hardware Control + +| Scenario | Modbus Current Control | Physical Lock Relay | +|----------|----------------------|-------------------| +| **Normal Operation** | ✅ Fast, flexible | ✅ Reliable | +| **ESP32 Power Loss** | ⚠️ Wallbox uses defaults | ✅ **Locks automatically** | +| **Network Failure** | ⚠️ No new commands possible | ✅ **Maintains safe state** | +| **Modbus Timeout** | ⚠️ May retry or timeout | ✅ **Lock remains active** | +| **Software Crash** | ⚠️ Unknown state | ✅ **Defaults to locked** | + +## Use Cases + +### **Primary Use Case: Fail-Safe Protection** +The main benefit is having a **guaranteed safe default**: if anything goes wrong with your automation system, the wallbox automatically locks and prevents new charging sessions. + +### **Secondary Use Cases:** +- **Maintenance safety**: Physically prevent charging during wallbox maintenance +- **Emergency situations**: Immediate, reliable way to prevent charging +- **Simple on/off control**: Clear physical state that doesn't rely on communication + +## Implementation Details + +### Hardware Connection +- **ESP32**: Relay control on GPIO25 +- **LILYGO T-CAN485**: Relay control on GPIO18 +- **Relay Logic**: + - HIGH = Unlocked (charging allowed) + - LOW = Locked (charging prevented) - **Default when ESP32 is off** + +### Wiring +Connect the relay contacts to the wallbox's lock control input (refer to your wallbox manual for specific connection details). + +## Home Assistant Integration + +The physical lock appears as a standard lock entity: +- **Entity**: `lock.{device_name}_lock_control` +- **States**: 🔒 Locked / 🔓 Unlocked +- **Default**: Locked (when ESP32 is disconnected) + +## Best Practice: Use Both Controls + +For optimal safety and flexibility: + +1. **Modbus current control** for normal operation: + - Dynamic current adjustment + - Fast response to changing conditions + - Integration with smart home systems + +2. **Physical lock relay** as safety backup: + - Guaranteed safe state if communication fails + - Simple emergency stop functionality + - Peace of mind for unattended operation + +## Conclusion + +The physical lock control provides a simple but crucial safety feature: **it defaults to the safe state when the ESP32 is disconnected**. While Modbus control offers flexibility for normal operation, the hardware relay ensures that if your home automation system fails, the wallbox will automatically lock and prevent unexpected charging sessions. + +This fail-safe behavior makes it an excellent complement to software-based controls, providing reliability where it matters most. diff --git a/docs/RelayWiringGuide.md b/docs/RelayWiringGuide.md new file mode 100644 index 0000000..b1e5478 --- /dev/null +++ b/docs/RelayWiringGuide.md @@ -0,0 +1,167 @@ +# Relay Wiring Guide for Physical Lock Control + +## Overview + +This guide explains how to wire a relay module to control the physical lock mechanism of your Heidelberg wallbox using the HeidelBridge system. + +## Required Components + +- **Relay Module**: 5V relay module with opto-isolation (recommended) +- **Jumper Wires**: For connections between ESP32/LILYGO and relay module +- **Multimeter**: For testing and verification + +### Recommended Relay Module Specifications +- **Type**: Single channel 5V relay module with opto-isolation +- **Trigger**: 3.3V compatible (most modules work with ESP32's 3.3V output) +- **Contacts**: At least 5A @ 250VAC rating (though wallbox lock circuits are typically low voltage) +- **Form**: SPDT (Single Pole, Double Throw) or SPST (Single Pole, Single Throw) + +## Pin Connections + +### Control Side (ESP32/LILYGO to Relay Module) + +| ESP32 Board | LILYGO Board | Relay Module | Function | +|-------------|--------------|--------------|----------| +| GPIO25 | GPIO18 | IN / Signal | Control signal | +| 5V or 3.3V | 5V or 3.3V | VCC | Power supply | +| GND | GND | GND | Ground reference | + +#### Wiring Diagram - Control Side +``` +ESP32/LILYGO Board Relay Module +┌─────────────────┐ ┌──────────────┐ +│ │ │ │ +│ GPIO25/18 ──┼─────────┼── IN/Signal │ +│ │ │ │ +│ VCC (5V) ────┼─────────┼── VCC │ +│ │ │ │ +│ GND ─────────┼─────────┼── GND │ +│ │ │ │ +└─────────────────┘ └──────────────┘ +``` + +### Contact Side (Relay to Wallbox) + +The relay contacts connect to the wallbox's remote lock control connector. + +#### Typical Wallbox Lock Connector Pinout +**⚠️ Important**: Check your specific wallbox manual for exact pinout! + +Common configurations: +- **Pin 1**: Lock control signal (connect to relay COM) +- **Pin 2**: Ground/Common (connect to relay NC for "normally locked") +- **Pin 3**: +12V or +24V supply (may not be used for lock control) + +#### Wiring Diagram - Contact Side +``` +Wallbox Lock Connector Relay Module Contacts +┌─────────────────────┐ ┌─────────────────────┐ +│ │ │ │ +│ Pin 1 (Lock Ctrl) ─┼─────┼── COM (Common) │ +│ │ │ │ +│ Pin 2 (Ground) ─┼─────┼── NC (Normally Closed) │ +│ │ │ │ +│ Pin 3 (Power) ─┼─ ─ ─┼── NO (Normally Open) │ +│ (if needed) │ │ (not used) │ +└─────────────────────┘ └─────────────────────┘ +``` + +## Complete Wiring Setup + +### Full Connection Diagram +``` +ESP32/LILYGO Relay Module Wallbox +┌─────────────────┐ ┌──────────────┐ ┌──────────────┐ +│ │ │ Control │ │ │ +│ GPIO25/18 ──┼─────────┼── IN │ │ │ +│ VCC (5V) ────┼─────────┼── VCC │ │ │ +│ GND ─────────┼─────────┼── GND │ │ │ +│ │ │ │ │ │ +│ │ │ Contacts │ │ Lock Conn │ +│ │ │ │ │ │ +│ │ │ COM ───────┼─────────┼── Pin 1 │ +│ │ │ NC ────────┼─────────┼── Pin 2 │ +│ │ │ NO │ │ │ +│ │ │ (unused) │ │ │ +└─────────────────┘ └──────────────┘ └──────────────┘ +``` + +## Logic Operation + +### Relay States +- **ESP32 GPIO LOW (0V)**: Relay OFF → Contacts connect COM to NC → **Wallbox LOCKED** +- **ESP32 GPIO HIGH (3.3V)**: Relay ON → Contacts connect COM to NO → **Wallbox UNLOCKED** + +### Fail-Safe Behavior +When ESP32 loses power or crashes: +- GPIO pin goes to LOW (0V) +- Relay turns OFF +- Contacts default to NC position +- **Wallbox automatically locks** ✅ + +## Step-by-Step Installation + +### Step 1: Power Down +1. Turn off power to the wallbox +2. Disconnect the ESP32/LILYGO from power +3. Ensure all components are de-energized + +### Step 2: Connect Control Side +1. Connect GPIO25 (ESP32) or GPIO18 (LILYGO) to relay module IN pin +2. Connect VCC pin to relay module VCC (5V preferred, 3.3V usually works) +3. Connect GND pin to relay module GND + +### Step 3: Connect Wallbox Side +1. Locate the wallbox remote lock connector +2. Identify lock control and ground pins (consult wallbox manual) +3. Connect relay COM to lock control pin +4. Connect relay NC to ground pin + +### Step 4: Testing +1. Power up ESP32/LILYGO (wallbox still off) +2. Use multimeter to test relay operation: + - Send unlock command → Should hear relay click, COM-NO connected + - Send lock command → Should hear relay click, COM-NC connected +3. Power up wallbox and test lock operation + +## Troubleshooting + +### Relay Doesn't Click +- **Check power supply**: Ensure VCC is connected and at correct voltage +- **Check control signal**: Verify GPIO pin voltage changes (0V ↔ 3.3V) +- **Check relay module**: Some modules need 5V, won't work with 3.3V + +### Lock Doesn't Respond +- **Check wallbox manual**: Verify lock connector pinout +- **Check connections**: Ensure solid connections to wallbox connector +- **Check polarity**: Some wallboxes are polarity sensitive + +### Inverted Operation +If locked/unlocked states are reversed: +- Swap NC and NO connections on relay +- Or modify software logic (not recommended) + +## Safety Notes + +⚠️ **Important Safety Warnings**: +- Always consult your wallbox manual for specific lock connector details +- Use appropriately rated relay for your wallbox lock circuit +- Follow local electrical codes and regulations +- Test the fail-safe behavior before relying on it +- Consider using industrial-grade relays for critical applications + +## Wallbox-Specific Notes + +### Heidelberg Energy Control +- Typically uses low-voltage lock control signals +- Connector may be labeled "Remote" or "Lock" +- Check manual for exact pinout + +### Other Wallbox Brands +- This wiring guide applies to most wallboxes with remote lock inputs +- Always verify connector pinout with manufacturer documentation +- Some wallboxes may use different voltage levels or logic + +## Conclusion + +This relay wiring setup provides a simple and reliable way to add physical lock control to your wallbox. The key benefit is the fail-safe operation: if the ESP32 loses power, the relay automatically defaults to the locked position, ensuring safe operation even during system failures. diff --git a/docs/RemoteLockStatusSensor.md b/docs/RemoteLockStatusSensor.md new file mode 100644 index 0000000..35d89c7 --- /dev/null +++ b/docs/RemoteLockStatusSensor.md @@ -0,0 +1,164 @@ +# Remote Lock Status Sensor and Lock Control Implementation + +## Overview +This document describes the implementation of: +1. A Home Assistant sensor that reads the remote lock status from Modbus register 259 (**always available**) +2. A Home Assistant lock control that controls a relay via GPIO pin for physical lock control (**optional, enabled with build flag**) + +## Build Configuration + +### Available Environments + +**Without Lock Relay Control (Default):** +- `lilygo` - LILYGO T-CAN485 board, Modbus sensor only +- `esp32` - Regular ESP32 board, Modbus sensor only +- `dummy` - Dummy wallbox simulation, Modbus sensor only + +**With Lock Relay Control:** +- `lilygo_with_lock_relay` - LILYGO T-CAN485 + GPIO relay control +- `esp32_with_lock_relay` - Regular ESP32 + GPIO relay control +- `dummy_with_lock_relay` - Dummy wallbox + GPIO relay control + +### Build Flag: `ENABLE_LOCK_RELAY` + +When this flag is defined: +- GPIO pins are configured for relay control +- Lock control MQTT topics are enabled +- Home Assistant lock entity is created +- Physical pin control is active + +When this flag is **not** defined: +- Only Modbus remote lock status sensor is available +- No GPIO pins are used for relay control +- No lock control MQTT topics +- Reduced memory usage + +## Implementation Details + +### Part 1: Remote Lock Status Sensor (Modbus-based) - **ALWAYS AVAILABLE** + +#### Modbus Register +- **Register Address**: 259 +- **Data Type**: 16-bit unsigned integer +- **Values**: + - 0 = Locked + - 1 = Unlocked (default as per specifications) + +### Part 2: Lock Control (GPIO-based Relay Control) - **OPTIONAL WITH BUILD FLAG** + +#### Hardware Setup (Only with ENABLE_LOCK_RELAY) +**LILYGO T-CAN485 Board:** +- **Control Pin**: GPIO18 +- **Relay Connection**: Connect relay control input to GPIO18 +- **Relay Logic**: HIGH = Unlock, LOW = Lock + +**Regular ESP32 Board:** +- **Control Pin**: GPIO25 +- **Relay Connection**: Connect relay control input to GPIO25 +- **Relay Logic**: HIGH = Unlock, LOW = Lock + +#### MQTT Topics for Lock Control (Only with ENABLE_LOCK_RELAY) +- **Command Topic**: `{device_name}/lock_control/command` + - Payloads: "LOCK" or "UNLOCK" +- **State Topic**: `{device_name}/lock_control/state` + - Payloads: "LOCKED" or "UNLOCKED" + +## Usage Instructions + +### Option 1: Modbus Remote Lock Status Only +```bash +# Build for LILYGO without relay control +pio run -e lilygo + +# Build for ESP32 without relay control +pio run -e esp32 + +# Build for dummy simulation without relay control +pio run -e dummy +``` + +**Features Available:** +- ✅ Remote lock status sensor (from Modbus register 259) +- ❌ Physical lock control via relay + +### Option 2: Modbus Remote Lock Status + Physical Lock Control +```bash +# Build for LILYGO with relay control +pio run -e lilygo_with_lock_relay + +# Build for ESP32 with relay control +pio run -e esp32_with_lock_relay + +# Build for dummy simulation with relay control +pio run -e dummy_with_lock_relay +``` + +**Features Available:** +- ✅ Remote lock status sensor (from Modbus register 259) +- ✅ Physical lock control via relay (GPIO pin) + +## Home Assistant Integration + +### Entities Created + +#### 1. Remote Lock Status (Binary Sensor) - **ALWAYS AVAILABLE** +- **Entity Name**: Remote Lock Status +- **Entity ID**: `binary_sensor.{device_name}_remote_lock_status` +- **Device Class**: Lock +- **Data Source**: Modbus register 259 + +#### 2. Lock Control (Lock Entity) - **ONLY WITH ENABLE_LOCK_RELAY** +- **Entity Name**: Lock Control +- **Entity ID**: `lock.{device_name}_lock_control` +- **Device Class**: Lock +- **Control Method**: MQTT commands to GPIO relay + +## Debugging and Troubleshooting + +### Build Flag Verification +Check serial output during startup: + +**Without ENABLE_LOCK_RELAY:** +``` +[INFO] Lock controller disabled (ENABLE_LOCK_RELAY not defined) +``` + +**With ENABLE_LOCK_RELAY:** +``` +[INFO] Initializing lock controller on pin 18 +[INFO] Lock controller initialized successfully +``` + +### Expected Behavior + +**Without Relay Control:** +- Only the binary sensor appears in Home Assistant +- No lock control entity is created +- No GPIO pins are configured for relay +- Commands like "UNLOCK" are ignored + +**With Relay Control:** +- Both binary sensor AND lock control entities appear +- GPIO pin voltage changes when commands are sent +- Lock control responds to Home Assistant commands + +## Migration Guide + +### From Relay Control to Modbus-Only +1. Change build environment from `*_with_lock_relay` to base environment +2. Rebuild and upload firmware +3. The lock control entity will disappear from Home Assistant +4. The binary sensor will continue working + +### From Modbus-Only to Relay Control +1. Connect relay hardware to appropriate GPIO pin +2. Change build environment to `*_with_lock_relay` +3. Rebuild and upload firmware +4. New lock control entity will appear in Home Assistant + +## Safety Considerations + +- The Modbus remote lock status sensor is read-only and always safe +- Physical relay control should only be enabled when appropriate hardware is connected +- Use the appropriate build flag for your hardware configuration +- Test thoroughly before permanent installation diff --git a/platformio.ini b/platformio.ini index cb6ecf4..0282838 100644 --- a/platformio.ini +++ b/platformio.ini @@ -9,11 +9,11 @@ ; https://docs.platformio.org/page/projectconf.html [platformio] -default_envs = esp32 +default_envs = esp32_with_lock_relay # Common settings [env] -upload_port = /dev/ttyUSB* +;upload_port = 192.168.178.170 framework = arduino board_build.partitions = partitions.csv monitor_speed = 115200 @@ -41,6 +41,18 @@ build_flags = -D BOARD_ESP32 -D LOGGING_LEVEL_DEBUG +# Dummy build with lock relay control enabled +[env:dummy_with_lock_relay] +platform = espressif32@^6.8.1 +board = esp32doit-devkit-v1 +upload_protocol = esptool +build_flags = + -O2 + -D DUMMY_WALLBOX + -D BOARD_ESP32 + -D LOGGING_LEVEL_DEBUG + -D ENABLE_LOCK_RELAY + # Default environment for regular ESP32 boards. [env:esp32] platform = espressif32@^6.8.1 @@ -51,6 +63,17 @@ build_flags = -D BOARD_ESP32 -D LOGGING_LEVEL_ERROR +# ESP32 with lock relay control enabled +[env:esp32_with_lock_relay] +platform = espressif32@^6.8.1 +board = esp32doit-devkit-v1 +upload_protocol = esptool +build_flags = + -O2 + -D BOARD_ESP32 + -D LOGGING_LEVEL_ERROR + -D ENABLE_LOCK_RELAY + # LILYGO T-CAN485, ESP32 board with on-board RS485 [env:lilygo] platform = espressif32@^6.8.1 @@ -59,4 +82,15 @@ upload_protocol = esptool build_flags = -O2 -D BOARD_LILYGO - -D LOGGING_LEVEL_ERROR \ No newline at end of file + -D LOGGING_LEVEL_ERROR + +# LILYGO T-CAN485 with lock relay control enabled +[env:lilygo_with_lock_relay] +platform = espressif32@^6.8.1 +board = esp32dev +upload_protocol = esptool +build_flags = + -O2 + -D BOARD_LILYGO + -D LOGGING_LEVEL_ERROR + -D ENABLE_LOCK_RELAY \ No newline at end of file diff --git a/src/Boards/Board.cpp b/src/Boards/Board.cpp index e4ab6e6..e8388a0 100644 --- a/src/Boards/Board.cpp +++ b/src/Boards/Board.cpp @@ -2,10 +2,17 @@ #include "Board.h" // Constructor +#ifdef ENABLE_LOCK_RELAY +Board::Board(uint8_t pinRx, uint8_t pinTx, uint8_t pinRts, uint8_t pinLockRelay) + : mPinRx(pinRx), mPinTx(pinTx), mPinRts(pinRts), mPinLockRelay(pinLockRelay) +{ +} +#else Board::Board(uint8_t pinRx, uint8_t pinTx, uint8_t pinRts) : mPinRx(pinRx), mPinTx(pinTx), mPinRts(pinRts) { } +#endif // These functions return the pins used by this board uint8_t Board::GetPinRx() @@ -23,4 +30,12 @@ uint8_t Board::GetPinTx() uint8_t Board::GetPinRts() { return mPinRts; -} \ No newline at end of file +} + +#ifdef ENABLE_LOCK_RELAY +// These functions return the pins used by this board +uint8_t Board::GetPinLockRelay() +{ + return mPinLockRelay; +} +#endif \ No newline at end of file diff --git a/src/Boards/Board.h b/src/Boards/Board.h index e0f9442..d1c6832 100644 --- a/src/Boards/Board.h +++ b/src/Boards/Board.h @@ -4,7 +4,11 @@ class Board { protected: // Constructor +#ifdef ENABLE_LOCK_RELAY + Board(uint8_t pinRx, uint8_t pinTx, uint8_t pinRts, uint8_t pinLockRelay); +#else Board(uint8_t pinRx, uint8_t pinTx, uint8_t pinRts); +#endif public: // Initializes the board @@ -17,7 +21,13 @@ class Board uint8_t GetPinRx(); uint8_t GetPinTx(); uint8_t GetPinRts(); +#ifdef ENABLE_LOCK_RELAY + uint8_t GetPinLockRelay(); +#endif private: uint8_t mPinRx, mPinTx, mPinRts; +#ifdef ENABLE_LOCK_RELAY + uint8_t mPinLockRelay; +#endif }; \ No newline at end of file diff --git a/src/Boards/ESP32/BoardESP32.cpp b/src/Boards/ESP32/BoardESP32.cpp index fbe6414..50a5260 100644 --- a/src/Boards/ESP32/BoardESP32.cpp +++ b/src/Boards/ESP32/BoardESP32.cpp @@ -10,10 +10,17 @@ constexpr uint8_t PinRX = GPIO_NUM_18; constexpr uint8_t PinTX = GPIO_NUM_19; constexpr uint8_t PinRTS = GPIO_NUM_21; +#ifdef ENABLE_LOCK_RELAY +constexpr uint8_t PinLockRelay = GPIO_NUM_25; // Available GPIO for lock relay control +#endif // Constructor BoardESP32::BoardESP32() +#ifdef ENABLE_LOCK_RELAY + : Board(PinRX, PinTX, PinRTS, PinLockRelay) +#else : Board(PinRX, PinTX, PinRTS) +#endif { // Nothing to do } @@ -21,7 +28,11 @@ BoardESP32::BoardESP32() // Initializes the board void BoardESP32::Init() { - // Nothing to do +#ifdef ENABLE_LOCK_RELAY + // Initialize lock relay pin (active HIGH to unlock) + pinMode(PinLockRelay, OUTPUT); + digitalWrite(PinLockRelay, LOW); // Default to locked +#endif } // Logs board name/information diff --git a/src/Boards/Lilygo/BoardLilygo.cpp b/src/Boards/Lilygo/BoardLilygo.cpp index 8c82622..499a2c6 100644 --- a/src/Boards/Lilygo/BoardLilygo.cpp +++ b/src/Boards/Lilygo/BoardLilygo.cpp @@ -10,6 +10,9 @@ constexpr uint8_t PinRX = GPIO_NUM_21; constexpr uint8_t PinTX = GPIO_NUM_22; constexpr uint8_t PinRTS = GPIO_NUM_21; +#ifdef ENABLE_LOCK_RELAY +constexpr uint8_t PinLockRelay = GPIO_NUM_18; // Available GPIO for lock relay control +#endif // Additional pins constexpr uint8_t Pin_5V_EN = 16; @@ -28,7 +31,11 @@ constexpr uint8_t Pin_WS2812 = 4; // Constructor BoardLilygo::BoardLilygo() +#ifdef ENABLE_LOCK_RELAY + : Board(PinRX, PinTX, PinRTS, PinLockRelay) +#else : Board(PinRX, PinTX, PinRTS) +#endif { // Nothing to do } @@ -44,6 +51,12 @@ void BoardLilygo::Init() pinMode(Pin_5V_EN, OUTPUT); digitalWrite(Pin_5V_EN, HIGH); + +#ifdef ENABLE_LOCK_RELAY + // Initialize lock relay pin (active HIGH to unlock) + pinMode(PinLockRelay, OUTPUT); + digitalWrite(PinLockRelay, LOW); // Default to locked +#endif } // Logs board name/information diff --git a/src/Components/LockController/LockController.cpp b/src/Components/LockController/LockController.cpp new file mode 100644 index 0000000..2ec9a5c --- /dev/null +++ b/src/Components/LockController/LockController.cpp @@ -0,0 +1,61 @@ +#include +#include "../Logger/Logger.h" +#include "../../Boards/BoardFactory.h" +#include "../../Boards/Board.h" +#include "LockController.h" + +// Returns the singleton instance of LockController +LockController *LockController::Instance() +{ + static LockController instance; + return &instance; +} + +// Initializes the LockController instance +void LockController::Init() +{ +#ifdef ENABLE_LOCK_RELAY + // Get the lock relay pin from the board configuration + mLockRelayPin = BoardFactory::Instance()->GetBoard()->GetPinLockRelay(); + + Logger::Info("Initializing lock controller on pin %d", mLockRelayPin); + + // Initialize the pin (already done in Board::Init(), but ensure it's set correctly) + pinMode(mLockRelayPin, OUTPUT); + SetLockState(false); // Default to locked state + + Logger::Info("Lock controller initialized successfully"); +#else + Logger::Info("Lock controller disabled (ENABLE_LOCK_RELAY not defined)"); +#endif +} + +// Sets the lock state (true = unlocked, false = locked) +void LockController::SetLockState(bool unlocked) +{ +#ifdef ENABLE_LOCK_RELAY + mIsUnlocked = unlocked; + + // Set the relay pin (HIGH = unlocked, LOW = locked) + digitalWrite(mLockRelayPin, unlocked ? HIGH : LOW); + + Logger::Info("Lock state changed to: %s (pin %d set to %s)", + unlocked ? "unlocked" : "locked", + mLockRelayPin, + unlocked ? "HIGH" : "LOW"); + + // Debug: Read back the pin state to verify + int pinState = digitalRead(mLockRelayPin); + Logger::Debug("Pin %d readback value: %d", mLockRelayPin, pinState); +#else + // Update state but don't control any pin + mIsUnlocked = unlocked; + Logger::Info("Lock state changed to: %s (no physical relay control)", unlocked ? "unlocked" : "locked"); +#endif +} + +// Gets the current lock state (true = unlocked, false = locked) +bool LockController::GetLockState() +{ + return mIsUnlocked; +} diff --git a/src/Components/LockController/LockController.h b/src/Components/LockController/LockController.h new file mode 100644 index 0000000..a911ebd --- /dev/null +++ b/src/Components/LockController/LockController.h @@ -0,0 +1,27 @@ +#pragma once + +class LockController +{ +private: + // Private constructor to prevent instantiation + LockController() {}; + +public: + // Returns the singleton instance of LockController + static LockController *Instance(); + + // Initializes the LockController instance + void Init(); + + // Sets the lock state (true = unlocked, false = locked) + void SetLockState(bool unlocked); + + // Gets the current lock state (true = unlocked, false = locked) + bool GetLockState(); + +private: + bool mIsUnlocked = false; // Current lock state +#ifdef ENABLE_LOCK_RELAY + uint8_t mLockRelayPin = 0; // Pin for controlling the lock relay +#endif +}; diff --git a/src/Components/MQTT/MQTTManager.cpp b/src/Components/MQTT/MQTTManager.cpp index 036f512..e579fa6 100644 --- a/src/Components/MQTT/MQTTManager.cpp +++ b/src/Components/MQTT/MQTTManager.cpp @@ -12,6 +12,7 @@ extern "C" #include "../../Configuration/Constants.h" #include "../../Configuration/Settings.h" #include "../Wallbox/IWallbox.h" +#include "../LockController/LockController.h" #include "../../Utils/PrefixedString.h" #include "../../Utils/StringUtils.h" #include "MQTTManager.h" @@ -27,7 +28,11 @@ namespace MQTTManager char PayloadBuffer[512]; PrefixedString gMqttTopic(128); - constexpr uint16_t NumMqttPublishedValues = 10; +#ifdef ENABLE_LOCK_RELAY + constexpr uint16_t NumMqttPublishedValues = 12; +#else + constexpr uint16_t NumMqttPublishedValues = 11; +#endif enum MqttPublishedValues { VehicleState, @@ -38,6 +43,10 @@ namespace MQTTManager ChargingCurrent, ChargingVoltage, Temperature, + RemoteLockStatus, +#ifdef ENABLE_LOCK_RELAY + LockControllerStatus, +#endif Internals, Discovery }; @@ -93,6 +102,16 @@ namespace MQTTManager "homeassistant/sensor/%/temperature/config", R"({"name":"Temperature","device_class":"temperature","state_topic":"%/temperature","unique_id":"%_temperature","object_id":"temperature","unit_of_measurement":"°C","device":{"identifiers":["%"],"name":"%","model":"EnergyControl","manufacturer":"Heidelberg"}})"); + PublishHomeAssistantDiscoveryTopic( + "homeassistant/binary_sensor/%/remote_lock_status/config", + R"({"name":"Remote Lock Status","device_class":"lock","state_topic":"%/remote_lock_status","payload_on":"1","payload_off":"0","unique_id":"%_remote_lock_status","object_id":"remote_lock_status","device":{"identifiers":["%"],"name":"%","model":"EnergyControl","manufacturer":"Heidelberg"}})"); + +#ifdef ENABLE_LOCK_RELAY + PublishHomeAssistantDiscoveryTopic( + "homeassistant/lock/%/lock_control/config", + R"({"name":"Lock Control","state_topic":"%/lock_control/state","command_topic":"%/lock_control/command","payload_lock":"LOCK","payload_unlock":"UNLOCK","state_locked":"LOCKED","state_unlocked":"UNLOCKED","unique_id":"%_lock_control","object_id":"lock_control","device":{"identifiers":["%"],"name":"%","model":"EnergyControl","manufacturer":"Heidelberg"}})"); +#endif + PublishHomeAssistantDiscoveryTopic( "homeassistant/switch/%/enable_charging/config", R"({ @@ -174,6 +193,16 @@ namespace MQTTManager gMqttClient.publish(gMqttTopic.SetString("/temperature"), 0, false, String(gWallbox->GetTemperature()).c_str()); break; + case (MqttPublishedValues::RemoteLockStatus): + gMqttClient.publish(gMqttTopic.SetString("/remote_lock_status"), 0, false, gWallbox->IsRemoteUnlocked() ? "1" : "0"); + break; + +#ifdef ENABLE_LOCK_RELAY + case (MqttPublishedValues::LockControllerStatus): + gMqttClient.publish(gMqttTopic.SetString("/lock_control/state"), 0, true, LockController::Instance()->GetLockState() ? "UNLOCKED" : "LOCKED"); + break; +#endif + case (MqttPublishedValues::Internals): gMqttClient.publish(gMqttTopic.SetString("/internal/wifi_disconnects"), 0, false, String(gStatistics.NumWifiDisconnects).c_str()); gMqttClient.publish(gMqttTopic.SetString("/internal/mqtt_disconnects"), 0, false, String(gStatistics.NumMqttDisconnects).c_str()); @@ -205,6 +234,9 @@ namespace MQTTManager // Subscribe to control topics gMqttClient.subscribe(gMqttTopic.SetString("/control/charging_current_limit"), 2); gMqttClient.subscribe(gMqttTopic.SetString("/control/enable_charging"), 2); +#ifdef ENABLE_LOCK_RELAY + gMqttClient.subscribe(gMqttTopic.SetString("/lock_control/command"), 2); +#endif // Publish version information String versionString = String(Version::Major) + "." + String(Version::Minor) + "." + String(Version::Patch); @@ -212,6 +244,13 @@ namespace MQTTManager gMqttClient.publish(gMqttTopic.SetString("/build_date"), 0, true, __DATE__); gMqttClient.publish(gMqttTopic.SetString("/ip_address"), 0, true, WiFi.localIP().toString().c_str()); +#ifdef ENABLE_LOCK_RELAY + // Publish initial lock control state + const char* lockState = LockController::Instance()->GetLockState() ? "UNLOCKED" : "LOCKED"; + gMqttClient.publish(gMqttTopic.SetString("/lock_control/state"), 0, true, lockState); + Logger::Info("Published initial lock state: %s", lockState); +#endif + // Publish discovery data PublishHomeAssistantDiscovery(); } @@ -240,6 +279,21 @@ namespace MQTTManager bool enableCharging = cmd.equalsIgnoreCase("ON"); gWallbox->SetChargingEnabled(enableCharging); } +#ifdef ENABLE_LOCK_RELAY + else if (strcmp(gMqttTopic.SetString("/lock_control/command"), topic) == 0) + { + String cmd(payload, len); + cmd.trim(); + Logger::Trace("Received MQTT lock control command: %s", cmd.c_str()); + bool unlock = cmd.equalsIgnoreCase("UNLOCK"); + LockController::Instance()->SetLockState(unlock); + + // Immediately publish the new state + const char* newState = LockController::Instance()->GetLockState() ? "UNLOCKED" : "LOCKED"; + gMqttClient.publish(gMqttTopic.SetString("/lock_control/state"), 0, true, newState); + Logger::Info("Lock state updated and published: %s", newState); + } +#endif } // Callback for MQTT publish diff --git a/src/Components/Wallbox/DummyWallbox.cpp b/src/Components/Wallbox/DummyWallbox.cpp index 70d6f04..465973d 100644 --- a/src/Components/Wallbox/DummyWallbox.cpp +++ b/src/Components/Wallbox/DummyWallbox.cpp @@ -126,4 +126,10 @@ bool DummyWallbox::GetChargingVoltages(float &v1V, float &v2V, float &v3V) float DummyWallbox::GetTemperature() { return Constants::DummyWallbox::TemperatureDegCel; +} + +bool DummyWallbox::IsRemoteUnlocked() +{ + Logger::Debug("Dummy wallbox: returning remote lock status: unlocked"); + return true; // Dummy wallbox is always unlocked } \ No newline at end of file diff --git a/src/Components/Wallbox/DummyWallbox.h b/src/Components/Wallbox/DummyWallbox.h index 53840c7..6d8b04c 100644 --- a/src/Components/Wallbox/DummyWallbox.h +++ b/src/Components/Wallbox/DummyWallbox.h @@ -24,6 +24,7 @@ class DummyWallbox : public IWallbox virtual bool GetChargingCurrents(float &c1A, float &c2A, float &c3A) override; virtual bool GetChargingVoltages(float &v1V, float &v2V, float &v3V) override; virtual bool IsChargingEnabled() override; + virtual bool IsRemoteUnlocked() override; #pragma endregion IWallbox private: diff --git a/src/Components/Wallbox/HeidelbergWallbox.cpp b/src/Components/Wallbox/HeidelbergWallbox.cpp index 6bce7f4..6215331 100644 --- a/src/Components/Wallbox/HeidelbergWallbox.cpp +++ b/src/Components/Wallbox/HeidelbergWallbox.cpp @@ -252,4 +252,21 @@ float HeidelbergWallbox::GetTemperature() Logger::Error("Heidelberg wallbox: ERROR: Could not read PCB temperature"); return 0.0f; } +} + +bool HeidelbergWallbox::IsRemoteUnlocked() +{ + uint16_t registerValue[1]; + if (ModbusRTU::Instance()->ReadRegisters(Constants::HeidelbergRegisters::RemoteLockStatus, 1, 0x4, registerValue)) + { + bool isUnlocked = (registerValue[0] == 1); + Logger::Debug("Heidelberg wallbox: Read remote lock status: %s", isUnlocked ? "unlocked" : "locked"); + return isUnlocked; + } + else + { + // Error reading modbus register + Logger::Error("Heidelberg wallbox: ERROR: Could not read remote lock status"); + return true; // Default to unlocked as per spec + } } \ No newline at end of file diff --git a/src/Components/Wallbox/HeidelbergWallbox.h b/src/Components/Wallbox/HeidelbergWallbox.h index 039a05e..478a7d7 100644 --- a/src/Components/Wallbox/HeidelbergWallbox.h +++ b/src/Components/Wallbox/HeidelbergWallbox.h @@ -24,6 +24,7 @@ class HeidelbergWallbox : public IWallbox virtual bool GetChargingCurrents(float &c1A, float &c2A, float &c3A) override; virtual bool GetChargingVoltages(float &v1V, float &v2V, float &v3V) override; virtual bool IsChargingEnabled() override; + virtual bool IsRemoteUnlocked() override; #pragma endregion IWallbox private: diff --git a/src/Components/Wallbox/IWallbox.h b/src/Components/Wallbox/IWallbox.h index 6f4807f..3c31f13 100644 --- a/src/Components/Wallbox/IWallbox.h +++ b/src/Components/Wallbox/IWallbox.h @@ -45,6 +45,9 @@ class IWallbox // Returns if charging is currently enabled virtual bool IsChargingEnabled() = 0; + // Returns the remote lock status (true = unlocked, false = locked) + virtual bool IsRemoteUnlocked() = 0; + // Write functions // Sets the charging current limit in Amperes diff --git a/src/Configuration/Constants.h b/src/Configuration/Constants.h index 1ee402f..4fd4b3b 100644 --- a/src/Configuration/Constants.h +++ b/src/Configuration/Constants.h @@ -34,6 +34,7 @@ namespace Constants { constexpr uint16_t WatchdogTimeout = 257; constexpr uint16_t DisableStandby = 258; + constexpr uint16_t RemoteLockStatus = 259; constexpr uint16_t ChargingState = 5; constexpr uint16_t MaximalCurrent = 261; constexpr uint16_t Power = 14; diff --git a/src/Main.cpp b/src/Main.cpp index 76958b3..f2fad5c 100644 --- a/src/Main.cpp +++ b/src/Main.cpp @@ -11,6 +11,7 @@ #include "Components/Modbus/ModbusTCP.h" #include "Components/Wallbox/DummyWallbox.h" #include "Components/Wallbox/HeidelbergWallbox.h" +#include "Components/LockController/LockController.h" #include "Components/MQTT/MQTTManager.h" #include "Components/AsyncDelay/AsyncDelay.h" #include "Boards/Board.h" @@ -64,6 +65,11 @@ void setup() #endif gWallbox->Init(); +#ifdef ENABLE_LOCK_RELAY + // Initialize lock controller + LockController::Instance()->Init(); +#endif + // Start Modbus TCP server ModbusTCP::Init(gWallbox);