import datetime import calendar from typing import List, Optional 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 BatteryChargingInfo, CheapestHours, DayPrice, MostExpensiveHours, Price from .consts import VAT def days_in_month(year: int, month: int) -> int: return calendar.monthrange(year, month)[1] app = FastAPI(title="Spot market home calculator", version="0.1", description="Calculate your energy costs based on spot market prices") app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) @app.get("/", description="Redirect to /docs") def docs(): return RedirectResponse(url="/docs") docs = """ Return spot prices for the whole day with all fees included.

**Options:**
**date** - date in format YYYY-MM-DD, default is today
**hour** - hour of the day, default is current hour, works only when date is today
**monthly_fees** - monthly fees, default is 509.24 (D57d, BezDodavatele)
**daily_fees** - daily fees, default is 4.18 (BezDodavatele)
**kwh_fees_low** - additional fees per kWh in low tariff, usually distribution + other fees, default is 1.62421 (D57d, BezDodavatele)
**kwh_fees_high** - additional fees per kWh in high tariff, usually distribution + other fees, default is 1.83474 (D57d, BezDodavatele)
**sell_fees** - selling energy fees, default is 0.45 (BezDodavatele)
**num_cheapest_hours** - number of cheapest hours to return, default is 8, use this to plan your consumption
**num_most_expensive_hours** - number of the most expensive hours to return, default is 8, use this to plan your consumption
**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)
**average_hours** - used for calculation of cheapest hours based on average price of first few hours, this sets number of cheapest hours used to calculate number that is a base number for the calculation
**average_hours_threshold** - used for calculation of average price, cheapest hours will be all hours with price lower than `average_price * threshold`

Output:
**spot** - current spot prices on our market
**total** - total price for the day including all fees and VAT
**sell** - current spot prices minus sell_fees
**cheapest_hours** - 8 (or configured) cheapest hours in the day
**most_expensive_hours** - 8 (or configured) the most expensive hours in the day
**cheapest_hours_by_average** - cheapest hours based on average_hours and average_hours_threshold
**most_expensive_hours_by_average** - the most expensive hours based on average_hours and average_hours_threshold

The final price on the invoice is calculated as:
monthly_fees + kWh consumption in low tariff * kwh_fees_low + kWh consumption in high tariff * kwh_fees_high

Except spot and sell prices all prices include VAT.


Integration into Home Assistant can be done in configuration.yaml file:

``` rest: - resource: https://pricepower2.rostiapp.cz/price/day scan_interval: 60 sensor: - name: "energy_price" value_template: "{{ value_json.total.now|round(2) }}" unit_of_measurement: "CZK/kWh" - name: "energy_market_price" value_template: "{{ value_json.spot.now|round(2) }}" unit_of_measurement: "CZK/kWh" - name: "energy_market_price_sell" value_template: "{{ value_json.sell.now|round(2) }}" unit_of_measurement: "CZK/kWh" binary_sensor: - name: "energy_cheap_hour" value_template: "{{ value_json.cheapest_hours.is_cheapest }}" - name: "energy_expensive_hour" value_template: "{{ value_json.most_expensive_hours.is_the_most_expensive }}" - name: "energy_expensive_hour_by_average" value_template: "{{ value_json.most_expensive_hours_by_average.is_the_most_expensive }}" - name: "energy_cheap_hour_by_average" value_template: "{{ value_json.cheapest_hours_by_average.is_cheapest }}"" ``` """ 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.

**Options:**
**kwh_fees_low** - additional fees per kWh in low tariff, usually distribution + other fees, default is 1.62421 (D57d, BezDodavatele)
**kwh_fees_high** - additional fees per kWh in high tariff, usually distribution + other fees, default is 1.83474 (D57d, BezDodavatele)
**sell_fees** - selling energy fees, default is 0.45 (BezDodavatele), not important for this endpoint
**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)
**battery_kwh_price** - price from which it is viable to charge and discharge the battery, default is 2.5

Output:
**diff** - Difference between the most expensive cheapest hour and the most expensive hour
**is_viable** - True if it is viable to charge or discharge the battery today
**charging_hours** - Hours when it is viable to charge the battery
**is_charging_hour** - True if it is viable to charge the battery in the current hour
**discharging_hours** - Hours when it is viable to discharge the battery
**is_discharging_hour** - True if it is viable to discharge the battery in the current hour
**total_price** - total price for the day including all fees and VAT




Integration into Home Assistant can be done in configuration.yaml file:

``` 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=610.84, daily_fees: float=4.18, 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, num_cheapest_hours:int = 8, num_most_expensive_hours:int = 8, average_hours:int=4, average_hours_threshold:float=1.25, ) -> DayPrice: if not date: date = datetime.date.today() if not hour: hour = datetime.datetime.now().hour is_today = datetime.date.today() == date 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 try: 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") 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_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_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, monthly_fees_hour=monthly_fees_hour * VAT, kwh_fees_low=kwh_fees_low * VAT, kwh_fees_high=kwh_fees_high * VAT, sell_fees=sell_fees, low_tariff_hours=low_tariff_hours_parsed, hour=hour, vat=VAT, 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), most_expensive_hours_by_average=MostExpensiveHours(hours=most_expensive_hours_by_average, is_the_most_expensive=hour in most_expensive_hours_by_average if is_today else None), ) 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: html_content = file.read() return HTMLResponse(content=html_content)