Carbon Exchange Carbon Exchange Manual técnico carbonexchange.com.br ↗

Carbon Exchange — Manual

Plataforma de monitoramento ambiental para vinícolas, agro e brigadas de incêndio. Recebe dados de sensores LoRaWAN, classifica risco, alerta brigadistas via WhatsApp e expõe dashboard com mapa e previsão meteorológica.

1. Overview

Carbon Exchange é uma stack de três componentes:

2. Arquitetura

+----------------+ HTTPS +-----------------+ +-------------------+ | Sensor LoRa |--------------->| nginx | proxy | funnel_backend | | (mile_07,...) | | api.danhausch |-------->| Flask + gunicorn | +----------------+ | cloud | | 127.0.0.1:5000 | +-----------------+ +---------+---------+ | +-----------------------+--------+-------------+-------+-------------+ | | | | | v v v v v +------------+ +-----------------+ +------------------+ +-----------------+ | PostgreSQL | | Baileys WA | | Open-Meteo | | JSONL audit log | | carbon.* | | 127.0.0.1:8790 | | api.open-meteo | | carbon_webhook_ | | clients | | (Node, Baileys) | | (forecast 24 h) | | events.jsonl | | devices | | | +------------------+ +-----------------+ | sensor_ | | | | readings | | v +------------+ | +----------------+ | | Carbon Software| <- grupo admin | | @ WhatsApp | <- (Fase 2: grupo por cliente) | +----------------+ +------------------+

3. Fluxo de dados (request lifecycle)

  1. Sensor envia POST com JSON para https://api.danhausch.cloud/carbon.
  2. nginx encaminha para 127.0.0.1:5000 (gunicorn → Flask funnel_backend.py).
  3. Handler carbon_webhook():
    1. Persiste body completo em carbon_webhook_events.jsonl.
    2. Extrai leitura via _extract_sensor_reading().
    3. Calcula risco via _compute_environment_risk().
    4. Encaminha mensagem formatada para WhatsApp via _forward_carbon_webhook_to_whatsapp().
    5. Salva em PostgreSQL (_carbon_db_save_reading): faz upsert em carbon.clients + carbon.devices, insere em carbon.sensor_readings.
    6. Se CO₂ > threshold, dispara alerta de áudio (_dispatch_carbon_audio_alert).
  4. Resposta JSON síncrona em < 2 s (depende do RTT do Open-Meteo se for chamado).

4. Casos de uso

UC-01 · Brigada recebe alerta de CO₂

Sensor detecta CO₂ acima de 800 ppm. POST /carbon recebe payload, classifica risco como vermelho e dispara mensagem WhatsApp para grupo admin com nível de risco, leitura completa, link para mapa e previsão de chuva/vento das próximas 24 h. Brigadista decide ir ao local.

UC-02 · Sensor é movido

Payload chega com movimento: 1. Mensagem WhatsApp recebe linha destacada "Sensor movido do local original!". Dashboard mostra badge laranja no card "Movimento". Operador investiga.

UC-03 · Operador consulta histórico

Acessa /carbon/dashboard, filtra por cliente Vinícola e device mile_07, ajusta janela para 168 h (7 dias). Vê 4 séries temporais (umidade, temp, pressão, CO₂) + última leitura + posição no mapa.

UC-04 · Admin provisiona novo cliente Fase 2

Admin loga com JWT, abre /carbon/admin, cria tenant Fazenda Boa Vista com seu JID de grupo WhatsApp. Adiciona 5 devices. A partir do próximo POST, mensagens vão para o grupo do cliente (e mantém cópia no admin).

UC-05 · Cliente vê apenas seu ambiente Fase 2

Usuário client_admin da Vinícola loga e o dashboard filtra automaticamente todos os queries por seu tenant_id. Não vê devices de outros clientes.

5. Telas

Dashboard

URL: /carbon/dashboard

4 KPIs primários (umidade, temp, pressão, CO₂); 4 secundários (cliente, device, bateria, badge de movimento); caixa de risco com cor; mapa Leaflet com markers; painel de previsão 24 h; 4 gráficos históricos (Chart.js).

Refresh automático: dados a cada 5 s; catálogo de clientes/devices a cada 60 s.

WhatsApp Pairing

URL: /carbon/whatsapp

QR code gerado pelo Baileys; botões "Atualizar QR" / "Reconectar"; envio de mensagem teste para qualquer telefone.

