diff --git a/.gitignore b/.gitignore index 6e08080..a0bf7db 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ src/*.sqlite src/config.ini src/rund.sh +src.old/* import.py check.py *.sqlite diff --git a/src/CHANGELOG.md b/src/CHANGELOG.md index 9f4bb06..90a2681 100644 --- a/src/CHANGELOG.md +++ b/src/CHANGELOG.md @@ -1,4 +1,18 @@ -## v0.1.0 [2023-10-21] -- Wersja beta +# EnergaMeter +## v1.0.0 [2023-11-06] +- Pierwsza wersja stabilna +- Zmieniono strukturę danych API (wymagana aktualizacja configuration.yaml oraz Grafana do nowego formatu danych) +## v0.1.8 [2023-11-04] +- Dodano możliwość odwrócenia znaku wartości pomiaru +## v0.1.4 - v0.1.7 [2023-11-03] +- Drobne poprawki +## v0.1.3 [2023-10-31] +- Dodano obsługę liczników wytwórcy (Uwaga: zmiana struktury JSON) +## v0.1.2 [2023-10-23] +- Dodano obsługę trybu serwisowego aplikacji Mój Licznik +- Poprawiono logowanie błędów +- W przypadku problemów z logowaniem liczniki w aplikacji pozostają aktywne ## v0.1.1 [2023-10-22] -- Dodano obsługę błędnego logowania \ No newline at end of file +- Dodano obsługę błędnego logowania +## v0.1.0 [2023-10-21] +- Wersja beta \ No newline at end of file diff --git a/src/Dockerfile b/src/Dockerfile index a4fd9db..83b8523 100644 --- a/src/Dockerfile +++ b/src/Dockerfile @@ -6,12 +6,12 @@ RUN apk add --update py3-pip # Copy data for add-on COPY run.sh / +COPY run.py / COPY requirements.txt / COPY main.py / COPY api.py / -COPY cron.py / -# COPY database.sqlite / COPY moj_licznik.py / +COPY log_config.py / RUN chmod a+x /run.sh RUN pip install -r requirements.txt diff --git a/src/INSTALL.md b/src/INSTALL.md index 71d4159..581d684 100644 --- a/src/INSTALL.md +++ b/src/INSTALL.md @@ -1,6 +1,6 @@ [![ha_badge](https://img.shields.io/badge/Home%20Assistant-Add%20On-blue.svg)](https://www.home-assistant.io/) # [Energa meter](https://github.com/tcich/ha-addon-energa-meter) Home Assistant add-on -# Wersja podstawowa +# Wersja dev [aarch64-shield]: https://img.shields.io/badge/aarch64-yes-green.svg [amd64-shield]: https://img.shields.io/badge/amd64-yes-green.svg [armv6-shield]: https://img.shields.io/badge/armv6-yes-green.svg @@ -12,13 +12,19 @@ ![armv7-shield] ![i386-shield] -Postaw kawę +[kawa-logo]: https://github.com/tcich/ha-addon-energa-meter/blob/main/img/buycoffeeto-btn-primary-outline.png +[kawa]: https://buycoffee.to/tcich + +Postaw kawę ## O dodatku To jest dodatek dla [Home Assistant](https://www.home-assistant.io/). Instalacja dodatku [Energa meter](https://github.com/tcich/ha-addon-energa-meter) umożliwia cykliczne pobieranie danych z aplikacji [Mój Licznik - Energa](https://mojlicznik.energa-operator.pl) udostępnianej klientom Operatora energetycznego Energa. +### Wersja dev +Wersja dev jest wersją developeską, nie należy jej używać w produkcyjnej wersji HA, może powodować różne problemy, może nie działać. + ## Instalacja 1) Dodaj repozytorium do repozytoriów dodatków swojego HA za pomocą poniższego przycisku @@ -47,59 +53,53 @@ Wymagane parametry: ## Konfiguracja sensorów +Do HA możesz dodać sensory, które zawierają informacje udostępniane przez API +Poniższa instrukcja zawiera założenia: +* dodatek jest dostępny pod adresem *localhost* na porcie *8000* +* ID Twojego licznika to *123456789* + +1) Ustal ID Twoich liczników, w tym celu przejdź do adresu Twojego HA na porcie 8000 lub innym jeźeli zmieniłeś go w konfiguracji, np. http://192.168.1.10:8000 wyświetli się w formacie json lista dostępnych liczników. +2) W pliku configuration.yaml w HA dodaj następującą konfigurację np.: ``` sensor: - platform: rest - resource: http://localhost:8000/meters/12335379 - name: "Energia aktualna T1" - unique_id: 12335379_sumz1 + resource: http://localhost:8000/123456789/A%2B/1 + name: "A+ Taryfa 1" + unique_id: 123456789_apt1 unit_of_measurement: "kWh" - value_template: "{{ value_json.meter.zone1.meter | round(2) }}" + value_template: "{{ value_json.countner.meter_value | round(2) }}" - platform: rest - resource: http://localhost:8000/meters/12335379 - name: "Dzienny odczyt licznika" - unique_id: 12335379_meterz1 + resource: http://localhost:8000/123456789/A%2B/2 + name: "A+ Taryfa 2" + unique_id: 123456789_apt2 unit_of_measurement: "kWh" - value_template: "{{ value_json.meter.zone1.sum | round(2) }}" - - platform: rest - resource: http://localhost:8000/meters/12335379 - name: "Energia aktualna T2" - unique_id: 12335379_sumz2 - unit_of_measurement: "kWh" - value_template: "{{ value_json.meter.zone2.meter | round(2) }}" - - platform: rest - resource: http://localhost:8000/meters/12335379 - name: "Dzienny odczyt licznika" - unique_id: 12335379_meterz2 - unit_of_measurement: "kWh" - value_template: "{{ value_json.meter.zone2.sum | round(2) }}" + value_template: "{{ value_json.countner.meter_value | round(2) }}" ``` -# Opis konfiguracji +### Opis konfiguracji | element konfiguracji | Opis | |-------------------|-------------------| -| resource: http://localhost:8000/meters/12335379 | Adres API z danymi konkretnego licznika, podajemy nazwę instancji dockera (**Nazwa hosta** z okna dodatku) lub localhost| -| name: "Energia aktualna" | Nazwa sensora, wpisz dowolną| +| resource: http://localhost:8000/123456789/A%2B/1 | Adres API z danymi konkretnego licznika, podajemy **localhost** lub nazwę instancji dockera (**Nazwa hosta** z okna dodatku), port, id licznika, rodzaj pomiaru, taryfa| +| name: "A+ Taryfa 1" | Nazwa sensora, wpisz dowolną| | unique_id | Unikalny ID sensora, nie mogą być w systemie dwa sensory z tym samym ID| | unit_of_measurement: "kWh" | Jednostka miary, nie zmieniaj chyba, że wiesz co robisz| -| value_template: "{{ value_json.meter.zone2.meter \| round(2) }}" | Zaokrąglony do dwóch miejsc po przecinku stan sensora| - -# Opis konfiguracji cd -| value_template | Opis | -|-------------------|-------------------| -| value_json.meter.zone1.sum | Suma licznika oraz dziennego zużycia dla tartfy1 (dostępne są: zone1, zone2, zone3)| -| value_json.meter.zone2.meter | Stan licznika dziennego dla taryfy1 (dostępne są: zone1, zone2, zone3)| - +| value_template: "{{ value_json.meter.countner.meter_value \| round(2) }}" | Zaokrąglony do dwóch miejsc po przecinku stan sensora| ## API dla wykresów, np. Grafana -Aby pobrać dane z API w formacie JSON należy użyć adresu http://home_assistant:8000/charts/12729?start_date=1695332400129&end_date=1697924583285 +Aby pobrać dane z API w formacie JSON należy użyć adresu http://home_assistant:8000/charts/12729?start_date=1695332400129&end_date=1697924583285&mp=123456789&zone=1 -gdzie: -* 12729 - jest to ID licznika -* start_date - początek okresu w milisekundach wg. standardu EPOCH (timestamp) -* end_date - koniec okresu w milisekundach wg. standardu EPOCH (timestamp) +### Opis konfiguracji +| element konfiguracji | Opis | +|-------------------|-------------------| +| resource: http://localhost:8000/charts | Adres API z danymi do wykresów, podajemy **localhost** lub nazwę instancji dockera (**Nazwa hosta** z okna dodatku), port, id licznika, rodzaj pomiaru, taryfa| +| start_date | data początkowa danych w formacie epoch (ms), domyślnie czas bieżący | +| end_date | data końcowa danych w formacie epoch (ms), domyślnie czas bieżący - 1 dzień | +| mp | numer licznika | +| meter_type_url | typ licznika (np. A+: A%2B, A-: A- ) | +| zone | numer strefy (np. 1, 2) | +| negative | dodanie parametru z dowolną wartością powoduje, że pomiar jest wartością ujemną| ## Jak dodać wykres do Grafana ### Źródło danych @@ -118,12 +118,13 @@ gdzie: 1) Przechodzimy do Dashboards 2) Klikamy New -> New dashboard -> Add visualization 3) Wskazujemy Data source: ENERGA -4) W **Path** wpisujemy: GET: /charts/12335379 (id Twojego licznika) +4) W **Path** wpisujemy: GET: /charts + + (id Twojego licznika) 5) W **Fields** wpisujemy \$.charts\[\*\].czas typu Time oraz $.charts[*].value typu number z aliasem kWh 5) W **Params** wpisujemy Key: start_date Value: $__from 6) W **Params** wpisujemy Key: end_date Value: $__to - Grafana Grafana @@ -134,10 +135,23 @@ gdzie: Grafana +7) W **Params** wpisujemy Key: meter_type_url Value: A+ lub A- jeżeli mamy energię pobieraną i oddawaną, w takim przypadku tworzymy również oddzielne **Query** dla A+ i A- + +Grafana + +8) W **Params** wpisujemy Key: negative Value: OK (może być dowolna wartość) jeżeli chcemy aby wykres był z wartościami ujemnymi (poniżej osi X) + +Grafana + +9) W **Params** aby uzyskać bilans energii należy dodać **Transform**, przy czym wartość energii oddawanej powinna być jako energia ujemna (parametr negative) + +Grafana + +Grafana ## Znane problemy -Czasami w aplikacji Mój Licznik włącza się captha (jeżeli masz dużo danych historycznych lub wielokrotnie instalujesz dodatek) -Dane wytwórcy (energia oddana oraz bilans) nie są dostępne, prace w tym zakresie trwają. +* Czasami w aplikacji Mój Licznik włącza się captha (jeżeli masz dużo danych historycznych lub wielokrotnie instalujesz dodatek) - należy poczekać kilka godzin, problem rozwiąże się sam + ## Uwagi Dostęp do aktualnej wersji API nie jest zabezpieczony tokenem diff --git a/src/README.md b/src/README.md index 337106a..4abf79b 100644 --- a/src/README.md +++ b/src/README.md @@ -1,6 +1,6 @@ [![ha_badge](https://img.shields.io/badge/Home%20Assistant-Add%20On-blue.svg)](https://www.home-assistant.io/) # [Energa meter](https://github.com/tcich/ha-addon-energa-meter) Home Assistant add-on -# Wersja podstawowa +# Wersja dev [aarch64-shield]: https://img.shields.io/badge/aarch64-yes-green.svg [amd64-shield]: https://img.shields.io/badge/amd64-yes-green.svg [armv6-shield]: https://img.shields.io/badge/armv6-yes-green.svg @@ -13,10 +13,17 @@ ![i386-shield] + +[kawa-logo]: https://github.com/tcich/ha-addon-energa-meter/blob/main/img/buycoffeeto-btn-primary-outline.png +[kawa]: https://buycoffee.to/tcich + ## O dodatku To jest dodatek dla [Home Assistant](https://www.home-assistant.io/). Instalacja dodatku [Energa meter](https://github.com/tcich/ha-addon-energa-meter) umożliwia cykliczne pobieranie danych z aplikacji [Mój Licznik - Energa](https://mojlicznik.energa-operator.pl) udostępnianej klientom Operatora energetycznego Energa. +### Wersja dev +Wersja dev jest wersją developeską, nie należy jej używać w produkcyjnej wersji HA, może powodować różne problemy, może nie działać. + ## Instalacja 1) Dodaj repozytorium do repozytoriów dodatków swojego HA za pomocą poniższego przycisku @@ -45,64 +52,72 @@ Wymagane parametry: ## Konfiguracja sensorów +Do HA możesz dodać sensory, które zawierają informacje udostępniane przez API +Poniższa instrukcja zawiera założenia: +* dodatek jest dostępny pod adresem *localhost* na porcie *8000* +* ID Twojego licznika to *123456789* + +1) Ustal ID Twoich liczników, w tym celu przejdź do adresu Twojego HA na porcie 8000 lub innym jeźeli zmieniłeś go w konfiguracji, np. http://192.168.1.10:8000 wyświetli się w formacie json lista dostępnych liczników. +2) W pliku configuration.yaml w HA dodaj następującą konfigurację np.: ``` sensor: - platform: rest - resource: http://localhost:8000/meters/12335379 - name: "Energia aktualna T1" - unique_id: 12335379_sumz1 + resource: http://localhost:8000/123456789/A%2B/1 + name: "A+ Taryfa 1" + unique_id: 123456789_apt1 unit_of_measurement: "kWh" - value_template: "{{ value_json.meter.zone1.meter | round(2) }}" + value_template: "{{ value_json.countner.meter_value | round(2) }}" - platform: rest - resource: http://localhost:8000/meters/12335379 - name: "Dzienny odczyt licznika" - unique_id: 12335379_meterz1 + resource: http://localhost:8000/123456789/A%2B/2 + name: "A+ Taryfa 2" + unique_id: 123456789_apt2 unit_of_measurement: "kWh" - value_template: "{{ value_json.meter.zone1.sum | round(2) }}" - - platform: rest - resource: http://localhost:8000/meters/12335379 - name: "Energia aktualna T2" - unique_id: 12335379_sumz2 - unit_of_measurement: "kWh" - value_template: "{{ value_json.meter.zone2.meter | round(2) }}" - - platform: rest - resource: http://localhost:8000/meters/12335379 - name: "Dzienny odczyt licznika" - unique_id: 12335379_meterz2 - unit_of_measurement: "kWh" - value_template: "{{ value_json.meter.zone2.sum | round(2) }}" + value_template: "{{ value_json.countner.meter_value | round(2) }}" ``` +## Suma liczników, bilans +W celu uzyskania sumy liczników, bilansu, itp należy użyć templates: +``` +template: + - sensor: + - name: "Suma liczników" + unit_of_measurement: "kWh" + state: "{{ states('sensor.123456789_apt1') | float + states('sensor.123456789_apt2') | float | round(2) }}" + - sensor: + - name: "Bilans/różnica liczników" + unit_of_measurement: "kWh" + state: "{{ states('sensor.123456789_apt1') | float - states('sensor.123456789_apt2') | float | round(2) }}" +``` -# Opis konfiguracji + +### Opis konfiguracji | element konfiguracji | Opis | |-------------------|-------------------| -| resource: http://localhost:8000/meters/12335379 | Adres API z danymi konkretnego licznika, podajemy nazwę instancji dockera (**Nazwa hosta** z okna dodatku) lub localhost| -| name: "Energia aktualna" | Nazwa sensora, wpisz dowolną| +| resource: http://localhost:8000/123456789/A%2B/1 | Adres API z danymi konkretnego licznika, podajemy **localhost** lub nazwę instancji dockera (**Nazwa hosta** z okna dodatku), port, id licznika, rodzaj pomiaru, taryfa| +| name: "A+ Taryfa 1" | Nazwa sensora, wpisz dowolną| | unique_id | Unikalny ID sensora, nie mogą być w systemie dwa sensory z tym samym ID| | unit_of_measurement: "kWh" | Jednostka miary, nie zmieniaj chyba, że wiesz co robisz| -| value_template: "{{ value_json.meter.zone2.meter \| round(2) }}" | Zaokrąglony do dwóch miejsc po przecinku stan sensora| - -# Opis konfiguracji cd -| value_template | Opis | -|-------------------|-------------------| -| value_json.meter.zone1.sum | Suma licznika oraz dziennego zużycia dla tartfy1 (dostępne są: zone1, zone2, zone3)| -| value_json.meter.zone2.meter | Stan licznika dziennego dla taryfy1 (dostępne są: zone1, zone2, zone3)| - +| value_template: "{{ value_json.meter.countner.meter_value \| round(2) }}" | Zaokrąglony do dwóch miejsc po przecinku stan sensora| ## API dla wykresów, np. Grafana -Aby pobrać dane z API w formacie JSON należy użyć adresu http://home_assistant:8000/charts/12729?start_date=1695332400129&end_date=1697924583285 +Aby pobrać dane z API w formacie JSON należy użyć adresu http://home_assistant:8000/charts/12729?start_date=1695332400129&end_date=1697924583285&mp=123456789&zone=1 + +### Opis konfiguracji +| element konfiguracji | Opis | +|-------------------|-------------------| +| resource: http://localhost:8000/charts | Adres API z danymi do wykresów, podajemy **localhost** lub nazwę instancji dockera (**Nazwa hosta** z okna dodatku), port, id licznika, rodzaj pomiaru, taryfa| +| start_date | data początkowa danych w formacie epoch (ms), domyślnie czas bieżący | +| end_date | data końcowa danych w formacie epoch (ms), domyślnie czas bieżący - 1 dzień | +| mp | numer licznika | +| meter_type_url | typ licznika (np. A+: A%2B, A-: A- ) | +| zone | numer strefy (np. 1, 2) | -gdzie: -* 12729 - jest to ID licznika -* start_date - początek okresu w milisekundach wg. standardu EPOCH (timestamp) -* end_date - koniec okresu w milisekundach wg. standardu EPOCH (timestamp) ## Grafana - Instrukcja konfiguracji dla Grafana znajduje się tutaj [link](https://github.com/tcich/ha-addon-energa-meter/tree/e702ed49436fb9e0b675dcac3001bd9de5aab3c0/src/INSTALL.md) + ## Znane problemy Czasami w aplikacji Mój Licznik włącza się captha (jeżeli masz dużo danych historycznych lub wielokrotnie instalujesz dodatek) Dane wytwórcy (energia oddana oraz bilans) nie są dostępne, prace w tym zakresie trwają. @@ -110,3 +125,4 @@ Dane wytwórcy (energia oddana oraz bilans) nie są dostępne, prace w tym zakre ## Uwagi Dostęp do aktualnej wersji API nie jest zabezpieczony tokenem Każde przeinstalowanie dodatku pobiera ponownie dane z aplikacji Mój Licznik + diff --git a/src/api.py b/src/api.py index 1074f89..522a136 100644 --- a/src/api.py +++ b/src/api.py @@ -1,37 +1,67 @@ from peewee import SqliteDatabase -from flask import Flask, jsonify, request, redirect, url_for +from flask import Flask, jsonify, request, redirect, url_for, abort from waitress import serve -import time -from datetime import datetime -from moj_licznik import PPETable, MainChartTable +#from datetime +import datetime +import time, os, logging +from moj_licznik import PPETable, MeterTable, CounterTable, MainChartTable +import urllib.parse -DEBUG = False +logger = logging.getLogger("energaMeter.api") + + +path = os.path.dirname(os.path.abspath(__file__)) +db_file = 'database.sqlite' +db = SqliteDatabase(os.path.join(path, db_file)) app = Flask(__name__) -db = SqliteDatabase('database.sqlite') - @app.route('/', methods=['GET']) -def root_redirect(): - query = PPETable.select().where(PPETable.is_active == True) +def root(): + query = PPETable.select() #.where(PPETable.is_active == True) result_ppes = list(query) - meters = [] - + ppes = [] for p in result_ppes: - meter = { - 'name': p.name, - 'id': p.id - } + meters_query = MeterTable.select().where(MeterTable.ppe_id == p.id) + meter_result = meters_query.execute() + meters = [] + for meter in meter_result: + countners_query = CounterTable.select().where(CounterTable.meter_id == meter.id) + countners_result = countners_query.execute() + countners = [] + for countner in countners_result: + countner = { + 'tariff': countner.tariff, + 'measurement_date': countner.measurement_date, + 'meter_value': countner.meter_value + } + countners.append(countner) - meters.append(meter) - if DEBUG: - print("API: GET /") + meter = { + 'meter_type': meter.meter_type, + 'meter_type_url': urllib.parse.quote_plus(meter.meter_type), + 'last_update_date': meter.last_update_date, + 'first_date': meter.first_date, + 'countners': countners + } + meters.append(meter) + + ppe = { + 'name': p.name, + 'id': p.id, + 'type': p.type, + 'isActive': p.is_active, + 'meters': meters + } + ppes.append(ppe) + logger.debug("API: GET /") - return jsonify({'meters': meters}) + return jsonify({'ppes': ppes}) @app.route('/meters', methods=['GET']) +@app.route('/meters/', methods=['GET']) def meters(): - query = PPETable.select().where(PPETable.is_active == True) + query = PPETable.select() #.where(PPETable.is_active == True) result_ppes = list(query) meters = [] @@ -40,11 +70,11 @@ def meters(): 'name': p.name, 'id': p.id, 'ppe': p.ppe, - 'number_of_zones': p.number_of_zones, + 'number_of_zones': '',#p.number_of_zones, 'tariffCode': p.tariffCode, - 'first_date': p.first_date, + # 'first_date': p.first_date, 'last_update_date': p.last_update_date, - 'measurement_date': p.measurement_date, + # 'measurement_date': p.measurement_date, } for i in range(1, p.number_of_zones + 1): @@ -65,139 +95,148 @@ def meters(): } meters.append(meter) - if DEBUG: - print("API: GET /") + logger.debug("GET /meters") return jsonify({'meters': meters}) -@app.route('/meters/', methods=['GET']) -def get_meter(meter_id): - query = PPETable.select().where((PPETable.is_active == True) & (PPETable.id == meter_id)) - result_ppes = list(query) - if result_ppes: - p = result_ppes[0] # There should be only one matching record +@app.route('/', methods=['GET']) +@app.route('//', methods=['GET']) +def get_ppe(ppe_id): + meters_query = MeterTable.select().where(MeterTable.ppe_id == ppe_id) + meter_result = meters_query.execute() + + if not meter_result: + abort(404) + + meters = [] + for meter in meter_result: + countners_query = CounterTable.select().where(CounterTable.meter_id == meter.id) + countners_result = countners_query.execute() + countners = [] + for countner in countners_result: + countner = { + 'tariff': countner.tariff, + 'measurement_date': countner.measurement_date, + 'meter_value': countner.meter_value + } + countners.append(countner) + + meter = { + 'meter_type': meter.meter_type, + 'meter_type_url': urllib.parse.quote_plus(meter.meter_type), + 'last_update_date': meter.last_update_date, + 'first_date': meter.first_date, + 'countners': countners + } + meters.append(meter) + logger.debug(f"API: GET /{ppe_id}") + return jsonify({'meters': meters}) + + +@app.route('//', methods=['GET']) +@app.route('///', methods=['GET']) +def get_meter_type(ppe_id, meter_type_url): + meter_type = urllib.parse.unquote(meter_type_url) + meters_query = MeterTable.select().where((MeterTable.ppe_id == str(ppe_id)) & (MeterTable.meter_type == meter_type)) + meter_result = meters_query.execute() + if not meter_result: + abort(404) + + meter = meter_result[0] + countners_query = CounterTable.select().where(CounterTable.meter_id == meter.id) + countners_result = countners_query.execute() + countners = [] + for countner in countners_result: + countner = { + 'tariff': countner.tariff, + 'measurement_date': countner.measurement_date, + 'meter_value': countner.meter_value + } + countners.append(countner) meter = { - 'name': p.name, - 'id': p.id, - 'ppe': p.ppe, - 'number_of_zones': p.number_of_zones, - 'tariffCode': p.tariffCode, - 'first_date': p.first_date, - 'last_update_date': p.last_update_date, - 'measurement_date': p.measurement_date + 'meter_type': meter.meter_type, + 'meter_type_url': urllib.parse.quote_plus(meter.meter_type), + 'last_update_date': meter.last_update_date, + 'first_date': meter.first_date, + 'countners': countners } - for i in range(1, p.number_of_zones + 1): - zone_key = f'zone{i}' - daily_chart_key = f'zone{i}_daily_chart_sum' - - zone_value = getattr(p, zone_key) - daily_chart_value = getattr(p, daily_chart_key) - - # Zamień None na zero podczas obliczania sumy - zone_value = float(zone_value) if zone_value is not None else 0 - daily_chart_value = float(daily_chart_value) if daily_chart_value is not None else 0 - - meter[zone_key] = { - 'meter': zone_value, - 'daily_chart': daily_chart_value, - 'sum': zone_value + daily_chart_value - } - - print(f"API: GET /meters/{meter_id}") + logger.debug(f"API: GET /{ppe_id}/{meter_type_url}") return jsonify({'meter': meter}) - else: - return jsonify({'error': 'Meter not found'}, 404) + +@app.route('///', methods=['GET']) +@app.route('////', methods=['GET']) +def get_countners(ppe_id, meter_type_url, tariff): + meter_type = urllib.parse.unquote(meter_type_url) + meters_query = MeterTable.select().where((MeterTable.ppe_id == str(ppe_id)) & (MeterTable.meter_type == meter_type)) + meter_result = meters_query.execute() + + if not meter_result: + abort(404) + + meter = meter_result[0] + countners_query = CounterTable.select().where((CounterTable.meter_id == meter.id) & (CounterTable.tariff == tariff)) + countners_result = countners_query.execute() + countner = countners_result[0] + countner = { + 'tariff': countner.tariff, + 'measurement_date': countner.measurement_date, + 'meter_value': countner.meter_value + } + + logger.debug(f"API: GET /{ppe_id}/{meter_type_url}/{countner}") + return jsonify({'countner': countner}) -# @app.route('/meters/', methods=['GET']) -# def get_meter(meter_id): -# query = PPETable.select().where((PPETable.is_active == True) & (PPETable.id == meter_id)) -# result_ppes = list(query) -# if result_ppes: -# p = result_ppes[0] # There should be only one matching record +@app.route('/charts', methods=['GET']) +@app.route('/charts/', methods=['GET']) +def charts(): + -# meter = { -# 'name': p.name, -# 'id': p.id, -# 'ppe': p.ppe, -# 'number_of_zones': p.number_of_zones, -# 'tariffCode': p.tariffCode, -# 'first_date': p.first_date, -# 'last_update_date': p.last_update_date, -# 'measurement_date': p.measurement_date -# } + current_time = datetime.datetime.now() + current_time_unix = time.mktime(current_time.timetuple()) + start_time = current_time - datetime.timedelta(days=1) + start_time_unix = time.mktime(start_time.timetuple()) + start_date = request.args.get('start_date', start_time_unix*1000) + end_date = request.args.get('end_date', current_time_unix*1000) + mp = request.args.get('mp', None) + meter_type_url = request.args.get('meter_type_url', None) + zone = request.args.get('zone', None) + negative = request.args.get('negative', type=bool, default=False) + logger.debug(f"API: GET /charts - {start_date} - {end_date}") + query = MainChartTable.select().where((MainChartTable.tm >= int(start_date)) & (MainChartTable.tm <= int(end_date))) + logger.debug(f"{query}") + factor = 1 + if negative: + factor = -1 + if mp: + query = query.where(MainChartTable.mp == mp) -# for i in range(1, p.number_of_zones + 1): -# zone_key = f'zone{i}' -# daily_chart_key = f'zone{i}_daily_chart_sum' + if meter_type_url: + meter_type = urllib.parse.unquote(meter_type_url) + query = query.where(MainChartTable.meter_type == meter_type) -# meter[zone_key] = getattr(p, zone_key) -# meter[daily_chart_key] = getattr(p, daily_chart_key) + if zone: + query = query.where(MainChartTable.zone == zone) -# print(f"API: GET /meters/{meter_id}") -# return jsonify({'meter': meter}) -# else: -# return jsonify({'error': 'Meter not found'}, 404) - -@app.route('/charts/', methods=['GET']) -def charts(mp): - start_time = time.time() - current_time = time.localtime() - start_date = request.args.get('start_date', (time.mktime(current_time) - 864000)) - end_date = request.args.get('end_date', (time.mktime(current_time))) - query = MainChartTable.select().where((MainChartTable.mp == mp) & (MainChartTable.tm >= start_date) & (MainChartTable.tm <= end_date)) result_ppes = list(query) charts = [] for p in result_ppes: czas = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(p.tm/1000)) - chart = { 'mp': p.mp, + 'meter_type': p.meter_type, + 'meter_type_url': urllib.parse.quote_plus(p.meter_type), 'zone': p.zone, - 'tm': p.tm, - 'czas': czas, - 'value': p.value + 'time_tm': p.tm, + 'time': czas, + 'value': p.value * factor } charts.append(chart) end_time = time.time() - if DEBUG: - print(f"API: GET / - {start_date} - {end_date}") - return jsonify({'charts': charts}) - -@app.route('//', methods=['GET']) -def charts_zone(mp, zone): - start_time = time.time() - current_time = time.localtime() - start_date = request.args.get('start_date', (time.mktime(current_time) - 864000)) - end_date = request.args.get('end_date', (time.mktime(current_time))) - query = MainChartTable.select().where((MainChartTable.mp == mp) & (MainChartTable.tm >= start_date) & (MainChartTable.tm <= end_date) & (MainChartTable.zone == zone)) - result_ppes = list(query) - charts = [] - - for p in result_ppes: - czas = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(p.tm/1000)) - - chart = { - 'mp': p.mp, - 'zone': p.zone, - 'tm': p.tm, - 'czas': czas, - 'value': p.value - } - charts.append(chart) - end_time = time.time() - - if DEBUG: - print(f"API: GET / - {start_date} - {end_date}") - - return jsonify({'charts': charts}) - -if __name__ == "__main__": - serve(app, host="0.0.0.0", port=8000, threads=8) \ No newline at end of file diff --git a/src/config.yaml b/src/config.yaml index 99aa6a6..19a6062 100644 --- a/src/config.yaml +++ b/src/config.yaml @@ -1,14 +1,16 @@ name: "Energa meter" description: "Energa meter addon" -version: "0.1.1" +version: "1.0.0" slug: "energa_meter" init: false options: energa_username: "" energa_password: "" + log_level: "INFO" schema: energa_username: str energa_password: password + log_level: str arch: - aarch64 - amd64 diff --git a/src/cron.py b/src/cron.py deleted file mode 100644 index f2ea842..0000000 --- a/src/cron.py +++ /dev/null @@ -1,33 +0,0 @@ -import configparser, time, datetime, os -from moj_licznik import MojLicznik -from pathlib import Path - -def main(): - plik = Path('config.ini') - username = None - password = None - if plik.is_file(): - print(f"Pobieram parametry z config.ini.") - config = configparser.ConfigParser() - config.read("config.ini") - username = config.get("Credentials", "username") - password = config.get("Credentials", "password") - else: - username = os.getenv("USERNAME") - password = os.getenv("PASSWORD") - try: - mojLicznik = MojLicznik() - print(f"Update...{datetime.datetime.now()}") - print(f"Logowanie...") - mojLicznik.login(username, password) - if mojLicznik.loginStatus: - print(f"Aktualizacja danych bieżących...") - mojLicznik.uppdate_measurments() - mojLicznik.update_last_days() - mojLicznik.set_daily_zones() - mojLicznik.logout() - except: - print("Błąd aktualizacji danych...") - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/src/img/grafana_02.png b/src/img/grafana_02.png index f17d352..72e64e6 100644 Binary files a/src/img/grafana_02.png and b/src/img/grafana_02.png differ diff --git a/src/img/grafana_03.png b/src/img/grafana_03.png index 8fbd681..603c262 100644 Binary files a/src/img/grafana_03.png and b/src/img/grafana_03.png differ diff --git a/src/img/grafana_06.png b/src/img/grafana_06.png index a0bc00d..a4a9233 100644 Binary files a/src/img/grafana_06.png and b/src/img/grafana_06.png differ diff --git a/src/img/grafana_07.png b/src/img/grafana_07.png deleted file mode 100644 index f4ba9ba..0000000 Binary files a/src/img/grafana_07.png and /dev/null differ diff --git a/srcdev/img/grafana_08.png b/src/img/grafana_08.png similarity index 100% rename from srcdev/img/grafana_08.png rename to src/img/grafana_08.png diff --git a/srcdev/img/grafana_09.png b/src/img/grafana_09.png similarity index 100% rename from srcdev/img/grafana_09.png rename to src/img/grafana_09.png diff --git a/srcdev/img/grafana_10.png b/src/img/grafana_10.png similarity index 100% rename from srcdev/img/grafana_10.png rename to src/img/grafana_10.png diff --git a/srcdev/log_config.py b/src/log_config.py similarity index 100% rename from srcdev/log_config.py rename to src/log_config.py diff --git a/src/main.py b/src/main.py index 8cd9209..6703d12 100644 --- a/src/main.py +++ b/src/main.py @@ -3,33 +3,24 @@ from moj_licznik import MojLicznik from pathlib import Path def main(): - plik = Path('config.ini') username = None password = None - if plik.is_file(): - print(f"Pobieram parametry z config.ini.") - config = configparser.ConfigParser() - config.read("config.ini") - username = config.get("Credentials", "username") - password = config.get("Credentials", "password") - else: - username = os.getenv("USERNAME") - password = os.getenv("PASSWORD") + username = os.getenv("USERNAME") + password = os.getenv("PASSWORD") print(f"Inicjacja...") mojLicznik = MojLicznik() print(f"Logowanie...", username) mojLicznik.login(username, password) - if mojLicznik.loginStatus: - print(f"Aktualizacja liczników...") - mojLicznik.uppdate_measurments() - print(f"Wyszukiwanie najstarszych danych...") - mojLicznik.update_first_date() - print(f"Pobieranie danych...") - mojLicznik.download_charts(True) - mojLicznik.update_last_days() - mojLicznik.set_daily_zones() - mojLicznik.logout() - + print(f"Aktualizacja liczników...") + mojLicznik.uppdate_measurments() + print(f"Wyszukiwanie najstarszych danych...") + mojLicznik.update_first_date() + print(f"Pobieranie danych...") + mojLicznik.download_charts(True) + mojLicznik.update_last_days() + mojLicznik.set_daily_zones() + mojLicznik.logout() + if __name__ == "__main__": main() \ No newline at end of file diff --git a/src/moj_licznik.py b/src/moj_licznik.py index 4450756..d10bf26 100644 --- a/src/moj_licznik.py +++ b/src/moj_licznik.py @@ -1,13 +1,18 @@ from peewee import SqliteDatabase from datetime import datetime, timedelta, date -import calendar, requests, re, time, json +import calendar, requests, re, time, json, os, logging import http.cookiejar as cookiejar from requests.exceptions import HTTPError from bs4 import BeautifulSoup from enum import Enum -from peewee import Model, CharField, IntegerField, DateField, BooleanField, CompositeKey, DecimalField +from peewee import AutoField, Model, CharField, IntegerField, DateField, BooleanField, CompositeKey, DecimalField, ForeignKeyField, SQL +import urllib.parse -db = SqliteDatabase('database.sqlite') +logger = logging.getLogger("energaMeter") + +path = os.path.dirname(os.path.abspath(__file__)) +db_file = 'database.sqlite' +db = SqliteDatabase(os.path.join(path, db_file)) class ChartType(Enum): DAY = "DAY" @@ -15,26 +20,54 @@ class ChartType(Enum): YEAR = "YEAR" class PPETable(Model): - ppe = CharField() + id = CharField(primary_key=True) + ppe = CharField(unique=True) tariffCode = CharField() + type = CharField() name = CharField() - zone1 = DecimalField(max_digits=15, decimal_places=5, null=True) - zone2 = DecimalField(max_digits=15, decimal_places=5, null=True) - zone3 = DecimalField(max_digits=15, decimal_places=5, null=True) - zone1_daily_chart_sum = DecimalField(max_digits=10, decimal_places=5, null=True) - zone2_daily_chart_sum = DecimalField(max_digits=10, decimal_places=5, null=True) - zone3_daily_chart_sum = DecimalField(max_digits=10, decimal_places=5, null=True) - number_of_zones = IntegerField(default=0) + last_update_date = DateField(null=True) is_active = BooleanField(default=True) - measurement_date = DateField(null=True) - first_date = DateField(null=True) - last_update_date = DateField(null=True) class Meta: database = db + table_name = 'PPE' + constraints = [SQL('UNIQUE (ppe, tariffCode)')] + +class MeterTable(Model): + id = AutoField() # Meter point + ppe_id = ForeignKeyField(PPETable, backref='zones') + meter_type = CharField() + last_update_date = DateField(null=True) + first_date = DateField(null=True) + + class Meta: + database = db + table_name = 'METER' + constraints = [SQL('UNIQUE (ppe_id, meter_type)')] + +class CounterTable(Model): + id = AutoField() + meter_id = ForeignKeyField(MeterTable, backref='meter') + tariff = CharField() + measurement_date = DateField(null=True) + meter_value = DecimalField(max_digits=15, decimal_places=5, null=True) + + class Meta: + database = db + table_name = 'COUNTER' + +# class CounterTable(Model): +# meter_id = ForeignKeyField(MeterTable, backref='zones') +# measurement_date = DateField(null=True) +# meter_value = DecimalField(max_digits=15, decimal_places=5, null=True) + +# class Meta: +# database = db + class ChartTable(Model): - id = IntegerField() + id = IntegerField() + meter_type = CharField() year = IntegerField() month = IntegerField(null=True) day = IntegerField(null=True) @@ -42,10 +75,12 @@ class ChartTable(Model): class Meta: database = db + table_name = 'CHART_CACHE' primary_key = CompositeKey('id', 'year', 'month', 'day') class MainChartTable(Model): mp = CharField() + meter_type = CharField() zone = IntegerField() tm = IntegerField() value = DecimalField(max_digits=20, decimal_places=16, null=True) @@ -55,9 +90,74 @@ class MainChartTable(Model): class Meta: database = db - primary_key = CompositeKey('mp', 'zone', 'tm') + table_name = 'CHART' + primary_key = CompositeKey('mp', 'meter_type', 'zone', 'tm') +def znajdz_typ_odbiorcy(element): + typ_odbiorcy = '' + div_elements = element.find_all('div', recursive=False) + for div_element in div_elements: + typ_span = div_element.find('span', text='Typ') + if typ_span: + typ_odbiorcy = typ_span.next_sibling.strip() + return typ_odbiorcy + typ_odbiorcy = znajdz_typ_odbiorcy(div_element) # Rekurencyjne przeszukiwanie zagnieżdżonych div + return typ_odbiorcy +def findCountners(page): + table = page.find('table') + countner_type_list = ["A-", "A+"] + data_list = [] + + # Jeśli znaleźliśmy tabelę, możemy przeszukać jej wiersze i komórki + if table: + for row in table.find_all('tr'): + cells = row.find_all('td') + if len(cells) > 1: + # Pobieramy opis z pierwszej komórki + description = cells[0].text.strip() + + # Pomijamy, jeśli rodzaj_licznika jest pusty + if not description: + continue + + # Usuwamy datę z opisu + description_parts = description.split('\n') + meter_type = description_parts[0][:2].strip() + + if meter_type not in countner_type_list: + continue + + tariff0 = description_parts[0][2:].strip() + tariff = ''.join(filter(str.isdigit, tariff0)) + measurement_date = description_parts[1].strip() + + # Pobieramy dane liczbowe z drugiej komórki + data = cells[1].text.strip() + + # Usuwamy znaki nowej linii i spacje z danych + data = data.replace('\n', '').replace(' ', '') + + data = data.replace(',', '.') + + # Dzielimy dane na część całkowitą i część dziesiętną + parts = data.split('.') + if len(parts) == 2: + integer_part = parts[0] + decimal_part = parts[1] + else: + integer_part = parts[0] + decimal_part = '0' + + data_dict = { + "meter_type": meter_type, + "tariff": tariff, + "measurement_date": measurement_date, + "meter_value": f"{integer_part}.{decimal_part}" + } + + data_list.append(data_dict) + return data_list class MojLicznik: @@ -66,9 +166,13 @@ class MojLicznik: meter_url = "https://mojlicznik.energa-operator.pl" + + def databaseInit(self): - db.create_tables([ChartTable], safe=True) db.create_tables([PPETable], safe=True) + db.create_tables([MeterTable], safe=True) + db.create_tables([CounterTable], safe=True) + db.create_tables([ChartTable], safe=True) db.create_tables([MainChartTable], safe=True) @@ -87,13 +191,18 @@ class MojLicznik: login_url = f"{self.meter_url}/dp/UserLogin.do" + self.loginStatus = False + try: + logger.debug("Pobieram formularz logowania.") response = self.session.get(login_url) response.raise_for_status() - print(f"Logowanie rozpoczęte.") + if response.url == 'https://mojlicznik.energa-operator.pl/maintenance.html': + logger.critical("Trwają prace serwisowe w Mój Licznik. Logowanie nie jest możliwe, spróbuj później.") + return except HTTPError as e: - print(f"Wystąpił błąd HTTP: {e}") + logger.error(f"Wystąpił błąd HTTP: {e}") soup = BeautifulSoup(response.text, 'html.parser') csrf_token = soup.find('input', {'name': '_antixsrf'})['value'] @@ -113,7 +222,7 @@ class MojLicznik: except HTTPError as e: - print(f"Wystąpił błąd HTTP: {e}") + logger.error(f"Wystąpił błąd HTTP: {e}") soup = BeautifulSoup(response.text, 'html.parser') @@ -121,12 +230,16 @@ class MojLicznik: login_error = soup.find('div', text=login_error_text) if login_error: - self.loginStatus = False - print(login_error_text) + logger.critical(login_error_text) + return else: self.loginStatus = True - print(f"Zalogowano") + logger.info(f"Zalogowano") + body = soup.find('body') + type_value = znajdz_typ_odbiorcy(body) + + logger.debug(f"Typ umowy: {type_value}.") select_elements = soup.find_all('script', type='text/javascript') meter_isd = [] for el in select_elements: @@ -140,16 +253,17 @@ class MojLicznik: meter_isd.append(id_value) retrieved_record = PPETable.get_or_none(id=id_value) if retrieved_record: - print(f"Licznik {id_value} istnieje w systemie.") + logger.info(f"Licznik {id_value} istnieje w systemie.") if not retrieved_record.is_active: retrieved_record.is_active = True retrieved_record.save() else: - print(f"Licznik {id_value} nie istnieje w systemie.") + logger.info(f"Licznik {id_value} nie istnieje w systemie, zostanie dodany.") data = PPETable.create( id=id_value, ppe=ppe_value, tariffCode=tariffCode_value, + type=type_value, name=name_value ) update_query = PPETable.update(is_active=0).where(PPETable.id.not_in(meter_isd)) @@ -161,11 +275,11 @@ class MojLicznik: response = self.session.get(logout_url) response.raise_for_status() self.loginStatus = False - print(f"Wylogowano.") + logger.info(f"Wylogowano.") except HTTPError as e: - print(f"Wystąpił błąd HTTP: {e}") + logger.error(f"Wystąpił błąd HTTP: {e}") - def uppdate_measurments(self): + def update_countners(self): query = PPETable.select().where(PPETable.is_active == True) result_ppes = query.execute() @@ -175,87 +289,86 @@ class MojLicznik: response = self.session.get(meter_url) response.raise_for_status() soup = BeautifulSoup(response.text, 'html.parser') - td_elements = soup.find_all('td', class_='last') - date_divs = soup.find_all("div", style="font-size: 10px") + countners_dict = findCountners(soup) - for div in date_divs: - p.measurement_date = datetime.strptime(div.text.strip(), "%Y-%m-%d %H:%M").date() - i = 0 - - for td in td_elements: - text = td.get_text() - cleaned_text = re.sub(r'[^\d,]', '', text) - cleaned_number_str = cleaned_text.lstrip('0').replace(',', '.') - i = i + 1 - if i == 1: - p.zone1 = float(cleaned_number_str) - p.number_of_zones = 1 - elif i == 2: - p.zone2 = float(cleaned_number_str) - p.number_of_zones = 2 - elif i == 3: - p.zone3 = float(cleaned_number_str) - p.number_of_zones = 1 - p.last_update_date = datetime.now() - p.save() - print(f"Zapisano stan licznika {p.name} na dzień: {p.measurement_date}") + for c in countners_dict: + mn, mu = MeterTable.get_or_create(ppe_id=p.id, meter_type=c['meter_type']) + mn.last_update_date = datetime.now() + mn.save() + cn, cu = CounterTable.get_or_create( + meter_id = mn.id, + tariff=c['tariff'] + ) + + cn.meter_value = c['meter_value'] + cn.measurement_date = c['measurement_date'] + cn.save() + + logger.info(f"Zapisano stan licznika {p.id} {c['meter_type']} taryfa {c['tariff']} z dnia: {c['measurement_date']} : {c['meter_value']}") except HTTPError as e: - print(f"Wystąpił błąd HTTP: {e}") + logger.error(f"Wystąpił błąd HTTP: {e}") def update_first_date(self): - query = PPETable.select().where(PPETable.first_date.is_null(True) & (PPETable.is_active == True)) - result_ppes = query.execute() + ppes_query = PPETable.select().where(PPETable.is_active == True) + result_ppes = ppes_query.execute() for p in result_ppes: - print(f"Szukam najstarsze dane historyczne licznika {p.name}") - meter_point = p.id - max_years_back = 5 - start_date = datetime.now() - last_chart_year = None - for n in range(max_years_back + 1): - first_day_of_year = datetime(start_date.year-n, 1, 1) - data_json = self.download_chart(ChartType.YEAR, first_day_of_year, meter_point) - if data_json: - data = json.loads(data_json) - if data and data.get("mainChart") and len(data["mainChart"]) > 0: - last_chart_year = first_day_of_year.year - last_chart_month = None - max_month = 12 - for n in range(max_month, 0, -1): - first_day_of_month = datetime(last_chart_year, n, 1) - data_json = self.download_chart(ChartType.MONTH, first_day_of_month, meter_point) - if data_json: - data = json.loads(data_json) - if data and data.get("mainChart") and len(data["mainChart"]) > 0: - last_chart_month = n - last_chart_day = None - max_day = 31 - first_day_of_day = datetime(last_chart_year, last_chart_month, 1) - _, max_day = calendar.monthrange(first_day_of_day.year, first_day_of_day.month) - for n in range(max_day, 0, -1): - first_day_of_day = datetime(last_chart_year, last_chart_month, n) - data_json = self.download_chart(ChartType.DAY, first_day_of_day, meter_point) - if data_json: - data = json.loads(data_json) - if data and data.get("mainChart") and len(data["mainChart"]) > 0: - last_chart_day = n - first_date = datetime(last_chart_year, last_chart_month, last_chart_day).date() - print(f"Najstarsze dane historyczne dla licznika {p.name}: {first_date}") - p.first_date = first_date - p.save() + meters_query = MeterTable.select().where((MeterTable.ppe_id == p.id) & (MeterTable.first_date.is_null(True))) + meters_result = meters_query.execute() - def save_main_charts(self, mp, vals): + for meter in meters_result: + meter_type = meter.meter_type + + print(f"Szukam najstarsze dane historyczne licznika {p.name} (PPE: {p.ppe}, {p.id}) typ: {meter_type}") + meter_point = p.id + max_years_back = 5 + start_date = datetime.now() + last_chart_year = None + for n in range(max_years_back + 1): + first_day_of_year = datetime(start_date.year-n, 1, 1) + data_json = self.download_chart(ChartType.YEAR, first_day_of_year, meter_point, meter_type) + if data_json: + data = json.loads(data_json) + if data and data.get("mainChart") and len(data["mainChart"]) > 0: + last_chart_year = first_day_of_year.year + last_chart_month = None + max_month = 12 + for n in range(max_month, 0, -1): + first_day_of_month = datetime(last_chart_year, n, 1) + data_json = self.download_chart(ChartType.MONTH, first_day_of_month, meter_point, meter_type) + if data_json: + data = json.loads(data_json) + if data and data.get("mainChart") and len(data["mainChart"]) > 0: + last_chart_month = n + last_chart_day = None + max_day = 31 + first_day_of_day = datetime(last_chart_year, last_chart_month, 1) + _, max_day = calendar.monthrange(first_day_of_day.year, first_day_of_day.month) + for n in range(max_day, 0, -1): + first_day_of_day = datetime(last_chart_year, last_chart_month, n) + data_json = self.download_chart(ChartType.DAY, first_day_of_day, meter_point, meter_type) + if data_json: + data = json.loads(data_json) + if data and data.get("mainChart") and len(data["mainChart"]) > 0: + last_chart_day = n + first_date = datetime(last_chart_year, last_chart_month, last_chart_day).date() + print(f"Najstarsze dane historyczne dla licznika {p.name} (PPE: {p.ppe}, {p.id}) typ: {meter_type}: {first_date}") + meter.first_date = first_date + meter.save() + + def save_main_charts(self, mp, vals, m_type): for val in vals: - #try: + try: + logger.debug(f"save_main_charts: mp: {mp}, val: {val}, meter_type: {m_type}") z = val["zones"] - # {"tm": "1690412400000", "tarAvg": 0.3899153269199055, "zones": [null, 0.232, null], "est": false, "cplt": true}, if z[0]: # MainChartTable.get_or_create(tm = val["tm"], zone = 1, value = z[0], tarAvg=val["tarAvg"], est=val["est"], cplt=val["cplt"]) try: - existing_record = MainChartTable.get((MainChartTable.mp == mp) & (MainChartTable.tm == val["tm"]) & (MainChartTable.zone == 1)) + existing_record = MainChartTable.get((MainChartTable.meter_type == m_type) & (MainChartTable.mp == mp) & (MainChartTable.tm == val["tm"]) & (MainChartTable.zone == 1)) except MainChartTable.DoesNotExist: # Jeśli rekord nie istnieje, utwórz nowy MainChartTable.create( mp=mp, + meter_type=m_type, tm=val["tm"], zone=1, value=z[0], @@ -266,11 +379,12 @@ class MojLicznik: if z[1]: try: - existing_record = MainChartTable.get((MainChartTable.mp == mp) & (MainChartTable.tm == val["tm"]) & (MainChartTable.zone == 2)) + existing_record = MainChartTable.get((MainChartTable.meter_type == m_type) & (MainChartTable.mp == mp) & (MainChartTable.tm == val["tm"]) & (MainChartTable.zone == 2)) except MainChartTable.DoesNotExist: # Jeśli rekord nie istnieje, utwórz nowy MainChartTable.create( mp=mp, + meter_type=m_type, tm=val["tm"], zone=2, value=z[1], @@ -281,11 +395,12 @@ class MojLicznik: if z[2]: try: - existing_record = MainChartTable.get((MainChartTable.mp == mp) & (MainChartTable.tm == val["tm"]) & (MainChartTable.zone == 1)) + existing_record = MainChartTable.get((MainChartTable.meter_type == m_type) & (MainChartTable.mp == mp) & (MainChartTable.tm == val["tm"]) & (MainChartTable.zone == 3)) except MainChartTable.DoesNotExist: # Jeśli rekord nie istnieje, utwórz nowy MainChartTable.create( mp=mp, + meter_type=m_type, tm=val["tm"], zone=3, value=z[2], @@ -294,12 +409,12 @@ class MojLicznik: cplt=val["cplt"] ) - #except: - # pass + except Exception as e: + logging.error(f"Wystąpił błąd: {str(e)}") return None - def download_chart(self, type, date, meter_point, update_mode=False): + def download_chart(self, type, date, meter_point, meter_type, update_mode=False): if type == ChartType.DAY: chart_type = "DAY" @@ -316,7 +431,9 @@ class MojLicznik: first_day = datetime(date.year, 1, 1) tsm_date = int(time.mktime(first_day.timetuple()) * 1000) - chart_url = f"{self.meter_url}/dp/resources/chart?mainChartDate={tsm_date}&type={chart_type}&meterPoint={meter_point}&mo=A%2B" + # meter_type = 'A+' + chart_url = f"{self.meter_url}/dp/resources/chart?mainChartDate={tsm_date}&type={chart_type}&meterPoint={meter_point}&mo={urllib.parse.quote_plus(meter_type)}" + logger.debug(f"chart_url: {chart_url}") try: response = self.session.get(chart_url) data = json.loads(response.text) @@ -326,7 +443,7 @@ class MojLicznik: mainChartDate = data["response"]["mainChartDate"] mainChart = data["response"]["mainChart"] if type == ChartType.DAY: - self.save_main_charts(meter_point, mainChart) + self.save_main_charts(meter_point, mainChart, meter_type) date = int(mainChartDate) / 1000 month = None @@ -345,35 +462,42 @@ class MojLicznik: chart_record.value = json_dump chart_record.save() except ChartTable.DoesNotExist: - chart_record = ChartTable.create(id=id, value=json_dump, year=year, month=month, day=day) + chart_record = ChartTable.create(id=id, meter_type=meter_type, value=json_dump, year=year, month=month, day=day) else: try: - ChartTable.create(id=id, value=json_dump, year=year, month=month, day=day) + ChartTable.create(id=id, meter_type=meter_type, value=json_dump, year=year, month=month, day=day) except: pass return json_dump return None except HTTPError as e: - print(f"Wystąpił błąd HTTP: {e}") + logger.error(f"Wystąpił błąd HTTP: {e}") def download_charts(self, full_mode=False): query = PPETable.select().where(PPETable.is_active == True) result_ppes = query.execute() for p in result_ppes: - current_date = p.first_date - if not full_mode: - current_date = p.measurement_date - timedelta(days=1) - - while current_date <= date.today(): - try: - record = ChartTable.get(id=p.id, year=current_date.year, month=current_date.month, day=current_date.day) - # Jeśli rekord o określonych wartościach klucza głównego istnieje, zostanie pobrany. - print(f"Posiadam dane historyczne dla {p.name} na dzień: {current_date}") - except ChartTable.DoesNotExist: - self.download_chart(ChartType.DAY, current_date, p.id) - print(f"Pobieram dane historyczne dla {p.name} na dzień: {current_date}") - current_date += timedelta(days=1) + meters_query = MeterTable.select().where((MeterTable.ppe_id == p.id)) # // & (MeterTable.first_date.is_null(True))) + meters_result = meters_query.execute() + + for meter in meters_result: + meter_type = meter.meter_type + + logger.info(f"Pobieram dane historyczne dla {p.name} ({p.id}) typ: {meter_type}") + current_date = meter.first_date + if not full_mode: + current_date = meter.last_update_date - timedelta(days=1) + + while current_date <= date.today(): + try: + record = ChartTable.get(id=p.id, meter_type=meter_type, year=current_date.year, month=current_date.month, day=current_date.day) + # Jeśli rekord o określonych wartościach klucza głównego istnieje, zostanie pobrany. + logger.debug(f"Posiadam dane historyczne dla {p.name} ({p.id}) typ: {meter_type} na dzień: {current_date}") + except ChartTable.DoesNotExist: + self.download_chart(ChartType.DAY, current_date, p.id, meter_type) + logger.debug(f"Pobieram dane historyczne dla {p.name} ({p.id}) typ: {meter_type} na dzień: {current_date}") + current_date += timedelta(days=1) def update_last_days(self): today = datetime.today().date() @@ -381,16 +505,23 @@ class MojLicznik: result_ppes = query.execute() for p in result_ppes: - if not p.last_update_date: - p.last_update_date = today - timedelta(days=5) - p.save() - last_update_date = p.last_update_date - timedelta(days=1) - while last_update_date <= today: - print(f"Aktualizacja danych dla {p.name} na dzień: {last_update_date}") - self.download_chart(ChartType.DAY, last_update_date, p.id, True) - p.last_update_date = last_update_date - p.save() - last_update_date += timedelta(days=1) + meters_query = MeterTable.select().where((MeterTable.ppe_id == p.id) & (MeterTable.first_date.is_null(True))) + meters_result = meters_query.execute() + + for meter in meters_result: + meter_type = meter.meter_type + + logger.info(f"Aktualizacja danych bieżących dla {p.name} ({p.id}) typ: {meter_type}") + if not p.last_update_date: + p.last_update_date = today - timedelta(days=5) + p.save() + last_update_date = p.last_update_date - timedelta(days=1) + while last_update_date <= today: + logger.debug(f"Aktualizacja danych dla {p.name} ({p.id}) typ: {meter_type} na dzień: {last_update_date}") + self.download_chart(ChartType.DAY, last_update_date, p.id, meter_type, True) + p.last_update_date = last_update_date + p.save() + last_update_date += timedelta(days=1) def get_current_meters(self, add_daily_char_data=False): @@ -408,81 +539,38 @@ class MojLicznik: if zones: zone1_data = zones[0] zone1_main_chart = zone1_data.get("mainChart", []) - #print(zone1_data) - #else: - #print(f"{p.name} ({p.measurement_date}) : {p.zone1}, {p.zone2}, {p.zone3}") - - #else: - #print(f"{p.name} ({p.measurement_date}) : {p.zone1}, {p.zone2}, {p.zone3}") - - def set_daily_zones(self): - query = PPETable.select().where(PPETable.is_active == True) - result_ppes = query.execute() - - for p in result_ppes: - query = ChartTable.select().where( - (ChartTable.id == p.id) & - ((ChartTable.year > p.measurement_date.year) | - ((ChartTable.year == p.measurement_date.year) & - (ChartTable.month > p.measurement_date.month)) | - ((ChartTable.year == p.measurement_date.year) & - (ChartTable.month == p.measurement_date.month) & - (ChartTable.day >= p.measurement_date.day)) - )) - - zones_sums = {f"zone{i+1}_daily_chart_sum": 0.0 for i in range(3)} - - for chart_entry in query: - value_json = json.loads(chart_entry.value) - main_chart = value_json.get("mainChart", []) - - for entry in main_chart: - zones = entry.get("zones", []) - - for i, value in enumerate(zones): - if value is not None: - zones_sums[f"zone{i+1}_daily_chart_sum"] += value - - for key, value in zones_sums.items(): - setattr(p, key, value) - - p.save() - # def set_daily_zones(self): # query = PPETable.select().where(PPETable.is_active == True) # result_ppes = query.execute() # for p in result_ppes: - # query = ChartTable.select().where((ChartTable.id == p.id) & (ChartTable.year >= p.measurement_date.year) & (ChartTable.month >= p.measurement_date.month) & (ChartTable.day >= p.measurement_date.day)) - # query_count = query.count() - # if (query_count > 0): - # query_first = query.first() - # value_json = json.loads(query_first.value) - # main_chart = value_json.get("mainChart", []) + # query = ChartTable.select().where( + # (ChartTable.id == p.id) & + # ((ChartTable.year > p.measurement_date.year) | + # ((ChartTable.year == p.measurement_date.year) & + # (ChartTable.month > p.measurement_date.month)) | + # ((ChartTable.year == p.measurement_date.year) & + # (ChartTable.month == p.measurement_date.month) & + # (ChartTable.day >= p.measurement_date.day)) + # )) - # # Inicjalizacja słownika do przechowywania sum dla każdej sekcji zones - # zones_sums = {f"zone{i+1}": 0.0 for i in range(len(main_chart[0].get("zones", [])))} + # zones_sums = {f"zone{i+1}_daily_chart_sum": 0.0 for i in range(3)} + + # for chart_entry in query: + # value_json = json.loads(chart_entry.value) + # main_chart = value_json.get("mainChart", []) # for entry in main_chart: # zones = entry.get("zones", []) # for i, value in enumerate(zones): # if value is not None: - # zones_sums[f"zone{i+1}"] += value - # if (zones_sums["zone1"] > 0): - # p.zone1_daily_chart_sum = zones_sums["zone1"] - # else: - # p.zone1_daily_chart_sum = None - # if (zones_sums["zone2"] > 0): - # p.zone2_daily_chart_sum = zones_sums["zone2"] - # else: - # p.zone2_daily_chart_sum = None - # if (zones_sums["zone3"]): - # p.zone3_daily_chart_sum = zones_sums["zone3"] - # else: - # p.zone3_daily_chart_sum = None - # p.save() + # zones_sums[f"zone{i+1}_daily_chart_sum"] += value + + # for key, value in zones_sums.items(): + # setattr(p, key, value) + # p.save() def print_summary_zones(self): diff --git a/srcdev/run.py b/src/run.py similarity index 100% rename from srcdev/run.py rename to src/run.py diff --git a/src/run.sh b/src/run.sh index fcb60a6..a85830e 100644 --- a/src/run.sh +++ b/src/run.sh @@ -2,16 +2,8 @@ export USERNAME=$(bashio::config 'energa_username') export PASSWORD=$(bashio::config 'energa_password') +export LOG_LEVEL=$(bashio::config 'log_level') bashio::log.info "Uruchamiam API" -python api.py & -bashio::log.info "Uruchamiam MAIN" -python main.py -bashio::log.info "Uruchamiam CRON" - -while true; do - python cron.py - bashio::log.info "Czekam..." - sleep 1800 -done +python run.py diff --git a/srcdev/CHANGELOG.md b/srcdev/CHANGELOG.md deleted file mode 100644 index 5c52fc8..0000000 --- a/srcdev/CHANGELOG.md +++ /dev/null @@ -1,15 +0,0 @@ -# EnergaMeter (dev) -## v0.1.8 [2023-11-04] -- Dodano możliwość odwrócenia znaku wartości pomiaru -## v0.1.4 - v0.1.7 [2023-11-03] -- Drobne poprawki -## v0.1.3 [2023-10-31] -- Dodano obsługę liczników wytwórcy (Uwaga: zmiana struktury JSON) -## v0.1.2 [2023-10-23] -- Dodano obsługę trybu serwisowego aplikacji Mój Licznik -- Poprawiono logowanie błędów -- W przypadku problemów z logowaniem liczniki w aplikacji pozostają aktywne -## v0.1.1 [2023-10-22] -- Dodano obsługę błędnego logowania -## v0.1.0 [2023-10-21] -- Wersja beta \ No newline at end of file diff --git a/srcdev/Dockerfile b/srcdev/Dockerfile deleted file mode 100644 index 328d165..0000000 --- a/srcdev/Dockerfile +++ /dev/null @@ -1,20 +0,0 @@ -ARG BUILD_FROM -FROM $BUILD_FROM - -RUN apk add --no-cache python3 -RUN apk add --update py3-pip - -# Copy data for add-on -COPY run.sh / -COPY run.py / -COPY requirements.txt / -COPY main.py / -COPY api.py / -COPY cron.py / -# COPY database.sqlite / -COPY moj_licznik.py / -COPY log_config.py / -RUN chmod a+x /run.sh -RUN pip install -r requirements.txt - -CMD [ "/run.sh" ] \ No newline at end of file diff --git a/srcdev/INSTALL.md b/srcdev/INSTALL.md deleted file mode 100644 index 32abde7..0000000 --- a/srcdev/INSTALL.md +++ /dev/null @@ -1,161 +0,0 @@ -[![ha_badge](https://img.shields.io/badge/Home%20Assistant-Add%20On-blue.svg)](https://www.home-assistant.io/) -# [Energa meter](https://github.com/tcich/ha-addon-energa-meter) Home Assistant add-on -# Wersja dev -[aarch64-shield]: https://img.shields.io/badge/aarch64-yes-green.svg -[amd64-shield]: https://img.shields.io/badge/amd64-yes-green.svg -[armv6-shield]: https://img.shields.io/badge/armv6-yes-green.svg -[armv7-shield]: https://img.shields.io/badge/armv7-yes-green.svg -[i386-shield]: https://img.shields.io/badge/i386-yes-green.svg -![aarch64-shield] -![amd64-shield] -![armv6-shield] -![armv7-shield] -![i386-shield] - - -[kawa-logo]: https://github.com/tcich/ha-addon-energa-meter/blob/main/img/buycoffeeto-btn-primary-outline.png -[kawa]: https://buycoffee.to/tcich - -Postaw kawę - -## O dodatku - -To jest dodatek dla [Home Assistant](https://www.home-assistant.io/). Instalacja dodatku [Energa meter](https://github.com/tcich/ha-addon-energa-meter) umożliwia cykliczne pobieranie danych z aplikacji [Mój Licznik - Energa](https://mojlicznik.energa-operator.pl) udostępnianej klientom Operatora energetycznego Energa. - -### Wersja dev -Wersja dev jest wersją developeską, nie należy jej używać w produkcyjnej wersji HA, może powodować różne problemy, może nie działać. - -## Instalacja -1) Dodaj repozytorium do repozytoriów dodatków swojego HA za pomocą poniższego przycisku - -[![Open your Home Assistant instance and show the add add-on repository dialog with a specific repository URL pre-filled.](https://my.home-assistant.io/badges/supervisor_add_addon_repository.svg)](https://my.home-assistant.io/redirect/supervisor_add_addon_repository/?repository_url=https%3A%2F%2Fgithub.com%2Ftcich%2Fha-addon-energa-meter) - -Lub zainstaluj manualnie z Ustawienia -> Dodatki -> Sklep z dodatkami -> ⁞ (Menu) -> Repozytoria -> Wpisz `https://github.com/tcich/hassio-mojlicznik` -> Dodaj. Następnie w ⁞ (Menu) -> Sprawdź aktualizacje (może być konieczne przeładowanie strony) - -2) Odszukaj dodatek na liście dodatków w sklepie z dodatkami i zainstaluj go. - -3) W zakładce konfiguracja uzupełnij nazwę użytkownika oraz hasło do aplikacji Mój Licznik, jeżeli potrzebujesz to zmień udostępniany port dla API - -4) Przejdź do zakładki informacje i uruchom dodatek (pierwsze uruchomienie może trwać kilkanaście minut), jeżeli w logu pojawi się informacja *INFO: Czekam...* oznacza to, że pierwsze inicjalne pobieranie danych zostało ukończone. - - -## Wersja Docker -Aby ruchomić wersję docker należy skorzystać z polecenia poniżej - -``` -docker run -p 8000:8000 -e ENERGA_USERNAME=LoginEnerga -e ENERGA_PASSWORD=HasloEnerga tomcic/energa-meter:v0.1.0 -``` - -Wymagane parametry: - -* ENERGA_USERNAME - nazwa użytkownika w aplikacji Energa Mój licznik -* ENERGA_PASSWORD - hasło użytkownika w aplikacji Energa Mój licznik - - -## Konfiguracja sensorów -Do HA możesz dodać sensory, które zawierają informacje udostępniane przez API - -Poniższa instrukcja zawiera założenia: -* dodatek jest dostępny pod adresem *localhost* na porcie *8000* -* ID Twojego licznika to *123456789* - -1) Ustal ID Twoich liczników, w tym celu przejdź do adresu Twojego HA na porcie 8000 lub innym jeźeli zmieniłeś go w konfiguracji, np. http://192.168.1.10:8000 wyświetli się w formacie json lista dostępnych liczników. -2) W pliku configuration.yaml w HA dodaj następującą konfigurację np.: - -``` -sensor: - - platform: rest - resource: http://localhost:8000/123456789/A%2B/1 - name: "A+ Taryfa 1" - unique_id: 123456789_apt1 - unit_of_measurement: "kWh" - value_template: "{{ value_json.countner.meter_value | round(2) }}" - - platform: rest - resource: http://localhost:8000/123456789/A%2B/2 - name: "A+ Taryfa 2" - unique_id: 123456789_apt2 - unit_of_measurement: "kWh" - value_template: "{{ value_json.countner.meter_value | round(2) }}" -``` - -### Opis konfiguracji -| element konfiguracji | Opis | -|-------------------|-------------------| -| resource: http://localhost:8000/123456789/A%2B/1 | Adres API z danymi konkretnego licznika, podajemy **localhost** lub nazwę instancji dockera (**Nazwa hosta** z okna dodatku), port, id licznika, rodzaj pomiaru, taryfa| -| name: "A+ Taryfa 1" | Nazwa sensora, wpisz dowolną| -| unique_id | Unikalny ID sensora, nie mogą być w systemie dwa sensory z tym samym ID| -| unit_of_measurement: "kWh" | Jednostka miary, nie zmieniaj chyba, że wiesz co robisz| -| value_template: "{{ value_json.meter.countner.meter_value \| round(2) }}" | Zaokrąglony do dwóch miejsc po przecinku stan sensora| - -## API dla wykresów, np. Grafana -Aby pobrać dane z API w formacie JSON należy użyć adresu http://home_assistant:8000/charts/12729?start_date=1695332400129&end_date=1697924583285&mp=123456789&zone=1 - -### Opis konfiguracji -| element konfiguracji | Opis | -|-------------------|-------------------| -| resource: http://localhost:8000/charts | Adres API z danymi do wykresów, podajemy **localhost** lub nazwę instancji dockera (**Nazwa hosta** z okna dodatku), port, id licznika, rodzaj pomiaru, taryfa| -| start_date | data początkowa danych w formacie epoch (ms), domyślnie czas bieżący | -| end_date | data końcowa danych w formacie epoch (ms), domyślnie czas bieżący - 1 dzień | -| mp | numer licznika | -| meter_type_url | typ licznika (np. A+: A%2B, A-: A- ) | -| zone | numer strefy (np. 1, 2) | -| negative | dodanie parametru z dowolną wartością powoduje, że pomiar jest wartością ujemną| - -## Jak dodać wykres do Grafana -### Źródło danych -1) Dodajemy źródło danych Home -> Data sources - Add new datasources: Wyszukujemy JSON API (jeżeli nie ma to musimy dodać) -2) NAME: ENERGA (1) -3) URL: http://twoj_addon:8000 (2) -4) Klikamy Save&test (3) -5) Uwaga: Jeżeli Grafana jest addonem w HA użyj właściwej nazwy hosta dostępnej w docker. - -Nazwa hosta - -Grafana źródło danych - - -### Dashboard -1) Przechodzimy do Dashboards -2) Klikamy New -> New dashboard -> Add visualization -3) Wskazujemy Data source: ENERGA -4) W **Path** wpisujemy: GET: /charts - - (id Twojego licznika) -5) W **Fields** wpisujemy \$.charts\[\*\].czas typu Time oraz $.charts[*].value typu number z aliasem kWh -5) W **Params** wpisujemy Key: start_date Value: $__from -6) W **Params** wpisujemy Key: end_date Value: $__to - -Grafana - -Grafana - -Grafana - -Grafana - -Grafana - -7) W **Params** wpisujemy Key: meter_type_url Value: A+ lub A- jeżeli mamy energię pobieraną i oddawaną, w takim przypadku tworzymy również oddzielne **Query** dla A+ i A- - -Grafana - -8) W **Params** wpisujemy Key: negative Value: OK (może być dowolna wartość) jeżeli chcemy aby wykres był z wartościami ujemnymi (poniżej osi X) - -Grafana - -9) W **Params** aby uzyskać bilans energii należy dodać **Transform**, przy czym wartość energii oddawanej powinna być jako energia ujemna (parametr negative) - -Grafana - -Grafana - -## Znane problemy -* Czasami w aplikacji Mój Licznik włącza się captha (jeżeli masz dużo danych historycznych lub wielokrotnie instalujesz dodatek) - należy poczekać kilka godzin, problem rozwiąże się sam - - -## Uwagi -Dostęp do aktualnej wersji API nie jest zabezpieczony tokenem -Każde przeinstalowanie dodatku pobiera ponownie dane z aplikacji Mój Licznik - -Postaw kawę - diff --git a/srcdev/README.md b/srcdev/README.md deleted file mode 100644 index 98aa77e..0000000 --- a/srcdev/README.md +++ /dev/null @@ -1,128 +0,0 @@ -[![ha_badge](https://img.shields.io/badge/Home%20Assistant-Add%20On-blue.svg)](https://www.home-assistant.io/) -# [Energa meter](https://github.com/tcich/ha-addon-energa-meter) Home Assistant add-on -# Wersja dev -[aarch64-shield]: https://img.shields.io/badge/aarch64-yes-green.svg -[amd64-shield]: https://img.shields.io/badge/amd64-yes-green.svg -[armv6-shield]: https://img.shields.io/badge/armv6-yes-green.svg -[armv7-shield]: https://img.shields.io/badge/armv7-yes-green.svg -[i386-shield]: https://img.shields.io/badge/i386-yes-green.svg -![aarch64-shield] -![amd64-shield] -![armv6-shield] -![armv7-shield] -![i386-shield] - - - -[kawa-logo]: https://github.com/tcich/ha-addon-energa-meter/blob/main/img/buycoffeeto-btn-primary-outline.png -[kawa]: https://buycoffee.to/tcich - -## O dodatku - -To jest dodatek dla [Home Assistant](https://www.home-assistant.io/). Instalacja dodatku [Energa meter](https://github.com/tcich/ha-addon-energa-meter) umożliwia cykliczne pobieranie danych z aplikacji [Mój Licznik - Energa](https://mojlicznik.energa-operator.pl) udostępnianej klientom Operatora energetycznego Energa. - -### Wersja dev -Wersja dev jest wersją developeską, nie należy jej używać w produkcyjnej wersji HA, może powodować różne problemy, może nie działać. - -## Instalacja -1) Dodaj repozytorium do repozytoriów dodatków swojego HA za pomocą poniższego przycisku - -[![Open your Home Assistant instance and show the add add-on repository dialog with a specific repository URL pre-filled.](https://my.home-assistant.io/badges/supervisor_add_addon_repository.svg)](https://my.home-assistant.io/redirect/supervisor_add_addon_repository/?repository_url=https%3A%2F%2Fgithub.com%2Ftcich%2Fha-addon-energa-meter) - -Lub zainstaluj manualnie z Ustawienia -> Dodatki -> Sklep z dodatkami -> ⁞ (Menu) -> Repozytoria -> Wpisz `https://github.com/tcich/hassio-mojlicznik` -> Dodaj. Następnie w ⁞ (Menu) -> Sprawdź aktualizacje (może być konieczne przeładowanie strony) - -2) Odszukaj dodatek na liście dodatków w sklepie z dodatkami i zainstaluj go. - -3) W zakładce konfiguracja uzupełnij nazwę użytkownika oraz hasło do aplikacji Mój Licznik, jeżeli potrzebujesz to zmień udostępniany port dla API - -4) Przejdź do zakładki informacje i uruchom dodatek (pierwsze uruchomienie może trwać kilkanaście minut), jeżeli w logu pojawi się informacja *INFO: Czekam...* oznacza to, że pierwsze inicjalne pobieranie danych zostało ukończone. - - -## Wersja Docker -Aby ruchomić wersję docker należy skorzystać z polecenia poniżej - -``` -docker run -p 8000:8000 -e ENERGA_USERNAME=LoginEnerga -e ENERGA_PASSWORD=HasloEnerga tomcic/energa-meter:v0.1.0 -``` - -Wymagane parametry: - -* ENERGA_USERNAME - nazwa użytkownika w aplikacji Energa Mój licznik -* ENERGA_PASSWORD - hasło użytkownika w aplikacji Energa Mój licznik - - -## Konfiguracja sensorów -Do HA możesz dodać sensory, które zawierają informacje udostępniane przez API - -Poniższa instrukcja zawiera założenia: -* dodatek jest dostępny pod adresem *localhost* na porcie *8000* -* ID Twojego licznika to *123456789* - -1) Ustal ID Twoich liczników, w tym celu przejdź do adresu Twojego HA na porcie 8000 lub innym jeźeli zmieniłeś go w konfiguracji, np. http://192.168.1.10:8000 wyświetli się w formacie json lista dostępnych liczników. -2) W pliku configuration.yaml w HA dodaj następującą konfigurację np.: - -``` -sensor: - - platform: rest - resource: http://localhost:8000/123456789/A%2B/1 - name: "A+ Taryfa 1" - unique_id: 123456789_apt1 - unit_of_measurement: "kWh" - value_template: "{{ value_json.countner.meter_value | round(2) }}" - - platform: rest - resource: http://localhost:8000/123456789/A%2B/2 - name: "A+ Taryfa 2" - unique_id: 123456789_apt2 - unit_of_measurement: "kWh" - value_template: "{{ value_json.countner.meter_value | round(2) }}" -``` -## Suma liczników, bilans -W celu uzyskania sumy liczników, bilansu, itp należy użyć templates: -``` -template: - - sensor: - - name: "Suma liczników" - unit_of_measurement: "kWh" - state: "{{ states('sensor.123456789_apt1') | float + states('sensor.123456789_apt2') | float | round(2) }}" - - sensor: - - name: "Bilans/różnica liczników" - unit_of_measurement: "kWh" - state: "{{ states('sensor.123456789_apt1') | float - states('sensor.123456789_apt2') | float | round(2) }}" -``` - - -### Opis konfiguracji -| element konfiguracji | Opis | -|-------------------|-------------------| -| resource: http://localhost:8000/123456789/A%2B/1 | Adres API z danymi konkretnego licznika, podajemy **localhost** lub nazwę instancji dockera (**Nazwa hosta** z okna dodatku), port, id licznika, rodzaj pomiaru, taryfa| -| name: "A+ Taryfa 1" | Nazwa sensora, wpisz dowolną| -| unique_id | Unikalny ID sensora, nie mogą być w systemie dwa sensory z tym samym ID| -| unit_of_measurement: "kWh" | Jednostka miary, nie zmieniaj chyba, że wiesz co robisz| -| value_template: "{{ value_json.meter.countner.meter_value \| round(2) }}" | Zaokrąglony do dwóch miejsc po przecinku stan sensora| - -## API dla wykresów, np. Grafana -Aby pobrać dane z API w formacie JSON należy użyć adresu http://home_assistant:8000/charts/12729?start_date=1695332400129&end_date=1697924583285&mp=123456789&zone=1 - -### Opis konfiguracji -| element konfiguracji | Opis | -|-------------------|-------------------| -| resource: http://localhost:8000/charts | Adres API z danymi do wykresów, podajemy **localhost** lub nazwę instancji dockera (**Nazwa hosta** z okna dodatku), port, id licznika, rodzaj pomiaru, taryfa| -| start_date | data początkowa danych w formacie epoch (ms), domyślnie czas bieżący | -| end_date | data końcowa danych w formacie epoch (ms), domyślnie czas bieżący - 1 dzień | -| mp | numer licznika | -| meter_type_url | typ licznika (np. A+: A%2B, A-: A- ) | -| zone | numer strefy (np. 1, 2) | - - -## Grafana -Instrukcja konfiguracji dla Grafana znajduje się tutaj [link](https://github.com/tcich/ha-addon-energa-meter/tree/e702ed49436fb9e0b675dcac3001bd9de5aab3c0/srcdev/INSTALL.md) - - -## Znane problemy -Czasami w aplikacji Mój Licznik włącza się captha (jeżeli masz dużo danych historycznych lub wielokrotnie instalujesz dodatek) -Dane wytwórcy (energia oddana oraz bilans) nie są dostępne, prace w tym zakresie trwają. - -## Uwagi -Dostęp do aktualnej wersji API nie jest zabezpieczony tokenem -Każde przeinstalowanie dodatku pobiera ponownie dane z aplikacji Mój Licznik - diff --git a/srcdev/api.py b/srcdev/api.py deleted file mode 100644 index 522a136..0000000 --- a/srcdev/api.py +++ /dev/null @@ -1,242 +0,0 @@ -from peewee import SqliteDatabase -from flask import Flask, jsonify, request, redirect, url_for, abort -from waitress import serve -#from datetime -import datetime -import time, os, logging -from moj_licznik import PPETable, MeterTable, CounterTable, MainChartTable -import urllib.parse - -logger = logging.getLogger("energaMeter.api") - - -path = os.path.dirname(os.path.abspath(__file__)) -db_file = 'database.sqlite' -db = SqliteDatabase(os.path.join(path, db_file)) - -app = Flask(__name__) - -@app.route('/', methods=['GET']) -def root(): - query = PPETable.select() #.where(PPETable.is_active == True) - result_ppes = list(query) - ppes = [] - for p in result_ppes: - meters_query = MeterTable.select().where(MeterTable.ppe_id == p.id) - meter_result = meters_query.execute() - meters = [] - for meter in meter_result: - countners_query = CounterTable.select().where(CounterTable.meter_id == meter.id) - countners_result = countners_query.execute() - countners = [] - for countner in countners_result: - countner = { - 'tariff': countner.tariff, - 'measurement_date': countner.measurement_date, - 'meter_value': countner.meter_value - } - countners.append(countner) - - meter = { - 'meter_type': meter.meter_type, - 'meter_type_url': urllib.parse.quote_plus(meter.meter_type), - 'last_update_date': meter.last_update_date, - 'first_date': meter.first_date, - 'countners': countners - } - meters.append(meter) - - ppe = { - 'name': p.name, - 'id': p.id, - 'type': p.type, - 'isActive': p.is_active, - 'meters': meters - } - ppes.append(ppe) - logger.debug("API: GET /") - - return jsonify({'ppes': ppes}) - -@app.route('/meters', methods=['GET']) -@app.route('/meters/', methods=['GET']) -def meters(): - query = PPETable.select() #.where(PPETable.is_active == True) - result_ppes = list(query) - meters = [] - - for p in result_ppes: - meter = { - 'name': p.name, - 'id': p.id, - 'ppe': p.ppe, - 'number_of_zones': '',#p.number_of_zones, - 'tariffCode': p.tariffCode, - # 'first_date': p.first_date, - 'last_update_date': p.last_update_date, - # 'measurement_date': p.measurement_date, - } - - for i in range(1, p.number_of_zones + 1): - zone_key = f'zone{i}' - daily_chart_key = f'zone{i}_daily_chart_sum' - - zone_value = getattr(p, zone_key) - daily_chart_value = getattr(p, daily_chart_key) - - # Zamień None na zero podczas obliczania sumy - zone_value = float(zone_value) if zone_value is not None else 0 - daily_chart_value = float(daily_chart_value) if daily_chart_value is not None else 0 - - meter[zone_key] = { - 'meter': zone_value, - 'daily_chart': daily_chart_value, - 'sum': zone_value + daily_chart_value - } - - meters.append(meter) - logger.debug("GET /meters") - - return jsonify({'meters': meters}) - - -@app.route('/', methods=['GET']) -@app.route('//', methods=['GET']) -def get_ppe(ppe_id): - meters_query = MeterTable.select().where(MeterTable.ppe_id == ppe_id) - meter_result = meters_query.execute() - - if not meter_result: - abort(404) - - meters = [] - for meter in meter_result: - countners_query = CounterTable.select().where(CounterTable.meter_id == meter.id) - countners_result = countners_query.execute() - countners = [] - for countner in countners_result: - countner = { - 'tariff': countner.tariff, - 'measurement_date': countner.measurement_date, - 'meter_value': countner.meter_value - } - countners.append(countner) - - meter = { - 'meter_type': meter.meter_type, - 'meter_type_url': urllib.parse.quote_plus(meter.meter_type), - 'last_update_date': meter.last_update_date, - 'first_date': meter.first_date, - 'countners': countners - } - meters.append(meter) - logger.debug(f"API: GET /{ppe_id}") - return jsonify({'meters': meters}) - - -@app.route('//', methods=['GET']) -@app.route('///', methods=['GET']) -def get_meter_type(ppe_id, meter_type_url): - meter_type = urllib.parse.unquote(meter_type_url) - meters_query = MeterTable.select().where((MeterTable.ppe_id == str(ppe_id)) & (MeterTable.meter_type == meter_type)) - meter_result = meters_query.execute() - if not meter_result: - abort(404) - - meter = meter_result[0] - countners_query = CounterTable.select().where(CounterTable.meter_id == meter.id) - countners_result = countners_query.execute() - countners = [] - for countner in countners_result: - countner = { - 'tariff': countner.tariff, - 'measurement_date': countner.measurement_date, - 'meter_value': countner.meter_value - } - countners.append(countner) - - meter = { - 'meter_type': meter.meter_type, - 'meter_type_url': urllib.parse.quote_plus(meter.meter_type), - 'last_update_date': meter.last_update_date, - 'first_date': meter.first_date, - 'countners': countners - } - - logger.debug(f"API: GET /{ppe_id}/{meter_type_url}") - return jsonify({'meter': meter}) - -@app.route('///', methods=['GET']) -@app.route('////', methods=['GET']) -def get_countners(ppe_id, meter_type_url, tariff): - meter_type = urllib.parse.unquote(meter_type_url) - meters_query = MeterTable.select().where((MeterTable.ppe_id == str(ppe_id)) & (MeterTable.meter_type == meter_type)) - meter_result = meters_query.execute() - - if not meter_result: - abort(404) - - meter = meter_result[0] - countners_query = CounterTable.select().where((CounterTable.meter_id == meter.id) & (CounterTable.tariff == tariff)) - countners_result = countners_query.execute() - countner = countners_result[0] - countner = { - 'tariff': countner.tariff, - 'measurement_date': countner.measurement_date, - 'meter_value': countner.meter_value - } - - logger.debug(f"API: GET /{ppe_id}/{meter_type_url}/{countner}") - return jsonify({'countner': countner}) - - - -@app.route('/charts', methods=['GET']) -@app.route('/charts/', methods=['GET']) -def charts(): - - - current_time = datetime.datetime.now() - current_time_unix = time.mktime(current_time.timetuple()) - start_time = current_time - datetime.timedelta(days=1) - start_time_unix = time.mktime(start_time.timetuple()) - start_date = request.args.get('start_date', start_time_unix*1000) - end_date = request.args.get('end_date', current_time_unix*1000) - mp = request.args.get('mp', None) - meter_type_url = request.args.get('meter_type_url', None) - zone = request.args.get('zone', None) - negative = request.args.get('negative', type=bool, default=False) - logger.debug(f"API: GET /charts - {start_date} - {end_date}") - query = MainChartTable.select().where((MainChartTable.tm >= int(start_date)) & (MainChartTable.tm <= int(end_date))) - logger.debug(f"{query}") - factor = 1 - if negative: - factor = -1 - if mp: - query = query.where(MainChartTable.mp == mp) - - if meter_type_url: - meter_type = urllib.parse.unquote(meter_type_url) - query = query.where(MainChartTable.meter_type == meter_type) - - if zone: - query = query.where(MainChartTable.zone == zone) - - result_ppes = list(query) - charts = [] - - for p in result_ppes: - czas = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(p.tm/1000)) - chart = { - 'mp': p.mp, - 'meter_type': p.meter_type, - 'meter_type_url': urllib.parse.quote_plus(p.meter_type), - 'zone': p.zone, - 'time_tm': p.tm, - 'time': czas, - 'value': p.value * factor - } - charts.append(chart) - end_time = time.time() - - return jsonify({'charts': charts}) diff --git a/srcdev/config.yaml b/srcdev/config.yaml deleted file mode 100644 index b783a0b..0000000 --- a/srcdev/config.yaml +++ /dev/null @@ -1,22 +0,0 @@ -name: "Energa meter (dev)" -description: "Energa meter addon" -version: "0.1.8" -slug: "energa_meter_dev" -init: false -options: - energa_username: "" - energa_password: "" - log_level: "INFO" -schema: - energa_username: str - energa_password: password - log_level: str -arch: - - aarch64 - - amd64 - - armhf - - armv7 - - i386 -startup: services -ports: - 8000/tcp: 8000 \ No newline at end of file diff --git a/srcdev/cron.py b/srcdev/cron.py deleted file mode 100644 index e69de29..0000000 diff --git a/srcdev/icon.png b/srcdev/icon.png deleted file mode 100644 index edfc4f9..0000000 Binary files a/srcdev/icon.png and /dev/null differ diff --git a/srcdev/img/addon.png b/srcdev/img/addon.png deleted file mode 100644 index 7daebfe..0000000 Binary files a/srcdev/img/addon.png and /dev/null differ diff --git a/srcdev/img/grafana_01.png b/srcdev/img/grafana_01.png deleted file mode 100644 index 6fbbedb..0000000 Binary files a/srcdev/img/grafana_01.png and /dev/null differ diff --git a/srcdev/img/grafana_02.png b/srcdev/img/grafana_02.png deleted file mode 100644 index 72e64e6..0000000 Binary files a/srcdev/img/grafana_02.png and /dev/null differ diff --git a/srcdev/img/grafana_03.png b/srcdev/img/grafana_03.png deleted file mode 100644 index 603c262..0000000 Binary files a/srcdev/img/grafana_03.png and /dev/null differ diff --git a/srcdev/img/grafana_04.png b/srcdev/img/grafana_04.png deleted file mode 100644 index 1cf2b31..0000000 Binary files a/srcdev/img/grafana_04.png and /dev/null differ diff --git a/srcdev/img/grafana_05.png b/srcdev/img/grafana_05.png deleted file mode 100644 index c0819e1..0000000 Binary files a/srcdev/img/grafana_05.png and /dev/null differ diff --git a/srcdev/img/grafana_06.png b/srcdev/img/grafana_06.png deleted file mode 100644 index a4a9233..0000000 Binary files a/srcdev/img/grafana_06.png and /dev/null differ diff --git a/srcdev/logo.png b/srcdev/logo.png deleted file mode 100644 index edfc4f9..0000000 Binary files a/srcdev/logo.png and /dev/null differ diff --git a/srcdev/main.py b/srcdev/main.py deleted file mode 100644 index 6703d12..0000000 --- a/srcdev/main.py +++ /dev/null @@ -1,26 +0,0 @@ -import configparser, time, datetime, os -from moj_licznik import MojLicznik -from pathlib import Path - -def main(): - username = None - password = None - username = os.getenv("USERNAME") - password = os.getenv("PASSWORD") - - print(f"Inicjacja...") - mojLicznik = MojLicznik() - print(f"Logowanie...", username) - mojLicznik.login(username, password) - print(f"Aktualizacja liczników...") - mojLicznik.uppdate_measurments() - print(f"Wyszukiwanie najstarszych danych...") - mojLicznik.update_first_date() - print(f"Pobieranie danych...") - mojLicznik.download_charts(True) - mojLicznik.update_last_days() - mojLicznik.set_daily_zones() - mojLicznik.logout() - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/srcdev/moj_licznik.py b/srcdev/moj_licznik.py deleted file mode 100644 index d10bf26..0000000 --- a/srcdev/moj_licznik.py +++ /dev/null @@ -1,601 +0,0 @@ -from peewee import SqliteDatabase -from datetime import datetime, timedelta, date -import calendar, requests, re, time, json, os, logging -import http.cookiejar as cookiejar -from requests.exceptions import HTTPError -from bs4 import BeautifulSoup -from enum import Enum -from peewee import AutoField, Model, CharField, IntegerField, DateField, BooleanField, CompositeKey, DecimalField, ForeignKeyField, SQL -import urllib.parse - -logger = logging.getLogger("energaMeter") - -path = os.path.dirname(os.path.abspath(__file__)) -db_file = 'database.sqlite' -db = SqliteDatabase(os.path.join(path, db_file)) - -class ChartType(Enum): - DAY = "DAY" - MONTH = "MONTH" - YEAR = "YEAR" - -class PPETable(Model): - id = CharField(primary_key=True) - ppe = CharField(unique=True) - tariffCode = CharField() - type = CharField() - name = CharField() - last_update_date = DateField(null=True) - is_active = BooleanField(default=True) - - class Meta: - database = db - table_name = 'PPE' - constraints = [SQL('UNIQUE (ppe, tariffCode)')] - -class MeterTable(Model): - id = AutoField() # Meter point - ppe_id = ForeignKeyField(PPETable, backref='zones') - meter_type = CharField() - last_update_date = DateField(null=True) - first_date = DateField(null=True) - - class Meta: - database = db - table_name = 'METER' - constraints = [SQL('UNIQUE (ppe_id, meter_type)')] - -class CounterTable(Model): - id = AutoField() - meter_id = ForeignKeyField(MeterTable, backref='meter') - tariff = CharField() - measurement_date = DateField(null=True) - meter_value = DecimalField(max_digits=15, decimal_places=5, null=True) - - class Meta: - database = db - table_name = 'COUNTER' - -# class CounterTable(Model): -# meter_id = ForeignKeyField(MeterTable, backref='zones') -# measurement_date = DateField(null=True) -# meter_value = DecimalField(max_digits=15, decimal_places=5, null=True) - -# class Meta: -# database = db - - -class ChartTable(Model): - id = IntegerField() - meter_type = CharField() - year = IntegerField() - month = IntegerField(null=True) - day = IntegerField(null=True) - value =CharField() - - class Meta: - database = db - table_name = 'CHART_CACHE' - primary_key = CompositeKey('id', 'year', 'month', 'day') - -class MainChartTable(Model): - mp = CharField() - meter_type = CharField() - zone = IntegerField() - tm = IntegerField() - value = DecimalField(max_digits=20, decimal_places=16, null=True) - tarAvg = DecimalField(max_digits=20, decimal_places=16, null=True) - est = BooleanField(default=False) - cplt = BooleanField(default=False) - - class Meta: - database = db - table_name = 'CHART' - primary_key = CompositeKey('mp', 'meter_type', 'zone', 'tm') - -def znajdz_typ_odbiorcy(element): - typ_odbiorcy = '' - div_elements = element.find_all('div', recursive=False) - for div_element in div_elements: - typ_span = div_element.find('span', text='Typ') - if typ_span: - typ_odbiorcy = typ_span.next_sibling.strip() - return typ_odbiorcy - typ_odbiorcy = znajdz_typ_odbiorcy(div_element) # Rekurencyjne przeszukiwanie zagnieżdżonych div - return typ_odbiorcy - -def findCountners(page): - table = page.find('table') - countner_type_list = ["A-", "A+"] - data_list = [] - - # Jeśli znaleźliśmy tabelę, możemy przeszukać jej wiersze i komórki - if table: - for row in table.find_all('tr'): - cells = row.find_all('td') - if len(cells) > 1: - # Pobieramy opis z pierwszej komórki - description = cells[0].text.strip() - - # Pomijamy, jeśli rodzaj_licznika jest pusty - if not description: - continue - - # Usuwamy datę z opisu - description_parts = description.split('\n') - meter_type = description_parts[0][:2].strip() - - if meter_type not in countner_type_list: - continue - - tariff0 = description_parts[0][2:].strip() - tariff = ''.join(filter(str.isdigit, tariff0)) - measurement_date = description_parts[1].strip() - - # Pobieramy dane liczbowe z drugiej komórki - data = cells[1].text.strip() - - # Usuwamy znaki nowej linii i spacje z danych - data = data.replace('\n', '').replace(' ', '') - - data = data.replace(',', '.') - - # Dzielimy dane na część całkowitą i część dziesiętną - parts = data.split('.') - if len(parts) == 2: - integer_part = parts[0] - decimal_part = parts[1] - else: - integer_part = parts[0] - decimal_part = '0' - - data_dict = { - "meter_type": meter_type, - "tariff": tariff, - "measurement_date": measurement_date, - "meter_value": f"{integer_part}.{decimal_part}" - } - - data_list.append(data_dict) - return data_list - -class MojLicznik: - - session = requests.Session() - session.cookies = cookiejar.LWPCookieJar(filename='cookies.txt') - - meter_url = "https://mojlicznik.energa-operator.pl" - - - - def databaseInit(self): - db.create_tables([PPETable], safe=True) - db.create_tables([MeterTable], safe=True) - db.create_tables([CounterTable], safe=True) - db.create_tables([ChartTable], safe=True) - db.create_tables([MainChartTable], safe=True) - - - def __init__(self): - self.username = None - self.password = None - self.loginStatus = False - self.data = [] # Change self.data to a list - self.ppes = [] - self.databaseInit() - - def login(self, _username, _password): - - self.username = _username - self.password = _password - - login_url = f"{self.meter_url}/dp/UserLogin.do" - - self.loginStatus = False - - try: - logger.debug("Pobieram formularz logowania.") - response = self.session.get(login_url) - response.raise_for_status() - if response.url == 'https://mojlicznik.energa-operator.pl/maintenance.html': - logger.critical("Trwają prace serwisowe w Mój Licznik. Logowanie nie jest możliwe, spróbuj później.") - return - - except HTTPError as e: - logger.error(f"Wystąpił błąd HTTP: {e}") - - soup = BeautifulSoup(response.text, 'html.parser') - csrf_token = soup.find('input', {'name': '_antixsrf'})['value'] - - login_data = { - 'j_username': self.username, - 'j_password': self.password, - 'selectedForm': '1', - 'save': 'save', - 'clientOS': 'web', - '_antixsrf': csrf_token - } - - try: - response = self.session.post(login_url, data=login_data) - response.raise_for_status() - - - except HTTPError as e: - logger.error(f"Wystąpił błąd HTTP: {e}") - - soup = BeautifulSoup(response.text, 'html.parser') - - login_error_text = 'Użytkownik lub hasło niepoprawne' - login_error = soup.find('div', text=login_error_text) - - if login_error: - logger.critical(login_error_text) - return - else: - self.loginStatus = True - logger.info(f"Zalogowano") - - body = soup.find('body') - type_value = znajdz_typ_odbiorcy(body) - - logger.debug(f"Typ umowy: {type_value}.") - select_elements = soup.find_all('script', type='text/javascript') - meter_isd = [] - for el in select_elements: - pattern = r"id:\s+(\d+),[\s\S]*?ppe:\s+'([\d\s]+)',[\s\S]*?tariffCode:\s+'([^']+)',[\s\S]*?name:\s+'([^']+)'" - matches = re.search(pattern, el.text) - if matches: - id_value = matches.group(1) - ppe_value = matches.group(2) - tariffCode_value = matches.group(3) - name_value = matches.group(4) - meter_isd.append(id_value) - retrieved_record = PPETable.get_or_none(id=id_value) - if retrieved_record: - logger.info(f"Licznik {id_value} istnieje w systemie.") - if not retrieved_record.is_active: - retrieved_record.is_active = True - retrieved_record.save() - else: - logger.info(f"Licznik {id_value} nie istnieje w systemie, zostanie dodany.") - data = PPETable.create( - id=id_value, - ppe=ppe_value, - tariffCode=tariffCode_value, - type=type_value, - name=name_value - ) - update_query = PPETable.update(is_active=0).where(PPETable.id.not_in(meter_isd)) - update_query.execute() - - def logout(self): - logout_url = f"{self.meter_url}/dp/MainLogout.go" - try: - response = self.session.get(logout_url) - response.raise_for_status() - self.loginStatus = False - logger.info(f"Wylogowano.") - except HTTPError as e: - logger.error(f"Wystąpił błąd HTTP: {e}") - - def update_countners(self): - - query = PPETable.select().where(PPETable.is_active == True) - result_ppes = query.execute() - for p in result_ppes: - meter_url = f"{self.meter_url}/dp/UserData.do?mpc={p.id}&ppe={p.ppe}" - try: - response = self.session.get(meter_url) - response.raise_for_status() - soup = BeautifulSoup(response.text, 'html.parser') - countners_dict = findCountners(soup) - - for c in countners_dict: - mn, mu = MeterTable.get_or_create(ppe_id=p.id, meter_type=c['meter_type']) - mn.last_update_date = datetime.now() - mn.save() - cn, cu = CounterTable.get_or_create( - meter_id = mn.id, - tariff=c['tariff'] - ) - - cn.meter_value = c['meter_value'] - cn.measurement_date = c['measurement_date'] - cn.save() - - logger.info(f"Zapisano stan licznika {p.id} {c['meter_type']} taryfa {c['tariff']} z dnia: {c['measurement_date']} : {c['meter_value']}") - except HTTPError as e: - logger.error(f"Wystąpił błąd HTTP: {e}") - - def update_first_date(self): - ppes_query = PPETable.select().where(PPETable.is_active == True) - result_ppes = ppes_query.execute() - for p in result_ppes: - meters_query = MeterTable.select().where((MeterTable.ppe_id == p.id) & (MeterTable.first_date.is_null(True))) - meters_result = meters_query.execute() - - for meter in meters_result: - meter_type = meter.meter_type - - print(f"Szukam najstarsze dane historyczne licznika {p.name} (PPE: {p.ppe}, {p.id}) typ: {meter_type}") - meter_point = p.id - max_years_back = 5 - start_date = datetime.now() - last_chart_year = None - for n in range(max_years_back + 1): - first_day_of_year = datetime(start_date.year-n, 1, 1) - data_json = self.download_chart(ChartType.YEAR, first_day_of_year, meter_point, meter_type) - if data_json: - data = json.loads(data_json) - if data and data.get("mainChart") and len(data["mainChart"]) > 0: - last_chart_year = first_day_of_year.year - last_chart_month = None - max_month = 12 - for n in range(max_month, 0, -1): - first_day_of_month = datetime(last_chart_year, n, 1) - data_json = self.download_chart(ChartType.MONTH, first_day_of_month, meter_point, meter_type) - if data_json: - data = json.loads(data_json) - if data and data.get("mainChart") and len(data["mainChart"]) > 0: - last_chart_month = n - last_chart_day = None - max_day = 31 - first_day_of_day = datetime(last_chart_year, last_chart_month, 1) - _, max_day = calendar.monthrange(first_day_of_day.year, first_day_of_day.month) - for n in range(max_day, 0, -1): - first_day_of_day = datetime(last_chart_year, last_chart_month, n) - data_json = self.download_chart(ChartType.DAY, first_day_of_day, meter_point, meter_type) - if data_json: - data = json.loads(data_json) - if data and data.get("mainChart") and len(data["mainChart"]) > 0: - last_chart_day = n - first_date = datetime(last_chart_year, last_chart_month, last_chart_day).date() - print(f"Najstarsze dane historyczne dla licznika {p.name} (PPE: {p.ppe}, {p.id}) typ: {meter_type}: {first_date}") - meter.first_date = first_date - meter.save() - - def save_main_charts(self, mp, vals, m_type): - for val in vals: - try: - logger.debug(f"save_main_charts: mp: {mp}, val: {val}, meter_type: {m_type}") - z = val["zones"] - if z[0]: - # MainChartTable.get_or_create(tm = val["tm"], zone = 1, value = z[0], tarAvg=val["tarAvg"], est=val["est"], cplt=val["cplt"]) - try: - existing_record = MainChartTable.get((MainChartTable.meter_type == m_type) & (MainChartTable.mp == mp) & (MainChartTable.tm == val["tm"]) & (MainChartTable.zone == 1)) - except MainChartTable.DoesNotExist: - # Jeśli rekord nie istnieje, utwórz nowy - MainChartTable.create( - mp=mp, - meter_type=m_type, - tm=val["tm"], - zone=1, - value=z[0], - tarAvg=val["tarAvg"], - est=val["est"], - cplt=val["cplt"] - ) - - if z[1]: - try: - existing_record = MainChartTable.get((MainChartTable.meter_type == m_type) & (MainChartTable.mp == mp) & (MainChartTable.tm == val["tm"]) & (MainChartTable.zone == 2)) - except MainChartTable.DoesNotExist: - # Jeśli rekord nie istnieje, utwórz nowy - MainChartTable.create( - mp=mp, - meter_type=m_type, - tm=val["tm"], - zone=2, - value=z[1], - tarAvg=val["tarAvg"], - est=val["est"], - cplt=val["cplt"] - ) - - if z[2]: - try: - existing_record = MainChartTable.get((MainChartTable.meter_type == m_type) & (MainChartTable.mp == mp) & (MainChartTable.tm == val["tm"]) & (MainChartTable.zone == 3)) - except MainChartTable.DoesNotExist: - # Jeśli rekord nie istnieje, utwórz nowy - MainChartTable.create( - mp=mp, - meter_type=m_type, - tm=val["tm"], - zone=3, - value=z[2], - tarAvg=val["tarAvg"], - est=val["est"], - cplt=val["cplt"] - ) - - except Exception as e: - logging.error(f"Wystąpił błąd: {str(e)}") - - return None - - def download_chart(self, type, date, meter_point, meter_type, update_mode=False): - - if type == ChartType.DAY: - chart_type = "DAY" - first_day = datetime(date.year, date.month, date.day) - tsm_date = int(time.mktime(first_day.timetuple()) * 1000) - - if type == ChartType.MONTH: - chart_type = "MONTH" - first_day = datetime(date.year, date.month, 1) - tsm_date = int(time.mktime(first_day.timetuple()) * 1000) - - if type == ChartType.YEAR: - chart_type = "YEAR" - first_day = datetime(date.year, 1, 1) - tsm_date = int(time.mktime(first_day.timetuple()) * 1000) - - # meter_type = 'A+' - chart_url = f"{self.meter_url}/dp/resources/chart?mainChartDate={tsm_date}&type={chart_type}&meterPoint={meter_point}&mo={urllib.parse.quote_plus(meter_type)}" - logger.debug(f"chart_url: {chart_url}") - try: - response = self.session.get(chart_url) - data = json.loads(response.text) - response.raise_for_status() - if data["response"]: - id = data["response"]["meterPoint"] - mainChartDate = data["response"]["mainChartDate"] - mainChart = data["response"]["mainChart"] - if type == ChartType.DAY: - self.save_main_charts(meter_point, mainChart, meter_type) - - date = int(mainChartDate) / 1000 - month = None - day = None - dt = datetime.fromtimestamp(date) - year = dt.year - if type == ChartType.MONTH: - month = dt.month - if type == ChartType.DAY: - month = dt.month - day = dt.day - json_dump = json.dumps(data["response"], ensure_ascii=False) - if update_mode: - try: - chart_record = ChartTable.get(id=id,year=year, month=month, day=day) - chart_record.value = json_dump - chart_record.save() - except ChartTable.DoesNotExist: - chart_record = ChartTable.create(id=id, meter_type=meter_type, value=json_dump, year=year, month=month, day=day) - - else: - try: - ChartTable.create(id=id, meter_type=meter_type, value=json_dump, year=year, month=month, day=day) - except: - pass - return json_dump - return None - except HTTPError as e: - logger.error(f"Wystąpił błąd HTTP: {e}") - - def download_charts(self, full_mode=False): - query = PPETable.select().where(PPETable.is_active == True) - result_ppes = query.execute() - for p in result_ppes: - meters_query = MeterTable.select().where((MeterTable.ppe_id == p.id)) # // & (MeterTable.first_date.is_null(True))) - meters_result = meters_query.execute() - - for meter in meters_result: - meter_type = meter.meter_type - - logger.info(f"Pobieram dane historyczne dla {p.name} ({p.id}) typ: {meter_type}") - current_date = meter.first_date - if not full_mode: - current_date = meter.last_update_date - timedelta(days=1) - - while current_date <= date.today(): - try: - record = ChartTable.get(id=p.id, meter_type=meter_type, year=current_date.year, month=current_date.month, day=current_date.day) - # Jeśli rekord o określonych wartościach klucza głównego istnieje, zostanie pobrany. - logger.debug(f"Posiadam dane historyczne dla {p.name} ({p.id}) typ: {meter_type} na dzień: {current_date}") - except ChartTable.DoesNotExist: - self.download_chart(ChartType.DAY, current_date, p.id, meter_type) - logger.debug(f"Pobieram dane historyczne dla {p.name} ({p.id}) typ: {meter_type} na dzień: {current_date}") - current_date += timedelta(days=1) - - def update_last_days(self): - today = datetime.today().date() - query = PPETable.select().where(PPETable.is_active == True) - result_ppes = query.execute() - - for p in result_ppes: - meters_query = MeterTable.select().where((MeterTable.ppe_id == p.id) & (MeterTable.first_date.is_null(True))) - meters_result = meters_query.execute() - - for meter in meters_result: - meter_type = meter.meter_type - - logger.info(f"Aktualizacja danych bieżących dla {p.name} ({p.id}) typ: {meter_type}") - if not p.last_update_date: - p.last_update_date = today - timedelta(days=5) - p.save() - last_update_date = p.last_update_date - timedelta(days=1) - while last_update_date <= today: - logger.debug(f"Aktualizacja danych dla {p.name} ({p.id}) typ: {meter_type} na dzień: {last_update_date}") - self.download_chart(ChartType.DAY, last_update_date, p.id, meter_type, True) - p.last_update_date = last_update_date - p.save() - last_update_date += timedelta(days=1) - - def get_current_meters(self, add_daily_char_data=False): - - query = PPETable.select().where(PPETable.is_active == True) - result_ppes = query.execute() - for p in result_ppes: - if add_daily_char_data: - query = ChartTable.select().where((ChartTable.id == p.id) & (ChartTable.year == p.measurement_date.year) & (ChartTable.month == p.measurement_date.month) & (ChartTable.day == p.measurement_date.day)) - query_count = query.count() - if (query_count > 0): - query_first = query.first() - value_json = json.loads(query_first.value) - print(query_first.value) - zones = value_json.get("zones", []) - if zones: - zone1_data = zones[0] - zone1_main_chart = zone1_data.get("mainChart", []) - - # def set_daily_zones(self): - # query = PPETable.select().where(PPETable.is_active == True) - # result_ppes = query.execute() - - # for p in result_ppes: - # query = ChartTable.select().where( - # (ChartTable.id == p.id) & - # ((ChartTable.year > p.measurement_date.year) | - # ((ChartTable.year == p.measurement_date.year) & - # (ChartTable.month > p.measurement_date.month)) | - # ((ChartTable.year == p.measurement_date.year) & - # (ChartTable.month == p.measurement_date.month) & - # (ChartTable.day >= p.measurement_date.day)) - # )) - - # zones_sums = {f"zone{i+1}_daily_chart_sum": 0.0 for i in range(3)} - - # for chart_entry in query: - # value_json = json.loads(chart_entry.value) - # main_chart = value_json.get("mainChart", []) - - # for entry in main_chart: - # zones = entry.get("zones", []) - - # for i, value in enumerate(zones): - # if value is not None: - # zones_sums[f"zone{i+1}_daily_chart_sum"] += value - - # for key, value in zones_sums.items(): - # setattr(p, key, value) - # p.save() - - - def print_summary_zones(self): - query = PPETable.select().where(PPETable.is_active == True) - result_ppes = query.execute() - for p in result_ppes: - zon1 = (p.zone1 if p.zone1 is not None else 0 ) + (p.zone1_daily_chart_sum if p.zone1_daily_chart_sum is not None else 0) - zon2 = (p.zone2 if p.zone2 is not None else 0 ) + (p.zone2_daily_chart_sum if p.zone2_daily_chart_sum is not None else 0) - zon3 = (p.zone3 if p.zone3 is not None else 0 ) + (p.zone3_daily_chart_sum if p.zone3_daily_chart_sum is not None else 0) - print(f"{p.name} : {round(zon1, 5)} " - f"{round(zon2,5)} " - f"{round(zon3,5)}") - - def get_current_meters_list(self): - query = PPETable.select().where(PPETable.is_active == True) - return query.execute() - - def get_current_meter_value(self, meter_id, zone): - if zone == "zone1": - pPETable = PPETable.get(PPETable.id == meter_id) - return pPETable.zone1 - if zone == "zone2": - pPETable = PPETable.get(PPETable.id == meter_id) - return pPETable.zone2 - if zone == "zone3": - pPETable = PPETable.get(PPETable.id == meter_id) - return pPETable.zone3 - return None \ No newline at end of file diff --git a/srcdev/requirements.txt b/srcdev/requirements.txt deleted file mode 100644 index d10d411..0000000 Binary files a/srcdev/requirements.txt and /dev/null differ diff --git a/srcdev/run.sh b/srcdev/run.sh deleted file mode 100644 index a85830e..0000000 --- a/srcdev/run.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/with-contenv bashio - -export USERNAME=$(bashio::config 'energa_username') -export PASSWORD=$(bashio::config 'energa_password') -export LOG_LEVEL=$(bashio::config 'log_level') - -bashio::log.info "Uruchamiam API" -python run.py -