modified: .gitignore

modified:   src/CHANGELOG.md
	modified:   src/Dockerfile
	modified:   src/INSTALL.md
	modified:   src/README.md
	modified:   src/api.py
	modified:   src/config.yaml
	deleted:    src/cron.py
	modified:   src/img/grafana_02.png
	modified:   src/img/grafana_03.png
	modified:   src/img/grafana_06.png
	deleted:    src/img/grafana_07.png
	renamed:    srcdev/img/grafana_08.png -> src/img/grafana_08.png
	renamed:    srcdev/img/grafana_09.png -> src/img/grafana_09.png
	renamed:    srcdev/img/grafana_10.png -> src/img/grafana_10.png
	renamed:    srcdev/log_config.py -> src/log_config.py
	modified:   src/main.py
	modified:   src/moj_licznik.py
	renamed:    srcdev/run.py -> src/run.py
	modified:   src/run.sh
	deleted:    srcdev/CHANGELOG.md
	deleted:    srcdev/Dockerfile
	deleted:    srcdev/INSTALL.md
	deleted:    srcdev/README.md
	deleted:    srcdev/api.py
	deleted:    srcdev/config.yaml
	deleted:    srcdev/cron.py
	deleted:    srcdev/icon.png
	deleted:    srcdev/img/addon.png
	deleted:    srcdev/img/grafana_01.png
	deleted:    srcdev/img/grafana_02.png
	deleted:    srcdev/img/grafana_03.png
	deleted:    srcdev/img/grafana_04.png
	deleted:    srcdev/img/grafana_05.png
	deleted:    srcdev/img/grafana_06.png
	deleted:    srcdev/logo.png
	deleted:    srcdev/main.py
	deleted:    srcdev/moj_licznik.py
	deleted:    srcdev/requirements.txt
	deleted:    srcdev/run.sh
This commit is contained in:
TC
2023-11-06 19:45:39 +01:00
parent 796fc615b3
commit 1e498d6856
40 changed files with 593 additions and 1693 deletions

1
.gitignore vendored
View File

