Skip to content

Commit 194b388

Browse files
andigclaude
andcommitted
fix: OCPP 2.0.1 schema conformance fixes
CRITICAL fixes: - CompositeSchedule: Redesign struct to match schema (evseId, duration, scheduleStart, chargingRateUnit, chargingSchedulePeriod) - ChargingSchedulePeriod: Add missing phaseToUse field (1-3) - IdTokenInfo: Add missing evseId array field - GetVariableResult: Add attributeStatusInfo field - GetVariableResult: Fix attributeValue maxLength (1000 -> 2500) HIGH fixes: - OCSPRequestDataType: Make responderURL required per schema - ConsumptionCost.Cost: Add minItems=1 validation - SalesTariffEntry.ConsumptionCost: Add minItems=1 validation Updates tests to match new validation requirements. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent b7f92ee commit 194b388

7 files changed

Lines changed: 50 additions & 46 deletions

File tree

ocpp2.0.1/provisioning/get_variables.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,12 @@ type GetVariableData struct {
3939
}
4040

4141
type GetVariableResult struct {
42-
AttributeStatus GetVariableStatus `json:"attributeStatus" validate:"required,getVariableStatus"`
43-
AttributeType types.Attribute `json:"attributeType,omitempty" validate:"omitempty,attribute"`
44-
AttributeValue string `json:"attributeValue,omitempty" validate:"omitempty,max=1000"`
45-
Component types.Component `json:"component" validate:"required"`
46-
Variable types.Variable `json:"variable" validate:"required"`
42+
AttributeStatus GetVariableStatus `json:"attributeStatus" validate:"required,getVariableStatus"`
43+
AttributeStatusInfo *types.StatusInfo `json:"attributeStatusInfo,omitempty"`
44+
AttributeType types.Attribute `json:"attributeType,omitempty" validate:"omitempty,attribute"`
45+
AttributeValue string `json:"attributeValue,omitempty" validate:"omitempty,max=2500"`
46+
Component types.Component `json:"component" validate:"required"`
47+
Variable types.Variable `json:"variable" validate:"required"`
4748
}
4849

4950
// The field definition of the GetVariables request payload sent by the CSMS to the Charging Station.

ocpp2.0.1/smartcharging/get_composite_schedule.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,11 @@ func isValidGetCompositeScheduleStatus(fl validator.FieldLevel) bool {
3030
}
3131

3232
type CompositeSchedule struct {
33-
StartDateTime *types.DateTime `json:"startDateTime,omitempty" validate:"omitempty"`
34-
ChargingSchedule *types.ChargingSchedule `json:"chargingSchedule,omitempty" validate:"omitempty"`
33+
EvseId int `json:"evseId" validate:"gte=0"`
34+
Duration int `json:"duration" validate:"gte=0"`
35+
ScheduleStart *types.DateTime `json:"scheduleStart" validate:"required"`
36+
ChargingRateUnit types.ChargingRateUnitType `json:"chargingRateUnit" validate:"required,chargingRateUnit201"`
37+
ChargingSchedulePeriod []types.ChargingSchedulePeriod `json:"chargingSchedulePeriod" validate:"required,min=1,dive"`
3538
}
3639

3740
// The field definition of the GetCompositeSchedule request payload sent by the CSMS to the Charging System.

ocpp2.0.1/types/types.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ type OCSPRequestDataType struct {
155155
IssuerNameHash string `json:"issuerNameHash" validate:"required,max=128"`
156156
IssuerKeyHash string `json:"issuerKeyHash" validate:"required,max=128"`
157157
SerialNumber string `json:"serialNumber" validate:"required,max=40"`
158-
ResponderURL string `json:"responderURL,omitempty" validate:"max=512"`
158+
ResponderURL string `json:"responderURL" validate:"required,max=512"`
159159
}
160160

