OCPP 2.x: Kurumsal EV Şarj Sistemleri Oluşturma
Küresel elektrikli araç şarj altyapısı pazarı yakalandı 2025'te 40,22 milyar dolar ve 2033 yılına kadar %25'lik bir Bileşik Büyüme Oranı ile büyüyecek, Grand View Araştırmasına göre. Avrupa'da AFIR (Alternatif Yakıtlar Altyapı Yönetmeliği) bağlayıcı son tarihler getirmektedir: TEN-T ağında her 60 km'de bir 2025 yılı sonuna kadar en az 150 kW'lık bir istasyonun olması gerekiyor. İtalya'da ise ötesinde 31 Aralık 2025'e kadar 73.000 halka açık şarj noktası ve bir PNRR Yüksek Güçlü Şarj için sektöre ve sektöre 700 milyon Euro'nun üzerinde kaynak ayırmış olan tam patlama.
Bu altyapının merkezindeAçık Şarj Noktası Protokolü (OCPP), Open Charge Alliance (OCA) tarafından geliştirilen ve nasıl yapılacağını tanımlayan açık standart şarj istasyonları (Şarj İstasyonları) merkezi yönetim sistemleriyle iletişim kurar (CSMS - Şarj İstasyonu Yönetim Sistemleri). 250'den fazla kuruluş tarafından benimsendi OCPP, 40'tan fazla ülkede sektörde fiili standart haline geldi ve Farklı üreticilerin donanımları ve farklı yönetim yazılımları arasında birlikte çalışabilirlik.
Bu gelişmiş teknik makalede şunları inceliyoruz: OCPP 2.0.1 ve 2.1 içinde derinlik: WebSocket mimarisi, mesaj yapısı, Cihaz Modeli, profiller güvenlik, akıllı şarj ve ölçeklenebilir, üretime hazır bir CSMS arka ucunun nasıl oluşturulacağı TypeScript ve Python'daki eksiksiz kod örnekleriyle birlikte 10 ila 100.000 istasyon.
Ne Öğreneceksiniz
- OCPP protokolünün 1.2'den 2.1'e evrimi ve temel mimari farklılıklar
- JSON mesaj yapısı (CALL, CALLRESULT, CALLERROR) ve WebSocket aktarımı
- OCPP 2.0.1'in 16 işlevsel bloğu ve hiyerarşik Cihaz Modeli
- 3 güvenlik profili: Temel Kimlik Doğrulama, TLS, X.509 sertifikalı mTLS
- Gelişmiş akıllı şarj: SetChargingProfile, yük yönetimi, zirve tıraşlama, güneş enerjisi entegrasyonu
- Python'da asyncio ve PostgreSQL ile CSMS arka uç uygulaması
- ISO 15118 ve Tak ve Şarj Et: PKI, eMAID, çift yönlü V2G
- Ölçeklenebilir mimari: WebSocket kümeleme ve Kafka ile 10'dan 100.000'e kadar şarj noktası
- İzleme, Grafana kontrol paneli ve temel operasyonel ölçümler
- AFIR, PNIRE düzenlemeleri ve şarj altyapısına yönelik İtalyan teşvikleri
EnergyTech Serisi: Dijital Enerji Üzerine 10 Makale
Bu makale, konuya adanmış bir serinin ilkidir.EnerjiTekniği: protokoller, elektrik yönetiminde devrim yaratan mimariler ve yazılımlar, EV şarjından akıllı şebekelere, BESS sistemlerinden yapay zeka ile enerji optimizasyonuna kadar.
| # | Öğe | Teknolojiler | Seviye |
|---|---|---|---|
| 1 | OCPP 2.x Protokolü: EV Şarj Sistemleri Oluşturmak (şu anda buradasınız) | OCPP, WebSocket, Python, ISO 15118 | Gelişmiş |
| 2 | Akıllı Şebeke ve OpenADR: Talep Yanıtı ve Enerji Esnekliği | OpenADR, IEEE 2030.5, REST, MQTT | Advanced |
| 3 | BESS (Battery Energy Storage): Algoritmi di Ottimizzazione e BMS | Python, LP optimization, CAN bus, Modbus | Advanced |
| 4 | Digital Twin per Reti Elettriche con Kafka e Machine Learning | Kafka, InfluxDB, Grafana, ML, Python | Advanced |
| 5 | SCADA e ICS per Infrastrutture Critiche: Sicurezza e Protocolli | Modbus, DNP3, IEC 61850, OPC UA | Advanced |
| 6 | Ottimizzazione Energetica con AI: Previsione del Consumo e Demand Forecasting | TensorFlow, LSTM, Prophet, FastAPI | Advanced |
| 7 | Virtual Power Plant: Aggregare DER con Python e API REST | DER, DERMS, REST, Python, PostgreSQL | Intermediate |
| 8 | Mercati Energetici e Trading Algoritmico: EPEX SPOT e API | Python, API trading, time series | Advanced |
| 9 | Carbon Accounting Software: Scope 1, 2, 3 e Reporting GHG | Python, GHG Protocol, API, reporting | Intermediate |
| 10 | Microgrids e Isola Energetica: Architetture Resiliente | Microgrid, EMS, edge computing, IoT | Advanced |
Evoluzione del Protocollo: Da OCPP 1.2 a 2.1
Comprendere l'evoluzione di OCPP e fondamentale per apprezzare le scelte architetturali della versione 2.0.1 e pianificare migrazioni da sistemi legacy. Il protocollo e nato nel 2010 per risolvere il problema dell'interoperabilità: ogni produttore di stazioni aveva il proprio protocollo proprietario, rendendo impossibile la gestione multi-vendor.
| Versione | Anno | Trasporto | Caratteristiche Chiave | Deployment |
|---|---|---|---|---|
| OCPP 1.2 | 2010 | SOAP/XML | Prima versione pubblica, operazioni base: boot, authorize, start/stop | Dismesso |
| OCPP 1.5 | 2012 | SOAP/XML | Riservazione, smart charging base, data transfer, reset | Legacy |
| OCPP 1.6 | 2015 | SOAP + JSON/WS | WebSocket, profili di carica, trigger messages, local auth list | Diffusissimo |
| OCPP 2.0 | 2018 | JSON/WS | Cihaz Modeli, fonksiyonel bloklar, ISO 15118 temeli (2.0.1 ile değiştirildi) | Nadir |
| OCPP 2.0.1 | 2020 | Yalnızca JSON/WS | 16 fonksiyonel blok, Cihaz Modeli, 3 güvenlik profili, gelişmiş akıllı şarj | Mevcut standart |
| OCPP 2.1 | 2025 | Yalnızca JSON/WS | Geriye doğru uyumlu 2.0.1, V2G ISO 15118-20, yerel şarj, pil değiştirme | Ortaya çıkan |
OCPP 1.6 ve 2.0.1 Arasındaki Temel Farklılıklar
OCPP 2.0.1 basit bir artımlı güncelleme değildir: yeniden yazma komple mimari terminolojiyi, mesaj yapısını ve kavramsal model. Bu uyumsuzluk "tasarım gereği" gerekliydi OCPP 1.6'nın yapısal sınırlarının üstesinden gelin.
| bekliyorum | OCPP 1.6 | OCPP 2.0.1 |
|---|---|---|
| Sunucu terminolojisi | Merkezi Sistem | CSMS (Şarj İstasyonu Yönetim Sistemi) |
| Müşteri Terminolojisi | Şarj Noktası | Şarj istasyonu |
| Şarj ünitesi | Bağlayıcı | EVSE (Elektrikli Araç Tedarik Ekipmanları) |
| İşlemler | İşlemi Başlat / İşlemi Durdur | Birleşik İşlem Etkinliği (Başladı/Güncellendi/Bitti) |
| Yapılandırma | Sabit tuşlar (Yapılandırmayı Değiştir) | Hiyerarşik Cihaz Modeli (GetVariables/SetVariables) |
| Emniyet | İsteğe bağlı, standartlaştırılmamış | 3 entegre ve zorunlu Güvenlik Profili |
| Akıllı Şarj | Taban (konektör profilleri) | Gelişmiş: EVSE için yığın önceliği, bileşik programlar |
| ISO 15118 | Desteklenmiyor | Yerel Blok M (Tak ve Şarj Et) |
| Özel organizasyon | Düz operasyon listesi | Kullanım senaryoları, gereksinimler ve diyagramlarla birlikte 16 fonksiyonel blok |
OCPP 2.1: 2025'teki Yenilikler
Open Charge Alliance tarafından Ocak 2025'te yayımlanan OCPP 2.1 hâlâ dolu 2.0.1 ile geriye doğru uyumludur ve geleceğe yönelik kritik özellikler ekler:
- Gelişmiş V2G (Araçtan Şebekeye).: EV'lerin sanal enerji santralleri olmasını sağlayan çift yönlü güç aktarımıyla ISO 15118-20 için tam destek
- DER entegrasyonu: Fotovoltaik paneller ve depolama sistemleri gibi kaynaklarla dağıtılmış enerji optimizasyonuna yönelik gelişmiş araçlar
- Yerel fiyatlandırma: satıcıya özel uzantılar olmadan gerçek zamanlı oranları (kWh, zaman, park ücretleri) iletmek için standartlaştırılmış veri yapıları
- Pil Değiştirme: iki ve üç tekerlekli araçlar için akü değiştirme istasyonları desteği
- İşlemi Devam Ettir: zorunlu yeniden başlatmanın ardından veri kaybı olmadan bir işlemi sürdürme olanağı
- Yerel maliyet: Çevrimdışı durumlar için doğrudan istasyonda maliyet hesaplaması
WebSocket İletişim Mimarisi
OCPP 2.0.1 özel olarak kullanır WebSocket üzerinden JSON protokol olarak SOAP/XML'i tamamen terk ederek taşıma. Bu mimari seçim şunları sağlar: Kalıcı iki yönlü iletişim, düşük gecikme süresi, hafif yük ve uyumluluk modern web altyapılarına sahip yerel.
İstemci-Sunucu topolojisi
OCPP modelinde, Şarj istasyonu gibi davranır müşteri WebSoketleri ve CSMS'ler gibi WebSocket sunucusu. Şarj istasyonu bir mekanizma ile bağlantıyı başlatır ve aktif tutar. kalp atışı. CSMS aynı bağlantıdaki istasyona komutlar gönderebilir WebSocket'i açın, ters bağlantıya gerek yok (yoklama yok, itme yok) ayrı).
Charging Station CSMS
| |
|--- WebSocket CONNECT ---------------->|
| wss://csms.example.com/ocpp/CS001 |
| Sec-WebSocket-Protocol: ocpp2.0.1 |
| Authorization: Basic base64(...) |
| |
|<-- HTTP 101 Switching Protocols -------|
| Sec-WebSocket-Protocol: ocpp2.0.1 |
| |
|--- BootNotification.req ------------->|
|<-- BootNotification.conf --------------|
| (interval: 300, status: Accepted) |
| |
|--- StatusNotification.req[EVSE1] ---->|
|<-- StatusNotification.conf ------------|
| |
|--- Heartbeat.req (ogni 300s) -------->|
|<-- Heartbeat.conf --------------------|
| |
| <-- utente avvicina RFID --- |
|--- Authorize.req -------------------->|
|<-- Authorize.conf (Accepted) ----------|
| |
|--- TransactionEvent(Started) -------->|
|<-- TransactionEvent.conf -------------|
| |
|<-- SetChargingProfile.req ------------| (CSMS gestisce load)
|--- SetChargingProfile.conf ---------->|
| |
|--- MeterValues (ogni 60s) ----------->|
|<-- MeterValues.conf ------------------|
| |
|--- TransactionEvent(Ended) ---------->|
|<-- TransactionEvent.conf -------------|
Bağlantı URL'si ve Alt Protokol
Şarj istasyonu CSMS'ye kendi URL'sini içeren bir URL ile bağlanır
benzersiz tanımlayıcı yolun son bölümü olarak.
WebSocket alt protokolü ocpp2.0.1 el sıkışma sırasında müzakere edilir
Protokol sürümü uyumluluğunu sağlamak için HTTP.
# Formato URL
wss://csms.example.com/ocpp/{chargingStationId}
# Esempi reali
wss://csms.example.com/ocpp/IT-MIL-STATION-001
wss://csms.example.com/ocpp/EVSE-PARK-NORD-042
wss://csms.example.com/ocpp/CPO-AUTOGRILL-A7-01
# Headers WebSocket obbligatori
Sec-WebSocket-Protocol: ocpp2.0.1
Authorization: Basic {base64(stationId:password)} # Security Profile 1-2
# Con Security Profile 3 (mTLS): nessun header Authorization,
# l'autenticazione avviene tramite certificato client TLS
OCPP Mesaj Yapısı 2.0.1
OCPP 2.0.1, tümü çerçeve olarak taşınan üç tür JSON mesajı tanımlar WebSocket metni. Her mesaj bir JSON dizisi bir formatla mesaj türüne göre doğrudur. Bu basit yapı ayrıştırmayı kolaylaştırır ve SOAP/XML yüküne karşı hata ayıklama.
ÇAĞRI (İstek) -MessageTypeId 2
Mesaj ARAMA bir tarafça gönderilen bir talebi temsil eder (Şarj İstasyonu veya CSMS) diğerine. Benzersiz bir kimlik ve eylemin adını içerir ve istek yükü.
// Formato: [MessageTypeId, MessageId, Action, Payload]
// Esempio: BootNotification dalla Charging Station
[2, "19223201", "BootNotification", {
"chargingStation": {
"model": "SuperCharger-500",
"vendorName": "EVPower Inc.",
"serialNumber": "SN-2025-00142",
"firmwareVersion": "3.2.1",
"modem": {
"iccid": "8939100000000000001",
"imsi": "310260000000001"
}
},
"reason": "PowerUp"
}]
// Esempio: TransactionEvent dalla Charging Station
[2, "tx-evt-001", "TransactionEvent", {
"eventType": "Started",
"timestamp": "2026-03-09T10:30:00Z",
"triggerReason": "CablePluggedIn",
"seqNo": 0,
"transactionInfo": {
"transactionId": "TXN-2026-0309-001",
"chargingState": "EVConnected"
},
"evse": { "id": 1, "connectorId": 1 },
"idToken": {
"idToken": "RFID-04A2B3C4D5",
"type": "ISO14443"
}
}]
CALLRESULT (Yanıt) -MessageTypeId 3
Mesaj ÇAĞRI SONUCU ve bir ÇAĞRI'ya verilen olumlu yanıt. Mesaj Kimliğinin izin verebilmesi için orijinal CALL ile tam olarak eşleşmesi gerekir. istek-yanıt korelasyonu.
// Formato: [MessageTypeId, MessageId, Payload]
// Risposta a BootNotification
[3, "19223201", {
"currentTime": "2026-03-09T10:00:00Z",
"interval": 300,
"status": "Accepted"
}]
// Risposta a TransactionEvent (Started)
[3, "tx-evt-001", {
"totalCost": 0,
"chargingPriority": 0,
"idTokenInfo": {
"status": "Accepted",
"groupIdToken": {
"idToken": "GROUP-FLEET-01",
"type": "Central"
}
}
}]
ARAYAN HATA (Hata) -MessageTypeId 4
Mesaj ARAYAN HATASI alıcı bunu yapmadığında gönderilir bir ÇAĞRI'yı işleyebilir. Standartlaştırılmış bir hata kodu ve açıklama içerir Okunabilir ve yapılandırılmış ayrıntılar.
// Formato: [MessageTypeId, MessageId, ErrorCode, ErrorDescription, ErrorDetails]
[4, "19223201", "FormatViolation",
"Il campo 'vendorName' supera la lunghezza massima di 50 caratteri",
{
"field": "chargingStation.vendorName",
"maxLength": 50,
"actualLength": 67
}
]
// Codici di errore OCPP 2.0.1 standardizzati:
// FormatViolation - messaggio JSON malformato
// GenericError - errore generico non classificabile
// InternalError - errore interno del ricevente
// MessageTypeNotSupported - tipo di messaggio non supportato
// NotImplemented - azione riconosciuta ma non implementata
// NotSupported - azione non supportata dall'implementazione
// OccurrenceConstraintViolation - violazione cardinalita elementi
// PropertyConstraintViolation - vincolo su una proprietà violato
// ProtocolError - violazione del protocollo OCPP
// RpcFrameworkError - errore nel framework RPC di base
// SecurityError - errore di sicurezza o autenticazione
// TypeConstraintViolation - tipo di dato non corretto
İstek-Yanıt Korelasyonu: Kritik Kurallar
Her ÇAĞRI'nın bir Benzersiz Mesaj Kimliği (maksimum 36 karakter tarafından aynı bağlantıda daha önce kullanılmamış alfasayısal) aynı gönderen. CALLRESULT veya CALLERROR bunu kullanmalıdır aynı Mesaj Kimliği. Gönderenin bir zaman aşımı süresi (önerilen: 30 saniye) sürdürmesi gerekir; bu sürenin sonunda istek başarısız olarak kabul edilir. Aynı anda yalnızca bir ÇAĞRI beklemede olabilir iletişimin her yönü: istasyon, şu ana kadar ikinci bir ÇAĞRI gönderemez ilkine yanıt gelmedi.
OCPP 2.0.1'in 16 İşlevsel Bloğu
OCPP 2.0.1 tüm işlevleri şu şekilde düzenler: 16 fonksiyonel blok (A'dan P'ye), her biri ayrıntılı gereksinimleri olan özel kullanım durumlarını içerir, ön koşullar ve sıra diyagramları. Bu modüler organizasyon şunları sağlar: uygulayıcılar hangi blokları desteklediklerini beyan edecek ve test uzmanları doğrulayacak blok blok uyumluluk.
| Engellemek | İsim | En Popüler Mesajlar | Zorunlu |
|---|---|---|---|
| A | Güvenlik | GüvenlikOlayBildirimi, İmzaSertifikası, Sertifikaİmzalı | Si |
| B | Sağlama | BootNotification, SetVariables, GetVariables, NotifyReport | Si |
| C | Yetkilendirme | Yetkilendir, ClearCache, GetLocalListVersion | Si |
| D | Yerel Yetkilendirme Listesi | SendLocalList, GetLocalListVersion | No |
| E | İşlem | TransactionEvent, GetTransactionStatus, MeterValues | Si |
| F | Uzaktan kumanda | requestStartTransaction,RequestStopTransaction,UnlockConnector | No |
| G | Kullanılabilirlik | Durum Bildirimi, Kullanılabilirlik Değişikliği, Kalp Atışı | Si |
| H | Rezervasyon | Hemen Rezervasyon Yap, Rezervasyonu İptal Et | No |
| I | Tarife ve Maliyet | Maliyet Güncellendi, Mesajı Göster | No |
| J | Ölçüm | MeterValues (enerji/güç/akım ölçümleri) | Si |
| K | Akıllı Şarj | SetChargingProfile, ClearChargingProfile, GetChargingProfiles, ReportChargingProfiles | No |
| L | Firmware Yönetimi | Firmware Güncelleme, Firmware Durum Bildirimi | No |
| M | ISO 15118 Sertifika Yönetimi | Get15118EVCertificate, SilSertifika, Sertifikaİmzalı | No |
| N | Teşhis | GetLog, LogStatusNotification, SetMonitoringBase, SetVariableMonitoring | No |
| O | Mesajı Görüntüle | SetDisplayMessage, GetDisplayMessages, ClearDisplayMessage | No |
| P | Veri Aktarımı | DataTransfer (satıcıya özel uzantılar) | No |
Cihaz Modeli: OCPP 2.0.1'in Kalbi
Il Cihaz Modeli ve OCPP 2.0.1'in en büyük mimari yeniliği. OCPP 1.6'nın sabit yapılandırma anahtarı sisteminin yerini alır (ChangeConfiguration anahtarlarla) ile esnek hiyerarşik model dayalı Bileşenler ve Değişkenler. Her istasyon kendi yapısını tamamen açıklar ve Satıcıdan bağımsız bir şekilde yapılandırma.
ChargingStation (radice)
|
+-- Controller (computer della stazione)
| +-- Variables: Vendor, Model, FirmwareVersion, SerialNumber
|
+-- EVSE[1] (punto di ricarica 1)
| +-- Variables: AvailabilityState, Power, SupplyPhases
| +-- Connector[1] (connettore CCS2 / DC)
| | +-- Variables: ConnectorType, AvailabilityState, MaxCurrent
| +-- Connector[2] (connettore CHAdeMO)
| +-- Variables: ConnectorType, AvailabilityState, MaxCurrent
|
+-- EVSE[2] (punto di ricarica 2)
| +-- Connector[1] (connettore Type2 AC / 22kW)
| +-- Variables: ConnectorType, Phases, MaxCurrent
|
+-- PowerMeter (contatore principale)
| +-- Variables: Energy.Active.Import.Register, Power.Active.Import
|
+-- NetworkInterface (ETH0/LTE)
| +-- Variables: Type, SSID, SignalStrength, ActiveNetworkProfile
|
+-- SecurityCtrlr
| +-- Variables: SecurityProfile, CertificateEntries
|
+-- SmartChargingCtrlr
+-- Variables: ChargingProfileMaxStackLevel, ChargeProfileKindsSupported
// CALL dal CSMS: legge stato EVSE e tipo connettore
[2, "get-var-001", "GetVariables", {
"getVariableData": [
{
"component": { "name": "EVSE", "evse": { "id": 1 } },
"variable": { "name": "AvailabilityState" },
"attributeType": "Actual"
},
{
"component": { "name": "Connector", "evse": { "id": 1, "connectorId": 1 } },
"variable": { "name": "ConnectorType" },
"attributeType": "Actual"
},
{
"component": { "name": "SmartChargingCtrlr" },
"variable": { "name": "ChargingProfileMaxStackLevel" },
"attributeType": "Actual"
}
]
}]
// CALLRESULT dalla Charging Station
[3, "get-var-001", {
"getVariableResult": [
{
"attributeStatus": "Accepted",
"component": { "name": "EVSE", "evse": { "id": 1 } },
"variable": { "name": "AvailabilityState" },
"attributeValue": "Available"
},
{
"attributeStatus": "Accepted",
"component": { "name": "Connector", "evse": { "id": 1, "connectorId": 1 } },
"variable": { "name": "ConnectorType" },
"attributeValue": "cCCS2"
},
{
"attributeStatus": "Accepted",
"component": { "name": "SmartChargingCtrlr" },
"variable": { "name": "ChargingProfileMaxStackLevel" },
"attributeValue": "5"
}
]
}]
OCPP Güvenlik Profilleri 2.0.1
OCPP 2.0.1 üç özelliği tanıtıyor Aşamalı Güvenlik Profilleri bu tanımlar Şarj İstasyonu ile CSMS arasındaki iletişimin koruma düzeyi. Profil dağıtım sırasında seçilmelidir ve mekanizmayı otomatik olarak yapılandırır kimlik doğrulama ve şifreleme.
| karakteristik | Profil 1 | Profil 2 | Profil 3 |
|---|---|---|---|
| WebSocket URL'si | ws:// (TLS yok) | wss:// (TLS) | wss:// (TLS) |
| Şifreleme | Hiçbiri | TLS 1.2+ | TLS 1.2+ |
| Kimlik Doğrulama İstasyonu | Şifre (Temel Kimlik Doğrulama) | Şifre (Temel Kimlik Doğrulama) | X.509 istemci sertifikası |
| Kimlik Doğrulama CSMS'si | Hiçbiri | TLS sunucu sertifikası | TLS sunucu sertifikası |
| MitM koruması | No | Kısmi (yalnızca CSMS kimlik doğrulaması) | Tam (karşılıklı TLS) |
| Sertifika Yönetimi | Gerekli değil | Cihazda yalnızca kök CA | Tam PKI: CA + müşteri sertifikası |
| Önerilen Kullanım | Yalnızca test ortamları | Standart üretim | Kritik Üretim, P&C ISO 15118 |
Üretimde Sertifikaların Yönetimi (Güvenlik Profili 3)
Güvenlik Profili 3 ile sertifika yaşam döngüsü yönetimi, kritik operasyon. OCPP 2.0.1 özel mesajları içerir: İmza Sertifikası (istasyon bir CSR'nin imzasını gerektirir), Sertifikaİmzalı (CSMS imzalı sertifikayı yükler), Sertifikayı Sil (eskimiş bir sertifikayı kaldırır), GetInstalledCertificateIds (kurulu sertifikaların listesi). Ve biri çok önemli Sağlam PKI en azından otomatik yenilemeyle Son kullanma tarihinden 30 gün önce, geçerliliğin sürekli izlenmesi ve CRL/OCSP iptal mekanizması.
Akıllı Şarj ve Yük Yönetimi
Lo Akıllı Şarj (K bloğu) ve en kritik işlevsellik büyük kurulumlara sahip operatörler. CSMS'nin dinamik olarak kontrol etmesini sağlar Şebeke kısıtlamalarına, enerji tarifelerine dayalı olarak her bir EVSE tarafından sağlanan güç, Kullanıcı önceliği ve transformatör kapasitesi.
Ücret Profilleri Hiyerarşisi
OCPP 2.0.1, yığın seviyeleri (öncelikler) ile dört tür şarj profilini tanımlar:
| Profil Türü | Kapsam | Başvuru | Geçersiz kıl |
|---|---|---|---|
| Şarj İstasyonuMax Profili | Tüm istasyonun mutlak maksimum sınırı | Trafo koruması, tedarik sözleşmesi | Geçersiz kılınamaz |
| Şarj İstasyonuHariciKısıtlamalar | Harici sistemlerden kaynaklanan sınırlamalar (DSO, toplayıcılar) | Talep yanıtı, ağ dengeleme | Yalnızca daha yüksek bir profilden |
| TxDefaultProfili | İşlemler için varsayılan profil | Tarife politikaları, temel planlama, güneş enerjisi | Belirli TxProfile'dan |
| Tx Profili | Bir işleme özel profil | Kullanıcı öncelikleri, bireysel tercihler | Maksimum gizlilik |
SetChargingProfile: Zirve Tıraş ve Güneş Enerjisi Entegrasyonu
// Strategia: integra produzione solare + peak shaving ore serali
// Scenario: sito con 50kW fotovoltaico, trasformatore 100A, picco 19-21h
// 1. Limite massimo stazione (rispetta contratto di fornitura)
[2, "smart-max-001", "SetChargingProfile", {
"evseId": 0,
"chargingProfile": {
"id": 1,
"stackLevel": 1,
"chargingProfilePurpose": "ChargingStationMaxProfile",
"chargingProfileKind": "Absolute",
"chargingSchedule": [{
"id": 1,
"chargingRateUnit": "A",
"chargingSchedulePeriod": [
{ "startPeriod": 0, "limit": 100.0 }
]
}]
}
}]
// 2. Profilo solare + peak shaving per un singolo EVSE
[2, "smart-solar-001", "SetChargingProfile", {
"evseId": 1,
"chargingProfile": {
"id": 100,
"stackLevel": 0,
"chargingProfilePurpose": "TxDefaultProfile",
"chargingProfileKind": "Absolute",
"validFrom": "2026-03-09T00:00:00Z",
"validTo": "2026-03-10T00:00:00Z",
"chargingSchedule": [{
"id": 1,
"chargingRateUnit": "A",
"startSchedule": "2026-03-09T06:00:00Z",
"chargingSchedulePeriod": [
{ "startPeriod": 0, "limit": 8.0, "numberPhases": 3 },
{ "startPeriod": 7200, "limit": 32.0, "numberPhases": 3 },
{ "startPeriod": 14400, "limit": 32.0, "numberPhases": 3 },
{ "startPeriod": 43200, "limit": 16.0, "numberPhases": 3 },
{ "startPeriod": 46800, "limit": 8.0, "numberPhases": 3 },
{ "startPeriod": 54000, "limit": 24.0, "numberPhases": 3 }
]
}]
}
}]
// Orario potenza: 06-08h: 8A (offpeak, bassa produzione solare)
// 08-12h: 32A (piena produzione FV, massima potenza)
// 12-18h: 32A (picco solare, alta produzione)
// 18-19h: 16A (calo solare, riduzione)
// 19-21h: 8A (picco domanda residenziale, min potenza)
// 21-24h: 24A (fine picco, potenza media)
Dinamik Yük Dengeleme algoritması
Bir yük yönetimi algoritması mevcut gücü oturumlar arasında dağıtır Trafo sınırına ve önceliklerine saygı göstererek gerçek zamanlı olarak aktif. En yaygın yaklaşım, Ağırlıklı Adil Pay min/maks kısıtlamalarla.
interface ChargingSession {
readonly stationId: string;
readonly evseId: number;
readonly transactionId: string;
readonly priority: number; // 0-9 (9 = massima)
readonly minChargingRate: number; // A minimi per caricare
readonly maxChargingRate: number; // A massimi del connettore
readonly currentChargingRate: number;
readonly energyDelivered: number; // Wh totali erogati
readonly targetEnergy?: number; // Wh target (se specificato dall'utente)
readonly isEV3Phase: boolean; // Veicolo trifase
}
interface LoadBalancerConfig {
readonly maxSitePowerAmps: number; // A max del trasformatore
readonly reservedBuildingAmps: number; // A riservati per l'edificio
readonly minSessionAmps: number; // A minimi per sessione (tipico: 6A)
readonly rebalanceIntervalSec: number; // Secondi tra ricalcoli (tipico: 30s)
}
interface Allocation {
readonly stationId: string;
readonly evseId: number;
readonly allocatedAmps: number;
readonly phases: number;
}
function calculateChargingAllocations(
sessions: ReadonlyArray<ChargingSession>,
config: LoadBalancerConfig
): ReadonlyArray<Allocation> {
if (sessions.length === 0) return [];
const availableAmps = config.maxSitePowerAmps
- config.reservedBuildingAmps;
// Step 1: ordina per priorità (desc), poi energia erogata (asc = meno carico prima)
const sorted = [...sessions].sort((a, b) => {
if (b.priority !== a.priority) return b.priority - a.priority;
return a.energyDelivered - b.energyDelivered;
});
// Step 2: garantisci potenza minima a tutti
const minRequired = sorted.length * config.minSessionAmps;
if (minRequired > availableAmps) {
// Caso critico: potenza insufficiente, sospendi sessioni a bassa priorità
return sorted
.slice(0, Math.floor(availableAmps / config.minSessionAmps))
.map((s) => ({
stationId: s.stationId,
evseId: s.evseId,
allocatedAmps: config.minSessionAmps,
phases: s.isEV3Phase ? 3 : 1,
}));
}
// Step 3: distribuzione proporzionale ai pesi di priorità
const totalWeight = sorted.reduce(
(sum, s) => sum + (1 + s.priority), 0
);
const remainingAmps = availableAmps - minRequired;
const allocations = sorted.map((session) => {
const weight = (1 + session.priority) / totalWeight;
const bonus = remainingAmps * weight;
const raw = config.minSessionAmps + bonus;
// Applica vincoli min/max del connettore
const allocatedAmps = Math.max(
config.minSessionAmps,
Math.min(session.maxChargingRate, Math.round(raw * 10) / 10)
);
return {
stationId: session.stationId,
evseId: session.evseId,
allocatedAmps,
phases: session.isEV3Phase ? 3 : 1,
};
});
// Step 4: verifica finale che il totale non superi il limite
const total = allocations.reduce((s, a) => s + a.allocatedAmps, 0);
if (total <= availableAmps) return allocations;
// Riscaling proporzionale
const scale = availableAmps / total;
return allocations.map((a) => ({
...a,
allocatedAmps: Math.max(
config.minSessionAmps,
Math.round(a.allocatedAmps * scale * 10) / 10
),
}));
}
Python ve PostgreSQL ile CSMS Arka Uç uygulaması
Python kütüphanesi ocpp MobilityHouse tarafından (açık kaynak, GitHub'da 2000'den fazla yıldız)
ve CSMS için en popüler referans uygulamasıdır. Kütüphaneyi birleştiriyoruz
asyncio, websockets e asyncpg PostgreSQL için
üretime hazır bir arka uç oluşturun.
CSMS için PostgreSQL şeması
-- Registro stazioni di ricarica
CREATE TABLE charging_stations (
station_id TEXT PRIMARY KEY,
vendor_name TEXT NOT NULL,
model TEXT NOT NULL,
serial_number TEXT,
firmware_version TEXT,
security_profile SMALLINT NOT NULL DEFAULT 1,
last_boot_reason TEXT,
last_seen_at TIMESTAMPTZ,
is_online BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Stato EVSE e connettori
CREATE TABLE evse_status (
station_id TEXT NOT NULL REFERENCES charging_stations(station_id),
evse_id SMALLINT NOT NULL,
connector_id SMALLINT NOT NULL,
connector_type TEXT, -- cCCS2, cCHAdeMO, cType2, sType3
status TEXT NOT NULL DEFAULT 'Unknown',
error_code TEXT,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (station_id, evse_id, connector_id)
);
-- Transazioni di ricarica
CREATE TABLE transactions (
transaction_id TEXT PRIMARY KEY,
station_id TEXT NOT NULL REFERENCES charging_stations(station_id),
evse_id SMALLINT NOT NULL,
connector_id SMALLINT,
id_token TEXT NOT NULL,
id_token_type TEXT NOT NULL,
state TEXT NOT NULL DEFAULT 'Started',
started_at TIMESTAMPTZ NOT NULL,
ended_at TIMESTAMPTZ,
meter_start_wh NUMERIC(12, 3),
meter_end_wh NUMERIC(12, 3),
energy_wh NUMERIC(12, 3) GENERATED ALWAYS AS (meter_end_wh - meter_start_wh) STORED,
stop_reason TEXT,
total_cost_cents INTEGER,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Meter values (time series - usare TimescaleDB in produzione)
CREATE TABLE meter_values (
id BIGSERIAL PRIMARY KEY,
transaction_id TEXT REFERENCES transactions(transaction_id),
station_id TEXT NOT NULL,
evse_id SMALLINT NOT NULL,
sampled_at TIMESTAMPTZ NOT NULL,
energy_wh NUMERIC(12, 3),
power_w NUMERIC(10, 2),
current_a NUMERIC(8, 3),
voltage_v NUMERIC(8, 2),
soc_pct SMALLINT -- State of Charge da ISO 15118
);
CREATE INDEX idx_meter_values_station_time
ON meter_values(station_id, sampled_at DESC);
CREATE INDEX idx_transactions_station_id
ON transactions(station_id, started_at DESC);
-- Token di autorizzazione (lista locale cache)
CREATE TABLE authorization_cache (
id_token TEXT NOT NULL,
id_token_type TEXT NOT NULL,
status TEXT NOT NULL, -- Accepted, Invalid, Blocked, Expired
group_id TEXT,
expiry_date TIMESTAMPTZ,
cached_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (id_token, id_token_type)
);
CSMS Tam Python Arka Uç
import asyncio
import logging
from datetime import datetime, timezone
from typing import Optional, Any
import asyncpg
import websockets
from ocpp.routing import on
from ocpp.v201 import ChargePoint as Cp
from ocpp.v201 import call, call_result
from ocpp.v201.enums import (
Action, RegistrationStatusType, AuthorizationStatusType,
ConnectorStatusType
)
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s %(name)s %(levelname)s %(message)s'
)
log = logging.getLogger('csms')
# Pool globale connessioni PostgreSQL
_db_pool: Optional[asyncpg.Pool] = None
async def get_db() -> asyncpg.Pool:
global _db_pool
if _db_pool is None:
_db_pool = await asyncpg.create_pool(
dsn='postgresql://csms:password@localhost/csms_db',
min_size=5,
max_size=20,
)
return _db_pool
class ChargePointHandler(Cp):
"""
Handler OCPP 2.0.1 per una singola Charging Station.
Un'istanza per ogni connessione WebSocket attiva.
"""
@on(Action.boot_notification)
async def on_boot_notification(
self, charging_station: dict, reason: str, **kwargs
) -> call_result.BootNotification:
log.info(
f"Boot: {self.id} | "
f"{charging_station['vendor_name']} {charging_station['model']} "
f"| reason={reason}"
)
db = await get_db()
await db.execute(
"""
INSERT INTO charging_stations
(station_id, vendor_name, model, serial_number,
firmware_version, last_boot_reason, last_seen_at, is_online)
VALUES ($1, $2, $3, $4, $5, $6, NOW(), TRUE)
ON CONFLICT (station_id) DO UPDATE SET
vendor_name = EXCLUDED.vendor_name,
model = EXCLUDED.model,
firmware_version = EXCLUDED.firmware_version,
last_boot_reason = EXCLUDED.last_boot_reason,
last_seen_at = NOW(),
is_online = TRUE
""",
self.id,
charging_station['vendor_name'],
charging_station['model'],
charging_station.get('serial_number'),
charging_station.get('firmware_version'),
reason,
)
return call_result.BootNotification(
current_time=datetime.now(timezone.utc).isoformat(),
interval=300,
status=RegistrationStatusType.accepted,
)
@on(Action.heartbeat)
async def on_heartbeat(self) -> call_result.Heartbeat:
db = await get_db()
await db.execute(
"UPDATE charging_stations SET last_seen_at = NOW() WHERE station_id = $1",
self.id
)
return call_result.Heartbeat(
current_time=datetime.now(timezone.utc).isoformat()
)
@on(Action.status_notification)
async def on_status_notification(
self, timestamp: str, connector_status: str,
evse_id: int, connector_id: int, **kwargs
) -> call_result.StatusNotification:
log.info(
f"Status: {self.id} EVSE[{evse_id}]"
f"Connector[{connector_id}] = {connector_status}"
)
db = await get_db()
await db.execute(
"""
INSERT INTO evse_status
(station_id, evse_id, connector_id, status, updated_at)
VALUES ($1, $2, $3, $4, NOW())
ON CONFLICT (station_id, evse_id, connector_id) DO UPDATE SET
status = EXCLUDED.status,
updated_at = NOW()
""",
self.id, evse_id, connector_id, connector_status
)
return call_result.StatusNotification()
@on(Action.authorize)
async def on_authorize(
self, id_token: dict, **kwargs
) -> call_result.Authorize:
token = id_token['id_token']
token_type = id_token['type']
log.info(f"Authorize: {self.id} token={token} type={token_type}")
db = await get_db()
# Controlla prima la cache locale
row = await db.fetchrow(
"""
SELECT status, group_id, expiry_date
FROM authorization_cache
WHERE id_token = $1 AND id_token_type = $2
AND (expiry_date IS NULL OR expiry_date > NOW())
""",
token, token_type
)
status = AuthorizationStatusType.invalid
if row and row['status'] == 'Accepted':
status = AuthorizationStatusType.accepted
return call_result.Authorize(
id_token_info={'status': status}
)
@on(Action.transaction_event)
async def on_transaction_event(
self,
event_type: str,
timestamp: str,
trigger_reason: str,
seq_no: int,
transaction_info: dict,
evse: Optional[dict] = None,
id_token: Optional[dict] = None,
meter_value: Optional[list] = None,
**kwargs
) -> call_result.TransactionEvent:
tx_id = transaction_info['transaction_id']
log.info(
f"TransactionEvent: {self.id} {event_type} "
f"tx={tx_id} trigger={trigger_reason}"
)
db = await get_db()
if event_type == 'Started':
await self._handle_tx_started(
db, tx_id, evse, id_token, timestamp, meter_value
)
elif event_type == 'Updated' and meter_value:
await self._handle_tx_updated(db, tx_id, evse, meter_value)
elif event_type == 'Ended':
await self._handle_tx_ended(
db, tx_id, timestamp, transaction_info, meter_value
)
return call_result.TransactionEvent(
total_cost=0,
charging_priority=0,
id_token_info={'status': AuthorizationStatusType.accepted},
)
async def _handle_tx_started(
self, db, tx_id, evse, id_token, timestamp, meter_value
):
evse_id = evse['id'] if evse else 0
connector_id = evse.get('connector_id') if evse else None
token = id_token['id_token'] if id_token else 'unknown'
token_type = id_token['type'] if id_token else 'Local'
meter_start = self._extract_energy(meter_value)
await db.execute(
"""
INSERT INTO transactions
(transaction_id, station_id, evse_id, connector_id,
id_token, id_token_type, state, started_at, meter_start_wh)
VALUES ($1, $2, $3, $4, $5, $6, 'Started', $7, $8)
ON CONFLICT (transaction_id) DO NOTHING
""",
tx_id, self.id, evse_id, connector_id,
token, token_type, timestamp, meter_start
)
async def _handle_tx_updated(self, db, tx_id, evse, meter_value):
evse_id = evse['id'] if evse else 0
energy = self._extract_energy(meter_value)
power = self._extract_power(meter_value)
if energy is not None:
await db.execute(
"""
INSERT INTO meter_values
(transaction_id, station_id, evse_id, sampled_at, energy_wh, power_w)
VALUES ($1, $2, $3, NOW(), $4, $5)
""",
tx_id, self.id, evse_id, energy, power
)
async def _handle_tx_ended(
self, db, tx_id, timestamp, transaction_info, meter_value
):
meter_end = self._extract_energy(meter_value)
stop_reason = transaction_info.get('stopped_reason')
await db.execute(
"""
UPDATE transactions SET
state = 'Ended',
ended_at = $1,
meter_end_wh = $2,
stop_reason = $3
WHERE transaction_id = $4
""",
timestamp, meter_end, stop_reason, tx_id
)
def _extract_energy(self, meter_values: Optional[list]) -> Optional[float]:
if not meter_values:
return None
for mv in meter_values:
for sv in mv.get('sampled_value', []):
if sv.get('measurand', '') == 'Energy.Active.Import.Register':
return float(sv['value'])
return None
def _extract_power(self, meter_values: Optional[list]) -> Optional[float]:
if not meter_values:
return None
for mv in meter_values:
for sv in mv.get('sampled_value', []):
if sv.get('measurand', '') == 'Power.Active.Import':
return float(sv['value'])
return None
# === Comandi CSMS -> Stazione ===
async def send_remote_start(
self, evse_id: int, id_token: str, limit_amps: float = 32.0
) -> str:
"""Avvia una sessione da remoto su EVSE specificato."""
request = call.RequestStartTransaction(
id_token={'id_token': id_token, 'type': 'Central'},
evse_id=evse_id,
charging_profile={
'id': 999,
'stack_level': 0,
'charging_profile_purpose': 'TxProfile',
'charging_profile_kind': 'Relative',
'charging_schedule': [{
'id': 1,
'charging_rate_unit': 'A',
'charging_schedule_period': [
{'start_period': 0, 'limit': limit_amps}
],
}],
},
)
response = await self.call(request)
log.info(f"RemoteStart {self.id} EVSE{evse_id}: {response.status}")
return response.status
async def send_charging_profile(
self, evse_id: int, profile: dict
) -> str:
"""Imposta un profilo di carica per smart charging."""
request = call.SetChargingProfile(
evse_id=evse_id,
charging_profile=profile
)
response = await self.call(request)
log.info(f"ChargingProfile {self.id} EVSE{evse_id}: {response.status}")
return response.status
# Registry globale delle connessioni attive
_connected_stations: dict[str, ChargePointHandler] = {}
async def on_connect(websocket, path: str):
"""Callback per nuove connessioni WebSocket OCPP."""
station_id = path.strip('/').split('/')[-1]
if not station_id:
await websocket.close(1008, 'Missing station ID')
return
log.info(f"Connessione da: {station_id} | path={path}")
cp = ChargePointHandler(station_id, websocket)
_connected_stations[station_id] = cp
try:
await cp.start()
except websockets.exceptions.ConnectionClosed as e:
log.info(f"Disconnesso: {station_id} code={e.code}")
except Exception as e:
log.error(f"Errore: {station_id} - {e}")
finally:
_connected_stations.pop(station_id, None)
db = await get_db()
await db.execute(
"UPDATE charging_stations SET is_online = FALSE WHERE station_id = $1",
station_id
)
async def main():
await get_db() # Inizializza pool DB
log.info("CSMS OCPP 2.0.1 avviato")
server = await websockets.serve(
on_connect,
'0.0.0.0',
9000,
subprotocols=['ocpp2.0.1'],
# TLS: aggiungere ssl=ssl_context per Security Profile 2-3
ping_interval=60,
ping_timeout=30,
max_size=1_048_576, # 1MB max message size
)
log.info("In ascolto su ws://0.0.0.0:9000/ocpp/{stationId}")
await server.wait_closed()
if __name__ == '__main__':
asyncio.run(main())
ISO 15118 ve Tak ve Şarj Et
ISO 15118 Araçlar arasında üst düzey iletişimi tanımlar Güç Hattı İletişimi aracılığıyla elektrik (EV) ve şarj istasyonu (EVSE) (PLC) DC şarj kablosuna (CCS) bağlayın. OCPP 2.0.1, ISO 15118'i yerel olarak entegre eder M fonksiyonel bloğu aracılığıyla Tak ve Şarj Et: araç, X.509 dijital sertifikası aracılığıyla otomatik olarak kimlik doğrulaması yapar. RFID veya mobil uygulama.
V2G (Araçtan Şebekeye) PKI mimarisi
Tak ve Şarj sertifika sistemi PKI'yı (Genel Anahtar) temel alır. Elektrikli mobilite için altyapıya özgü hiyerarşi:
V2G Root CA (Root of Trust - gestita da OEM o eMSP)
|
+-- V2G Intermediate CA
| |
| +-- EVSE Certificate (installato nella stazione)
| CN = EVSE-IT-MIL-001
|
+-- eMobility Service Provider CA (eMSP)
|
+-- Contract Certificate (installato nel veicolo)
CN = IT.CPO.000001234 (eMAID - e-Mobility Account Identifier)
SubjectAltName = eMAID:IT.CPO.000001234
Tam Tak ve Şarj akışı
EV EVSE CSMS eMSP
| | | |
|-- Plug cavo DC --> | | |
| | | |
|<= ISO 15118-2 TLS =>| (PLC sul cavo SLAC) | |
| | | |
|-- ContractCert --->| | |
| (eMAID, X.509) | | |
| |--- Authorize req ----->| |
| | idToken.type=eMAID | |
| | |--- OCPI check --->|
| | |<-- Contract OK --|
| | | |
| |<-- Authorize.conf ----| |
| | status: Accepted | |
| | | |
|<= Charging Start ==>| | |
| | | |
| EV invia target | | |
|-- EnergyRequest --->| | |
| SoC: 45% | | |
| Target: 80% | | |
| Departure: 18:30 | | |
| |--- TransactionEvent -->| |
| | ISO15118Trigger | |
| | | |
| |<-- SetChargingProfile--| |
|<= Schedule via PLC =>| | |
ISO 15118 Üretimdeki Durumu (2026)
- ISO 15118-2: Tak ve Şarj AC/DC - HPC DC şarj cihazları (Ionity, Fastned, Tesla Supercharger V3) tarafından yaygın olarak desteklenir
- ISO 15118-20: Çift Yönlü V2G desteği - donanım desteği hazır, yazılım 2025-2026'da kullanıma sunulacak
- AFIR Gereksinimi: Tüm yeni V2G özellikli istasyonlar 2026'dan itibaren ISO 15118'i desteklemelidir
- AFIR 2027 uyumluluğu: 01/01/2027 tarihinden sonra takılan tüm şarj cihazlarının akıllı şarja hazır olması gerekir
- İtalya'da Gerçek V2G: V2H (Araçtan Eve) standardında Enel X Way ve Nissan Leaf ile ilk pilotlar
Ölçeklenebilir Mimari: 10'dan 100.000'e kadar Şarj Noktası
Kurumsal bir CSMS'nin birkaç on ila yüz binlerce bağlantıyı yönetmesi gerekir Rakip WebSocket'ler. Mimari, ek bileşenlerle aşamalar halinde gelişir. farklı ölçeklerde devreye giriyorlar.
Aşama 1: Küçük Ölçekli (10-500 istasyon)
+------------------+ WebSocket/OCPP +------------------+
| Charging |------------------------| CSMS Monolitico |
| Stations (10-500)| wss://csms:9000/ocpp | Python/asyncio |
+------------------+ | Port 9000 |
+--------+---------+
|
+-------+-------+
| PostgreSQL |
| Redis (cache) |
+---------------+
Stack: Python asyncio + PostgreSQL + Redis
Deployment: 1 VM (4 vCPU, 8GB RAM), 1 DB managed
Costo: ~$200/mese
Aşama 2: Orta Ölçekli (500-10.000 istasyon)
+------------+ +------------------+ +--------------+
| Load | | WS Gateway #1 | | Message |
| Balancer +---->| (asyncio CSMS) +---->| Broker |
| (HAProxy) | | Max 2000 conn | | (RabbitMQ) |
| | +------------------+ | |
| Sticky +---->| WS Gateway #2 +---->| |
| Sessions | | (asyncio CSMS) | +--------------+
| | +------------------+ |
+------------+ +------+------+
| Business |
| Services |
| (FastAPI) |
+------+------+
|
+----------+----------+
| PostgreSQL (HA) |
| TimescaleDB |
| Redis Cluster |
+---------------------+
Sticky sessions: basate su station_id nel path URL
Cross-node ops: Redis pub/sub per inviare comandi alle stazioni
Costo: ~$2.000/mese (K8s managed)
Aşama 3: Büyük Ölçekli (10.000-100.000 istasyon)
Global Load Balancer (Anycast)
|
+---------------+---------------+
| |
Region EU-WEST Region EU-SOUTH
+------------------+ +------------------+
| WS Gateway Pool | | WS Gateway Pool |
| (50 pods, 2000 | | (30 pods) |
| conn each = 100K)| +--------+---------+
+--------+---------+ |
| |
+-------------+----------------+
|
+-------+--------+
| Apache Kafka |
| (12 partitions)|
| per topic |
+-------+--------+
|
+-----------------+------------------+
| | |
+------+------+ +-------+------+ +--------+------+
| Transaction | | Smart | | Device |
| Service | | Charging Svc | | Mgmt Svc |
| (10 replicas)| | (5 replicas) | | (3 replicas) |
+------+------+ +-------+------+ +--------+------+
| | |
+--------+--------+------------------+
|
+--------+--------+
| PostgreSQL |
| Citus (sharding) |
| Shard key: |
| station_id hash |
+--------+---------+
|
+--------+--------+
| TimescaleDB |
| (meter values) |
+--------+--------+
Kafka Topics:
- ocpp.boot-notification (chiave: station_id)
- ocpp.transaction-events (chiave: transaction_id)
- ocpp.meter-values (chiave: station_id)
- ocpp.status-notifications (chiave: station_id)
- csms.commands (chiave: station_id)
Throughput target: 1M messaggi/ora, latenza P99 < 200ms
Costo: ~$30.000/mese (multi-region Kubernetes)
Redis ile Düğümler Arası Bağlantı Yönetimi
Çok düğümlü bir dağıtımda CSMS'nin her birinin hangi ağ geçidinde olduğunu bilmesi gerekir komutları göndermek için istasyona (SetChargingProfile, RemoteStart, vb.) Redis pub/sub sorunu çözer:
import json
import asyncio
import redis.asyncio as aioredis
redis_client = aioredis.from_url(
'redis://redis-cluster:6379',
encoding='utf-8',
decode_responses=True
)
# Registra il nodo della connessione
async def register_connection(station_id: str, gateway_id: str):
await redis_client.setex(
f"csms:gateway:{station_id}",
value=gateway_id,
time=600 # TTL: 10 minuti, rinnovato a ogni heartbeat
)
# Pubblica un comando verso una stazione (qualunque nodo sia)
async def publish_command(station_id: str, action: str, payload: dict):
channel = f"csms:commands:{station_id}"
await redis_client.publish(channel, json.dumps({
'action': action,
'payload': payload
}))
# Su ogni nodo gateway: ascolta i comandi per le stazioni connesse
async def listen_for_commands(connected_stations: dict):
pubsub = redis_client.pubsub()
# Sottoscrivi ai canali delle stazioni connesse a questo nodo
async def subscribe_station(station_id: str):
await pubsub.subscribe(f"csms:commands:{station_id}")
async for message in pubsub.listen():
if message['type'] != 'message':
continue
station_id = message['channel'].split(':')[-1]
cp = connected_stations.get(station_id)
if not cp:
continue # Stazione non su questo nodo, ignora
cmd = json.loads(message['data'])
try:
if cmd['action'] == 'SetChargingProfile':
await cp.send_charging_profile(
cmd['payload']['evse_id'],
cmd['payload']['profile']
)
elif cmd['action'] == 'RemoteStart':
await cp.send_remote_start(
cmd['payload']['evse_id'],
cmd['payload']['id_token'],
)
except Exception as e:
log.error(f"Errore esecuzione comando {cmd['action']}: {e}")
İzleme, Metrikler ve Grafana Kontrol Paneli
Üretimdeki bir CSMS, kapsamlı bir gözlemlenebilirlik sistemi gerektirir. Metrikler izlemenin anahtarı altyapının sağlığı ve kalitesi ile ilgilidir. hizmet ve operasyonel performans.
Temel Operasyonel Metrikler
| Metrik | Formül/Kaynak | Hedef SLA | Uyarı Eşiği |
|---|---|---|---|
| İstasyon Kullanılabilirliği | Çevrimiçi istasyonlar / Toplam istasyon x 100 | >= %99 | < %95 |
| OCPP Mesaj Gecikmesi P99 | ÇAĞRI süresi -> ÇAĞRI SONUCU (95. yüzdelik dilim) | < 2s | > 5s |
| İşlem Başarı Oranı | TX tamamlandı / TX başladı x 100 | >= %98 | < %95 |
| Sağlanan Enerji (kWh/saat) | Şimdilik Toplam MetreDeğerleri | Başlangıç +%10 | < başlangıç -%20 |
| Kimlik Doğrulama Reddetme Oranı | Kimlik Doğrulaması Geçersiz / Toplam Kimlik Doğrulaması x 100 | < %2 | > %10 (olası saldırı) |
| WebSocket Yeniden Bağlantı Sayısı/saat | İstasyon başına yeni bağlantıları sayın | < 2/saat/istasyon | > 10/saat/istasyon |
| Akıllı Şarj Uyumluluğu | Gerçek güç ve ayarlanan profil | +/- %5 | Sapma > %15 |
| Sertifikalı Sona Erme Günleri | TLS sertifikalarının süresinin dolmasına kalan gün sayısı | > 30 gün | < 30 gün (yenileme uyarısı) |
CSMS için Prometheus İhracatçısı
from prometheus_client import (
Counter, Gauge, Histogram, start_http_server
)
# Metriche Prometheus
OCPP_MESSAGES_TOTAL = Counter(
'ocpp_messages_total',
'Numero totale messaggi OCPP processati',
['action', 'direction', 'status'] # direction: inbound/outbound
)
OCPP_MESSAGE_DURATION = Histogram(
'ocpp_message_duration_seconds',
'Latenza elaborazione messaggi OCPP',
['action'],
buckets=[0.01, 0.05, 0.1, 0.5, 1.0, 2.0, 5.0, 10.0]
)
STATIONS_CONNECTED = Gauge(
'csms_stations_connected_total',
'Numero stazioni connesse al CSMS'
)
ACTIVE_TRANSACTIONS = Gauge(
'csms_active_transactions_total',
'Numero transazioni di ricarica attive'
)
ENERGY_DELIVERED_WH = Counter(
'csms_energy_delivered_wh_total',
'Energia totale erogata in Wh',
['station_id']
)
AUTH_RESULTS = Counter(
'csms_authorization_results_total',
'Risultati delle autorizzazioni OCPP',
['status'] # Accepted, Invalid, Blocked, Expired
)
SMART_CHARGING_EVENTS = Counter(
'csms_smart_charging_events_total',
'Operazioni smart charging',
['action', 'result']
)
def start_metrics_server(port: int = 8001):
"""Avvia il server HTTP Prometheus su porta specificata."""
start_http_server(port)
log.info(f"Prometheus metrics su http://0.0.0.0:{port}/metrics")
# Decorator per misurare latenza handler
import time
import functools
def track_ocpp_handler(action: str):
def decorator(func):
@functools.wraps(func)
async def wrapper(*args, **kwargs):
start = time.monotonic()
try:
result = await func(*args, **kwargs)
OCPP_MESSAGES_TOTAL.labels(
action=action, direction='inbound', status='success'
).inc()
return result
except Exception as e:
OCPP_MESSAGES_TOTAL.labels(
action=action, direction='inbound', status='error'
).inc()
raise
finally:
OCPP_MESSAGE_DURATION.labels(action=action).observe(
time.monotonic() - start
)
return wrapper
return decorator
// Pannelli principali per dashboard Grafana CSMS
// 1. Stazioni online (Gauge)
{
"title": "Stazioni Connesse",
"type": "gauge",
"fieldConfig": {
"defaults": {
"thresholds": {
"steps": [
{ "color": "red", "value": 0 },
{ "color": "yellow", "value": 90 },
{ "color": "green", "value": 99 }
]
}
}
},
"targets": [{
"expr": "csms_stations_connected_total / csms_stations_registered_total * 100",
"legendFormat": "Availability %"
}]
}
// 2. Latenza messaggi OCPP P99 (Time Series)
{
"title": "OCPP Message Latency P99",
"type": "timeseries",
"targets": [{
"expr": "histogram_quantile(0.99, rate(ocpp_message_duration_seconds_bucket[5m]))",
"legendFormat": "P99 - {{action}}"
}]
}
// 3. Energia erogata (Stat panel)
{
"title": "Energia Totale Erogata oggi (kWh)",
"type": "stat",
"targets": [{
"expr": "increase(csms_energy_delivered_wh_total[24h]) / 1000",
"legendFormat": "kWh"
}]
}
// 4. Auth rejection rate - alert su attacchi
{
"title": "Rejection Rate Autorizzazioni (%)",
"type": "timeseries",
"targets": [{
"expr": "rate(csms_authorization_results_total{status='Invalid'}[5m]) / rate(csms_authorization_results_total[5m]) * 100"
}]
}
İtalyan ve Avrupa düzenlemeleri: AFIR, PNIRE, PNRR
İtalya ve Avrupa'daki elektrikli araç şarj altyapısı sıkı bir şekilde denetleniyor. Mevzuata uygunluk isteğe bağlı değildir: hem teknik gerekliliklerle ilgilidir istasyonlar (yasal metroloji, erişilebilirlik, ödeme) ve iletişim standartları.
AFIR (Alternatif Yakıtlar Altyapı Yönetmeliği - AB 2023/1804)
AFIR, Nisan 2024'te yürürlüğe girmiştir ve kesin bir zaman çizelgesi tanımlamaktadır. TEN-T ağlarında ve kentsel alanlarda altyapının ücretlendirilmesine ilişkin yükümlülükler:
| Son kullanma tarihi | Gereklilik | Uygulanabilirlik |
|---|---|---|
| 31 Aralık 2025 | İstasyon >= TEN-T Çekirdek ağında her 60 km'de bir 150 kW | Bütün AB |
| 31 Aralık 2027 | İstasyon >= 150 kW her 60 km'de TEN-T Kapsamlı ağ | Bütün AB |
| 14 Nisan 2025 | Ücretsiz statik ve dinamik veriler (konum, bağlayıcı türü, kullanılabilirlik) | Halka açık istasyonlar |
| 01 Ocak 2027 | Yeni/yenilenmiş istasyonlar için hazır akıllı şarj > 22 kW | Halka açık istasyonlar |
| hemen | Abonelik olmadan anlık ödeme (temassız banka kartı) | İstasyonlar > 50 kW kamuya açık |
| 2026+ | V2G özellikli istasyonlar için ISO 15118 | Çift yönlü istasyonlar |
İtalya için PNIRE ve PNRR
İtalya'da AFIR'ın uygulanması iki ana araçla gerçekleşir:
- PNIRE (Ulusal Elektrik Şarj Altyapı Planı): MASE (Çevre ve Enerji Güvenliği Bakanlığı) tarafından yönetilen ulusal hedefler: 2025'te 13.755 kamu istasyonu, karayolu ağına odaklanmak ve kentsel alanlar
- PNRR Misyon 2, Yatırım 4.3: 700 milyon euronun üzerinde otoyollarda ve alanlarda Yüksek Güçlü Şarj (HPC >= 150 kW) için tahsis edilmiştir hizmet. MEMORY'ye göre PNRR toplam 12,7 milyar Avro tahsis etti. hala kısmi kullanım
- MASE çağrısı 2024-2025: özel işletmecilere yönelik teşvikler Düşük şarj yoğunluğuna sahip bölgelere (Güney İtalya, kırsal alanlar). Kurulum maliyetlerinin %60'ına kadar sübvansiyon
İtalya'daki durum: 2025'te 73.000 şarj noktası
31 Aralık 2025 itibarıyla İtalya sayılıyor 73.000'den fazla halka açık şarj noktası (2024'e kıyasla +%18), %93 ulusal bölgesel kapsam. Bunlardan: yaklaşık 12.000'i hızlı şarj noktasıdır (> 22 kW) ve 5.000'i HPC'dir (>= 150 kW). En fazla altyapıya sahip bölge Lombardiya (%23) olurken, onu takip ediyor Lazio'dan (%12) ve Toskana'dan (%9). Güney benim için öncelikli bir bölge olmaya devam ediyor PNRR finansmanı, açığı 2026 yılına kadar kapatmayı hedefliyor.
CSMS Yazılımına İlişkin Teknik Yükümlülükler
- OCPI (Açık Şarj Noktası Arayüzü): CPO (Şarj Noktası Operatörü) ve eMSP (e-Mobilite Hizmet Sağlayıcısı) arasında dolaşım için zorunlu protokol. OCPI sürüm 2.2.1 önerilir
- Yasal Metroloji: Almanya'da (Eichrecht) ve aşamalı olarak AB'de (MID - Ölçüm Cihazları Direktifi), ölçüm sistemleri sertifikalı olmalı ve okumalar şeffaf olmalı ve kullanıcı tarafından değiştirilemez olmalıdır
- GDPR: Şarj seansı verileri (RFID, konum, saatler) kişisel verilerdir. Gizlilik politikası, veri minimizasyonu ve unutulma hakkı gerektirir
- CDR (Ücret Ayrıntı Kayıtları): Birlikte çalışabilir faturalandırma için OCPI formatına uygun olmalı ve en az 5 yıl saklanmalıdır (İtalyan vergi yükümlülükleri)
Örnek Olay: 50'den Fazla İstasyona Sahip İtalyan Şarj Ağı
İtalyan bir operatör için gerçek bir CSMS sisteminin mimarisini inceleyelim. şehir içi otoparklara dağıtılan 50 DC şarj istasyonunu (22-150 kW) yönetiyor ve 3 bölgede alışveriş merkezleri.
Operasyonel Gereksinimler
- 50 istasyon, toplam 150 EVSE (istasyon başına ortalama 3 EVSE), 300 konnektör (CCS2 + Tip2)
- Günlük zirve: 400-600 şarj seansı (sabah 7-9, akşam 12-14, akşam 17-21)
- Akıllı şarj: istasyon başına 200A sınırına uygunluk, SEM (Site Enerji Yöneticisi) ile entegrasyon
- Çok kiracılı: Kısmi istasyon görünürlüğüne sahip 3 CPO, Enel ile OCPI dolaşımı
- Çalışma Süresi SLA'sı: İstasyon başına aylık %99,5, CSMS arka ucu için %99,9
Seçilmiş Mimari
+------------------+ +------------------+ +------------------+
| 50 Stazioni | | HAProxy | | CSMS Primary |
| OCPP 2.0.1 |---->| (WS sticky sess.)|---->| Python asyncio |
| Security Profile 2| | Port 443 (TLS) | | 2 replicas |
+------------------+ +------------------+ +--------+---------+
|
+------------------+ +--------+---------+
| CSMS Worker |<----| Redis Cluster |
| (FastAPI REST) | | (stato sessioni) |
| Dashboard, API | +------------------+
+------------------+
|
+-----------+----------+
| |
+-------+-------+ +---------+------+
| PostgreSQL 16 | | TimescaleDB |
| (transazioni, | | (meter values)|
| auth, device | | 2TB/anno est. |
| model, CDR) | +---------------+
+---------------+
Monitoring: Prometheus + Grafana Cloud
Alerting: PagerDuty (P1: stazione offline >5min, P2: CSMS latency >2s)
CDR/Billing: integrazione ERP via webhook PostgreSQL NOTIFY
Gerçek Operasyonel Metrikler (Tipik Ay)
| Metrik | Değer | Notlar |
|---|---|---|
| Oturum/ay | ~14.000 | Ortalama 280 seans/gün |
| Verilen enerji | ~85.000 kWh/ay | Ortalama 6 kWh/sesans |
| Ortalama istasyon kullanılabilirliği | %99,3 | %0,7 kesinti = istasyon başına ~5 saat/ay |
| Kimlik Doğrulama Kabul Oranı | %96,8 | %3,2 reddedildi = süresi dolmuş veya kaydedilmemiş |
| OCPP Gecikme P95 | 180ms | Gidiş Dönüş LTE istasyonunu içerir |
| Akıllı Şarj Etkinlikleri/gün | ~1.200 | Ortalama 24 yeniden dengeleme/saat |
| Zirve Tıraş Etkinliği | %92 | Zamanın %92'si güç <= ayarlanan limit |
CSMS güvenliği: OWASP ve Tehdit Modeli
CSMS kritik bir altyapıdır: bir uzlaşma kesintiye neden olabilir şarj etme, faturalandırmada manipülasyon veya elektrik şebekesine saldırılar yüklerin şarj edilmesi yoluyla. Ana tehditler şu şekilde tanımlanabilir: belirli bir tehdit modeli.
CSMS Tehdit Modeli
| Tehdit | Vektör | Darbe | Azaltma |
|---|---|---|---|
| Yetkisiz istasyon | Çalınan kimlik bilgileriyle bağlantı | Yanlış veri enjeksiyonu, hileli tüketim | Güvenlik Profili 3 (mTLS), sertifikalı sabitleme |
| Ortadaki Adam | Güvenli olmayan ağlarda WS müdahalesi | RFID belirteci müdahalesi, komut manipülasyonu | TLS 1.3 zorunlu, sertifikalı şeffaflık |
| Tekrar Saldırısı | Yakalanan OCPP mesajlarının yeniden iletilmesi | Çift faturalandırma, geçersiz izinler | Benzersiz Mesaj Kimliği, zaman damgası doğrulaması, tek seferlik |
| DDoS Web Soketi | Bağlantı veya mesaj seli | CSMS'ye erişilemiyor, altyapı DoS'si | Hız sınırlama, bağlantı azaltma, WAF |
| OCPP yükü aracılığıyla SQL Enjeksiyonu | idToken alanında SQL verisi bulunan OCPP verisi | Veritabanı sızması, ayrıcalık yükseltme | Hazırlanan ifadeler, ORM, giriş doğrulama |
| RFID Klonlama | Meşru RFID kartlarının klonlanması | Diğer kullanıcılar tarafından ödenen oturumlar | ISO 15118 P&C, RFID beyaz listesi, anormallik tespiti |
| Kötü Amaçlı Firmware | Kötü amaçlı yazılım içeren ürün yazılımı güncellemesi | İstasyonun fiziksel kontrolü, şebeke manipülasyonu | Ürün yazılımı dijital imzası, güvenli önyükleme, SBOM |
CSMS Güçlendirme: Güvenlik Kontrol Listesi
import ssl
import re
from functools import wraps
# 1. Configurazione TLS sicura (Security Profile 2-3)
def create_tls_context(
certfile: str,
keyfile: str,
cafile: str,
require_client_cert: bool = False
) -> ssl.SSLContext:
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ctx.minimum_version = ssl.TLSVersion.TLSv1_2
ctx.load_cert_chain(certfile=certfile, keyfile=keyfile)
if require_client_cert: # Security Profile 3 (mTLS)
ctx.verify_mode = ssl.CERT_REQUIRED
ctx.load_verify_locations(cafile=cafile)
# Disabilita cipher suite deboli
ctx.set_ciphers(
'ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:!aNULL:!eNULL:!LOW:!EXPORT'
)
return ctx
# 2. Rate limiting per connessioni WebSocket
from collections import defaultdict
import time
_connection_attempts: dict[str, list[float]] = defaultdict(list)
MAX_CONN_PER_MINUTE = 10
def check_rate_limit(client_ip: str) -> bool:
"""Ritorna True se il client può connettersi, False se throttled."""
now = time.monotonic()
window = _connection_attempts[client_ip]
# Rimuovi tentativi più vecchi di 60 secondi
_connection_attempts[client_ip] = [
t for t in window if now - t < 60
]
if len(_connection_attempts[client_ip]) >= MAX_CONN_PER_MINUTE:
log.warning(f"Rate limit superato per IP: {client_ip}")
return False
_connection_attempts[client_ip].append(now)
return True
# 3. Validazione MessageId per prevenire replay attack
_seen_message_ids: set[str] = set()
_message_id_pattern = re.compile(r'^[a-zA-Z0-9\-_\.]{1,36}






