4 Commits

Author SHA1 Message Date
7d41f00cce api: only open database connection while handling requests
Helps with recovery when database restarts during long-held connections.

Code from https://docs.peewee-orm.com/en/3.15.3/peewee/database.html#flask
2024-07-21 17:01:51 +02:00
ae1bcf84d5 support PostgreSQL database 2024-06-08 21:13:00 +02:00
fe6472b73d compatibility fixes for the database schema
When using PostgreSQL:

- `CharField()` is limited to 255 character; use `TextField()`

- `IntegerField()` is signed, current unix timestamps exceeds
  its range; use correct TimestampField()
2024-06-05 22:19:10 +02:00
42af5df506 move database.sqlite to separate directory
Makes it easier to mount a volume and get persistence.
2024-06-02 17:57:54 +02:00
8 changed files with 55 additions and 94 deletions

View File

@@ -1,24 +0,0 @@
---
name: Build and push container
run-name: Container creation
# change later for only tags
on: [push]
jobs:
Container-Build:
runs-on: ubuntu-latest
steps:
- name: Get the code
uses: actions/checkout@v4
with:
sparse-checkout: |
src
- name: Build & push image ${{ gitea.ref }}
uses: bymarshall/kaniko-action@main
with:
image: ha-addon-energa-meter
tag: ${{ gitea.ref }}
registry: pipebreaker.pl:5000
file: Containerfile
context: src/
debug_mode: true

View File

@@ -1,22 +0,0 @@
name: Gitea Actions Demo
run-name: ${{ gitea.actor }} is testing out GA 🍆
on: [push]
jobs:
Explore-Gitea-Actions:
runs-on: ubuntu-latest
steps:
- run: echo "🎉 The job was automatically triggered by a ${{ gitea.event_name }} event."
- run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by Gitea!"
- run: echo "🔎 The name of your branch is ${{ gitea.ref }} and your repository is ${{ gitea.repository }}."
- name: Checkout code
uses: actions/checkout@v3
- run: echo "💡 The ${{ gitea.repository }} repository has been cloned to the runner."
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
- name: List files
run: |
echo ${{ gitea.workspace }}:
ls ${{ gitea.workspace }}
- run: echo "🍏 This job's status is ${{ gitea.status }}."

View File

@@ -1,23 +0,0 @@
FROM fedora-minimal:latest
RUN microdnf install -y python3-pip && pip install uv
WORKDIR /app
COPY requirements.txt .
RUN uv venv /app/venv && \
source /app/venv/bin/activate && \
uv pip install -r requirements.txt
# Copy data for add-on
COPY database_empty.sqlite data/database.sqlite
COPY rund.sh .
COPY run.py .
COPY main.py .
COPY api.py .
COPY moj_licznik.py .
COPY log_config.py .
RUN chmod a+x rund.sh
CMD [ "/app/rund.sh" ]

View File

@@ -13,7 +13,7 @@ COPY main.py .
COPY api.py . COPY api.py .
COPY moj_licznik.py . COPY moj_licznik.py .
COPY log_config.py . COPY log_config.py .
COPY database_empty.sqlite database.sqlite COPY database_empty.sqlite data/database.sqlite
RUN chmod a+x run.sh RUN chmod a+x run.sh
RUN apk add --update --no-cache py3-pip && \ RUN apk add --update --no-cache py3-pip && \

View File

@@ -47,6 +47,11 @@ Wymagane parametry:
* ENERGA_USERNAME - nazwa użytkownika w aplikacji Energa Mój licznik * ENERGA_USERNAME - nazwa użytkownika w aplikacji Energa Mój licznik
* ENERGA_PASSWORD - hasło użytkownika w aplikacji Energa Mój licznik * ENERGA_PASSWORD - hasło użytkownika w aplikacji Energa Mój licznik
Opcjonalne parametry:
* POSTGRESQL_CONNSTRING - namiar na bazę PostgreSQL do przechowywania odczytów;
format opisany w [dokumentacji PGSQL](https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING-URIS)
(przykładowo `postgresql://uzytkownik:haslo@serwer/mojlicznik`)
## Konfiguracja sensorów ## Konfiguracja sensorów
Do HA możesz dodać sensory, które zawierają informacje udostępniane przez API Do HA możesz dodać sensory, które zawierają informacje udostępniane przez API

