From 255895bcb8d24b42d8dd3da535147fb734204eb9 Mon Sep 17 00:00:00 2001 From: Bluemedia Date: Sun, 27 Apr 2025 13:56:20 +0000 Subject: [PATCH 1/5] WIP: Add chargepoint list --- frontend/src/lib/types/chargepoint.ts | 19 ++++ .../routes/(navbar)/chargepoint/+page.svelte | 98 +++++++++++++++++++ frontend/static/locales/de/chargepoint.json | 13 +++ frontend/static/locales/en/chargepoint.json | 13 +++ 4 files changed, 143 insertions(+) create mode 100644 frontend/src/lib/types/chargepoint.ts create mode 100644 frontend/static/locales/de/chargepoint.json create mode 100644 frontend/static/locales/en/chargepoint.json diff --git a/frontend/src/lib/types/chargepoint.ts b/frontend/src/lib/types/chargepoint.ts new file mode 100644 index 0000000..ade067b --- /dev/null +++ b/frontend/src/lib/types/chargepoint.ts @@ -0,0 +1,19 @@ +export type ChargePoint = { + identity: string; + is_active: boolean; + price: number; + id: string; + last_seen: string; + vendor_name: string; + model: string; + serial_number: string, + firmware_version: string; + connectors: [ + { + id: string; + evse: number; + index: number; + status: 'Available' | 'Occupied' | 'Reserved' | 'Unavailable' | 'Faulted' + } + ] +} \ No newline at end of file diff --git a/frontend/src/routes/(navbar)/chargepoint/+page.svelte b/frontend/src/routes/(navbar)/chargepoint/+page.svelte index e69de29..f12908f 100644 --- a/frontend/src/routes/(navbar)/chargepoint/+page.svelte +++ b/frontend/src/routes/(navbar)/chargepoint/+page.svelte @@ -0,0 +1,98 @@ + + +
+

+ {$i18n.t('chargepoint:header')} +

