New D57 prices
This commit is contained in:
parent
11399bf21b
commit
b00617768b
4 changed files with 219 additions and 36 deletions
89
calculator/calc.py
Normal file
89
calculator/calc.py
Normal 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
|
||||
)
|
|
@ -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;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
||||
|
|
Loading…
Reference in a new issue