logo elektroda
logo elektroda
X
logo elektroda
REKLAMA
REKLAMA
Adblock/uBlockOrigin/AdGuard mogą powodować znikanie niektórych postów z powodu nowej reguły.

Optymalizacja strategii ładowania i rozładowania banku energii PV – model LP, HomeAssistant

kubickim 03 Sie 2025 22:13 1089 9

TL;DR

  • Skrypt w Pythonie optymalizuje ładowanie, rozładowanie i sprzedaż energii z banku PV sterowanego przez HomeAssistant.
  • Model LP uwzględnia prognozę produkcji Solcast, zużycie z ostatnich 3 dni, ceny sprzedaży TGE oraz parametry akumulatora.
  • Optymalizacja działa w horyzoncie 24 godzin, z minimalnym SOC 30%, mocą ładowania 4,3 kW i rozładowania 8,6 kW.
  • Przykładowe uruchomienie zwróciło status Optimal i 12,96 € zysku.
  • Największą niewiadomą pozostaje wycena energii rezydualnej na końcu okresu, bo może promować pełny lub pusty bank.
Wygenerowane przez model językowy.
REKLAMA
📢 Słuchaj (AI):
  • [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:

    
    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
    Zaloguj się, aby zobaczyć kod


    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
    O autorze
    kubickim
    Poziom 6  
    Offline 
    kubickim napisał 10 postów o ocenie 10. Jest z nami od 2021 roku.
  • REKLAMA
  • #2 21625256
    gulson
    Administrator Systemowy
    Posty: 29312
    Pomógł: 148
    Ocena: 6016
    O ciekawe, czyli jest możliwa "inteligentna" automatyka bez sztucznej inteligencji ;)

    Czyli to taki automatyczny doradca, który mówi, kiedy:
    - Sprzedać energię wtedy, gdy jest najdroższa
    - Kupić energię z sieci wtedy, gdy jest najtańsza
    - Naładować lub rozładować akumulator w optymalny sposób
    - Oszczędzać tam, gdzie się da – bez przepłacania

    Leci na główną! Dzięki! Napisz do mnie Paczkomat, a wyślę mały upominek.
  • REKLAMA
  • #3 21625327
    kubickim
    Poziom 6  
    Posty: 10
    Ocena: 10
    >>21625256 Dzięki! Odpisałem na priv. Co do punktu odnośnie zakupu energii - na razie nie mam dodanej opcji zmiennej ceny zakupu, jednak nie byłoby to trudne, gdyby ktoś miał taryfę nocną. Wtedy na pewno by proponował ciekawe rozwiązania co do zakupu energii w nocy w pochmurne dni dla pokrycia zapotrzebowania dziennego, o ile różnica w cenie jest większa od strat falownika.
  • REKLAMA
  • #4 21629542
    kubickim
    Poziom 6  
    Posty: 10
    Ocena: 10
    Nowa wersja skryptu jest mocno przebudowana. Sam model został napisany od nowa, z uproszczonymi warunkami i z rozdzieleniem zmiennych rozładowania baterii z podziałem na kierunki (grid / load). Lista zmian:
    - Dodałem również komentarze w j. polskim.
    - Czujniki pomiaru energii mozna wpisać w dowolnej ilości jako tablicę. Do wyboru są dwie jednostki pracy czujników - kWh lub Wh. Można dodać więcej dodając stosowne przeliczniki (wiersze 21 i 22)
    - Czujniki prognozy produkcji PV również można wpisać w dowolnej ilości jako tabelę
    - Wyniki prognozy wysyłane są do HA do [sensor.planned_battery_state]. Można odczytać planowany stan baterrii na okres planowania.
    - Skrypt odnotowuje wynik modelowania w logu (zapisywana jest. godzina wykonania, oraz czy model znalazł rozwiązanie optymalne)
    - Dodany jest parametr 'pesymizmu' którym można sterować w jakim stopniu chcemu ufać prognozie SOLARCAST - bliżej mediany, czy bliżej dolnego przedziału produkcji, celem uniknięcia przeszacowania sprzedaży z baterii dnia poprzedniego.

    Wyniki pracy można zobrazować takim wykresem:

    Wykres słupkowy pokazujący eksport, ładowanie baterii i ceny energii w ciągu dnia

    Na czerwono oznaczono okresy sprzedaży do sieci, zielono - ładowanie baterii. Niebieska linia to ceny rynkowe. Jak widać, model poprawnie zaplanował sprzedaż w godzinach porannych, a ładowanie baterii podczas okresu niskich cen. zaplanował również sprzedaż z baterii o godzinie najwyższych cen.

    Listing 1: Kod skryptu
    
    import pytz
    import os
    import sys
    from pulp import *
    from ha_client import HAClient
    from datetime import datetime, timedelta
    from dataclasses import dataclass
    
    # Komunikacja z Homeassistant
    HA_URL = "http://192.168.1.3:8123"
    TOKEN = "twój_długoterminowy_klucz_wygenerowany_w_homeassistant"
    
    # --- Pliki logowania ---
    BASE_DIR = os.path.dirname(os.path.abspath(__file__))
    LOG_PATH = os.path.join(BASE_DIR, "pv_optimizer.log")
    
    # --- Sensory wejściowe ---
    
    # Sensory podające konsumpcje domu w kWh narastająco
    # przeliczniki dla jednostek sensorów zużycia
    kWh = 1
    Wh = 1 / 1000
    CONSUMPTION_SENSORS = [("sensor.inverter_essential_power_consumption", Wh), # podaj sensor oraz jednostkę sensora
                           ("sensor.inverter_non_essential_power_consumption", Wh),
                           ("sensor.inverter_total_losses", kWh)]
    
    # Sensory SOLCARCAST:
    PV_FORECAST_SENSORS = ["sensor.solcast_pv_forecast_forecast_today", 
                           "sensor.solcast_pv_forecast_forecast_tomorrow", 
                           "sensor.solcast_pv_forecast_forecast_day_3" ]
    
    # Sensory stanu baterii:
    BATTERY_SOC_SENSOR      = "sensor.inverter_battery" # Stan baterii w %
    BATTERY_CAPACITY_SENSOR = "sensor.inverter_battery_capacity" # Całkowita pojemność 
    
    # Sensory podające prognozę cen rynku TGE:
    SELL_PRICES_SENSOR  = "sensor.tge_fixing_1_rate"
    SELL_PRICE_ATTR     = "prices"
    
    # Sensory ustalające ceny zakupu energii dla każdej godziny
    BUY_PRICE_PEAK_SENSOR           = "input_number.electricity_buy_price_peak" # Ceny w szczycie
    BUY_PRICE_OFFPEAK_SENSOR        = "input_number.electricity_buy_price_offpeak" # Cena poza szczytem
    BUY_PRICE_TRANSMISSION_SENSOR   = "input_number.electricity_buy_price_transmission_cost" # Łączna suma opłat tranferowych
    
    # Zestaw sensorów programujących godziny szczytu (sensory ON/OFF dla każdej godziny):
    # Potrzebne jest 24 sensorów z podanym wzorem nazewy (końcówka 00 do 23 dla każdej godziny)
    PEAK_HOURS_SENSORS              = "input_boolean.buy_price_peak_hour_" 
    
    # --- Sensory wynikowe ---
    BATTERY_SALE_LIMITER = "input_number.stop_discharge_at_this_soc"
    CHARGING_POLICY_HELPER = "input_boolean.charge_battery"
    DISCHARGE_TO_GRID_HELPER = "input_boolean.discharge_to_grid"
    BATTERY_STATE_FORECAST_HELPER = "sensor.planned_battery_state"
    
    # === Parametry instalacji ===
    wskaznik_pesymizmu = 1        # Ustawienie preferencji zaufania do prognozy: 1 - prognoza pesymistyczna, 0 - optymistyczna
    inverter_efficiency = 0.90    # Sprawność inwertera.
    max_charging_power = 3        # kW - średnia wartośc podczas godziny - powinna być mniejsza od zdolności baterii z uwagi na możliwość nierównego ładowanai podczas przejściowego zachmurzenia
    max_discharging_power = 6     # kW
    min_battery_reserve = 30      # % - minimalne wskazanie jakie powinien zachować planner dla stanu baterri w godzinach porannych (np. na niespodziewane zmiany w konsumpcji)
    fallback_sell_price = 0.40    # zł/kWh - zakładana cena sprzedaży w przypadku braku prognozy
    min_battery_sale = 1          # kW - minimalna wartość zadanej sprzedażyz baterii dla przestawienia falownika w tryb sprzedaży - chroni falownik przed 'frywolnymi' decyzjami modelu
    
    # === Horyzont planowania ===
    hours = list(range(48))
    hours_int = len(hours)
    
    # === Zmienne Globalne ===  
    ha = HAClient(HA_URL, TOKEN) 
    tz = pytz.timezone("Europe/Warsaw")
    now_local = datetime.now(tz)
    
    # === Funkcje pomocnicze ===
    
    def log(msg: str) -> None:    
        stamp = datetime.now(tz).strftime("%Y-%m-%d %H:%M:%S")
        with open(LOG_PATH, "a", encoding="utf-8") as f:
            f.write(f"[{stamp}] {msg}\n")
    def get_average_consumption(T):
        doba = 24
        consumption_profile = [[] for _ in range(doba)]
        now = datetime.now(tz)
        for day_offset in [1,2,3]:
            start =  (now - timedelta(days=day_offset, hours=doba+1)).replace(minute=0, second=0, microsecond=0)
            for h in range(doba):
                st = start + timedelta(hours=h)
                en = st + timedelta(hours=1)
                try:
                    total = 0
                    for sensor, multiplier in CONSUMPTION_SENSORS:
                        total += ha.get_delta_kwh(sensor, st.isoformat(), en.isoformat()) * multiplier
                    consumption_profile[h].append(total)
                except ValueError:
                    pass
        avg_consumption = [  #
            round(sum(vals) / len(vals), 3) if vals else 0.0
            for vals in consumption_profile
        ]
        avg_for_period_T = [avg_consumption[t % doba] for t in range(T)]
        return avg_for_period_T
    def get_pv_forecast(T, PESIMISM=0.0):
        """
        PESIMISM:
            0.0 → bez korekty - używa tylko korekty w oparciu o różnicę w projekcji 
            1.0 → always use estimate_10 (full pessimism)
            0.5 → halfway between adjusted_forecast and estimate_10
        """
        pv_profile = {h: 0.0 for h in range(T)}
        max_spread = 0.8 # Typical difference in forecast estimations, if larger we need to accound for risks.
        now = datetime.now(tz)
        # Load sensor data:
        forecasts = []
        for s in PV_FORECAST_SENSORS:
            data = ha.get_attr(s, 'detailedForecast')
            if data:
                forecasts.extend(data)
        # Process forecasts:
        for item in forecasts:
            start = datetime.fromisoformat(item['period_start']).astimezone(tz)
            offset = (start - now).total_seconds() // 3600
            if 0 <= offset < T: #Here we make the forecast more conservative
                estimate_50 = item.get('pv_estimate', 0)
                estimate_10 = item.get('pv_estimate10', 0)
                estimate_90 = item.get('pv_estimate90', 0)
                forecast_spread = estimate_90 - estimate_10                         
                confidence = max(0.3, min(1.0, 1 - forecast_spread / max_spread))   
                adjusted_forecast = confidence * estimate_50 + (1 - confidence) * estimate_10
                final_forecast = (1 - PESIMISM) * adjusted_forecast + PESIMISM * estimate_10             
                pv_profile[offset] += final_forecast
        pv_list = [pv_profile[h] for h in range(T)]
        return pv_list
    def get_battery_state():
        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):
        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
        return sell_price
    def get_dynamic_buy_price(T):
        prices = []
        peak_price = float(ha.get_state(BUY_PRICE_PEAK_SENSOR))
        offpeak_price = float(ha.get_state(BUY_PRICE_OFFPEAK_SENSOR))
        transmission_cost = float(ha.get_state(BUY_PRICE_TRANSMISSION_SENSOR))
        start = datetime.now(tz).replace(minute=0, second=0, microsecond=0)
        for t in range(T):
            hour = (start + timedelta(hours=t)).hour
            ent = f"{PEAK_HOURS_SENSORS}{hour:02d}"
            is_peak = ha.get_state(ent) == "on"
            prices.append(peak_price+transmission_cost if is_peak else offpeak_price+transmission_cost)
        return prices
    
    #
    # MAIN FUNCTION
    #
    
    # === Pobranie danych z systemu ===
    try:
        pv = get_pv_forecast(hours_int, wskaznik_pesymizmu)
        load = get_average_consumption(hours_int)
        max_battery_capacity, initial_battery_level = get_battery_state()
        cena_sprzedazy = get_sell_prices(hours_int, fallback_sell_price)
        cena_zakupu = get_dynamic_buy_price(hours_int)
    except Exception as e:
        log(f"Błąd odczytu to danych z Homeassitant: {e}")
        sys.exit(1)
    
    reserved_battery = min_battery_reserve / 100 * max_battery_capacity
    model = LpProblem("Optymalizacja_energii_domowej", LpMinimize)
    
    # === Definicja zmiennych decyzyjnych dla modelu ===
    pv_to_load      = LpVariable.dicts("pv_to_load", hours, lowBound=0)
    pv_to_bat       = LpVariable.dicts("pv_to_bat", hours, lowBound=0, upBound=max_charging_power)
    pv_to_grid      = LpVariable.dicts("pv_to_grid", hours, lowBound=0)
    bat_to_grid     = LpVariable.dicts("bat_to_grid", hours, lowBound=0, upBound=max_discharging_power)
    bat_to_load     = LpVariable.dicts("bat_to_load", hours, lowBound=0, upBound=max_discharging_power)
    grid_to_load    = LpVariable.dicts("grid_to_load", hours, lowBound=0)
    #grid_to_battery    = LpVariable.dicts("grid_to_batteyr", hours, lowBound=0, upBound=max_charging_power)
    bateria         = LpVariable.dicts("bateria", hours, lowBound=reserved_battery, upBound=max_battery_capacity)
    is_charging     = LpVariable.dicts("is_charging", hours, cat="Binary")
    
    # === Funkcja celu ===
    kara_za_export = 1e-4 # preferencja ładowania
    model += lpSum(
        grid_to_load[h] * cena_zakupu[h]
        - (bat_to_grid[h] + pv_to_grid[h]) * cena_sprzedazy[h] 
        + kara_za_export * pv_to_grid[h]
        for h in hours
        ), "Koszt_netto"
    
    # === Ograniczenia ===
    
    for h in hours:
        model += pv_to_load[h] + bat_to_load[h] + grid_to_load[h] == load[h], f"Bilans_domu_{h}"
        model += pv_to_load[h] + pv_to_bat[h] + pv_to_grid[h] <= pv[h], f"Bilans_PV_{h}"
        model += pv_to_bat[h] <= max_charging_power * is_charging[h], f"Max_ladowanie_{h}"
        model += (bat_to_grid[h] + bat_to_load[h]) <= max_discharging_power * (1 - is_charging[h]), f"Max_rozladowanie_{h}"
        ie =  inverter_efficiency
        if h == 0:
            model += bateria[h] == initial_battery_level + pv_to_bat[h] * ie - (bat_to_load[h] + bat_to_grid[h]) / ie, f"Bilans_bat_{h}"
        else:
            model += bateria[h] == bateria[h-1] + pv_to_bat[h] * ie - (bat_to_load[h] + bat_to_grid[h]) / ie, f"Bilans_bat_{h}"
        model += pv_to_bat[h] <= pv[h] - pv_to_load[h], f"Priorytet_ładowania_{h}"
        deficyt_pv = max (0, load[h] - pv[h])
        model += bat_to_load[h] <= deficyt_pv, f"Priorytet_zasilania_z_pv_{h}"
    
    # === Rozwiązanie ===
    model.solve()
    status = LpStatus[model.status]
    log(f"Solver status: {status}")
    
    # === Wyniki ===
    total_cost = value(model.objective)
    print(f"\n Koszt netto: {round(total_cost, 2)} zł\n")
    
    print("        |      |      |          PV           |        GRID       |           BATERIA           |       Ceny         |")
    print("Godzina |  PV  | Load | PV>LO | PV>BA | PV>GR | GR > LO | GR > BA | BA > LO | BA > GR | Bateria | Zakupu | Sprzedaży |")
    print("--------|------|------|-------|-------|-------|---------|---------|---------|---------|---------|--------|-----------|")
    for h in hours:
        print(f"{h:>7} | {pv[h]:>4.2f} | {load[h]:>4.2f} | "
              f"{pv_to_load[h].varValue:>5.2f} | {pv_to_bat[h].varValue:>5.2f} | {pv_to_grid[h].varValue:>5.2f} | "
              f"{grid_to_load[h].varValue:>7.2f} |         | "
              f"{bat_to_load[h].varValue:>7.2f} | {bat_to_grid[h].varValue:>7.2f} | {'*' if is_charging[h].varValue else ' '}{bateria[h].varValue:>6.2f} |  "
              f"{cena_zakupu[h]:>5.2f} | {cena_sprzedazy[h]:>6.2f}"
             )
    
    # Przesłanie wyników modelowania do Homeassistant do pomocników 
    # Ustawienie trybu pracy falownika odbywa się przez automatyzacje które monitorują stan tych pomocników 
    
    ha.set_state( CHARGING_POLICY_HELPER, bool(round(is_charging[0].varValue) ) )
    ha.set_state( DISCHARGE_TO_GRID_HELPER, int(bat_to_grid[0].varValue > min_battery_sale))
    ha.set_state( BATTERY_SALE_LIMITER, round(bateria[0].varValue / max_battery_capacity * 100, 1) )
    
    # Przesłanie wyników modelowania stanu baterii - można wyświetlić w Apex_charts.
    #
    
    battery_state_forecast = []
    start = now_local.replace(minute=0, second=0, microsecond=0)
    for h in hours:
        td = (start + timedelta(hours=h)).isoformat()
        soc = round((bateria[h].varValue / max_battery_capacity) * 100)
        battery_state_forecast.append({"period_start": td, "soc": f"{soc}%"})
    ha.set_forecast( BATTERY_STATE_FORECAST_HELPER, battery_state_forecast, "PlannedBatteryState" )
    


    Listing 2: Biblioteka dostępu to HA - dodana została funkcja zapisu projekcji stanu baterii oraz zmieniono odczyt energii:
    
    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), 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()
    
        def set_forecast(self, entity_id, series, attr_name):
            url = f"{self.base_url}/api/states/{entity_id}"
            attributes = {attr_name: series}
            payload = {
                "state": "forecast",
                "attributes": attributes
            }
            r = requests.post(url, headers=self.headers, json=payload, timeout=10)
            r.raise_for_status()
            return r.json()
    



    Listing 3: Plik konfiguracyjny do homeassistant. Można zachować w /config/packages. Tworzy on potrzebne pomocniki:
    
    ### Return values from inverter calculations:
    
    input_boolean:
      charge_battery:
        name: Charge Battery
        icon: mdi:battery-arrow-up
      discharge_to_grid:
        name: Discharge to Grid
        icon: mdi:battery-arrow-down
    
    ### Dynamic buy prices:
    
      buy_price_peak_hour_00:
      buy_price_peak_hour_01:
      buy_price_peak_hour_02:
      buy_price_peak_hour_03:
      buy_price_peak_hour_04:
      buy_price_peak_hour_05:
      buy_price_peak_hour_06:
      buy_price_peak_hour_07:
      buy_price_peak_hour_08:
      buy_price_peak_hour_09:
      buy_price_peak_hour_10:
      buy_price_peak_hour_11:
      buy_price_peak_hour_12:
      buy_price_peak_hour_13:
      buy_price_peak_hour_14:
      buy_price_peak_hour_15:
      buy_price_peak_hour_16:
      buy_price_peak_hour_17:
      buy_price_peak_hour_18:
      buy_price_peak_hour_19:
      buy_price_peak_hour_20:
      buy_price_peak_hour_21:
      buy_price_peak_hour_22:
      buy_price_peak_hour_23:
    
    input_number:
      electricity_buy_price_peak:
        name: Peak Buy Price
        min: 0
        max: 5
        step: 0.01
        unit_of_measurement: 'PLN/kWh'
      electricity_buy_price_offpeak:
        name: Off-Peak Buy Price
        min: 0
        max: 5
        step: 0.01
        unit_of_measurement: 'PLN/kWh'
      electricity_buy_price_transmission_cost:
        name: Electricity Transmission Unit Cost
        min: 0
        max: 5
        step: 0.01
        unit_of_measurement: 'PLN/kWh'
    
      stop_discharge_at_this_soc:
        name: SoC After Discharge (%)
        icon: mdi:battery-50ś
        initial: 30
        min: 0
        max: 100
        step: 1
        unit_of_measurement: '%'
        mode: box  
    
    shell_command:
      calculate_battery_buffer: 'python3 /config/scripts/calculate_battery_buffer.py'
    
    template:
      - sensor:
          - name: "Current Energy Buy Price"
            unit_of_measurement: "PLN/kWh"
            state: >
              {% set peak = is_state('input_boolean.buy_price_peak_hour_' ~ "%02d"|format(now().hour), 'on') %}
              {% set peak_price = states('input_number.electricity_buy_price_peak')|float %}
              {% set offpeak_price = states('input_number.electricity_buy_price_offpeak')|float %}
              {% set transmission_cost = states('input_number.electricity_buy_price_transmission_cost')|float %}
              {{ peak_price+transmission_cost if peak else offpeak_price+transmission_cost }}
      - sensor:
          - name: "Planned Battery State"
            unique_id: planned_battery_state
            state: "unknown"
    


    Listing 4: W taki sposób można wyświetlić planowany stan baterii za pomocą APEX_CHATS:
    
    series:
      - entity: sensor.planned_battery_state
        name: Planned Battery
        float_precision: 2
        extend_to: false
        type: line
        color: blue
        stroke_width: 2
        yaxis_id: capacity
        show:
          legend_value: false
          in_header: false
        data_generator: |
          return entity.attributes.PlannedBatteryState.map((entry) => {
            return [new Date(entry.period_start), entry.soc];
          });
    

    Można otrzymać taki wykres:
    Wykres przedstawiający planowany stan baterii i produkcję PV w dniach 7–10 sierpnia
  • #5 21633668
    kubickim
    Poziom 6  
    Posty: 10
    Ocena: 10
    Ciekawa sytuacja jest dzisiaj. Przez ponad dwie godziny w okresie wieczornym ceny sprzedaży są wyższe nić ceny zakupu w nocy (nawet w taryfie G11), zatem skrypt wyliczył, ze opłaca się sprzedać całą baterię i pobierać energię z sieci w nocy. (Wcześniej taka sytuacja występowała tylko przez 1 godzinę, więc nie był wstanie tak szybko jej sprzedać całej baterii). Na wykresie poniżej zaznaczono ceny (niebieska linia) oraz drugi wykres obrazuje planowany stan baterii.

    Wykres z planem produkcji i poziomem energii w baterii na dobęWykresy zużycia, stanu baterii i prognozowanego zapotrzebowania na energię od 12 do 14 sierpnia

    Przy takich różnicach w cenach sprzedaży pomiędzy godzinami produkcji (między 0 a 20 gr) a godzinami szczytu (1 do 1.20 zł) w ostatnich dniach większość przychodów generuje sprzedaż z baterii pomimo relatywnie niskich wielkości prądów. Wyraźnie to widać na wykresie poniżej, gdzie dwa największe słupki to właśnie sprzedaż z baterii w dniu wczorajszym:

    Wykres słupkowy przychodów ze sprzedaży energii w dniach 11–13 sierpnia
  • REKLAMA
  • #6 21633699
    xury
    Specjalista automatyka domowa
    Posty: 7074
    Pomógł: 877
    Ocena: 1488
    Pytanie trochę off topic. Czy miałeś rozliczenie z ZE? Sam mam PV na starych zasadach, ale założyłem parę instalacji opartych o Victron i tam zrobiłem prosty flow dla nodered, który oddawał energię z magazynu w czasie najwyższych cen. Do klienta przyszło rozliczenie w którym stwierdzili, że na razie nie są w stanie rozliczyć godzinowo i rozliczyli po stawce miesięcznej plus bonus 4 grosze za kWh. PGE Radom.
  • #7 21634020
    kubickim
    Poziom 6  
    Posty: 10
    Ocena: 10
    >>21633699 Ja mam umowę z Energa. Otrzymałem rozliczenie prosumenckie za lipiec, w którym poprawnie wyliczyli wg. rozliczenia godzinowego (zgodnie z moimi wyliczeniami w HA). Można dodatkowo ściągnąć z ich strony szczegółowy wykaz godzinowy w pliku .csv gdzie raczej wszystko się zgada. Więc podobnych kwiatków się nie spodziewam. Pozdrowienia dla PGE Radom. Ciekawe jakie są warunki w zapisane w umowie. Jakiś prawnik powinien ich nastraszyć.
  • #8 21762103
    pawelbunko
    Poziom 2  
    Posty: 2
    >>21634020 i mozesz legalnie sprzedawac energie ktora zaladowales w tanich okresach np g12? myslalem ze tylko to co wyprodukowales ze slonca mozesz wyslac do sieci.
  • #10 21775046
    xury
    Specjalista automatyka domowa
    Posty: 7074
    Pomógł: 877
    Ocena: 1488
    A jak ZE ma rozróżnić energię ze słońca od energii z sieci? Nie wiem tylko czy taka operacja się opłaca bo pomimo taniej energii podczas ładowania to dochodzi jeszcze opłata przesyłowa i mocowa. Trzeba by wprowadzić dane do chataGpt i niech wyliczy.
