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
5 changes: 4 additions & 1 deletion .github/workflows/docker_feature.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ jobs:
- name: Convert repository owner to lowercase
run: echo "owner=$(echo '${{ github.repository_owner }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV

- name: Sanitize ref name for Docker tag
run: echo "clean_ref=$(echo '${{ github.ref_name }}' | sed 's/\//_/g')" >> $GITHUB_ENV

# 1. Setup QEMU for ARM64 emulation
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
Expand All @@ -69,5 +72,5 @@ jobs:
context: .
platforms: linux/amd64,linux/arm64
tags: |
ghcr.io/${{ env.owner }}/eos_connect:feature_dev_${{ github.ref_name }}
ghcr.io/${{ env.owner }}/eos_connect:feature_dev_${{ env.clean_ref }}
push: true
39 changes: 38 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ EOS Connect helps you get the most out of your solar and storage systems—wheth
- [OpenHAB](#openhab)
- [PV Forecast](#pv-forecast)
- [Energy Price Forecast](#energy-price-forecast)
- [Battery Price Analysis (Historical Cost)](#battery-price-analysis-historical-cost)
- [Webpage Example](#webpage-example)
- [Provided Data per **EOS connect** API](#provided-data-per-eos-connect-api)
- [Web API (REST/JSON)](#web-api-restjson)
Expand Down Expand Up @@ -83,6 +84,11 @@ EOS Connect helps you get the most out of your solar and storage systems—wheth
- EVCC-specific modes (e.g., fast charge, PV mode).
- **Dynamic Charging Curve**:
- If enabled, EOS Connect automatically adjusts the maximum battery charging power based on the current state of charge (SOC). This helps to optimize battery health and efficiency by reducing charge power as the battery approaches full capacity.
- **Dynamic Battery Price Calculation**:
- Analyzes charging history to determine the real cost of energy currently stored in the battery.
- Uses an **Inventory Valuation (LIFO)** model to ensure the price reflects the most recent charging sessions.
- Distinguishes between PV surplus and grid charging to provide an accurate cost basis.
- Helps the optimizer make better decisions about when to discharge the battery based on the actual cost of the stored energy.
- **Cost and Solar Optimization**:
- Aligns energy usage with real-time electricity prices (e.g., from Tibber, [smartenergy.at](https://www.smartenergy.at/), or [Stromligning.dk](https://stromligning.dk/)) to minimize costs.
- Incorporates PV forecasts to prioritize charging during periods of high solar output.
Expand Down Expand Up @@ -306,6 +312,13 @@ EOS Connect supports multiple sources for solar (PV) production forecasts. You c
#### Energy Price Forecast
Energy price forecasts are retrieved from the chosen source (e.g. tibber, Akkudoktor, Smartenergy, ...). **Note**: Prices for tomorrow are available earliest at 1 PM. Until then, today's prices are used to feed the model.

#### Battery Price Analysis (Inventory Valuation)
EOS Connect calculates the actual cost of the energy currently stored in your battery by analyzing your recent charging history. Instead of a simple average, it uses an **Inventory Valuation (LIFO)** model:
- **Smart Tracking**: It automatically identifies if energy came from your solar panels (0€) or the grid.
- **Inventory Focus**: It calculates the price based on the most recent charging sessions that match your current battery level. This means the price reflects the "value" of the energy actually inside the battery.
- **Live Pricing**: For grid charging, it uses the exact electricity price at that time.
- **Visual Feedback**: The dashboard highlights which charging sessions are currently "in inventory" and which are historical.

> **Note:**
> All data collection, forecasting, and optimization cycles are now driven by the `time_frame` setting in your configuration.
> For more precise and responsive optimization, set `time_frame: 900` for a 15-minute cycle.
Expand All @@ -316,6 +329,10 @@ Energy price forecasts are retrieved from the chosen source (e.g. tibber, Akkudo

The dashbaord of **EOS connect** is available at `http://localhost:8081`.

- **Main Dashboard**: Real-time overview of PV, Load, Battery, and Optimization states.
- **Battery Overview**: Detailed analysis of battery costs, charging sessions, and PV/Grid ratios. Accessible via the main menu or by clicking the battery SOC/Capacity icons.
- **Log Viewer**: Real-time application logs with component-based filtering (e.g., `BATTERY-PRICE`, `OPTIMIZER`).

![webpage screenshot](doc/screenshot_0_1_20.png)

## Provided Data per **EOS connect** API
Expand Down Expand Up @@ -397,7 +414,27 @@ Get current system control states and battery information.
"soc": 23.8,
"usable_capacity": 3867.11,
"max_charge_power_dyn": 10000,
"max_grid_charge_rate": 10000
"max_grid_charge_rate": 10000,
"stored_energy": {
"stored_energy_price": 0.000215,
"duration_of_analysis": 96,
"charged_energy": 12450.5,
"charged_from_pv": 8500.0,
"charged_from_grid": 3950.5,
"ratio": 68.3,
"charging_sessions": [
{
"start_time": "2025-12-21T11:58:06+00:00",
"end_time": "2025-12-21T14:03:17+00:00",
"charged_energy": 772.9,
"charged_from_pv": 772.3,
"charged_from_grid": 0.6,
"ratio": 99.9,
"cost": 0.0002
}
],
"last_update": "2025-12-22T10:15:00Z"
}
},
"inverter": {
"inverter_special_data": {
Expand Down
65 changes: 58 additions & 7 deletions src/CONFIG_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- [EOS Server Configuration](#eos-server-configuration)
- [Electricity Price Configuration](#electricity-price-configuration)
- [Battery Configuration](#battery-configuration)
- [Dynamic Battery Price Calculation](#dynamic-battery-price-calculation)
- [PV Forecast Configuration](#pv-forecast-configuration)
- [PV Forecast Source](#pv-forecast-source)
- [PV Forecast installations](#pv-forecast-installations)
Expand Down Expand Up @@ -197,19 +198,69 @@ A default config file will be created with the first start, if there is no `conf
- **`battery.max_soc_percentage`**:
Maximum state of charge for the battery, as a percentage.

- **`battery.price_euro_per_wh_accu`**:
Price for battery in €/Wh - can be used to shift the result over the day according to the available energy (more details follow).


- **`battery.price_euro_per_wh_sensor`**:
Sensor/item identifier that exposes the battery price in €/Wh. If `battery.source` is set to `homeassistant` or `openhab` and a sensor/item is configured here, the system will fetch the value from that sensor/item. If no sensor is configured, the static value at `price_euro_per_wh_accu` will be used. For Home Assistant use an entity ID (e.g., `sensor.battery_price`); for OpenHAB use an item name (e.g., `BatteryPrice`).

- **`battery.charging_curve_enabled`**:
Enables or disables the dynamic charging curve for the battery.
- `true`: The system will automatically reduce the maximum charging power as the battery SOC increases, helping to protect battery health and optimize efficiency.
- `false`: The battery will always charge at the configured maximum power, regardless of SOC.
- **Default:** `true`

- **`battery.price_euro_per_wh_accu`**:
Price for battery in €/Wh - can be used to shift the result over the day according to the available energy (more details follow).


- **`battery.price_euro_per_wh_sensor`**:
Sensor/item identifier that exposes the battery price in €/Wh. If `battery.source` is set to `homeassistant` or `openhab` and a sensor/item is configured here, the system will fetch the value from that sensor/item. If no sensor is configured, the static value at `price_euro_per_wh_accu` will be used. For Home Assistant use an entity ID (e.g., `sensor.battery_price`); for OpenHAB use an item name (e.g., `BatteryPrice`).

- **`battery.price_calculation_enabled`**:
Enables dynamic battery price calculation by analyzing historical charging events.
- `true`: The system will analyze the last 96 hours (configurable) of power data to determine the real cost of energy stored in the battery.
- `false`: Uses the static price or sensor value.
- **Default:** `false`

- **`battery.price_update_interval`**:
Interval in seconds between dynamic price recalculations.
- **Default:** `900` (15 minutes)

- **`battery.price_history_lookback_hours`**:
Number of hours of history to analyze for the price calculation.
- **Default:** `96`

- **`battery.battery_power_sensor`**:
Home Assistant entity ID or OpenHAB item name for the battery power sensor (in Watts). Positive values must represent charging.

- **`battery.pv_power_sensor`**:
Home Assistant entity ID or OpenHAB item name for the total PV power sensor (in Watts).

- **`battery.grid_power_sensor`**:
Home Assistant entity ID or OpenHAB item name for the grid power sensor (in Watts). Positive values represent import from the grid.

- **`battery.load_power_sensor`**:
Home Assistant entity ID or OpenHAB item name for the household load power sensor (in Watts).

- **`battery.price_sensor`**:
Home Assistant entity ID or OpenHAB item name for the current electricity price sensor (in €/kWh or ct/kWh).

- **`battery.charging_threshold_w`**:
Minimum battery power in Watts to consider the battery as "charging" during historical analysis.
- **Default:** `50.0`

- **`battery.grid_charge_threshold_w`**:
Minimum grid import power in Watts to attribute charging energy to the grid rather than PV surplus.
- **Default:** `100.0`



#### Dynamic Battery Price Calculation

When `price_calculation_enabled` is set to `true`, the system performs a detailed analysis of your battery's charging history to determine the real cost of the energy currently stored.

**How it works:**
1. **Event Detection:** The system scans historical data (default 48h) to identify "charging events" where the battery power was above the `charging_threshold_w`.
2. **Source Attribution:** For each event, it compares battery power with PV production and Grid import. If grid import is significant (above `grid_charge_threshold_w`), the energy is attributed to the grid at the current market price. Otherwise, it is attributed to PV surplus at the `feed_in_price` (opportunity cost).
3. **Inventory Valuation (LIFO):** Instead of a simple average, the system uses a **Last-In, First-Out** model. It looks at the most recent charging sessions that match your current battery level. This ensures the price reflects the actual "value" of the energy currently inside the battery.
4. **Optimizer Integration:** This resulting price is used by the optimizer to decide when it is profitable to discharge the battery.
5. **Efficiency:** To minimize API load, the system uses a two-step fetching strategy: it first fetches low-resolution data to find events, and then high-resolution data only for the specific periods when the battery was actually charging.

---

### PV Forecast Configuration
Expand Down
49 changes: 48 additions & 1 deletion src/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,19 @@ def create_default_config(self):
"max_charge_power_w": 5000,
"min_soc_percentage": 5,
"max_soc_percentage": 100,
"charging_curve_enabled": True, # enable charging curve
"price_euro_per_wh_accu": 0.0, # price for battery in euro/Wh
"price_euro_per_wh_sensor": "", # sensor/item providing battery energy cost in €/Wh
"charging_curve_enabled": True, # enable charging curve
"price_calculation_enabled": False,
"price_update_interval": 900,
"price_history_lookback_hours": 96,
"battery_power_sensor": "",
"pv_power_sensor": "",
"grid_power_sensor": "",
"load_power_sensor": "",
"price_sensor": "",
"charging_threshold_w": 50.0,
"grid_charge_threshold_w": 100.0,
}
),
"pv_forecast_source": CommentedMap(
Expand Down Expand Up @@ -291,6 +301,43 @@ def create_default_config(self):
+ " according to the SOC (default: true)",
"charging_curve_enabled",
)
config["battery"].yaml_add_eol_comment(
"enable dynamic battery price calculation based on history",
"price_calculation_enabled",
)
config["battery"].yaml_add_eol_comment(
"interval for price update in seconds - default: 900 (15 min)",
"price_update_interval",
)
config["battery"].yaml_add_eol_comment(
"hours of history to analyze for price calculation - default: 96",
"price_history_lookback_hours",
)
config["battery"].yaml_add_eol_comment(
"HA entity ID or OpenHAB item for battery power in W (positive = charging)",
"battery_power_sensor",
)
config["battery"].yaml_add_eol_comment(
"HA entity ID or OpenHAB item for PV power in W", "pv_power_sensor"
)
config["battery"].yaml_add_eol_comment(
"HA entity ID or OpenHAB item for grid power in W (positive = import)",
"grid_power_sensor",
)
config["battery"].yaml_add_eol_comment(
"HA entity ID or OpenHAB item for load power in W", "load_power_sensor"
)
config["battery"].yaml_add_eol_comment(
"HA entity ID or OpenHAB item for electricity price in €/kWh or ct/kWh",
"price_sensor",
)
config["battery"].yaml_add_eol_comment(
"minimum battery power to consider as charging (W)", "charging_threshold_w"
)
config["battery"].yaml_add_eol_comment(
"minimum grid surplus to consider as grid charging (W)",
"grid_charge_threshold_w",
)

# pv forecast source configuration
config.yaml_set_comment_before_after_key(
Expand Down
3 changes: 3 additions & 0 deletions src/eos_connect.py
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,8 @@ def mqtt_control_callback(mqtt_cmd):
battery_interface = BatteryInterface(
config_manager.config["battery"],
on_bat_max_changed=None,
load_interface=load_interface,
timezone=time_zone,
)

price_interface = PriceInterface(
Expand Down Expand Up @@ -1496,6 +1498,7 @@ def get_controls():
"max_grid_charge_rate": config_manager.config["inverter"][
"max_grid_charge_rate"
],
"stored_energy": battery_interface.get_stored_energy_info(),
},
"inverter": {
"inverter_special_data": (
Expand Down
31 changes: 30 additions & 1 deletion src/interfaces/battery_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import threading
import time
import requests
from .battery_price_handler import BatteryPriceHandler

logger = logging.getLogger("__main__")
logger.info("[BATTERY-IF] loading module ")
Expand All @@ -63,7 +64,9 @@ class BatteryInterface:
Fetches the current SOC of the battery based on the configured source.
"""

def __init__(self, config, on_bat_max_changed=None):
def __init__(
self, config, on_bat_max_changed=None, load_interface=None, timezone=None
):
self.src = config.get("source", "default")
self.url = config.get("url", "")
self.soc_sensor = config.get("soc_sensor", "")
Expand All @@ -82,6 +85,11 @@ def __init__(self, config, on_bat_max_changed=None):

self.soc_fail_count = 0

# Initialize dynamic price handler
self.price_handler = BatteryPriceHandler(
config, load_interface=load_interface, timezone=timezone
)

self.update_interval = 30
self._update_thread = None
self._stop_event = threading.Event()
Expand Down Expand Up @@ -189,6 +197,18 @@ def __update_price_euro_per_wh(self):
"""
Update the battery price from the configured source if needed.
"""
# If dynamic price calculation is enabled, use the handler
if self.price_handler and self.price_handler.price_calculation_enabled:
if self.price_handler.update_price_if_needed(
inventory_wh=self.current_usable_capacity
):
self.price_euro_per_wh = self.price_handler.get_current_price()
logger.info(
"[BATTERY-IF] Dynamic battery price updated: %.4f €/kWh",
self.price_euro_per_wh * 1000,
)
return self.price_euro_per_wh

# If top-level source is default, keep configured static price
if self.src == "default":
return self.price_euro_per_wh
Expand Down Expand Up @@ -285,6 +305,15 @@ def get_price_euro_per_wh(self):
"""
return self.price_euro_per_wh

def get_stored_energy_info(self):
"""
Returns detailed information about the stored energy cost analysis.
"""
results = self.price_handler.get_analysis_results().copy()
results["enabled"] = self.price_handler.price_calculation_enabled
results["price_source"] = "sensor" if self.price_sensor else "fixed"
return results

def set_min_soc(self, min_soc):
"""
Sets the minimum state of charge (SOC) percentage of the battery.
Expand Down
Loading