API Docs

URL: /carbon/docs

Página HTML simples com exemplos de payload e endpoints.

Test UI

URL: /carbon/test

Form para POST manual de payloads ao webhook.

Login

URL: /carbon/login

Email + senha → JWT em localStorage. Redireciona para dashboard ou admin conforme role.

Admin

URL: /carbon/admin

Tabs Tenants / Usuários / Auditoria. CRUD de cliente (incl. JID do grupo WhatsApp via dropdown), criação de usuários, log de alertas com status de replay.

6. Features

7. API / Endpoints

Base URL pública: https://api.danhausch.cloud · Base local: http://127.0.0.1:5000

MétodoPathDescrição
POST/carbonWebhook principal: ingestão de leituras, dispara WA, salva em PG.
GET/carbonHealth/info: tamanho do JSONL e path do arquivo.
GET/carbon/latest?limit=20Últimos eventos brutos do JSONL (1–200).
GET/carbon/dashboardServe dashboard/index.html.
GET/carbon/dashboard/dataJSON com última leitura + risco. Filtros: client_id, device_id.
GET/carbon/readingsHistórico de leituras. Params: limit (1–2000), hours (1–720), client_id, device_id.
GET/carbon/catalogLista de clients e devices (incl. lat/lon).
GET/carbon/forecast?lat=&lon=Previsão 24 h (chuva, vento, rajada) via Open-Meteo, cache 10 min.
GET/carbon/whatsappUI de pareamento Baileys.
GET/carbon/whatsapp/statusEstado da sessão (proxy → :8790/wa/status).
GET/carbon/whatsapp/pairingQR code data URL (proxy → :8790/wa/pairing).
POST/carbon/whatsapp/reconnectForça reconexão Baileys.
POST/carbon/whatsapp/send-testBody: {phone, text}. Envia mensagem teste.
GET/carbon/testUI de teste do webhook.
GET/carbon/docsDocumentação HTML simples.
GET/carbon/manualEste manual.
POST/carbon/auth/loginEmail + senha → access + refresh tokens.
POST/carbon/auth/refresh{refresh_token} → novo access.
POST/carbon/auth/logoutRevoga refresh.
GET/carbon/auth/meUser + memberships (lê do JWT).
GET/carbon/loginUI de login.
GET/carbon/adminUI admin (tabs tenants/users/audit).
GET/carbon/admin/tenantsLista tenants. require_admin.
POST/carbon/admin/tenantsCria tenant {name, slug?, whatsapp_jid?}.
POST/carbon/admin/tenants/<id> (PUT)Edita nome, JID, is_active. admin OU client_admin do próprio tenant.
GET/carbon/admin/usersLista usuários + memberships.
POST/carbon/admin/usersCria {email, password, name?, is_admin?}.
POST/carbon/admin/tenants/<id>/membersAdiciona/atualiza membership. {email, role} (viewer/client_admin).
GET/carbon/admin/wa-groupsLista grupos do Baileys (@g.us) para popular dropdown.
GET/carbon/admin/auditAudit log alert_events; tenant-scoped para client_admin.

Exemplo de chamada

curl -X POST "https://api.danhausch.cloud/carbon" \
  -H "Content-Type: application/json" \
  -d '{
    "movimento": 0,
    "temp": 30.1,
    "umid": 47,
    "co2": 305,
    "pa": 949.5,
    "bat": 3.48,
    "device_id": "24e124126f099489",
    "device_name": "mile_07",
    "cliente": "Vinícola",
    "latitude": -22.548545,
    "longitude": -48.819028,
    "fCnt": 144,
    "fPort": 85,
    "token": "pgpanfd115jpr7yu0dv5",
    "devname": "mile-07"
  }'

Resposta típica

{
  "ok": true,
  "saved": true,
  "forwarded": true,
  "forward_status": "sent",
  "reading": {
    "temperature": 30.1, "humidity": 47.0, "pressure": 949.5, "co2": 305.0,
    "battery": 3.48, "movimento": 0,
    "latitude": -22.548545, "longitude": -48.819028
  },
  "db_saved": true,
  "db_status": "db_saved",
  "audio_alert": false,
  "audio_alert_status": "co2_normal"
}

8. Payload do sensor

