mirror of
https://github.com/OMGeeky/homecontrol.esp-sensors.git
synced 2026-02-23 15:49:52 +01:00
add configuration system for ESP Sensors project
This commit is contained in:
185
src/esp_sensors/config.py
Normal file
185
src/esp_sensors/config.py
Normal 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)
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
30
src/main.py
30
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()
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user