let junie create initial structure

This commit is contained in:
OMGeeky
2025-05-07 19:17:19 +02:00
parent c4c15db82e
commit 5d15df6bde
7 changed files with 373 additions and 0 deletions

152
.junie/guidelines.md Normal file
View File

@@ -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.

22
pyproject.toml Normal file
View File

@@ -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_*"

8
requirements.txt Normal file
View File

@@ -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

View File

@@ -0,0 +1,3 @@
"""
ESP Sensors package for home control and automation.
"""

48
src/esp_sensors/sensor.py Normal file
View File

@@ -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,
}

View File

@@ -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

View File

@@ -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