diff --git a/.junie/guidelines.md b/.junie/guidelines.md new file mode 100644 index 0000000..71200c4 --- /dev/null +++ b/.junie/guidelines.md @@ -0,0 +1,152 @@ +# ESP Sensors Project Guidelines + +This document provides guidelines and instructions for developing and maintaining the ESP Sensors project. + +## Build and Configuration Instructions + +### Environment Setup + +1. **Python Version**: This project uses Python 3.12. Ensure you have this version installed. + +2. **Virtual Environment**: Always use a virtual environment for development: + ```bash + python -m venv .venv + source .venv/bin/activate # On Windows: .venv\Scripts\activate + ``` + +3. **Dependencies**: Install the required dependencies: + ```bash + pip install -r requirements.txt + ``` + +### Project Structure + +The project follows this structure: +``` +esp-sensors/ +├── src/ +│ └── esp_sensors/ # Main package +│ ├── __init__.py +│ ├── sensor.py # Base sensor class +│ └── temperature.py # Temperature sensor implementation +├── tests/ # Test directory +├── .junie/ # Project documentation +├── pyproject.toml # Project configuration +└── requirements.txt # Dependencies +``` + +## Testing Information + +### Running Tests + +1. **Basic Test Run**: + ```bash + python -m pytest + ``` + +2. **Verbose Output**: + ```bash + python -m pytest -v + ``` + +3. **With Coverage**: + ```bash + python -m pytest --cov=src.esp_sensors + ``` + +4. **Generate Coverage Report**: + ```bash + python -m pytest --cov=src.esp_sensors --cov-report=html + ``` + This will create a `htmlcov` directory with an HTML coverage report. + +### Adding New Tests + +1. Create test files in the `tests` directory with the naming pattern `test_*.py`. +2. Test functions should be named with the prefix `test_`. +3. Use pytest fixtures for common setup and teardown operations. + +### Example Test + +Here's a simple example of a test for a temperature sensor: + +```python +import pytest +from src.esp_sensors.temperature import TemperatureSensor + +def test_temperature_sensor_initialization(): + """Test that a temperature sensor can be initialized with valid parameters.""" + sensor = TemperatureSensor("test_sensor", 5, 30, "C") + assert sensor.name == "test_sensor" + assert sensor.pin == 5 + assert sensor.interval == 30 + assert sensor.unit == "C" +``` + +## Code Style and Development Guidelines + +### Code Formatting + +This project uses [Black](https://black.readthedocs.io/) for code formatting: + +```bash +# Check if files need formatting +black --check . + +# Format files +black . +``` + +### Type Hints + +Always use type hints in function signatures and variable declarations: + +```python +from typing import Dict, Any, Optional + +def process_reading(value: float, metadata: Dict[str, Any]) -> Optional[float]: + # Function implementation + pass +``` + +### Documentation + +- Use docstrings for all modules, classes, and functions. +- Follow the Google docstring style. +- Include examples in docstrings where appropriate. + +Example: +```python +def read(self) -> float: + """ + Read the current sensor value. + + Returns: + The sensor reading as a float + """ + # Implementation +``` + +### Error Handling + +- Use specific exception types rather than generic exceptions. +- Handle exceptions at the appropriate level. +- Log exceptions with context information. + +### Development Workflow + +1. Create a new branch for each feature or bug fix. +2. Write tests before implementing features (Test-Driven Development). +3. Ensure all tests pass before submitting changes. +4. Format code with Black before committing. +5. Update documentation as needed. + +## ESP-Specific Development Notes + +When developing for actual ESP hardware: + +1. This project is designed to work with MicroPython on ESP32/ESP8266 devices. +2. For hardware testing, you'll need to flash MicroPython to your device. +3. Use tools like `ampy` or `rshell` to upload code to the device. +4. Consider memory constraints when developing for ESP devices. +5. For production, optimize code to reduce memory usage and power consumption. \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..a5a1b2a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,22 @@ +[tool.black] +line-length = 88 +target-version = ['py312'] +include = '\.pyi?$' +exclude = ''' +/( + \.git + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | _build + | buck-out + | build + | dist +)/ +''' + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = "test_*.py" +python_functions = "test_*" \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..bd1c775 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,8 @@ +# Core dependencies +pytest==7.4.0 +pytest-cov==4.1.0 +black==24.3.0 + +# ESP-specific dependencies (these would be used in a real implementation) +# micropython-esp32==1.19.1 +# micropython-umqtt.simple==1.3.4 \ No newline at end of file diff --git a/src/esp_sensors/__init__.py b/src/esp_sensors/__init__.py new file mode 100644 index 0000000..f311599 --- /dev/null +++ b/src/esp_sensors/__init__.py @@ -0,0 +1,3 @@ +""" +ESP Sensors package for home control and automation. +""" diff --git a/src/esp_sensors/sensor.py b/src/esp_sensors/sensor.py new file mode 100644 index 0000000..4265f1d --- /dev/null +++ b/src/esp_sensors/sensor.py @@ -0,0 +1,48 @@ +""" +Base sensor module for ESP-based sensors. +""" +from typing import Dict, Any, Optional + + +class Sensor: + """Base class for all sensors.""" + + def __init__(self, name: str, pin: int, interval: int = 60): + """ + 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) + """ + self.name = name + self.pin = pin + self.interval = interval + self._last_reading: Optional[float] = None + + def read(self) -> float: + """ + Read the current sensor value. + + Returns: + The sensor reading as a float + """ + # This is a placeholder that would be overridden by subclasses + # In a real implementation, this would interact with the hardware + self._last_reading = 0.0 + return self._last_reading + + def get_metadata(self) -> Dict[str, Any]: + """ + Get sensor metadata. + + Returns: + A dictionary containing sensor metadata + """ + return { + "name": self.name, + "pin": self.pin, + "interval": self.interval, + "last_reading": self._last_reading, + } diff --git a/src/esp_sensors/temperature.py b/src/esp_sensors/temperature.py new file mode 100644 index 0000000..2b3e208 --- /dev/null +++ b/src/esp_sensors/temperature.py @@ -0,0 +1,72 @@ +""" +Temperature sensor module for ESP-based sensors. +""" +import random +from .sensor import Sensor + + +class TemperatureSensor(Sensor): + """Temperature sensor implementation.""" + + def __init__(self, name: str, pin: int, interval: int = 60, unit: str = "C"): + """ + 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") + """ + super().__init__(name, pin, interval) + if unit not in ["C", "F"]: + raise ValueError("Unit must be either 'C' or 'F'") + self.unit = unit + + def read(self) -> float: + """ + Read the current temperature. + + Returns: + The temperature reading as a float + """ + # This is a simulation for testing purposes + # In a real implementation, this would read from the actual sensor + if self.unit == "C": + self._last_reading = round(random.uniform(15.0, 30.0), 1) + else: + self._last_reading = round(random.uniform(59.0, 86.0), 1) + return self._last_reading + + def get_metadata(self): + """ + Get sensor metadata including temperature unit. + + Returns: + A dictionary containing sensor metadata + """ + metadata = super().get_metadata() + metadata["unit"] = self.unit + return metadata + + def to_fahrenheit(self) -> float | None: + """ + Convert the last reading to Fahrenheit if it was in Celsius. + + Returns: + The temperature in Fahrenheit + """ + if self.unit == "F" or self._last_reading is None: + return self._last_reading + return (self._last_reading * 9 / 5) + 32 + + def to_celsius(self) -> float | None: + """ + Convert the last reading to Celsius if it was in Fahrenheit. + + Returns: + The temperature in Celsius + """ + if self.unit == "C" or self._last_reading is None: + return self._last_reading + return (self._last_reading - 32) * 5 / 9 diff --git a/tests/test_temperature_sensor.py b/tests/test_temperature_sensor.py new file mode 100644 index 0000000..5cbf74b --- /dev/null +++ b/tests/test_temperature_sensor.py @@ -0,0 +1,68 @@ +""" +Tests for the temperature sensor module. +""" +import pytest +from src.esp_sensors.temperature import TemperatureSensor + + +def test_temperature_sensor_initialization(): + """Test that a temperature sensor can be initialized with valid parameters.""" + sensor = TemperatureSensor("test_sensor", 5, 30, "C") + assert sensor.name == "test_sensor" + assert sensor.pin == 5 + assert sensor.interval == 30 + assert sensor.unit == "C" + + +def test_temperature_sensor_invalid_unit(): + """Test that initializing with an invalid unit raises a ValueError.""" + with pytest.raises(ValueError): + TemperatureSensor("test_sensor", 5, 30, "K") + + +def test_temperature_sensor_read(): + """Test that reading from the sensor returns a float value.""" + sensor = TemperatureSensor("test_sensor", 5) + reading = sensor.read() + assert isinstance(reading, float) + # For Celsius, the reading should be between 15.0 and 30.0 + assert 15.0 <= reading <= 30.0 + + +def test_temperature_sensor_fahrenheit(): + """Test that a sensor initialized with Fahrenheit returns appropriate values.""" + sensor = TemperatureSensor("test_sensor", 5, unit="F") + reading = sensor.read() + assert isinstance(reading, float) + # For Fahrenheit, the reading should be between 59.0 and 86.0 + assert 59.0 <= reading <= 86.0 + + +def test_temperature_conversion(): + """Test temperature conversion methods.""" + # Test Celsius to Fahrenheit + c_sensor = TemperatureSensor("celsius_sensor", 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 = TemperatureSensor("fahrenheit_sensor", 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 + + +def test_metadata(): + """Test that metadata includes the temperature unit.""" + sensor = TemperatureSensor("test_sensor", 5, unit="C") + metadata = sensor.get_metadata() + assert metadata["name"] == "test_sensor" + assert metadata["pin"] == 5 + assert metadata["unit"] == "C" + assert metadata["last_reading"] is None # No reading yet + + # After a reading + sensor.read() + metadata = sensor.get_metadata() + assert metadata["last_reading"] is not None