@@ -3,6 +3,7 @@
src/*.sqlite src/*.sqlite
src/config.ini src/config.ini
src/rund.sh src/rund.sh
src.old/*
import.py import.py
check.py check.py
*.sqlite *.sqlite

View File

@@ -1,4 +1,18 @@
## v0.1.0 [2023-10-21] # EnergaMeter
- Wersja beta ## 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] ## v0.1.1 [2023-10-22]
- Dodano obsługę błędnego logowania - Dodano obsługę błędnego logowania
## v0.1.0 [2023-10-21]
- Wersja beta

View File

@@ -6,12 +6,12 @@ RUN apk add --update py3-pip
# Copy data for add-on # Copy data for add-on
COPY run.sh / COPY run.sh /
COPY run.py /
COPY requirements.txt / COPY requirements.txt /
COPY main.py / COPY main.py /
COPY api.py / COPY api.py /
COPY cron.py /
# COPY database.sqlite /
COPY moj_licznik.py / COPY moj_licznik.py /
COPY log_config.py /
RUN chmod a+x /run.sh RUN chmod a+x /run.sh
RUN pip install -r requirements.txt RUN pip install -r requirements.txt

View File

@@ -1,6 +1,6 @@
[![ha_badge](https://img.shields.io/badge/Home%20Assistant-Add%20On-blue.svg)](https://www.home-assistant.io/) [![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 # [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 [aarch64-shield]: https://img.shields.io/badge/aarch64-yes-green.svg
[amd64-shield]: https://img.shields.io/badge/amd64-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 [armv6-shield]: https://img.shields.io/badge/armv6-yes-green.svg
@@ -12,13 +12,19 @@
![armv7-shield] ![armv7-shield]
![i386-shield] ![i386-shield]
<a href="https://buycoffee.to/tcich"><img src="../img/logo-buycoffee-wide.jpg" width=200 alt="Postaw kawę"></a>
[kawa-logo]: https://github.com/tcich/ha-addon-energa-meter/blob/main/img/buycoffeeto-btn-primary-outline.png
[kawa]: https://buycoffee.to/tcich
<a href="https://buycoffee.to/tcich"><img src="../img/logo-buycoffee-wide.jpg" width=200 alt="Postaw kawę"></a>
## O dodatku ## 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. 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 ## Instalacja
1) Dodaj repozytorium do repozytoriów dodatków swojego HA za pomocą poniższego przycisku 1) Dodaj repozytorium do repozytoriów dodatków swojego HA za pomocą poniższego przycisku
@@ -47,59 +53,53 @@ Wymagane parametry:
## Konfiguracja sensorów ## 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: sensor:
- platform: rest - platform: rest
resource: http://localhost:8000/meters/12335379 resource: http://localhost:8000/123456789/A%2B/1
name: "Energia aktualna T1" name: "A+ Taryfa 1"
unique_id: 12335379_sumz1 unique_id: 123456789_apt1
unit_of_measurement: "kWh" unit_of_measurement: "kWh"
value_template: "{{ value_json.meter.zone1.meter | round(2) }}" value_template: "{{ value_json.countner.meter_value | round(2) }}"
- platform: rest - platform: rest
resource: http://localhost:8000/meters/12335379 resource: http://localhost:8000/123456789/A%2B/2
name: "Dzienny odczyt licznika" name: "A+ Taryfa 2"
unique_id: 12335379_meterz1 unique_id: 123456789_apt2
unit_of_measurement: "kWh" unit_of_measurement: "kWh"
value_template: "{{ value_json.meter.zone1.sum | round(2) }}" value_template: "{{ value_json.countner.meter_value | 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) }}"
``` ```
# Opis konfiguracji ### Opis konfiguracji
| element konfiguracji | Opis | | 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| | 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: "Energia aktualna" | Nazwa sensora, wpisz dowolną| | name: "A+ Taryfa 1" | Nazwa sensora, wpisz dowolną|
| unique_id | Unikalny ID sensora, nie mogą być w systemie dwa sensory z tym samym ID| | 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| | 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| | value_template: "{{ value_json.meter.countner.meter_value \| 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)|
## API dla wykresów, np. Grafana ## 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: ### Opis konfiguracji
* 12729 - jest to ID licznika | element konfiguracji | Opis |
* start_date - początek okresu w milisekundach wg. standardu EPOCH (timestamp) |-------------------|-------------------|
* end_date - koniec okresu w milisekundach wg. standardu EPOCH (timestamp) | 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 ## Jak dodać wykres do Grafana
### Źródło danych ### Źródło danych
@@ -118,12 +118,13 @@ gdzie:
1) Przechodzimy do Dashboards 1) Przechodzimy do Dashboards
2) Klikamy New -> New dashboard -> Add visualization 2) Klikamy New -> New dashboard -> Add visualization
3) Wskazujemy Data source: ENERGA 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 **Fields** wpisujemy \$.charts\[\*\].czas typu Time oraz $.charts[*].value typu number z aliasem kWh
5) W **Params** wpisujemy Key: start_date Value: $__from 5) W **Params** wpisujemy Key: start_date Value: $__from
6) W **Params** wpisujemy Key: end_date Value: $__to 6) W **Params** wpisujemy Key: end_date Value: $__to
<img src="img/grafana_06.png" style="width: 80%;" alt="Grafana"> <img src="img/grafana_06.png" style="width: 80%;" alt="Grafana">
<img src="img/grafana_02.png" style="width: 80%;" alt="Grafana"> <img src="img/grafana_02.png" style="width: 80%;" alt="Grafana">
@@ -134,10 +135,23 @@ gdzie:
<img src="img/grafana_05.png" style="width: 80%;" alt="Grafana"> <img src="img/grafana_05.png" style="width: 80%;" alt="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-
<img src="img/grafana_07.png" style="width: 80%;" alt="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)
<img src="img/grafana_08.png" style="width: 80%;" alt="Grafana">
9) W **Params** aby uzyskać bilans energii należy dodać **Transform**, przy czym wartość energii oddawanej powinna być jako energia ujemna (parametr negative)
<img src="img/grafana_09.png" style="width: 80%;" alt="Grafana">
<img src="img/grafana_10.png" style="width: 80%;" alt="Grafana">
## Znane problemy ## Znane problemy
Czasami w aplikacji Mój Licznik włącza się captha (jeżeli masz dużo danych historycznych lub wielokrotnie instalujesz dodatek) * 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
Dane wytwórcy (energia oddana oraz bilans) nie są dostępne, prace w tym zakresie trwają.
## Uwagi ## Uwagi
Dostęp do aktualnej wersji API nie jest zabezpieczony tokenem Dostęp do aktualnej wersji API nie jest zabezpieczony tokenem

View File

@@ -1,6 +1,6 @@
[![ha_badge](https://img.shields.io/badge/Home%20Assistant-Add%20On-blue.svg)](https://www.home-assistant.io/) [![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 # [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 [aarch64-shield]: https://img.shields.io/badge/aarch64-yes-green.svg
[amd64-shield]: https://img.shields.io/badge/amd64-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 [armv6-shield]: https://img.shields.io/badge/armv6-yes-green.svg
@@ -13,10 +13,17 @@
![i386-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 ## 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. 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 ## Instalacja
1) Dodaj repozytorium do repozytoriów dodatków swojego HA za pomocą poniższego przycisku 1) Dodaj repozytorium do repozytoriów dodatków swojego HA za pomocą poniższego przycisku
@@ -45,64 +52,72 @@ Wymagane parametry:
## Konfiguracja sensorów ## 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: sensor:
- platform: rest - platform: rest
resource: http://localhost:8000/meters/12335379 resource: http://localhost:8000/123456789/A%2B/1
name: "Energia aktualna T1" name: "A+ Taryfa 1"
unique_id: 12335379_sumz1 unique_id: 123456789_apt1
unit_of_measurement: "kWh" unit_of_measurement: "kWh"
value_template: "{{ value_json.meter.zone1.meter | round(2) }}" value_template: "{{ value_json.countner.meter_value | round(2) }}"
- platform: rest - platform: rest
resource: http://localhost:8000/meters/12335379 resource: http://localhost:8000/123456789/A%2B/2
name: "Dzienny odczyt licznika" name: "A+ Taryfa 2"
unique_id: 12335379_meterz1 unique_id: 123456789_apt2
unit_of_measurement: "kWh" unit_of_measurement: "kWh"
value_template: "{{ value_json.meter.zone1.sum | round(2) }}" value_template: "{{ value_json.countner.meter_value | round(2) }}"
- platform: rest ```
resource: http://localhost:8000/meters/12335379 ## Suma liczników, bilans
name: "Energia aktualna T2" W celu uzyskania sumy liczników, bilansu, itp należy użyć templates:
unique_id: 12335379_sumz2 ```
template:
- sensor:
- name: "Suma liczników"
unit_of_measurement: "kWh" unit_of_measurement: "kWh"
value_template: "{{ value_json.meter.zone2.meter | round(2) }}" state: "{{ states('sensor.123456789_apt1') | float + states('sensor.123456789_apt2') | float | round(2) }}"
- platform: rest - sensor:
resource: http://localhost:8000/meters/12335379 - name: "Bilans/różnica liczników"
name: "Dzienny odczyt licznika"
unique_id: 12335379_meterz2
unit_of_measurement: "kWh" unit_of_measurement: "kWh"
value_template: "{{ value_json.meter.zone2.sum | round(2) }}" state: "{{ states('sensor.123456789_apt1') | float - states('sensor.123456789_apt2') | float | round(2) }}"
``` ```
# Opis konfiguracji
### Opis konfiguracji
| element konfiguracji | Opis | | 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| | 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: "Energia aktualna" | Nazwa sensora, wpisz dowolną| | name: "A+ Taryfa 1" | Nazwa sensora, wpisz dowolną|
| unique_id | Unikalny ID sensora, nie mogą być w systemie dwa sensory z tym samym ID| | 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| | 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| | value_template: "{{ value_json.meter.countner.meter_value \| 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)|
## API dla wykresów, np. Grafana ## 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 ## Grafana
Instrukcja konfiguracji dla Grafana znajduje się tutaj [link](https://github.com/tcich/ha-addon-energa-meter/tree/e702ed49436fb9e0b675dcac3001bd9de5aab3c0/src/INSTALL.md) Instrukcja konfiguracji dla Grafana znajduje się tutaj [link](https://github.com/tcich/ha-addon-energa-meter/tree/e702ed49436fb9e0b675dcac3001bd9de5aab3c0/src/INSTALL.md)
## Znane problemy ## Znane problemy
Czasami w aplikacji Mój Licznik włącza się captha (jeżeli masz dużo danych historycznych lub wielokrotnie instalujesz dodatek) 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ą. 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 ## Uwagi
Dostęp do aktualnej wersji API nie jest zabezpieczony tokenem Dostęp do aktualnej wersji API nie jest zabezpieczony tokenem
Każde przeinstalowanie dodatku pobiera ponownie dane z aplikacji Mój Licznik Każde przeinstalowanie dodatku pobiera ponownie dane z aplikacji Mój Licznik

View File

@@ -1,37 +1,67 @@
from peewee import SqliteDatabase 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 from waitress import serve
import time #from datetime
from datetime import datetime import datetime
from moj_licznik import PPETable, MainChartTable 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__) app = Flask(__name__)
db = SqliteDatabase('database.sqlite')
@app.route('/', methods=['GET']) @app.route('/', methods=['GET'])
def root_redirect(): def root():
query = PPETable.select().where(PPETable.is_active == True) query = PPETable.select() #.where(PPETable.is_active == True)
result_ppes = list(query) result_ppes = list(query)
meters = [] ppes = []
for p in result_ppes: for p in result_ppes:
meter = { meters_query = MeterTable.select().where(MeterTable.ppe_id == p.id)
'name': p.name, meter_result = meters_query.execute()
'id': p.id 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) meters.append(meter)
if DEBUG:
print("API: GET /")
return jsonify({'meters': meters}) 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'])
@app.route('/meters/', methods=['GET'])
def meters(): def meters():
query = PPETable.select().where(PPETable.is_active == True) query = PPETable.select() #.where(PPETable.is_active == True)
result_ppes = list(query) result_ppes = list(query)
meters = [] meters = []
@@ -40,11 +70,11 @@ def meters():
'name': p.name, 'name': p.name,
'id': p.id, 'id': p.id,
'ppe': p.ppe, 'ppe': p.ppe,
'number_of_zones': p.number_of_zones, 'number_of_zones': '',#p.number_of_zones,
'tariffCode': p.tariffCode, 'tariffCode': p.tariffCode,
'first_date': p.first_date, # 'first_date': p.first_date,
'last_update_date': p.last_update_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): for i in range(1, p.number_of_zones + 1):
@@ -65,139 +95,148 @@ def meters():
} }
meters.append(meter) meters.append(meter)
if DEBUG: logger.debug("GET /meters")
print("API: GET /")
return jsonify({'meters': meters}) return jsonify({'meters': meters})
@app.route('/meters/<int:meter_id>', 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: @app.route('/<int:ppe_id>', methods=['GET'])
p = result_ppes[0] # There should be only one matching record @app.route('/<int:ppe_id>/', 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 = {
'name': p.name, 'meter_type': meter.meter_type,
'id': p.id, 'meter_type_url': urllib.parse.quote_plus(meter.meter_type),
'ppe': p.ppe, 'last_update_date': meter.last_update_date,
'number_of_zones': p.number_of_zones, 'first_date': meter.first_date,
'tariffCode': p.tariffCode, 'countners': countners
'first_date': p.first_date, }
'last_update_date': p.last_update_date, meters.append(meter)
'measurement_date': p.measurement_date logger.debug(f"API: GET /{ppe_id}")
return jsonify({'meters': meters})
@app.route('/<int:ppe_id>/<meter_type_url>', methods=['GET'])
@app.route('/<int:ppe_id>/<meter_type_url>/', 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
} }
for i in range(1, p.number_of_zones + 1): logger.debug(f"API: GET /{ppe_id}/{meter_type_url}")
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}")
return jsonify({'meter': meter}) return jsonify({'meter': meter})
else:
return jsonify({'error': 'Meter not found'}, 404) @app.route('/<int:ppe_id>/<meter_type_url>/<tariff>', methods=['GET'])
@app.route('/<int:ppe_id>/<meter_type_url>/<tariff>/', 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/<int:meter_id>', 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: @app.route('/charts', methods=['GET'])
# p = result_ppes[0] # There should be only one matching record @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
# }
# for i in range(1, p.number_of_zones + 1): current_time = datetime.datetime.now()
# zone_key = f'zone{i}' current_time_unix = time.mktime(current_time.timetuple())
# daily_chart_key = f'zone{i}_daily_chart_sum' 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)
# meter[zone_key] = getattr(p, zone_key) if meter_type_url:
# meter[daily_chart_key] = getattr(p, daily_chart_key) meter_type = urllib.parse.unquote(meter_type_url)
query = query.where(MainChartTable.meter_type == meter_type)
# print(f"API: GET /meters/{meter_id}") if zone:
# return jsonify({'meter': meter}) query = query.where(MainChartTable.zone == zone)
# else:
# return jsonify({'error': 'Meter not found'}, 404)
@app.route('/charts/<mp>', 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) result_ppes = list(query)
charts = [] charts = []
for p in result_ppes: for p in result_ppes:
czas = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(p.tm/1000)) czas = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(p.tm/1000))
chart = { chart = {
'mp': p.mp, 'mp': p.mp,
'meter_type': p.meter_type,
'meter_type_url': urllib.parse.quote_plus(p.meter_type),
'zone': p.zone, 'zone': p.zone,
'tm': p.tm, 'time_tm': p.tm,
'czas': czas, 'time': czas,
'value': p.value 'value': p.value * factor
} }
charts.append(chart) charts.append(chart)
end_time = time.time() end_time = time.time()
if DEBUG:
print(f"API: GET / - {start_date} - {end_date}")
return jsonify({'charts': charts}) return jsonify({'charts': charts})
@app.route('/<mp>/<zone>', 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)

View File

@@ -1,14 +1,16 @@
name: "Energa meter" name: "Energa meter"
description: "Energa meter addon" description: "Energa meter addon"
version: "0.1.1" version: "1.0.0"
slug: "energa_meter" slug: "energa_meter"
init: false init: false
options: options:
energa_username: "" energa_username: ""
energa_password: "" energa_password: ""
log_level: "INFO"
schema: schema:
energa_username: str energa_username: str
energa_password: password energa_password: password
log_level: str
arch: arch:
- aarch64 - aarch64
- amd64 - amd64

View File

@@ -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()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

View File

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 108 KiB

View File

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 65 KiB

View File

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

View File

@@ -3,16 +3,8 @@ from moj_licznik import MojLicznik
from pathlib import Path from pathlib import Path
def main(): def main():
plik = Path('config.ini')
username = None username = None
password = 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") username = os.getenv("USERNAME")
password = os.getenv("PASSWORD") password = os.getenv("PASSWORD")
@@ -20,7 +12,6 @@ def main():
mojLicznik = MojLicznik() mojLicznik = MojLicznik()
print(f"Logowanie...", username) print(f"Logowanie...", username)
mojLicznik.login(username, password) mojLicznik.login(username, password)
if mojLicznik.loginStatus:
print(f"Aktualizacja liczników...") print(f"Aktualizacja liczników...")
mojLicznik.uppdate_measurments() mojLicznik.uppdate_measurments()
print(f"Wyszukiwanie najstarszych danych...") print(f"Wyszukiwanie najstarszych danych...")

View File

@@ -1,13 +1,18 @@
from peewee import SqliteDatabase from peewee import SqliteDatabase
from datetime import datetime, timedelta, date 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 import http.cookiejar as cookiejar
from requests.exceptions import HTTPError from requests.exceptions import HTTPError
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from enum import Enum 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): class ChartType(Enum):
DAY = "DAY" DAY = "DAY"
@@ -15,26 +20,54 @@ class ChartType(Enum):
YEAR = "YEAR" YEAR = "YEAR"
class PPETable(Model): class PPETable(Model):
ppe = CharField() id = CharField(primary_key=True)
ppe = CharField(unique=True)
tariffCode = CharField() tariffCode = CharField()
type = CharField()
name = 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)
is_active = BooleanField(default=True)
measurement_date = DateField(null=True)
first_date = DateField(null=True)
last_update_date = DateField(null=True) last_update_date = DateField(null=True)
is_active = BooleanField(default=True)
class Meta: class Meta:
database = db 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): class ChartTable(Model):
id = IntegerField() id = IntegerField()
meter_type = CharField()
year = IntegerField() year = IntegerField()
month = IntegerField(null=True) month = IntegerField(null=True)
day = IntegerField(null=True) day = IntegerField(null=True)
@@ -42,10 +75,12 @@ class ChartTable(Model):
class Meta: class Meta:
database = db database = db
table_name = 'CHART_CACHE'
primary_key = CompositeKey('id', 'year', 'month', 'day') primary_key = CompositeKey('id', 'year', 'month', 'day')
class MainChartTable(Model): class MainChartTable(Model):
mp = CharField() mp = CharField()
meter_type = CharField()
zone = IntegerField() zone = IntegerField()
tm = IntegerField() tm = IntegerField()
value = DecimalField(max_digits=20, decimal_places=16, null=True) value = DecimalField(max_digits=20, decimal_places=16, null=True)
@@ -55,9 +90,74 @@ class MainChartTable(Model):
class Meta: class Meta:
database = db 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: class MojLicznik:
@@ -66,9 +166,13 @@ class MojLicznik:
meter_url = "https://mojlicznik.energa-operator.pl" meter_url = "https://mojlicznik.energa-operator.pl"
def databaseInit(self): def databaseInit(self):
db.create_tables([ChartTable], safe=True)
db.create_tables([PPETable], 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) db.create_tables([MainChartTable], safe=True)
@@ -87,13 +191,18 @@ class MojLicznik:
login_url = f"{self.meter_url}/dp/UserLogin.do" login_url = f"{self.meter_url}/dp/UserLogin.do"
self.loginStatus = False
try: try:
logger.debug("Pobieram formularz logowania.")
response = self.session.get(login_url) response = self.session.get(login_url)
response.raise_for_status() 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: 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') soup = BeautifulSoup(response.text, 'html.parser')
csrf_token = soup.find('input', {'name': '_antixsrf'})['value'] csrf_token = soup.find('input', {'name': '_antixsrf'})['value']
@@ -113,7 +222,7 @@ class MojLicznik:
except HTTPError as e: 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') soup = BeautifulSoup(response.text, 'html.parser')
@@ -121,12 +230,16 @@ class MojLicznik:
login_error = soup.find('div', text=login_error_text) login_error = soup.find('div', text=login_error_text)
if login_error: if login_error:
self.loginStatus = False logger.critical(login_error_text)
print(login_error_text) return
else: else:
self.loginStatus = True 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') select_elements = soup.find_all('script', type='text/javascript')
meter_isd = [] meter_isd = []
for el in select_elements: for el in select_elements:
@@ -140,16 +253,17 @@ class MojLicznik:
meter_isd.append(id_value) meter_isd.append(id_value)
retrieved_record = PPETable.get_or_none(id=id_value) retrieved_record = PPETable.get_or_none(id=id_value)
if retrieved_record: 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: if not retrieved_record.is_active:
retrieved_record.is_active = True retrieved_record.is_active = True
retrieved_record.save() retrieved_record.save()
else: 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( data = PPETable.create(
id=id_value, id=id_value,
ppe=ppe_value, ppe=ppe_value,
tariffCode=tariffCode_value, tariffCode=tariffCode_value,
type=type_value,
name=name_value name=name_value
) )
update_query = PPETable.update(is_active=0).where(PPETable.id.not_in(meter_isd)) 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 = self.session.get(logout_url)
response.raise_for_status() response.raise_for_status()
self.loginStatus = False self.loginStatus = False
print(f"Wylogowano.") logger.info(f"Wylogowano.")
except HTTPError as e: 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) query = PPETable.select().where(PPETable.is_active == True)
result_ppes = query.execute() result_ppes = query.execute()
@@ -175,45 +289,43 @@ class MojLicznik:
response = self.session.get(meter_url) response = self.session.get(meter_url)
response.raise_for_status() response.raise_for_status()
soup = BeautifulSoup(response.text, 'html.parser') soup = BeautifulSoup(response.text, 'html.parser')
td_elements = soup.find_all('td', class_='last') countners_dict = findCountners(soup)
date_divs = soup.find_all("div", style="font-size: 10px")
for div in date_divs: for c in countners_dict:
p.measurement_date = datetime.strptime(div.text.strip(), "%Y-%m-%d %H:%M").date() mn, mu = MeterTable.get_or_create(ppe_id=p.id, meter_type=c['meter_type'])
i = 0 mn.last_update_date = datetime.now()
mn.save()
cn, cu = CounterTable.get_or_create(
meter_id = mn.id,
tariff=c['tariff']
)
for td in td_elements: cn.meter_value = c['meter_value']
text = td.get_text() cn.measurement_date = c['measurement_date']
cleaned_text = re.sub(r'[^\d,]', '', text) cn.save()
cleaned_number_str = cleaned_text.lstrip('0').replace(',', '.')
i = i + 1 logger.info(f"Zapisano stan licznika {p.id} {c['meter_type']} taryfa {c['tariff']} z dnia: {c['measurement_date']} : {c['meter_value']}")
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}")
except HTTPError as e: 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): def update_first_date(self):
query = PPETable.select().where(PPETable.first_date.is_null(True) & (PPETable.is_active == True)) ppes_query = PPETable.select().where(PPETable.is_active == True)
result_ppes = query.execute() result_ppes = ppes_query.execute()
for p in result_ppes: for p in result_ppes:
print(f"Szukam najstarsze dane historyczne licznika {p.name}") 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 meter_point = p.id
max_years_back = 5 max_years_back = 5
start_date = datetime.now() start_date = datetime.now()
last_chart_year = None last_chart_year = None
for n in range(max_years_back + 1): for n in range(max_years_back + 1):
first_day_of_year = datetime(start_date.year-n, 1, 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) data_json = self.download_chart(ChartType.YEAR, first_day_of_year, meter_point, meter_type)
if data_json: if data_json:
data = json.loads(data_json) data = json.loads(data_json)
if data and data.get("mainChart") and len(data["mainChart"]) > 0: if data and data.get("mainChart") and len(data["mainChart"]) > 0:
@@ -222,7 +334,7 @@ class MojLicznik:
max_month = 12 max_month = 12
for n in range(max_month, 0, -1): for n in range(max_month, 0, -1):
first_day_of_month = datetime(last_chart_year, n, 1) first_day_of_month = datetime(last_chart_year, n, 1)
data_json = self.download_chart(ChartType.MONTH, first_day_of_month, meter_point) data_json = self.download_chart(ChartType.MONTH, first_day_of_month, meter_point, meter_type)
if data_json: if data_json:
data = json.loads(data_json) data = json.loads(data_json)
if data and data.get("mainChart") and len(data["mainChart"]) > 0: if data and data.get("mainChart") and len(data["mainChart"]) > 0:
@@ -233,29 +345,30 @@ class MojLicznik:
_, max_day = calendar.monthrange(first_day_of_day.year, first_day_of_day.month) _, max_day = calendar.monthrange(first_day_of_day.year, first_day_of_day.month)
for n in range(max_day, 0, -1): for n in range(max_day, 0, -1):
first_day_of_day = datetime(last_chart_year, last_chart_month, n) 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) data_json = self.download_chart(ChartType.DAY, first_day_of_day, meter_point, meter_type)
if data_json: if data_json:
data = json.loads(data_json) data = json.loads(data_json)
if data and data.get("mainChart") and len(data["mainChart"]) > 0: if data and data.get("mainChart") and len(data["mainChart"]) > 0:
last_chart_day = n last_chart_day = n
first_date = datetime(last_chart_year, last_chart_month, last_chart_day).date() first_date = datetime(last_chart_year, last_chart_month, last_chart_day).date()
print(f"Najstarsze dane historyczne dla licznika {p.name}: {first_date}") print(f"Najstarsze dane historyczne dla licznika {p.name} (PPE: {p.ppe}, {p.id}) typ: {meter_type}: {first_date}")
p.first_date = first_date meter.first_date = first_date
p.save() meter.save()
def save_main_charts(self, mp, vals): def save_main_charts(self, mp, vals, m_type):
for val in vals: for val in vals:
#try: try:
logger.debug(f"save_main_charts: mp: {mp}, val: {val}, meter_type: {m_type}")
z = val["zones"] z = val["zones"]
# {"tm": "1690412400000", "tarAvg": 0.3899153269199055, "zones": [null, 0.232, null], "est": false, "cplt": true},
if z[0]: if z[0]:
# MainChartTable.get_or_create(tm = val["tm"], zone = 1, value = z[0], tarAvg=val["tarAvg"], est=val["est"], cplt=val["cplt"]) # MainChartTable.get_or_create(tm = val["tm"], zone = 1, value = z[0], tarAvg=val["tarAvg"], est=val["est"], cplt=val["cplt"])
try: 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: except MainChartTable.DoesNotExist:
# Jeśli rekord nie istnieje, utwórz nowy # Jeśli rekord nie istnieje, utwórz nowy
MainChartTable.create( MainChartTable.create(
mp=mp, mp=mp,
meter_type=m_type,
tm=val["tm"], tm=val["tm"],
zone=1, zone=1,
value=z[0], value=z[0],
@@ -266,11 +379,12 @@ class MojLicznik:
if z[1]: if z[1]:
try: 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: except MainChartTable.DoesNotExist:
# Jeśli rekord nie istnieje, utwórz nowy # Jeśli rekord nie istnieje, utwórz nowy
MainChartTable.create( MainChartTable.create(
mp=mp, mp=mp,
meter_type=m_type,
tm=val["tm"], tm=val["tm"],
zone=2, zone=2,
value=z[1], value=z[1],
@@ -281,11 +395,12 @@ class MojLicznik:
if z[2]: if z[2]:
try: 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: except MainChartTable.DoesNotExist:
# Jeśli rekord nie istnieje, utwórz nowy # Jeśli rekord nie istnieje, utwórz nowy
MainChartTable.create( MainChartTable.create(
mp=mp, mp=mp,
meter_type=m_type,
tm=val["tm"], tm=val["tm"],
zone=3, zone=3,
value=z[2], value=z[2],
@@ -294,12 +409,12 @@ class MojLicznik:
cplt=val["cplt"] cplt=val["cplt"]
) )
#except: except Exception as e:
# pass logging.error(f"Wystąpił błąd: {str(e)}")
return None 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: if type == ChartType.DAY:
chart_type = "DAY" chart_type = "DAY"
@@ -316,7 +431,9 @@ class MojLicznik:
first_day = datetime(date.year, 1, 1) first_day = datetime(date.year, 1, 1)
tsm_date = int(time.mktime(first_day.timetuple()) * 1000) 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: try:
response = self.session.get(chart_url) response = self.session.get(chart_url)
data = json.loads(response.text) data = json.loads(response.text)
@@ -326,7 +443,7 @@ class MojLicznik:
mainChartDate = data["response"]["mainChartDate"] mainChartDate = data["response"]["mainChartDate"]
mainChart = data["response"]["mainChart"] mainChart = data["response"]["mainChart"]
if type == ChartType.DAY: if type == ChartType.DAY:
self.save_main_charts(meter_point, mainChart) self.save_main_charts(meter_point, mainChart, meter_type)
date = int(mainChartDate) / 1000 date = int(mainChartDate) / 1000
month = None month = None
@@ -345,34 +462,41 @@ class MojLicznik:
chart_record.value = json_dump chart_record.value = json_dump
chart_record.save() chart_record.save()
except ChartTable.DoesNotExist: 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: else:
try: 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: except:
pass pass
return json_dump return json_dump
return None return None
except HTTPError as e: 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): def download_charts(self, full_mode=False):
query = PPETable.select().where(PPETable.is_active == True) query = PPETable.select().where(PPETable.is_active == True)
result_ppes = query.execute() result_ppes = query.execute()
for p in result_ppes: for p in result_ppes:
current_date = p.first_date 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: if not full_mode:
current_date = p.measurement_date - timedelta(days=1) current_date = meter.last_update_date - timedelta(days=1)
while current_date <= date.today(): while current_date <= date.today():
try: try:
record = ChartTable.get(id=p.id, year=current_date.year, month=current_date.month, day=current_date.day) 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. # 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}") logger.debug(f"Posiadam dane historyczne dla {p.name} ({p.id}) typ: {meter_type} na dzień: {current_date}")
except ChartTable.DoesNotExist: except ChartTable.DoesNotExist:
self.download_chart(ChartType.DAY, current_date, p.id) self.download_chart(ChartType.DAY, current_date, p.id, meter_type)
print(f"Pobieram dane historyczne dla {p.name} na dzień: {current_date}") logger.debug(f"Pobieram dane historyczne dla {p.name} ({p.id}) typ: {meter_type} na dzień: {current_date}")
current_date += timedelta(days=1) current_date += timedelta(days=1)
def update_last_days(self): def update_last_days(self):
@@ -381,13 +505,20 @@ class MojLicznik:
result_ppes = query.execute() result_ppes = query.execute()
for p in result_ppes: 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: if not p.last_update_date:
p.last_update_date = today - timedelta(days=5) p.last_update_date = today - timedelta(days=5)
p.save() p.save()
last_update_date = p.last_update_date - timedelta(days=1) last_update_date = p.last_update_date - timedelta(days=1)
while last_update_date <= today: while last_update_date <= today:
print(f"Aktualizacja danych dla {p.name} na dzień: {last_update_date}") 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, True) self.download_chart(ChartType.DAY, last_update_date, p.id, meter_type, True)
p.last_update_date = last_update_date p.last_update_date = last_update_date
p.save() p.save()
last_update_date += timedelta(days=1) last_update_date += timedelta(days=1)
@@ -408,80 +539,37 @@ class MojLicznik:
if zones: if zones:
zone1_data = zones[0] zone1_data = zones[0]
zone1_main_chart = zone1_data.get("mainChart", []) 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): # def set_daily_zones(self):
# query = PPETable.select().where(PPETable.is_active == True) # query = PPETable.select().where(PPETable.is_active == True)
# result_ppes = query.execute() # result_ppes = query.execute()
# for p in result_ppes: # 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 = ChartTable.select().where(
# query_count = query.count() # (ChartTable.id == p.id) &
# if (query_count > 0): # ((ChartTable.year > p.measurement_date.year) |
# query_first = query.first() # ((ChartTable.year == p.measurement_date.year) &
# value_json = json.loads(query_first.value) # (ChartTable.month > p.measurement_date.month)) |
# main_chart = value_json.get("mainChart", []) # ((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}_daily_chart_sum": 0.0 for i in range(3)}
# zones_sums = {f"zone{i+1}": 0.0 for i in range(len(main_chart[0].get("zones", [])))}
# for chart_entry in query:
# value_json = json.loads(chart_entry.value)
# main_chart = value_json.get("mainChart", [])
# for entry in main_chart: # for entry in main_chart:
# zones = entry.get("zones", []) # zones = entry.get("zones", [])
# for i, value in enumerate(zones): # for i, value in enumerate(zones):
# if value is not None: # if value is not None:
# zones_sums[f"zone{i+1}"] += value # zones_sums[f"zone{i+1}_daily_chart_sum"] += value
# if (zones_sums["zone1"] > 0):
# p.zone1_daily_chart_sum = zones_sums["zone1"] # for key, value in zones_sums.items():
# else: # setattr(p, key, value)
# 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() # p.save()

View File

@@ -2,16 +2,8 @@
export USERNAME=$(bashio::config 'energa_username') export USERNAME=$(bashio::config 'energa_username')
export PASSWORD=$(bashio::config 'energa_password') export PASSWORD=$(bashio::config 'energa_password')
export LOG_LEVEL=$(bashio::config 'log_level')
bashio::log.info "Uruchamiam API" bashio::log.info "Uruchamiam API"
python api.py & python run.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

View File

@@ -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

View File

@@ -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" ]

View File

@@ -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
<a href="https://buycoffee.to/tcich"><img src="../img/logo-buycoffee-wide.jpg" width=200 alt="Postaw kawę"></a>
## 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.
<img src="https://github.com/tcich/ha-addon-energa-meter/blob/e702ed49436fb9e0b675dcac3001bd9de5aab3c0/srcdev/img/addon.png" style="width: 80%;" alt="Nazwa hosta">
<img src="img/grafana_01.png" style="width: 80%;" alt="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
<img src="img/grafana_06.png" style="width: 80%;" alt="Grafana">
<img src="img/grafana_02.png" style="width: 80%;" alt="Grafana">
<img src="img/grafana_03.png" style="width: 80%;" alt="Grafana">
<img src="img/grafana_04.png" style="width: 80%;" alt="Grafana">
<img src="img/grafana_05.png" style="width: 80%;" alt="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-
<img src="img/grafana_07.png" style="width: 80%;" alt="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)
<img src="img/grafana_08.png" style="width: 80%;" alt="Grafana">
9) W **Params** aby uzyskać bilans energii należy dodać **Transform**, przy czym wartość energii oddawanej powinna być jako energia ujemna (parametr negative)
<img src="img/grafana_09.png" style="width: 80%;" alt="Grafana">
<img src="img/grafana_10.png" style="width: 80%;" alt="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
<a href="https://buycoffee.to/tcich"><img src="../img/logo-buycoffee-wide.jpg" width=200 alt="Postaw kawę"></a>

View File

@@ -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

View File

@@ -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('/<int:ppe_id>', methods=['GET'])
@app.route('/<int:ppe_id>/', 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('/<int:ppe_id>/<meter_type_url>', methods=['GET'])
@app.route('/<int:ppe_id>/<meter_type_url>/', 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('/<int:ppe_id>/<meter_type_url>/<tariff>', methods=['GET'])
@app.route('/<int:ppe_id>/<meter_type_url>/<tariff>/', 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})

View File

@@ -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

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -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()

View File

@@ -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

Binary file not shown.

View File

@@ -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