[EDIT 8/08/2025: Nowsza wersja w poniżej tutaj: https://www.elektroda.pl/rtvforum/topic4132861.html#21629542]
Witam wszystkich.
Dzisiaj mam dla was skrypt w pythonie do optymalizacji akumulacji i sprzedaży energii elektrycznej z instalacji fotowoltaicznej. Skrypt służy do wyznaczania optymalnej strategii wykorzystania energii elektrycznej produkowanej przez instalację PV, z uwzględnieniem:
- prognozowanej produkcji energii z PV,
- prognozowanego zużycia energii w gospodarstwie domowym,
- zmiennych cen sprzedaży energii do sieci,
- stałej ceny zakupu energii z sieci,
- parametrów technicznych akumulatora (pojemność, maksymalna moc ładowania i rozładowania, sprawność).
Skrypt bazuje na modelu matematycznym, opartym na programowaniu liniowym, maksymalizuje zysk z energii sprzedanej do sieci i minimalizuje koszt zakupu z sieci, uwzględniając wartość energii pozostałej w akumulatorze na koniec okresu prognozy. Dzięki temu możliwe jest inteligentne zarządzanie energią – ładowanie i rozładowywanie akumulatora oraz sprzedaż i zakup z sieci – w sposób maksymalizujący opłacalność ekonomiczną oraz zmniejszające zakłócenia w pracy lokalnych stacji energetycznych.
W mojej instalacji skrypt wyzwalany jest przez HomeAssistant co godzinę i zwraca z powrotem docelowe parametry pracy falownika (tryb pracy falownika jest sterowany przez osobne automaty w HomeAssistant które obserwują stan pomocników (helpers), które zapisywane są przez skrypt. W moje instalacji wykorzystuję następujące integracje dla pozyskania potrzebnych danych:
- SOLARMAN dla odczytu i ustawiania parametrów pracy falownika
- SOLCAST dla prognozy produkcji PV
- TGE Piotra Machowskiego dla odczytu cen sprzedaży z rynku dnia następnego
Skrypt uruchamiany jest jako shell_command gdyż ograniczenia integracji python_script uniemożliwiają pobieranie danych historycznych. Dlatego wykorzystuję zapytania API do HomeAssistanta (listing ha_client.py poniżej oraz trzeba ustawić adres IP i token dostępu w sekcji CONFIGURATION). Skrypt wyznacza prognozę zużycia energii domu na podstawie średniego zużycia z ostatnich 3 dni (dla każdej godziny). W mojej instalacji zasilanie domu mam podzielone na dwie sekcje - jedna zasilana jest z wyjścia UPS, druga z wyjścia powrotnego GRID w falowniku. Dlatego używam dwóch encji 'ENTITY1' oraz 'ENTITY2' do sumowania łącznego zużycia energii. Skrypt używa bibliotek pulp oraz pytz, które trzeba zainstalować pip'em.
Kilka słów o modelu optymalizacji - model wyznacza strategię minimalizacji kosztów energii na następne 24 godziny. Trudność polega na poprawnym rozwiązaniu kwestii ostatniej godziny. Model może np. pozbyć się całego zapasu energii jeśli nie włączymy do formuły celu wartości energii zgromadzonej w banku na koniec modelowanego okresu. Tutaj kluczowe znaczenie ma wyznaczenie jej wartości, jednak chyba nie ma jednego poprawnego rozwiązania. Jeśli wyznaczę wartość wg. kosztu alternatywnego (koszt zakupu), wtedy model będzie zawsze dążył do zostawienia pełnego banku na koniec okresu. Jeśłi wyznaczę np. średnej ceny sprzedaży w prognozowanym okresie, model może zakończyć okres z pustym bankiem. Ponieważ skrypt wykonywany jest co godzinę i parametry falownika ustawiane są na podstawie pierwszej godziny w modelu, dlatego jeszcze nie wiem jakie znaczenie na długoterminową pracę będzie miało wyznaczenie wartości rezydualnej energii w banku.
Skrypt można również uruchomić samodzielnie bezpośrednio z konsoli, wtedy pokazuje całą tablicę wyliczonej optymalizacji:
Za sterowanie falownika odpowiadają dwa ostatnie parametry przedstawione w tabeli, które służą do ustawienia falownika w tryb sprzedaży z baterii, lub ładowania baterii. Ten ostatni parametr ustawiony na '0' będzie powodował sprzedaż produkcji z PV, np. przed ładowaniem w godzinach porannych.
Tutaj jest kod:
ha_client.py - biblioteka odczytu i zapisu danych z HomeAssistant:
Inverter_pulp_hourly_script.py - właściwy skrypt.
Witam wszystkich.
Dzisiaj mam dla was skrypt w pythonie do optymalizacji akumulacji i sprzedaży energii elektrycznej z instalacji fotowoltaicznej. Skrypt służy do wyznaczania optymalnej strategii wykorzystania energii elektrycznej produkowanej przez instalację PV, z uwzględnieniem:
- prognozowanej produkcji energii z PV,
- prognozowanego zużycia energii w gospodarstwie domowym,
- zmiennych cen sprzedaży energii do sieci,
- stałej ceny zakupu energii z sieci,
- parametrów technicznych akumulatora (pojemność, maksymalna moc ładowania i rozładowania, sprawność).
Skrypt bazuje na modelu matematycznym, opartym na programowaniu liniowym, maksymalizuje zysk z energii sprzedanej do sieci i minimalizuje koszt zakupu z sieci, uwzględniając wartość energii pozostałej w akumulatorze na koniec okresu prognozy. Dzięki temu możliwe jest inteligentne zarządzanie energią – ładowanie i rozładowywanie akumulatora oraz sprzedaż i zakup z sieci – w sposób maksymalizujący opłacalność ekonomiczną oraz zmniejszające zakłócenia w pracy lokalnych stacji energetycznych.
W mojej instalacji skrypt wyzwalany jest przez HomeAssistant co godzinę i zwraca z powrotem docelowe parametry pracy falownika (tryb pracy falownika jest sterowany przez osobne automaty w HomeAssistant które obserwują stan pomocników (helpers), które zapisywane są przez skrypt. W moje instalacji wykorzystuję następujące integracje dla pozyskania potrzebnych danych:
- SOLARMAN dla odczytu i ustawiania parametrów pracy falownika
- SOLCAST dla prognozy produkcji PV
- TGE Piotra Machowskiego dla odczytu cen sprzedaży z rynku dnia następnego
Skrypt uruchamiany jest jako shell_command gdyż ograniczenia integracji python_script uniemożliwiają pobieranie danych historycznych. Dlatego wykorzystuję zapytania API do HomeAssistanta (listing ha_client.py poniżej oraz trzeba ustawić adres IP i token dostępu w sekcji CONFIGURATION). Skrypt wyznacza prognozę zużycia energii domu na podstawie średniego zużycia z ostatnich 3 dni (dla każdej godziny). W mojej instalacji zasilanie domu mam podzielone na dwie sekcje - jedna zasilana jest z wyjścia UPS, druga z wyjścia powrotnego GRID w falowniku. Dlatego używam dwóch encji 'ENTITY1' oraz 'ENTITY2' do sumowania łącznego zużycia energii. Skrypt używa bibliotek pulp oraz pytz, które trzeba zainstalować pip'em.
Kilka słów o modelu optymalizacji - model wyznacza strategię minimalizacji kosztów energii na następne 24 godziny. Trudność polega na poprawnym rozwiązaniu kwestii ostatniej godziny. Model może np. pozbyć się całego zapasu energii jeśli nie włączymy do formuły celu wartości energii zgromadzonej w banku na koniec modelowanego okresu. Tutaj kluczowe znaczenie ma wyznaczenie jej wartości, jednak chyba nie ma jednego poprawnego rozwiązania. Jeśli wyznaczę wartość wg. kosztu alternatywnego (koszt zakupu), wtedy model będzie zawsze dążył do zostawienia pełnego banku na koniec okresu. Jeśłi wyznaczę np. średnej ceny sprzedaży w prognozowanym okresie, model może zakończyć okres z pustym bankiem. Ponieważ skrypt wykonywany jest co godzinę i parametry falownika ustawiane są na podstawie pierwszej godziny w modelu, dlatego jeszcze nie wiem jakie znaczenie na długoterminową pracę będzie miało wyznaczenie wartości rezydualnej energii w banku.
Skrypt można również uruchomić samodzielnie bezpośrednio z konsoli, wtedy pokazuje całą tablicę wyliczonej optymalizacji:
Status: Optimal
Total profit: 12.96 €
T00 | Buy: 0.00 | Sell: 0.00 | Charge: 0.00 | Discharge to Grid: 0.00 | Discharge to Load: 0.55 | SOC: 6.31 | Is charging: 0.00 | Is selling battery: 0.00
T01 | Buy: 0.53 | Sell: 0.00 | Charge: 0.00 | Discharge to Grid: 0.00 | Discharge to Load: 0.00 | SOC: 6.31 | Is charging: 0.00 | Is selling battery: 0.00
T02 | Buy: 0.53 | Sell: 0.00 | Charge: 0.00 | Discharge to Grid: 0.00 | Discharge to Load: 0.00 | SOC: 6.31 | Is charging: 0.00 | Is selling battery: 0.00
T03 | Buy: 0.67 | Sell: 0.00 | Charge: 0.00 | Discharge to Grid: 0.00 | Discharge to Load: 0.00 | SOC: 6.31 | Is charging: 0.00 | Is selling battery: 0.00
T04 | Buy: 1.10 | Sell: 0.00 | Charge: 0.00 | Discharge to Grid: 0.00 | Discharge to Load: 0.00 | SOC: 6.31 | Is charging: 0.00 | Is selling battery: 0.00
T05 | Buy: 0.43 | Sell: 0.00 | Charge: 0.00 | Discharge to Grid: 0.00 | Discharge to Load: 0.05 | SOC: 6.26 | Is charging: 0.00 | Is selling battery: 0.00
T06 | Buy: 0.00 | Sell: 0.00 | Charge: 0.00 | Discharge to Grid: 0.00 | Discharge to Load: 0.52 | SOC: 5.68 | Is charging: 0.00 | Is selling battery: 0.00
T07 | Buy: 0.00 | Sell: 0.00 | Charge: 0.00 | Discharge to Grid: 0.00 | Discharge to Load: 0.44 | SOC: 5.19 | Is charging: 0.00 | Is selling battery: 0.00
T08 | Buy: 0.34 | Sell: 0.00 | Charge: 0.00 | Discharge to Grid: 0.00 | Discharge to Load: 0.00 | SOC: 5.19 | Is charging: 0.00 | Is selling battery: 0.00
T09 | Buy: 0.08 | Sell: 0.00 | Charge: 0.00 | Discharge to Grid: 0.00 | Discharge to Load: 0.00 | SOC: 5.19 | Is charging: 0.00 | Is selling battery: 0.00
T10 | Buy: 0.00 | Sell: 0.11 | Charge: 0.00 | Discharge to Grid: 0.00 | Discharge to Load: 0.00 | SOC: 5.19 | Is charging: 0.00 | Is selling battery: 0.00
T11 | Buy: 0.00 | Sell: 0.53 | Charge: 0.00 | Discharge to Grid: 0.00 | Discharge to Load: 0.00 | SOC: 5.19 | Is charging: 0.00 | Is selling battery: 0.00
T12 | Buy: 0.00 | Sell: 2.54 | Charge: 0.00 | Discharge to Grid: 0.00 | Discharge to Load: 0.00 | SOC: 5.19 | Is charging: 0.00 | Is selling battery: 0.00
T13 | Buy: 0.00 | Sell: 4.76 | Charge: 0.00 | Discharge to Grid: 0.00 | Discharge to Load: 0.00 | SOC: 5.19 | Is charging: 0.00 | Is selling battery: 0.00
T14 | Buy: 0.00 | Sell: 6.19 | Charge: 0.00 | Discharge to Grid: 0.00 | Discharge to Load: 0.00 | SOC: 5.19 | Is charging: 0.00 | Is selling battery: 0.00
T15 | Buy: 0.00 | Sell: 1.95 | Charge: 3.08 | Discharge to Grid: 0.00 | Discharge to Load: 0.00 | SOC: 7.96 | Is charging: 1.00 | Is selling battery: 0.00
T16 | Buy: 0.00 | Sell: 0.38 | Charge: 4.30 | Discharge to Grid: 0.00 | Discharge to Load: 0.00 | SOC: 11.83 | Is charging: 1.00 | Is selling battery: 0.00
T17 | Buy: 0.00 | Sell: 0.87 | Charge: 4.30 | Discharge to Grid: 0.00 | Discharge to Load: 0.00 | SOC: 15.70 | Is charging: 1.00 | Is selling battery: 0.00
T18 | Buy: 0.00 | Sell: 0.00 | Charge: 1.77 | Discharge to Grid: 0.00 | Discharge to Load: 0.00 | SOC: 17.30 | Is charging: 1.00 | Is selling battery: 0.00
T19 | Buy: 0.00 | Sell: 2.35 | Charge: 0.00 | Discharge to Grid: 0.00 | Discharge to Load: 0.00 | SOC: 17.30 | Is charging: 0.00 | Is selling battery: 0.00
T20 | Buy: 0.00 | Sell: 1.81 | Charge: 0.00 | Discharge to Grid: 0.00 | Discharge to Load: 0.00 | SOC: 17.30 | Is charging: 0.00 | Is selling battery: 0.00
T21 | Buy: 0.00 | Sell: 1.82 | Charge: 0.00 | Discharge to Grid: 0.00 | Discharge to Load: 0.00 | SOC: 17.30 | Is charging: 0.00 | Is selling battery: 0.00
T22 | Buy: 0.00 | Sell: 2.65 | Charge: 0.00 | Discharge to Grid: 1.66 | Discharge to Load: 0.64 | SOC: 14.75 | Is charging: 0.00 | Is selling battery: 1.00
T23 | Buy: 0.00 | Sell: 7.66 | Charge: 0.00 | Discharge to Grid: 7.46 | Discharge to Load: 1.14 | SOC: 5.19 | Is charging: 0.00 | Is selling battery: 1.00
Za sterowanie falownika odpowiadają dwa ostatnie parametry przedstawione w tabeli, które służą do ustawienia falownika w tryb sprzedaży z baterii, lub ładowania baterii. Ten ostatni parametr ustawiony na '0' będzie powodował sprzedaż produkcji z PV, np. przed ładowaniem w godzinach porannych.
Tutaj jest kod:
ha_client.py - biblioteka odczytu i zapisu danych z HomeAssistant:
import requests
class HAClient:
def __init__(self, base_url, token):
self.base_url = base_url
self.headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
def get_state(self, sensor_id):
url = f"{self.base_url}/api/states/{sensor_id}"
r = requests.get(url, headers=self.headers)
r.raise_for_status()
return r.json()['state']
def get_attr(self, sensor_id, attr):
url = f"{self.base_url}/api/states/{sensor_id}"
r = requests.get(url, headers=self.headers)
r.raise_for_status()
return r.json().get('attributes', {}).get(attr, [])
def get_history(self, sensor_id, start_iso, end_iso):
url = f"{self.base_url}/api/history/period/{start_iso}"
params = {
"end_time": end_iso,
"filter_entity_id": sensor_id
}
r = requests.get(url, headers=self.headers, params=params)
r.raise_for_status()
return r.json()
def get_delta_kwh(self, entity, start, end):
data = self.get_history(entity, start, end)
if not data or len(data[0]) < 2:
raise ValueError(f"No data for {entity} between {start} and {end}")
v_start = float(data[0][0]['state'])
v_end = float(data[0][-1]['state'])
return round((v_end - v_start) / 1000, 3)
def set_state(self, entity_id, value):
domain, _ = entity_id.split(".", 1)
if domain == "input_number":
url = f"{self.base_url}/api/services/input_number/set_value"
payload = {"entity_id": entity_id, "value": value}
elif domain == "input_boolean":
service = "turn_on" if bool(value) else "turn_off"
url = f"{self.base_url}/api/services/input_boolean/{service}"
payload = {"entity_id": entity_id}
else:
raise ValueError(f"Unsupported helper domain: {domain}")
r = requests.post(url, headers=self.headers, json=payload)
r.raise_for_status()Kod: Python
Inverter_pulp_hourly_script.py - właściwy skrypt.
import pulp
import pytz
import numpy as np
from datetime import datetime, timedelta
from dataclasses import dataclass
from typing import List
# --- External dependencies ---
from ha_client import HAClient
##########################################################################
#### CONFIGURATION
HA_URL = "http://192.168.1.3:8123"
TOKEN = "twój-token-dostępu-wygenerowany-w-homeassistant"
# Sensors
ENTITY1 = "sensor.inverter_essential_power_consumption"
ENTITY2 = "sensor.inverter_non_essential_power_consumption"
OVERNIGHT_SOC_HELPER = "input_number.stop_discharge_at_this_soc"
CHARGING_POLICY_HELPER = "input_boolean.charge_battery"
DISCHARGE_TO_GRID_HELPER = "input_boolean.discharge_to_grid"
BATTERY_SOC_SENSOR = "sensor.inverter_battery"
BATTERY_CAPACITY_SENSOR = "sensor.inverter_battery_capacity"
PV_FORECAST_SENSOR_TODAY= "sensor.solcast_pv_forecast_forecast_today"
PV_FORECAST_SENSOR_TMR = "sensor.solcast_pv_forecast_forecast_tomorrow"
BUY_PRICE = 1.00 #PLN
FALLBACK_SELL_PRICE = 0.40 #PLN
SELL_PRICES_SENSOR = "sensor.tge_fixing_1_rate"
SELL_PRICE_ATTR = "prices"
# Parameters
INVERTER_EFFICIENCY = 0.90
BATTERY_MIN_SOC = 30 # in %
BATTERY_MAX_CHARGE = 4.3 # Max charging/discharging power in kW
BATTERY_MAX_DISCHARGE = 8.6
HOUR_PROFILE_START = 18 # 6 PM
DAYS_HISTORY = [1, 2, 3]
# Global context
ha = HAClient(HA_URL, TOKEN)
tz = pytz.timezone("Europe/Warsaw")
now_local = datetime.now(tz)
##########################################################################
#### OPTIMIZATION MODEL CLASS
@dataclass
class OptimizationModel:
T: int
pv: List[float]
consumption: List[float]
sell_price: List[float]
buy_price: float
soc_init: float
battery_capacity: float
max_charge: float
max_discharge: float
average_sell_price: float = 0
inverter_efficiency: float = 0.90
battery_min_soc_pct: float = 0.3
grid_buy: List = None
grid_sell: List = None
charge: List = None
discharge_to_grid: List = None
discharge_to_load: List = None
soc: List = None
is_charging: List = None
is_battery_selling: List = None # Binary variable: 1 if discharging to grid allowed
##########################################################################
#### HELPER FUNCTIONS
[inContentAd]
def get_average_consumption(T):
"""Return average hourly consumption profile (kWh) over last DAYS_HISTORY days."""
consumption_profile = [[] for _ in range(T)]
for day_offset in DAYS_HISTORY:
start_base = now_local - timedelta(days=day_offset)
start_base = start_base.replace(hour=HOUR_PROFILE_START, minute=0, second=0, microsecond=0)
for h in range(T):
st = start_base + timedelta(hours=h)
en = st + timedelta(hours=1)
st_iso = st.astimezone(pytz.UTC).isoformat()
en_iso = en.astimezone(pytz.UTC).isoformat()
try:
d1 = ha.get_delta_kwh(ENTITY1, st_iso, en_iso)
d2 = ha.get_delta_kwh(ENTITY2, st_iso, en_iso)
consumption_profile[h].append(d1 + d2)
except ValueError:
consumption_profile[h].append(0.0)
avg_profile = [
round(sum(vals) / len(vals), 3) if vals else 0.0
for vals in consumption_profile
]
print("Average consumption profile per hour (kWh):")
for h, val in enumerate(avg_profile):
print(f" Hour {h:02d}: {val:.3f}")
return avg_profile
def get_pv_forecast(hours_into_future):
"""Return hourly PV forecast (kWh) for the next hours_into_future hours."""
forecast_today = ha.get_attr(PV_FORECAST_SENSOR_TODAY, 'detailedForecast')
forecast_tomorrow = ha.get_attr(PV_FORECAST_SENSOR_TMR, 'detailedForecast')
pv_profile = {h: 0.0 for h in range(hours_into_future)}
now = datetime.now(tz)
for item in forecast_today:
start = datetime.fromisoformat(item['period_start']).astimezone(tz)
offset = (start - now).total_seconds() // 3600
if 0 <= offset < hours_into_future:
bucket = int(offset)
pv_profile[bucket] += item.get('pv_estimate', 0)
for item in forecast_tomorrow:
start = datetime.fromisoformat(item['period_start']).astimezone(tz)
offset = (start - now).total_seconds() // 3600
if 0 <= offset < hours_into_future:
bucket = int(offset)
pv_profile[bucket] += item.get('pv_estimate', 0)
pv_list = [pv_profile[h] for h in range(hours_into_future)]
print("Forecast PV production per hour (kWh):")
for h, val in enumerate(pv_list):
print(f" Hour {h:02d}: {val:.3f}")
return pv_list
def get_battery_state():
"""Return (capacity_kWh, initial_soc_kWh)."""
capacity = float(ha.get_state(BATTERY_CAPACITY_SENSOR))
soc_pct = float(ha.get_state(BATTERY_SOC_SENSOR))
soc_kwh = (soc_pct / 100) * capacity
return capacity, soc_kwh
def get_sell_prices(hours_into_future, fallback_price=FALLBACK_SELL_PRICE):
""" Get forecast of prices untill midnight """
forecast = ha.get_attr(SELL_PRICES_SENSOR, SELL_PRICE_ATTR)
sell_price = [None for _ in range(hours_into_future)]
now = datetime.now(tz)
for item in forecast:
start = datetime.fromisoformat(item['time']).astimezone(tz)
offset = (start - now).total_seconds() / 3600
if 0 < offset < hours_into_future:
bucket = int(offset)
sell_price[bucket] = item.get("price", 0) / 1000 # convert prices to per kwH
for i in range(hours_into_future):
if sell_price[i] is None:
sell_price[i] = fallback_price
print("Sell price per hour (€/kWh):")
for h, val in enumerate(sell_price):
print(f" Hour {h:02d}: {val:.4f}")
return sell_price
def define_lp_variables(model: OptimizationModel):
"""Define LP variables and attach them to the model."""
T = model.T
capacity = model.battery_capacity
max_charge = model.max_charge
max_discharge = model.max_discharge
model.grid_buy = [pulp.LpVariable(f"grid_buy_{t}", lowBound=0) for t in range(T)]
model.grid_sell = [pulp.LpVariable(f"grid_sell_{t}", lowBound=0) for t in range(T)]
model.charge = [pulp.LpVariable(f"charge_{t}", lowBound=0, upBound=max_charge) for t in range(T)]
model.discharge_to_grid = [pulp.LpVariable(f"discharge_to_grid{t}", lowBound=0, upBound=max_discharge) for t in range(T)]
model.discharge_to_load = [pulp.LpVariable(f"discharge_to_load{t}", lowBound=0, upBound=max_discharge) for t in range(T)]
model.soc = [pulp.LpVariable(f"soc_{t}", lowBound=0, upBound=capacity) for t in range(T)]
model.is_charging = [pulp.LpVariable(f"is_charging_{t}", cat="Binary") for t in range(T)]
model.is_battery_selling = [pulp.LpVariable(f"is_battery_selling_{t}", cat="Binary") for t in range(T)]
def build_lp_problem(model: OptimizationModel):
"""Build and return the LP problem."""
prob = pulp.LpProblem("Electricity_Optimization", pulp.LpMaximize)
# Objective function: maximize profit
prob += pulp.lpSum([
model.sell_price[t] * model.grid_sell[t] - model.buy_price * model.grid_buy[t]
for t in range(model.T)
]) + model.average_sell_price * model.soc[model.T - 1]
# Constraints
for t in range(model.T):
# Energy balance
prob += (
model.pv[t]
+ model.grid_buy[t]
+ model.discharge_to_load[t]
+ model.discharge_to_grid[t]
==
model.consumption[t]
+ model.charge[t]
+ model.grid_sell[t])
# Battery state of charge (SOC)
if t == 0:
prob += model.soc[t] == model.soc_init \
+ model.inverter_efficiency * model.charge[t] \
- (model.discharge_to_grid[t] + model.discharge_to_load[t]) * (1 / model.inverter_efficiency)
else:
prob += model.soc[t] == model.soc[t-1] \
+ model.inverter_efficiency * model.charge[t] \
- (model.discharge_to_grid[t] + model.discharge_to_load[t]) * (1 / model.inverter_efficiency)
# Limit battery charging power to what is the battery able to absorb
prob += model.charge[t] <= model.max_charge * model.is_charging[t]
# Limit discharging components by control variables
prob += model.discharge_to_load[t] <= model.max_discharge * (1 - model.is_charging[t])
prob += model.discharge_to_grid[t] <= model.max_discharge * model.is_battery_selling[t]
prob += model.discharge_to_grid[t] >= 0.001 * model.is_battery_selling[t] # Ensures it stays 0 if no selling.
# Battery cannot supply more than the house needs
prob += model.discharge_to_load[t] <= model.consumption[t]
# Limit total discharge
prob += model.discharge_to_load[t] + model.discharge_to_grid[t] <= model.max_discharge
# Put discharge to sell on the export side:
prob += model.grid_sell[t] >= model.discharge_to_grid[t]
# Ensure min. battery level
prob += model.soc[t] >= model.battery_min_soc_pct * model.battery_capacity
return prob
def solve_and_report(prob, model: OptimizationModel):
model.average_sell_price = sum(model.sell_price) / model.T
"""Solve the problem and print results."""
prob.solve()
status = pulp.LpStatus[prob.status]
print(f"Status: {status}")
profit = pulp.value(prob.objective)
if profit is not None:
print(f"Total profit: {profit:.2f} €")
else:
print("No valid solution found.")
return
for t in range(model.T):
print(f"T{t:02d} | Buy: {model.grid_buy[t].varValue:.2f} | Sell: {model.grid_sell[t].varValue:.2f} | "
f"Charge: {model.charge[t].varValue:.2f} | Discharge to Grid: {model.discharge_to_grid[t].varValue:.2f} | Discharge to Load: {model.discharge_to_load[t].varValue:.2f} | SOC: {model.soc[t].varValue:.2f} | "
f"Is charging: {model.is_charging[t].varValue:.2f} | Is selling battery: {model.is_battery_selling[t].varValue:.2f}"
)
##########################################################################
#### MAIN
def main():
T = 24
consumption = get_average_consumption(T)
pv = get_pv_forecast(T)
sell_price = get_sell_prices(T)
buy_price = BUY_PRICE
capacity, soc_init = get_battery_state()
model = OptimizationModel(
T=T,
pv=pv,
consumption=consumption,
sell_price=sell_price,
buy_price=buy_price,
soc_init=soc_init,
battery_capacity=capacity,
max_charge=BATTERY_MAX_CHARGE,
max_discharge=BATTERY_MAX_DISCHARGE,
battery_min_soc_pct=BATTERY_MIN_SOC/100
)
define_lp_variables(model)
prob = build_lp_problem(model)
solve_and_report(prob, model)
# Send results back to HA
ha.set_state( CHARGING_POLICY_HELPER, bool(round(model.is_charging[0].varValue) ) )
ha.set_state( DISCHARGE_TO_GRID_HELPER, bool(round(model.is_battery_selling[0].varValue) ) )
ha.set_state( OVERNIGHT_SOC_HELPER, round(model.soc[0].varValue / capacity * 100, 1) )
if __name__ == "__main__":
main()
Fajne? Ranking DIY