1use ocpp_core::{ErrorCode, OcppVersion, RpcError};
7use ocpp21::*;
8use serde_json::Value;
9
10use super::{StationCtx, i32_of, internal};
11use crate::db::{self, Ts, stations::BootInfo, stations::StatusUpdate, transactions::TxEvent};
12
13pub fn decode_error(e: DecodeError) -> RpcError {
14 match e {
15 DecodeError::UnknownAction(a) => RpcError::not_implemented(&a),
16 DecodeError::WrongDirection(a) => RpcError::not_supported(a.as_str()),
17 DecodeError::Invalid(v) => v.into(),
18 }
19}
20
21fn json<T: serde::Serialize>(v: T) -> Result<Value, RpcError> {
22 serde_json::to_value(v).map_err(internal)
23}
24
25fn id_token_info(a: &db::tokens::AuthResult) -> IdTokenInfo {
26 let status = match a.status {
27 "Accepted" => AuthorizationStatusEnum::Accepted,
28 "Blocked" => AuthorizationStatusEnum::Blocked,
29 "Expired" => AuthorizationStatusEnum::Expired,
30 "Unknown" => AuthorizationStatusEnum::Unknown,
31 _ => AuthorizationStatusEnum::Invalid,
32 };
33 IdTokenInfo {
34 custom_data: None,
35 status,
36 cache_expiry_date_time: a.expires_at.map(|t| t.0),
37 charging_priority: None,
38 language1: None,
39 evse_id: None,
40 group_id_token: a.group_id.as_ref().map(|g| IdToken {
41 custom_data: None,
42 additional_info: None,
43 id_token: g.clone(),
44 r#type: "Central".to_owned(),
45 }),
46 language2: None,
47 personal_message: None,
48 }
49}
50
51fn measurand(values: &[MeterValue], m: MeasurandEnum) -> Option<f64> {
54 let scale = |s: &SampledValue| {
55 let (unit, mult) = s
56 .unit_of_measure
57 .as_ref()
58 .map(|u| (u.unit.as_deref().unwrap_or("Wh"), u.multiplier.unwrap_or(0)))
59 .unwrap_or(("Wh", 0));
60 let v = s.value * 10f64.powi(mult as i32);
61 if unit.starts_with('k') { v * 1000.0 } else { v }
62 };
63 for mv in values.iter().rev() {
64 let matching: Vec<&SampledValue> = mv
66 .sampled_value
67 .iter()
68 .filter(|s| {
69 s.measurand
70 .unwrap_or(MeasurandEnum::EnergyActiveImportRegister)
71 == m
72 })
73 .collect();
74 if let Some(total) = matching.iter().find(|s| s.phase.is_none()) {
75 return Some(scale(total));
76 }
77 let phases: Vec<f64> = matching
78 .iter()
79 .filter(|s| matches!(s.phase, Some(PhaseEnum::L1 | PhaseEnum::L2 | PhaseEnum::L3)))
80 .map(|s| scale(s))
81 .collect();
82 if !phases.is_empty() {
83 return Some(phases.iter().sum());
84 }
85 }
86 None
87}
88
89fn energy_wh(values: &[MeterValue]) -> Option<f64> {
90 measurand(values, MeasurandEnum::EnergyActiveImportRegister)
91}
92
93fn message(text: String) -> MessageContent {
94 let content: String = text.chars().take(512).collect();
95 MessageContent {
96 format: MessageFormatEnum::UTF8,
97 language: None,
98 content,
99 custom_data: None,
100 }
101}
102
103fn additional_info<'a>(token: Option<&'a IdToken>, kind: &str) -> Option<&'a str> {
104 token?
105 .additional_info
106 .as_ref()?
107 .iter()
108 .find(|a| a.r#type == kind)
109 .map(|a| a.additional_id_token.as_str())
110}
111
112pub async fn handle(ctx: &mut StationCtx, action: &str, payload: &str) -> Result<Value, RpcError> {
114 let call = StationCall::decode(action, payload).map_err(decode_error)?;
115 match call {
116 StationCall::BootNotification(r) => {
117 let cs = &r.charging_station;
118 let info = BootInfo {
119 vendor: &cs.vendor_name,
120 model: &cs.model,
121 serial_number: cs.serial_number.as_deref(),
122 firmware_version: cs.firmware_version.as_deref(),
123 iccid: cs.modem.as_ref().and_then(|m| m.iccid.as_deref()),
124 imsi: cs.modem.as_ref().and_then(|m| m.imsi.as_deref()),
125 meter_type: None,
126 meter_serial_number: None,
127 reason: Some(r.reason.as_str()),
128 };
129 let status = match ctx.boot(&info).await? {
130 super::Registration::Pending => RegistrationStatusEnum::Pending,
131 super::Registration::Rejected => RegistrationStatusEnum::Rejected,
132 _ => RegistrationStatusEnum::Accepted,
133 };
134 json(BootNotificationResponse {
135 custom_data: None,
136 current_time: StationCtx::now(),
137 interval: ctx.heartbeat_interval(),
138 status,
139 status_info: None,
140 })
141 }
142 StationCall::Heartbeat(_) => {
143 ctx.heartbeat().await?;
144 json(HeartbeatResponse {
145 custom_data: None,
146 current_time: StationCtx::now(),
147 })
148 }
149 StationCall::StatusNotification(r) => {
150 ctx.status(&StatusUpdate {
151 evse_number: i32_of(r.evse_id),
152 connector_number: i32_of(r.connector_id),
153 status: r.connector_status.as_str(),
154 error_code: None,
155 info: None,
156 vendor_error_code: None,
157 at: Ts(r.timestamp),
158 })
159 .await?;
160 json(StatusNotificationResponse::default())
161 }
162 StationCall::Authorize(r) => {
163 let ty = r.id_token.r#type.as_str();
164 let a = ctx.authorize(&r.id_token.id_token, Some(ty)).await?;
165 let mut info = id_token_info(&a);
166 let mut tariff = None;
167 if a.status == "Accepted" {
168 let extras = crate::tariff::on_authorize(
170 &ctx.app,
171 ctx.station_id,
172 OcppVersion::V21,
173 &r.id_token.id_token,
174 Some(ty),
175 )
176 .await
177 .map_err(internal)?;
178 info.personal_message = extras.personal_message.map(message);
179 tariff = extras.tariff;
180 }
181 json(AuthorizeResponse {
182 id_token_info: info,
183 certificate_status: None,
184 allowed_energy_transfer: None,
185 tariff,
186 custom_data: None,
187 })
188 }
189 StationCall::TransactionEvent(r) => {
190 let ti = &r.transaction_info;
191 let event = TxEvent {
192 version: "2.1",
193 uid: &ti.transaction_id,
194 event_type: r.event_type.as_str(),
195 at: Some(Ts(r.timestamp)),
196 seq_no: r.seq_no,
197 evse_number: r.evse.as_ref().map(|e| i32_of(e.id)),
198 connector_number: r.evse.as_ref().and_then(|e| e.connector_id).map(i32_of),
199 id_token: r.id_token.as_ref().map(|t| t.id_token.as_str()),
200 id_token_type: r.id_token.as_ref().map(|t| t.r#type.as_str()),
201 charging_state: ti.charging_state.map(|s| s.as_str()),
202 stop_reason: ti.stopped_reason.map(|s| s.as_str()),
203 reservation_id: r.reservation_id.map(i32_of),
204 remote_start_id: ti.remote_start_id.map(i32_of),
205 energy_wh: r.meter_value.as_deref().and_then(energy_wh),
206 };
207 let tx_id = db::transactions::apply_event(&ctx.app.db, ctx.station_id, &event)
208 .await
209 .map_err(internal)?;
210 if let Some(reservation) = r.reservation_id {
211 crate::reservation::on_used(&ctx.app, ctx.station_id, reservation, tx_id)
213 .await
214 .map_err(internal)?;
215 }
216 if r.event_type == TransactionEventEnum::Ended {
217 db::charging::clear_for_transaction(
219 &ctx.app.db,
220 ctx.station_id,
221 &ti.transaction_id,
222 )
223 .await
224 .map_err(internal)?;
225 }
226 let evse = event.evse_number.unwrap_or(0);
227 for mv in r.meter_value.iter().flatten() {
228 let sampled = serde_json::to_value(&mv.sampled_value).map_err(internal)?;
229 ctx.meter_values(Some(tx_id), evse, Ts(mv.timestamp), &sampled)
230 .await?;
231 }
232 tracing::debug!(station = %ctx.identity, transaction = %ti.transaction_id, event = r.event_type.as_str(), seq = r.seq_no, "TransactionEvent");
233 let id_token_info = match &r.id_token {
234 Some(t) => Some(id_token_info(
235 &ctx.authorize(&t.id_token, Some(t.r#type.as_str())).await?,
236 )),
237 None => None,
238 };
239 let values = r.meter_value.as_deref().unwrap_or_default();
240 let cost_event = crate::tariff::TxEventCost {
241 event_type: r.event_type.as_str(),
242 at: r.timestamp,
243 seq_no: r.seq_no,
244 charging_state: ti.charging_state.map(|s| s.as_str()),
245 energy_wh: energy_wh(values),
246 power_w: measurand(values, MeasurandEnum::PowerActiveImport),
247 current_a: measurand(values, MeasurandEnum::CurrentImport),
248 tariff_id: ti.tariff_id.as_deref(),
249 cost_details: r
250 .cost_details
251 .as_ref()
252 .map(serde_json::to_value)
253 .transpose()
254 .map_err(internal)?,
255 payment_brand: additional_info(r.id_token.as_ref(), "PaymentBrand"),
256 payment_recognition: additional_info(r.id_token.as_ref(), "PaymentRecognition"),
257 };
258 let total_cost =
260 crate::tariff::on_transaction_event(&ctx.app, ctx.station_id, tx_id, &cost_event)
261 .await
262 .map_err(internal)?;
263 json(TransactionEventResponse {
264 id_token_info,
265 total_cost,
266 ..Default::default()
267 })
268 }
269 StationCall::MeterValues(r) => {
270 for mv in &r.meter_value {
271 let sampled = serde_json::to_value(&mv.sampled_value).map_err(internal)?;
272 ctx.meter_values(None, i32_of(r.evse_id), Ts(mv.timestamp), &sampled)
273 .await?;
274 }
275 json(MeterValuesResponse::default())
276 }
277 StationCall::DataTransfer(r) => {
278 ctx.log_event(action, payload).await;
279 tracing::info!(station = %ctx.identity, vendor_id = %r.vendor_id, message_id = ?r.message_id, "DataTransfer");
280 json(DataTransferResponse {
281 custom_data: None,
282 status: DataTransferStatusEnum::UnknownVendorId,
283 status_info: None,
284 data: None,
285 })
286 }
287 StationCall::FirmwareStatusNotification(r) => {
288 ctx.log_event(action, payload).await;
289 if r.status == FirmwareStatusEnum::Installed {
290 tracing::info!(station = %ctx.identity, "firmware installed");
291 }
292 json(FirmwareStatusNotificationResponse::default())
293 }
294 StationCall::SignCertificate(_) => {
296 ctx.log_event(action, payload).await;
297 json(SignCertificateResponse {
298 custom_data: None,
299 status: GenericStatusEnum::Rejected,
300 status_info: None,
301 })
302 }
303 StationCall::Get15118EVCertificate(_) => {
304 ctx.log_event(action, payload).await;
305 json(Get15118EVCertificateResponse {
306 custom_data: None,
307 status: Iso15118EVCertificateStatusEnum::Failed,
308 status_info: None,
309 exi_response: String::new(),
310 remaining_contracts: None,
311 })
312 }
313 StationCall::GetCertificateStatus(_) => json(GetCertificateStatusResponse {
314 custom_data: None,
315 status: GetCertificateStatusEnum::Failed,
316 status_info: None,
317 ocsp_result: None,
318 }),
319 StationCall::NotifyEVChargingNeeds(r) => {
320 ctx.log_event(action, payload).await;
321 let n = &r.charging_needs;
322 let needs = crate::smart::EvNeeds {
323 evse: r.evse_id,
324 bidirectional: {
325 let mode = n.requested_energy_transfer.as_str();
326 mode.contains("BPT") || mode.contains("DER")
327 },
328 ac_max_current: n
329 .ac_charging_parameters
330 .as_ref()
331 .map(|ac| ac.ev_max_current),
332 dc_max_power: n
333 .dc_charging_parameters
334 .as_ref()
335 .map(|d| {
336 d.ev_max_power
337 .unwrap_or(d.ev_max_current * d.ev_max_voltage)
338 })
339 .or_else(|| {
340 n.v2x_charging_parameters
341 .as_ref()
342 .and_then(|x| x.max_charge_power)
343 }),
344 departure: n.departure_time,
345 max_schedule_tuples: r.max_schedule_tuples,
346 };
347 let status = crate::smart::on_ev_charging_needs(
348 &ctx.app,
349 ctx.station_id,
350 OcppVersion::V21,
351 needs,
352 )
353 .await
354 .map_err(internal)?;
355 let status = match status {
356 "Accepted" => NotifyEVChargingNeedsStatusEnum::Accepted,
357 _ => NotifyEVChargingNeedsStatusEnum::Rejected,
358 };
359 json(NotifyEVChargingNeedsResponse {
360 custom_data: None,
361 status,
362 status_info: None,
363 })
364 }
365 StationCall::NotifyEVChargingSchedule(_) => {
366 ctx.log_event(action, payload).await;
367 json(NotifyEVChargingScheduleResponse {
368 custom_data: None,
369 status: GenericStatusEnum::Accepted,
370 status_info: None,
371 })
372 }
373 StationCall::ClearedChargingLimit(r) => {
375 db::charging::delete_external_limits(
376 &ctx.app.db,
377 ctx.station_id,
378 r.charging_limit_source.as_str(),
379 r.evse_id,
380 )
381 .await
382 .map_err(internal)?;
383 ack(
384 ctx,
385 action,
386 payload,
387 ClearedChargingLimitResponse::default(),
388 )
389 .await
390 }
391 StationCall::LogStatusNotification(_) => {
392 ack(
393 ctx,
394 action,
395 payload,
396 LogStatusNotificationResponse::default(),
397 )
398 .await
399 }
400 StationCall::NotifyChargingLimit(r) => {
401 let schedules = r
402 .charging_schedule
403 .as_ref()
404 .map(serde_json::to_value)
405 .transpose()
406 .map_err(internal)?;
407 db::charging::upsert_external_limit(
408 &ctx.app.db,
409 ctx.station_id,
410 r.evse_id.unwrap_or(0),
411 r.charging_limit.charging_limit_source.as_str(),
412 r.charging_limit.is_grid_critical,
413 r.charging_limit.is_local_generation,
414 schedules.as_ref(),
415 )
416 .await
417 .map_err(internal)?;
418 ack(ctx, action, payload, NotifyChargingLimitResponse::default()).await
419 }
420 StationCall::NotifyCustomerInformation(_) => {
421 ack(
422 ctx,
423 action,
424 payload,
425 NotifyCustomerInformationResponse::default(),
426 )
427 .await
428 }
429 StationCall::NotifyDisplayMessages(_) => {
430 ack(
431 ctx,
432 action,
433 payload,
434 NotifyDisplayMessagesResponse::default(),
435 )
436 .await
437 }
438 StationCall::NotifyEvent(_) => {
439 ack(ctx, action, payload, NotifyEventResponse::default()).await
440 }
441 StationCall::NotifyMonitoringReport(_) => {
442 ack(
443 ctx,
444 action,
445 payload,
446 NotifyMonitoringReportResponse::default(),
447 )
448 .await
449 }
450 StationCall::NotifyReport(_) => {
451 ack(ctx, action, payload, NotifyReportResponse::default()).await
452 }
453 StationCall::PublishFirmwareStatusNotification(_) => {
454 ack(
455 ctx,
456 action,
457 payload,
458 PublishFirmwareStatusNotificationResponse::default(),
459 )
460 .await
461 }
462 StationCall::ReportChargingProfiles(r) => {
463 let profiles: Vec<Value> = r
464 .charging_profile
465 .iter()
466 .filter_map(|p| serde_json::to_value(p).ok())
467 .collect();
468 crate::smart::record_reported(
469 &ctx.app,
470 ctx.station_id,
471 OcppVersion::V21,
472 r.evse_id,
473 r.charging_limit_source.as_str(),
474 &profiles,
475 )
476 .await
477 .map_err(internal)?;
478 ack(
479 ctx,
480 action,
481 payload,
482 ReportChargingProfilesResponse::default(),
483 )
484 .await
485 }
486 StationCall::ReservationStatusUpdate(r) => {
487 crate::reservation::on_status_update(
488 &ctx.app,
489 ctx.station_id,
490 r.reservation_id,
491 r.reservation_update_status.as_str(),
492 )
493 .await
494 .map_err(internal)?;
495 ack(
496 ctx,
497 action,
498 payload,
499 ReservationStatusUpdateResponse::default(),
500 )
501 .await
502 }
503 StationCall::SecurityEventNotification(_) => {
504 ack(
505 ctx,
506 action,
507 payload,
508 SecurityEventNotificationResponse::default(),
509 )
510 .await
511 }
512 StationCall::BatterySwap(_) => {
514 ack(ctx, action, payload, BatterySwapResponse::default()).await
515 }
516 StationCall::NotifyDERAlarm(_) => {
517 ack(ctx, action, payload, NotifyDERAlarmResponse::default()).await
518 }
519 StationCall::NotifyDERStartStop(_) => {
520 ack(ctx, action, payload, NotifyDERStartStopResponse::default()).await
521 }
522 StationCall::NotifyPriorityCharging(_) => {
523 ack(
524 ctx,
525 action,
526 payload,
527 NotifyPriorityChargingResponse::default(),
528 )
529 .await
530 }
531 StationCall::NotifySettlement(_) => {
532 ack(ctx, action, payload, NotifySettlementResponse::default()).await
533 }
534 StationCall::ReportDERControl(_) => {
535 ack(ctx, action, payload, ReportDERControlResponse::default()).await
536 }
537 StationCall::OpenPeriodicEventStream(_) => {
538 ctx.log_event(action, payload).await;
539 json(OpenPeriodicEventStreamResponse {
540 status: GenericStatusEnum::Accepted,
541 status_info: None,
542 custom_data: None,
543 })
544 }
545 StationCall::ClosePeriodicEventStream(_) => {
546 ack(
547 ctx,
548 action,
549 payload,
550 ClosePeriodicEventStreamResponse::default(),
551 )
552 .await
553 }
554 StationCall::PullDynamicScheduleUpdate(r) => {
555 let update =
557 crate::smart::pull_dynamic_update(&ctx.app, ctx.station_id, r.charging_profile_id)
558 .await
559 .map_err(internal)?;
560 let update =
561 update.and_then(|u| ocpp_core::decode_value::<ChargingScheduleUpdate>(u).ok());
562 json(match update {
563 Some(u) => PullDynamicScheduleUpdateResponse {
564 schedule_update: Some(u),
565 status: ChargingProfileStatusEnum::Accepted,
566 status_info: None,
567 custom_data: None,
568 },
569 None => PullDynamicScheduleUpdateResponse {
570 schedule_update: None,
571 status: ChargingProfileStatusEnum::Rejected,
572 status_info: Some(StatusInfo {
573 reason_code: "InvalidProfile".into(),
574 additional_info: None,
575 custom_data: None,
576 }),
577 custom_data: None,
578 },
579 })
580 }
581 StationCall::VatNumberValidation(r) => {
582 ctx.log_event(action, payload).await;
583 json(VatNumberValidationResponse {
585 company: None,
586 status_info: None,
587 vat_number: r.vat_number,
588 evse_id: r.evse_id,
589 status: GenericStatusEnum::Rejected,
590 custom_data: None,
591 })
592 }
593 StationCall::GetCertificateChainStatus(_) => Err(RpcError::not_supported(action)),
595 }
596}
597
598pub async fn handle_send(ctx: &mut StationCtx, action: &str, payload: &str) {
600 match StationSend::decode(action, payload) {
601 Ok(StationSend::NotifyPeriodicEventStream(s)) => {
602 let at = Ts(s.basetime);
604 let data =
605 serde_json::json!({ "streamId": s.id, "pending": s.pending, "data": s.data });
606 if let Err(e) = ctx.meter_values(None, 0, at, &data).await {
607 tracing::warn!(station = %ctx.identity, error = %e, "failed to store periodic event stream data");
608 }
609 }
610 Err(e) => {
611 tracing::warn!(station = %ctx.identity, action, error = %e, "invalid SEND message ignored")
612 }
613 }
614}
615
616async fn ack<T: serde::Serialize>(
617 ctx: &StationCtx,
618 action: &str,
619 payload: &str,
620 response: T,
621) -> Result<Value, RpcError> {
622 ctx.log_event(action, payload).await;
623 json(response)
624}
625
626pub fn validate_command(action: &str, payload: Value) -> Result<Value, RpcError> {
628 CsmsCall::from_value(action, payload)
629 .map(|c| c.to_payload())
630 .map_err(|e| match e {
631 DecodeError::Invalid(v) => RpcError::new(v.kind.error_code(), v.message),
632 other => RpcError::new(ErrorCode::NotSupported, other.to_string()),
633 })
634}
635
636pub fn validate_response(action: &str, payload: &str) -> Result<Value, ocpp_core::Violation> {
638 let a = Action::from_name(action).ok_or_else(|| {
639 ocpp_core::Violation::new(
640 ocpp_core::ViolationKind::Format,
641 format!("unknown action {action}"),
642 )
643 })?;
644 ocpp21::validate_response(a, payload)
645}