initial, step 2 already
This commit is contained in:
103
apps/rules/README.md
Normal file
103
apps/rules/README.md
Normal 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
1
apps/rules/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Rules application."""
|
||||
84
apps/rules/main.py
Normal file
84
apps/rules/main.py
Normal 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()
|
||||
Reference in New Issue
Block a user