CampoTipoSignificadoUso
movimento0/11 = sensor movido do local originalBadge no dashboard, alerta no WA
tempfloatTemperatura °C (1 casa decimal)KPI, gráfico, risco
umidintUmidade relativa %KPI, gráfico, risco
co2intCO₂ em ppmKPI, gráfico, risco crítico, alerta áudio
pafloatPressão atmosférica em hPaKPI, gráfico, risco
batfloatTensão da bateria (V)KPI
device_idstringID único do deviceChave em carbon.devices
device_namestringNome amigável do cliente (ex.: "caixa d'água")Exibido no WA + dashboard
clientestringNome do clienteTenant (Fase 1: vira client_id)
latitude, longitudefloatCoordenadas WGS-84Mapa, link Google Maps no WA, previsão
fCnt, fPort, token, devnameMetadados internos LoRa/plataformaIgnorados na mensagem WA; persistidos só em raw_payload

9. Risco ambiental

Função _compute_environment_risk(reading) em funnel_backend.py:1129. Decisão atual:

CondiçãoNívelCor
CO₂ > CARBON_CO2_ALERT_THRESHOLDalerta críticovermelho
Umidade 80–100%risco mínimoazul
Umidade 60–79%risco baixoverde
Umidade 30–59% & temp > 30 °Crisco médioamarelo
Umidade < 30% & temp > 35 °Crisco altolaranja
Demais combinaçõesindeterminadocinza
Pressão é considerada como guarda (precisa estar disponível) mas não pondera o nível ainda. Refinar thresholds é candidato para Fase 2.

10. WhatsApp routing

Backend Baileys

Node.js rodando em 127.0.0.1:8790. Service systemd openclaw-wa-backend.service. Workdir: /root/.openclaw/workspace/apps/openclaw-cockpit/wa-backend.

Endpoints internos do Baileys

Forward webhook → grupo

Função _forward_carbon_webhook_to_whatsapp(payload, reading, risk) formata mensagem PT-BR com _format_carbon_whatsapp_message() e envia via _wa_send_to(target, text).

Target controlado por env var CARBON_WEBHOOK_FORWARD_PHONE. Se terminar em @g.us ou @s.whatsapp.net, é tratado como JID e enviado direto; caso contrário, normalize_phone() converte para digits-only.

Atualmente: [email protected] = grupo Carbon Software.

Formato da mensagem

🌿 Carbon Exchange — Vinícola / mile_07
🌡️ Temp: 30.1 °C    💧 Umid: 47.0 %
🫁 CO₂: 305 ppm     🌐 Pressão: 949.5 hPa
🔋 Bateria: 3.48 V
📍 -22.548545, -48.819028  https://maps.google.com/?q=-22.548545,-48.819028
🛰️ Sensor no local original.
🟡 Risco: risco médio — Calor com humidade moderada.

11. Previsão (Open-Meteo)

Endpoint /carbon/forecast?lat=…&lon=… faz proxy de:

https://api.open-meteo.com/v1/forecast
  ?latitude=&longitude=
  &hourly=precipitation,precipitation_probability,wind_speed_10m,wind_gusts_10m,wind_direction_10m
  &forecast_hours=24&timezone=auto&wind_speed_unit=kmh

Retorna summary (total chuva mm, máx vento, máx rajada, máx prob. chuva) + hourly[] com 24 pontos. Cache em memória 10 min, chave por coordenada arredondada a 4 casas decimais.

Por que Open-Meteo e não Windy? Windy.com não tem API pública gratuita — só Point Forecast paga. Open-Meteo é open-source, sem chave, e usa os mesmos modelos (ECMWF, GFS, ICON) que alimentam o Windy. Se chave Windy for adquirida no futuro, troca-se a URL no handler.

12. Schema PostgreSQL

Schema carbon em goku_games @ 127.0.0.1:5432. Migrações idempotentes via _carbon_db_ensure_schema() rodam a cada save.

carbon.clients (
  id BIGSERIAL PK,
  client_id TEXT UNIQUE NOT NULL,
  name TEXT,
  created_at TIMESTAMPTZ DEFAULT NOW()
)

carbon.devices (
  id BIGSERIAL PK,
  device_id TEXT UNIQUE NOT NULL,
  client_id TEXT REFERENCES carbon.clients,
  label TEXT,           -- = device_name do payload
  location TEXT,
  active BOOLEAN DEFAULT TRUE,
  latitude DOUBLE PRECISION,    -- adicionado Fase 1
  longitude DOUBLE PRECISION,   -- adicionado Fase 1
  created_at TIMESTAMPTZ DEFAULT NOW()
)

