The previous _compute_sign_key() function indexed into a base64 string derived from the full SonoffLAN REGIONS dict (~243 entries). Our partial dict only produced a 7876-char a string but needed index 7872+, so the function must use the full dict. Solution: pre-compute the key once from the full dict and hardcode the resulting 32-byte ASCII key. This is deterministic — the SonoffLAN algorithm always produces the same output regardless of when it runs. The sonoff_ewelink driver now loads cleanly alongside all other drivers.
130 lines
5.1 KiB
Python
130 lines
5.1 KiB
Python
"""Flask application factory."""
|
|
import os
|
|
from flask import Flask
|
|
from flask_sqlalchemy import SQLAlchemy
|
|
from flask_login import LoginManager
|
|
from flask_migrate import Migrate
|
|
from flask_socketio import SocketIO
|
|
|
|
from config import config_map
|
|
|
|
db = SQLAlchemy()
|
|
login_manager = LoginManager()
|
|
migrate = Migrate()
|
|
socketio = SocketIO()
|
|
|
|
|
|
def create_app(config_name: str = "default") -> Flask:
|
|
app = Flask(__name__, template_folder="templates", static_folder="static")
|
|
app.config.from_object(config_map[config_name])
|
|
|
|
# ── extensions ────────────────────────────────────────────────────────────
|
|
db.init_app(app)
|
|
migrate.init_app(app, db)
|
|
socketio.init_app(app, cors_allowed_origins="*", async_mode="threading")
|
|
|
|
login_manager.init_app(app)
|
|
login_manager.login_view = "auth.login"
|
|
login_manager.login_message = "Please log in to access this page."
|
|
login_manager.login_message_category = "warning"
|
|
|
|
# ── blueprints ────────────────────────────────────────────────────────────
|
|
from app.routes.auth import auth_bp
|
|
from app.routes.dashboard import dashboard_bp
|
|
from app.routes.boards import boards_bp
|
|
from app.routes.workflows import workflows_bp
|
|
from app.routes.api import api_bp
|
|
from app.routes.admin import admin_bp
|
|
from app.routes.sonoff import sonoff_bp
|
|
|
|
app.register_blueprint(auth_bp)
|
|
app.register_blueprint(dashboard_bp)
|
|
app.register_blueprint(boards_bp, url_prefix="/boards")
|
|
app.register_blueprint(workflows_bp, url_prefix="/workflows")
|
|
app.register_blueprint(api_bp, url_prefix="/api")
|
|
app.register_blueprint(admin_bp, url_prefix="/admin")
|
|
app.register_blueprint(sonoff_bp)
|
|
|
|
# ── user loader ───────────────────────────────────────────────────────────
|
|
from app.models.user import User
|
|
|
|
@login_manager.user_loader
|
|
def load_user(user_id):
|
|
return db.session.get(User, int(user_id))
|
|
|
|
# ── discover board drivers ─────────────────────────────────────────────────
|
|
from app.drivers.registry import registry
|
|
registry.discover()
|
|
app.logger.info("Board drivers loaded: %s", [d.DRIVER_ID for d in registry.all()])
|
|
|
|
# ── create tables & seed admin on first run ───────────────────────────────
|
|
with app.app_context():
|
|
# Import all models so their tables are registered before create_all
|
|
from app.models import board, user, workflow # noqa: F401
|
|
from app.models import sonoff_device # noqa: F401
|
|
db.create_all()
|
|
_seed_admin(app)
|
|
_add_entities_column(app)
|
|
_add_config_json_column(app)
|
|
_migrate_board_types(app)
|
|
|
|
return app
|
|
|
|
|
|
def _add_entities_column(app: Flask) -> None:
|
|
"""Add entities_json column to boards table if it doesn't exist yet."""
|
|
from sqlalchemy import text
|
|
with app.app_context():
|
|
try:
|
|
db.session.execute(text("ALTER TABLE boards ADD COLUMN entities_json TEXT DEFAULT '{}'"))
|
|
db.session.commit()
|
|
app.logger.info("Added entities_json column to boards table")
|
|
except Exception:
|
|
db.session.rollback() # Column already exists — safe to ignore
|
|
|
|
|
|
def _add_config_json_column(app: Flask) -> None:
|
|
"""Add config_json column to boards table if it doesn't exist yet."""
|
|
from sqlalchemy import text
|
|
with app.app_context():
|
|
try:
|
|
db.session.execute(text("ALTER TABLE boards ADD COLUMN config_json TEXT DEFAULT '{}'"))
|
|
db.session.commit()
|
|
app.logger.info("Added config_json column to boards table")
|
|
except Exception:
|
|
db.session.rollback() # Column already exists — safe to ignore
|
|
|
|
|
|
def _migrate_board_types(app: Flask) -> None:
|
|
"""Rename legacy board_type values to current driver IDs."""
|
|
from app.models.board import Board
|
|
renames = {
|
|
"olimex_esp32_c6": "olimex_esp32_c6_evb",
|
|
}
|
|
total = 0
|
|
for old, new in renames.items():
|
|
rows = Board.query.filter_by(board_type=old).all()
|
|
for b in rows:
|
|
b.board_type = new
|
|
total += 1
|
|
if total:
|
|
db.session.commit()
|
|
app.logger.info("Migrated %d board(s) to updated driver IDs", total)
|
|
|
|
|
|
def _seed_admin(app: Flask) -> None:
|
|
"""Create default admin account if no users exist."""
|
|
from app.models.user import User
|
|
from werkzeug.security import generate_password_hash
|
|
|
|
if User.query.count() == 0:
|
|
admin = User(
|
|
username="admin",
|
|
password_hash=generate_password_hash("admin"),
|
|
role="admin",
|
|
is_active=True,
|
|
)
|
|
db.session.add(admin)
|
|
db.session.commit()
|
|
app.logger.info("Default admin user created (username=admin password=admin)")
|