Files
location_managemet/app/__init__.py

208 lines
8.6 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
from app.routes.tuya import tuya_bp
from app.routes.layouts import layouts_bp
from app.routes.devices import devices_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")
app.register_blueprint(devices_bp, url_prefix="/devices")
# ── 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
from app.models import device # noqa: F401
db.create_all()
_seed_admin(app)
_add_entities_column(app)
_add_config_json_column(app)
_migrate_board_types(app)
_add_device_hardware_id_column(app)
_add_device_app_id_column(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)")
def _add_device_app_id_column(app: Flask) -> None:
"""Add app_id column to devices table and backfill existing rows."""
from sqlalchemy import text
with app.app_context():
try:
db.session.execute(text("ALTER TABLE devices ADD COLUMN app_id TEXT"))
db.session.commit()
app.logger.info("Added app_id column to devices table")
except Exception:
db.session.rollback() # Column already exists — safe to ignore
# Backfill any rows that have no app_id yet
from app.models.device import Device
unset = Device.query.filter(Device.app_id.is_(None)).all()
for d in unset:
d.app_id = Device.generate_app_id(d.name, exclude_id=d.id)
if unset:
db.session.commit()
app.logger.info("Backfilled app_id for %d device(s)", len(unset))
def _add_device_hardware_id_column(app: Flask) -> None:
"""Add hardware_device_id column and backfill from existing entity_num bindings."""
from sqlalchemy import text
with app.app_context():
try:
db.session.execute(text("ALTER TABLE devices ADD COLUMN hardware_device_id TEXT"))
db.session.commit()
app.logger.info("Added hardware_device_id column to devices table")
except Exception:
db.session.rollback() # Column already exists — safe to ignore
# Backfill: resolve entity_num → actual hardware device_id string
from app.models.device import Device
from app.models.sonoff_device import SonoffDevice
from app.models.tuya_device import TuyaDevice
unset = Device.query.filter(
Device.hardware_device_id.is_(None),
Device.entity_num.isnot(None),
Device.entity_type.in_(["sonoff", "tuya"]),
).all()
for d in unset:
hw_id = None
sd_or_td_id = d.entity_num // 100
if d.entity_type == "sonoff":
obj = db.session.get(SonoffDevice, sd_or_td_id)
if obj:
hw_id = obj.device_id
elif d.entity_type == "tuya":
obj = db.session.get(TuyaDevice, sd_or_td_id)
if obj:
hw_id = obj.device_id
if hw_id:
d.hardware_device_id = hw_id
if unset:
db.session.commit()
app.logger.info("Backfilled hardware_device_id for %d device(s)", len(unset))