This commit is contained in:
@@ -0,0 +1,30 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
pull_request:
|
||||||
|
branches: [master]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: "3.13"
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
pip install ruff
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
- name: Lint
|
||||||
|
run: ruff check .
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: pytest tests/ -v
|
||||||
@@ -35,7 +35,7 @@ def _safe_next(default_url):
|
|||||||
def _filter_compatible(query, battery_size):
|
def _filter_compatible(query, battery_size):
|
||||||
"""Restrict a Battery query to size-compatible batteries (matching size or unsized)."""
|
"""Restrict a Battery query to size-compatible batteries (matching size or unsized)."""
|
||||||
if battery_size:
|
if battery_size:
|
||||||
query = query.filter((Battery.size == battery_size) | (Battery.size == None))
|
query = query.filter((Battery.size == battery_size) | (Battery.size == None)) # noqa: E711
|
||||||
return query
|
return query
|
||||||
|
|
||||||
|
|
||||||
@@ -130,7 +130,7 @@ def create_app(config_object="config"):
|
|||||||
avail_count = sum(1 for b in batteries if b.status == "available")
|
avail_count = sum(1 for b in batteries if b.status == "available")
|
||||||
installed_count = sum(1 for b in batteries if b.status == "installed")
|
installed_count = sum(1 for b in batteries if b.status == "installed")
|
||||||
retired_count = sum(1 for b in batteries if b.status == "retired")
|
retired_count = sum(1 for b in batteries if b.status == "retired")
|
||||||
devices = db.query(Device).filter(Device.parent_id == None).all()
|
devices = db.query(Device).filter(Device.parent_id == None).all() # noqa: E711
|
||||||
total_devices = len(devices)
|
total_devices = len(devices)
|
||||||
full_devices = sum(
|
full_devices = sum(
|
||||||
1 for d in devices
|
1 for d in devices
|
||||||
@@ -283,16 +283,16 @@ def create_app(config_object="config"):
|
|||||||
.order_by(BatteryPctLog.recorded_at.desc())
|
.order_by(BatteryPctLog.recorded_at.desc())
|
||||||
.all())
|
.all())
|
||||||
charge_logs_data = [
|
charge_logs_data = [
|
||||||
{"id": l.id, "date": l.charged_date, "cycles": l.increment_cycles, "notes": l.notes or ""}
|
{"id": log.id, "date": log.charged_date, "cycles": log.increment_cycles, "notes": log.notes or ""}
|
||||||
for l in charge_logs
|
for log in charge_logs
|
||||||
]
|
]
|
||||||
capacity_tests_data = [
|
capacity_tests_data = [
|
||||||
{"id": t.id, "date": t.tested_date, "mah": t.tested_capacity_mah, "notes": t.notes or ""}
|
{"id": t.id, "date": t.tested_date, "mah": t.tested_capacity_mah, "notes": t.notes or ""}
|
||||||
for t in sorted(capacity_tests, key=lambda t: (t.tested_date, t.id), reverse=True)
|
for t in sorted(capacity_tests, key=lambda t: (t.tested_date, t.id), reverse=True)
|
||||||
]
|
]
|
||||||
pct_logs_data = [
|
pct_logs_data = [
|
||||||
{"recorded_at": str(l.recorded_at), "pct": l.percentage, "source": l.source or ""}
|
{"recorded_at": str(log.recorded_at), "pct": log.percentage, "source": log.source or ""}
|
||||||
for l in pct_logs
|
for log in pct_logs
|
||||||
]
|
]
|
||||||
return render_template("battery_detail.html", battery=battery,
|
return render_template("battery_detail.html", battery=battery,
|
||||||
storage_locations=storage_locations,
|
storage_locations=storage_locations,
|
||||||
@@ -699,7 +699,7 @@ def create_app(config_object="config"):
|
|||||||
|
|
||||||
@app.route("/device/")
|
@app.route("/device/")
|
||||||
def device_list():
|
def device_list():
|
||||||
devices = db.query(Device).filter(Device.parent_id == None).order_by(Device.name).all()
|
devices = db.query(Device).filter(Device.parent_id == None).order_by(Device.name).all() # noqa: E711
|
||||||
device_types = sorted({d.device_type for d in devices if d.device_type})
|
device_types = sorted({d.device_type for d in devices if d.device_type})
|
||||||
device_locations = sorted({d.location for d in devices if d.location})
|
device_locations = sorted({d.location for d in devices if d.location})
|
||||||
device_battery_sizes = sorted({d.battery_size for d in devices if d.battery_size})
|
device_battery_sizes = sorted({d.battery_size for d in devices if d.battery_size})
|
||||||
@@ -1237,9 +1237,9 @@ def create_app(config_object="config"):
|
|||||||
buf = io.StringIO()
|
buf = io.StringIO()
|
||||||
w = csv.writer(buf)
|
w = csv.writer(buf)
|
||||||
w.writerow(["id", "battery_id", "battery_label", "charged_date", "increment_cycles", "notes"])
|
w.writerow(["id", "battery_id", "battery_label", "charged_date", "increment_cycles", "notes"])
|
||||||
for l in rows:
|
for row in rows:
|
||||||
w.writerow([l.id, l.battery_id, l.battery.label,
|
w.writerow([row.id, row.battery_id, row.battery.label,
|
||||||
l.charged_date, l.increment_cycles, l.notes or ""])
|
row.charged_date, row.increment_cycles, row.notes or ""])
|
||||||
return buf.getvalue()
|
return buf.getvalue()
|
||||||
|
|
||||||
def _capacity_tests_csv():
|
def _capacity_tests_csv():
|
||||||
@@ -1257,9 +1257,9 @@ def create_app(config_object="config"):
|
|||||||
buf = io.StringIO()
|
buf = io.StringIO()
|
||||||
w = csv.writer(buf)
|
w = csv.writer(buf)
|
||||||
w.writerow(["id", "battery_id", "battery_label", "percentage", "recorded_at", "source"])
|
w.writerow(["id", "battery_id", "battery_label", "percentage", "recorded_at", "source"])
|
||||||
for l in rows:
|
for row in rows:
|
||||||
w.writerow([l.id, l.battery_id, l.battery.label,
|
w.writerow([row.id, row.battery_id, row.battery.label,
|
||||||
l.percentage, l.recorded_at, l.source or ""])
|
row.percentage, row.recorded_at, row.source or ""])
|
||||||
return buf.getvalue()
|
return buf.getvalue()
|
||||||
|
|
||||||
@app.route("/export")
|
@app.route("/export")
|
||||||
@@ -1334,10 +1334,10 @@ def create_app(config_object="config"):
|
|||||||
for d in devices
|
for d in devices
|
||||||
],
|
],
|
||||||
"charge_logs": [
|
"charge_logs": [
|
||||||
{"id": l.id, "battery_id": l.battery_id, "battery_label": l.battery.label,
|
{"id": log.id, "battery_id": log.battery_id, "battery_label": log.battery.label,
|
||||||
"charged_date": l.charged_date, "increment_cycles": l.increment_cycles,
|
"charged_date": log.charged_date, "increment_cycles": log.increment_cycles,
|
||||||
"notes": l.notes}
|
"notes": log.notes}
|
||||||
for l in charge_logs
|
for log in charge_logs
|
||||||
],
|
],
|
||||||
"capacity_tests": [
|
"capacity_tests": [
|
||||||
{"id": t.id, "battery_id": t.battery_id, "battery_label": t.battery.label,
|
{"id": t.id, "battery_id": t.battery_id, "battery_label": t.battery.label,
|
||||||
@@ -1346,9 +1346,9 @@ def create_app(config_object="config"):
|
|||||||
for t in capacity_tests
|
for t in capacity_tests
|
||||||
],
|
],
|
||||||
"pct_logs": [
|
"pct_logs": [
|
||||||
{"id": l.id, "battery_id": l.battery_id, "battery_label": l.battery.label,
|
{"id": log.id, "battery_id": log.battery_id, "battery_label": log.battery.label,
|
||||||
"percentage": l.percentage, "recorded_at": l.recorded_at, "source": l.source}
|
"percentage": log.percentage, "recorded_at": log.recorded_at, "source": log.source}
|
||||||
for l in pct_logs
|
for log in pct_logs
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
return Response(json.dumps(payload, indent=2), mimetype="application/json",
|
return Response(json.dumps(payload, indent=2), mimetype="application/json",
|
||||||
|
|||||||
+1
-1
@@ -37,7 +37,7 @@ class HaPoller:
|
|||||||
self._poll_once()
|
self._poll_once()
|
||||||
|
|
||||||
def _poll_once(self):
|
def _poll_once(self):
|
||||||
from models import Battery, BatteryPctLog, Device # local import avoids circular-import risk
|
from models import BatteryPctLog, Device # local import avoids circular-import risk
|
||||||
|
|
||||||
session = self._Session()
|
session = self._Session()
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from sqlalchemy import Column, Integer, String, Text, ForeignKey, Table, UniqueConstraint
|
from sqlalchemy import Column, Integer, String, Text, ForeignKey, Table, UniqueConstraint
|
||||||
from sqlalchemy.orm import declarative_base, relationship
|
from sqlalchemy.orm import declarative_base, relationship
|
||||||
|
|||||||
+3
-1
@@ -1,5 +1,7 @@
|
|||||||
"""Generate solid-color PNG icons for PWA manifest using stdlib only (no Pillow)."""
|
"""Generate solid-color PNG icons for PWA manifest using stdlib only (no Pillow)."""
|
||||||
import zlib, struct, os
|
import zlib
|
||||||
|
import struct
|
||||||
|
import os
|
||||||
|
|
||||||
def make_png(size, rgb=(0x25, 0x63, 0xEB)):
|
def make_png(size, rgb=(0x25, 0x63, 0xEB)):
|
||||||
"""Create a minimal valid RGB PNG of the given size filled with one color."""
|
"""Create a minimal valid RGB PNG of the given size filled with one color."""
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ Usage:
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
|
||||||
from datetime import date
|
from datetime import date
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|||||||
@@ -31,11 +31,11 @@ from pathlib import Path
|
|||||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||||
sys.path.insert(0, str(REPO_ROOT))
|
sys.path.insert(0, str(REPO_ROOT))
|
||||||
|
|
||||||
from sqlalchemy import create_engine, text
|
from sqlalchemy import create_engine, text # noqa: E402
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker # noqa: E402
|
||||||
|
|
||||||
import config
|
import config # noqa: E402
|
||||||
from models import Base, Battery, BatteryPctLog, CapacityTest, ChargeLog, Device
|
from models import Base, Battery, BatteryPctLog, CapacityTest, ChargeLog, Device # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -62,7 +62,7 @@ def collect_credentials() -> str:
|
|||||||
"""Return a MariaDB SQLAlchemy URL, prompting for any missing pieces."""
|
"""Return a MariaDB SQLAlchemy URL, prompting for any missing pieces."""
|
||||||
url = os.environ.get("MARIADB_URL", "").strip()
|
url = os.environ.get("MARIADB_URL", "").strip()
|
||||||
if url:
|
if url:
|
||||||
print(f" Using MARIADB_URL from environment.")
|
print(" Using MARIADB_URL from environment.")
|
||||||
return url
|
return url
|
||||||
|
|
||||||
print("Enter MariaDB connection details (press Enter to accept defaults):\n")
|
print("Enter MariaDB connection details (press Enter to accept defaults):\n")
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ The `seeded_client` fixture pre-populates:
|
|||||||
BrandX 002 (id=3, retired)
|
BrandX 002 (id=3, retired)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import pytest
|
import io
|
||||||
|
import json as _json
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
@@ -169,7 +170,7 @@ def test_edit_details_percentage_valid(seeded_client):
|
|||||||
data = _json.loads(seeded_client.get("/export/all.json").data)
|
data = _json.loads(seeded_client.get("/export/all.json").data)
|
||||||
bat = next(b for b in data["batteries"] if b["id"] == 1)
|
bat = next(b for b in data["batteries"] if b["id"] == 1)
|
||||||
assert bat["battery_percentage"] == 100
|
assert bat["battery_percentage"] == 100
|
||||||
assert any(l["battery_id"] == 1 and l["source"] == "manual" for l in data["pct_logs"])
|
assert any(entry["battery_id"] == 1 and entry["source"] == "manual" for entry in data["pct_logs"])
|
||||||
|
|
||||||
|
|
||||||
def test_assign_battery(seeded_client):
|
def test_assign_battery(seeded_client):
|
||||||
@@ -699,9 +700,6 @@ def test_bulk_install_filters_by_size(client):
|
|||||||
# Import
|
# Import
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
|
|
||||||
import io
|
|
||||||
import json as _json
|
|
||||||
|
|
||||||
|
|
||||||
def _make_import_payload(devices=None, batteries=None,
|
def _make_import_payload(devices=None, batteries=None,
|
||||||
charge_logs=None, capacity_tests=None, pct_logs=None):
|
charge_logs=None, capacity_tests=None, pct_logs=None):
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ ha_client/ha_poller branches, export/import edge cases, bulk actions.
|
|||||||
|
|
||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
import pytest
|
|
||||||
from unittest.mock import patch, MagicMock
|
from unittest.mock import patch, MagicMock
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -484,7 +484,8 @@ def test_poll_skips_update_when_percentage_unchanged(ha_app, ha_client_f):
|
|||||||
engine = create_engine(ha_app.config["SQLALCHEMY_DATABASE_URI"])
|
engine = create_engine(ha_app.config["SQLALCHEMY_DATABASE_URI"])
|
||||||
s = sessionmaker(bind=engine)()
|
s = sessionmaker(bind=engine)()
|
||||||
s.get(Battery, 1).battery_percentage = 50
|
s.get(Battery, 1).battery_percentage = 50
|
||||||
s.commit(); s.close()
|
s.commit()
|
||||||
|
s.close()
|
||||||
|
|
||||||
from ha_client import HomeAssistantClient
|
from ha_client import HomeAssistantClient
|
||||||
from ha_poller import HaPoller
|
from ha_poller import HaPoller
|
||||||
|
|||||||
Reference in New Issue
Block a user