BMS/backend/core/database.py
2026-03-19 11:32:17 +00:00

130 lines
5.3 KiB
Python

import logging
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from sqlalchemy import text
from core.config import settings
logger = logging.getLogger(__name__)
engine = create_async_engine(settings.DATABASE_URL, echo=False, pool_size=10, max_overflow=20)
AsyncSessionLocal = async_sessionmaker(engine, expire_on_commit=False)
async def init_db() -> None:
async with engine.begin() as conn:
# Enable TimescaleDB
await conn.execute(text("CREATE EXTENSION IF NOT EXISTS timescaledb CASCADE"))
# Sensor readings — core time-series table
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS readings (
recorded_at TIMESTAMPTZ NOT NULL,
sensor_id VARCHAR(120) NOT NULL,
sensor_type VARCHAR(50) NOT NULL,
site_id VARCHAR(50) NOT NULL,
room_id VARCHAR(50),
rack_id VARCHAR(50),
value DOUBLE PRECISION NOT NULL,
unit VARCHAR(20)
)
"""))
# Convert to hypertable — no-op if already one
try:
await conn.execute(text(
"SELECT create_hypertable('readings', by_range('recorded_at'), if_not_exists => TRUE)"
))
except Exception:
try:
await conn.execute(text(
"SELECT create_hypertable('readings', 'recorded_at', if_not_exists => TRUE)"
))
except Exception as e:
logger.warning(f"Hypertable setup skipped (table still works): {e}")
await conn.execute(text("""
CREATE INDEX IF NOT EXISTS idx_readings_sensor_time
ON readings (sensor_id, recorded_at DESC)
"""))
# Alarms table
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS alarms (
id BIGSERIAL PRIMARY KEY,
sensor_id VARCHAR(120),
site_id VARCHAR(50),
room_id VARCHAR(50),
rack_id VARCHAR(50),
severity VARCHAR(20) NOT NULL,
message TEXT NOT NULL,
state VARCHAR(20) NOT NULL DEFAULT 'active',
triggered_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
acknowledged_at TIMESTAMPTZ,
resolved_at TIMESTAMPTZ
)
"""))
# Site config — generic key/value JSON store (used for floor layout etc.)
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS site_config (
site_id VARCHAR(50) NOT NULL,
key VARCHAR(100) NOT NULL,
value JSONB NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (site_id, key)
)
"""))
# Sensor device registry
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS sensors (
id SERIAL PRIMARY KEY,
site_id VARCHAR(50) NOT NULL,
device_id VARCHAR(100) NOT NULL,
name VARCHAR(200) NOT NULL,
device_type VARCHAR(50) NOT NULL,
room_id VARCHAR(50),
rack_id VARCHAR(50),
protocol VARCHAR(30) NOT NULL DEFAULT 'mqtt',
protocol_config JSONB NOT NULL DEFAULT '{}',
enabled BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(site_id, device_id)
)
"""))
# Configurable alarm thresholds (replaces hard-coded list at runtime)
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS alarm_thresholds (
id SERIAL PRIMARY KEY,
site_id VARCHAR(50) NOT NULL,
sensor_type VARCHAR(50) NOT NULL,
threshold_value FLOAT NOT NULL,
direction VARCHAR(10) NOT NULL,
severity VARCHAR(20) NOT NULL,
message_template TEXT NOT NULL,
enabled BOOLEAN NOT NULL DEFAULT true,
locked BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)
"""))
# Site-level settings (profile, notifications, integrations, page prefs)
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS site_settings (
site_id VARCHAR(50) NOT NULL,
category VARCHAR(50) NOT NULL,
key VARCHAR(100) NOT NULL,
value JSONB NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (site_id, category, key)
)
"""))
logger.info("Database initialised")
async def get_session():
async with AsyncSessionLocal() as session:
yield session