+
+ + + + + + + + + + + + {#if chargepoints.length > 0} + {#each chargepoints as chargepoint} + + + + + + + + {/each} + {/if} + +
{$i18n.t('chargepoint:tableHeader.name')}{$i18n.t('chargepoint:tableHeader.active')}{$i18n.t('chargepoint:tableHeader.lastSeen')}{$i18n.t('chargepoint:tableHeader.price')}
+
+
+

+ {$i18n.t('common:transactionTable.startTime', { + time: transaction.begin, + formatParams: { + time: { + year: 'numeric', + month: 'numeric', + day: 'numeric', + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + }, + }, + })} +

+

+ {$i18n.t('common:transactionTable.endTime', { + time: transaction.end, + formatParams: { + time: { + year: 'numeric', + month: 'numeric', + day: 'numeric', + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + }, + }, + })} +

+
+
+
+ + + {transaction.chargepoint.name} + + {transaction.energyAmmount} kWh{transaction.cost} € + + {$i18n.t('common:transactionTable.detailButton')} + + +
+ {#if chargepoints.length === 0} +

+ {$i18n.t('chargepoint:noChargepoints')} +

+ {/if} +
+
+ + + +
+
diff --git a/frontend/static/locales/de/chargepoint.json b/frontend/static/locales/de/chargepoint.json new file mode 100644 index 0000000..4f76841 --- /dev/null +++ b/frontend/static/locales/de/chargepoint.json @@ -0,0 +1,13 @@ +{ + "header": "Ladepunkte", + "tableHeader": { + "name": "Name", + "active": "Aktiv", + "price": "Preis", + "lastSeen": "Zuletzt gesehen" + }, + "tableFormatting": { + "lastSeen": "{{val, relativetime}}" + }, + "noChargepoints": "Noch keine Ladepunkte vorhanden." +} \ No newline at end of file diff --git a/frontend/static/locales/en/chargepoint.json b/frontend/static/locales/en/chargepoint.json new file mode 100644 index 0000000..0bcbf00 --- /dev/null +++ b/frontend/static/locales/en/chargepoint.json @@ -0,0 +1,13 @@ +{ + "header": "Chargepoints", + "tableHeader": { + "name": "Name", + "active": "Active", + "price": "Price", + "lastSeen": "Last seen" + }, + "tableFormatting": { + "lastSeen": "{{val, relativetime}}" + }, + "noChargepoints": "There are no chargepoints yet." +} \ No newline at end of file From 00fe7ca13812cf94341cc114d5ede6fa9c5b309a Mon Sep 17 00:00:00 2001 From: BluemediaDev Date: Sun, 25 May 2025 16:49:50 +0000 Subject: [PATCH 2/5] Add additional ORM relations and thumbs in API responses --- backend/app/models/chargepoint.py | 1 + backend/app/models/firmware_update.py | 2 ++ backend/app/models/meter_value.py | 4 +++- backend/app/models/session.py | 4 +++- backend/app/models/transaction.py | 6 ++++++ backend/app/models/user.py | 1 + backend/app/schemas/chargepoint.py | 9 +++++++++ backend/app/schemas/transaction.py | 9 +++++---- backend/app/schemas/user.py | 7 +++++++ 9 files changed, 37 insertions(+), 6 deletions(-) diff --git a/backend/app/models/chargepoint.py b/backend/app/models/chargepoint.py index e24e180..c41d0d3 100644 --- a/backend/app/models/chargepoint.py +++ b/backend/app/models/chargepoint.py @@ -25,3 +25,4 @@ class ChargePoint(Base): connectors = relationship("Connector", cascade="delete, delete-orphan") transactions = relationship("Transaction", cascade="delete, delete-orphan") variables = relationship("ChargepointVariable", cascade="delete, delete-orphan") + firmware_updates = relationship("FirmwareUpdate", cascade="delete, delete-orphan") diff --git a/backend/app/models/firmware_update.py b/backend/app/models/firmware_update.py index 9e4fc4b..5d0d9f7 100644 --- a/backend/app/models/firmware_update.py +++ b/backend/app/models/firmware_update.py @@ -1,5 +1,6 @@ import uuid from sqlalchemy import Column, DateTime, Enum, ForeignKey, Integer, String, Text, Uuid +from sqlalchemy.orm import relationship from app.database import Base from app.schemas.firmware_update import FirmwareUpdateStatus @@ -20,3 +21,4 @@ class FirmwareUpdate(Base): signature = Column(String, nullable=True) chargepoint_id = Column(Uuid, ForeignKey("chargepoints.id"), index=True) + chargepoint = relationship("ChargePoint", back_populates="firmware_updates") diff --git a/backend/app/models/meter_value.py b/backend/app/models/meter_value.py index e2528ed..5adf6a1 100644 --- a/backend/app/models/meter_value.py +++ b/backend/app/models/meter_value.py @@ -1,5 +1,6 @@ import uuid from sqlalchemy import Uuid, Column, DateTime, Enum, Float, ForeignKey, String +from sqlalchemy.orm import relationship from app.database import Base from app.schemas.meter_value import Measurand, PhaseType @@ -14,4 +15,5 @@ class MeterValue(Base): unit = Column(String, nullable=True) value = Column(Float) - transaction_id = Column(String, ForeignKey("transactions.id"), index=True) \ No newline at end of file + transaction_id = Column(String, ForeignKey("transactions.id"), index=True) + transaction = relationship("Transaction", back_populates="meter_values") \ No newline at end of file diff --git a/backend/app/models/session.py b/backend/app/models/session.py index cd0c5ba..d9c0ce2 100644 --- a/backend/app/models/session.py +++ b/backend/app/models/session.py @@ -1,5 +1,6 @@ import uuid from sqlalchemy import Column, DateTime, ForeignKey, String, Uuid +from sqlalchemy.orm import relationship from app.database import Base @@ -11,4 +12,5 @@ class Session(Base): refresh_token = Column(String, nullable=False, unique=True, index=True) last_used = Column(DateTime(timezone=True)) - user_id = Column(Uuid, ForeignKey("users.id"), nullable=False, index=True) \ No newline at end of file + user_id = Column(Uuid, ForeignKey("users.id"), nullable=False, index=True) + user = relationship("User", back_populates="sessions") \ No newline at end of file diff --git a/backend/app/models/transaction.py b/backend/app/models/transaction.py index a918430..b4dcbce 100644 --- a/backend/app/models/transaction.py +++ b/backend/app/models/transaction.py @@ -1,4 +1,5 @@ from sqlalchemy import String, Uuid, Column, DateTime, Enum, Numeric, ForeignKey +from sqlalchemy.orm import relationship from app.schemas.transaction import TransactionEventTriggerReason, TransactionStatus from app.database import Base @@ -16,4 +17,9 @@ class Transaction(Base): price = Column(Numeric(10,2)) user_id = Column(Uuid, ForeignKey("users.id"), nullable=True, index=True) + user = relationship("User", back_populates="transactions") + chargepoint_id = Column(Uuid, ForeignKey("chargepoints.id"), index=True) + chargepoint = relationship("ChargePoint", back_populates="transactions") + + meter_values = relationship("MeterValue", cascade="delete, delete-orphan") diff --git a/backend/app/models/user.py b/backend/app/models/user.py index 75cf5da..bfdefb9 100644 --- a/backend/app/models/user.py +++ b/backend/app/models/user.py @@ -17,3 +17,4 @@ class User(Base): id_tokens = relationship("IdToken", back_populates="owner", cascade="delete, delete-orphan") transactions = relationship("Transaction", cascade="delete, delete-orphan") + sessions = relationship("Session", cascade="delete, delete-orphan") diff --git a/backend/app/schemas/chargepoint.py b/backend/app/schemas/chargepoint.py index 2493315..a7d4972 100644 --- a/backend/app/schemas/chargepoint.py +++ b/backend/app/schemas/chargepoint.py @@ -36,6 +36,15 @@ class ChargePoint(ChargePointBase): from_attributes = True json_encoders = {Decimal: decimal_encoder} +class ChargePointThumb(BaseModel): + id: UUID + identity: str + price: Decimal + + class Config: + from_attributes = True + json_encoders = {Decimal: decimal_encoder} + class ChargePointPassword(BaseModel): password: str diff --git a/backend/app/schemas/transaction.py b/backend/app/schemas/transaction.py index 0188f91..7ad8b8d 100644 --- a/backend/app/schemas/transaction.py +++ b/backend/app/schemas/transaction.py @@ -1,10 +1,11 @@ +import enum from datetime import datetime from decimal import Decimal -import enum from typing import Optional -from uuid import UUID from pydantic import BaseModel +from app.schemas.chargepoint import ChargePointThumb +from app.schemas.user import UserThumb from app.util.encoders import decimal_encoder class TransactionStatus(enum.Enum): @@ -47,8 +48,8 @@ class Transaction(BaseModel): meter_end: Optional[Decimal] = None end_reason: Optional[TransactionEventTriggerReason] = None price: Decimal - user_id: Optional[UUID] = None - chargepoint_id: UUID + user: UserThumb + chargepoint: ChargePointThumb class Config: from_attributes = True diff --git a/backend/app/schemas/user.py b/backend/app/schemas/user.py index 8737aa4..c113e88 100644 --- a/backend/app/schemas/user.py +++ b/backend/app/schemas/user.py @@ -30,6 +30,13 @@ class User(UserBase): class Config: from_attributes = True +class UserThumb(BaseModel): + id: UUID + friendly_name: str + + class Config: + from_attributes = True + class PasswordUpdate(BaseModel): old_password: str = Field(max_length=100) new_password: str = Field(max_length=100) From e5c5d9e8c496783ced5153c4815b2043d3a1a2f9 Mon Sep 17 00:00:00 2001 From: BluemediaDev Date: Sun, 25 May 2025 21:04:35 +0000 Subject: [PATCH 3/5] Make transaction.meter_end required and force tz utc for all datetimes --- backend/app/models/transaction.py | 2 +- backend/app/schemas/auth_token.py | 4 ++++ backend/app/schemas/chargepoint.py | 4 ++-- backend/app/schemas/meter_value.py | 4 ++-- backend/app/schemas/session.py | 5 ++++- backend/app/schemas/transaction.py | 6 +++--- backend/app/services/transaction_service.py | 10 ++++++++++ backend/app/util/encoders.py | 7 ++++++- 8 files changed, 32 insertions(+), 10 deletions(-) diff --git a/backend/app/models/transaction.py b/backend/app/models/transaction.py index b4dcbce..a9d4452 100644 --- a/backend/app/models/transaction.py +++ b/backend/app/models/transaction.py @@ -12,7 +12,7 @@ class Transaction(Base): started_at = Column(DateTime, index=True) ended_at = Column(DateTime, nullable=True, index=True) meter_start = Column(Numeric(10,2)) - meter_end = Column(Numeric(10,2), nullable=True) + meter_end = Column(Numeric(10,2)) end_reason = Column(Enum(TransactionEventTriggerReason), nullable=True) price = Column(Numeric(10,2)) diff --git a/backend/app/schemas/auth_token.py b/backend/app/schemas/auth_token.py index bb9ee60..9ae5534 100644 --- a/backend/app/schemas/auth_token.py +++ b/backend/app/schemas/auth_token.py @@ -3,6 +3,7 @@ from datetime import datetime from pydantic import BaseModel from app.schemas.user import Role +from app.util.encoders import force_utc_datetime @dataclass @@ -19,3 +20,6 @@ class TokenResponse(BaseModel): access_token: str refresh_token: str not_after: datetime + + class Config: + json_encoders = {datetime: force_utc_datetime} diff --git a/backend/app/schemas/chargepoint.py b/backend/app/schemas/chargepoint.py index a7d4972..878b20a 100644 --- a/backend/app/schemas/chargepoint.py +++ b/backend/app/schemas/chargepoint.py @@ -8,7 +8,7 @@ from app.schemas.connector import Connector from ocpp.v201.enums import ResetEnumType, ResetStatusEnumType -from app.util.encoders import decimal_encoder +from app.util.encoders import decimal_encoder, force_utc_datetime class ChargePointBase(BaseModel): identity: str @@ -34,7 +34,7 @@ class ChargePoint(ChargePointBase): class Config: from_attributes = True - json_encoders = {Decimal: decimal_encoder} + json_encoders = {Decimal: decimal_encoder, datetime: force_utc_datetime} class ChargePointThumb(BaseModel): id: UUID diff --git a/backend/app/schemas/meter_value.py b/backend/app/schemas/meter_value.py index 99f6945..abc08f1 100644 --- a/backend/app/schemas/meter_value.py +++ b/backend/app/schemas/meter_value.py @@ -5,7 +5,7 @@ from typing import Optional from uuid import UUID from pydantic import BaseModel -from app.util.encoders import decimal_encoder +from app.util.encoders import decimal_encoder, force_utc_datetime class PhaseType(enum.Enum): L1 = "L1" @@ -57,4 +57,4 @@ class MeterValue(BaseModel): class Config: from_attributes = True - json_encoders = {Decimal: decimal_encoder} + json_encoders = {Decimal: decimal_encoder, datetime: force_utc_datetime} diff --git a/backend/app/schemas/session.py b/backend/app/schemas/session.py index 07b98a1..d9bbf35 100644 --- a/backend/app/schemas/session.py +++ b/backend/app/schemas/session.py @@ -2,6 +2,8 @@ from datetime import datetime from uuid import UUID from pydantic import BaseModel +from app.util.encoders import force_utc_datetime + class Session(BaseModel): id: UUID @@ -9,4 +11,5 @@ class Session(BaseModel): last_used: datetime class Config: - from_attributes = True \ No newline at end of file + from_attributes = True + json_encoders = {datetime: force_utc_datetime} \ No newline at end of file diff --git a/backend/app/schemas/transaction.py b/backend/app/schemas/transaction.py index 7ad8b8d..ca242ee 100644 --- a/backend/app/schemas/transaction.py +++ b/backend/app/schemas/transaction.py @@ -6,7 +6,7 @@ from pydantic import BaseModel from app.schemas.chargepoint import ChargePointThumb from app.schemas.user import UserThumb -from app.util.encoders import decimal_encoder +from app.util.encoders import decimal_encoder, force_utc_datetime class TransactionStatus(enum.Enum): ONGOING = "ongoing" @@ -45,7 +45,7 @@ class Transaction(BaseModel): started_at: datetime ended_at: Optional[datetime] = None meter_start: Decimal - meter_end: Optional[Decimal] = None + meter_end: Decimal end_reason: Optional[TransactionEventTriggerReason] = None price: Decimal user: UserThumb @@ -53,7 +53,7 @@ class Transaction(BaseModel): class Config: from_attributes = True - json_encoders = {Decimal: decimal_encoder} + json_encoders = {Decimal: decimal_encoder, datetime: force_utc_datetime} class RemoteTransactionStartStopResponse(BaseModel): status: RemoteTransactionStartStopStatus diff --git a/backend/app/services/transaction_service.py b/backend/app/services/transaction_service.py index d8719b4..9d3c992 100644 --- a/backend/app/services/transaction_service.py +++ b/backend/app/services/transaction_service.py @@ -21,19 +21,23 @@ async def create_transaction( with SessionLocal() as db: chargepoint = db.query(ChargePoint).filter(ChargePoint.identity == chargepoint_identity).first() meter_start=0 + meter_end=0 if "meter_value" in transaction_data.keys(): for meter_value_entry in transaction_data['meter_value']: for sampled_value in meter_value_entry['sampled_value']: if "measurand" in sampled_value.keys(): if sampled_value['measurand'] == str(Measurand.ENERGY_ACTIVE_IMPORT_REGISTER): meter_start = sampled_value['value'] + meter_end = sampled_value['value'] else: meter_start = sampled_value['value'] + meter_end = sampled_value['value'] transaction = Transaction( id=transaction_info["transaction_id"], status=TransactionStatus.ONGOING, started_at=timestamp, meter_start=meter_start, + meter_end=meter_end, price=chargepoint.price, chargepoint_id=chargepoint.id, user_id=user_id @@ -55,6 +59,12 @@ async def update_transaction( transaction_id=transaction.id, meter_value_data=meter_value_entry ) + # Update current meter_end value + for sampled_value in meter_value_entry['sampled_value']: + if "measurand" in sampled_value.keys(): + if sampled_value['measurand'] == str(Measurand.ENERGY_ACTIVE_IMPORT_REGISTER): + transaction.meter_end = sampled_value['value'] + db.commit() async def end_transaction( transaction_id: str, diff --git a/backend/app/util/encoders.py b/backend/app/util/encoders.py index fbdec8d..0f0d21f 100644 --- a/backend/app/util/encoders.py +++ b/backend/app/util/encoders.py @@ -1,3 +1,4 @@ +from datetime import datetime, timezone from decimal import Decimal from typing import Union @@ -19,4 +20,8 @@ def decimal_encoder(dec_value: Decimal) -> Union[int, float]: if isinstance(exponent, int) and exponent >= 0: return int(dec_value) else: - return float(dec_value) \ No newline at end of file + return float(dec_value) + +def force_utc_datetime(datetime_value: datetime) -> datetime: + """Force a datetime to be in the UTC timzone""" + return datetime_value.replace(tzinfo=timezone.utc) \ No newline at end of file From 43d6c7a3e6f7e4e4bfb75797e8e27375b8d4b5d8 Mon Sep 17 00:00:00 2001 From: Bluemedia Date: Sun, 27 Apr 2025 13:56:20 +0000 Subject: [PATCH 4/5] Implement chargepoint list route --- frontend/src/lib/types/chargepoint.ts | 19 ++++ frontend/src/lib/util.ts | 19 ++++ .../routes/(navbar)/chargepoint/+page.svelte | 106 ++++++++++++++++++ frontend/static/locales/de/chargepoint.json | 17 +++ frontend/static/locales/en/chargepoint.json | 17 +++ 5 files changed, 178 insertions(+) create mode 100644 frontend/src/lib/types/chargepoint.ts create mode 100644 frontend/static/locales/de/chargepoint.json create mode 100644 frontend/static/locales/en/chargepoint.json diff --git a/frontend/src/lib/types/chargepoint.ts b/frontend/src/lib/types/chargepoint.ts new file mode 100644 index 0000000..ade067b --- /dev/null +++ b/frontend/src/lib/types/chargepoint.ts @@ -0,0 +1,19 @@ +export type ChargePoint = { + identity: string; + is_active: boolean; + price: number; + id: string; + last_seen: string; + vendor_name: string; + model: string; + serial_number: string, + firmware_version: string; + connectors: [ + { + id: string; + evse: number; + index: number; + status: 'Available' | 'Occupied' | 'Reserved' | 'Unavailable' | 'Faulted' + } + ] +} \ No newline at end of file diff --git a/frontend/src/lib/util.ts b/frontend/src/lib/util.ts index 9d3747c..bfabf44 100644 --- a/frontend/src/lib/util.ts +++ b/frontend/src/lib/util.ts @@ -7,4 +7,23 @@ export const currentDaytime = function(): string { return 'day' } return 'evening' +} + +export function relativeTimeFromDate(date: Date): {val: number, range: Intl.RelativeTimeFormatUnit} | undefined { + const units: {unit: Intl.RelativeTimeFormatUnit; ms: number}[] = [ + {unit: "year", ms: 31536000000}, + {unit: "month", ms: 2628000000}, + {unit: "day", ms: 86400000}, + {unit: "hour", ms: 3600000}, + {unit: "minute", ms: 60000}, + {unit: "second", ms: 1000}, + ]; + + const elapsed = date.getTime() - new Date().getTime(); + for (const {unit, ms} of units) { + if (Math.abs(elapsed) >= ms || unit === "second") { + return {val: Math.round(elapsed / ms), range: unit}; + } + } + return undefined; } \ No newline at end of file diff --git a/frontend/src/routes/(navbar)/chargepoint/+page.svelte b/frontend/src/routes/(navbar)/chargepoint/+page.svelte index e69de29..0d65da3 100644 --- a/frontend/src/routes/(navbar)/chargepoint/+page.svelte +++ b/frontend/src/routes/(navbar)/chargepoint/+page.svelte @@ -0,0 +1,106 @@ + + +
+

+ {$i18n.t('chargepoint:header')} +

+
+ + + + + + + + + + + + {#if chargepoints.length > 0} + {#each chargepoints as chargepoint} + + + + + + + + {/each} + {/if} + +
{$i18n.t('chargepoint:tableHeader.name')}{$i18n.t('chargepoint:tableHeader.active')}{$i18n.t('chargepoint:tableHeader.lastSeen')}{$i18n.t('chargepoint:tableHeader.price')}
+ {chargepoint.identity} + + {#if chargepoint.is_active} + {$i18n.t('chargepoint:state.enabled')} + {:else} + {$i18n.t('chargepoint:state.disabled')} + {/if} + + {$i18n.t( + 'chargepoint:tableFormatting.lastSeen', + relativeTimeFromDate(new Date(chargepoint.last_seen))! + )} + {chargepoint.price} € + + {$i18n.t('common:transactionTable.detailButton')} + + +
+ {#if chargepoints.length === 0} +

+ {$i18n.t('chargepoint:noChargepoints')} +

+ {/if} +
+
+ + + +
+
diff --git a/frontend/static/locales/de/chargepoint.json b/frontend/static/locales/de/chargepoint.json new file mode 100644 index 0000000..25d2220 --- /dev/null +++ b/frontend/static/locales/de/chargepoint.json @@ -0,0 +1,17 @@ +{ + "header": "Ladepunkte", + "tableHeader": { + "name": "Name", + "active": "Aktiv", + "price": "Preis", + "lastSeen": "Zuletzt gesehen" + }, + "tableFormatting": { + "lastSeen": "{{val, relativetime}}" + }, + "state": { + "enabled": "Aktiv", + "disabled": "Deaktiviert" + }, + "noChargepoints": "Noch keine Ladepunkte vorhanden." +} \ No newline at end of file diff --git a/frontend/static/locales/en/chargepoint.json b/frontend/static/locales/en/chargepoint.json new file mode 100644 index 0000000..e485e10 --- /dev/null +++ b/frontend/static/locales/en/chargepoint.json @@ -0,0 +1,17 @@ +{ + "header": "Chargepoints", + "tableHeader": { + "name": "Name", + "active": "Active", + "price": "Price", + "lastSeen": "Last seen" + }, + "tableFormatting": { + "lastSeen": "{{val, relativetime}}" + }, + "state": { + "enabled": "Active", + "disabled": "Disabled" + }, + "noChargepoints": "There are no chargepoints yet." +} \ No newline at end of file From a61231957b6bcb160b46212651f8f9b340964833 Mon Sep 17 00:00:00 2001 From: BluemediaDev Date: Sun, 25 May 2025 21:07:28 +0000 Subject: [PATCH 5/5] Implement dashboard data logic --- .../src/lib/component/DashboardCard.svelte | 26 ++--- .../src/lib/component/TransactionTable.svelte | 27 +++-- frontend/src/lib/types/chargepoint.ts | 6 ++ frontend/src/lib/types/dashboard.ts | 14 +++ frontend/src/lib/types/transaction.ts | 17 ++++ frontend/src/lib/types/user.ts | 4 + frontend/src/routes/(navbar)/+page.svelte | 99 +++++++++---------- 7 files changed, 115 insertions(+), 78 deletions(-) create mode 100644 frontend/src/lib/types/dashboard.ts create mode 100644 frontend/src/lib/types/transaction.ts create mode 100644 frontend/src/lib/types/user.ts diff --git a/frontend/src/lib/component/DashboardCard.svelte b/frontend/src/lib/component/DashboardCard.svelte index 2b77f43..c476707 100644 --- a/frontend/src/lib/component/DashboardCard.svelte +++ b/frontend/src/lib/component/DashboardCard.svelte @@ -25,18 +25,20 @@

{props.value}{#if props.unit}{' ' + props.unit}{/if}

- {#if diff > 0} -
- {diff}% -
- {:else if diff < 0} -
- {diff}% -
- {:else} -
- {diff}% -
+ {#if !Number.isNaN(diff)} + {#if diff > 0} +
+ {diff}% +
+ {:else if diff < 0} +
+ {diff}% +
+ {:else} +
+ {diff}% +
+ {/if} {/if} diff --git a/frontend/src/lib/component/TransactionTable.svelte b/frontend/src/lib/component/TransactionTable.svelte index 0940a37..6b5a73e 100644 --- a/frontend/src/lib/component/TransactionTable.svelte +++ b/frontend/src/lib/component/TransactionTable.svelte @@ -1,18 +1,9 @@ @@ -36,7 +27,7 @@

{$i18n.t('common:transactionTable.startTime', { - time: transaction.begin, + time: new Date(transaction.started_at), formatParams: { time: { year: 'numeric', @@ -51,7 +42,7 @@

{$i18n.t('common:transactionTable.endTime', { - time: transaction.end, + time: new Date(transaction.ended_at!), formatParams: { time: { year: 'numeric', @@ -70,11 +61,15 @@ - {transaction.chargepoint.name} + {transaction.chargepoint.identity} - {transaction.energyAmmount} kWh - {transaction.cost} € + {(transaction.meter_end - transaction.meter_start).toFixed(2)} kWh + {((transaction.meter_end - transaction.meter_start) * transaction.price).toFixed(2)} € {$i18n.t('common:transactionTable.detailButton')} diff --git a/frontend/src/lib/types/chargepoint.ts b/frontend/src/lib/types/chargepoint.ts index ade067b..4e4574e 100644 --- a/frontend/src/lib/types/chargepoint.ts +++ b/frontend/src/lib/types/chargepoint.ts @@ -16,4 +16,10 @@ export type ChargePoint = { status: 'Available' | 'Occupied' | 'Reserved' | 'Unavailable' | 'Faulted' } ] +} + +export type ChargePointThumb = { + id: string; + identity: string; + price: number; } \ No newline at end of file diff --git a/frontend/src/lib/types/dashboard.ts b/frontend/src/lib/types/dashboard.ts new file mode 100644 index 0000000..51713ff --- /dev/null +++ b/frontend/src/lib/types/dashboard.ts @@ -0,0 +1,14 @@ +import type { Transaction } from "./transaction"; + +export type Dashboard = { + stats: { + transaction_count: number; + transaction_count_previous: number; + transaction_energy_total: number; + transaction_energy_total_previous: number; + transaction_cost_total: number; + transaction_cost_total_previous: number; + }; + current_transaction: Transaction | undefined; + recent_transactions: Transaction[]; +} \ No newline at end of file diff --git a/frontend/src/lib/types/transaction.ts b/frontend/src/lib/types/transaction.ts new file mode 100644 index 0000000..3fc6501 --- /dev/null +++ b/frontend/src/lib/types/transaction.ts @@ -0,0 +1,17 @@ +import type { ChargePointThumb } from "./chargepoint"; +import type { UserThumb } from "./user"; + +export type TransactionEventTriggerReason = 'Authorized' | 'CablePluggedIn' | 'ChargingRateChanged' | 'ChargingStateChanged' | 'Deauthorized' | 'EnergyLimitReached' | 'EVCommunicationLost' | 'EVConnectTimeout' | 'MeterValueClock' | 'MeterValuePeriodic' | 'TimeLimitReached' | 'Trigger' | 'UnlockCommand' | 'StopAuthorized' | 'EVDeparted' | 'EVDetected' | 'RemoteStop' | 'RemoteStart' | 'AbnormalCondition' | 'SignedDataReceived' | 'ResetCommand'; + +export type Transaction = { + id: string; + status: 'ongoing' | 'ended'; + started_at: string; + ended_at: string | undefined; + meter_start: number; + meter_end: number; + end_reason: TransactionEventTriggerReason; + price: number; + user: UserThumb; + chargepoint: ChargePointThumb; +} \ No newline at end of file diff --git a/frontend/src/lib/types/user.ts b/frontend/src/lib/types/user.ts new file mode 100644 index 0000000..aa1d184 --- /dev/null +++ b/frontend/src/lib/types/user.ts @@ -0,0 +1,4 @@ +export type UserThumb = { + id: string; + friendly_name: string; +} \ No newline at end of file diff --git a/frontend/src/routes/(navbar)/+page.svelte b/frontend/src/routes/(navbar)/+page.svelte index e96407d..de8c463 100644 --- a/frontend/src/routes/(navbar)/+page.svelte +++ b/frontend/src/routes/(navbar)/+page.svelte @@ -4,10 +4,39 @@ import i18n from '$lib/i18n' import DashboardCard from '$lib/component/DashboardCard.svelte' import TransactionTable from '$lib/component/TransactionTable.svelte' + import type { Dashboard } from '$lib/types/dashboard' + import axios from '$lib/axios.svelte' + import { onMount } from 'svelte' $i18n.loadNamespaces('dashboard') - let hasActiveTransaction = $state(true) + let dashboardData: Dashboard = $state({ + stats: { + transaction_count: 0, + transaction_count_previous: 0, + transaction_energy_total: 0, + transaction_energy_total_previous: 0, + transaction_cost_total: 0, + transaction_cost_total_previous: 0, + }, + current_transaction: undefined, + recent_transactions: [], + }) + + function refreshDashboard() { + axios.get('/me/dashboard').then((res) => { + dashboardData = res.data + }) + } + + onMount(() => { + refreshDashboard() + const interval = setInterval(() => { + refreshDashboard() + }, 15000) + + return () => clearInterval(interval) + })

- {#if hasActiveTransaction} - {$i18n.t('dashboard:cards.chargepoint', { name: 'DE-EXMPL-0001' })} + {#if dashboardData.current_transaction} + {$i18n.t('dashboard:cards.chargepoint', { + name: dashboardData.current_transaction.chargepoint.identity, + })} {:else} {$i18n.t('dashboard:cards.noCurrentTransaction')} {/if}

- {#if hasActiveTransaction} + {#if dashboardData.current_transaction}

{$i18n.t('dashboard:table.title')}

- +