Allow using CSV and Chart API

This commit is contained in:
Michał Leśniewski
2023-05-22 21:18:31 +02:00
committed by Michał Leśniewski
parent 3c5ce038a6
commit 30b39739a6
4 changed files with 97 additions and 21 deletions

View File

@@ -17,22 +17,23 @@ $ pip3 install elicznik
With the package installed readings can be retrieved by simply running the `elicznik` command:
```
usage: elicznik [-h] [--format {raw,table,csv}] username password [start date] [end date]
usage: elicznik [-h] [--format {table,csv}] [--api {chart,csv}] username password [start_date] [end_date]
positional arguments:
username tauron-dystrybucja.pl user name
password tauron-dystrybucja.pl password
start date Start date of date range to be retrieved, in ISO8601 format. If the end date is omitted, it's the only date for which
start_date Start date of date range to be retrieved, in ISO8601 format. If the end date is omitted, it's the only date for which
measurements are retrieved.
end date End date of date range to be retrieved, inclusive, in ISO8601 format. Can be omitted to only retrieve a single day's
end_date End date of date range to be retrieved, inclusive, in ISO8601 format. Can be omitted to only retrieve a single day's
measurements.
optional arguments:
options:
-h, --help show this help message and exit
--format {raw,table,csv}
Specify the output format
--format {table,csv} Specify the output format
--api {chart,csv} Specify which Tauron API to use to get the measurements.
```
### Example
```
@@ -92,6 +93,32 @@ with elicznik.ELicznik("freddy@example.com", "secretpassword") as m:
```
## Notes on APIs and the `--api` command line switch
Tauron exposes two API endpoints for retrieving meter readings -- one for downloading CSV (and XLS) data,
the other is a back-end supporting the charts in the Web UI. In theory, both endpoints are equivalent.
They can be used to get exactly the same data. In practice, the endpoint for downloading CSV data seems
more stable -- in contrast to the chart one, which changed a few times in the past. The CSV endpoint is
also more robust and allows downloading more data with fewer requests.
This project supports fetching data from both. CSV is the default and recommended one, but it's possible
to switch to the chart API endpoint in case of problems.
This can be done by adding `--api=chart` on the command line:
```
$ elicznik --api=chart freddy@example.com secretpassword 2021-07-10
```
Both APIs can also be used explicitly from code. The `elicznik` module defines two classes for that `ELicznikChart`
and `ELicznikCSV`. `ELicznik` is just an alias for `ELicznikCSV`:
```
import elicznik
with elicznik.ELicznikChart("freddy@example.com", "secretpassword") as m:
...
```
## TODO & bugs
* Add support for accounts with multiple meters

View File

@@ -1 +1 @@
from .elicznik import ELicznik
from .elicznik import ELicznik, ELicznikChart, ELicznikCSV

View File

@@ -5,7 +5,7 @@ import sys
import tabulate
from .elicznik import ELicznik
from .elicznik import ELicznikChart, ELicznikCSV
def main():
@@ -16,6 +16,12 @@ def main():
default="table",
help="Specify the output format",
)
parser.add_argument(
"--api",
choices=["chart", "csv"],
default="csv",
help="Specify which Tauron API to use to get the measurements. "
)
parser.add_argument("username", help="tauron-dystrybucja.pl user name")
parser.add_argument("password", help="tauron-dystrybucja.pl password")
parser.add_argument(
@@ -39,7 +45,9 @@ def main():
args = parser.parse_args()
with ELicznik(args.username, args.password) as elicznik:
elicznik_class = ELicznikCSV if args.api == "csv" else ELicznikChart
with elicznik_class(args.username, args.password) as elicznik:
result = elicznik.get_readings(args.start_date, args.end_date)
if args.format == "table":

View File

@@ -5,10 +5,8 @@ import datetime
from .session import Session
class ELicznik:
class ELicznikBase:
LOGIN_URL = "https://logowanie.tauron-dystrybucja.pl/login"
DATA_URL = "https://elicznik.tauron-dystrybucja.pl/energia/do/dane"
def __init__(self, username, password):
self.username = username
@@ -33,7 +31,48 @@ class ELicznik:
def __exit__(self, exc_type, exc_val, exc_tb):
pass
def get_raw_data(self, start_date, end_date=None):
class ELicznikChart(ELicznikBase):
CHART_URL = "https://elicznik.tauron-dystrybucja.pl/energia/api"
def _get_raw_daily_readings(self, type_, date):
data = self.session.post(
self.CHART_URL,
data={
"type": type_,
"from": date.strftime("%d.%m.%Y"),
"to": date.strftime("%d.%m.%Y"),
"profile": "full time",
},
).json().get("data", {}).get("values", [])
return ((datetime.datetime.combine(date, datetime.time(h)), value) for h, value in enumerate(data))
def _get_raw_readings(self, type_, start_date, end_date=None):
end_date = end_date or start_date
while start_date <= end_date:
yield from self._get_raw_daily_readings(type_, start_date)
start_date += datetime.timedelta(days=1)
def get_readings_production(self, start_date, end_date=None):
return dict(self._get_raw_readings("oze", start_date, end_date))
def get_readings_consumption(self, start_date, end_date=None):
return dict(self._get_raw_readings("consum", start_date, end_date))
def get_readings(self, start_date, end_date=None):
consumed = self.get_readings_consumption(start_date, end_date)
produced = self.get_readings_production(start_date, end_date)
return sorted(
(timestamp, consumed.get(timestamp), produced.get(timestamp))
for timestamp in set(consumed) | set(produced)
)
class ELicznikCSV(ELicznikBase):
DATA_URL = "https://elicznik.tauron-dystrybucja.pl/energia/do/dane"
def _get_raw_data(self, start_date, end_date=None):
end_date = end_date or start_date
return self.session.post(
self.DATA_URL,
@@ -48,20 +87,18 @@ class ELicznik:
).text.splitlines()
@staticmethod
def parse_timestamp(timespec):
def _parse_timestamp(timespec):
date, time = timespec.split(None, 1)
hour = int(time.split(":")[0]) - 1
return datetime.datetime.strptime(date, "%Y-%m-%d") + datetime.timedelta(
hours=hour
)
return datetime.datetime.strptime(date, "%Y-%m-%d") + datetime.timedelta(hours=hour)
def get_readings(self, start_date, end_date=None):
end_date = end_date or start_date
data = self.get_raw_data(start_date, end_date)
data = self._get_raw_data(start_date, end_date)
records = [
{
"timestamp": self.parse_timestamp(rec["Data"]),
"timestamp": self._parse_timestamp(rec["Data"]),
"value": float(rec[" Wartość kWh"].replace(",", ".")),
"type": rec["Rodzaj"],
}
@@ -75,12 +112,14 @@ class ELicznik:
]
prod = {
rec["timestamp"]: rec["value"] for rec in records if rec["type"] == "pobór"
rec["timestamp"]: rec["value"]
for rec in records
if rec["type"] == "oddanie"
}
cons = {
rec["timestamp"]: rec["value"]
for rec in records
if rec["type"] == "oddanie"
if rec["type"] == "pobór"
}
# TODO
@@ -90,3 +129,5 @@ class ELicznik:
(timestamp, cons.get(timestamp), prod.get(timestamp))
for timestamp in set(cons) | set(prod)
)
ELicznik = ELicznikCSV