add configuration system for ESP Sensors project

This commit is contained in:
OMGeeky
2025-05-07 21:27:32 +02:00
parent e12ba1aaee
commit f544a64ecf
14 changed files with 813 additions and 158 deletions

185
src/esp_sensors/config.py Normal file
View File

@@ -0,0 +1,185 @@
"""
Configuration module for ESP sensors.
This module provides functionality to load and save configuration settings
from/to a file, making it easy to change parameters like pins, display resolution,
sensor names, and intervals without modifying the code.
"""
import json
import os
from typing import Dict, Any, Optional, Union, List
# Default configuration file path
DEFAULT_CONFIG_PATH = "config.json"
# Default configuration values
DEFAULT_CONFIG = {
"sensors": {
"temperature": {
"name": "Temperature Sensor",
"pin": 4,
"interval": 60,
"unit": "C"
},
"humidity": {
"name": "Humidity Sensor",
"pin": 4,
"interval": 60
},
"dht22": {
"name": "DHT22 Sensor",
"pin": 4,
"interval": 60,
"temperature": {
"name": "DHT22 Temperature",
"unit": "C"
},
"humidity": {
"name": "DHT22 Humidity"
}
}
},
"displays": {
"oled": {
"name": "OLED Display",
"scl_pin": 22,
"sda_pin": 21,
"width": 128,
"height": 64,
"address": "0x3C",
"interval": 1
}
},
"buttons": {
"main_button": {
"pin": 0,
"pull_up": True
}
}
}
def load_config(config_path: str = DEFAULT_CONFIG_PATH) -> Dict[str, Any]:
"""
Load configuration from a JSON file.
Args:
config_path: Path to the configuration file (default: config.json)
Returns:
A dictionary containing the configuration
If the file doesn't exist or can't be read, returns the default configuration.
"""
try:
if os.path.exists(config_path):
with open(config_path, 'r') as f:
config = json.load(f)
return config
else:
print(f"Configuration file {config_path} not found. Using default configuration.")
return DEFAULT_CONFIG
except Exception as e:
print(f"Error loading configuration: {e}. Using default configuration.")
return DEFAULT_CONFIG
def save_config(config: Dict[str, Any], config_path: str = DEFAULT_CONFIG_PATH) -> bool:
"""
Save configuration to a JSON file.
Args:
config: Configuration dictionary to save
config_path: Path to the configuration file (default: config.json)
Returns:
True if the configuration was saved successfully, False otherwise
"""
try:
with open(config_path, 'w') as f:
json.dump(config, f, indent=4)
return True
except Exception as e:
print(f"Error saving configuration: {e}")
return False
def get_sensor_config(sensor_type: str, config: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
"""
Get configuration for a specific sensor type.
Args:
sensor_type: Type of the sensor (e.g., 'temperature', 'humidity', 'dht22')
config: Configuration dictionary (if None, loads from default path)
Returns:
A dictionary containing the sensor configuration
"""
if config is None:
config = load_config()
# Try to get the sensor configuration, fall back to default if not found
sensor_config = config.get("sensors", {}).get(sensor_type)
if sensor_config is None:
sensor_config = DEFAULT_CONFIG.get("sensors", {}).get(sensor_type, {})
return sensor_config
def get_display_config(display_type: str, config: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
"""
Get configuration for a specific display type.
Args:
display_type: Type of the display (e.g., 'oled')
config: Configuration dictionary (if None, loads from default path)
Returns:
A dictionary containing the display configuration
"""
if config is None:
config = load_config()
# Try to get the display configuration, fall back to default if not found
display_config = config.get("displays", {}).get(display_type)
if display_config is None:
display_config = DEFAULT_CONFIG.get("displays", {}).get(display_type, {})
return display_config
def get_button_config(button_name: str = "main_button", config: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
"""
Get configuration for a specific button.
Args:
button_name: Name of the button (e.g., 'main_button')
config: Configuration dictionary (if None, loads from default path)
Returns:
A dictionary containing the button configuration
"""
if config is None:
config = load_config()
# Try to get the button configuration, fall back to default if not found
button_config = config.get("buttons", {}).get(button_name)
if button_config is None:
button_config = DEFAULT_CONFIG.get("buttons", {}).get(button_name, {})
return button_config
def create_default_config(config_path: str = DEFAULT_CONFIG_PATH) -> bool:
"""
Create a default configuration file.
Args:
config_path: Path to the configuration file (default: config.json)
Returns:
True if the configuration was created successfully, False otherwise
"""
return save_config(DEFAULT_CONFIG, config_path)

View File

@@ -2,6 +2,7 @@
DHT22 temperature and humidity sensor module for ESP32.
"""
import time
from typing import Dict, Any, Optional
try:
import dht
from machine import Pin
@@ -12,24 +13,48 @@ except ImportError:
from .temperature import TemperatureSensor
from .humidity import HumiditySensor
from .config import get_sensor_config
class DHT22Sensor(TemperatureSensor, HumiditySensor):
"""DHT22 temperature and humidity sensor implementation."""
def __init__(self, name: str, pin: int, interval: int = 60, unit: str = "C"):
def __init__(self, name: str = None, pin: int = None, interval: int = None,
unit: str = None, sensor_type: str = "dht22", config: Dict[str, Any] = None):
"""
Initialize a new DHT22 sensor.
Args:
name: The name of the sensor
pin: The GPIO pin number the sensor is connected to
interval: Reading interval in seconds (default: 60)
unit: Temperature unit, either "C" for Celsius or "F" for Fahrenheit (default: "C")
name: The name of the sensor (if None, loaded from config)
pin: The GPIO pin number the sensor is connected to (if None, loaded from config)
interval: Reading interval in seconds (if None, loaded from config)
unit: Temperature unit, either "C" or "F" (if None, loaded from config)
sensor_type: Type of the sensor for loading config (default: 'dht22')
config: Configuration dictionary (if provided, used instead of loading from file)
"""
# Load configuration
sensor_config = get_sensor_config(sensor_type, config)
# Get main parameters from config if not provided
self.name = name if name is not None else sensor_config.get('name', 'DHT22 Sensor')
self.pin = pin if pin is not None else sensor_config.get('pin', 0)
self.interval = interval if interval is not None else sensor_config.get('interval', 60)
# Get temperature configuration
temp_config = sensor_config.get('temperature', {})
temp_name = temp_config.get('name', self.name + ' Temperature')
temp_unit = unit if unit is not None else temp_config.get('unit', 'C')
# Get humidity configuration
humidity_config = sensor_config.get('humidity', {})
humidity_name = humidity_config.get('name', self.name + ' Humidity')
# Initialize both parent classes
TemperatureSensor.__init__(self, name, pin, interval, unit)
HumiditySensor.__init__(self, name, pin, interval)
TemperatureSensor.__init__(self, name=temp_name, pin=self.pin, interval=self.interval, unit=temp_unit)
HumiditySensor.__init__(self, name=humidity_name, pin=self.pin, interval=self.interval)
# Store the original sensor name (it gets overwritten by HumiditySensor.__init__)
self.name = name if name is not None else sensor_config.get('name', 'DHT22 Sensor')
# Initialize the sensor if not in simulation mode
if not SIMULATION:
@@ -117,6 +142,8 @@ class DHT22Sensor(TemperatureSensor, HumiditySensor):
# Combine metadata from both parent classes
metadata = {**temp_metadata, **humidity_metadata}
# Ensure the name is the main sensor name, not the humidity sensor name
metadata["name"] = self.name
metadata["type"] = "DHT22"
return metadata

View File

@@ -2,22 +2,29 @@
Humidity sensor module for ESP-based sensors.
"""
import random
from typing import Dict, Any, Optional
from .sensor import Sensor
from .config import get_sensor_config
class HumiditySensor(Sensor):
"""Humidity sensor implementation."""
def __init__(self, name: str, pin: int, interval: int = 60):
def __init__(self, name: str = None, pin: int = None, interval: int = None,
sensor_type: str = None, config: Dict[str, Any] = None, **kwargs):
"""
Initialize a new humidity sensor.
Args:
name: The name of the sensor
pin: The GPIO pin number the sensor is connected to
interval: Reading interval in seconds (default: 60)
name: The name of the sensor (if None, loaded from config)
pin: The GPIO pin number the sensor is connected to (if None, loaded from config)
interval: Reading interval in seconds (if None, loaded from config)
sensor_type: Type of the sensor for loading config (e.g., 'humidity')
config: Configuration dictionary (if provided, used instead of loading from file)
**kwargs: Additional keyword arguments to pass to the parent class
"""
super().__init__(name, pin, interval)
# Initialize base class with sensor_type for configuration loading
super().__init__(name, pin, interval, sensor_type=sensor_type or "humidity", config=config)
self._last_humidity = None
def read_humidity(self) -> float:
@@ -41,4 +48,4 @@ class HumiditySensor(Sensor):
"""
metadata = super().get_metadata()
metadata["last_humidity"] = self._last_humidity
return metadata
return metadata

View File

@@ -2,6 +2,7 @@
OLED display module for ESP32 using SSD1306 controller.
"""
import time
from typing import Dict, Any, Optional
try:
from machine import Pin, I2C
import ssd1306
@@ -10,35 +11,57 @@ except ImportError:
SIMULATION = True
from .sensor import Sensor
from .config import get_display_config
class OLEDDisplay(Sensor):
"""SSD1306 OLED display implementation."""
def __init__(self, name: str, scl_pin: int, sda_pin: int, width: int = 128, height: int = 64,
address: int = 0x3C, interval: int = 60):
def __init__(self, name: str = None, scl_pin: int = None, sda_pin: int = None,
width: int = None, height: int = None, address: str = None,
interval: int = None, config: Dict[str, Any] = None):
"""
Initialize a new OLED display.
Args:
name: The name of the display
scl_pin: The GPIO pin number for the SCL (clock) line
sda_pin: The GPIO pin number for the SDA (data) line
width: Display width in pixels (default: 128)
height: Display height in pixels (default: 64)
address: I2C address of the display (default: 0x3C)
interval: Refresh interval in seconds (default: 60)
name: The name of the display (if None, loaded from config)
scl_pin: The GPIO pin number for the SCL (clock) line (if None, loaded from config)
sda_pin: The GPIO pin number for the SDA (data) line (if None, loaded from config)
width: Display width in pixels (if None, loaded from config)
height: Display height in pixels (if None, loaded from config)
address: I2C address of the display (if None, loaded from config)
interval: Refresh interval in seconds (if None, loaded from config)
config: Configuration dictionary (if provided, used instead of loading from file)
"""
# Use sda_pin as the pin parameter for the Sensor base class
super().__init__(name, sda_pin, interval)
self.scl_pin = scl_pin
self.sda_pin = sda_pin
self.width = width
self.height = height
self.address = address
# Load configuration if needed
display_config = get_display_config("oled", config)
# Get parameters from config if not provided
name = name if name is not None else display_config.get("name", "OLED Display")
sda_pin = sda_pin if sda_pin is not None else display_config.get("sda_pin", 21)
interval = interval if interval is not None else display_config.get("interval", 60)
# Initialize base class with the pin parameter
super().__init__(name=name, pin=sda_pin, interval=interval)
# Set display-specific parameters
self.scl_pin = scl_pin if scl_pin is not None else display_config.get("scl_pin", 22)
self.sda_pin = sda_pin # Already set above
self.width = width if width is not None else display_config.get("width", 128)
self.height = height if height is not None else display_config.get("height", 64)
# Handle address (could be string in config)
if address is None:
address = display_config.get("address", "0x3C")
# Convert address to int if it's a hex string
if isinstance(address, str) and address.startswith("0x"):
self.address = int(address, 16)
else:
self.address = address
self._values = []
# Initialize the display if not in simulation mode
if not SIMULATION:
try:
@@ -91,7 +114,7 @@ class OLEDDisplay(Sensor):
values: List of values to display (strings or objects with __str__ method)
"""
self._values = values
if SIMULATION:
print("Simulated OLED display values:")
for i, value in enumerate(values):
@@ -99,12 +122,12 @@ class OLEDDisplay(Sensor):
else:
if self._display:
self._display.fill(0) # Clear the display
# Display each value on a new line (8 pixels per line)
for i, value in enumerate(values):
if i * 10 < self.height: # Make sure we don't go off the screen
self._display.text(str(value), 0, i * 10, 1)
self._display.show()
def read(self) -> float:
@@ -133,4 +156,4 @@ class OLEDDisplay(Sensor):
metadata["address"] = self.address
metadata["type"] = "SSD1306"
metadata["values_count"] = len(self._values)
return metadata
return metadata

View File

@@ -2,23 +2,38 @@
Base sensor module for ESP-based sensors.
"""
from typing import Dict, Any, Optional
from .config import get_sensor_config
class Sensor:
"""Base class for all sensors."""
def __init__(self, name: str, pin: int, interval: int = 60):
def __init__(self, name: str = None, pin: int = None, interval: int = None,
sensor_type: str = None, config: Dict[str, Any] = None):
"""
Initialize a new sensor.
Args:
name: The name of the sensor
pin: The GPIO pin number the sensor is connected to
interval: Reading interval in seconds (default: 60)
name: The name of the sensor (if None, loaded from config)
pin: The GPIO pin number the sensor is connected to (if None, loaded from config)
interval: Reading interval in seconds (if None, loaded from config)
sensor_type: Type of the sensor for loading config (e.g., 'temperature')
config: Configuration dictionary (if provided, used instead of loading from file)
"""
self.name = name
self.pin = pin
self.interval = interval
# Load configuration if sensor_type is provided
if sensor_type:
sensor_config = get_sensor_config(sensor_type, config)
# Use provided values or fall back to config values
self.name = name if name is not None else sensor_config.get('name', 'Unnamed Sensor')
self.pin = pin if pin is not None else sensor_config.get('pin', 0)
self.interval = interval if interval is not None else sensor_config.get('interval', 60)
else:
# Use provided values or defaults
self.name = name if name is not None else 'Unnamed Sensor'
self.pin = pin if pin is not None else 0
self.interval = interval if interval is not None else 60
self._last_reading: Optional[float] = None
def read(self) -> float:

View File

@@ -2,25 +2,38 @@
Temperature sensor module for ESP-based sensors.
"""
import random
from typing import Dict, Any, Optional
from .sensor import Sensor
from .config import get_sensor_config
class TemperatureSensor(Sensor):
"""Temperature sensor implementation."""
def __init__(self, name: str, pin: int, interval: int = 60, unit: str = "C"):
def __init__(self, name: str = None, pin: int = None, interval: int = None,
unit: str = None, config: Dict[str, Any] = None):
"""
Initialize a new temperature sensor.
Args:
name: The name of the sensor
pin: The GPIO pin number the sensor is connected to
interval: Reading interval in seconds (default: 60)
unit: Temperature unit, either "C" for Celsius or "F" for Fahrenheit (default: "C")
name: The name of the sensor (if None, loaded from config)
pin: The GPIO pin number the sensor is connected to (if None, loaded from config)
interval: Reading interval in seconds (if None, loaded from config)
unit: Temperature unit, either "C" or "F" (if None, loaded from config)
config: Configuration dictionary (if provided, used instead of loading from file)
"""
super().__init__(name, pin, interval)
# Initialize base class with sensor_type for configuration loading
super().__init__(name, pin, interval, sensor_type="temperature", config=config)
# Load configuration if not provided in parameters
if unit is None:
sensor_config = get_sensor_config("temperature", config)
unit = sensor_config.get("unit", "C")
# Validate unit
if unit not in ["C", "F"]:
raise ValueError("Unit must be either 'C' or 'F'")
self.unit = unit
def read_temperature(self) -> float:

View File

@@ -12,6 +12,7 @@ import sys
from esp_sensors.oled_display import OLEDDisplay
from esp_sensors.dht22 import DHT22Sensor
from esp_sensors.config import load_config, get_button_config
# Import hardware-specific modules if available (for ESP32/ESP8266)
try:
@@ -40,28 +41,25 @@ def main():
"""
Main function to demonstrate button-triggered sensor display.
"""
# Initialize a DHT22 sensor
# Load configuration
config = load_config()
button_config = get_button_config("main_button", config)
# Initialize a DHT22 sensor using configuration
dht_sensor = DHT22Sensor(
name="Living Room",
pin=4, # GPIO pin for DHT22 data
interval=5, # Read every 5 seconds
unit="C" # Celsius
config=config # Pass the loaded config
)
# Initialize an OLED display
# Initialize an OLED display using configuration
display = OLEDDisplay(
name="Status Display",
scl_pin=22, # GPIO pin for I2C clock
sda_pin=21, # GPIO pin for I2C data
width=128, # Display width in pixels
height=64, # Display height in pixels
interval=1 # Update every second
config=config # Pass the loaded config
)
# Set up button on GPIO pin 0 (common button pin on many ESP boards)
button_pin = 0
# Set up button using configuration
button_pin = button_config.get("pin", 0)
if not SIMULATION:
button = Pin(button_pin, Pin.IN, Pin.PULL_UP)
pull_up = button_config.get("pull_up", True)
button = Pin(button_pin, Pin.IN, Pin.PULL_UP if pull_up else None)
# Display initialization message
display.clear()
@@ -130,4 +128,4 @@ def main():
if __name__ == "__main__":
main()
main()