View File

@@ -1,4 +1,4 @@
from peewee import SqliteDatabase from peewee import SqliteDatabase, PostgresqlDatabase
from flask import Flask, jsonify, request, redirect, url_for, abort from flask import Flask, jsonify, request, redirect, url_for, abort
from waitress import serve from waitress import serve
#from datetime #from datetime
@@ -9,13 +9,30 @@ import urllib.parse
logger = logging.getLogger("energaMeter.api") logger = logging.getLogger("energaMeter.api")
if postgresql_connstring := os.getenv("POSTGRESQL_CONNSTRING"):
from psycopg2.extensions import parse_dsn
db_name = parse_dsn(postgresql_connstring)['dbname']
db = PostgresqlDatabase(db_name, dsn=postgresql_connstring)
else:
path = os.path.dirname(os.path.abspath(__file__)) path = os.path.dirname(os.path.abspath(__file__))
db_file = 'database.sqlite' db_file = 'data/database.sqlite'
db = SqliteDatabase(os.path.join(path, db_file)) db = SqliteDatabase(os.path.join(path, db_file))
app = Flask(__name__) app = Flask(__name__)
# This hook ensures that a connection is opened to handle any queries
# generated by the request.
@app.before_request
def _db_connect():
db.connect()
# This hook ensures that the connection is closed when we've finished
# processing the request.
@app.teardown_request
def _db_close(exc):
if not db.is_closed():
db.close()
@app.route('/', methods=['GET']) @app.route('/', methods=['GET'])
def root(): def root():
query = PPETable.select() #.where(PPETable.is_active == True) query = PPETable.select() #.where(PPETable.is_active == True)
@@ -207,7 +224,7 @@ def charts():
zone = request.args.get('zone', None) zone = request.args.get('zone', None)
negative = request.args.get('negative', type=bool, default=False) negative = request.args.get('negative', type=bool, default=False)
logger.debug(f"API: GET /charts - {start_date} - {end_date}") logger.debug(f"API: GET /charts - {start_date} - {end_date}")
query = MainChartTable.select().where((MainChartTable.tm >= int(start_date)) & (MainChartTable.tm <= int(end_date))) query = MainChartTable.select().where((MainChartTable.tm >= int(start_date)/1000) & (MainChartTable.tm <= int(end_date)/1000))
logger.debug(f"{query}") logger.debug(f"{query}")
factor = 1 factor = 1
if negative: if negative:
@@ -226,13 +243,13 @@ def charts():
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 = p.tm.strftime("%Y-%m-%d %H:%M:%S")
chart = { chart = {
'mp': p.mp, 'mp': p.mp,
'meter_type': p.meter_type, 'meter_type': p.meter_type,
'meter_type_url': urllib.parse.quote_plus(p.meter_type), 'meter_type_url': urllib.parse.quote_plus(p.meter_type),
'zone': p.zone, 'zone': p.zone,
'time_tm': p.tm, 'time_tm': int(p.tm.timestamp()*1000),
'time': czas, 'time': czas,
'value': p.value * factor 'value': p.value * factor
} }

View File

