diff --git a/.env.example b/.env.example index f618594..9c532c6 100644 --- a/.env.example +++ b/.env.example @@ -5,6 +5,7 @@ JWT_SECRET_KEY=development-only-change-me-minimum-32-bytes JWT_EXPIRATION_MINUTES=60 JWT_ALGORITHM=HS256 GRID_EMISSION_FACTOR_KG_PER_KWH=0.0 +# For a fresh demo database only: DEMO_SIMULATION_START_UTC=2026-09-18T11:58:00Z POSTGRES_DB=chargegrid POSTGRES_USER=chargegrid diff --git a/.gitignore b/.gitignore index 0e055f1..3138f0c 100644 --- a/.gitignore +++ b/.gitignore @@ -153,6 +153,7 @@ activemq-data/ !.env.example .envrc .venv +.video-edit-venv/ env/ venv/ ENV/ @@ -236,5 +237,8 @@ data/models/* !data/processed/.gitkeep !data/models/.gitkeep +# Generated temporary artifacts +tmp/ + # Streamlit .streamlit/secrets.toml diff --git a/README.md b/README.md index 6c028e0..c72ae12 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,34 @@ make check O alvo executa testes, lint e verificação de tipos no backend e no frontend, além do build web. Consulte [CONTRIBUTING.md](docs/CONTRIBUTING.md) para o fluxo detalhado. +## Demo da Sprint 3 + +Execute o [roteiro reproduzível da Sprint 3](docs/sprint-3/README.md) em um banco isolado. Ele inclui comandos de seed, configuração UTC, cenário por endpoints públicos e resultados esperados. + +### Documento da Sprint 3 em PDF + +Acesse o [documento da Sprint 3 em PDF](output/pdf/chargegrid_sprint_3.pdf). + +### Documento da Sprint 3 em Markdown + +Acesse o [documento da Sprint 3 em Markdown](docs/sprint-3/chargegrid_sprint_3.md). + +### Vídeo pitch da Sprint 3 em MP4 + +Acesse o [arquivo MP4 do vídeo pitch da Sprint 3](video_editado/Pitch_challenge_sprint3.mp4). + +### Vídeo pitch da Sprint 3 no YouTube + +Assista ao [vídeo pitch da Sprint 3 no YouTube](https://youtu.be/caGX1bfDN7s). + +Após configurar `.env` e exportar `DEMO_ADMIN_PASSWORD` e `DEMO_USER_PASSWORD` conforme o roteiro: + +```bash +docker compose up --build -d +docker compose exec -e DEMO_ADMIN_PASSWORD -e DEMO_USER_PASSWORD backend python -m app.demo_seed +python3 scripts/sprint3_demo.py +``` + ## Documentação - [Briefing](BRIEFING.md) @@ -132,5 +160,4 @@ limites em [docs/PHASE_6.md](docs/PHASE_6.md). A Fase 6 ainda não está concluída: o KPI obrigatório de risco de pico depende de uma previsão futura válida, e o fluxo automático de previsão/classificação da Fase 7 ainda não existe. Sem esses dados, a tela mostra um estado -informativo. O simulador também depende de ticks manuais pela API; não há -seed reproduzível oficial para a demonstração completa. +informativo. O simulador também depende de ticks manuais pela API; a Sprint 3 fornece seed e roteiro reproduzíveis para a demonstração completa. diff --git a/backend/app/core/config.py b/backend/app/core/config.py index d14f016..bf395f8 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -1,7 +1,8 @@ +from datetime import UTC, datetime from functools import lru_cache from typing import Literal -from pydantic import Field, model_validator +from pydantic import Field, field_validator, model_validator from pydantic_settings import BaseSettings, SettingsConfigDict DEFAULT_JWT_SECRET = "development-only-change-me-minimum-32-bytes" @@ -21,13 +22,28 @@ class Settings(BaseSettings): app_cors_origins: list[str] = Field(default_factory=lambda: ["http://localhost:5173"]) api_v1_prefix: str = "/api/v1" grid_emission_factor_kg_per_kwh: float = Field(default=0.0, ge=0) + demo_simulation_start_utc: datetime | None = None database_url: str = "postgresql+psycopg://chargegrid:chargegrid@localhost:5432/chargegrid" jwt_secret_key: str = Field(default=DEFAULT_JWT_SECRET, min_length=32) jwt_expiration_minutes: int = Field(default=60, gt=0) jwt_algorithm: Literal["HS256"] = "HS256" + @field_validator("demo_simulation_start_utc", mode="before") + @classmethod + def empty_demo_start_is_unset(cls, value: object) -> object: + return None if value == "" else value + @model_validator(mode="after") def reject_default_jwt_secret_outside_local_environments(self) -> "Settings": + if self.demo_simulation_start_utc is not None: + instant = self.demo_simulation_start_utc + if self.app_env.strip().lower() not in {"development", "demo", "test"}: + raise ValueError( + "DEMO_SIMULATION_START_UTC is allowed only in local demo environments" + ) + if instant.tzinfo is None or instant.utcoffset() != UTC.utcoffset(instant): + raise ValueError("DEMO_SIMULATION_START_UTC must be an explicit UTC instant") + self.demo_simulation_start_utc = instant.astimezone(UTC) if ( self.app_env.strip().lower() in NON_LOCAL_ENVIRONMENTS and self.jwt_secret_key == DEFAULT_JWT_SECRET diff --git a/backend/app/demo_seed.py b/backend/app/demo_seed.py new file mode 100644 index 0000000..a5067b8 --- /dev/null +++ b/backend/app/demo_seed.py @@ -0,0 +1,100 @@ +"""Idempotent, explicitly named seed for the isolated Sprint 3 demo database.""" + +import os +from datetime import UTC, datetime +from decimal import Decimal + +from sqlalchemy import select +from sqlalchemy.orm import Session + +from app.core.security import hash_password, verify_password +from app.db.session import SessionLocal +from app.models.billing import Tariff +from app.models.infrastructure import Charger, ChargerStatus, ChargingStation +from app.models.prediction import SystemConfiguration +from app.models.user import User, UserRole +from app.models.vehicle import Vehicle + +PREFIX = "SPRINT3-DEMO" +EMAILS = ["sprint3-admin@demo.invalid"] + [f"sprint3-user-{i}@demo.invalid" for i in range(1, 5)] +TARIFF = Decimal("0.8000") +EMISSION_FACTOR = 0.4 + + +def seed(db: Session, admin_password: str, user_password: str) -> None: + if len(admin_password) < 8 or len(user_password) < 8: + raise ValueError("Demo passwords must each have at least 8 characters") + for index, email in enumerate(EMAILS): + role = UserRole.ADMIN if index == 0 else UserRole.USER + user = db.scalar(select(User).where(User.email == email)) + if user is None: + user = User(name=f"Sprint 3 Demo {'Admin' if index == 0 else f'User {index}'}", + email=email, + password_hash=hash_password( + admin_password if index == 0 else user_password + ), + role=role, is_active=True) + db.add(user) + db.flush() + elif (user.role != role or not user.is_active or not verify_password( + admin_password if index == 0 else user_password, user.password_hash + )): + raise ValueError(f"Demo account conflicts with existing user: {email}") + if index: + plate = f"S3D-{index:04d}" + vehicle = db.scalar(select(Vehicle).where(Vehicle.license_plate == plate)) + if vehicle is None: + db.add(Vehicle(user_id=user.id, name=f"Demo EV {index}", brand="Demo", + model="EV", license_plate=plate, max_charge_power_kw=20)) + elif vehicle.user_id != user.id or vehicle.max_charge_power_kw != 20: + raise ValueError(f"Demo vehicle conflicts: {plate}") + + station = db.scalar(select(ChargingStation).where(ChargingStation.name == PREFIX)) + if station is None: + station = ChargingStation(name=PREFIX, description="Sprint 3 simulated station", + grid_limit_kw=60, station_peak_solar_kw=0, is_active=True) + db.add(station) + db.flush() + elif (station.grid_limit_kw != 60 or station.station_peak_solar_kw not in (0, 20) + or not station.is_active): + raise ValueError("Demo station has conflicting configuration") + for index in range(1, 5): + code = f"{PREFIX}-CH-{index:02d}" + charger = db.scalar(select(Charger).where(Charger.code == code)) + if charger is None: + db.add(Charger(station_id=station.id, name=f"CH-{index:02d}", code=code, + max_power_kw=22, status=ChargerStatus.AVAILABLE, is_active=True)) + elif charger.station_id != station.id or charger.max_power_kw != 22: + raise ValueError(f"Demo charger conflicts: {code}") + + tariff = db.scalar(select(Tariff).where(Tariff.name == PREFIX)) + if tariff is None: + if db.scalar(select(Tariff).where(Tariff.is_active.is_(True))) is not None: + raise ValueError("Another active tariff exists; use an isolated demo database") + db.add(Tariff(name=PREFIX, price_per_kwh=TARIFF, currency="BRL", is_active=True, + valid_from=datetime(2020, 1, 1, tzinfo=UTC))) + elif tariff.price_per_kwh != TARIFF or not tariff.is_active: + raise ValueError("Demo tariff has conflicting configuration") + config = db.scalar(select(SystemConfiguration).limit(1)) + if config is None: + db.add(SystemConfiguration(simulation_speed=60, + grid_emission_factor_kg_per_kwh=EMISSION_FACTOR, + high_demand_threshold=0.85, + medium_peak_threshold=0.7, high_peak_threshold=0.9)) + elif config.grid_emission_factor_kg_per_kwh != EMISSION_FACTOR or config.simulation_speed != 60: + raise ValueError("Existing system configuration conflicts with demo") + + +def main() -> None: + admin_password = os.environ.get("DEMO_ADMIN_PASSWORD") + user_password = os.environ.get("DEMO_USER_PASSWORD") + if not admin_password or not user_password: + raise SystemExit("Set DEMO_ADMIN_PASSWORD and DEMO_USER_PASSWORD in the environment") + with SessionLocal() as db, db.begin(): + seed(db, admin_password, user_password) + print("Sprint 3 demo seed ready: 1 admin, 4 users, 4 vehicles, " + "1 station, 4 chargers, 1 tariff, ESG config") + + +if __name__ == "__main__": + main() diff --git a/backend/app/simulation/control.py b/backend/app/simulation/control.py index d36f3bc..b2bbb1b 100644 --- a/backend/app/simulation/control.py +++ b/backend/app/simulation/control.py @@ -7,6 +7,7 @@ from sqlalchemy import func, select from sqlalchemy.orm import Session +from app.core.config import get_settings from app.models.energy import EnergyReading, SolarReading from app.models.prediction import SystemConfiguration from app.services.energy_allocation import EqualSharePowerResolver @@ -19,7 +20,9 @@ class SimulationController: def __init__(self, clock: SimulationClock | None = None) -> None: - self.clock = clock or SimulationClock(initial_instant=datetime.now(UTC)) + self.clock = clock or SimulationClock( + initial_instant=get_settings().demo_simulation_start_utc or datetime.now(UTC) + ) self.last_tick: datetime | None = None self.lock = RLock() diff --git a/backend/tests/test_demo_seed.py b/backend/tests/test_demo_seed.py new file mode 100644 index 0000000..386f25e --- /dev/null +++ b/backend/tests/test_demo_seed.py @@ -0,0 +1,51 @@ +from datetime import UTC, datetime + +import pytest +from sqlalchemy import func, select +from sqlalchemy.orm import Session + +from app.core.config import Settings, get_settings +from app.demo_seed import seed +from app.models.billing import Tariff +from app.models.infrastructure import Charger, ChargingStation +from app.models.prediction import SystemConfiguration +from app.models.user import User +from app.models.vehicle import Vehicle +from app.simulation.control import SimulationController + + +def test_demo_seed_is_idempotent(db_session: Session) -> None: + seed(db_session, "admin-secret", "user-secret") + db_session.commit() + seed(db_session, "admin-secret", "user-secret") + db_session.commit() + for model, expected in ((User, 5), (Vehicle, 4), (ChargingStation, 1), + (Charger, 4), (Tariff, 1), (SystemConfiguration, 1)): + assert db_session.scalar(select(func.count()).select_from(model)) == expected + assert db_session.scalar(select(ChargingStation)).grid_limit_kw == 60 + assert db_session.scalar(select(Tariff)).is_active + + +def test_demo_seed_requires_passwords(db_session: Session) -> None: + with pytest.raises(ValueError, match="at least 8"): + seed(db_session, "short", "user-secret") + + +def test_demo_clock_is_opt_in_and_utc(monkeypatch: pytest.MonkeyPatch) -> None: + with monkeypatch.context() as env: + env.setenv("DEMO_SIMULATION_START_UTC", "2026-09-18T11:58:00Z") + get_settings.cache_clear() + assert SimulationController().clock.current_instant == datetime( + 2026, 9, 18, 11, 58, tzinfo=UTC + ) + env.setenv("APP_ENV", "production") + env.setenv("JWT_SECRET_KEY", "a" * 32) + with pytest.raises(ValueError, match="local demo"): + Settings() + env.setenv("APP_ENV", "development") + env.setenv("DEMO_SIMULATION_START_UTC", "2026-09-18T11:58:00") + with pytest.raises(ValueError, match="explicit UTC"): + Settings() + get_settings.cache_clear() + elapsed = SimulationController().clock.current_instant - datetime.now(UTC) + assert abs(elapsed.total_seconds()) < 5 diff --git a/backend/tests/test_sprint3_demo_script.py b/backend/tests/test_sprint3_demo_script.py new file mode 100644 index 0000000..6bd9c61 --- /dev/null +++ b/backend/tests/test_sprint3_demo_script.py @@ -0,0 +1,59 @@ +import importlib.util +from pathlib import Path +from types import ModuleType +from urllib.error import URLError + +import pytest + + +def load_demo_script() -> ModuleType: + path = Path(__file__).parents[2] / "scripts" / "sprint3_demo.py" + spec = importlib.util.spec_from_file_location("sprint3_demo", path) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +class HealthyResponse: + status = 200 + + def __enter__(self) -> "HealthyResponse": + return self + + def __exit__(self, *args: object) -> None: + return None + + +def test_wait_for_api_retries_transient_startup_failure( + monkeypatch: pytest.MonkeyPatch, +) -> None: + demo = load_demo_script() + attempts = iter([URLError("starting"), HealthyResponse()]) + monotonic_values = iter([0.0, 0.0, 0.1]) + + def open_api(*args: object, **kwargs: object) -> HealthyResponse: + result = next(attempts) + if isinstance(result, Exception): + raise result + return result + + monkeypatch.setattr(demo, "urlopen", open_api) + monkeypatch.setattr(demo.time, "monotonic", lambda: next(monotonic_values)) + monkeypatch.setattr(demo.time, "sleep", lambda _: None) + + demo.wait_for_api(timeout_seconds=1) + + +def test_wait_for_api_reports_backend_diagnostics(monkeypatch: pytest.MonkeyPatch) -> None: + demo = load_demo_script() + monotonic_values = iter([0.0, 0.0, 1.0]) + + monkeypatch.setattr(demo, "urlopen", lambda *args, **kwargs: (_ for _ in ()).throw( + URLError("unavailable") + )) + monkeypatch.setattr(demo.time, "monotonic", lambda: next(monotonic_values)) + monkeypatch.setattr(demo.time, "sleep", lambda _: None) + + with pytest.raises(RuntimeError, match=r"docker compose logs backend"): + demo.wait_for_api(timeout_seconds=0.5) diff --git a/docker-compose.yml b/docker-compose.yml index 71ef3b8..f67059a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -26,7 +26,15 @@ services: JWT_SECRET_KEY: ${JWT_SECRET_KEY:-development-only-change-me-minimum-32-bytes} JWT_EXPIRATION_MINUTES: ${JWT_EXPIRATION_MINUTES:-60} JWT_ALGORITHM: ${JWT_ALGORITHM:-HS256} + GRID_EMISSION_FACTOR_KG_PER_KWH: ${GRID_EMISSION_FACTOR_KG_PER_KWH:-0.0} + DEMO_SIMULATION_START_UTC: ${DEMO_SIMULATION_START_UTC:-} command: sh -c "alembic upgrade head && uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload" + healthcheck: + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/api/v1/health', timeout=2)"] + interval: 2s + timeout: 3s + retries: 15 + start_period: 5s depends_on: db: condition: service_healthy @@ -41,7 +49,8 @@ services: environment: VITE_API_URL: ${VITE_API_URL:-http://localhost:8000/api/v1} depends_on: - - backend + backend: + condition: service_healthy ports: - "5173:5173" volumes: diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 53f505c..4dff1a7 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,61 +1,7 @@ -# Arquitetura +# Arquitetura executada -O ChargeGrid Intelligence é um monólito modular composto por três processos em desenvolvimento: +O ChargeGrid Intelligence é um monólito modular. O navegador React consome a API REST FastAPI sob `/api/v1`; os serviços usam SQLAlchemy e PostgreSQL. Os contratos HTTP são Pydantic. Consulte os [diagramas da Sprint 3](sprint-3/README.md) para as conexões do fluxo de demonstração. -```text -Browser → React/Vite → FastAPI → PostgreSQL -``` +O serviço `charging_sessions` valida início e encerramento, captura tarifa e cria invoice simulada. O controlador em `simulation/control.py` executa apenas ticks manuais por `POST /simulation/ticks`. O provedor `SimulationEnergyDataProvider` calcula geração solar simulada pela curva UTC; o alocador `EqualSharePowerResolver` aplica limites individuais e de rede e prioriza solar. `simulation/tick.py` persiste leituras, acumuladores e alertas na mesma transação. Analytics e dashboards consultam dados persistidos pela API. O navegador não calcula as regras de energia. -O backend separa transporte HTTP (`api`), configuração transversal (`core`), acesso -ao banco (`db`), persistência (`models`), contratos (`schemas`) e regras de domínio -(`services`). Os pacotes `repositories`, `simulation`, `analytics` e `ml` já reservam -as fronteiras previstas no `SPEC.md`, mas ainda não possuem implementação funcional. -Todos os módulos compartilham um único processo e um único banco. - -## Fronteiras - -- Rotas usam schemas Pydantic para validar HTTP e, no estado atual, executam CRUD e - consultas simples diretamente pela `Session` do SQLAlchemy. -- O serviço `services/charging_sessions.py` concentra as regras existentes de início - e encerramento da sessão: usuário ativo, propriedade do veículo, disponibilidade - do carregador, exclusividade de sessão ativa, potência solicitada, cálculo final, - invoice e alerta. -- `api/routes/common.py` centraliza a injeção da sessão de banco, busca com resposta - 404 e commit com conversão de conflito de integridade para HTTP 409. -- O pacote `repositories` está vazio. Repositórios serão adicionados quando consultas - repetidas ou complexas exigirem essa separação; não são uma camada ativa hoje. -- Modelos representam persistência; schemas representam contratos da API. -- `simulation` será uma fonte de dados substituível, não o proprietário do domínio. -- `analytics` e `ml` ainda serão implementados; ML permanecerá consultivo e nunca - substituirá restrições energéticas determinísticas. - -## Fluxo implementado de sessão - -```text -HTTP /api/v1/sessions - ↓ -schemas Pydantic + carregamento das entidades - ↓ -serviço de sessões (regras e alterações da transação) - ↓ -commit na rota - ↓ -SQLAlchemy models → PostgreSQL -``` - -As demais rotas seguem, por enquanto, o fluxo direto -`rota → Session SQLAlchemy → models → PostgreSQL`. Regras novas não devem ser -acrescentadas a esse fluxo: quando houver regra de negócio, ela deve ser movida para -um serviço testável. - -## Decisões da fundação - -- Todo endpoint público usa `/api/v1`. -- Configuração e segredos chegam por variáveis de ambiente. -- Alterações de schema são versionadas por Alembic. -- PostgreSQL 16 é o banco de desenvolvimento e produção do MVP. -- O frontend consome a URL configurável `VITE_API_URL`. -- Datas são armazenadas em UTC, IDs usam UUID e valores monetários usam tipos - decimais. - -Consulte `SPEC.md` para regras funcionais e precedência de requisitos. +A previsão de demanda e a classificação de risco ainda não são produzidas automaticamente. Não há integração física, agendador de ticks ou comandos para carregadores. O pacote `repositories` permanece reservado, sem camada ativa. Todas as alterações de esquema usam Alembic; datas são UTC, IDs são UUID e valores monetários usam tipos decimais. diff --git a/docs/SPRINT_3_PLAN.md b/docs/SPRINT_3_PLAN.md new file mode 100644 index 0000000..7bff238 --- /dev/null +++ b/docs/SPRINT_3_PLAN.md @@ -0,0 +1,103 @@ +# Sprint 3 — Plano de desenvolvimento e evidências + +## Objetivo e limite + +Entregar uma demonstração reproduzível do protótipo **simulado** do ChargeGrid +Intelligence, com código, diagramas e resultados que representem o mesmo fluxo. +O cenário segue `SPEC.md` §§ 54–55 e o Golden Path de `BRIEFING.md` § 17. +`SPEC.md` continua sendo a fonte de verdade para regras de negócio. + +Esta sprint prepara e evidencia o fluxo já implementado: sessões, alocação +igualitária, limite da rede, prioridade solar, leituras, alertas, billing, +indicadores de sustentabilidade e dashboards. Previsão automática de demanda e +classificação de risco pertencem à Fase 7; a apresentação deve identificar +claramente essa lacuna, sem preencher o dashboard com uma previsão inventada. +Integração com carregadores, inversores ou medidores físicos permanece futura. + +## Estado de partida + +- `backend/tests/test_phase_6_flow.py` já exercita o cenário integrado pela API. +- `backend/app/simulation/control.py` executa ticks manuais, um por chamada a + `POST /api/v1/simulation/ticks`; não existe agendador de ticks. +- `backend/app/simulation/energy_data.py` calcula solar por curva determinística + entre 06:00 e 18:00 UTC, com pico às 12:00 UTC. +- `frontend/src/pages/AdminDashboardPage.tsx` e `UserDashboardPage.tsx` + consultam APIs existentes e oferecem atualização manual da tela. +- Não existe seed oficial nem roteiro operacional para a demonstração. O + `docs/ARCHITECTURE.md` ainda descreve simulação e analytics como futuros. +- A API de sustentabilidade lê o fator de emissão de + `GRID_EMISSION_FACTOR_KG_PER_KWH` (`backend/app/core/config.py`); não se deve + atribuir o valor exibido ao registro `SystemConfiguration` sem mudar o código. + +## Entregas de código, em ordem + +| Etapa | Mudança pequena e verificável | Aceite | +| --- | --- | --- | +| 1. Dados iniciais | Criar um comando de seed em `backend/` ou `scripts/` que use os modelos e a configuração existentes: 1 ADMIN, 4 USER, 4 veículos de 20 kW, 1 estação com limite de rede de 60 kW, 4 carregadores de 22 kW, 1 tarifa ativa e configuração ESG. Identificar os registros da demo para permitir reexecução sem duplicatas. Receber senhas por variáveis de ambiente, sem gravá-las no Git. | Uma instalação limpa recebe os dados previstos no `SPEC.md` § 54; uma segunda execução é segura. | +| 2. Relógio reproduzível | Se o ensaio ao vivo confirmar a necessidade, acrescentar uma configuração **opt-in para desenvolvimento/demonstração** que inicialize o `SimulationClock` em um instante UTC escolhido, próximo de 12:00 UTC. Sem a configuração, manter o início no horário corrente. Validar entrada e documentar que o processo da API deve ter um único controlador de simulação na demo. | O primeiro tick pode ser repetido no pico solar em um banco de demo isolado; produção mantém o comportamento atual. | +| 3. Roteiro executável | Criar script de demonstração que use **endpoints públicos**, autentique cada usuário e imprima respostas relevantes. Sequência: iniciar três sessões; tick sem solar; iniciar a quarta; tick sem solar; alterar `station_peak_solar_kw` pela API administrativa; tick com solar; consultar leituras, alertas e dashboards; encerrar uma sessão; consultar invoice e sustentabilidade. Não escrever diretamente nas tabelas durante o fluxo. | Saída verificável de 60 kW na rede, depois 4 × 15 kW, depois aproximadamente 80 kW totais (20 solar + 60 rede), respeitando limites individuais. | +| 4. Interface e ajustes | Ensaiar `/admin` e `/user` com os dados do script. Corrigir apenas defeitos que impeçam a demonstração ou tornem dados exibidos enganosos. Se comandos na UI forem necessários, colocá-los em um componente administrativo que chame a API existente; não duplicar regras energéticas em React. | Prints mostram sessões, gráficos solar/rede, alerta, histórico, cobrança e indicadores com valores coerentes com as respostas da API. | +| 5. Regressão | Acrescentar testes para seed, configuração opcional do relógio e script/fluxo onde houver risco real de regressão. Reaproveitar o teste integrado existente para as invariantes energéticas. Executar `make check` e ensaio com PostgreSQL via Docker Compose. | Testes, lint, tipos e build passam; o cenário funciona em banco limpo e pode ser repetido. | + +O script de demonstração é um **orquestrador de chamadas**, não um novo motor de +simulação. O servidor continua responsável por autenticação, transições de +sessão, alocação, cálculo energético, persistência e faturamento. +Para a demo, definir no ambiente o mesmo fator de emissão registrado na +configuração seed e documentar que a API usa a variável de ambiente. + +## Diagramas e rastreabilidade + +Criar em `docs/sprint-3/` dois diagramas Mermaid versionáveis e exportar PNGs +para o PDF/vídeo somente após conferir o fluxo executado. A tabela determina o +que cada seta pode afirmar: + +| Diagrama | Elementos e conexões permitidos | Código que comprova | +| --- | --- | --- | +| Arquitetura executada | Navegador React → API REST FastAPI → serviços de sessão, simulação, energia, billing e analytics → SQLAlchemy/PostgreSQL. O simulador fornece dados solares; o dashboard lê dados pela API. | `frontend/src/api/client.ts`, `backend/app/api/router.py`, `backend/app/services/`, `backend/app/simulation/`, `backend/app/db/`. | +| Sequência da demo | USER inicia sessão → ADMIN aciona tick → controlador consulta sessões e solar → alocador calcula potência → tick grava leituras/alerta → dashboards consultam API → USER encerra sessão → serviço cria invoice. | `backend/app/api/routes/sessions.py`, `simulation.py`, `backend/app/simulation/tick.py`, `backend/app/services/energy_allocation.py`, `charging_sessions.py`, rotas de analytics/billing. | + +Revisar `docs/ARCHITECTURE.md` para retirar afirmações antigas de que +simulação e analytics não funcionam. Diagramas devem indicar **tick manual**, +**solar simulada** e **cobrança simulada**. Não desenhar fluxo de ML treinado, +controle de hardware, OCPP/Modbus, agendador ou comandos físicos, pois não +existem nessa implementação. Se uma dessas funções for implementada numa fase +posterior, atualizar os diagramas na mesma alteração de código. + +## Evidências e apresentação + +1. Registrar saída do roteiro com os valores por tick, kWh, alerta, invoice e + CO₂ evitado, incluindo tarifa e fator de emissão usados. Distinguir + claramente medições **simuladas** de medições reais. +2. Capturar prints da API/OpenAPI e dos dashboards após atualizar as telas. +3. Escrever `docs/sprint-3/README.md` (ou seção equivalente no README raiz) + com título, integrantes informados pela equipe, diagramas, escolhas + técnicas, resultados, instruções de reprodução e relação com a disciplina. +4. Preparar roteiro e vídeo de até cinco minutos: arquitetura; três sessões; + quarta sessão e redistribuição; solar; dados/alerta; encerramento, + faturamento e sustentabilidade; limites conhecidos. +5. Atualizar o README raiz com um link para a entrega e comandos exatos de + execução. Manter credenciais e arquivos `.env` fora do repositório. + +## Compatibilidade com as próximas fases + +- Manter o monólito modular, os contratos `/api/v1` e as regras de energia nos + serviços existentes. O script da sprint não deve ser importado pelo backend + de produção. +- Preservar `EnergyDataProvider` como fronteira da origem solar: esta sprint + usa `SimulationEnergyDataProvider`; provedores físicos futuros não precisam + ser implementados agora. +- Não alterar o esquema do banco para conveniência da apresentação. Se uma + mudança de esquema se mostrar indispensável, criar migration Alembic e teste. +- Não criar previsão artificial para completar o KPI de risco. A Fase 7 deverá + integrar dataset, baseline, treinamento, métricas e inferência nos contratos + existentes, sem interferir nas invariantes de potência. +- Incorporar na `main` somente o código e a documentação revisados, após + `make check`, ensaio da demo e revisão dos diagramas contra o código final. + +## Critério de fechamento da sprint + +A entrega estará pronta quando outra pessoa conseguir iniciar um ambiente +limpo, executar o seed e o roteiro, obter os resultados previstos, abrir as +telas e confirmar que cada conexão desenhada corresponde a uma chamada ou +serviço implementado. Os materiais devem declarar as limitações atuais e +responder aos itens específicos do enunciado acadêmico da Sprint 3. diff --git a/docs/sprint-3/EVIDENCE.md b/docs/sprint-3/EVIDENCE.md new file mode 100644 index 0000000..4e9cf0e --- /dev/null +++ b/docs/sprint-3/EVIDENCE.md @@ -0,0 +1,13 @@ +# Evidência do ensaio da Sprint 3 + +Ensaio executado em 18/09/2026 com banco PostgreSQL isolado, migrations Alembic, seed executado duas vezes e API temporária usando relógio inicial `2026-09-18T11:58:00Z`. Todos os números abaixo são **simulados**. O arquivo bruto do roteiro foi gerado fora do Git em `/tmp/chargegrid-sprint3-evidence.txt`. + +| Tick UTC | Sessões | Alocação | Solar | Rede | Energia do intervalo | +| --- | ---: | ---: | ---: | ---: | ---: | +| 11:58 | 3 | 3 × 20 = 60 kW | 0 kW | 60 kW | 1 kWh | +| 11:59 | 4 | 4 × 15 = 60 kW | 0 kW | 60 kW | 1 kWh | +| 12:00 | 4 | 4 × 20 = 80 kW | 20 kW | 60 kW | 1,3333 kWh | + +O roteiro retornou alerta `HIGH_DEMAND`. A quarta sessão foi encerrada e recebeu invoice `CLOSED` de **R$ 0,47** com tarifa seed de **R$ 0,8000/kWh**. A API de sustentabilidade retornou **0,1333 kg de CO₂ evitado**, usando **0,4 kg/kWh** configurado em `GRID_EMISSION_FACTOR_KG_PER_KWH`. O dashboard administrativo passou a mostrar faturamento de R$ 0,47, e o dashboard do usuário passou a listar a invoice da sessão concluída. Esses valores resultam da precisão e do arredondamento implementados pela API. + +`make check` passou: 179 testes backend, 14 testes frontend, Ruff, mypy, ESLint, TypeScript e build Vite. Capturas visuais da API e dos dashboards ainda devem ser feitas no ambiente de apresentação; não são atribuídas a este ensaio automatizado. diff --git a/docs/sprint-3/README.md b/docs/sprint-3/README.md new file mode 100644 index 0000000..6cd57dd --- /dev/null +++ b/docs/sprint-3/README.md @@ -0,0 +1,54 @@ +# Sprint 3 — demonstração reproduzível + +**Equipe:** Equipe 3 (FIAP × GoodWe) +**Disciplina:** Pensamento Computacional e Automação com Python + +**Integrantes:** + +- Bernardo Zauza Amorim +- Bruno Almeida de Oliveira +- Gabriel Góes Nunes Pereira +- Guilherme Vinciguerra Carvalho +- Marcos Peterson Martins Pereira +- Matheus Jorge Santana + +Este roteiro demonstra, por simulação, o Golden Path de `SPEC.md` §§ 54–55. As leituras solares e de energia são **simuladas**; não representam medições físicas. Billing e invoice também são simulados. A previsão automática e o risco de pico dependem da Fase 7; o dashboard deixa esse KPI sem valor quando não há previsão válida. + +## Arquitetura e escolhas + +- [Arquitetura executada em PNG](diagrams/architecture.png) ([fonte Mermaid](architecture.mmd)) e [sequência da demo em PNG](diagrams/sequence.png) ([fonte Mermaid](sequence.mmd)) representam o fluxo conferido contra a API. +- O monólito modular usa React, FastAPI, SQLAlchemy e PostgreSQL. O controlador de simulação recebe ticks manuais; `SimulationEnergyDataProvider` fornece a curva solar UTC. O alocador Equal Share respeita os limites de rede, carregador e veículo. A API calcula billing e analytics. +- A simulação não possui agendador, controle físico, OCPP/Modbus nem modelo ML treinado. O script apenas orquestra endpoints públicos. + +## Reprodução em banco de demo limpo + +Use um banco isolado e um único processo controlador da API. Edite `.env` localmente para definir `APP_ENV=development`, `DEMO_SIMULATION_START_UTC=2026-09-18T11:58:00Z` e `GRID_EMISSION_FACTOR_KG_PER_KWH=0.4`. O horário é deliberadamente próximo do pico solar UTC. O fator da API vem dessa variável de ambiente; o seed registra o mesmo 0,4 em `SystemConfiguration`. Guarde `DEMO_ADMIN_PASSWORD` e `DEMO_USER_PASSWORD` somente no ambiente, com pelo menos oito caracteres cada. + +```bash +cp .env.example .env +# edite .env conforme acima; não a adicione ao Git +read -rsp 'Senha ADMIN da demo: ' DEMO_ADMIN_PASSWORD; echo +read -rsp 'Senha USER da demo: ' DEMO_USER_PASSWORD; echo +export DEMO_ADMIN_PASSWORD DEMO_USER_PASSWORD +docker compose up --build --wait -d +docker compose exec -e DEMO_ADMIN_PASSWORD -e DEMO_USER_PASSWORD backend python -m app.demo_seed +DEMO_ADMIN_PASSWORD="$DEMO_ADMIN_PASSWORD" DEMO_USER_PASSWORD="$DEMO_USER_PASSWORD" python3 scripts/sprint3_demo.py | tee /tmp/chargegrid-sprint3-evidence.txt +``` + +O seed aceita reexecução sem duplicar entidades. O roteiro exige banco limpo e sessão de simulação parada, pois cria sessões e leituras novas. O relógio configurado é opt-in e só vale em desenvolvimento/demo/teste; sem ele, inicia no horário atual. Reinicie a API após mudar `.env`. + +Confira os [resultados do ensaio](EVIDENCE.md). O roteiro imprime cada leitura e total por tick, alertas, dashboards, invoice e sustentabilidade. Resultados esperados: primeiro tick, três sessões a 20 kW; segundo, quatro sessões a 15 kW e 60 kW da rede; terceiro, aproximadamente 20 kW solares + 60 kW da rede, totalizando 80 kW. Cada tick representa um minuto. A tarifa seed é R$ 0,8000/kWh, e o fator de emissão é 0,4 kg/kWh. O valor final da invoice e CO₂ evitado devem ser conferidos na saída, considerando os arredondamentos da API. + +Abra [OpenAPI](http://localhost:8000/docs), [dashboard administrativo](http://localhost:5173/admin) e [dashboard do usuário](http://localhost:5173/user). Entre com as contas `sprint3-admin@demo.invalid` e `sprint3-user-4@demo.invalid`; ambas usam as senhas definidas no ambiente. Atualize manualmente as telas após o roteiro. Capture prints da API, gráficos, histórico, alerta, invoice e sustentabilidade para o PDF/vídeo. Ainda não há prints versionados, pois dependem do ensaio visual no ambiente de entrega. + +## Roteiro do vídeo (até cinco minutos) + +1. Mostrar a arquitetura e explicar tick manual e solar simulada. +2. Iniciar três sessões e mostrar 3 × 20 kW. +3. Iniciar a quarta e mostrar o rateio 4 × 15 kW, limitado a 60 kW da rede. +4. Configurar 20 kW de pico solar pela API e mostrar o terceiro tick de cerca de 80 kW. +5. Mostrar leituras, alerta e dashboards atualizados. +6. Encerrar a quarta sessão, mostrar invoice, tarifa e CO₂ evitado. +7. Explicar os limites: sem equipamento real e sem previsão automática de demanda. + +Esta entrega do EV Challenge 2026 aplica Pensamento Computacional e Automação com Python à modelagem do cenário, ao controlador de simulação, ao seed reproduzível, à orquestração das chamadas HTTP e à validação automática dos limites energéticos. Inclua as evidências visuais capturadas antes da apresentação. diff --git a/docs/sprint-3/architecture.mmd b/docs/sprint-3/architecture.mmd new file mode 100644 index 0000000..e7cc914 --- /dev/null +++ b/docs/sprint-3/architecture.mmd @@ -0,0 +1,14 @@ +flowchart LR + B[Navegador React] -->|HTTP /api/v1| A[API REST FastAPI] + A --> S[Serviço de sessões] + A --> C[Controlador de simulação: tick manual] + A --> N[Analytics e dashboards] + S --> E[Billing simulado] + C --> P[Alocador de energia] + D[Provedor solar simulado] --> C + S --> O[SQLAlchemy] + C --> O + E --> O + N --> O + O --> DB[(PostgreSQL)] + N -->|resposta da API| A diff --git a/docs/sprint-3/chargegrid_sprint_3.md b/docs/sprint-3/chargegrid_sprint_3.md new file mode 100644 index 0000000..25f8e45 --- /dev/null +++ b/docs/sprint-3/chargegrid_sprint_3.md @@ -0,0 +1,195 @@ +# ChargeGrid Intelligence + +> Integração, automação e eficiência para recarga elétrica + +**Relatório técnico do protótipo simulado** +**Disciplina:** Pensamento Computacional e Automação com Python +**Equipe:** Equipe 3 - FIAP × GoodWe +**Sprint:** 3 +**Data:** 21 de setembro de 2026 + +## Visão executiva + +### Uma plataforma que transforma recarga em decisão energética + +O **ChargeGrid Intelligence** integra sessões de recarga, gestão de demanda, prioridade solar, dados, cobrança e indicadores ambientais em um monólito modular demonstrável. Nesta Sprint 3, a integração é comprovada por um cenário automatizado e reproduzível, com componentes físicos substituídos por provedores simulados explicitamente identificados. + +> **Demonstração integrada:** sessão → alocação → solar/rede → leitura → alerta → billing → ESG + +| Entregável | Evidência no protótipo | +| --- | --- | +| Integração | React → API FastAPI → serviços → SQLAlchemy/PostgreSQL | +| Automação | Seed idempotente, roteiro HTTP e ticks manuais determinísticos | +| Eficiência | Limite de 60 kW da rede e rateio Equal Share | +| Sustentabilidade | Prioridade solar e cálculo de CO₂ evitado | +| Qualidade | 179 testes backend, 14 frontend, lint, tipos e build aprovados no ensaio registrado | + +### Escopo e transparência + +As grandezas apresentadas são **simuladas**. Não houve medição em carregador, inversor ou medidor físico. Não há OCPP/Modbus, agendador de ticks ou modelo de ML treinado nesta entrega; o risco de pico preditivo permanece para fase posterior. Essa delimitação preserva a rastreabilidade técnica do MVP. + +## Equipe e contexto + +### Equipe 3 - FIAP × GoodWe + +| Nome completo | RM | +| --- | ---: | +| Bernardo Zauza Amorim | 568808 | +| Bruno Almeida de Oliveira | 572648 | +| Gabriel Góes Nunes Pereira | 571735 | +| Guilherme Vinciguerra Carvalho | 571951 | +| Marcos Peterson Martins Pereira | 573857 | +| Matheus Jorge Santana | 574166 | + +### Problema abordado + +Quando quatro veículos solicitam 20 kW cada, a demanda chega a 80 kW, acima dos 60 kW disponíveis da rede. O sistema precisa limitar a importação, dividir potência de forma previsível, aproveitar solar antes da rede e registrar consequências operacionais, financeiras e ambientais. + +## Arquitetura executada + +### Integração dos componentes + +O navegador React consome contratos REST sob `/api/v1`. A API FastAPI coordena serviços de sessão, simulação, energia, billing e analytics. O SQLAlchemy persiste os dados em PostgreSQL. O provedor solar é intercambiável e, nesta sprint, entrega uma curva determinística simulada. + +![Arquitetura executada do ChargeGrid Intelligence](diagrams/architecture.png) + +| Componente | Responsabilidade | Contribuição | +| --- | --- | --- | +| React + TypeScript | Dashboards e interação | Visibilidade operacional e atualização manual | +| FastAPI + Pydantic | Contratos e orquestração | Automação rastreável com validação | +| Serviços de domínio | Sessões, alocação, billing e ESG | Regras centralizadas e testáveis | +| Simulador | Relógio, curva solar e ticks | Ensaio seguro sem hardware | +| PostgreSQL + SQLAlchemy | Persistência e consultas | Histórico para auditoria e análise | + +> **ML é consultivo:** previsões futuras nunca poderão violar os limites determinísticos de energia. + +## Fluxo ponta a ponta + +### Sequência funcional da demonstração + +![Sequência funcional da demonstração](diagrams/sequence.png) + +Cada seta corresponde a uma rota, serviço ou persistência existente. O tick é manual; o script apenas encadeia chamadas públicas. + +## Aplicação em execução + +### Dashboard administrativo: energia e sustentabilidade + +![Dashboard do gestor com KPIs e gráficos](screenshots/captura_dashboard_gestor_kpis_graficos.png) + +*Figura 3. Captura real do dashboard do gestor após a execução do cenário simulado da Sprint 3. A interface consolida demanda, limite da rede, utilização solar, sessões, faturamento e indicadores ambientais.* + +Após o encerramento da quarta sessão, três recargas permanecem ativas: a demanda total é 60 kW, atendida por 15 kW solares e 45 kW da rede. Os gráficos preservam o histórico do cenário anterior, no qual a entrega chegou a 80 kW com quatro sessões. + +## Evidência operacional + +### Sessões, histórico e alertas + +![Dashboard do gestor com operação e alertas](screenshots/captura_dashboard_gestor_operacao_alertas.png) + +*Figura 4. Recorte da mesma captura real, com sessões, faturamento, leituras persistidas e alertas operacionais.* + +O histórico registra a evolução de 20 kW por sessão para o rateio de 15 kW e, depois, a contribuição solar de 5 kW por sessão. O alerta **High grid demand** confirma que a importação atingiu o limite configurado de 60 kW. + +## Resultados funcionais + +### Medições reproduzidas pelo roteiro + +| Tick UTC | Sessões | Alocação | Solar | Rede | Energia | +| --- | ---: | --- | ---: | ---: | ---: | +| 11:58 | 3 | 3 × 20 = 60 kW | 0 kW | 60 kW | 1,0000 kWh | +| 11:59 | 4 | 4 × 15 = 60 kW | 0 kW | 60 kW | 1,0000 kWh | +| 12:00 | 4 | 4 × 20 = 80 kW | 20 kW | 60 kW | 1,3333 kWh | + +| Indicador | Resultado | Interpretação | +| --- | --- | --- | +| Alerta | `HIGH_DEMAND` | Demanda elevada registrada para decisão do gestor | +| Invoice | `CLOSED` - R$ 0,47 | Billing pay-per-use com tarifa de R$ 0,8000/kWh | +| CO₂ evitado | 0,1333 kg | Solar utilizada × fator de 0,4 kg/kWh | +| Validação | 179 + 14 testes | Backend e frontend, além de lint, tipos e build | + +## Justificativas técnicas + +### Escolhas orientadas a correção e demonstração + +| Escolha | Justificativa | +| --- | --- | +| Monólito modular | Reduz infraestrutura e mantém fronteiras claras entre API, domínio, simulação, billing e analytics. | +| Equal Share | É determinístico, simples de explicar e distribui o recurso escasso igualmente, respeitando limites individuais. | +| Solar priorizada | Reduz importação da rede e torna explícito o aproveitamento renovável em cada leitura. | +| Ticks manuais | Permitem repetir o cenário minuto a minuto e inspecionar o efeito de cada decisão. | +| PostgreSQL + migrations | Garantem persistência estruturada, histórico e evolução de esquema reproduzível. | +| UTC + Decimal | UTC evita ambiguidade temporal; tipos decimais preservam valores monetários. | +| API tipada | Pydantic e TypeScript reduzem inconsistências entre backend e frontend. | +| Simulação desacoplada | `EnergyDataProvider` permite substituir a fonte simulada por integração física futura sem mover regras críticas. | + +> **Invariante central:** potência alocada nunca é negativa nem excede pedido, carregador, veículo ou limite da rede. + +## Sustentabilidade e automação + +### Como cada tecnologia gera valor + +| Dimensão | Mecanismo | Efeito demonstrado | +| --- | --- | --- | +| Sustentabilidade | Prioridade solar + segregação solar/rede + fator de emissão | 20 kW solares no terceiro tick e 0,1333 kg de CO₂ evitado na API | +| Automação inteligente | Seed, script HTTP, controlador, alertas e regras determinísticas | Cenário inteiro repetível sem intervenção no banco durante o fluxo | +| Eficiência energética | Limite de rede e rateio Equal Share | Quatro sessões atendidas sem exceder 60 kW de importação | +| Eficiência operacional | Dashboards, histórico e billing integrados | Estado energético convertido em alerta, invoice e indicadores | + +### Cadeia de valor + +**Recarga → dados → informação → inteligência → decisão energética.** A plataforma não se limita a registrar consumo: ela relaciona restrição elétrica, fonte energética, evento operacional, custo e impacto ambiental. Essa integração é o núcleo da proposta de valor. + +### Limites e evolução responsável + +O protótipo não comanda potência física. A futura conexão com equipamentos deve entrar pela fronteira do provedor de dados/integração, mantendo os serviços determinísticos como autoridade sobre segurança energética. Um modelo preditivo poderá estimar demanda e classificar risco, mas terá caráter consultivo. + +## Conexão com a disciplina + +### Pensamento Computacional e Automação com Python + +| Conteúdo | Aplicação no ChargeGrid | +| --- | --- | +| Decomposição | Separação em sessões, energia, simulação, billing, analytics e persistência | +| Abstração | Modelos de usuário, veículo, carregador, estação, leitura e invoice | +| Algoritmos | Cálculo de potência solicitada, Equal Share, prioridade solar, energia, custo e ESG | +| Automação em Python | FastAPI, controlador de ticks, seed idempotente e roteiro de demonstração | +| Estruturas e persistência | Schemas Pydantic, entidades SQLAlchemy e PostgreSQL | +| Testes e validação | Invariantes energéticas, transições, APIs e fluxo integrado automatizados | +| Dados e tomada de decisão | Dashboards, alerta `HIGH_DEMAND`, histórico, cobrança e sustentabilidade | + +### Síntese acadêmica + +O projeto materializa o pensamento computacional ao decompor um problema físico e multidimensional em dados, regras e interfaces verificáveis. A automação em Python coordena o experimento, enquanto os testes convertem requisitos energéticos em critérios objetivos. Assim, o software demonstra sustentabilidade e eficiência sem depender de uma alegação de hardware inexistente. + +## Reprodução e rastreabilidade + +### Como verificar a demonstração + +1. Configurar ambiente de demonstração isolado, relógio UTC opt-in e fator de emissão de 0,4 kg/kWh. +2. Subir os serviços via Docker Compose e aplicar migrations. +3. Executar o seed idempotente com senhas fornecidas apenas por variáveis de ambiente. +4. Rodar `scripts/sprint3_demo.py`, que usa endpoints públicos. +5. Conferir OpenAPI, dashboards, leituras, alerta, invoice e sustentabilidade. +6. Executar `make check` para validar backend e frontend. + +> As credenciais permanecem fora do Git; a demonstração deve usar banco limpo e um único controlador de simulação. + +### Fontes internas consultadas + +- `SPEC.md` - fonte de verdade técnica e regras; +- `BRIEFING.md` - visão, disciplina e organização; +- `docs/SPRINT_3_PLAN.md`; +- `docs/sprint-3/README.md`; +- `docs/sprint-3/EVIDENCE.md`; +- diagramas Mermaid/PNG; +- código e testes do backend/frontend. + +## Conclusão + +A Sprint 3 apresenta um protótipo simulado integrado, rastreável e demonstrável. O sistema mantém a rede em seu limite, redistribui potência, incorpora energia solar, persiste leituras, emite alerta, encerra sessão, calcula cobrança e apresenta indicador ambiental. O resultado conecta automação inteligente, sustentabilidade e eficiência energética em um único fluxo coerente. + +--- + +**ChargeGrid Intelligence** +*Transformando cada recarga em inteligência acionável.* diff --git a/docs/sprint-3/diagrams/architecture.png b/docs/sprint-3/diagrams/architecture.png new file mode 100644 index 0000000..5700e52 Binary files /dev/null and b/docs/sprint-3/diagrams/architecture.png differ diff --git a/docs/sprint-3/diagrams/sequence.png b/docs/sprint-3/diagrams/sequence.png new file mode 100644 index 0000000..bce4f16 Binary files /dev/null and b/docs/sprint-3/diagrams/sequence.png differ diff --git a/docs/sprint-3/screenshots/captura_dashboard_gestor.png b/docs/sprint-3/screenshots/captura_dashboard_gestor.png new file mode 100644 index 0000000..6afe295 Binary files /dev/null and b/docs/sprint-3/screenshots/captura_dashboard_gestor.png differ diff --git a/docs/sprint-3/screenshots/captura_dashboard_gestor_kpis_graficos.png b/docs/sprint-3/screenshots/captura_dashboard_gestor_kpis_graficos.png new file mode 100644 index 0000000..c0a8059 Binary files /dev/null and b/docs/sprint-3/screenshots/captura_dashboard_gestor_kpis_graficos.png differ diff --git a/docs/sprint-3/screenshots/captura_dashboard_gestor_operacao_alertas.png b/docs/sprint-3/screenshots/captura_dashboard_gestor_operacao_alertas.png new file mode 100644 index 0000000..21fe41e Binary files /dev/null and b/docs/sprint-3/screenshots/captura_dashboard_gestor_operacao_alertas.png differ diff --git a/docs/sprint-3/sequence.mmd b/docs/sprint-3/sequence.mmd new file mode 100644 index 0000000..aa4dd4b --- /dev/null +++ b/docs/sprint-3/sequence.mmd @@ -0,0 +1,29 @@ +sequenceDiagram + participant U as USER + participant A as ADMIN + participant API as API FastAPI + participant C as Controlador de simulação + participant S as Provedor solar simulado + participant P as Alocador Equal Share + participant DB as PostgreSQL + U->>API: POST /sessions/start (3, depois 4) + API->>DB: grava sessões + A->>API: POST /simulation/ticks (manual) + API->>C: execute_tick + C->>DB: consulta sessões ativas + C->>S: consulta solar disponível + C->>P: resolve potência e rateio solar + C->>DB: grava leituras e alerta + API-->>A: valores do tick + A->>API: PATCH /stations/{id} (pico solar 20 kW) + A->>API: POST /simulation/ticks (manual, com solar) + API->>C: execute_tick com novo pico + C->>S: consulta curva solar UTC + C->>P: calcula 20 kW solar + 60 kW rede + C->>DB: grava leituras do terceiro tick + U->>API: GET /user/dashboard + A->>API: GET /analytics/dashboard e /alerts + API->>DB: consulta dados persistidos + U->>API: POST /sessions/{id}/stop + API->>DB: encerra sessão e cria invoice simulada + U->>API: GET /billing/invoices diff --git a/frontend/src/pages/UserDashboardPage.tsx b/frontend/src/pages/UserDashboardPage.tsx index 6816d50..d498476 100644 --- a/frontend/src/pages/UserDashboardPage.tsx +++ b/frontend/src/pages/UserDashboardPage.tsx @@ -27,8 +27,8 @@ export function UserDashboardPage() { return

