270 lines
9.9 KiB
Markdown
270 lines
9.9 KiB
Markdown
# CLAUDE.md
|
||
|
||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||
|
||
## Project Overview
|
||
|
||
Intelligent battery charging optimizer for Home Assistant integrated with OpenEMS and GoodWe hardware. The system optimizes battery charging based on dynamic electricity pricing from haStrom FLEX PRO tariff and solar forecasts, automatically scheduling charging during the cheapest price periods.
|
||
|
||
**Hardware**: 10 kWh GoodWe battery, 10 kW inverter, 9.2 kWp PV (east-west orientation)
|
||
**Control**: BeagleBone running OpenEMS, controlled via Modbus TCP and JSON-RPC
|
||
**Home Assistant**: PyScript-based optimization running on /config/pyscript/
|
||
|
||
## Repository Structure
|
||
|
||
This repository contains **versioned iterations** of the battery optimization system:
|
||
|
||
```
|
||
/
|
||
├── v1/ # Initial implementation (threshold-based)
|
||
├── v2/ # Improved version
|
||
├── v3/ # Latest version (ranking-based optimization, sections dashboards)
|
||
├── battery_charging_optimizer.py # Current production PyScript (v3.1.0)
|
||
├── hastrom_flex_extended.py # Tomorrow-aware price fetcher
|
||
├── ess_set_power.py # Modbus FLOAT32 power control
|
||
├── EMS_OpenEMS_HomeAssistant_Dokumentation.md # Comprehensive technical docs
|
||
└── project_memory.md # AI assistant context memory
|
||
```
|
||
|
||
**Important**: The root-level `.py` files are the **current production versions**. Version folders contain historical snapshots and documentation from development iterations.
|
||
|
||
## Core Architecture
|
||
|
||
### Control Flow
|
||
|
||
```
|
||
14:05 daily → Fetch prices → Optimize schedule → Store in pyscript state
|
||
↓
|
||
xx:05 hourly → Read schedule → Check current hour → Execute action
|
||
↓
|
||
If charging → Enable manual mode → Set power via Modbus → Trigger automation
|
||
If auto → Disable manual mode → Let OpenEMS manage battery
|
||
```
|
||
|
||
### Critical Components
|
||
|
||
**1. Battery Charging Optimizer** (`battery_charging_optimizer.py`)
|
||
- Ranking-based optimization: selects N cheapest hours from combined today+tomorrow data
|
||
- Runs daily at 14:05 (after price publication) and hourly at xx:05
|
||
- Stores schedule in `pyscript.battery_charging_schedule` state with attributes
|
||
- Conservative strategy: 20-100% SOC range, 2 kWh reserve for self-consumption
|
||
|
||
**2. Price Fetcher** (`hastrom_flex_extended.py`)
|
||
- Fetches haStrom FLEX PRO prices with tomorrow support
|
||
- Creates sensors: `sensor.hastrom_flex_pro_ext` and `sensor.hastrom_flex_ext`
|
||
- **Critical**: Field name is `t_price_has_pro_incl_vat` (not standard field name)
|
||
- Updates hourly, with special triggers at 14:05 and midnight
|
||
|
||
**3. Modbus Power Control** (`ess_set_power.py`)
|
||
- Controls battery via Modbus register 706 (SetActivePowerEquals)
|
||
- **Critical**: Uses IEEE 754 FLOAT32 Big-Endian encoding
|
||
- Negative values = charging, positive = discharging
|
||
|
||
### OpenEMS Integration Details
|
||
|
||
**Controller Priority System**:
|
||
- Controllers execute in **alphabetical order**
|
||
- Later controllers can override earlier ones
|
||
- Use `ctrlBalancing0` with `SET_GRID_ACTIVE_POWER` for highest priority
|
||
- Direct ESS register writes can be overridden by subsequent controllers
|
||
|
||
**ESS Modes**:
|
||
- `REMOTE`: External Modbus control active
|
||
- `INTERNAL`: OpenEMS manages battery
|
||
- Mode switching via JSON-RPC API on port 8074
|
||
|
||
**Modbus Communication**:
|
||
- IP: 192.168.89.144, Port: 502
|
||
- Register pairs use 2 consecutive registers for FLOAT32 values
|
||
- Example: Register 2752/2753 for SET_GRID_ACTIVE_POWER
|
||
|
||
### PyScript-Specific Considerations
|
||
|
||
**Limitations**:
|
||
- Generator expressions with `selectattr()` not supported
|
||
- Use explicit `for` loops instead of complex comprehensions
|
||
- State values limited to 255 characters; use attributes for complex data
|
||
|
||
**Timezone Handling**:
|
||
- PyScript `datetime.now()` returns UTC
|
||
- Home Assistant stores times in local (Europe/Berlin)
|
||
- Always use `datetime.now().astimezone()` for local time
|
||
- Explicit timezone conversion required when comparing PyScript times with HA states
|
||
|
||
**State Management**:
|
||
```python
|
||
# Store complex data in attributes, not state value
|
||
state.set('pyscript.battery_charging_schedule',
|
||
value='active', # Simple status
|
||
new_attributes={'schedule': [...]} # Complex data here
|
||
)
|
||
```
|
||
|
||
## Development Commands
|
||
|
||
### Testing PyScript Changes
|
||
|
||
```bash
|
||
# In Home Assistant Developer Tools → Services:
|
||
service: pyscript.reload
|
||
```
|
||
|
||
### Manual Schedule Calculation
|
||
|
||
```bash
|
||
# In Home Assistant Developer Tools → Services:
|
||
service: pyscript.calculate_charging_schedule
|
||
```
|
||
|
||
### Manual Execution Test
|
||
|
||
```bash
|
||
# In Home Assistant Developer Tools → Services:
|
||
service: pyscript.execute_charging_schedule
|
||
```
|
||
|
||
### Check Schedule State
|
||
|
||
```bash
|
||
# In Home Assistant Developer Tools → States, search for:
|
||
pyscript.battery_charging_schedule
|
||
```
|
||
|
||
### OpenEMS Logs (on BeagleBone)
|
||
|
||
```bash
|
||
tail -f /var/log/openems/openems.log
|
||
```
|
||
|
||
## Key Integration Points
|
||
|
||
### Required Home Assistant Entities
|
||
|
||
**Input Booleans**:
|
||
- `input_boolean.battery_optimizer_enabled` - Master enable/disable
|
||
- `input_boolean.goodwe_manual_control` - Manual vs auto mode
|
||
- `input_boolean.battery_optimizer_manual_override` - Skip automation
|
||
|
||
**Input Numbers**:
|
||
- `input_number.battery_capacity_kwh` - Battery capacity (10 kWh)
|
||
- `input_number.battery_optimizer_min_soc` - Minimum SOC (20%)
|
||
- `input_number.battery_optimizer_max_soc` - Maximum SOC (100%)
|
||
- `input_number.battery_optimizer_max_charge_power` - Max charge power (5000W)
|
||
- `input_number.charge_power_battery` - Target charging power
|
||
|
||
**Sensors**:
|
||
- `sensor.esssoc` - Current battery SOC
|
||
- `sensor.openems_ess0_activepower` - Battery power
|
||
- `sensor.openems_grid_activepower` - Grid power
|
||
- `sensor.openems_production_activepower` - PV production
|
||
- `sensor.energy_production_today` / `sensor.energy_production_today_2` - PV forecast (east/west)
|
||
- `sensor.energy_production_tomorrow` / `sensor.energy_production_tomorrow_2` - PV tomorrow
|
||
|
||
### Existing Automations
|
||
|
||
Three manual control automations exist (in Home Assistant automations.yaml):
|
||
- Battery charge start (ID: `1730457901370`)
|
||
- Battery charge stop (ID: `1730457994517`)
|
||
- Battery discharge
|
||
|
||
**Important**: These automations are **used by** the optimizer, not replaced. The PyScript sets input helpers that trigger these automations.
|
||
|
||
## Dashboard Variants
|
||
|
||
Multiple dashboard configurations exist in `v3/`:
|
||
|
||
- **Standard** (`battery_optimizer_dashboard.yaml`): Detailed view with all metrics
|
||
- **Compact** (`battery_optimizer_dashboard_compact.yaml`): Balanced mobile-friendly view
|
||
- **Minimal** (`battery_optimizer_dashboard_minimal.yaml`): Quick status check
|
||
- **Sections variants**: Modern HA 2024.2+ layouts with auto-responsive behavior
|
||
|
||
All use maximum 4-column layouts for mobile compatibility.
|
||
|
||
**Required HACS Custom Cards**:
|
||
- Mushroom Cards
|
||
- Bubble Card
|
||
- Plotly Graph Card
|
||
- Power Flow Card Plus
|
||
- Stack-in-Card
|
||
|
||
## Common Troubleshooting
|
||
|
||
### Battery Not Charging Despite Schedule
|
||
|
||
**Symptom**: Schedule shows charging hour but battery stays idle
|
||
**Causes**:
|
||
1. Controller priority issue - another controller overriding
|
||
2. Manual override active (`input_boolean.battery_optimizer_manual_override == on`)
|
||
3. Optimizer disabled (`input_boolean.battery_optimizer_enabled == off`)
|
||
|
||
**Solution**: Check OpenEMS logs for controller execution order, verify input boolean states
|
||
|
||
### Wrong Charging Time (Off by Hours)
|
||
|
||
**Symptom**: Charging starts at wrong hour
|
||
**Cause**: UTC/local timezone mismatch in PyScript
|
||
**Solution**: Verify all datetime operations use `.astimezone()` for local time
|
||
|
||
### No Tomorrow Prices in Schedule
|
||
|
||
**Symptom**: Schedule only covers today
|
||
**Cause**: Tomorrow prices not yet available (published at 14:00)
|
||
**Solution**: Normal before 14:00; if persists after 14:05, check `sensor.hastrom_flex_pro_ext` attributes for `tomorrow_available`
|
||
|
||
### Modbus Write Failures
|
||
|
||
**Symptom**: Modbus errors in logs when setting power
|
||
**Cause**: Incorrect FLOAT32 encoding or wrong byte order
|
||
**Solution**: Verify Big-Endian format in `ess_set_power.py`, check OpenEMS Modbus configuration
|
||
|
||
## Data Sources
|
||
|
||
**haStrom FLEX PRO API**:
|
||
- Endpoint: `http://eex.stwhas.de/api/spotprices/flexpro?start_date=YYYYMMDD&end_date=YYYYMMDD`
|
||
- Price field: `t_price_has_pro_incl_vat` (specific to FLEX PRO tariff)
|
||
- Supports date range queries for multi-day optimization
|
||
|
||
**Forecast.Solar**:
|
||
- Two arrays configured: East (90°) and West (270°) on flat roof
|
||
- Daily totals available, hourly breakdown simplified
|
||
|
||
**InfluxDB2**:
|
||
- Long-term storage for historical analysis
|
||
- Configuration in Home Assistant `configuration.yaml`
|
||
|
||
## File Locations in Production
|
||
|
||
When this code runs in production Home Assistant:
|
||
|
||
```
|
||
/config/
|
||
├── pyscripts/
|
||
│ ├── battery_charging_optimizer.py # Main optimizer
|
||
│ ├── hastrom_flex_extended.py # Price fetcher
|
||
│ └── ess_set_power.py # Modbus control
|
||
├── automations.yaml # Contains battery control automations
|
||
├── configuration.yaml # Modbus, InfluxDB configs
|
||
└── dashboards/
|
||
└── battery_optimizer.yaml # Dashboard config
|
||
```
|
||
|
||
## Algorithm Overview
|
||
|
||
**Ranking-Based Optimization** (v3.1.0):
|
||
1. Calculate needed charging hours: `(target_SOC - current_SOC) × capacity ÷ charge_power`
|
||
2. Combine today + tomorrow price data into single dataset
|
||
3. Score each hour: `price - (pv_forecast_wh / 1000)`
|
||
4. Sort by score (lowest = best)
|
||
5. Select top N hours where N = needed charging hours
|
||
6. Execute chronologically
|
||
|
||
**Key Insight**: This approach finds globally optimal charging times across midnight boundaries, unlike threshold-based methods that treat days separately.
|
||
|
||
## Version History Context
|
||
|
||
- **v1**: Threshold-based optimization, single-day planning
|
||
- **v2**: Enhanced with better dashboard, improved error handling
|
||
- **v3**: Ranking-based optimization, tomorrow support, modern sections dashboards
|
||
|
||
Each version folder contains complete snapshots including installation guides and checklists for that iteration.
|