initial, step 2 already

This commit is contained in:
2025-10-31 14:25:12 +01:00
commit ea17d048ad
24 changed files with 1431 additions and 0 deletions

103
apps/rules/README.md Normal file
View File

@@ -0,0 +1,103 @@
# Rules Engine
APScheduler-based automation rules engine for the home automation system.
## Features
- **APScheduler**: Background job scheduler for rule execution
- **Interval Jobs**: Periodic rule evaluation
- **Graceful Shutdown**: Proper signal handling (SIGINT, SIGTERM)
- **Logging**: Structured logging at INFO level
## Running
```bash
poetry run python -m apps.rules.main
```
## Architecture
The rules engine uses APScheduler's `BackgroundScheduler` to run automation rules on a schedule.
### Current Jobs
- **rule_tick**: Example job that runs every minute
- Logs "Rule tick" message
- Can be extended with actual rule logic
## Example Output
```
2025-10-31 13:05:46,865 - __main__ - INFO - Rules engine starting...
2025-10-31 13:05:46,868 - __main__ - INFO - Scheduler started with rule_tick job (every 1 minute)
2025-10-31 13:05:46,868 - __main__ - INFO - Rule tick
2025-10-31 13:06:46,874 - __main__ - INFO - Rule tick
2025-10-31 13:07:46,874 - __main__ - INFO - Rule tick
```
## Signal Handling
The application handles shutdown signals gracefully:
- **SIGINT** (Ctrl+C): Initiates graceful shutdown
- **SIGTERM**: Initiates graceful shutdown
On shutdown:
1. Stops accepting new jobs
2. Waits for running jobs to complete
3. Shuts down the scheduler
4. Exits cleanly
## Adding New Rules
To add a new rule, define a function and schedule it:
```python
def my_custom_rule() -> None:
"""Custom automation rule."""
# Your rule logic here
logger.info("Custom rule executed")
# In main():
scheduler.add_job(
my_custom_rule,
'interval',
minutes=5, # Run every 5 minutes
id='custom_rule',
name='My Custom Rule'
)
```
## Scheduler Triggers
APScheduler supports various trigger types:
- **interval**: Run at fixed intervals (e.g., every N minutes)
- **cron**: Run at specific times (e.g., daily at 8:00 AM)
- **date**: Run once at a specific datetime
Example with cron trigger:
```python
scheduler.add_job(
morning_routine,
'cron',
hour=8,
minute=0,
id='morning',
name='Morning Routine'
)
```
## Dependencies
- **APScheduler**: Advanced job scheduling
## Future Enhancements
- [ ] Load rules from configuration file
- [ ] MQTT integration for device state monitoring
- [ ] Rule conditions (if/then logic)
- [ ] Rule chaining and dependencies
- [ ] Web API for dynamic rule management
- [ ] Persistent job store (database)

1
apps/rules/__init__.py Normal file
View File

@@ -0,0 +1 @@
"""Rules application."""

84
apps/rules/main.py Normal file
View File

@@ -0,0 +1,84 @@
"""Rules main entry point."""
import logging
import signal
import sys
import time
from typing import NoReturn
from apscheduler.schedulers.background import BackgroundScheduler
# Configure logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)
# Global scheduler instance
scheduler: BackgroundScheduler | None = None
def rule_tick() -> None:
"""Example job that runs every minute.
This is a placeholder for actual rule evaluation logic.
"""
logger.info("Rule tick")
def shutdown_handler(signum: int, frame: object) -> NoReturn:
"""Handle shutdown signals gracefully.
Args:
signum: Signal number
frame: Current stack frame
"""
logger.info(f"Received signal {signum}, shutting down...")
if scheduler:
scheduler.shutdown(wait=True)
logger.info("Scheduler stopped")
sys.exit(0)
def main() -> None:
"""Run the rules application."""
global scheduler
logger.info("Rules engine starting...")
# Register signal handlers
signal.signal(signal.SIGINT, shutdown_handler)
signal.signal(signal.SIGTERM, shutdown_handler)
# Initialize scheduler
scheduler = BackgroundScheduler()
# Add example job - runs every minute
scheduler.add_job(
rule_tick,
'interval',
minutes=1,
id='rule_tick',
name='Rule Tick Job'
)
# Start scheduler
scheduler.start()
logger.info("Scheduler started with rule_tick job (every 1 minute)")
# Run initial tick immediately
rule_tick()
# Keep the application running
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
logger.info("KeyboardInterrupt received, shutting down...")
scheduler.shutdown(wait=True)
logger.info("Scheduler stopped")
if __name__ == "__main__":
main()