New D57 prices

This commit is contained in:
Adam Štrauch 2025-01-16 15:52:15 +01:00
parent 11399bf21b
commit b00617768b
Signed by: cx
GPG key ID: 7262DAFE292BCE20
4 changed files with 219 additions and 36 deletions

89
calculator/calc.py Normal file
View file

@ -0,0 +1,89 @@
from dataclasses import dataclass
import datetime
from typing import List
from fastapi import HTTPException
from calculator.miner import PriceNotFound, get_energy_prices, get_eur_czk_ratio
from calculator.schema import BatteryChargingInfo, Price, SpotPrices
def get_spot_prices(date: datetime.date, hour:int, kwh_fees_low:float, kwh_fees_high:float, sell_fees:float, VAT:float, low_tariff_hours:List[int], no_cache: bool = False) -> SpotPrices:
is_today = datetime.date.today() == date
spot_hours = {}
spot_hours_for_sell = {}
spot_hours_total = {}
spot_data = get_energy_prices(date, no_cache=no_cache)
currency_ratio = get_eur_czk_ratio(date, no_cache=no_cache)
for key, value in spot_data.items():
kwh_fees = kwh_fees_low if int(key) in low_tariff_hours else kwh_fees_high
spot_hours[key] = value * currency_ratio / 1000
spot_hours_total[key] = (value * currency_ratio / 1000 + kwh_fees) * VAT
spot_hours_for_sell[key] = value * currency_ratio / 1000 - sell_fees
spot = Price(hours=spot_hours, now=spot_hours[str(hour)] if is_today else None)
spot_hours_total_sorted = {k: v for k, v in sorted(spot_hours_total.items(), key=lambda item: item[1])}
spot_total = Price(hours=spot_hours_total_sorted, now=spot_hours_total[str(hour)] if is_today else None)
spot_for_sell = Price(hours=spot_hours_for_sell, now=spot_hours_for_sell[str(hour)] if is_today else None)
return SpotPrices(
spot=spot,
spot_hours_total_sorted=Price(hours=spot_hours_total_sorted, now=spot_hours_total[str(hour)] if is_today else None),
spot_total=spot_total,
spot_for_sell=spot_for_sell,
)
def battery_charging_info(kwh_fees_low:float, kwh_fees_high:float, sell_fees:float, VAT:float, low_tariff_hours:List[int], no_cache: bool = False, battery_kwh_price:float=2.5) -> BatteryChargingInfo:
today = datetime.date.today()
hour = datetime.datetime.now().hour
tomorrow = today + datetime.timedelta(days=1)
spot_prices_today:SpotPrices = get_spot_prices(today, hour, kwh_fees_low, kwh_fees_high, sell_fees, VAT, low_tariff_hours, no_cache)
# average4hours = sum(list(spot_prices_today.spot_hours_total_sorted.hours.values())[0:4]) / 4
max_cheapest_hour = max(list(spot_prices_today.spot_hours_total_sorted.hours.values())[0:4])
average4expensive_hours = sum(list(spot_prices_today.spot_hours_total_sorted.hours.values())[20:24]) / 4
max_most_expensive_hour = max(
[x[1] for x in list(spot_prices_today.spot_hours_total_sorted.hours.items())[0:20]]
) if spot_prices_today.spot_hours_total_sorted.hours else 0
diff = max_most_expensive_hour - max_cheapest_hour
charging_hours = [int(k) for k, v in spot_prices_today.spot_hours_total_sorted.hours.items()][0:4]
discharging_hours = [int(k) for k, v in spot_prices_today.spot_hours_total_sorted.hours.items() if v > (max_cheapest_hour + battery_kwh_price)]
# Add charging hours if the price is just 10% above the most expensive charging hour
if charging_hours:
for h in range(5,24):
value = spot_prices_today.spot_hours_total_sorted.hours[str(h)]
if value <= max_cheapest_hour*1.1:
charging_hours.append(h)
# We remove end of the day hours from charging hours if the average of the last 4 hours is higher than the average of the first 4 hours of the next day
try:
spot_prices_tomorrow:SpotPrices = get_spot_prices(tomorrow, hour, kwh_fees_low, kwh_fees_high, sell_fees, VAT, low_tariff_hours, no_cache)
average_last_4hours_today = sum(list(spot_prices_today.spot_hours_total_sorted.hours.values())[20:24]) / 4
average_first_4hours_tomorrow = sum(list(spot_prices_tomorrow.spot_hours_total_sorted.hours.values())[0:4]) / 4
if average_last_4hours_today > average_first_4hours_tomorrow:
for h in range(20,24):
if h in charging_hours:
charging_hours.remove(h)
except PriceNotFound:
pass
is_viable = len(discharging_hours) > 0
return BatteryChargingInfo(
diff=diff,
is_viable=is_viable,
charging_hours=sorted(charging_hours) if len(charging_hours) > 0 else [],
is_charging_hour=hour in charging_hours if len(charging_hours) > 0 and is_viable else False,
discharging_hours=sorted(discharging_hours) if len(discharging_hours) > 0 else [],
is_discharging_hour=hour in discharging_hours if len(discharging_hours) > 0 and is_viable else False,
total_price=spot_prices_today.spot_hours_total_sorted
)

