init
This commit is contained in:
parent
aadcd4a193
commit
a5b16136f0
185
.gitignore
vendored
Normal file
185
.gitignore
vendored
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
# Created by .ignore support plugin (hsz.mobi)
|
||||||
|
### Python template
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
env/
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*,cover
|
||||||
|
.hypothesis/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
target/
|
||||||
|
|
||||||
|
# IPython Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
.python-version
|
||||||
|
|
||||||
|
# celery beat schedule file
|
||||||
|
celerybeat-schedule
|
||||||
|
|
||||||
|
# dotenv
|
||||||
|
.env
|
||||||
|
|
||||||
|
# virtualenv
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
### VirtualEnv template
|
||||||
|
# Virtualenv
|
||||||
|
# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/
|
||||||
|
[Bb]in
|
||||||
|
[Ii]nclude
|
||||||
|
[Ll]ib
|
||||||
|
[Ll]ib64
|
||||||
|
[Ll]ocal
|
||||||
|
[Ss]cripts
|
||||||
|
pyvenv.cfg
|
||||||
|
.venv
|
||||||
|
pip-selfcheck.json
|
||||||
|
|
||||||
|
### JetBrains template
|
||||||
|
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||||
|
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||||
|
|
||||||
|
# User-specific stuff
|
||||||
|
.idea/**/workspace.xml
|
||||||
|
.idea/**/tasks.xml
|
||||||
|
.idea/**/usage.statistics.xml
|
||||||
|
.idea/**/dictionaries
|
||||||
|
.idea/**/shelf
|
||||||
|
|
||||||
|
# AWS User-specific
|
||||||
|
.idea/**/aws.xml
|
||||||
|
|
||||||
|
# Generated files
|
||||||
|
.idea/**/contentModel.xml
|
||||||
|
|
||||||
|
# Sensitive or high-churn files
|
||||||
|
.idea/**/dataSources/
|
||||||
|
.idea/**/dataSources.ids
|
||||||
|
.idea/**/dataSources.local.xml
|
||||||
|
.idea/**/sqlDataSources.xml
|
||||||
|
.idea/**/dynamic.xml
|
||||||
|
.idea/**/uiDesigner.xml
|
||||||
|
.idea/**/dbnavigator.xml
|
||||||
|
|
||||||
|
# Gradle
|
||||||
|
.idea/**/gradle.xml
|
||||||
|
.idea/**/libraries
|
||||||
|
|
||||||
|
# Gradle and Maven with auto-import
|
||||||
|
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||||
|
# since they will be recreated, and may cause churn. Uncomment if using
|
||||||
|
# auto-import.
|
||||||
|
# .idea/artifacts
|
||||||
|
# .idea/compiler.xml
|
||||||
|
# .idea/jarRepositories.xml
|
||||||
|
# .idea/modules.xml
|
||||||
|
# .idea/*.iml
|
||||||
|
# .idea/modules
|
||||||
|
# *.iml
|
||||||
|
# *.ipr
|
||||||
|
|
||||||
|
# CMake
|
||||||
|
cmake-build-*/
|
||||||
|
|
||||||
|
# Mongo Explorer plugin
|
||||||
|
.idea/**/mongoSettings.xml
|
||||||
|
|
||||||
|
# File-based project format
|
||||||
|
*.iws
|
||||||
|
|
||||||
|
# IntelliJ
|
||||||
|
out/
|
||||||
|
|
||||||
|
# mpeltonen/sbt-idea plugin
|
||||||
|
.idea_modules/
|
||||||
|
|
||||||
|
# JIRA plugin
|
||||||
|
atlassian-ide-plugin.xml
|
||||||
|
|
||||||
|
# Cursive Clojure plugin
|
||||||
|
.idea/replstate.xml
|
||||||
|
|
||||||
|
# SonarLint plugin
|
||||||
|
.idea/sonarlint/
|
||||||
|
|
||||||
|
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||||
|
com_crashlytics_export_strings.xml
|
||||||
|
crashlytics.properties
|
||||||
|
crashlytics-build.properties
|
||||||
|
fabric.properties
|
||||||
|
|
||||||
|
# Editor-based Rest Client
|
||||||
|
.idea/httpRequests
|
||||||
|
|
||||||
|
# Android studio 3.1+ serialized cache file
|
||||||
|
.idea/caches/build_file_checksums.ser
|
||||||
|
|
||||||
|
# idea folder, uncomment if you don't need it
|
||||||
|
# .idea
|
||||||
0
EnergyPriceProvider/PstrykBuyProvider.py
Normal file
0
EnergyPriceProvider/PstrykBuyProvider.py
Normal file
@ -1,178 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
from datetime import datetime, timedelta, timezone, date
|
|
||||||
from typing import Dict, List, Tuple, Optional
|
|
||||||
|
|
||||||
import psycopg
|
|
||||||
from psycopg.rows import dict_row
|
|
||||||
|
|
||||||
from EnergyPrice import EnergyPriceBase
|
|
||||||
from utils.time import WARSAW_TZ
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class RDNProviderPG(EnergyPriceBase):
|
|
||||||
"""
|
|
||||||
Odczyt stawek RDN z tabeli pricing.energy_prices w Postgresie.
|
|
||||||
|
|
||||||
- rate(ts) -> PLN/kWh (netto) dla znacznika czasu.
|
|
||||||
- Korzysta z kolumny ts_range (tstzrange, '[)'), więc obsługuje dowolne podziały doby.
|
|
||||||
- Caching per-dzień (w czasie lokalnym Europe/Warsaw) dla wydajności.
|
|
||||||
"""
|
|
||||||
dsn: str
|
|
||||||
provider: str
|
|
||||||
kind: str
|
|
||||||
side: str # 'buy' | 'sell'
|
|
||||||
table: str = "pricing.energy_prices"
|
|
||||||
_conn: Optional[psycopg.Connection] = field(default=None, init=False, repr=False)
|
|
||||||
_day_cache: Dict[date, List[Tuple[datetime, datetime, float]]] = field(default_factory=dict, init=False, repr=False)
|
|
||||||
|
|
||||||
# --- Połączenie ----------------------------------------------------------
|
|
||||||
def _connect(self) -> psycopg.Connection:
|
|
||||||
if self._conn is None or self._conn.closed:
|
|
||||||
self._conn = psycopg.connect(self.dsn, row_factory=dict_row)
|
|
||||||
return self._conn
|
|
||||||
|
|
||||||
def close(self) -> None:
|
|
||||||
if self._conn and not self._conn.closed:
|
|
||||||
self._conn.close()
|
|
||||||
self._conn = None
|
|
||||||
self._day_cache.clear()
|
|
||||||
|
|
||||||
# --- Pobranie i cache jednego dnia --------------------------------------
|
|
||||||
def _day_bounds_local(self, d: date) -> Tuple[datetime, datetime]:
|
|
||||||
start_local = datetime(d.year, d.month, d.day, 0, 0, tzinfo=self.tz)
|
|
||||||
end_local = start_local + timedelta(days=1)
|
|
||||||
return start_local, end_local
|
|
||||||
|
|
||||||
def _load_day(self, d: date) -> List[Tuple[datetime, datetime, float]]:
|
|
||||||
"""
|
|
||||||
Zwraca listę interwałów [(start_utc, end_utc, price_net), ...] dla dnia d (lokalnie).
|
|
||||||
Wynik sortowany po starcie; przycina dobie lokalnej po stronie SQL.
|
|
||||||
"""
|
|
||||||
if d in self._day_cache:
|
|
||||||
return self._day_cache[d]
|
|
||||||
|
|
||||||
start_local, end_local = self._day_bounds_local(d)
|
|
||||||
start_utc = start_local.astimezone(timezone.utc)
|
|
||||||
end_utc = end_local.astimezone(timezone.utc)
|
|
||||||
|
|
||||||
sql = f"""
|
|
||||||
SELECT ts_start, ts_end, price_pln_net
|
|
||||||
FROM {self.table}
|
|
||||||
WHERE provider = %s AND kind = %s AND side = %s
|
|
||||||
AND ts_range && tstzrange(%s, %s, '[)')
|
|
||||||
ORDER BY ts_start;
|
|
||||||
"""
|
|
||||||
with self._connect().cursor() as cur:
|
|
||||||
cur.execute(sql, (self.provider, self.kind, self.side, start_utc, end_utc))
|
|
||||||
rows = cur.fetchall()
|
|
||||||
|
|
||||||
# Clip do granic doby lokalnej i zapisz do cache
|
|
||||||
out: List[Tuple[datetime, datetime, float]] = []
|
|
||||||
for r in rows:
|
|
||||||
s = max(r["ts_start"], start_utc)
|
|
||||||
e = min(r["ts_end"], end_utc)
|
|
||||||
if e > s:
|
|
||||||
out.append((s, e, float(r["price_pln_net"])))
|
|
||||||
|
|
||||||
# Porządek i scalanie nakładających się fragmentów o tej samej cenie
|
|
||||||
out.sort(key=lambda t: t[0])
|
|
||||||
merged: List[Tuple[datetime, datetime, float]] = []
|
|
||||||
for s, e, p in out:
|
|
||||||
if not merged:
|
|
||||||
merged.append((s, e, p))
|
|
||||||
continue
|
|
||||||
ps, pe, pp = merged[-1]
|
|
||||||
# jeśli overlap i ta sama cena -> łączymy
|
|
||||||
if s <= pe and p == pp:
|
|
||||||
merged[-1] = (ps, max(pe, e), pp)
|
|
||||||
else:
|
|
||||||
# jeśli overlap, ale inna cena — rozcinamy granicę (zachowujemy kolejność)
|
|
||||||
if s < pe and p != pp:
|
|
||||||
s = pe
|
|
||||||
if s >= e:
|
|
||||||
continue
|
|
||||||
merged.append((s, e, p))
|
|
||||||
|
|
||||||
self._day_cache[d] = merged
|
|
||||||
return merged
|
|
||||||
|
|
||||||
# --- Public API: jedna stawka w danym ts --------------------------------
|
|
||||||
def rate(self, ts: datetime) -> float:
|
|
||||||
"""
|
|
||||||
Zwraca PLN/kWh (netto) dla timestampa ts.
|
|
||||||
Rzuca KeyError, jeśli w dobie brakuje pokrycia interwałami lub ts wpada w lukę.
|
|
||||||
"""
|
|
||||||
ts_local = self.to_local_dt(ts)
|
|
||||||
d = ts_local.date()
|
|
||||||
intervals = self._load_day(d)
|
|
||||||
if not intervals:
|
|
||||||
raise KeyError(f"Brak danych cenowych dla {d.isoformat()} ({self.provider}/{self.kind}/{self.side})")
|
|
||||||
|
|
||||||
ts_utc = ts_local.astimezone(timezone.utc)
|
|
||||||
# znajdź interwał ts_start <= ts < ts_end
|
|
||||||
# (lista jest posortowana)
|
|
||||||
lo, hi = 0, len(intervals) - 1
|
|
||||||
while lo <= hi:
|
|
||||||
mid = (lo + hi) // 2
|
|
||||||
s, e, p = intervals[mid]
|
|
||||||
if ts_utc < s:
|
|
||||||
hi = mid - 1
|
|
||||||
elif ts_utc >= e:
|
|
||||||
lo = mid + 1
|
|
||||||
else:
|
|
||||||
return p
|
|
||||||
raise KeyError(f"Brak ceny dla {ts_local.isoformat()} (luka w dobie {d.isoformat()})")
|
|
||||||
|
|
||||||
# --- Dodatkowe: eksport do HH:MM->price (np. do debug/raportów) ----------
|
|
||||||
def day_schedule_local(self, d: date) -> List[Tuple[str, str, float]]:
|
|
||||||
"""
|
|
||||||
Zwraca listę [(from_HHMM, to_HHMM, price)] w CZASIE LOKALNYM,
|
|
||||||
przyciętą do pełnej doby lokalnej. Ostatni 'to' = '00:00'.
|
|
||||||
"""
|
|
||||||
start_local, end_local = self._day_bounds_local(d)
|
|
||||||
intervals = self._load_day(d)
|
|
||||||
if not intervals:
|
|
||||||
return []
|
|
||||||
|
|
||||||
# przekładamy na lokalny czas
|
|
||||||
parts: List[Tuple[datetime, datetime, float]] = []
|
|
||||||
for s, e, p in intervals:
|
|
||||||
sl = s.astimezone(self.tz)
|
|
||||||
el = e.astimezone(self.tz)
|
|
||||||
# przycięcie na wszelki wypadek
|
|
||||||
sl = max(sl, start_local)
|
|
||||||
el = min(el, end_local)
|
|
||||||
if el > sl:
|
|
||||||
parts.append((sl, el, p))
|
|
||||||
|
|
||||||
# łączenie sąsiadów z tą samą ceną
|
|
||||||
parts.sort(key=lambda x: x[0])
|
|
||||||
merged: List[Tuple[datetime, datetime, float]] = []
|
|
||||||
for s, e, p in parts:
|
|
||||||
if merged and merged[-1][1] == s and merged[-1][2] == p:
|
|
||||||
merged[-1] = (merged[-1][0], e, p)
|
|
||||||
else:
|
|
||||||
merged.append((s, e, p))
|
|
||||||
|
|
||||||
# upewnij się, że start=00:00, koniec=00:00
|
|
||||||
if merged and merged[0][0] != start_local:
|
|
||||||
if merged[0][0] > start_local:
|
|
||||||
# luka na początku doby
|
|
||||||
raise KeyError(f"Luka na początku doby {d}")
|
|
||||||
merged[0] = (start_local, merged[0][1], merged[0][2])
|
|
||||||
if merged and merged[-1][1] != end_local:
|
|
||||||
if merged[-1][1] < end_local:
|
|
||||||
# luka na końcu doby
|
|
||||||
raise KeyError(f"Luka na końcu doby {d}")
|
|
||||||
merged[-1] = (merged[-1][0], end_local, merged[-1][2])
|
|
||||||
|
|
||||||
def hhmm(x: datetime) -> str:
|
|
||||||
return x.strftime("%H:%M")
|
|
||||||
|
|
||||||
out: List[Tuple[str, str, float]] = []
|
|
||||||
for i, (s, e, p) in enumerate(merged):
|
|
||||||
out.append((hhmm(s), "00:00" if i == len(merged) - 1 else hhmm(e), p))
|
|
||||||
return out
|
|
||||||
6
requirements.txt
Normal file
6
requirements.txt
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
psycopg[binary]>=3.1,<3.3
|
||||||
|
pandas~=2.3.2
|
||||||
|
requests>=2.32,<3.0
|
||||||
|
apscheduler>=3.10,<4.0
|
||||||
|
|
||||||
|
systemd-python>=235; sys_platform != "win32"
|
||||||
Loading…
Reference in New Issue
Block a user