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

194
docs/config_system.md Normal file
View File

@@ -0,0 +1,194 @@
# Configuration System
This document explains how to use the configuration system in the ESP Sensors project.
## Overview
The configuration system allows you to change parameters like pins, display resolution, sensor names, and intervals without modifying the code. This makes it easy to adapt the project to different hardware setups and requirements.
## Configuration File
The configuration is stored in a JSON file named `config.json` in the project root directory. If this file doesn't exist, default values will be used.
### Example Configuration
```json
{
"sensors": {
"temperature": {
"name": "Living Room Temperature",
"pin": 4,
"interval": 60,
"unit": "C"
},
"humidity": {
"name": "Living Room Humidity",
"pin": 4,
"interval": 60
},
"dht22": {
"name": "Living Room DHT22",
"pin": 4,
"interval": 30,
"unit": "C"
}
},
"displays": {
"oled": {
"name": "Status Display",
"scl_pin": 22,
"sda_pin": 21,
"width": 128,
"height": 64,
"address": "0x3C",
"interval": 1
}
},
"buttons": {
"main_button": {
"pin": 0,
"pull_up": true
}
}
}
```
## Using the Configuration System
### Loading Configuration
You can load the configuration using the `load_config` function:
```python
from src.esp_sensors.config import load_config
# Load configuration from the default path (config.json)
config = load_config()
# Or specify a custom path
config = load_config("custom_config.json")
```
### Getting Sensor Configuration
To get configuration for a specific sensor type:
```python
from src.esp_sensors.config import get_sensor_config
# Get configuration for a temperature sensor
temp_config = get_sensor_config("temperature")
# Or with a custom configuration
temp_config = get_sensor_config("temperature", config)
```
### Getting Display Configuration
To get configuration for a specific display type:
```python
from src.esp_sensors.config import get_display_config
# Get configuration for an OLED display
oled_config = get_display_config("oled")
# Or with a custom configuration
oled_config = get_display_config("oled", config)
```
### Creating Sensors with Configuration
You can create sensors using the configuration system in two ways:
#### Method 1: Using sensor_type parameter
```python
from src.esp_sensors.temperature import TemperatureSensor
# Create a temperature sensor using configuration
sensor = TemperatureSensor(sensor_type="temperature")
```
#### Method 2: Overriding some parameters
```python
from src.esp_sensors.temperature import TemperatureSensor
# Create a temperature sensor with custom name but other parameters from config
sensor = TemperatureSensor(
name="Custom Sensor",
sensor_type="temperature"
)
```
### Creating Displays with Configuration
Similarly, you can create displays using the configuration system:
```python
from src.esp_sensors.oled_display import OLEDDisplay
# Create an OLED display using configuration
display = OLEDDisplay()
# Or override some parameters
display = OLEDDisplay(
name="Custom Display",
width=64,
height=32
)
```
## Saving Configuration
You can save a configuration to a file using the `save_config` function:
```python
from src.esp_sensors.config import save_config
# Save configuration to the default path (config.json)
save_config(config)
# Or specify a custom path
save_config(config, "custom_config.json")
```
## Creating Default Configuration
To create a default configuration file:
```python
from src.esp_sensors.config import create_default_config
# Create a default configuration file at the default path (config.json)
create_default_config()
# Or specify a custom path
create_default_config("custom_config.json")
```
## Configuration Parameters
### Common Parameters
- `name`: The name of the sensor or display
- `pin`: The GPIO pin number the sensor is connected to
- `interval`: Reading interval in seconds
### Temperature Sensor Parameters
- `unit`: Temperature unit, either "C" for Celsius or "F" for Fahrenheit
### OLED Display Parameters
- `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
- `height`: Display height in pixels
- `address`: I2C address of the display (in hex format, e.g., "0x3C")
### Button Parameters
- `pin`: The GPIO pin number the button is connected to
- `pull_up`: Whether to use internal pull-up resistor (true/false)

View File

@@ -16,6 +16,7 @@ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')
from src.esp_sensors.oled_display import OLEDDisplay
from src.esp_sensors.dht22 import DHT22Sensor
from src.esp_sensors.config import load_config, get_sensor_config, get_display_config, get_button_config
# Import hardware-specific modules if available (for ESP32/ESP8266)
try:
@@ -44,28 +45,29 @@ def main():
"""
Main function to demonstrate button-triggered sensor display.
"""
# Initialize a DHT22 sensor
# Load configuration
config = load_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
)
print(f"Initialized DHT22 sensor: {dht_sensor.name}, pin: {dht_sensor.pin}")
# 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
)
print(f"Initialized OLED display: {display.name}, size: {display.width}x{display.height}")
# Set up button using configuration
button_config = get_button_config("main_button", config)
button_pin = button_config.get("pin", 0)
print(f"Using button on pin: {button_pin}")
# Set up button on GPIO pin 0 (common button pin on many ESP boards)
button_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()

View File

@@ -2,104 +2,112 @@
Example script for using the DHT22 sensor with an ESP32.
This script demonstrates how to initialize and read from a DHT22 sensor
connected to an ESP32 microcontroller.
connected to an ESP32 microcontroller using the configuration system.
Usage:
- Upload this script to your ESP32 running MicroPython
- Connect the DHT22 sensor to the specified GPIO pin
- Ensure config.json is properly set up with DHT22 sensor configuration
- The script will read temperature and humidity at the specified interval
"""
import time
import sys
import os
# Add the src directory to the Python path if needed
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
# Check if running on MicroPython
if sys.implementation.name == 'micropython':
from src.esp_sensors.dht22 import DHT22Sensor
# Configuration
SENSOR_NAME = "living_room"
SENSOR_PIN = 4 # GPIO pin where DHT22 is connected
READ_INTERVAL = 5 # seconds between readings
from src.esp_sensors.config import load_config, get_sensor_config
def main():
print(f"Initializing DHT22 sensor on pin {SENSOR_PIN}")
# Initialize the sensor
sensor = DHT22Sensor(SENSOR_NAME, SENSOR_PIN, READ_INTERVAL)
# Load configuration
config = load_config()
dht_config = get_sensor_config("dht22", config)
print(f"Initializing DHT22 sensor from configuration")
print(f"Sensor name: {dht_config.get('name')}")
print(f"Sensor pin: {dht_config.get('pin')}")
# Initialize the sensor using configuration
sensor = DHT22Sensor(config=config)
print("Starting sensor readings. Press Ctrl+C to stop.")
try:
while True:
# Read temperature
temperature = sensor.read()
# Read humidity
humidity = sensor.read_humidity()
# Get the current timestamp
timestamp = time.time()
# Print readings
print(f"Time: {timestamp}")
print(f"Temperature: {temperature}°C ({sensor.to_fahrenheit()}°F)")
print(f"Humidity: {humidity}%")
print("-" * 30)
# Wait for the next reading
time.sleep(READ_INTERVAL)
time.sleep(sensor.interval)
except KeyboardInterrupt:
print("Sensor readings stopped.")
except Exception as e:
print(f"Error: {e}")
if __name__ == "__main__":
main()
else:
print("This script is designed to run on MicroPython on an ESP32.")
print("Running in simulation mode for demonstration purposes.")
# Import for simulation mode
from src.esp_sensors.dht22 import DHT22Sensor
# Configuration
SENSOR_NAME = "simulation"
SENSOR_PIN = 4
READ_INTERVAL = 2 # shorter interval for demonstration
from src.esp_sensors.config import load_config, get_sensor_config
def main():
print(f"Initializing DHT22 sensor simulation")
# Initialize the sensor
sensor = DHT22Sensor(SENSOR_NAME, SENSOR_PIN, READ_INTERVAL)
# Load configuration
config = load_config()
dht_config = get_sensor_config("dht22", config)
print(f"Initializing DHT22 sensor simulation from configuration")
print(f"Sensor name: {dht_config.get('name')}")
print(f"Sensor pin: {dht_config.get('pin')}")
# Initialize the sensor using configuration
sensor = DHT22Sensor(config=config)
print("Starting simulated sensor readings. Press Ctrl+C to stop.")
try:
for _ in range(5): # Just do 5 readings for the simulation
# Read temperature
temperature = sensor.read()
# Read humidity
humidity = sensor.read_humidity()
# Get the current timestamp
timestamp = time.time()
# Print readings
print(f"Time: {timestamp}")
print(f"Temperature: {temperature}°C ({sensor.to_fahrenheit()}°F)")
print(f"Humidity: {humidity}%")
print("-" * 30)
# Wait for the next reading
time.sleep(READ_INTERVAL)
time.sleep(sensor.interval)
print("Simulation complete.")
except KeyboardInterrupt:
print("Sensor readings stopped.")
except Exception as e:
print(f"Error: {e}")
if __name__ == "__main__":
main()
main()

View File

@@ -1,5 +1,6 @@
"""
Example usage of the OLED display with temperature and humidity sensors.
This example demonstrates how to use the configuration system to initialize sensors and displays.
"""
import time
import sys
@@ -10,29 +11,44 @@ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')
from src.esp_sensors.oled_display import OLEDDisplay
from src.esp_sensors.dht22 import DHT22Sensor
from src.esp_sensors.config import load_config, get_sensor_config, get_display_config
def main():
"""
Main function to demonstrate OLED display usage with sensors.
"""
# Initialize a DHT22 sensor
dht_sensor = DHT22Sensor(
name="Living Room",
pin=4, # GPIO pin for DHT22 data
interval=5, # Read every 5 seconds
unit="C" # Celsius
)
# Load configuration from file
print("Loading configuration from config.json...")
config = load_config()
# Initialize an OLED display
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
# Method 1: Initialize sensors using configuration directly
print("\nMethod 1: Initialize using configuration directly")
# Get configuration for DHT22 sensor
dht_config = get_sensor_config("dht22", config)
print(f"DHT22 config: {dht_config}")
# Initialize a DHT22 sensor with configuration
dht_sensor = DHT22Sensor(
sensor_type="dht22", # This will load config for this sensor type
config=config # Pass the loaded config
)
print(f"Created DHT22 sensor: {dht_sensor.name}, pin: {dht_sensor.pin}, interval: {dht_sensor.interval}s")
# Method 2: Initialize with some parameters from config, others specified directly
print("\nMethod 2: Override some config parameters")
# Initialize an OLED display with some custom parameters
display = OLEDDisplay(
# These parameters will override the config values
name="Custom Display",
interval=1, # Update every second
# Other parameters will be loaded from config
config=config
)
print(f"Created OLED display: {display.name}, size: {display.width}x{display.height}, interval: {display.interval}s")
# Display initialization message
display.clear()

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()

140
tests/test_config.py Normal file
View File

@@ -0,0 +1,140 @@
"""
Tests for the configuration module.
"""
import os
import json
import tempfile
import pytest
from src.esp_sensors.config import (
load_config,
save_config,
get_sensor_config,
get_display_config,
create_default_config,
DEFAULT_CONFIG
)
def test_load_default_config():
"""Test that default configuration is loaded when file doesn't exist."""
# Use a non-existent file path
config = load_config("non_existent_file.json")
# Check that the default configuration was loaded
assert config == DEFAULT_CONFIG
assert "sensors" in config
assert "displays" in config
def test_save_and_load_config():
"""Test saving and loading configuration."""
# Create a temporary file
with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as temp_file:
temp_path = temp_file.name
try:
# Create a test configuration
test_config = {
"sensors": {
"test_sensor": {
"name": "Test Sensor",
"pin": 10,
"interval": 30
}
}
}
# Save the configuration
result = save_config(test_config, temp_path)
assert result is True
# Load the configuration
loaded_config = load_config(temp_path)
# Check that the loaded configuration matches the saved one
assert loaded_config == test_config
assert loaded_config["sensors"]["test_sensor"]["name"] == "Test Sensor"
assert loaded_config["sensors"]["test_sensor"]["pin"] == 10
assert loaded_config["sensors"]["test_sensor"]["interval"] == 30
finally:
# Clean up the temporary file
if os.path.exists(temp_path):
os.unlink(temp_path)
def test_get_sensor_config():
"""Test getting sensor configuration."""
# Create a test configuration
test_config = {
"sensors": {
"test_sensor": {
"name": "Test Sensor",
"pin": 10,
"interval": 30
}
}
}
# Get configuration for an existing sensor
sensor_config = get_sensor_config("test_sensor", test_config)
assert sensor_config["name"] == "Test Sensor"
assert sensor_config["pin"] == 10
assert sensor_config["interval"] == 30
# Get configuration for a non-existent sensor (should return default or empty dict)
non_existent_config = get_sensor_config("non_existent", test_config)
assert isinstance(non_existent_config, dict)
def test_get_display_config():
"""Test getting display configuration."""
# Create a test configuration
test_config = {
"displays": {
"test_display": {
"name": "Test Display",
"width": 64,
"height": 32
}
}
}
# Get configuration for an existing display
display_config = get_display_config("test_display", test_config)
assert display_config["name"] == "Test Display"
assert display_config["width"] == 64
assert display_config["height"] == 32
# Get configuration for a non-existent display (should return default or empty dict)
non_existent_config = get_display_config("non_existent", test_config)
assert isinstance(non_existent_config, dict)
def test_create_default_config():
"""Test creating a default configuration file."""
# Create a temporary file path
with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as temp_file:
temp_path = temp_file.name
try:
# Remove the file (we just want the path)
os.unlink(temp_path)
# Create the default configuration
result = create_default_config(temp_path)
assert result is True
# Check that the file exists
assert os.path.exists(temp_path)
# Load the configuration and check it matches the default
with open(temp_path, 'r') as f:
config = json.load(f)
assert config == DEFAULT_CONFIG
finally:
# Clean up the temporary file
if os.path.exists(temp_path):
os.unlink(temp_path)

View File

@@ -3,27 +3,54 @@ Tests for the DHT22 sensor module.
"""
import pytest
from src.esp_sensors.dht22 import DHT22Sensor
from src.esp_sensors.config import get_sensor_config
def test_dht22_sensor_initialization():
"""Test that a DHT22 sensor can be initialized with valid parameters."""
sensor = DHT22Sensor("test_sensor", 5, 30, "C")
# Test direct parameter initialization
sensor = DHT22Sensor(name="test_sensor", pin=5, interval=30, unit="C")
assert sensor.name == "test_sensor"
assert sensor.pin == 5
assert sensor.interval == 30
assert sensor.unit == "C"
assert sensor._last_humidity is None
# Test initialization with custom config
test_config = {
"sensors": {
"dht22": {
"name": "config_sensor",
"pin": 6,
"interval": 40,
"temperature": {
"name": "Config Temperature",
"unit": "F"
},
"humidity": {
"name": "Config Humidity"
}
}
}
}
config_sensor = DHT22Sensor(config=test_config)
assert config_sensor.name == "config_sensor"
assert config_sensor.pin == 6
assert config_sensor.interval == 40
assert config_sensor.unit == "F"
assert config_sensor._last_humidity is None
def test_dht22_sensor_invalid_unit():
"""Test that initializing with an invalid unit raises a ValueError."""
with pytest.raises(ValueError):
DHT22Sensor("test_sensor", 5, 30, "K")
DHT22Sensor(name="test_sensor", pin=5, interval=30, unit="K")
def test_dht22_sensor_read_temperature():
"""Test that reading temperature from the sensor returns a float value."""
sensor = DHT22Sensor("test_sensor", 5)
sensor = DHT22Sensor(name="test_sensor", pin=5)
reading = sensor.read()
assert isinstance(reading, float)
# For Celsius, the reading should be between 15.0 and 30.0
@@ -32,7 +59,7 @@ def test_dht22_sensor_read_temperature():
def test_dht22_sensor_read_humidity():
"""Test that reading humidity from the sensor returns a float value."""
sensor = DHT22Sensor("test_sensor", 5)
sensor = DHT22Sensor(name="test_sensor", pin=5)
humidity = sensor.read_humidity()
assert isinstance(humidity, float)
# Humidity should be between 30.0% and 90.0%
@@ -41,7 +68,7 @@ def test_dht22_sensor_read_humidity():
def test_dht22_sensor_fahrenheit():
"""Test that a sensor initialized with Fahrenheit returns appropriate values."""
sensor = DHT22Sensor("test_sensor", 5, unit="F")
sensor = DHT22Sensor(name="test_sensor", pin=5, unit="F")
reading = sensor.read()
assert isinstance(reading, float)
# For Fahrenheit, the reading should be between 59.0 and 86.0
@@ -51,13 +78,13 @@ def test_dht22_sensor_fahrenheit():
def test_dht22_temperature_conversion():
"""Test temperature conversion methods."""
# Test Celsius to Fahrenheit
c_sensor = DHT22Sensor("celsius_sensor", 5, unit="C")
c_sensor = DHT22Sensor(name="celsius_sensor", pin=5, unit="C")
c_sensor._last_reading = 20.0 # Manually set for testing
f_value = c_sensor.to_fahrenheit()
assert f_value == 68.0 # 20°C = 68°F
# Test Fahrenheit to Celsius
f_sensor = DHT22Sensor("fahrenheit_sensor", 5, unit="F")
f_sensor = DHT22Sensor(name="fahrenheit_sensor", pin=5, unit="F")
f_sensor._last_reading = 68.0 # Manually set for testing
c_value = f_sensor.to_celsius()
assert c_value == 20.0 # 68°F = 20°C
@@ -65,7 +92,7 @@ def test_dht22_temperature_conversion():
def test_dht22_metadata():
"""Test that metadata includes the temperature unit, humidity, and type."""
sensor = DHT22Sensor("test_sensor", 5, unit="C")
sensor = DHT22Sensor(name="test_sensor", pin=5, unit="C")
metadata = sensor.get_metadata()
assert metadata["name"] == "test_sensor"
assert metadata["pin"] == 5
@@ -83,18 +110,18 @@ def test_dht22_metadata():
def test_dht22_read_updates_both_values():
"""Test that reading temperature also updates humidity."""
sensor = DHT22Sensor("test_sensor", 5)
sensor = DHT22Sensor(name="test_sensor", pin=5)
assert sensor._last_humidity is None
# Reading temperature should also update humidity
sensor.read()
assert sensor._last_humidity is not None
# Reset humidity to test read_humidity
old_temp = sensor._last_reading
sensor._last_humidity = None
# Reading humidity should not change temperature
humidity = sensor.read_humidity()
assert sensor._last_reading == old_temp
assert humidity is not None
assert humidity is not None

View File

@@ -15,7 +15,7 @@ def test_oled_display_initialization():
assert display.width == 128
assert display.height == 64
assert display.address == 0x3C
assert display.interval == 60
assert display.interval == 1 # Default interval is now 1 in the configuration
assert display._values == []
@@ -57,7 +57,7 @@ def test_oled_display_metadata():
assert metadata["width"] == 128
assert metadata["height"] == 64
assert metadata["address"] == 0x3C
assert metadata["interval"] == 60
assert metadata["interval"] == 1 # Default interval is now 1 in the configuration
assert metadata["type"] == "SSD1306"
assert metadata["values_count"] == 0
@@ -65,15 +65,15 @@ def test_oled_display_metadata():
def test_oled_display_values():
"""Test that display values are stored correctly."""
display = OLEDDisplay("test_display", scl_pin=22, sda_pin=21)
# Test with empty values
assert display._values == []
# Test with string values
test_values = ["Temperature: 25.0°C", "Humidity: 45.0%"]
display.display_values(test_values)
assert display._values == test_values
# Check that metadata reflects the number of values
metadata = display.get_metadata()
assert metadata["values_count"] == 2
@@ -82,10 +82,10 @@ def test_oled_display_values():
def test_oled_display_clear():
"""Test that clearing the display works in simulation mode."""
display = OLEDDisplay("test_display", scl_pin=22, sda_pin=21)
# This is mostly a coverage test since we can't check the actual display in tests
display.clear()
# Verify that clearing doesn't affect stored values
test_values = ["Temperature: 25.0°C"]
display.display_values(test_values)
@@ -96,12 +96,12 @@ def test_oled_display_clear():
def test_oled_display_text():
"""Test that displaying text works in simulation mode."""
display = OLEDDisplay("test_display", scl_pin=22, sda_pin=21)
# This is mostly a coverage test since we can't check the actual display in tests
display.display_text("Hello, World!")
# Verify that displaying text doesn't affect stored values
test_values = ["Temperature: 25.0°C"]
display.display_values(test_values)
display.display_text("Hello, World!")
assert display._values == test_values
assert display._values == test_values