@@ -1,17 +1,24 @@
from peewee import SqliteDatabase from peewee import SqliteDatabase, PostgresqlDatabase
from datetime import datetime, timedelta, date from datetime import datetime, timedelta, date
import calendar, requests, re, time, json, os, logging 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 AutoField, Model, CharField, IntegerField, DateField, BooleanField, CompositeKey, DecimalField, ForeignKeyField, SQL from peewee import AutoField, Model, CharField, IntegerField, DateField, BooleanField, CompositeKey, DecimalField, ForeignKeyField, SQL, TextField, TimestampField
import urllib.parse import urllib.parse
logger = logging.getLogger("energaMeter") logger = logging.getLogger("energaMeter")
if postgresql_connstring := os.getenv("POSTGRESQL_CONNSTRING"):
from psycopg2.extensions import parse_dsn
db_name = parse_dsn(postgresql_connstring)['dbname']
db_host = parse_dsn(postgresql_connstring)['host']
db = PostgresqlDatabase(db_name, dsn=postgresql_connstring)
logger.info(f"Używam bazy PostgreSQL „{db_name}” na {db_host}")
else:
path = os.path.dirname(os.path.abspath(__file__)) path = os.path.dirname(os.path.abspath(__file__))
db_file = 'database.sqlite' db_file = 'data/database.sqlite'
db = SqliteDatabase(os.path.join(path, db_file)) db = SqliteDatabase(os.path.join(path, db_file))
class ChartType(Enum): class ChartType(Enum):
@@ -31,7 +38,7 @@ class PPETable(Model):
class Meta: class Meta:
database = db database = db
table_name = 'PPE' table_name = 'PPE'
constraints = [SQL('UNIQUE (ppe, tariffCode)')] constraints = [SQL('UNIQUE ("ppe", "tariffCode")')]
class MeterTable(Model): class MeterTable(Model):
id = AutoField() # Meter point id = AutoField() # Meter point
@@ -43,7 +50,7 @@ class MeterTable(Model):
class Meta: class Meta:
database = db database = db
table_name = 'METER' table_name = 'METER'
constraints = [SQL('UNIQUE (ppe_id, meter_type)')] constraints = [SQL('UNIQUE ("ppe_id", "meter_type")')]
class CounterTable(Model): class CounterTable(Model):
id = AutoField() id = AutoField()
@@ -71,7 +78,7 @@ class ChartTable(Model):
year = IntegerField() year = IntegerField()
month = IntegerField(null=True) month = IntegerField(null=True)
day = IntegerField(null=True) day = IntegerField(null=True)
value =CharField() value = TextField()
class Meta: class Meta:
database = db database = db
@@ -82,7 +89,7 @@ class MainChartTable(Model):
mp = CharField() mp = CharField()
meter_type = CharField() meter_type = CharField()
zone = IntegerField() zone = IntegerField()
tm = IntegerField() tm = TimestampField()
value = DecimalField(max_digits=20, decimal_places=16, null=True) value = DecimalField(max_digits=20, decimal_places=16, null=True)
tarAvg = DecimalField(max_digits=20, decimal_places=16, null=True) tarAvg = DecimalField(max_digits=20, decimal_places=16, null=True)
est = BooleanField(default=False) est = BooleanField(default=False)
@@ -360,16 +367,17 @@ class MojLicznik:
try: try:
logger.debug(f"save_main_charts: mp: {mp}, val: {val}, meter_type: {m_type}") logger.debug(f"save_main_charts: mp: {mp}, val: {val}, meter_type: {m_type}")
z = val["zones"] z = val["zones"]
tm = int(val["tm"]) / 1000 # convert JS timestamp (milliseconds) to unix (seconds)
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.meter_type == m_type) & (MainChartTable.mp == mp) & (MainChartTable.tm == val["tm"]) & (MainChartTable.zone == 1)) existing_record = MainChartTable.get((MainChartTable.meter_type == m_type) & (MainChartTable.mp == mp) & (MainChartTable.tm == 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, meter_type=m_type,
tm=val["tm"], tm=tm,
zone=1, zone=1,
value=z[0], value=z[0],
tarAvg=val["tarAvg"], tarAvg=val["tarAvg"],
@@ -379,13 +387,13 @@ class MojLicznik:
if z[1]: if z[1]:
try: try:
existing_record = MainChartTable.get((MainChartTable.meter_type == m_type) & (MainChartTable.mp == mp) & (MainChartTable.tm == val["tm"]) & (MainChartTable.zone == 2)) existing_record = MainChartTable.get((MainChartTable.meter_type == m_type) & (MainChartTable.mp == mp) & (MainChartTable.tm == 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, meter_type=m_type,
tm=val["tm"], tm=tm,
zone=2, zone=2,
value=z[1], value=z[1],
tarAvg=val["tarAvg"], tarAvg=val["tarAvg"],
@@ -395,13 +403,13 @@ class MojLicznik:
if z[2]: if z[2]:
try: try:
existing_record = MainChartTable.get((MainChartTable.meter_type == m_type) & (MainChartTable.mp == mp) & (MainChartTable.tm == val["tm"]) & (MainChartTable.zone == 3)) existing_record = MainChartTable.get((MainChartTable.meter_type == m_type) & (MainChartTable.mp == mp) & (MainChartTable.tm == 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, meter_type=m_type,
tm=val["tm"], tm=tm,
zone=3, zone=3,
value=z[2], value=z[2],
tarAvg=val["tarAvg"], tarAvg=val["tarAvg"],

Binary file not shown.