"""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 from app.routes.tuya import tuya_bp from app.routes.layouts import layouts_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) app.register_blueprint(tuya_bp) app.register_blueprint(layouts_bp, url_prefix="/layouts") # ── 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()]) # ── Jinja filters ───────────────────────────────────────────────────────── from app.drivers.tuya_cloud.driver import format_dp_value as _tuya_fmt @app.template_filter("tuya_dp") def _tuya_dp_filter(value, dp_code: str, status=None): """Format a raw Tuya DP value for display (temperature scaling, units…).""" return _tuya_fmt(dp_code, value, status) # ── 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 from app.models import tuya_device # noqa: F401 from app.models import layout # 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)")