View file

@ -6,6 +6,24 @@
<script src="https://cdn.tailwindcss.com"></script>
<script>
const priceColorMap = {
0: "bg-lime-200",
2: "bg-lime-500",
4: "bg-lime-700",
6: "bg-amber-400",
8: "bg-amber-700",
10: "bg-rose-500",
12: "bg-rose-700",
14: "bg-rose-950",
16: "bg-fuchsia-500",
18: "bg-fuchsia-800",
20: "bg-fuchsia-950",
}
function getBgColor(value) {
return priceColorMap[Math.floor(value / 2) * 2];
}
function formatDate(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
@ -34,18 +52,25 @@
for (let i = 0; i < 24; i++) {
let extra_class = "";
let border = "border-dotted"
if(today && i == new Date().getHours()) {
extra_class = " border-solid border-4 border-gray-900 font-bold";
extra_class = " font-bold text-xl";
border = "border-solid"
}
let value = Math.round(data.total.hours[i]*100)/100;
if(data.cheapest_hours_by_average.hours.includes(i)) {
document.getElementById(prefix + i).className = "px-1 bg-green-500" + extra_class;
document.getElementById(prefix + i).className = "px-1 "+border+" border-4 border-lime-400 "+ getBgColor(value) + extra_class;
} else if (data.most_expensive_hours_by_average.hours.includes(i)) {
document.getElementById(prefix + i).className = "px-1 bg-rose-400" + extra_class;
document.getElementById(prefix + i).className = "px-1 "+border+" border-4 border-rose-400 "+ getBgColor(value) + extra_class;
} else {
document.getElementById(prefix + i).className = "px-1 bg-amber-100" + extra_class;
if(extra_class != "") {
extra_class = " border-4 border-solid border-gray-500";
}
document.getElementById(prefix + i).className = "px-1 "+ getBgColor(value) + extra_class;
}
document.getElementById(prefix + i).innerText = Math.round(data.total.hours[i]*100)/100;
document.getElementById(prefix + i).innerText = value;
}
})
}

View file

