Release v3.0.0
- Bump version to 3.0.0 and update docs - Fix Debian payload to include TUI and install /usr/bin/sensorpajen-tui wrapper - Make systemd unit upgrades safer and ignore deb build artifacts
This commit is contained in:
@@ -34,6 +34,35 @@ def test_sensor_config_load(tmp_path):
|
||||
assert sensor_cfg.get_name("AA:BB:CC:DD:EE:FF") == "Living Room"
|
||||
assert sensor_cfg.get_name("UNKNOWN") == "UNKNOWN"
|
||||
|
||||
|
||||
def test_sensor_config_comment_load_and_clear(tmp_path):
|
||||
import json
|
||||
import sensorpajen.config as config
|
||||
|
||||
config_file = tmp_path / "sensors.json"
|
||||
config_file.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"sensors": [
|
||||
{"mac": "AA:BB:CC:DD:EE:FF", "name": "Living Room", "comment": "hello"},
|
||||
]
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
)
|
||||
|
||||
sensor_cfg = config.SensorConfig(config_file=str(config_file))
|
||||
assert sensor_cfg.get_comment("AA:BB:CC:DD:EE:FF") == "hello"
|
||||
|
||||
# Clear comment explicitly (empty string means remove comment key)
|
||||
sensor_cfg.add_sensor("AA:BB:CC:DD:EE:FF", "Living Room", "")
|
||||
assert sensor_cfg.get_comment("AA:BB:CC:DD:EE:FF") is None
|
||||
|
||||
saved = json.loads(config_file.read_text())
|
||||
assert saved["sensors"][0]["mac"] == "AA:BB:CC:DD:EE:FF"
|
||||
assert saved["sensors"][0]["name"] == "Living Room"
|
||||
assert "comment" not in saved["sensors"][0]
|
||||
|
||||
def test_sensor_config_add_remove(tmp_path):
|
||||
import sensorpajen.config as config
|
||||
config_file = tmp_path / "sensors.json"
|
||||
@@ -47,10 +76,12 @@ def test_sensor_config_add_remove(tmp_path):
|
||||
# Add
|
||||
sensor_cfg.add_sensor("AA:BB:CC:DD:EE:FF", "Living Room", "Test comment")
|
||||
assert sensor_cfg.sensors["AA:BB:CC:DD:EE:FF"] == "Living Room"
|
||||
assert sensor_cfg.get_comment("AA:BB:CC:DD:EE:FF") == "Test comment"
|
||||
|
||||
# Verify persistence
|
||||
sensor_cfg2 = config.SensorConfig(config_file=str(config_file))
|
||||
assert sensor_cfg2.sensors["AA:BB:CC:DD:EE:FF"] == "Living Room"
|
||||
assert sensor_cfg2.get_comment("AA:BB:CC:DD:EE:FF") == "Test comment"
|
||||
|
||||
# Remove
|
||||
sensor_cfg.remove_sensor("AA:BB:CC:DD:EE:FF")
|
||||
|
||||
@@ -3,6 +3,12 @@ import os
|
||||
from pathlib import Path
|
||||
from sensorpajen.discovery_manager import DiscoveryManager, DiscoveredSensor
|
||||
|
||||
|
||||
class _DummyCompletedProcess:
|
||||
def __init__(self, returncode: int = 0, stderr: bytes = b""):
|
||||
self.returncode = returncode
|
||||
self.stderr = stderr
|
||||
|
||||
def test_discovery_manager_init(tmp_path):
|
||||
db_file = tmp_path / "sensors.db"
|
||||
manager = DiscoveryManager(str(db_file))
|
||||
@@ -55,3 +61,94 @@ def test_discovery_manager_persistence(tmp_path):
|
||||
assert len(pending) == 1
|
||||
assert pending[0].mac == mac
|
||||
assert pending[0].name == "ATC_123456"
|
||||
|
||||
|
||||
def test_send_ntfy_notification_disabled(monkeypatch, tmp_path):
|
||||
from sensorpajen import discovery_manager as dm_mod
|
||||
|
||||
monkeypatch.setattr(dm_mod.config, "NTFY_ENABLED", False)
|
||||
monkeypatch.setattr(dm_mod.config, "NTFY_TOKEN", "token")
|
||||
|
||||
called = {"run": False}
|
||||
|
||||
def _fake_run(*args, **kwargs):
|
||||
called["run"] = True
|
||||
return _DummyCompletedProcess(0)
|
||||
|
||||
monkeypatch.setattr(dm_mod.subprocess, "run", _fake_run)
|
||||
|
||||
manager = dm_mod.DiscoveryManager(str(tmp_path / "dummy.db"))
|
||||
sensor = dm_mod.DiscoveredSensor(
|
||||
mac="AA",
|
||||
name="N",
|
||||
rssi=-1,
|
||||
first_seen="now",
|
||||
last_seen="now",
|
||||
sample_reading={"temperature": 1, "humidity": 2, "battery_percent": 3},
|
||||
)
|
||||
|
||||
manager.send_ntfy_notification(sensor)
|
||||
assert called["run"] is False
|
||||
|
||||
|
||||
def test_send_ntfy_notification_missing_token(monkeypatch, tmp_path):
|
||||
from sensorpajen import discovery_manager as dm_mod
|
||||
|
||||
monkeypatch.setattr(dm_mod.config, "NTFY_ENABLED", True)
|
||||
monkeypatch.setattr(dm_mod.config, "NTFY_TOKEN", "")
|
||||
|
||||
called = {"run": False}
|
||||
|
||||
def _fake_run(*args, **kwargs):
|
||||
called["run"] = True
|
||||
return _DummyCompletedProcess(0)
|
||||
|
||||
monkeypatch.setattr(dm_mod.subprocess, "run", _fake_run)
|
||||
|
||||
manager = dm_mod.DiscoveryManager(str(tmp_path / "dummy2.db"))
|
||||
sensor = dm_mod.DiscoveredSensor(
|
||||
mac="AA",
|
||||
name="N",
|
||||
rssi=-1,
|
||||
first_seen="now",
|
||||
last_seen="now",
|
||||
sample_reading={"temperature": 1, "humidity": 2, "battery_percent": 3},
|
||||
)
|
||||
|
||||
manager.send_ntfy_notification(sensor)
|
||||
assert called["run"] is False
|
||||
|
||||
|
||||
def test_send_ntfy_notification_message_mentions_tui(monkeypatch, tmp_path):
|
||||
from sensorpajen import discovery_manager as dm_mod
|
||||
|
||||
monkeypatch.setattr(dm_mod.config, "NTFY_ENABLED", True)
|
||||
monkeypatch.setattr(dm_mod.config, "NTFY_TOKEN", "token")
|
||||
monkeypatch.setattr(dm_mod.config, "NTFY_URL", "https://ntfy.sh")
|
||||
monkeypatch.setattr(dm_mod.config, "NTFY_TOPIC", "sensorpajen")
|
||||
|
||||
captured = {"args": None}
|
||||
|
||||
def _fake_run(args, capture_output=True, timeout=10):
|
||||
captured["args"] = args
|
||||
return _DummyCompletedProcess(0)
|
||||
|
||||
monkeypatch.setattr(dm_mod.subprocess, "run", _fake_run)
|
||||
|
||||
manager = dm_mod.DiscoveryManager(str(tmp_path / "sensors.db"))
|
||||
sensor = dm_mod.DiscoveredSensor(
|
||||
mac="AA:BB",
|
||||
name="ATC_123",
|
||||
rssi=-1,
|
||||
first_seen="2025-01-01T00:00:00",
|
||||
last_seen="2025-01-01T00:00:00",
|
||||
sample_reading={"temperature": 10, "humidity": 20, "battery_percent": 30},
|
||||
)
|
||||
|
||||
manager.send_ntfy_notification(sensor)
|
||||
|
||||
assert captured["args"] is not None
|
||||
# curl args: [..., "-d", message, url]
|
||||
assert "-d" in captured["args"]
|
||||
message = captured["args"][captured["args"].index("-d") + 1]
|
||||
assert "sensorpajen-tui" in message
|
||||
|
||||
@@ -1,8 +1,225 @@
|
||||
import pytest
|
||||
from sensorpajen.tui.app import SensorpajenApp
|
||||
import tempfile
|
||||
import json
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from sensorpajen.config import SensorConfig
|
||||
from sensorpajen.discovery_manager import DiscoveryManager
|
||||
|
||||
def test_tui_app_init():
|
||||
# Just test that we can instantiate it
|
||||
app = SensorpajenApp()
|
||||
assert app.discovery_manager is not None
|
||||
assert app.sensor_config is not None
|
||||
def test_tui_sensor_config_edit():
|
||||
"""Integration test: Test that editing a sensor works end-to-end"""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
config_file = Path(tmpdir) / "sensors.json"
|
||||
db_file = Path(tmpdir) / "test.db"
|
||||
|
||||
# Create initial config
|
||||
initial_data = {
|
||||
"sensors": [
|
||||
{"mac": "AA:BB:CC:DD:EE:FF", "name": "Living Room Sensor"}
|
||||
]
|
||||
}
|
||||
config_file.write_text(json.dumps(initial_data, indent=2))
|
||||
|
||||
# Initialize database
|
||||
conn = sqlite3.connect(str(db_file))
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS discovered_sensors (
|
||||
mac TEXT PRIMARY KEY,
|
||||
name TEXT,
|
||||
rssi INTEGER,
|
||||
first_seen TIMESTAMP,
|
||||
last_seen TIMESTAMP,
|
||||
count INTEGER DEFAULT 0,
|
||||
last_temp REAL,
|
||||
last_humidity REAL,
|
||||
last_battery_percent INTEGER,
|
||||
last_battery_voltage INTEGER,
|
||||
status TEXT DEFAULT 'pending',
|
||||
reviewed BOOLEAN DEFAULT 0,
|
||||
ignored_at TIMESTAMP,
|
||||
ignore_reason TEXT
|
||||
)
|
||||
""")
|
||||
|
||||
now = datetime.now().isoformat()
|
||||
cursor.execute("""
|
||||
INSERT INTO discovered_sensors
|
||||
(mac, name, rssi, first_seen, last_seen, count, last_temp, last_humidity,
|
||||
last_battery_percent, last_battery_voltage, status, reviewed)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'approved', 1)
|
||||
""", ("AA:BB:CC:DD:EE:FF", "Living Room Sensor", -65, now, now, 50, 23.5, 55, 85, 2950))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
# Load config and discovery manager (simulating TUI)
|
||||
config = SensorConfig(str(config_file))
|
||||
dm = DiscoveryManager(str(db_file), config)
|
||||
|
||||
# Verify initial state
|
||||
assert config.sensors["AA:BB:CC:DD:EE:FF"] == "Living Room Sensor"
|
||||
|
||||
# Edit sensor (simulate user action in TUI)
|
||||
config.add_sensor("AA:BB:CC:DD:EE:FF", "Bedroom Sensor")
|
||||
|
||||
# Verify in-memory update
|
||||
assert config.sensors["AA:BB:CC:DD:EE:FF"] == "Bedroom Sensor"
|
||||
|
||||
# Verify disk update
|
||||
saved_data = json.loads(config_file.read_text())
|
||||
assert saved_data["sensors"][0]["name"] == "Bedroom Sensor"
|
||||
|
||||
# Simulate refresh_data() - create new config instance and verify
|
||||
config2 = SensorConfig(str(config_file))
|
||||
assert config2.sensors["AA:BB:CC:DD:EE:FF"] == "Bedroom Sensor"
|
||||
|
||||
def test_sensor_config_edit_updates_memory():
|
||||
"""Test that editing a sensor updates both disk and memory"""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
config_file = Path(tmpdir) / "sensors.json"
|
||||
|
||||
# Create initial config
|
||||
initial_data = {
|
||||
"sensors": [
|
||||
{"mac": "AA:BB:CC:DD:EE:FF", "name": "Original Name"}
|
||||
]
|
||||
}
|
||||
config_file.write_text(json.dumps(initial_data, indent=2))
|
||||
|
||||
# Load config
|
||||
config = SensorConfig(str(config_file))
|
||||
assert config.sensors["AA:BB:CC:DD:EE:FF"] == "Original Name"
|
||||
|
||||
# Edit sensor
|
||||
config.add_sensor("AA:BB:CC:DD:EE:FF", "Updated Name")
|
||||
|
||||
# Check in-memory is updated
|
||||
assert config.sensors["AA:BB:CC:DD:EE:FF"] == "Updated Name"
|
||||
|
||||
# Check disk is updated
|
||||
saved_data = json.loads(config_file.read_text())
|
||||
assert saved_data["sensors"][0]["name"] == "Updated Name"
|
||||
|
||||
# Reload from disk and verify
|
||||
config2 = SensorConfig(str(config_file))
|
||||
assert config2.sensors["AA:BB:CC:DD:EE:FF"] == "Updated Name"
|
||||
|
||||
def test_sensor_config_remove_sensor():
|
||||
"""Test that removing a sensor works correctly"""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
config_file = Path(tmpdir) / "sensors.json"
|
||||
|
||||
# Create config with multiple sensors
|
||||
initial_data = {
|
||||
"sensors": [
|
||||
{"mac": "AA:BB:CC:DD:EE:FF", "name": "Sensor 1"},
|
||||
{"mac": "AA:BB:CC:DD:EE:11", "name": "Sensor 2"}
|
||||
]
|
||||
}
|
||||
config_file.write_text(json.dumps(initial_data, indent=2))
|
||||
|
||||
# Load and verify
|
||||
config = SensorConfig(str(config_file))
|
||||
assert len(config.sensors) == 2
|
||||
|
||||
# Remove one sensor
|
||||
config.remove_sensor("AA:BB:CC:DD:EE:FF")
|
||||
|
||||
# Check in-memory removal
|
||||
assert "AA:BB:CC:DD:EE:FF" not in config.sensors
|
||||
assert "AA:BB:CC:DD:EE:11" in config.sensors
|
||||
|
||||
# Check disk update
|
||||
saved_data = json.loads(config_file.read_text())
|
||||
assert len(saved_data["sensors"]) == 1
|
||||
assert saved_data["sensors"][0]["mac"] == "AA:BB:CC:DD:EE:11"
|
||||
|
||||
def test_sensor_config_reload():
|
||||
"""Test that reload() re-reads from disk"""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
config_file = Path(tmpdir) / "sensors.json"
|
||||
|
||||
# Create initial config
|
||||
initial_data = {
|
||||
"sensors": [
|
||||
{"mac": "AA:BB:CC:DD:EE:FF", "name": "Original Name"}
|
||||
]
|
||||
}
|
||||
config_file.write_text(json.dumps(initial_data, indent=2))
|
||||
|
||||
# Load config
|
||||
config = SensorConfig(str(config_file))
|
||||
assert config.sensors["AA:BB:CC:DD:EE:FF"] == "Original Name"
|
||||
|
||||
# Manually modify file on disk
|
||||
new_data = {
|
||||
"sensors": [
|
||||
{"mac": "AA:BB:CC:DD:EE:FF", "name": "Externally Modified"}
|
||||
]
|
||||
}
|
||||
config_file.write_text(json.dumps(new_data, indent=2))
|
||||
|
||||
# Reload should pick up the changes
|
||||
config.load()
|
||||
assert config.sensors["AA:BB:CC:DD:EE:FF"] == "Externally Modified"
|
||||
|
||||
def test_discovery_manager_approve_sensor():
|
||||
"""Test that approving a sensor works correctly"""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
config_file = Path(tmpdir) / "sensors.json"
|
||||
db_file = Path(tmpdir) / "test.db"
|
||||
|
||||
# Create empty config
|
||||
config_file.write_text(json.dumps({"sensors": []}, indent=2))
|
||||
|
||||
# Initialize database with pending sensor
|
||||
conn = sqlite3.connect(str(db_file))
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS discovered_sensors (
|
||||
mac TEXT PRIMARY KEY,
|
||||
name TEXT,
|
||||
rssi INTEGER,
|
||||
first_seen TIMESTAMP,
|
||||
last_seen TIMESTAMP,
|
||||
count INTEGER DEFAULT 0,
|
||||
last_temp REAL,
|
||||
last_humidity REAL,
|
||||
last_battery_percent INTEGER,
|
||||
last_battery_voltage INTEGER,
|
||||
status TEXT DEFAULT 'pending',
|
||||
reviewed BOOLEAN DEFAULT 0,
|
||||
ignored_at TIMESTAMP,
|
||||
ignore_reason TEXT
|
||||
)
|
||||
""")
|
||||
|
||||
now = datetime.now().isoformat()
|
||||
cursor.execute("""
|
||||
INSERT INTO discovered_sensors
|
||||
(mac, name, rssi, first_seen, last_seen, count, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""", ("AA:BB:CC:DD:EE:33", "Unknown Sensor", -80, now, now, 1, "pending"))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
# Load config and DM
|
||||
config = SensorConfig(str(config_file))
|
||||
dm = DiscoveryManager(str(db_file), config)
|
||||
|
||||
# Verify sensor is pending
|
||||
pending = dm.get_pending()
|
||||
assert len(pending) == 1
|
||||
assert pending[0].mac == "AA:BB:CC:DD:EE:33"
|
||||
|
||||
# Approve and add to config (simulate TUI action)
|
||||
config.add_sensor("AA:BB:CC:DD:EE:33", "Kitchen Sensor")
|
||||
dm.approve("AA:BB:CC:DD:EE:33")
|
||||
|
||||
# Verify sensor is no longer pending (filtered by config)
|
||||
pending = dm.get_pending()
|
||||
assert len(pending) == 0
|
||||
|
||||
# Verify it's in config
|
||||
assert config.sensors["AA:BB:CC:DD:EE:33"] == "Kitchen Sensor"
|
||||
|
||||
Reference in New Issue
Block a user