fix: lifetime optional in validation + type — app no longer sends it
validateConsumption required lifetime, but the app stopped sending it (backend now derives it). asObject(undefined) threw 'body is required', rejecting every chunk. Now lifetime is optional in both the validator and the DailyConsumption type. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -88,7 +88,7 @@ final class ConsumptionCloudClient {
|
|||||||
void syncIfDue(ConsumptionTracker tracker, ChargeSessionTracker charges, long nowMs) {
|
void syncIfDue(ConsumptionTracker tracker, ChargeSessionTracker charges, long nowMs) {
|
||||||
long elapsed = android.os.SystemClock.elapsedRealtime();
|
long elapsed = android.os.SystemClock.elapsedRealtime();
|
||||||
if (endpoint.isEmpty() || token.isEmpty()) {
|
if (endpoint.isEmpty() || token.isEmpty()) {
|
||||||
Log.d(TAG, "Sync skipped: API not configured");
|
Log.i(TAG, "Sync skipped: API not configured");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
syncPendingChargesIfDue(charges, elapsed);
|
syncPendingChargesIfDue(charges, elapsed);
|
||||||
@@ -107,10 +107,10 @@ final class ConsumptionCloudClient {
|
|||||||
|
|
||||||
ConsumptionTracker.CloudSnapshot chunk = tracker.takeChunkDelta();
|
ConsumptionTracker.CloudSnapshot chunk = tracker.takeChunkDelta();
|
||||||
if (chunk == null) {
|
if (chunk == null) {
|
||||||
Log.d(TAG, "Sync due but no new consumption since last chunk");
|
Log.i(TAG, "Sync due but no new consumption since last chunk");
|
||||||
return; // no driving since the last chunk
|
return; // no driving since the last chunk
|
||||||
}
|
}
|
||||||
Log.d(TAG, "New chunk for " + chunk.date + ": +" + String.format("%.1f", chunk.day.km)
|
Log.i(TAG, "New chunk for " + chunk.date + ": +" + String.format("%.1f", chunk.day.km)
|
||||||
+ "km, +" + String.format("%.2f", chunk.day.kwh) + "kWh");
|
+ "km, +" + String.format("%.2f", chunk.day.kwh) + "kWh");
|
||||||
try {
|
try {
|
||||||
JSONObject body = consumptionBody(chunk, nowMs);
|
JSONObject body = consumptionBody(chunk, nowMs);
|
||||||
@@ -153,7 +153,7 @@ final class ConsumptionCloudClient {
|
|||||||
String date = body.optString("date", "?");
|
String date = body.optString("date", "?");
|
||||||
if (send("/v1/consumption/daily", "PUT", body)) {
|
if (send("/v1/consumption/daily", "PUT", body)) {
|
||||||
sent++;
|
sent++;
|
||||||
Log.d(TAG, "Cached chunk for " + date + " sent");
|
Log.i(TAG, "Cached chunk for " + date + " sent");
|
||||||
} else {
|
} else {
|
||||||
// Server rejected — keep it, try the next one too.
|
// Server rejected — keep it, try the next one too.
|
||||||
remaining.put(body);
|
remaining.put(body);
|
||||||
@@ -227,16 +227,16 @@ final class ConsumptionCloudClient {
|
|||||||
|
|
||||||
private void syncPendingCharges(ChargeSessionTracker charges, List<JSONObject> pending)
|
private void syncPendingCharges(ChargeSessionTracker charges, List<JSONObject> pending)
|
||||||
throws Exception {
|
throws Exception {
|
||||||
Log.d(TAG, "Found " + pending.size() + " pending charge sessions");
|
Log.i(TAG, "Found " + pending.size() + " pending charge sessions");
|
||||||
int sent = 0;
|
int sent = 0;
|
||||||
for (JSONObject session : pending) {
|
for (JSONObject session : pending) {
|
||||||
JSONObject body = new JSONObject(session.toString()).put("installationId", installationId);
|
JSONObject body = new JSONObject(session.toString()).put("installationId", installationId);
|
||||||
String id = session.getString("sessionId");
|
String id = session.getString("sessionId");
|
||||||
Log.d(TAG, "Uploading charge session: " + id);
|
Log.i(TAG, "Uploading charge session: " + id);
|
||||||
if (send("/v1/charging-sessions/" + id, "PUT", body)) {
|
if (send("/v1/charging-sessions/" + id, "PUT", body)) {
|
||||||
charges.markUploaded(id);
|
charges.markUploaded(id);
|
||||||
logEvent(true, 200, "Şarj session gönderildi: " + id);
|
logEvent(true, 200, "Şarj session gönderildi: " + id);
|
||||||
Log.d(TAG, "Successfully uploaded charge session: " + id);
|
Log.i(TAG, "Successfully uploaded charge session: " + id);
|
||||||
} else {
|
} else {
|
||||||
logEvent(false, 0, "Şarj session gönderilemedi: " + id);
|
logEvent(false, 0, "Şarj session gönderilemedi: " + id);
|
||||||
Log.w(TAG, "Failed to upload charge session: " + id);
|
Log.w(TAG, "Failed to upload charge session: " + id);
|
||||||
@@ -251,7 +251,8 @@ final class ConsumptionCloudClient {
|
|||||||
try {
|
try {
|
||||||
byte[] bytes = body.toString().getBytes(StandardCharsets.UTF_8);
|
byte[] bytes = body.toString().getBytes(StandardCharsets.UTF_8);
|
||||||
String url = trimSlash(endpoint) + path;
|
String url = trimSlash(endpoint) + path;
|
||||||
Log.d(TAG, "Sending " + method + " request to " + url + " (" + bytes.length + " bytes)");
|
Log.i(TAG, "Sending " + method + " request to " + url + " (" + bytes.length + " bytes)");
|
||||||
|
Log.i(TAG, "Body: " + body.toString());
|
||||||
connection = (HttpURLConnection) new URL(url).openConnection();
|
connection = (HttpURLConnection) new URL(url).openConnection();
|
||||||
connection.setRequestMethod(method);
|
connection.setRequestMethod(method);
|
||||||
connection.setConnectTimeout(TIMEOUT_MS);
|
connection.setConnectTimeout(TIMEOUT_MS);
|
||||||
@@ -262,7 +263,7 @@ final class ConsumptionCloudClient {
|
|||||||
connection.setRequestProperty("Authorization", "Bearer " + token);
|
connection.setRequestProperty("Authorization", "Bearer " + token);
|
||||||
try (OutputStream out = connection.getOutputStream()) { out.write(bytes); }
|
try (OutputStream out = connection.getOutputStream()) { out.write(bytes); }
|
||||||
int status = connection.getResponseCode();
|
int status = connection.getResponseCode();
|
||||||
Log.d(TAG, "Response: HTTP " + status);
|
Log.i(TAG, "Response: HTTP " + status);
|
||||||
if (status < 200 || status >= 300) {
|
if (status < 200 || status >= 300) {
|
||||||
String errBody = readErrorBody(connection);
|
String errBody = readErrorBody(connection);
|
||||||
Log.w(TAG, "Sync rejected with HTTP " + status + (errBody.isEmpty() ? "" : ": " + errBody));
|
Log.w(TAG, "Sync rejected with HTTP " + status + (errBody.isEmpty() ? "" : ": " + errBody));
|
||||||
@@ -308,7 +309,7 @@ final class ConsumptionCloudClient {
|
|||||||
try {
|
try {
|
||||||
List<DayRecord> fresh = fetchHistory();
|
List<DayRecord> fresh = fetchHistory();
|
||||||
if (!fresh.isEmpty()) cachedHistory = fresh;
|
if (!fresh.isEmpty()) cachedHistory = fresh;
|
||||||
Log.d(TAG, "Fetched " + fresh.size() + " history records from cloud");
|
Log.i(TAG, "Fetched " + fresh.size() + " history records from cloud");
|
||||||
logEvent(true, 200, "History çekildi: " + fresh.size() + " gün");
|
logEvent(true, 200, "History çekildi: " + fresh.size() + " gün");
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
String msg = e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage();
|
String msg = e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage();
|
||||||
@@ -363,7 +364,7 @@ final class ConsumptionCloudClient {
|
|||||||
connection.setReadTimeout(TIMEOUT_MS);
|
connection.setReadTimeout(TIMEOUT_MS);
|
||||||
connection.setRequestProperty("Authorization", "Bearer " + token);
|
connection.setRequestProperty("Authorization", "Bearer " + token);
|
||||||
int status = connection.getResponseCode();
|
int status = connection.getResponseCode();
|
||||||
Log.d(TAG, "History GET response: HTTP " + status);
|
Log.i(TAG, "History GET response: HTTP " + status);
|
||||||
if (status < 200 || status >= 300) {
|
if (status < 200 || status >= 300) {
|
||||||
String err = readErrorBody(connection);
|
String err = readErrorBody(connection);
|
||||||
Log.w(TAG, "History fetch rejected with HTTP " + status + (err.isEmpty() ? "" : ": " + err));
|
Log.w(TAG, "History fetch rejected with HTTP " + status + (err.isEmpty() ? "" : ": " + err));
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ export interface DailyConsumption {
|
|||||||
date: string;
|
date: string;
|
||||||
recordedAt: Date;
|
recordedAt: Date;
|
||||||
day: Totals;
|
day: Totals;
|
||||||
lifetime: Totals;
|
/** Optional — the backend derives lifetime as the sum of all days' totals. */
|
||||||
|
lifetime?: Totals;
|
||||||
sohPercent?: number;
|
sohPercent?: number;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
|||||||
@@ -21,12 +21,15 @@ export function validateConsumption(body: unknown): ConsumptionInput {
|
|||||||
const date = record.date;
|
const date = record.date;
|
||||||
if (typeof date !== "string" || !ISO_DATE.test(date)) throw new ValidationError("date is invalid");
|
if (typeof date !== "string" || !ISO_DATE.test(date)) throw new ValidationError("date is invalid");
|
||||||
const sohPercent = record.sohPercent;
|
const sohPercent = record.sohPercent;
|
||||||
|
const lifetime = record.lifetime;
|
||||||
return {
|
return {
|
||||||
installationId: validateUuid(record.installationId, "installationId"),
|
installationId: validateUuid(record.installationId, "installationId"),
|
||||||
date,
|
date,
|
||||||
recordedAt: validateDate(record.recordedAt, "recordedAt"),
|
recordedAt: validateDate(record.recordedAt, "recordedAt"),
|
||||||
day: validateTotals(record.day, "day"),
|
day: validateTotals(record.day, "day"),
|
||||||
lifetime: validateTotals(record.lifetime, "lifetime"),
|
// Lifetime is optional — the backend derives it from day totals, and the app no longer
|
||||||
|
// sends it. Legacy documents may still carry one; new chunks do not.
|
||||||
|
...(lifetime === undefined ? {} : { lifetime: validateTotals(lifetime, "lifetime") }),
|
||||||
...(sohPercent === undefined ? {} : { sohPercent: bounded(sohPercent, 0, 100, "sohPercent") }),
|
...(sohPercent === undefined ? {} : { sohPercent: bounded(sohPercent, 0, 100, "sohPercent") }),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user