Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions include/client.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@

namespace TeslaBLE
{
/**
* @brief Structure to hold State of Charge (SOC) data
*/
struct SOCData {
bool valid = false; // Indicates if data is valid
int32_t battery_level = -1; // Current SOC percentage
int32_t usable_battery_level = -1; // Usable SOC percentage
int32_t charge_limit_soc = -1; // Target SOC percentage
};

/**
* @brief Main client class for Tesla BLE communication
*
Expand Down Expand Up @@ -156,6 +166,24 @@ namespace TeslaBLE
UniversalMessage_MessageFault_E signed_message_fault,
CarServer_Response* output);

// SOC (State of Charge) functionality
int extractSOCFromChargeState(
CarServer_ChargeState* charge_state,
int32_t* battery_level,
int32_t* usable_battery_level);

int populateSOCData(
CarServer_ChargeState* charge_state,
SOCData* soc_data);

int parseChargeStateFromVehicleData(
CarServer_VehicleData* vehicle_data,
CarServer_ChargeState** charge_state);

int extractSOCFromVehicleData(
CarServer_VehicleData* vehicle_data,
SOCData* soc_data);

private:
// Legacy implementation - to be phased out
int buildCarServerVehicleActionMessageLegacy(
Expand Down
91 changes: 91 additions & 0 deletions src/client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -982,5 +982,96 @@ namespace TeslaBLE
output_buffer, output_length);
return TeslaBLE_Status_E_OK;
}

int Client::extractSOCFromChargeState(
CarServer_ChargeState* charge_state,
int32_t* battery_level,
int32_t* usable_battery_level)
{
if (!charge_state || !battery_level || !usable_battery_level) {
return TeslaBLE_Status_E_ERROR_INVALID_PARAMS;
}

// Extract battery level
if (charge_state->which_optional_battery_level == CarServer_ChargeState_battery_level_tag) {
*battery_level = charge_state->optional_battery_level.battery_level;
} else {
*battery_level = -1; // Indicates field not present
}

// Extract usable battery level
if (charge_state->which_optional_usable_battery_level == CarServer_ChargeState_usable_battery_level_tag) {
*usable_battery_level = charge_state->optional_usable_battery_level.usable_battery_level;
} else {
*usable_battery_level = -1; // Indicates field not present
}

return TeslaBLE_Status_E_OK;
}

int Client::populateSOCData(
CarServer_ChargeState* charge_state,
SOCData* soc_data)
{
if (!charge_state || !soc_data) {
return TeslaBLE_Status_E_ERROR_INVALID_PARAMS;
}

// Initialize structure
*soc_data = SOCData{};

// Extract battery level
if (charge_state->which_optional_battery_level == CarServer_ChargeState_battery_level_tag) {
soc_data->battery_level = charge_state->optional_battery_level.battery_level;
}

// Extract usable battery level
if (charge_state->which_optional_usable_battery_level == CarServer_ChargeState_usable_battery_level_tag) {
soc_data->usable_battery_level = charge_state->optional_usable_battery_level.usable_battery_level;
}

// Extract charge limit
if (charge_state->which_optional_charge_limit_soc == CarServer_ChargeState_charge_limit_soc_tag) {
soc_data->charge_limit_soc = charge_state->optional_charge_limit_soc.charge_limit_soc;
}

// Mark as valid if we got at least one SOC value
soc_data->valid = (soc_data->battery_level != -1 || soc_data->usable_battery_level != -1);

return TeslaBLE_Status_E_OK;
}

int Client::parseChargeStateFromVehicleData(
CarServer_VehicleData* vehicle_data,
CarServer_ChargeState** charge_state)
{
if (!vehicle_data || !charge_state) {
return TeslaBLE_Status_E_ERROR_INVALID_PARAMS;
}

if (!vehicle_data->has_charge_state) {
return TeslaBLE_Status_E_ERROR_INTERNAL;
}

*charge_state = &vehicle_data->charge_state;
return TeslaBLE_Status_E_OK;
}

int Client::extractSOCFromVehicleData(
CarServer_VehicleData* vehicle_data,
SOCData* soc_data)
{
if (!vehicle_data || !soc_data) {
return TeslaBLE_Status_E_ERROR_INVALID_PARAMS;
}

CarServer_ChargeState* charge_state;
int result = parseChargeStateFromVehicleData(vehicle_data, &charge_state);
if (result != TeslaBLE_Status_E_OK) {
return result;
}

return populateSOCData(charge_state, soc_data);
}
} // namespace TeslaBLE
// #endif // MBEDTLS_CONFIG_FILE
110 changes: 110 additions & 0 deletions tests/test_message_parsing.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -273,3 +273,113 @@ TEST_F(MessageParsingTest, ParsePayloadCarServerResponseEdgeCases) {
);
EXPECT_NE(result2, 0) << "Parsing with truncated data should fail";
}

TEST_F(MessageParsingTest, ExtractSOCFromChargeState) {
// Create a mock ChargeState with SOC data
CarServer_ChargeState charge_state = CarServer_ChargeState_init_default;

// Set battery level to 85%
charge_state.which_optional_battery_level = CarServer_ChargeState_battery_level_tag;
charge_state.optional_battery_level.battery_level = 85;

// Set usable battery level to 82%
charge_state.which_optional_usable_battery_level = CarServer_ChargeState_usable_battery_level_tag;
charge_state.optional_usable_battery_level.usable_battery_level = 82;

// Extract SOC values
int32_t battery_level, usable_battery_level;
int result = client->extractSOCFromChargeState(&charge_state, &battery_level, &usable_battery_level);

// Verify results
EXPECT_EQ(result, TeslaBLE_Status_E_OK);
EXPECT_EQ(battery_level, 85);
EXPECT_EQ(usable_battery_level, 82);
}

TEST_F(MessageParsingTest, PopulateSOCData) {
// Create a mock ChargeState with comprehensive SOC data
CarServer_ChargeState charge_state = CarServer_ChargeState_init_default;

// Set battery level to 85%
charge_state.which_optional_battery_level = CarServer_ChargeState_battery_level_tag;
charge_state.optional_battery_level.battery_level = 85;

// Set usable battery level to 82%
charge_state.which_optional_usable_battery_level = CarServer_ChargeState_usable_battery_level_tag;
charge_state.optional_usable_battery_level.usable_battery_level = 82;

// Set charge limit to 90%
charge_state.which_optional_charge_limit_soc = CarServer_ChargeState_charge_limit_soc_tag;
charge_state.optional_charge_limit_soc.charge_limit_soc = 90;

// Populate SOCData structure
TeslaBLE::SOCData soc_data;
int result = client->populateSOCData(&charge_state, &soc_data);

// Verify results
EXPECT_EQ(result, TeslaBLE_Status_E_OK);
EXPECT_TRUE(soc_data.valid);
EXPECT_EQ(soc_data.battery_level, 85);
EXPECT_EQ(soc_data.usable_battery_level, 82);
EXPECT_EQ(soc_data.charge_limit_soc, 90);
}

TEST_F(MessageParsingTest, ParseChargeStateFromVehicleData) {
// Create a mock VehicleData with ChargeState
CarServer_VehicleData vehicle_data = CarServer_VehicleData_init_default;
vehicle_data.has_charge_state = true;

// Set up ChargeState data
vehicle_data.charge_state.which_optional_battery_level = CarServer_ChargeState_battery_level_tag;
vehicle_data.charge_state.optional_battery_level.battery_level = 75;

// Extract ChargeState from VehicleData
CarServer_ChargeState* charge_state;
int result = client->parseChargeStateFromVehicleData(&vehicle_data, &charge_state);

// Verify results
EXPECT_EQ(result, TeslaBLE_Status_E_OK);
ASSERT_NE(charge_state, nullptr);
EXPECT_EQ(charge_state->which_optional_battery_level, CarServer_ChargeState_battery_level_tag);
EXPECT_EQ(charge_state->optional_battery_level.battery_level, 75);
}

TEST_F(MessageParsingTest, ParseChargeStateFromVehicleDataNoChargeState) {
// Create a mock VehicleData WITHOUT ChargeState
CarServer_VehicleData vehicle_data = CarServer_VehicleData_init_default;
vehicle_data.has_charge_state = false;

// Try to extract ChargeState
CarServer_ChargeState* charge_state;
int result = client->parseChargeStateFromVehicleData(&vehicle_data, &charge_state);

// Should fail gracefully
EXPECT_EQ(result, TeslaBLE_Status_E_ERROR_INTERNAL);
}

TEST_F(MessageParsingTest, ExtractSOCFromVehicleDataEndToEnd) {
// Create a complete mock VehicleData response
CarServer_VehicleData vehicle_data = CarServer_VehicleData_init_default;
vehicle_data.has_charge_state = true;

// Set comprehensive SOC data
vehicle_data.charge_state.which_optional_battery_level = CarServer_ChargeState_battery_level_tag;
vehicle_data.charge_state.optional_battery_level.battery_level = 88;

vehicle_data.charge_state.which_optional_usable_battery_level = CarServer_ChargeState_usable_battery_level_tag;
vehicle_data.charge_state.optional_usable_battery_level.usable_battery_level = 85;

vehicle_data.charge_state.which_optional_charge_limit_soc = CarServer_ChargeState_charge_limit_soc_tag;
vehicle_data.charge_state.optional_charge_limit_soc.charge_limit_soc = 95;

// End-to-end SOC extraction
TeslaBLE::SOCData soc_data;
int result = client->extractSOCFromVehicleData(&vehicle_data, &soc_data);

// Verify complete extraction
EXPECT_EQ(result, TeslaBLE_Status_E_OK);
EXPECT_TRUE(soc_data.valid);
EXPECT_EQ(soc_data.battery_level, 88);
EXPECT_EQ(soc_data.usable_battery_level, 85);
EXPECT_EQ(soc_data.charge_limit_soc, 95);
}