diff --git a/docs/config_system.md b/docs/config_system.md new file mode 100644 index 0000000..9f09eca --- /dev/null +++ b/docs/config_system.md @@ -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) \ No newline at end of file diff --git a/examples/button_triggered_display.py b/examples/button_triggered_display.py index c450c31..d1fa62f 100644 --- a/examples/button_triggered_display.py +++ b/examples/button_triggered_display.py @@ -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() diff --git a/examples/dht22_example.py b/examples/dht22_example.py index 3595fe0..cc54d78 100644 --- a/examples/dht22_example.py +++ b/examples/dht22_example.py @@ -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() \ No newline at end of file + main() diff --git a/examples/oled_display_example.py b/examples/oled_display_example.py index 3557fad..7510747 100644 --- a/examples/oled_display_example.py +++ b/examples/oled_display_example.py @@ -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() diff --git a/src/esp_sensors/config.py b/src/esp_sensors/config.py new file mode 100644 index 0000000..e50fb35 --- /dev/null +++ b/src/esp_sensors/config.py @@ -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) diff --git a/src/esp_sensors/dht22.py b/src/esp_sensors/dht22.py index 5e0d038..13aefe6 100644 --- a/src/esp_sensors/dht22.py +++ b/src/esp_sensors/dht22.py @@ -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 diff --git a/src/esp_sensors/humidity.py b/src/esp_sensors/humidity.py index 4d67874..a032166 100644 --- a/src/esp_sensors/humidity.py +++ b/src/esp_sensors/humidity.py @@ -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 \ No newline at end of file + return metadata diff --git a/src/esp_sensors/oled_display.py b/src/esp_sensors/oled_display.py index 31bcd22..21205c3 100644 --- a/src/esp_sensors/oled_display.py +++ b/src/esp_sensors/oled_display.py @@ -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 \ No newline at end of file + return metadata diff --git a/src/esp_sensors/sensor.py b/src/esp_sensors/sensor.py index 4265f1d..ee28be4 100644 --- a/src/esp_sensors/sensor.py +++ b/src/esp_sensors/sensor.py @@ -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: diff --git a/src/esp_sensors/temperature.py b/src/esp_sensors/temperature.py index 7152798..df405fe 100644 --- a/src/esp_sensors/temperature.py +++ b/src/esp_sensors/temperature.py @@ -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: diff --git a/src/main.py b/src/main.py index 56341fa..d606ad0 100644 --- a/src/main.py +++ b/src/main.py @@ -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() \ No newline at end of file + main() diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..26f5310 --- /dev/null +++ b/tests/test_config.py @@ -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) \ No newline at end of file diff --git a/tests/test_dht22_sensor.py b/tests/test_dht22_sensor.py index e2ed0ea..0058a29 100644 --- a/tests/test_dht22_sensor.py +++ b/tests/test_dht22_sensor.py @@ -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 \ No newline at end of file + assert humidity is not None diff --git a/tests/test_oled_display.py b/tests/test_oled_display.py index b3ce197..16b766b 100644 --- a/tests/test_oled_display.py +++ b/tests/test_oled_display.py @@ -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 \ No newline at end of file + assert display._values == test_values