carbon.sensor_readings (
  id BIGSERIAL PK,
  device_id TEXT REFERENCES carbon.devices,
  client_id TEXT REFERENCES carbon.clients,
  humidity NUMERIC(6,2),
  temperature NUMERIC(6,2),
  pressure NUMERIC(8,2),
  co2 NUMERIC(10,2),
  battery NUMERIC(5,2),         -- adicionado Fase 1
  movimento SMALLINT,           -- adicionado Fase 1
  latitude DOUBLE PRECISION,    -- adicionado Fase 1
  longitude DOUBLE PRECISION,   -- adicionado Fase 1
  risk_level TEXT,
  raw_payload JSONB NOT NULL,
  measured_at TIMESTAMPTZ DEFAULT NOW(),
  received_at TIMESTAMPTZ DEFAULT NOW()
)

-- Índices
idx_carbon_readings_received_at  ON sensor_readings (received_at DESC)
idx_carbon_readings_device_time  ON sensor_readings (device_id, measured_at DESC)

13. Infraestrutura

ComponenteTecnologiaLocalizaçãoService
Reverse proxy / TLSnginx + Let's Encrypt/etc/nginx/sites-enabled/danhausch.cloud.confnginx.service
API/WebhookPython 3 + Flask + gunicorn (3 workers)/root/.openclaw/workspace/funnel_backend.pyfunnel_backend.service
WhatsApp clientNode.js + Baileys…/apps/openclaw-cockpit/wa-backendopenclaw-wa-backend.service
DBPostgreSQL127.0.0.1:5432 · DB goku_games
Persistência auditJSONL append-only/root/.openclaw/workspace/data/carbon_webhook_events.jsonl
Frontend dashboardHTML estático + Chart.js + Leaflet…/carbon/dashboard/index.htmlservido pelo Flask
Frontend WA pairingHTML estático…/carbon/whatsapp/index.htmlservido pelo Flask
Forecast externoOpen-Meteo (free)api.open-meteo.com

Variáveis de ambiente relevantes

VariávelDefaultFunção
WA_BACKEND_BASE_URLhttp://127.0.0.1:8790URL do backend Baileys
CARBON_WEBHOOK_FORWARD_ENABLEDtrueLiga/desliga forward para WhatsApp
CARBON_WEBHOOK_FORWARD_PHONEJID configuradoTelefone OU JID de grupo (admin)
CARBON_AUDIO_ALERT_PHONES= forward phoneLista CSV de telefones para alerta CO₂
CARBON_CO2_ALERT_THRESHOLD(numérico)Limiar CO₂ ppm para alerta crítico
PG_HOST/PG_PORT/PGDATABASE/PGUSER/PGPASSWORDConexão PostgreSQL
CARBON_WEBHOOK_FILEJSONL pathAudit log local

14. Deploy / Operação

Restart de serviços

# API/webhook (após editar funnel_backend.py)
sudo systemctl restart funnel_backend.service

# WhatsApp Baileys
sudo systemctl restart openclaw-wa-backend.service

# Logs
sudo journalctl -u funnel_backend.service -f
sudo journalctl -u openclaw-wa-backend.service -f

Configurar grupo WhatsApp (admin)

# Listar grupos
curl -s http://127.0.0.1:8790/wa/chats | jq '.items[] | select(.id | endswith("@g.us"))'

# Setar grupo via systemd drop-in
sudo mkdir -p /etc/systemd/system/funnel_backend.service.d
sudo tee /etc/systemd/system/funnel_backend.service.d/carbon-forward.conf <<EOF
[Service]
Environment="CARBON_WEBHOOK_FORWARD_PHONE=<jid>@g.us"
EOF
sudo systemctl daemon-reload
sudo systemctl restart funnel_backend.service

Smoke test

curl -s -X POST http://127.0.0.1:5000/carbon \
  -H 'Content-Type: application/json' \
  -d '{"temp":30.1,"umid":47,"co2":305,"pa":949.5,"bat":3.48,"movimento":0,
       "device_id":"test-001","device_name":"smoke","cliente":"QA",
       "latitude":-22.5,"longitude":-48.8}'

Status check