161161
// CertificateHashDataType
@@ -283,6 +283,7 @@ type IdTokenInfo struct {
283283
ChargingPriority int `json:"chargingPriority,omitempty" validate:"min=-9,max=9"`
284284
Language1 string `json:"language1,omitempty" validate:"max=8"`
285285
Language2 string `json:"language2,omitempty" validate:"max=8"`
286+
EvseId []int `json:"evseId,omitempty" validate:"omitempty,min=1,dive,gte=0"`
286287
GroupIdToken *GroupIdToken `json:"groupIdToken,omitempty"`
287288
PersonalMessage *MessageContent `json:"personalMessage,omitempty"`
288289
}
@@ -386,7 +387,7 @@ type CostType struct {
386387
// Contains price information and/or alternative costs.
387388
type ConsumptionCost struct {
388389
StartValue float64 `json:"startValue"` // The lowest level of consumption that defines the starting point of this consumption block
389-
Cost []CostType `json:"cost" validate:"required,max=3,dive"` // Contains the cost details.
390+
Cost []CostType `json:"cost" validate:"required,min=1,max=3,dive"` // Contains the cost details.
390391
}
391392

392393
// NewConsumptionCost instantiates a new ConsumptionCost struct. No additional parameters need to be set.
@@ -401,7 +402,7 @@ func NewConsumptionCost(startValue float64, cost []CostType) ConsumptionCost {
401402
type SalesTariffEntry struct {
402403
EPriceLevel *int `json:"ePriceLevel,omitempty" validate:"omitempty,gte=0"` // The price level of this SalesTariffEntry (referring to NumEPriceLevels). Small values for the EPriceLevel represent a cheaper TariffEntry.
403404
RelativeTimeInterval RelativeTimeInterval `json:"relativeTimeInterval" validate:"required"` // The time interval the SalesTariffEntry is valid for, based upon relative times.
404-
ConsumptionCost []ConsumptionCost `json:"consumptionCost,omitempty" validate:"omitempty,max=3,dive"` // Additional means for further relative price information and/or alternative costs.
405+
ConsumptionCost []ConsumptionCost `json:"consumptionCost,omitempty" validate:"omitempty,min=1,max=3,dive"` // Additional means for further relative price information and/or alternative costs.
405406
}
406407

407408
// Sales tariff associated with this charging schedule.
@@ -500,6 +501,7 @@ type ChargingSchedulePeriod struct {
500501
StartPeriod int `json:"startPeriod" validate:"gte=0"`
501502
Limit float64 `json:"limit" validate:"gte=0"`
502503
NumberPhases *int `json:"numberPhases,omitempty" validate:"omitempty,gte=0"`
504+
PhaseToUse *int `json:"phaseToUse,omitempty" validate:"omitempty,gte=1,lte=3"`
503505
}
504506

505507
func NewChargingSchedulePeriod(startPeriod int, limit float64) ChargingSchedulePeriod {

ocpp2.0.1_test/common_test.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -157,9 +157,10 @@ func (suite *OcppV2TestSuite) TestSalesTariffEntryValidation() {
157157
var testTable = []GenericTestEntry{
158158
{types.SalesTariffEntry{EPriceLevel: newInt(8), RelativeTimeInterval: types.RelativeTimeInterval{Start: 500, Duration: newInt(1200)}, ConsumptionCost: []types.ConsumptionCost{dummyCostType}}, true},
159159
{types.SalesTariffEntry{EPriceLevel: newInt(8), RelativeTimeInterval: types.RelativeTimeInterval{Start: 500}}, true},
160+
{types.SalesTariffEntry{EPriceLevel: newInt(8), RelativeTimeInterval: types.RelativeTimeInterval{Start: 0}}, true},
161+
{types.SalesTariffEntry{RelativeTimeInterval: types.RelativeTimeInterval{Start: 0}}, true},
162+
{types.SalesTariffEntry{RelativeTimeInterval: types.RelativeTimeInterval{Start: 0}}, true},
160163
{types.SalesTariffEntry{EPriceLevel: newInt(8), RelativeTimeInterval: types.RelativeTimeInterval{}}, true},
161-
{types.SalesTariffEntry{RelativeTimeInterval: types.RelativeTimeInterval{}}, true},
162-
{types.SalesTariffEntry{}, true},
163164
{types.SalesTariffEntry{EPriceLevel: newInt(-1), RelativeTimeInterval: types.RelativeTimeInterval{Start: 500, Duration: newInt(1200)}, ConsumptionCost: []types.ConsumptionCost{dummyCostType}}, false},
164165
{types.SalesTariffEntry{EPriceLevel: newInt(8), RelativeTimeInterval: types.RelativeTimeInterval{Start: 500, Duration: newInt(-1)}, ConsumptionCost: []types.ConsumptionCost{dummyCostType}}, false},
165166
{types.SalesTariffEntry{EPriceLevel: newInt(8), RelativeTimeInterval: types.RelativeTimeInterval{Start: 500, Duration: newInt(1200)}, ConsumptionCost: []types.ConsumptionCost{dummyCostType, dummyCostType, dummyCostType, dummyCostType}}, false},
@@ -169,7 +170,7 @@ func (suite *OcppV2TestSuite) TestSalesTariffEntryValidation() {
169170
}
170171

171172
func (suite *OcppV2TestSuite) TestSalesTariffValidation() {
172-
dummySalesTariffEntry := types.SalesTariffEntry{}
173+
dummySalesTariffEntry := types.SalesTariffEntry{RelativeTimeInterval: types.RelativeTimeInterval{Start: 0}}
173174
var testTable = []GenericTestEntry{
174175
{types.SalesTariff{ID: 1, SalesTariffDescription: "someDesc", NumEPriceLevels: newInt(1), SalesTariffEntry: []types.SalesTariffEntry{dummySalesTariffEntry}}, true},
175176
{types.SalesTariff{ID: 1, NumEPriceLevels: newInt(1), SalesTariffEntry: []types.SalesTariffEntry{dummySalesTariffEntry}}, true},

ocpp2.0.1_test/get_composite_schedule_test.go

Lines changed: 28 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -29,23 +29,23 @@ func (suite *OcppV2TestSuite) TestGetCompositeScheduleRequestValidation() {
2929

3030
func (suite *OcppV2TestSuite) TestGetCompositeScheduleConfirmationValidation() {
3131
t := suite.T()
32-
chargingSchedule := types.NewChargingSchedule(1, types.ChargingRateUnitWatts, types.NewChargingSchedulePeriod(0, 10.0))
33-
chargingSchedule.Duration = newInt(600)
34-
chargingSchedule.MinChargingRate = newFloat(6.0)
35-
chargingSchedule.StartSchedule = types.NewDateTime(time.Now())
32+
chargingSchedulePeriod := types.NewChargingSchedulePeriod(0, 10.0)
3633
compositeSchedule := smartcharging.CompositeSchedule{
37-
StartDateTime: types.NewDateTime(time.Now()),
38-
ChargingSchedule: chargingSchedule,
34+
EvseId: 1,
35+
Duration: 600,
36+
ScheduleStart: types.NewDateTime(time.Now()),
37+
ChargingRateUnit: types.ChargingRateUnitWatts,
38+
ChargingSchedulePeriod: []types.ChargingSchedulePeriod{chargingSchedulePeriod},
3939
}
4040
var confirmationTable = []GenericTestEntry{
4141
{smartcharging.GetCompositeScheduleResponse{Status: smartcharging.GetCompositeScheduleStatusAccepted, StatusInfo: types.NewStatusInfo("reasoncode", ""), Schedule: &compositeSchedule}, true},
42-
{smartcharging.GetCompositeScheduleResponse{Status: smartcharging.GetCompositeScheduleStatusAccepted, StatusInfo: types.NewStatusInfo("reasoncode", ""), Schedule: &smartcharging.CompositeSchedule{}}, true},
4342
{smartcharging.GetCompositeScheduleResponse{Status: smartcharging.GetCompositeScheduleStatusAccepted, StatusInfo: types.NewStatusInfo("reasoncode", "")}, true},
4443
{smartcharging.GetCompositeScheduleResponse{Status: smartcharging.GetCompositeScheduleStatusAccepted}, true},
4544
{smartcharging.GetCompositeScheduleResponse{}, false},
4645
{smartcharging.GetCompositeScheduleResponse{Status: "invalidGetCompositeScheduleStatus"}, false},
4746
{smartcharging.GetCompositeScheduleResponse{Status: smartcharging.GetCompositeScheduleStatusAccepted, StatusInfo: types.NewStatusInfo("invalidreasoncodeasitslongerthan20", "")}, false},
48-
{smartcharging.GetCompositeScheduleResponse{Status: smartcharging.GetCompositeScheduleStatusAccepted, StatusInfo: types.NewStatusInfo("", ""), Schedule: &smartcharging.CompositeSchedule{StartDateTime: types.NewDateTime(time.Now()), ChargingSchedule: types.NewChargingSchedule(1, "invalidChargingRateUnit")}}, false},
47+
{smartcharging.GetCompositeScheduleResponse{Status: smartcharging.GetCompositeScheduleStatusAccepted, StatusInfo: types.NewStatusInfo("", ""), Schedule: &smartcharging.CompositeSchedule{EvseId: 1, Duration: 600, ScheduleStart: types.NewDateTime(time.Now()), ChargingRateUnit: "invalidChargingRateUnit", ChargingSchedulePeriod: []types.ChargingSchedulePeriod{chargingSchedulePeriod}}}, false},
48+
{smartcharging.GetCompositeScheduleResponse{Status: smartcharging.GetCompositeScheduleStatusAccepted, StatusInfo: types.NewStatusInfo("", ""), Schedule: &smartcharging.CompositeSchedule{EvseId: 1, Duration: 600, ScheduleStart: types.NewDateTime(time.Now()), ChargingRateUnit: types.ChargingRateUnitWatts, ChargingSchedulePeriod: []types.ChargingSchedulePeriod{}}}, false},
4949
}
5050
ExecuteGenericTestTable(t, confirmationTable)
5151
}
@@ -62,17 +62,20 @@ func (suite *OcppV2TestSuite) TestGetCompositeScheduleE2EMocked() {
6262
scheduleStart := types.NewDateTime(time.Now())
6363
chargingSchedulePeriod := types.NewChargingSchedulePeriod(0, 10.0)
6464
chargingSchedulePeriod.NumberPhases = newInt(3)
65-
chargingSchedule := types.NewChargingSchedule(1, chargingRateUnit, chargingSchedulePeriod)
66-
chargingSchedule.Duration = newInt(600)
67-
chargingSchedule.StartSchedule = types.NewDateTime(time.Now())
68-
chargingSchedule.MinChargingRate = newFloat(6.0)
6965
statusInfo := types.NewStatusInfo("reasonCode", "")
70-
compositeSchedule := smartcharging.CompositeSchedule{StartDateTime: scheduleStart, ChargingSchedule: chargingSchedule}
66+
compositeSchedule := smartcharging.CompositeSchedule{
67+
EvseId: evseID,
68+
Duration: duration,
69+
ScheduleStart: scheduleStart,
70+
ChargingRateUnit: chargingRateUnit,
71+
ChargingSchedulePeriod: []types.ChargingSchedulePeriod{chargingSchedulePeriod},
72+
}
7173
requestJson := fmt.Sprintf(`[2,"%v","%v",{"duration":%v,"chargingRateUnit":"%v","evseId":%v}]`,
7274
messageId, smartcharging.GetCompositeScheduleFeatureName, duration, chargingRateUnit, evseID)
73-
responseJson := fmt.Sprintf(`[3,"%v",{"status":"%v","statusInfo":{"reasonCode":"%v"},"schedule":{"startDateTime":"%v","chargingSchedule":{"id":%v,"startSchedule":"%v","duration":%v,"chargingRateUnit":"%v","minChargingRate":%v,"chargingSchedulePeriod":[{"startPeriod":%v,"limit":%v,"numberPhases":%v}]}}}]`,
74-
messageId, status, statusInfo.ReasonCode, compositeSchedule.StartDateTime.FormatTimestamp(), chargingSchedule.ID, chargingSchedule.StartSchedule.FormatTimestamp(), *chargingSchedule.Duration, chargingSchedule.ChargingRateUnit, *chargingSchedule.MinChargingRate, chargingSchedulePeriod.StartPeriod, chargingSchedulePeriod.Limit, *chargingSchedulePeriod.NumberPhases)
75+
responseJson := fmt.Sprintf(`[3,"%v",{"status":"%v","statusInfo":{"reasonCode":"%v"},"schedule":{"evseId":%v,"duration":%v,"scheduleStart":"%v","chargingRateUnit":"%v","chargingSchedulePeriod":[{"startPeriod":%v,"limit":%v,"numberPhases":%v}]}}]`,
76+
messageId, status, statusInfo.ReasonCode, evseID, duration, compositeSchedule.ScheduleStart.FormatTimestamp(), chargingRateUnit, chargingSchedulePeriod.StartPeriod, chargingSchedulePeriod.Limit, *chargingSchedulePeriod.NumberPhases)
7577
getCompositeScheduleConfirmation := smartcharging.NewGetCompositeScheduleResponse(status)
78+
getCompositeScheduleConfirmation.StatusInfo = statusInfo
7679
getCompositeScheduleConfirmation.Schedule = &compositeSchedule
7780
channel := NewMockWebSocket(wsId)
7881

@@ -98,22 +101,16 @@ func (suite *OcppV2TestSuite) TestGetCompositeScheduleE2EMocked() {
98101
assert.Equal(t, status, confirmation.Status)
99102
assert.Equal(t, statusInfo.ReasonCode, confirmation.StatusInfo.ReasonCode)
100103
require.NotNil(t, confirmation.Schedule)
101-
require.NotNil(t, confirmation.Schedule.StartDateTime)
102-
assert.Equal(t, compositeSchedule.StartDateTime.FormatTimestamp(), confirmation.Schedule.StartDateTime.FormatTimestamp())
103-
require.NotNil(t, confirmation.Schedule.ChargingSchedule)
104-
assert.Equal(t, chargingSchedule.ID, confirmation.Schedule.ChargingSchedule.ID)
105-
assert.Equal(t, chargingSchedule.ChargingRateUnit, confirmation.Schedule.ChargingSchedule.ChargingRateUnit)
106-
require.NotNil(t, confirmation.Schedule.ChargingSchedule.Duration)
107-
assert.Equal(t, *chargingSchedule.Duration, *confirmation.Schedule.ChargingSchedule.Duration)
108-
require.NotNil(t, confirmation.Schedule.ChargingSchedule.MinChargingRate)
109-
assert.Equal(t, *chargingSchedule.MinChargingRate, *confirmation.Schedule.ChargingSchedule.MinChargingRate)
110-
require.NotNil(t, confirmation.Schedule.ChargingSchedule.StartSchedule)
111-
assert.Equal(t, chargingSchedule.StartSchedule.FormatTimestamp(), confirmation.Schedule.ChargingSchedule.StartSchedule.FormatTimestamp())
112-
require.Len(t, confirmation.Schedule.ChargingSchedule.ChargingSchedulePeriod, len(chargingSchedule.ChargingSchedulePeriod))
113-
assert.Equal(t, chargingSchedule.ChargingSchedulePeriod[0].Limit, confirmation.Schedule.ChargingSchedule.ChargingSchedulePeriod[0].Limit)
114-
assert.Equal(t, chargingSchedule.ChargingSchedulePeriod[0].StartPeriod, confirmation.Schedule.ChargingSchedule.ChargingSchedulePeriod[0].StartPeriod)
115-
require.NotNil(t, confirmation.Schedule.ChargingSchedule.ChargingSchedulePeriod[0].NumberPhases)
116-
assert.Equal(t, *chargingSchedule.ChargingSchedulePeriod[0].NumberPhases, *confirmation.Schedule.ChargingSchedule.ChargingSchedulePeriod[0].NumberPhases)
104+
assert.Equal(t, evseID, confirmation.Schedule.EvseId)
105+
assert.Equal(t, duration, confirmation.Schedule.Duration)
106+
require.NotNil(t, confirmation.Schedule.ScheduleStart)
107+
assert.Equal(t, compositeSchedule.ScheduleStart.FormatTimestamp(), confirmation.Schedule.ScheduleStart.FormatTimestamp())
108+
assert.Equal(t, chargingRateUnit, confirmation.Schedule.ChargingRateUnit)
109+
require.Len(t, confirmation.Schedule.ChargingSchedulePeriod, 1)
110+
assert.Equal(t, chargingSchedulePeriod.Limit, confirmation.Schedule.ChargingSchedulePeriod[0].Limit)
111+
assert.Equal(t, chargingSchedulePeriod.StartPeriod, confirmation.Schedule.ChargingSchedulePeriod[0].StartPeriod)
112+
require.NotNil(t, confirmation.Schedule.ChargingSchedulePeriod[0].NumberPhases)
113+
assert.Equal(t, *chargingSchedulePeriod.NumberPhases, *confirmation.Schedule.ChargingSchedulePeriod[0].NumberPhases)
117114
resultChannel <- true
118115
}, duration, evseID, func(request *smartcharging.GetCompositeScheduleRequest) {
119116
request.ChargingRateUnit = chargingRateUnit

0 commit comments

Comments
 (0)