Repository navigation
Development
git clone https://github.com/CervezaStallone/XY-MD02_WebApp.git
cd XY-MD02_WebApp
python -m venv .venv
source .venv/bin/activate # Linux/macOS
.venv\Scripts\activate # Windows
pip install -r requirements.txtEdit .env:
APP_DEBUG=False # Keep False - uses Waitress, not Flask dev server
DEBUG_LOGGING=True # Enable detailed console output
APP_PORT=8050Debug logging enables:
- Database query details (table counts, timestamps)
- Downsampling statistics (before/after point counts)
- Comfort score calculations (first 5 points per update)
- Modbus communication details
- Error tracebacks
Note: Application uses Waitress production server, so APP_DEBUG doesn't enable hot reloading. For development iteration, restart the application after code changes.
XY-MD02_WebApp/
├── app.py # Main entry point - Waitress WSGI server
├── callbacks.py # Dash callbacks (8 callbacks for interactivity)
├── layout.py # UI layout (5-graph dashboard)
├── database.py # SQLite operations (table-per-day partitioning)
├── modbus_reader.py # Modbus RTU communication thread
├── psychrometric.py # Psychrometric chart generation
├── translations.py # Multi-language support (5 languages)
├── requirements.txt # Python dependencies
├── pyproject.toml # Project metadata (version, etc.)
├── startApp.ps1 # PowerShell startup script (Windows)
├── .env.example # Configuration template
├── .env # Your configuration (not in git)
├── .gitignore # Git ignore rules
├── .github/
│ ├── workflows/ # CI/CD automation
│ ├── ISSUE_TEMPLATE/ # Issue templates
│ └── dependabot.yml # Dependency updates
├── .wiki/ # Wiki documentation (markdown)
├── img/ # Static images
├── src/ # Data directory (database storage)
│ └── modbus_sensor_data.db # SQLite database
└── tests/ # Test files (if any)
app.py
├── layout.py
│ └── translations.py
├── callbacks.py
│ ├── database.py
│ ├── psychrometric.py
│ └── translations.py
└── modbus_reader.py
└── database.py
-
modbus_reader.py:
- Reads sensor every 1 second via Modbus RTU
- Validates readings (-50 to 100°C, 0-100% RH)
- Calculates dewpoint, absolute humidity
- Buffers 30 measurements
- Batch writes to database every 30 seconds
- Runs as background daemon thread
-
database.py:
- SQLite operations with WAL mode
- Table-per-day partitioning (
measurements_YYYYMMDD) - Automatic cleanup based on DATA_RETENTION_DAYS
- Optimized union queries across tables
- Indexed timestamp filtering
-
callbacks.py:
- 8 Dash callbacks for UI updates
- Queries database via union queries
- Applies adaptive downsampling for performance
- Calculates comfort scores (Humidex)
- Updates 5 synchronized graphs
- Handles historical data replay
-
layout.py:
- Renders UI components (5 cards, 5 graphs)
- Psychrometric chart container
- Historical replay modal and slider
- Multi-language dropdown
- Responsive design
-
psychrometric.py:
- Generates Mollier (psychrometric) diagrams
- Plots current or historical conditions
- Shows RH curves, comfort zone, saturation line
- Follow PEP 8
- Line length: 120 characters
- Use type hints where practical
- Docstrings for all functions
Run Black formatter:
black --line-length 120 *.pyRun flake8:
flake8 . --max-line-length=120 --exclude=.venv,__pycache__Before committing:
# Format code
black --line-length 120 *.py
# Check syntax
python -m py_compile *.py
# Run linter
flake8 . --max-line-length=120
# Verify imports
python -c "import database; import callbacks; import layout"# Run application
python app.py
# Open browser
# Test all features manuallypython -m py_compile app.py
python -m py_compile database.py
python -m py_compile modbus_reader.py
# ... etcVerify all languages display correctly:
- Switch to each language
- Check all UI elements
- Verify graph labels and tooltips
- Test with actual data
- Add to all languages in
translations.py:
"new_key": {
"nl": "Nederlandse tekst",
"en": "English text",
"de": "Deutscher Text",
"fr": "Texte français",
"es": "Texto español"
}- Use in layout or callbacks:
t("new_key", lang)- Add to
callbacks.py - Use @app.callback decorator
- Follow existing patterns
- Add error handling
Example:
@app.callback(
Output('output-id', 'property'),
Input('input-id', 'property')
)
def callback_function(input_value):
try:
# Process input
result = process(input_value)
return result
except Exception as e:
print(f"Error: {e}")
return default_value- Add creation in
database.py - Update
ensure_table_exists() - Add cleanup logic if needed
- Test with various data volumes
GitHub Actions automatically:
- Tests Python 3.10, 3.11, 3.12, 3.13
- Checks code quality (Black, flake8)
- Validates translations
- Runs on every push and PR
Location: callbacks.py - update_graph() callback
How it works:
if time_range_minutes > 60 or original_points > 5000:
df = df.set_index('timestamp')
# Select appropriate interval based on time range
if time_range_minutes <= 360: # 1-6 hours
df = df.resample('1min').mean()
elif time_range_minutes <= 1440: # 6-24 hours
df = df.resample('5min').mean()
# ... more intervals ...
df = df.dropna().reset_index()Key points:
- Uses pandas
resample()with mean aggregation - Automatically triggers for long ranges
- Preserves timestamp index during resampling
- Logs reduction statistics when DEBUG_LOGGING=True
- Maintains visual quality while dramatically improving performance
Location: callbacks.py - calculate_comfort_score() function
Formula:
- Calculate dewpoint:
Td = T - ((100 - RH) / 5.0) - Calculate vapor pressure:
e = 6.11 × exp(5417.7530 × (1/273.16 - 1/(273.15+Td))) - Calculate Humidex:
H = T + 0.5555 × (e - 10) - Map Humidex to 0-6 score based on ranges
Used in:
- Real-time comfort card display (via
get_comfort_level()) - Historical comfort score graph (5th graph)
- Applied to every datapoint after downsampling
Location: callbacks.py - Multiple callbacks
Implementation:
- User clicks "Choose range" button → opens modal
- User selects preset (5min-1month) or custom date/time range
-
handle_range_selection()captures choice -
load_historical_data()queries database and resamples to 1-minute intervals - Creates slider with appropriate time marks
-
update_historical_view()responds to slider movement (including drag) - Psychrometric chart updates in real-time showing selected moment
Features:
- Supports both preset and custom ranges
- Intelligent slider marks based on time span
- Live updates during drag
- Uses
create_psychrometric_chart_historical()for styling
Location: database.py
Benefits:
- Faster queries (only relevant tables scanned)
- Efficient cleanup (drop entire tables vs. delete rows)
- Better organization
- Scales to years of data
Implementation:
def get_table_name(date=None):
if date is None:
date = datetime.now()
return f"measurements_{date.strftime('%Y%m%d')}"
def build_union_query(cursor, columns, start_timestamp=None, end_timestamp=None):
# Determine relevant tables for time range
tables = get_tables_for_timerange(cursor, start_timestamp, end_timestamp)
# Build UNION ALL query across tables
queries = [f'SELECT {columns} FROM {table} WHERE ...' for table in tables]
return ' UNION ALL '.join(queries)Location: translations.py, callbacks.py, layout.py
Adding new translation key:
- Add to all 5 languages in
translations.py:
'new_key': {
'nl': 'Nederlandse tekst',
'en': 'English text',
'de': 'Deutscher Text',
'fr': 'Texte français',
'es': 'Texto español'
}- Use in callbacks:
t = TRANSLATIONS[lang]
text = t['new_key']- Use in layout (for static elements):
t = TRANSLATIONS[DEFAULT_LANGUAGE]
html.Div(t['new_key'])Note: Layout uses DEFAULT_LANGUAGE for initial render, callbacks update dynamically.
Location: app.py
Configuration:
serve(
server, # Flask/Dash server object
host='0.0.0.0', # Listen on all interfaces
port=8050, # Port number
threads=8, # Worker threads for concurrent requests
channel_timeout=60, # Idle connection timeout
cleanup_interval=10, # Connection cleanup frequency
asyncore_use_poll=True # Better Windows performance
)Why Waitress:
- Production-ready (not Flask dev server)
- Cross-platform (Windows/Linux/macOS)
- Multi-threaded for concurrent users
- Better error handling and logging
- No dependencies on C compilers (pure Python)
View workflow runs: https://github.com/CervezaStallone/XY-MD02_WebApp/actions
- Install in virtual environment:
pip install new-package- Update requirements:
pip freeze > requirements.txt- Update pyproject.toml:
dependencies = [
"existing-package==1.0.0",
"new-package==2.0.0",
]- Test installation:
pip install -r requirements.txtDependabot automatically creates PRs for updates.
Manual update:
pip install --upgrade package-name
pip freeze > requirements.txtUse Conventional Commits format:
type(scope): description
[optional body]
[optional footer]
Types:
-
feat: New feature -
fix: Bug fix -
docs: Documentation -
style: Formatting -
refactor: Code restructuring -
test: Adding tests -
chore: Maintenance
Examples:
feat(ui): Add temperature unit selection
fix(modbus): Handle timeout errors gracefully
docs(readme): Update installation instructions
chore(deps): Bump pandas from 2.2.3 to 2.3.0
- Update version in
pyproject.toml - Commit:
chore: Bump version to X.Y.Z - Create tag:
git tag -a vX.Y.Z -m "Release vX.Y.Z" - Push:
git push origin main --tags - Create GitHub release with changelog
See CONTRIBUTING.md for:
- Code of Conduct
- Pull request process
- Issue reporting guidelines