curl -s http://127.0.0.1:8790/wa/status
curl -s http://127.0.0.1:5000/carbon/dashboard/data | jq
curl -s "http://127.0.0.1:5000/carbon/forecast?lat=-22.5&lon=-48.8" | jq .summary
Cuidado: a sessão Baileys persiste credenciais em /root/.openclaw/credentials/whatsapp/. Backup automático via carbon-backup.timer roda diariamente; rotação keep-last-14 em /var/backups/carbon/.

14.1 Testing — checklist E2E

Plano de teste para validar todas as features. Substitua $BASE pelo host (http://127.0.0.1:5000 em dev ou https://api.danhausch.cloud em prod).

#PassoComando / AçãoEsperado
1Healthcurl -sI $BASE/carbonHTTP 200
2Webhook anônimoPOST /carbon com payload do sensorok:true, db_saved:true, forward_legs.tenant_id resolvido
3Mensagem WhatsAppConferir grupo Carbon SoftwareLinha "🌿 Carbon Exchange — <cliente> / <device>" recebida em < 5 s
4Audit rowSELECT * FROM carbon.alert_events ORDER BY id DESC LIMIT 1status='sent', attempts=1, reading_id ligado
5Login adminPOST /carbon/auth/login {email,password}Devolve access_token, refresh_token, user.is_admin:true
6Listar tenantsGET /carbon/admin/tenants com BearerLista os 4 backfilled
7Configurar JID do tenantUI /carbon/admin → editar tenant 4 → escolher grupotenants.whatsapp_jid salvo
8Dual-routingNovo POST /carbon com cliente do tenant 4forward_legs.tenant.sent:true + admin.sent:true
9Replay (Baileys offline)systemctl stop openclaw-wa-backend, mandar webhook, religarEm ≤ 60 s, carbon-replay.timer reenvia; alert_events.status vira sent e attempts=2
10Backup manualsystemctl start carbon-backup.serviceArquivo /var/backups/carbon/db/carbon-*.dump criado
11IsolationCriar viewer ligado ao tenant 4, logar como ele, GET /carbon/catalogclients.length=1, só vê tenant 4
12Filtro do mapaSelecionar cliente/device no dashboardMapa zoom + somente markers do filtro
13Hard enforcementSetar CARBON_AUTH_ENFORCE=true, restart, GET anônimoHTTP 401
14Anti-spoof por tokenSetar CARBON_TOKEN_ENFORCE=true, POST /carbon com token errado de um device cadastradoHTTP 401 {error:"invalid_token", reason:"token_mismatch"}
15TOFUPOST com novo device_id + token X → segundo POST mesmo device + token YPrimeiro: 200 OK; segundo: 401

14.2 Smoke test rápido

BASE=https://api.danhausch.cloud

# 1. Login
TOKEN=$(curl -s -X POST $BASE/carbon/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"email":"[email protected]","password":"<senha>"}' | jq -r .access_token)

# 2. Webhook + verificar dual-routing
curl -s -X POST $BASE/carbon -H 'Content-Type: application/json' -d '{
  "movimento":0,"temp":31.0,"umid":42,"co2":380,"pa":947.2,"bat":3.45,
  "device_id":"sensor-test-01","device_name":"caixa-d-agua",
  "cliente":"Vinícola Teste",
  "latitude":-22.548545,"longitude":-48.819028
}' | jq '{ok,forward_legs,reading}'

# 3. Audit
curl -s "$BASE/carbon/admin/audit?limit=5" \
  -H "Authorization: Bearer $TOKEN" | jq '.events[] | {channel,target,status,attempts}'

15. Fase 2 — entregue

Implementação do design em /carbon/phase2-design. Ativada via flag CARBON_AUTH_ENFORCE (default false para migração suave).

15.1 Auth (JWT)

15.2 Multi-tenant

15.3 WhatsApp routing por tenant

15.4 Audit + replay queue

15.5 Backup automático

15.6 UI admin

15.7 Anti-spoof: token por device

Cada device tem um platform_token (string opaca ~20 chars) atribuído pela plataforma da Greenbug e enviado no campo token de cada payload. Quando CARBON_TOKEN_ENFORCE=true:

Os 19 devices conhecidos da Greenbug (iza01–iza10, khomp, mile-01–mile-08) foram seedados via scripts/seed_greenbug_tokens.py.

15.8 Pendente para Fase 3

Documento atualizado em 2026-05-10. Carbon Exchange • funnel_backend.py · repo danielharagao/carbon.