Fix system installation state directory

The service was failing with 'Read-only file system' when trying to create
discovered_sensors.json in the /etc/sensorpajen config directory.

Changes:
- config.py: Add STATE_DIR for runtime state
  - System mode: /var/lib/sensorpajen (writable at runtime)
  - Dev mode: config/ (same as config directory)
- config.py: Use STATE_DIR for discovered_sensors.json path
- debian/postinst: Create and own /var/lib/sensorpajen
- debian/sensorpajen.service: Add /var/lib/sensorpajen to ReadWritePaths
- debian/postinst: Remove discovered_sensors.json.example copy (created at runtime)

This separates:
- Config: /etc/sensorpajen (static, not updated by service)
- State: /var/lib/sensorpajen (dynamic, updated by service at runtime)
This commit is contained in:
2025-12-28 09:33:26 +01:00
parent 85af215d73
commit fc0399a454
11 changed files with 51 additions and 22 deletions

1
debian/files vendored
View File

@@ -1,2 +1 @@
sensorpajen_2.0.0-dev_all.deb misc optional sensorpajen_2.0.0-dev_all.deb misc optional
sensorpajen_2.0.0-dev_amd64.buildinfo misc optional

7
debian/postinst vendored
View File

@@ -14,8 +14,13 @@ case "$1" in
chown sensorpajen:sensorpajen /etc/sensorpajen chown sensorpajen:sensorpajen /etc/sensorpajen
chmod 750 /etc/sensorpajen chmod 750 /etc/sensorpajen
# Create state directory with proper permissions (writable at runtime)
mkdir -p /var/lib/sensorpajen
chown sensorpajen:sensorpajen /var/lib/sensorpajen
chmod 750 /var/lib/sensorpajen
# Copy example configs to /etc/sensorpajen if they don't exist # Copy example configs to /etc/sensorpajen if they don't exist
for sample in sensorpajen.env.example sensors.json.example discovered_sensors.json.example; do for sample in sensorpajen.env.example sensors.json.example; do
source_file="/usr/share/doc/sensorpajen/examples/$sample" source_file="/usr/share/doc/sensorpajen/examples/$sample"
target_file="/etc/sensorpajen/${sample%.example}" target_file="/etc/sensorpajen/${sample%.example}"

View File

@@ -26,7 +26,7 @@ SyslogIdentifier=sensorpajen
PrivateTmp=true PrivateTmp=true
ProtectSystem=strict ProtectSystem=strict
ProtectHome=true ProtectHome=true
ReadWritePaths=/etc/sensorpajen ReadWritePaths=/etc/sensorpajen /var/lib/sensorpajen
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

View File

@@ -2,7 +2,7 @@ Package: sensorpajen
Version: 2.0.0-dev Version: 2.0.0-dev
Architecture: all Architecture: all
Maintainer: Fredrik <fredrik@wahlberg.se> Maintainer: Fredrik <fredrik@wahlberg.se>
Installed-Size: 110 Installed-Size: 111
Depends: python3 (>= 3.9), python3-venv, python3-pip, bluetooth, bluez, libcap2-bin Depends: python3 (>= 3.9), python3-venv, python3-pip, bluetooth, bluez, libcap2-bin
Recommends: mosquitto-clients Recommends: mosquitto-clients
Section: misc Section: misc

View File