@ -6,8 +6,9 @@ from fastapi import FastAPI, HTTPException
from fastapi.responses import RedirectResponse, HTMLResponse
from fastapi.middleware.cors import CORSMiddleware
from calculator.calc import battery_charging_info, get_spot_prices
from calculator.miner import PriceNotFound, get_energy_prices, get_eur_czk_ratio
from calculator.schema import CheapestHours, DayPrice, MostExpensiveHours, Price
from calculator.schema import BatteryChargingInfo, CheapestHours, DayPrice, MostExpensiveHours, Price
from .consts import VAT
@ -88,15 +89,59 @@ rest:
"""
docs_battery = """
Returns data for battery charging and discharging. It takes four cheapest hours from today
and calculates if it's viable and when it's viable to charge or discharge the battery.
It partly checks data from the next day and doesn't charge the battery in the evening if
it's going to be cheaper next day in the morning.<br>
<br>
**Options:**<br>
**kwh_fees_low** - additional fees per kWh in low tariff, usually distribution + other fees, default is 1.62421 (D57d, BezDodavatele)<br>
**kwh_fees_high** - additional fees per kWh in high tariff, usually distribution + other fees, default is 1.83474 (D57d, BezDodavatele)<br>
**sell_fees** - selling energy fees, default is 0.45 (BezDodavatele), not important for this endpoint<br>
**low_tariff_hours** - list of low tariff hours, default is 0,1,2,3,4,5,6,7,9,10,11,13,14,16,17,18,20,21,22,23 (D57d, ČEZ)<br>
**battery_kwh_price** - price from which it is viable to charge and discharge the battery, default is 2.5<br>
<br>
Output:<br>
**diff** - Difference between the most expensive cheapest hour and the most expensive hour<br>
**is_viable** - True if it is viable to charge or discharge the battery today<br>
**charging_hours** - Hours when it is viable to charge the battery<br>
**is_charging_hour** - True if it is viable to charge the battery in the current hour<br>
**discharging_hours** - Hours when it is viable to discharge the battery<br>
**is_discharging_hour** - True if it is viable to discharge the battery in the current hour<br>
**total_price** - total price for the day including all fees and VAT<br>
<br>
<br>
<br>
<br>
Integration into Home Assistant can be done in configuration.yaml file:<br>
<br>
```
rest:
- resource: https://pricepower2.rostiapp.cz/battery/charging
scan_interval: 60
binary_sensor:
- name: "battery_charging_plan_is_viable"
value_template: "{{ value_json.is_viable }}"
- name: "battery_charging_plan_is_charging_hour"
value_template: "{{ value_json.is_charging_hour }}"
- name: "battery_charging_plan_is_discharging_hour"
value_template: "{{ value_json.is_discharging_hour }}"
```
"""
@app.get("/price/day", description=docs)
@app.get("/price/day/{date}", description=docs)
def read_item(
date: Optional[datetime.date]=None,
hour: Optional[int]=None,
monthly_fees: float=509.24,
monthly_fees: float=610.84,
daily_fees: float=4.18,
kwh_fees_low: float=1.62421,
kwh_fees_high: float=1.83474,
kwh_fees_low: float=1.35022,
kwh_fees_high: float=1.86567,
sell_fees: float=0.45,
low_tariff_hours:str="0,1,2,3,4,5,6,7,9,10,11,13,14,16,17,18,20,21,22,23",
no_cache:bool = False,
@ -116,36 +161,20 @@ def read_item(
low_tariff_hours_parsed = [int(x.strip()) for x in low_tariff_hours.split(",")]
monthly_fees = (monthly_fees + daily_fees * days_in_month(date.year, date.month))
monthly_fees_hour = monthly_fees / days_in_month(date.year, date.month) / 24
currency_ratio = get_eur_czk_ratio(date, no_cache=no_cache)
spot_hours = {}
spot_hours_for_sell = {}
spot_hours_total = {}
try:
spot_data = get_energy_prices(date, no_cache=no_cache)
spot_prices = get_spot_prices(date, hour, kwh_fees_low, kwh_fees_high, sell_fees, VAT, low_tariff_hours_parsed, no_cache=no_cache)
except PriceNotFound:
raise HTTPException(status_code=404, detail="prices not found")
for key, value in spot_data.items():
kwh_fees = kwh_fees_low if int(key) in low_tariff_hours_parsed else kwh_fees_high
raise HTTPException(status_code=404, detail="prices not found")
spot_hours[key] = value * currency_ratio / 1000
spot_hours_total[key] = (value * currency_ratio / 1000 + kwh_fees) * VAT
spot_hours_for_sell[key] = value * currency_ratio / 1000 - sell_fees
spot = Price(hours=spot_hours, now=spot_hours[str(hour)] if is_today else None)
spot_hours_total_sorted = {k: v for k, v in sorted(spot_hours_total.items(), key=lambda item: item[1])}
spot_total = Price(hours=spot_hours_total_sorted, now=spot_hours_total[str(hour)] if is_today else None)
spot_for_sell = Price(hours=spot_hours_for_sell, now=spot_hours_for_sell[str(hour)] if is_today else None)
cheapest_hours = [int(k) for k, v in list(spot_hours_total_sorted.items())[0:num_cheapest_hours]]
most_expensive_hours = [int(k) for k, v in list(reversed(spot_hours_total_sorted.items()))[0:num_most_expensive_hours]]
cheapest_hours = [int(k) for k, v in list(spot_prices.spot_hours_total_sorted.hours.items())[0:num_cheapest_hours]]
most_expensive_hours = [int(k) for k, v in list(reversed(spot_prices.spot_hours_total_sorted.hours.items()))[0:num_most_expensive_hours]]
# Average over four cheapest hours and calculation of all hours that are in this average +20 %
four_cheapest_hours = [v for k, v in list(spot_hours_total_sorted.items())[0:average_hours]]
four_cheapest_hours = [v for k, v in list(spot_prices.spot_hours_total_sorted.hours.items())[0:average_hours]]
four_cheapest_hours_average = sum(four_cheapest_hours) / average_hours
cheapest_hours_by_average = [int(k) for k, v in list(spot_hours_total_sorted.items()) if v < four_cheapest_hours_average * average_hours_threshold]
most_expensive_hours_by_average = set(range(24)) - set(cheapest_hours_by_average)
cheapest_hours_by_average = [int(k) for k, v in list(spot_prices.spot_hours_total_sorted.hours.items()) if v < four_cheapest_hours_average * average_hours_threshold]
most_expensive_hours_by_average = list(set(range(24)) - set(cheapest_hours_by_average))
data = DayPrice(
monthly_fees=monthly_fees * VAT,
@ -156,9 +185,9 @@ def read_item(
low_tariff_hours=low_tariff_hours_parsed,
hour=hour,
vat=VAT,
spot=spot,
total=spot_total,
sell=spot_for_sell,
spot=spot_prices.spot,
total=spot_prices.spot_total,
sell=spot_prices.spot_for_sell,
cheapest_hours=CheapestHours(hours=cheapest_hours, is_cheapest=hour in cheapest_hours if is_today else None),
most_expensive_hours=MostExpensiveHours(hours=most_expensive_hours, is_the_most_expensive=hour in most_expensive_hours if is_today else None),
cheapest_hours_by_average=CheapestHours(hours=cheapest_hours_by_average, is_cheapest=hour in cheapest_hours_by_average if is_today else None),
@ -166,6 +195,29 @@ def read_item(
)
return data
@app.get("/battery/charging", description=docs_battery)
def battery_charging(
kwh_fees_low: float=1.35022,
kwh_fees_high: float=1.86567,
sell_fees: float=0.45,
battery_kwh_price: float=2.5,
low_tariff_hours:str="0,1,2,3,4,5,6,7,9,10,11,13,14,16,17,18,20,21,22,23",
no_cache:bool = False,
) -> BatteryChargingInfo:
low_tariff_hours_parsed = [int(x.strip()) for x in low_tariff_hours.split(",")]
info = battery_charging_info(
kwh_fees_low,
kwh_fees_high,
sell_fees,
VAT,
low_tariff_hours_parsed,
no_cache,
battery_kwh_price
)
return info
@app.get("/widget", response_class=HTMLResponse)
def get_widget():
with open("calculator/index.html", "r") as file:

View file

@ -1,4 +1,5 @@
from dataclasses import dataclass
from operator import is_
from typing import Dict, List, Optional
@ -38,7 +39,23 @@ class DayPrice:
cheapest_hours_by_average: CheapestHours
most_expensive_hours_by_average: MostExpensiveHours
@dataclass
class SpotPrices:
spot: Price
spot_hours_total_sorted: Price
spot_total: Price
spot_for_sell: Price
@dataclass
class BatteryChargingInfo:
diff: float # Difference between the cheapest and the most expensive hour
is_viable: bool # Is it viable to charge or discharge the battery today
charging_hours: List[int] # Hours when it is viable to charge the battery
is_charging_hour:bool # Is it viable to charge the battery in the current hour
discharging_hours: List[int] # Hours when it is viable to discharge the battery
is_discharging_hour:bool # Is it viable to discharge the battery in the current hour
total_price:Price