Faz 4 — Real-time Event Propagation + Celery Jobs
PR #266 ile production'a alındı. Backend OCPP handler'larından frontend'e canlı event push + 4 zamanlanmış bakım task'ı.
7 WebSocket Event Tipi
Tüm event'ler /ws/telemetry kanalı üzerinden tenant-scoped olarak yayınlanır. Frontend useOcppLiveStream(chargerId, eventTypes) hook'u ile filtreli abone olur.
| Event Type | Tetikleyici Handler | Payload Özet |
|---|---|---|
ocpp.boot | BootNotification Accepted | charger_id, vendor, model, firmware_version, last_boot_at |
ocpp.status | StatusNotification | connector_id, status (Available/Charging/Faulted/...), error_code, info |
ocpp.meter | MeterValues (downsample whitelist sonrası) | connector_id, transaction_id, samples[]: measurand, value, unit, phase |
ocpp.session_started | StartTransaction Accepted | session_id, charger_id, connector_id, id_tag_masked, meter_start, started_at |
ocpp.session_stopped | StopTransaction | session_id, energy_wh, duration_s, stop_reason, stopped_at |
ocpp.command_result | Dispatcher response (REST komut callback) | command_id, action, status (accepted/rejected/timeout), latency_ms |
ocpp.firmware_status | FirmwareStatusNotification | charger_id, status (Downloading/Installing/Installed/Failed), progress |
Event Envelope
Tüm event'ler ortak zarf yapısına uyar:
{
"type": "ocpp.<event_name>",
"ts": "2026-04-25T17:00:00.000Z",
"tenant_id": "<uuid>",
"charger_id": "<uuid>",
"data": {
/* event-specific */
}
}
ts: UTC ISO-8601, milisaniye hassasiyetinde.tenant_id: Frontend kendi tenant'ı dışındaki event'leri receive etmez (backend filtrede).charger_id: Frontend chargerId-bazlı filter için kullanır.data: Event'e göre tipli payload (OcppEventdiscriminated union,frontend/src/lib/ws/ocpp-events.ts).
MeterValues Downsample Whitelist
ocpp.meter event'leri yüksek hacimlidir (her connector × 30 sn × N measurand). UI'a kritik olmayan örnekler DB'ye yazılır ama broadcast edilmez.
Whitelist'e Dahil Measurand'lar
| Measurand | Sebep |
|---|---|
Power.Active.Import | Anlık şarj gücü (kW) — Live Meter Chart |
Voltage | Faz gerilimi — diagnostic |
Current.Import | Faz akımı — diagnostic |
Energy.Active.Import.Register | Kümülatif enerji — session toplam |
SoC | EV battery state-of-charge (varsa) |
Whitelist'e Dahil Context'ler
Sample.Periodic(transaction sırasında periyodik)Transaction.Begin/Transaction.End(oturum açılış/kapanış)
Diğer tüm context'ler (Sample.Clock, Trigger, Other) DB'ye yazılır ama WS'e push edilmez (UI ihtiyacı yok).
samples[].phase alanı normalize semantiği (PR-B, 2026-05-11)
ocpp.meter event payload'ında her samples[] item'ın phase alanı:
- Tip:
string | null(frontend TypeScript:phase: 'L1' | 'L2' | 'L3' | 'L1-N' | 'L2-N' | 'L3-N' | 'N' | null). nullliteral anlamı: Phase belirtilmedi (OCPP spec'inde opsiyonel; tek-faz cihaz veya N/A).'L1'/'L2'/'L3'değerleri: 3-fazlı charger sample'ları — her phase ayrı row + ayrı WS event içinde gelir (Migration 0042 sonrası PK 4-tuple desteği).
Backend normalize: DB seviyesinde phase NOT NULL DEFAULT '' (Migration 0042). Vendor phase atlarsa '' literal'i yazılır. WS event publish ve REST response site'lerinde '' → null olarak çevrilir. Frontend asla '' sentinel'i görmez.
Handler refactor (commit 6b3a022): on_meter_values artık persist + broadcast'i ayrı try/except bloklarında yürütür. Persist hatası (örn. eski 3-tuple PK ile 3-fazlı conflict) broadcast'i bloke etmez → LiveMeterChart UI canlı kalır. Detay: PR-B Changelog Entry + Runbook.
4 Celery Beat Task
backend/app/tasks/ocpp_tasks.py — Celery beat schedule'a kayıtlı zamanlanmış bakım task'ları.
| Task | Schedule | Amaç | Side Effect |
|---|---|---|---|
heartbeat_watchdog | PR-H/4-fix 2026-05-21: DEPRECATED — heartbeat/inactivity kapsamı PR-A1/A2 domain alarm task'larına taşındı. Task name registry'de no-op stub korunur (geri uyumluluk). | (yok — no-op) | |
reconcile_charging_profiles | 5 dk | GetCompositeSchedule ile DB beklenen profil drift kontrolü | Drift varsa re-push (Faz 4-C ileri) |
purge_command_log | Günlük 02:00 UTC | 90 gün retention politikası | ocpp_command_log tablosundan eski kayıtlar DELETE |
sync_firmware_status | Saatlik | Aktif firmware update'i olan charger'lara TriggerMessage(FirmwareStatusNotification) | dispatcher.send (charger response'u event olarak gelir) |
ocpp_domain.check_charger_offline_extended (PR-A2) | 5 dk | last_heartbeat_at > 1h ise critical alarm açar (ocpp_charger_offline_extended) | record_ocpp_domain_incident + notification dispatch |
Task Idempotency Garantileri
: DEPRECATED PR-H/4-fix. Heartbeat/inactivity izleme artık PR-A1 (heartbeat_watchdogocpp_charger_offline, WebSocket disconnect anlık high) + PR-A2 (ocpp_charger_offline_extended, 1h+ critical) domain evaluator'ları ile yapılır. Detay: changelog.purge_command_log:DELETE WHERE created_at < NOW() - INTERVAL '90 days'— multiple çalıştırma güvenli.
Defensive Patterns (Faz 4 Hardening)
1. Best-Effort Publish
try:
await publish_ocpp_event(...)
except Exception:
logger.warning("ocpp event publish failed", exc_info=True)
# Handler akışı kesilmez — OCPP yanıtı charger'a verilir
2. Lazy Import (Circular Koruma)
events.py modülü service.py'den lazy import edilir; uygulama startup zamanı circular reference yaratmaz.
3. DetachedInstanceError Snapshot
StartTransaction / StopTransaction handler'larında SQLAlchemy session kapanmadan önce gerekli alanlar plain dict snapshot olarak alınır; event publish sonrası lazy-load denemesi DetachedInstanceError fırlatmaz.
4. id_tag Son-4 Mask (KVKK)
def mask_id_tag(id_tag: str) -> str:
if len(id_tag) <= 4:
return "****"
return "*" * (len(id_tag) - 4) + id_tag[-4:]
RFID ID'leri WS event'lerinde, audit log'larda ve UI'da maskelenmiş gönderilir. Plaintext id_tag yalnızca DB'de (encrypted at rest yakında).
5. Tenant İzolasyonu Her Publish'te
WS broker event.tenant_id field'ını subscriber tenant ile karşılaştırır; cross-tenant fan-out mümkün değil.
Prometheus Metric'leri
| Metric | Faz | Etiketler | Amaç |
|---|---|---|---|
ocpp_ws_events_published_total | Faz 4-A (yeni) | event_type, tenant_id | Event volume + drop detection |
ocpp_messages_total | Faz 2 | action, direction, status | Protokol mesaj sayacı |
ocpp_command_latency_seconds | Faz 2 | action | REST → dispatcher → charger response süresi |
ocpp_command_timeouts_total | Faz 2 remediation | action, timeout_kind | 30sn dispatcher timeout sayacı |
ocpp_active_chargers | Faz 2 | tenant_id | Online charger count gauge |
Grafana Panel Önerileri
- Event throughput:
rate(ocpp_ws_events_published_total[1m])— event tipine göre breakdown. - Command tail latency:
histogram_quantile(0.99, ocpp_command_latency_seconds_bucket)— p99 < 5sn target. - Timeout oranı:
rate(ocpp_command_timeouts_total[5m]) / rate(ocpp_messages_total{direction="outbound"}[5m])— < %1 target.
Faz 4 Çıktı Listesi (tamamlandı — PR #266)
| # | İş | Dosya |
|---|---|---|
| 1 | 7 publish helper fonksiyonu | backend/app/core/ocpp/events.py |
| 2 | 10 publish çağrısı (handler entegrasyonu) | backend/app/core/ocpp/zeus_charge_point.py |
| 3 | 4 Celery beat task | backend/app/tasks/ocpp_tasks.py |
| 4 | Beat schedule kaydı | backend/app/core/celery/beat_schedule.py |
| 5 | Prometheus metric | backend/app/core/observability/__init__.py |
| 6 | Defensive pattern test'leri | backend/tests/core/ocpp/test_events.py |
Sonraki: Faz 5 — Frontend MVP