@@ -3,17 +3,17 @@
940d73f24eb9f971ce27f9355e3072f3 opt/sensorpajen/scripts/approve-sensors.sh 940d73f24eb9f971ce27f9355e3072f3 opt/sensorpajen/scripts/approve-sensors.sh
20eb4f3839b990a530410768897402c0 opt/sensorpajen/src/sensorpajen/__init__.py 20eb4f3839b990a530410768897402c0 opt/sensorpajen/src/sensorpajen/__init__.py
1f452c46e42f8dc3751dba6ca68256e9 opt/sensorpajen/src/sensorpajen/approve_sensors.py 1f452c46e42f8dc3751dba6ca68256e9 opt/sensorpajen/src/sensorpajen/approve_sensors.py
da40d9df301d523d517d7cf2809d6f11 opt/sensorpajen/src/sensorpajen/config.py 8d781ed202be540358a970c2be50f54d opt/sensorpajen/src/sensorpajen/config.py
65c63383dde4f0b249b708f854ec75a3 opt/sensorpajen/src/sensorpajen/discovery_manager.py 65c63383dde4f0b249b708f854ec75a3 opt/sensorpajen/src/sensorpajen/discovery_manager.py
592f8a534833c9e403967fcc0ead8eb1 opt/sensorpajen/src/sensorpajen/main.py 7604c2bc0a854d6d43ff0f0646386fc5 opt/sensorpajen/src/sensorpajen/main.py
331bf9b314492acc6ce03896367f3cf6 opt/sensorpajen/src/sensorpajen/mqtt_publisher.py 331bf9b314492acc6ce03896367f3cf6 opt/sensorpajen/src/sensorpajen/mqtt_publisher.py
5f4ea191e35ce092f39ec0a4f663cb38 opt/sensorpajen/src/sensorpajen/sensor_reader.py 5f4ea191e35ce092f39ec0a4f663cb38 opt/sensorpajen/src/sensorpajen/sensor_reader.py
c8dd8fe8fc174a9cd35251fdf80e7b5f opt/sensorpajen/src/sensorpajen/utils.py c8dd8fe8fc174a9cd35251fdf80e7b5f opt/sensorpajen/src/sensorpajen/utils.py
c9c22f9c1d65bfafd89fa45f16b7192b usr/lib/systemd/system/sensorpajen.service b9ad3ea8307d8ed8e938da37ad00f229 usr/lib/systemd/system/sensorpajen.service
4ddb9618c940286f91df901ec818959a usr/share/doc/sensorpajen/INSTALL.md.gz 4ddb9618c940286f91df901ec818959a usr/share/doc/sensorpajen/INSTALL.md.gz
bd2f1371c60af415bc9d0dbc1111184d usr/share/doc/sensorpajen/ROADMAP.md.gz bd2f1371c60af415bc9d0dbc1111184d usr/share/doc/sensorpajen/ROADMAP.md.gz
380e8e6b01b757ceac05bc5805844ae4 usr/share/doc/sensorpajen/changelog.Debian.gz 380e8e6b01b757ceac05bc5805844ae4 usr/share/doc/sensorpajen/changelog.Debian.gz
14152a98d7cd7fe8daf280aacc4cbf3f usr/share/doc/sensorpajen/examples/discovered_sensors.json.example 14152a98d7cd7fe8daf280aacc4cbf3f usr/share/doc/sensorpajen/examples/discovered_sensors.json.example
387cb9ee7f22570312604e2cc07ca7a0 usr/share/doc/sensorpajen/examples/sensorpajen.env.example 74c99b732363f93f0a1c134e1a8c3d35 usr/share/doc/sensorpajen/examples/sensorpajen.env.example
292efbddd951c39cb2c9546d5fac5e05 usr/share/doc/sensorpajen/examples/sensors.json.example 292efbddd951c39cb2c9546d5fac5e05 usr/share/doc/sensorpajen/examples/sensors.json.example
5f647c63bfc3b174611694779fd215e0 usr/share/doc/sensorpajen/readme.md.gz 5f647c63bfc3b174611694779fd215e0 usr/share/doc/sensorpajen/readme.md.gz

View File