Área do usuário

Minha recarga

Olá, {state.status === 'authenticated' ? state.user.name : ''}

{error ?

Não foi possível carregar o dashboard.

: !data ?

Carregando dashboard...

: <> -

Sessão atual

{current ? <>

{current.vehicle_name} · {current.charger_name} · {statusLabel[current.status]}

:

Nenhuma recarga em andamento.

}
-

Histórico de sessões

{data.session_history.length ?
{data.session_history.map(session => )}
InícioVeículoCarregadorStatusDuraçãoEnergiaSolarCusto final
{date(session.started_at)}{session.vehicle_name}{session.charger_name}{statusLabel[session.status]}{duration(session.duration_seconds)}{number(session.energy_consumed_kwh)} kWh{number(session.solar_percentage)}%{session.invoice_total === null ? '—' : money(session.invoice_total)}
:

Nenhuma sessão anterior.

}
+

Sessão atual

{current ? <>

{current.vehicle_name} · {current.charger_name} · {statusLabel[current.status]}

:

Nenhuma recarga em andamento.

}
+

Histórico de sessões

{data.session_history.length ?
{data.session_history.map(session => )}
InícioVeículoCarregadorStatusTempo realEnergiaSolarCusto final
{date(session.started_at)}{session.vehicle_name}{session.charger_name}{statusLabel[session.status]}{duration(session.duration_seconds)}{number(session.energy_consumed_kwh)} kWh{number(session.solar_percentage)}%{session.invoice_total === null ? '—' : money(session.invoice_total)}
:

Nenhuma sessão anterior.

}

Invoices

{data.invoices.length ?
{data.invoices.map(invoice => )}
DataSessãoStatusEnergiaValor final
{date(invoice.closed_at ?? invoice.created_at)}{invoice.session_id}{invoice.status === 'CLOSED' ? 'Fechada' : invoice.status === 'OPEN' ? 'Aberta' : 'Cancelada'}{number(Number(invoice.energy_kwh))} kWh{invoice.status === 'CLOSED' ? money(invoice.total) : '—'}
:

Nenhuma invoice disponível.

}
}
diff --git a/output/pdf/chargegrid_sprint_3.pdf b/output/pdf/chargegrid_sprint_3.pdf new file mode 100644 index 0000000..2a8b9ad Binary files /dev/null and b/output/pdf/chargegrid_sprint_3.pdf differ diff --git a/scripts/build_sprint3_pdf.py b/scripts/build_sprint3_pdf.py new file mode 100644 index 0000000..7de101c --- /dev/null +++ b/scripts/build_sprint3_pdf.py @@ -0,0 +1,178 @@ +from __future__ import annotations + +from pathlib import Path + +from reportlab.graphics.charts.barcharts import VerticalBarChart +from reportlab.graphics.charts.legends import Legend +from reportlab.graphics.shapes import Drawing, Rect, String +from reportlab.lib import colors +from reportlab.lib.colors import HexColor +from reportlab.lib.enums import TA_CENTER, TA_LEFT +from reportlab.lib.pagesizes import A4, landscape +from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet +from reportlab.lib.units import cm +from reportlab.platypus import ( + BaseDocTemplate, Flowable, Frame, Image, KeepTogether, NextPageTemplate, + PageBreak, PageTemplate, Paragraph, Spacer, Table, TableStyle, +) + +ROOT = Path(__file__).resolve().parents[1] +OUT = ROOT / "output" / "pdf" / "chargegrid_sprint_3.pdf" +ARCH = ROOT / "docs" / "sprint-3" / "diagrams" / "architecture.png" +SEQ = ROOT / "docs" / "sprint-3" / "diagrams" / "sequence.png" +DASHBOARD_TOP = ROOT / "docs" / "sprint-3" / "screenshots" / "captura_dashboard_gestor_kpis_graficos.png" +DASHBOARD_BOTTOM = ROOT / "docs" / "sprint-3" / "screenshots" / "captura_dashboard_gestor_operacao_alertas.png" + +NAVY = HexColor("#071C2C") +NAVY2 = HexColor("#0D2B3E") +GREEN = HexColor("#35D39A") +MINT = HexColor("#B9F5DE") +CYAN = HexColor("#4CC9F0") +ORANGE = HexColor("#FFB45B") +RED = HexColor("#FF6B6B") +INK = HexColor("#132A36") +MUTED = HexColor("#58717E") +PAPER = HexColor("#F5F8F7") +LINE = HexColor("#D6E2E0") +WHITE = colors.white + + +def footer(canvas, doc): + canvas.saveState() + canvas.setFillColor(NAVY) + canvas.rect(0, 0, doc.pagesize[0], 1.15 * cm, fill=1, stroke=0) + canvas.setFont("Helvetica", 7.5) + canvas.setFillColor(MINT) + canvas.drawString(1.5 * cm, 0.43 * cm, "CHARGEGRID INTELLIGENCE | SPRINT 3 | PROTÓTIPO SIMULADO") + canvas.drawRightString(doc.pagesize[0] - 1.5 * cm, 0.43 * cm, f"{doc.page}") + canvas.restoreState() + + +class AccentBox(Flowable): + def __init__(self, text, color=GREEN, width=17.5 * cm): + super().__init__(); self.text=text; self.color=color; self.width=width; self.height=1.35*cm + def draw(self): + self.canv.setFillColor(HexColor("#EAF5F1")); self.canv.roundRect(0,0,self.width,self.height,7,fill=1,stroke=0) + self.canv.setFillColor(self.color); self.canv.rect(0,0,0.14*cm,self.height,fill=1,stroke=0) + self.canv.setFillColor(INK); self.canv.setFont("Helvetica-Bold",9) + self.canv.drawString(.42*cm,.51*cm,self.text) + + +def styles(): + s = getSampleStyleSheet() + s.add(ParagraphStyle(name="Kicker", fontName="Helvetica-Bold", fontSize=8, leading=10, textColor=GREEN, spaceAfter=7, tracking=1.4)) + s.add(ParagraphStyle(name="H1x", fontName="Helvetica-Bold", fontSize=25, leading=28, textColor=NAVY, spaceAfter=12)) + s.add(ParagraphStyle(name="H2x", fontName="Helvetica-Bold", fontSize=16, leading=19, textColor=NAVY, spaceBefore=4, spaceAfter=9)) + s.add(ParagraphStyle(name="H3x", fontName="Helvetica-Bold", fontSize=10.5, leading=13, textColor=INK, spaceBefore=6, spaceAfter=4)) + s.add(ParagraphStyle(name="Bodyx", fontName="Helvetica", fontSize=9, leading=13.2, textColor=INK, spaceAfter=7)) + s.add(ParagraphStyle(name="Smallx", fontName="Helvetica", fontSize=7.5, leading=10.5, textColor=MUTED, spaceAfter=5)) + s.add(ParagraphStyle(name="Callout", fontName="Helvetica-Bold", fontSize=12, leading=16, textColor=NAVY, alignment=TA_CENTER, spaceAfter=8)) + s.add(ParagraphStyle(name="Cover", fontName="Helvetica-Bold", fontSize=31, leading=34, textColor=WHITE, spaceAfter=12)) + s.add(ParagraphStyle(name="CoverSub", fontName="Helvetica", fontSize=13, leading=18, textColor=MINT, spaceAfter=8)) + s.add(ParagraphStyle(name="Cell", fontName="Helvetica", fontSize=7.7, leading=10, textColor=INK)) + s.add(ParagraphStyle(name="CellHead", fontName="Helvetica-Bold", fontSize=7.7, leading=9, textColor=WHITE)) + return s + + +S = styles() + + +def table(data, widths, header=True): + t = Table(data, colWidths=widths, repeatRows=1 if header else 0, hAlign="LEFT") + commands=[("VALIGN",(0,0),(-1,-1),"TOP"),("GRID",(0,0),(-1,-1),.35,LINE),("LEFTPADDING",(0,0),(-1,-1),7),("RIGHTPADDING",(0,0),(-1,-1),7),("TOPPADDING",(0,0),(-1,-1),6),("BOTTOMPADDING",(0,0),(-1,-1),6)] + if header: + commands += [("BACKGROUND",(0,0),(-1,0),NAVY2),("TEXTCOLOR",(0,0),(-1,0),WHITE)] + for r in range(1 if header else 0, len(data)): + if r % 2 == 0: commands.append(("BACKGROUND",(0,r),(-1,r),HexColor("#EEF4F2"))) + t.setStyle(TableStyle(commands)); return t + + +def p(text, style="Bodyx"): return Paragraph(text, S[style]) +def cell(text, head=False): return Paragraph(text, S["CellHead" if head else "Cell"]) + + +def energy_chart(): + d=Drawing(500,210) + d.add(Rect(0,0,500,210,rx=10,ry=10,fillColor=WHITE,strokeColor=LINE)) + c=VerticalBarChart(); c.x=55; c.y=42; c.height=125; c.width=390 + c.data=[[60,60,60],[0,0,20]]; c.categoryAxis.categoryNames=["11:58 | 3 sessões","11:59 | 4 sessões","12:00 | 4 sessões"] + c.valueAxis.valueMin=0; c.valueAxis.valueMax=90; c.valueAxis.valueStep=20 + c.bars[0].fillColor=CYAN; c.bars[1].fillColor=GREEN; c.bars.strokeColor=None + c.categoryAxis.labels.fontSize=7; c.valueAxis.labels.fontSize=7 + d.add(c); d.add(String(18,184,"Potência por fonte em cada tick (kW)",fontName="Helvetica-Bold",fontSize=11,fillColor=NAVY)) + leg=Legend(); leg.x=330; leg.y=190; leg.fontSize=7; leg.colorNamePairs=[(CYAN,"Rede"),(GREEN,"Solar")]; d.add(leg) + return d + + +def dashboard_visual(): + d=Drawing(500,260) + d.add(Rect(0,0,500,260,rx=12,ry=12,fillColor=NAVY,strokeColor=None)) + d.add(String(18,235,"VISUALIZAÇÃO DO ENSAIO SIMULADO",fontName="Helvetica-Bold",fontSize=8,fillColor=GREEN)) + d.add(String(18,214,"Operação energética integrada",fontName="Helvetica-Bold",fontSize=17,fillColor=WHITE)) + cards=[(18,"80 kW","potência total",GREEN),(136,"60 kW","rede (limite)",CYAN),(254,"20 kW","solar",ORANGE),(372,"HIGH","alerta",RED)] + for x,val,label,col in cards: + d.add(Rect(x,150,105,48,rx=7,ry=7,fillColor=NAVY2,strokeColor=HexColor("#24495A"))) + d.add(String(x+9,177,val,fontName="Helvetica-Bold",fontSize=14,fillColor=col)); d.add(String(x+9,160,label,fontName="Helvetica",fontSize=7,fillColor=MINT)) + d.add(Rect(18,35,459,94,rx=7,ry=7,fillColor=HexColor("#0A2233"),strokeColor=HexColor("#24495A"))) + vals=[20,15,20]; labels=["Tick 1\n3 × 20 kW","Tick 2\n4 × 15 kW","Tick 3\n4 × 20 kW"] + for i,v in enumerate(vals): + x=55+i*140; h=v*3.2 + d.add(Rect(x,55,42,h,fillColor=GREEN,strokeColor=None)); d.add(String(x+8,59+h,f"{v} kW",fontName="Helvetica-Bold",fontSize=8,fillColor=WHITE)) + d.add(String(x-3,43,labels[i].replace("\n"," | "),fontName="Helvetica",fontSize=6.5,fillColor=MINT)) + return d + + +def cover(canvas, doc): + canvas.saveState(); w,h=doc.pagesize + canvas.setFillColor(NAVY); canvas.rect(0,0,w,h,fill=1,stroke=0) + canvas.setFillColor(GREEN); canvas.circle(w-2.5*cm,h-2.7*cm,4.2*cm,fill=1,stroke=0) + canvas.setFillColor(NAVY2); canvas.circle(w-2.5*cm,h-2.7*cm,3.35*cm,fill=1,stroke=0) + canvas.setStrokeColor(GREEN); canvas.setLineWidth(5) + canvas.line(1.55*cm,6.0*cm,w-1.55*cm,6.0*cm) + canvas.setFont("Helvetica-Bold",8); canvas.setFillColor(GREEN); canvas.drawString(1.55*cm,h-2.0*cm,"FIAP × GOODWE | EQUIPE 3 | SPRINT 3") + canvas.setFont("Helvetica-Bold",31); canvas.setFillColor(WHITE); canvas.drawString(1.55*cm,h-7.0*cm,"ChargeGrid") + canvas.drawString(1.55*cm,h-8.25*cm,"Intelligence") + canvas.setFont("Helvetica",12); canvas.setFillColor(MINT); canvas.drawString(1.55*cm,h-9.3*cm,"Integração, automação e eficiência para recarga elétrica") + canvas.setFont("Helvetica-Bold",10); canvas.setFillColor(WHITE); canvas.drawString(1.55*cm,4.85*cm,"RELATÓRIO TÉCNICO DO PROTÓTIPO SIMULADO") + canvas.setFont("Helvetica",8); canvas.setFillColor(MINT); canvas.drawString(1.55*cm,4.25*cm,"Pensamento Computacional e Automação com Python") + canvas.drawString(1.55*cm,3.75*cm,"21 de setembro de 2026") + canvas.restoreState() + + +def build(): + OUT.parent.mkdir(parents=True, exist_ok=True) + doc=BaseDocTemplate(str(OUT),pagesize=A4,rightMargin=1.55*cm,leftMargin=1.55*cm,topMargin=1.35*cm,bottomMargin=1.55*cm,title="ChargeGrid Intelligence - Sprint 3",author="Equipe 3") + portrait=Frame(doc.leftMargin,doc.bottomMargin,doc.width,doc.height,id="portrait") + land=Frame(1.35*cm,1.45*cm,landscape(A4)[0]-2.7*cm,landscape(A4)[1]-2.7*cm,id="land") + doc.addPageTemplates([PageTemplate(id="cover",pagesize=A4,onPage=cover,frames=[portrait]),PageTemplate(id="portrait",pagesize=A4,onPage=footer,frames=[portrait]),PageTemplate(id="landscape",pagesize=landscape(A4),onPage=footer,frames=[land])]) + story=[Spacer(1,23*cm),NextPageTemplate("portrait"),PageBreak()] + story += [p("VISÃO EXECUTIVA","Kicker"),p("Uma plataforma que transforma recarga em decisão energética","H1x"),p("O ChargeGrid Intelligence integra sessões de recarga, gestão de demanda, prioridade solar, dados, cobrança e indicadores ambientais em um monólito modular demonstrável. Nesta Sprint 3, a integração é comprovada por um cenário automatizado e reproduzível, com componentes físicos substituídos por provedores simulados explicitamente identificados.")] + story += [AccentBox("DEMONSTRAÇÃO INTEGRADA: sessão → alocação → solar/rede → leitura → alerta → billing → ESG"),Spacer(1,.35*cm)] + story += [table([[cell("Entregável",True),cell("Evidência no protótipo",True)],[cell("Integração"),cell("React → API FastAPI → serviços → SQLAlchemy/PostgreSQL")],[cell("Automação"),cell("Seed idempotente, roteiro HTTP e ticks manuais determinísticos")],[cell("Eficiência"),cell("Limite de 60 kW da rede e rateio Equal Share")],[cell("Sustentabilidade"),cell("Prioridade solar e cálculo de CO₂ evitado")],[cell("Qualidade"),cell("179 testes backend, 14 frontend, lint, tipos e build aprovados no ensaio registrado")]], [4.2*cm,13.3*cm])] + story += [Spacer(1,.3*cm),p("Escopo e transparência","H2x"),p("As grandezas apresentadas são simuladas. Não houve medição em carregador, inversor ou medidor físico. Não há OCPP/Modbus, agendador de ticks ou modelo de ML treinado nesta entrega; o risco de pico preditivo permanece para fase posterior. Essa delimitação preserva a rastreabilidade técnica do MVP."),PageBreak()] + story += [p("EQUIPE E CONTEXTO","Kicker"),p("Equipe 3 - FIAP × GoodWe","H1x")] + members=[("Bernardo Zauza Amorim","568808"),("Bruno Almeida de Oliveira","572648"),("Gabriel Góes Nunes Pereira","571735"),("Guilherme Vinciguerra Carvalho","571951"),("Marcos Peterson Martins Pereira","573857"),("Matheus Jorge Santana","574166")] + rows=[[cell("Nome completo",True),cell("RM",True)]]+[[cell(name),cell(rm)] for name,rm in members] + story += [table(rows,[13.5*cm,4*cm]),Spacer(1,.4*cm),p("Problema abordado","H2x"),p("Quando quatro veículos solicitam 20 kW cada, a demanda chega a 80 kW, acima dos 60 kW disponíveis da rede. O sistema precisa limitar a importação, dividir potência de forma previsível, aproveitar solar antes da rede e registrar consequências operacionais, financeiras e ambientais."),PageBreak()] + story += [p("ARQUITETURA EXECUTADA","Kicker"),p("Integração dos componentes","H1x"),p("O navegador React consome contratos REST sob /api/v1. A API FastAPI coordena serviços de sessão, simulação, energia, billing e analytics. O SQLAlchemy persiste os dados em PostgreSQL. O provedor solar é intercambiável e, nesta sprint, entrega uma curva determinística simulada."),Image(str(ARCH),width=17.4*cm,height=3.73*cm),Spacer(1,.25*cm)] + story += [table([[cell("Componente",True),cell("Responsabilidade",True),cell("Contribuição",True)],[cell("React + TypeScript"),cell("Dashboards e interação"),cell("Visibilidade operacional e atualização manual")],[cell("FastAPI + Pydantic"),cell("Contratos e orquestração"),cell("Automação rastreável com validação")],[cell("Serviços de domínio"),cell("Sessões, alocação, billing e ESG"),cell("Regras centralizadas e testáveis")],[cell("Simulador"),cell("Relógio, curva solar e ticks"),cell("Ensaio seguro sem hardware")],[cell("PostgreSQL + SQLAlchemy"),cell("Persistência e consultas"),cell("Histórico para auditoria e análise")]], [4.0*cm,6.1*cm,7.3*cm]),Spacer(1,.25*cm),AccentBox("ML é consultivo: previsões futuras nunca poderão violar os limites determinísticos de energia."),NextPageTemplate("landscape"),PageBreak()] + story += [KeepTogether([p("FLUXO PONTA A PONTA","Kicker"),p("Sequência funcional da demonstração","H1x"),Image(str(SEQ),width=22.6*cm,height=14.51*cm),p("Cada seta corresponde a uma rota, serviço ou persistência existente. O tick é manual; o script apenas encadeia chamadas públicas.","Smallx")]),NextPageTemplate("portrait"),PageBreak()] + story += [p("APLICAÇÃO EM EXECUÇÃO","Kicker"),p("Dashboard administrativo: energia e sustentabilidade","H1x"),Image(str(DASHBOARD_TOP),width=17.2*cm,height=18.42*cm),Spacer(1,.15*cm),p("Figura 3. Captura real do dashboard do gestor após a execução do cenário simulado da Sprint 3. A interface consolida demanda, limite da rede, utilização solar, sessões, faturamento e indicadores ambientais.","Smallx"),p("Após o encerramento da quarta sessão, três recargas permanecem ativas: a demanda total é 60 kW, atendida por 15 kW solares e 45 kW da rede. Os gráficos preservam o histórico do cenário anterior, no qual a entrega chegou a 80 kW com quatro sessões."),PageBreak()] + story += [p("EVIDÊNCIA OPERACIONAL","Kicker"),p("Sessões, histórico e alertas","H1x"),Image(str(DASHBOARD_BOTTOM),width=16.5*cm,height=18.56*cm),Spacer(1,.15*cm),p("Figura 4. Recorte da mesma captura real, com sessões, faturamento, leituras persistidas e alertas operacionais.","Smallx"),p("O histórico registra a evolução de 20 kW por sessão para o rateio de 15 kW e, depois, a contribuição solar de 5 kW por sessão. O alerta High grid demand confirma que a importação atingiu o limite configurado de 60 kW."),PageBreak()] + story += [p("RESULTADOS FUNCIONAIS","Kicker"),p("Medições reproduzidas pelo roteiro","H1x"),energy_chart(),Spacer(1,.15*cm)] + data=[[cell(x,True) for x in ["Tick UTC","Sessões","Alocação","Solar","Rede","Energia"]],[cell("11:58"),cell("3"),cell("3 × 20 = 60 kW"),cell("0 kW"),cell("60 kW"),cell("1,0000 kWh")],[cell("11:59"),cell("4"),cell("4 × 15 = 60 kW"),cell("0 kW"),cell("60 kW"),cell("1,0000 kWh")],[cell("12:00"),cell("4"),cell("4 × 20 = 80 kW"),cell("20 kW"),cell("60 kW"),cell("1,3333 kWh")]] + story += [table(data,[2*cm,1.7*cm,4.3*cm,2.2*cm,2.2*cm,3.1*cm]),Spacer(1,.3*cm),table([[cell("Indicador",True),cell("Resultado",True),cell("Interpretação",True)],[cell("Alerta"),cell("HIGH_DEMAND"),cell("Demanda elevada registrada para decisão do gestor")],[cell("Invoice"),cell("CLOSED - R$ 0,47"),cell("Billing pay-per-use com tarifa de R$ 0,8000/kWh")],[cell("CO₂ evitado"),cell("0,1333 kg"),cell("Solar utilizada × fator de 0,4 kg/kWh")],[cell("Validação"),cell("179 + 14 testes"),cell("Backend e frontend, além de lint, tipos e build")]], [3.2*cm,4.2*cm,10.1*cm]),PageBreak()] + story += [p("JUSTIFICATIVAS TÉCNICAS","Kicker"),p("Escolhas orientadas a correção e demonstração","H1x")] + choices=[("Monólito modular","Reduz infraestrutura e mantém fronteiras claras entre API, domínio, simulação, billing e analytics."),("Equal Share","É determinístico, simples de explicar e distribui o recurso escasso igualmente, respeitando limites individuais."),("Solar priorizada","Reduz importação da rede e torna explícito o aproveitamento renovável em cada leitura."),("Ticks manuais","Permitem repetir o cenário minuto a minuto e inspecionar o efeito de cada decisão."),("PostgreSQL + migrations","Garantem persistência estruturada, histórico e evolução de esquema reproduzível."),("UTC + Decimal","UTC evita ambiguidade temporal; tipos decimais preservam valores monetários."),("API tipada","Pydantic e TypeScript reduzem inconsistências entre backend e frontend."),("Simulação desacoplada","EnergyDataProvider permite substituir a fonte simulada por integração física futura sem mover regras críticas.")] + rows=[[cell("Escolha",True),cell("Justificativa",True)]]+[[cell(a),cell(b)] for a,b in choices] + story += [table(rows,[4.4*cm,13.1*cm]),Spacer(1,.25*cm),AccentBox("Invariante central: potência alocada nunca é negativa nem excede pedido, carregador, veículo ou limite da rede."),PageBreak()] + story += [p("SUSTENTABILIDADE E AUTOMAÇÃO","Kicker"),p("Como cada tecnologia gera valor","H1x")] + story += [table([[cell("Dimensão",True),cell("Mecanismo",True),cell("Efeito demonstrado",True)],[cell("Sustentabilidade"),cell("Prioridade solar + segregação solar/rede + fator de emissão"),cell("20 kW solares no terceiro tick e 0,1333 kg de CO₂ evitado na API")],[cell("Automação inteligente"),cell("Seed, script HTTP, controlador, alertas e regras determinísticas"),cell("Cenário inteiro repetível sem intervenção no banco durante o fluxo")],[cell("Eficiência energética"),cell("Limite de rede e rateio Equal Share"),cell("Quatro sessões atendidas sem exceder 60 kW de importação")],[cell("Eficiência operacional"),cell("Dashboards, histórico e billing integrados"),cell("Estado energético convertido em alerta, invoice e indicadores")]], [3.6*cm,6.8*cm,7.1*cm]),Spacer(1,.35*cm),p("Cadeia de valor","H2x"),p("Recarga → dados → informação → inteligência → decisão energética. A plataforma não se limita a registrar consumo: ela relaciona restrição elétrica, fonte energética, evento operacional, custo e impacto ambiental. Essa integração é o núcleo da proposta de valor."),p("Limites e evolução responsável","H2x"),p("O protótipo não comanda potência física. A futura conexão com equipamentos deve entrar pela fronteira do provedor de dados/integração, mantendo os serviços determinísticos como autoridade sobre segurança energética. Um modelo preditivo poderá estimar demanda e classificar risco, mas terá caráter consultivo."),PageBreak()] + story += [p("CONEXÃO COM A DISCIPLINA","Kicker"),p("Pensamento Computacional e Automação com Python","H1x")] + story += [table([[cell("Conteúdo",True),cell("Aplicação no ChargeGrid",True)],[cell("Decomposição"),cell("Separação em sessões, energia, simulação, billing, analytics e persistência")],[cell("Abstração"),cell("Modelos de usuário, veículo, carregador, estação, leitura e invoice")],[cell("Algoritmos"),cell("Cálculo de potência solicitada, Equal Share, prioridade solar, energia, custo e ESG")],[cell("Automação em Python"),cell("FastAPI, controlador de ticks, seed idempotente e roteiro de demonstração")],[cell("Estruturas e persistência"),cell("Schemas Pydantic, entidades SQLAlchemy e PostgreSQL")],[cell("Testes e validação"),cell("Invariantes energéticas, transições, APIs e fluxo integrado automatizados")],[cell("Dados e tomada de decisão"),cell("Dashboards, alerta HIGH_DEMAND, histórico, cobrança e sustentabilidade")]], [4.5*cm,13*cm]),Spacer(1,.35*cm),p("Síntese acadêmica","H2x"),p("O projeto materializa o pensamento computacional ao decompor um problema físico e multidimensional em dados, regras e interfaces verificáveis. A automação em Python coordena o experimento, enquanto os testes convertem requisitos energéticos em critérios objetivos. Assim, o software demonstra sustentabilidade e eficiência sem depender de uma alegação de hardware inexistente."),PageBreak()] + story += [p("REPRODUÇÃO E RASTREABILIDADE","Kicker"),p("Como verificar a demonstração","H1x"),p("1. Configurar ambiente de demonstração isolado, relógio UTC opt-in e fator de emissão de 0,4 kg/kWh.
2. Subir os serviços via Docker Compose e aplicar migrations.
3. Executar o seed idempotente com senhas fornecidas apenas por variáveis de ambiente.
4. Rodar scripts/sprint3_demo.py, que usa endpoints públicos.
5. Conferir OpenAPI, dashboards, leituras, alerta, invoice e sustentabilidade.
6. Executar make check para validar backend e frontend."),AccentBox("As credenciais permanecem fora do Git; a demonstração deve usar banco limpo e um único controlador de simulação."),Spacer(1,.35*cm),p("Fontes internas consultadas","H2x"),p("SPEC.md (fonte de verdade técnica e regras); BRIEFING.md (visão, disciplina e organização); docs/SPRINT_3_PLAN.md; docs/sprint-3/README.md; docs/sprint-3/EVIDENCE.md; diagramas Mermaid/PNG; código e testes do backend/frontend.","Bodyx"),p("Conclusão","H2x"),p("A Sprint 3 apresenta um protótipo simulado integrado, rastreável e demonstrável. O sistema mantém a rede em seu limite, redistribui potência, incorpora energia solar, persiste leituras, emite alerta, encerra sessão, calcula cobrança e apresenta indicador ambiental. O resultado conecta automação inteligente, sustentabilidade e eficiência energética em um único fluxo coerente."),Spacer(1,.4*cm),p("CHARGEGRID INTELLIGENCE","Callout"),p("Transformando cada recarga em inteligência acionável.","Callout")] + doc.build(story) + print(OUT) + + +if __name__ == "__main__": build() diff --git a/scripts/sprint3_demo.py b/scripts/sprint3_demo.py new file mode 100644 index 0000000..7f180dd --- /dev/null +++ b/scripts/sprint3_demo.py @@ -0,0 +1,146 @@ +"""Run the Sprint 3 scenario through public HTTP endpoints only.""" + +import json +import os +import time +from datetime import datetime +from urllib.error import HTTPError, URLError +from urllib.parse import urlencode +from urllib.request import Request, urlopen + +BASE = os.environ.get("DEMO_API_URL", "http://localhost:8000/api/v1").rstrip("/") +PREFIX = "SPRINT3-DEMO" +API_READY_TIMEOUT_SECONDS = 30 + + +def call(method: str, path: str, token: str | None = None, body: dict | None = None, + params: dict | None = None) -> object: + url = BASE + path + ("?" + urlencode(params) if params else "") + headers = {"Content-Type": "application/json"} + if token: + headers["Authorization"] = f"Bearer {token}" + request = Request(url, method=method, headers=headers, + data=json.dumps(body).encode() if body is not None else None) + try: + with urlopen(request, timeout=15) as response: + return json.load(response) + except HTTPError as exc: + raise RuntimeError(f"{method} {path}: HTTP {exc.code} {exc.read().decode()}") from exc + except (ConnectionError, TimeoutError, URLError) as exc: + raise RuntimeError( + f"{method} {path}: API connection failed at {BASE}. " + "Check `docker compose ps` and `docker compose logs backend`." + ) from exc + + +def wait_for_api(timeout_seconds: float = API_READY_TIMEOUT_SECONDS) -> None: + """Wait for startup without retrying any operation that changes demo state.""" + deadline = time.monotonic() + timeout_seconds + last_error: OSError | None = None + health_url = BASE + "/health" + while time.monotonic() < deadline: + try: + with urlopen(health_url, timeout=2) as response: + if response.status == 200: + return + except (ConnectionError, TimeoutError, URLError) as exc: + last_error = exc + time.sleep(0.5) + raise RuntimeError( + f"API did not become ready at {health_url} within {timeout_seconds:g}s. " + "Check `docker compose ps` and `docker compose logs backend`." + ) from last_error + + +def login(email: str, password: str) -> str: + result = call("POST", "/auth/login", body={"email": email, "password": password}) + return result["access_token"] + + +def show(label: str, value: object) -> None: + print(json.dumps({label: value}, ensure_ascii=False, indent=2)) + + +def tick(admin: str, station_id: str, expected_count: int, + expected_solar_kw: float) -> None: + result = call("POST", "/simulation/ticks", admin) + readings = call("GET", "/energy/history", params={"station_id": station_id}) + latest = [row for row in readings if row["timestamp"] == result["timestamp"]] + if len(latest) != expected_count: + raise RuntimeError(f"Expected {expected_count} readings, got {len(latest)}") + total_keys = ( + "allocated_power_kw", + "solar_power_kw", + "grid_power_kw", + "interval_energy_kwh", + ) + totals = {key: round(sum(row[key] for row in latest), 4) for key in total_keys} + allocation_exceeded = any(row["allocated_power_kw"] > 20.0001 for row in latest) + if totals["grid_power_kw"] > 60.0001 or allocation_exceeded: + raise RuntimeError("Energy limit violated") + if (abs(totals["grid_power_kw"] - 60) > 0.01 + or abs(totals["solar_power_kw"] - expected_solar_kw) > 0.1 + or abs(totals["allocated_power_kw"] - (60 + expected_solar_kw)) > 0.1): + raise RuntimeError(f"Unexpected power allocation: {totals}") + show("tick", {"timestamp": result["timestamp"], "sessions": latest, "totals": totals}) + + +def main() -> None: + admin_password = os.environ.get("DEMO_ADMIN_PASSWORD") + user_password = os.environ.get("DEMO_USER_PASSWORD") + if not admin_password or not user_password: + raise SystemExit("Set DEMO_ADMIN_PASSWORD and DEMO_USER_PASSWORD") + wait_for_api() + admin = login("sprint3-admin@demo.invalid", admin_password) + users = [login(f"sprint3-user-{i}@demo.invalid", user_password) for i in range(1, 5)] + stations = call("GET", "/stations", admin) + station = next(row for row in stations if row["name"] == PREFIX) + chargers = call("GET", "/chargers", admin) + chargers = sorted((row for row in chargers if row["station_id"] == station["id"]), + key=lambda row: row["code"]) + if len(chargers) != 4 or station["station_peak_solar_kw"] != 0: + raise RuntimeError("Use a fresh seeded demo database with solar peak 0") + status = call("GET", "/simulation/status", admin) + instant = datetime.fromisoformat(status["current_instant"].replace("Z", "+00:00")) + if status["state"] != "STOPPED" or instant.utcoffset().total_seconds() != 0: + raise RuntimeError("Expected a stopped UTC simulation clock") + hour, minute = instant.hour, instant.minute + if hour != 11 or not 55 <= minute <= 59: + raise RuntimeError("Set DEMO_SIMULATION_START_UTC near 11:58 UTC and restart the API") + if call("GET", "/energy/history", params={"station_id": station["id"]}): + raise RuntimeError("Use a fresh demo database; readings already exist") + vehicles = [next(row for row in call("GET", "/vehicles", user) + if row["license_plate"] == f"S3D-{i:04d}") + for i, user in enumerate(users, 1)] + sessions = [] + for i in range(3): + session = call("POST", "/sessions/start", users[i], + {"vehicle_id": vehicles[i]["id"], "charger_id": chargers[i]["id"]}) + sessions.append(session) + show("three_sessions", sessions) + call("POST", "/simulation/start", admin) + tick(admin, station["id"], 3, 0) + fourth = call("POST", "/sessions/start", users[3], + {"vehicle_id": vehicles[3]["id"], "charger_id": chargers[3]["id"]}) + show("fourth_session", fourth) + tick(admin, station["id"], 4, 0) + updated = call("PATCH", f"/stations/{station['id']}", admin, + {"station_peak_solar_kw": 20}) + show("solar_configuration", updated) + tick(admin, station["id"], 4, 20) + show("solar_readings", call("GET", "/solar/history", params={"station_id": station["id"]})) + show("alerts", call("GET", "/alerts", admin, params={"station_id": station["id"]})) + show("admin_dashboard", call("GET", "/analytics/dashboard", admin, + params={"station_id": station["id"]})) + show("user_dashboard_before", call("GET", "/user/dashboard", users[3])) + show("completed_session", call("POST", f"/sessions/{fourth['id']}/stop", users[3])) + show("invoices", call("GET", "/billing/invoices", users[3])) + show("sustainability", call("GET", "/analytics/sustainability", admin, + params={"station_id": station["id"]})) + show("admin_dashboard_after", call("GET", "/analytics/dashboard", admin, + params={"station_id": station["id"]})) + show("user_dashboard_after", call("GET", "/user/dashboard", users[3])) + + +if __name__ == "__main__": + main() diff --git a/video_editado/2026-09-21_17-09-13_editado.mp4 b/video_editado/2026-09-21_17-09-13_editado.mp4 new file mode 100644 index 0000000..d5ae39b Binary files /dev/null and b/video_editado/2026-09-21_17-09-13_editado.mp4 differ diff --git a/video_editado/2026-09-21_17-09-13_legendas.srt b/video_editado/2026-09-21_17-09-13_legendas.srt new file mode 100644 index 0000000..eada0a0 --- /dev/null +++ b/video_editado/2026-09-21_17-09-13_legendas.srt @@ -0,0 +1,191 @@ +1 +00:00:00,250 --> 00:00:07,890 +Olá! Somos a Equipe 3 e este é o ChargeGrid Intelligence, desenvolvido para o desafio FIAP em parceria com a GoodWe. + +2 +00:00:08,350 --> 00:00:15,110 +Nesta Sprint 3 entregamos um protótipo integrado para gerenciar recargas de veículos elétricos, + +3 +00:00:15,350 --> 00:00:22,970 +controlar a demanda da rede, priorizar a energia solar e transformar os dados da operação em alertas, + +4 +00:00:23,390 --> 00:00:26,150 +cobrança e indicadores ambientais. + +5 +00:00:26,150 --> 00:00:35,590 +No diagrama da arquitetura do projeto, nós podemos ver que a arquitetura segue um monolito + +6 +00:00:35,590 --> 00:00:36,150 +modular. + +7 +00:00:36,610 --> 00:00:42,050 +O front-end foi desenvolvido em React e TypeScript e consome a API FastAPI. + +8 +00:00:42,630 --> 00:00:48,870 +No back-end, o serviço de sessão, energia, simulação, billing e analytics concentram + +9 +00:00:48,870 --> 00:00:53,510 +as regras de negócio, enquanto o PostgreSQL mantém o histórico da operação. + +10 +00:00:53,510 --> 00:01:00,110 +Nesta entrega, os carregadores e a geração solar são simulados. Cada minuto da operação + +11 +00:01:00,110 --> 00:01:06,750 +é avançado por um tick manual, permitindo reproduzir o cenário de forma determinística. + +12 +00:01:07,250 --> 00:01:15,350 +Iniciamos o cenário com 3 veículos. Cada um solicita 20 kW. Como a demanda total é + +13 +00:01:15,350 --> 00:01:22,170 +de 60 kW e esse também é o limite da rede, cada sessão recebe integralmente os + +14 +00:01:22,170 --> 00:01:29,050 +20 kW solicitados. Nesse primeiro momento, não há geração solar, então toda a energia + +15 +00:01:29,050 --> 00:01:38,080 +utilizada vem da rede elétrica. Em seguida, inicia-se uma quarta recarga também + +16 +00:01:38,080 --> 00:01:47,990 +solicitando 20 kW. A demanda solicitada passa para 80 kW, acima do limite disponível + +17 +00:01:47,990 --> 00:01:55,270 +da rede. O algoritmo Equal Share entra em ação e redistribui a capacidade igualmente. Cada + +18 +00:01:55,270 --> 00:02:02,250 +veículo passa a receber 15 kW. Dessa forma, as quatro sessões continuam carregando, mas + +19 +00:02:02,250 --> 00:02:14,050 +a importação permanece limitada a 60 kW. No terceiro momento, configura-se uma disponibilidade + +20 +00:02:14,050 --> 00:02:20,550 +de 20 kW de energia solar. O sistema prioriza essa fonte renovável e mantém a demanda dentro + +21 +00:02:20,550 --> 00:02:27,830 +do mesmo limite de 60 kW. Assim, a instalação consegue entregar os 80 kW solicitados, 20 + +22 +00:02:27,830 --> 00:02:34,010 +provenientes da energia solar e 60 da rede. Cada uma das quatro sessões volta a receber + +23 +00:02:34,010 --> 00:02:41,070 +os 20 kW solicitados, sem violar os limites dos veículos, carregadores ou da estação. + +24 +00:02:41,570 --> 00:02:48,250 +No dashboard administrativo, o gestor acompanha a demanda, o limite da rede e a utilização + +25 +00:02:48,250 --> 00:02:55,470 +da geração solar, as sessões ativas, a energia consumida e os indicadores ambientais. + +26 +00:02:56,190 --> 00:03:03,790 +Os gráficos mostram a evolução de demanda de 60 para 80 kW e separam claramente a participação + +27 +00:03:03,790 --> 00:03:09,790 +da rede e da geração solar. Cada tick também gera leituras persistidas para cada sessão. + +28 +00:03:10,210 --> 00:03:17,290 +Quando a importação atinge o limite de 60 kW, o sistema registra o alerta de alta demanda, + +29 +00:03:17,290 --> 00:03:21,870 +permitindo que o gestor identifique a condição operacional. + +30 +00:03:22,370 --> 00:03:26,970 +Para concluir o fluxo, encerramos a quarta sessão. + +31 +00:03:27,370 --> 00:03:31,210 +O sistema calcula automaticamente energia consumida + +32 +00:03:31,210 --> 00:03:34,610 +e gera uma invoice fechada de 47 centavos, + +33 +00:03:34,970 --> 00:03:38,890 +utilizando a tarifa cadastrada de 80 centavos por quilowatt-hora. + +34 +00:03:39,370 --> 00:03:43,890 +O uso da energia solar também é convertido em um indicador ambiental. + +35 +00:03:43,890 --> 00:03:53,590 +Neste cenário, a API calculou aproximadamente 13 centésimos de quilograma de CO2 evitado. + +36 +00:03:54,030 --> 00:03:59,610 +Dessa forma, o mesmo fluxo conecta operação, cobrança e sustentabilidade. + +37 +00:04:00,800 --> 00:04:07,060 +O projeto aplica pensamento computacional ao decompor problemas em sessões, alocação, + +38 +00:04:07,500 --> 00:04:11,040 +simulação, cobrança e análise de dados. + +39 +00:04:11,040 --> 00:04:17,410 +como a gente pode analisar aqui no nosso diagrama de sequência. + +40 +00:04:18,640 --> 00:04:24,680 +A automação em Python está presente na API, no controlador de ticks, no seed reproduzível + +41 +00:04:24,680 --> 00:04:29,160 +e no roteiro que executa o cenário por endpoints públicos. + +42 +00:04:29,660 --> 00:04:33,280 +É importante destacar que os valores apresentados são simulados. + +43 +00:04:33,580 --> 00:04:38,620 +Nesta sprint ainda não há integração com carregadores + +44 +00:04:38,620 --> 00:04:39,360 +físicos. + +45 +00:04:39,360 --> 00:04:46,700 +protocolo OCPP ou modelo de Machine Learning treinado. Como resultado, entregamos o protótipo + +46 +00:04:46,700 --> 00:04:53,740 +integrado, reproduzível e demonstrado, capaz de manter a rede dentro do limite, priorizar + +47 +00:04:53,740 --> 00:04:58,920 +energia solar e transformar cada recarga em inteligência operacional. + +48 +00:04:59,620 --> 00:05:02,080 +Este é o Charge Grid Intelligence. Muito obrigado. diff --git a/video_editado/relatorio_edicao.json b/video_editado/relatorio_edicao.json new file mode 100644 index 0000000..0132263 --- /dev/null +++ b/video_editado/relatorio_edicao.json @@ -0,0 +1,32 @@ +{ + "source": "/home/mathsant/2026-09-21 17-09-13.mkv", + "output": "/home/mathsant/Documentos/Fiap/ChallengeGoodwe2026/ChargeGrid_IntelligencePlatform/video_editado/2026-09-21_17-09-13_editado.mp4", + "language": "pt", + "language_probability": 1, + "kept_intervals": [ + [ + 2.2, + 69.2 + ], + [ + 71.08, + 165.4 + ], + [ + 166.63, + 207.43 + ], + [ + 213.0, + 280.29 + ], + [ + 281.86, + 314.78 + ] + ], + "kept_duration_seconds": 302.33, + "original_duration_seconds": 317.12, + "removed_duration_seconds": 14.79, + "caption_count": 48 +} \ No newline at end of file