📢 Słuchaj (AI):

Podsumowanie tematu

✨ Dyskusja dotyczy skryptu w Pythonie służącego do optymalizacji strategii ładowania i rozładowania akumulatora w instalacji fotowoltaicznej, opartego na modelu programowania liniowego (LP). Skrypt uwzględnia prognozy produkcji PV, prognozy zużycia energii w gospodarstwie, zmienne ceny sprzedaży energii do sieci, stałą cenę zakupu energii oraz parametry techniczne baterii (pojemność, moc ładowania/rozładowania, sprawność). Model maksymalizuje zysk ze sprzedaży energii i minimalizuje koszty zakupu, biorąc pod uwagę wartość energii pozostającej w akumulatorze na koniec okresu prognozy. W nowszej wersji skryptu wprowadzono uproszczenia modelu, rozdzielenie zmiennych rozładowania na kierunki (sieć i obciążenie), możliwość definiowania wielu czujników pomiaru i prognozy produkcji oraz integrację wyników z HomeAssistant. Dodano także parametr "pesymizmu" do regulacji zaufania do prognoz SOLARCAST. W praktyce skrypt potrafi wykorzystać okresy wysokich cen sprzedaży energii, np. wieczorem, do optymalnej sprzedaży energii z baterii i zakupu energii w tańszych godzinach nocnych. W dyskusji poruszono także kwestie rozliczeń godzinowych energii przez zakłady energetyczne (Energa, PGE Radom) oraz legalności sprzedaży energii zmagazynowanej w baterii, zwłaszcza w kontekście taryf i opłat przesyłowych. Wspomniano o prostych rozwiązaniach automatyzacji opartych na Node-RED i urządzeniach Victron.
Wygenerowane przez model językowy.
REKLAMA