@@ -14,8 +14,13 @@ case "$1" in
chown sensorpajen:sensorpajen /etc/sensorpajen chown sensorpajen:sensorpajen /etc/sensorpajen
chmod 750 /etc/sensorpajen chmod 750 /etc/sensorpajen
# Create state directory with proper permissions (writable at runtime)
mkdir -p /var/lib/sensorpajen
chown sensorpajen:sensorpajen /var/lib/sensorpajen
chmod 750 /var/lib/sensorpajen
# Copy example configs to /etc/sensorpajen if they don't exist # Copy example configs to /etc/sensorpajen if they don't exist
for sample in sensorpajen.env.example sensors.json.example discovered_sensors.json.example; do for sample in sensorpajen.env.example sensors.json.example; do
source_file="/usr/share/doc/sensorpajen/examples/$sample" source_file="/usr/share/doc/sensorpajen/examples/$sample"
target_file="/etc/sensorpajen/${sample%.example}" target_file="/etc/sensorpajen/${sample%.example}"
@@ -48,7 +53,7 @@ case "$1" in
venv/bin/pip install -r /opt/sensorpajen/requirements.txt venv/bin/pip install -r /opt/sensorpajen/requirements.txt
else else
echo "Warning: requirements.txt not found, installing bluepy and paho-mqtt directly" echo "Warning: requirements.txt not found, installing bluepy and paho-mqtt directly"
venv/bin/pip install bluepy paho-mqtt venv/bin/pip install bluepy paho-mqtt pybluez
fi fi
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
@@ -56,6 +61,15 @@ case "$1" in
exit 1 exit 1
fi fi
# Install sensorpajen package itself
echo "Installing sensorpajen application..."
cd /opt/sensorpajen
venv/bin/pip install --no-deps . || {
echo "Error: Failed to install sensorpajen package"
exit 1
}
cd /
# Set ownership of application directory BEFORE setting capabilities # Set ownership of application directory BEFORE setting capabilities
chown -R sensorpajen:sensorpajen /opt/sensorpajen chown -R sensorpajen:sensorpajen /opt/sensorpajen

View File

@@ -19,10 +19,12 @@ if Path('/opt/sensorpajen').exists():
# System installation # System installation
PROJECT_ROOT = Path('/opt/sensorpajen') PROJECT_ROOT = Path('/opt/sensorpajen')
CONFIG_DIR = Path('/etc/sensorpajen') CONFIG_DIR = Path('/etc/sensorpajen')
STATE_DIR = Path('/var/lib/sensorpajen')
else: else:
# Development installation (3 levels up from this file: src/sensorpajen/config.py) # Development installation (3 levels up from this file: src/sensorpajen/config.py)
PROJECT_ROOT = Path(__file__).parent.parent.parent PROJECT_ROOT = Path(__file__).parent.parent.parent
CONFIG_DIR = PROJECT_ROOT / "config" CONFIG_DIR = PROJECT_ROOT / "config"
STATE_DIR = CONFIG_DIR
# MQTT Configuration from environment # MQTT Configuration from environment
MQTT_HOST = os.environ.get("MQTT_HOST") MQTT_HOST = os.environ.get("MQTT_HOST")
@@ -63,7 +65,7 @@ NTFY_TOKEN = os.environ.get("NTFY_TOKEN", "")
# Discovery settings # Discovery settings
DISCOVERED_SENSORS_FILE = os.environ.get( DISCOVERED_SENSORS_FILE = os.environ.get(
"DISCOVERED_SENSORS_FILE", "DISCOVERED_SENSORS_FILE",
str(CONFIG_DIR / "discovered_sensors.json") str(STATE_DIR / "discovered_sensors.json")
) )
CONFIG_RELOAD_INTERVAL = int(os.environ.get("CONFIG_RELOAD_INTERVAL", "900")) # 15 minutes CONFIG_RELOAD_INTERVAL = int(os.environ.get("CONFIG_RELOAD_INTERVAL", "900")) # 15 minutes
@@ -85,11 +87,11 @@ class SensorConfig:
def load(self): def load(self):
"""Load sensor configuration from JSON file.""" """Load sensor configuration from JSON file."""
if not self.config_file.exists(): if not self.config_file.exists():
raise FileNotFoundError( logger.warning(
f"Sensor configuration file not found: {self.config_file}\n" f"Sensor configuration file not found: {self.config_file}\n"
f"Please copy config/sensors.json.example to config/sensors.json " f"Starting with no sensors - use discovery to add sensors"
f"and configure your sensors."
) )
return
try: try:
with open(self.config_file, 'r') as f: with open(self.config_file, 'r') as f:

View File

@@ -125,9 +125,9 @@ class Sensorpajen:
self.sensor_config = config.SensorConfig() self.sensor_config = config.SensorConfig()
if len(self.sensor_config.sensors) == 0: if len(self.sensor_config.sensors) == 0:
self.logger.error("No sensors configured!") self.logger.warning("No sensors configured")
self.logger.error("Please configure sensors in config/sensors.json") self.logger.warning("Starting in discovery-only mode")
sys.exit(1) self.logger.warning("Use 'sensorpajen approve-sensors' to add sensors")
# Initialize discovery manager # Initialize discovery manager
self.logger.info("Initializing discovery manager...") self.logger.info("Initializing discovery manager...")

View File

@@ -26,7 +26,7 @@ SyslogIdentifier=sensorpajen
PrivateTmp=true PrivateTmp=true
ProtectSystem=strict ProtectSystem=strict
ProtectHome=true ProtectHome=true
ReadWritePaths=/etc/sensorpajen ReadWritePaths=/etc/sensorpajen /var/lib/sensorpajen
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

View File

@@ -5,9 +5,16 @@ MQTT_USER=hasse
MQTT_PASSWORD=casablanca MQTT_PASSWORD=casablanca
MQTT_CLIENT_ID=mibridge MQTT_CLIENT_ID=mibridge
# Sensor Configuration (relative to project root) # Sensor Configuration
SENSOR_CONFIG_FILE=config/sensors.json # For system installation (/opt/sensorpajen): Use absolute paths
DISCOVERED_SENSORS_FILE=config/discovered_sensors.json # SENSOR_CONFIG_FILE=/etc/sensorpajen/sensors.json
# DISCOVERED_SENSORS_FILE=/etc/sensorpajen/discovered_sensors.json
#
# For development installation: Use relative paths (from project root)
# SENSOR_CONFIG_FILE=config/sensors.json
# DISCOVERED_SENSORS_FILE=config/discovered_sensors.json
#
# If not set, defaults will be used based on installation type
# Application Settings # Application Settings
WATCHDOG_TIMEOUT=5 WATCHDOG_TIMEOUT=5

View File

@@ -19,10 +19,12 @@ if Path('/opt/sensorpajen').exists():
# System installation # System installation
PROJECT_ROOT = Path('/opt/sensorpajen') PROJECT_ROOT = Path('/opt/sensorpajen')
CONFIG_DIR = Path('/etc/sensorpajen') CONFIG_DIR = Path('/etc/sensorpajen')
STATE_DIR = Path('/var/lib/sensorpajen')
else: else:
# Development installation (3 levels up from this file: src/sensorpajen/config.py) # Development installation (3 levels up from this file: src/sensorpajen/config.py)
PROJECT_ROOT = Path(__file__).parent.parent.parent PROJECT_ROOT = Path(__file__).parent.parent.parent
CONFIG_DIR = PROJECT_ROOT / "config" CONFIG_DIR = PROJECT_ROOT / "config"
STATE_DIR = CONFIG_DIR
# MQTT Configuration from environment # MQTT Configuration from environment
MQTT_HOST = os.environ.get("MQTT_HOST") MQTT_HOST = os.environ.get("MQTT_HOST")
@@ -63,7 +65,7 @@ NTFY_TOKEN = os.environ.get("NTFY_TOKEN", "")
# Discovery settings # Discovery settings
DISCOVERED_SENSORS_FILE = os.environ.get( DISCOVERED_SENSORS_FILE = os.environ.get(
"DISCOVERED_SENSORS_FILE", "DISCOVERED_SENSORS_FILE",
str(CONFIG_DIR / "discovered_sensors.json") str(STATE_DIR / "discovered_sensors.json")
) )
CONFIG_RELOAD_INTERVAL = int(os.environ.get("CONFIG_RELOAD_INTERVAL", "900")) # 15 minutes CONFIG_RELOAD_INTERVAL = int(os.environ.get("CONFIG_RELOAD_INTERVAL", "900")) # 15 minutes