diff --git a/FrontEnd/package.json b/FrontEnd/package.json index 35aaeef..b33a1e2 100644 --- a/FrontEnd/package.json +++ b/FrontEnd/package.json @@ -11,14 +11,15 @@ "private": true, "packageManager": "pnpm@11.1.1", "dependencies": { - "@angular/compiler": "^21.2.14", "@angular/common": "^21.2.14", + "@angular/compiler": "^21.2.14", "@angular/core": "^21.2.0", "@angular/forms": "^21.2.14", "@angular/platform-browser": "^21.2.0", "@angular/router": "^21.2.13", "@fortawesome/fontawesome-free": "^7.2.0", "@primeuix/themes": "^2.0.3", + "chart.js": "^4.5.1", "primeng": "^21.1.7", "rxjs": "~7.8.0", "tslib": "^2.3.0" diff --git a/FrontEnd/pnpm-lock.yaml b/FrontEnd/pnpm-lock.yaml index 6c8e1e3..87fd7b0 100644 --- a/FrontEnd/pnpm-lock.yaml +++ b/FrontEnd/pnpm-lock.yaml @@ -32,6 +32,9 @@ importers: '@primeuix/themes': specifier: ^2.0.3 version: 2.0.3 + chart.js: + specifier: ^4.5.1 + version: 4.5.1 primeng: specifier: ^21.1.7 version: 21.1.7(c887dd6943ee5fd9e904a6f33e01a80f) @@ -759,6 +762,9 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@kurkle/color@0.3.4': + resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==} + '@listr2/prompt-adapter-inquirer@3.0.5': resolution: {integrity: sha512-WELs+hj6xcilkloBXYf9XXK8tYEnKsgLj01Xl5ONUJpKjmT5hGVUzNUS5tooUxs7pGMrw+jFD/41WpqW4V3LDA==} engines: {node: '>=20.0.0'} @@ -1631,6 +1637,10 @@ packages: chardet@2.1.1: resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} + chart.js@4.5.1: + resolution: {integrity: sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==} + engines: {pnpm: '>=8'} + chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} @@ -3602,6 +3612,8 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@kurkle/color@0.3.4': {} + '@listr2/prompt-adapter-inquirer@3.0.5(@inquirer/prompts@7.10.1)(listr2@9.0.5)': dependencies: '@inquirer/prompts': 7.10.1 @@ -4336,6 +4348,10 @@ snapshots: chardet@2.1.1: {} + chart.js@4.5.1: + dependencies: + '@kurkle/color': 0.3.4 + chokidar@4.0.3: dependencies: readdirp: 4.1.2 diff --git a/FrontEnd/src/app/app.html b/FrontEnd/src/app/app.html index e160eee..f5cf8bd 100644 --- a/FrontEnd/src/app/app.html +++ b/FrontEnd/src/app/app.html @@ -8,7 +8,17 @@
diff --git a/FrontEnd/src/app/app.routes.ts b/FrontEnd/src/app/app.routes.ts index e26f1ab..0c7900a 100644 --- a/FrontEnd/src/app/app.routes.ts +++ b/FrontEnd/src/app/app.routes.ts @@ -3,6 +3,7 @@ import { Home } from './pages/home/home'; import { HomeAssistantConnection } from './pages/home-assistant-connection/home-assistant-connection'; import { PowerEntitySetup } from './pages/power-entity-setup/power-entity-setup'; import { EnergyHistorySetup } from './pages/energy-history-setup/energy-history-setup'; +import { EnergyHistoryView } from './pages/energy-history-view/energy-history-view'; export const routes: Routes = [ { @@ -20,5 +21,9 @@ export const routes: Routes = [ { path: 'energy-history', component: EnergyHistorySetup, + }, + { + path: 'energy-history-view', + component: EnergyHistoryView, } ]; diff --git a/FrontEnd/src/app/app.scss b/FrontEnd/src/app/app.scss index 9dc1a3b..0e3ede1 100644 --- a/FrontEnd/src/app/app.scss +++ b/FrontEnd/src/app/app.scss @@ -72,9 +72,27 @@ font-weight: 600; } -:host ::ng-deep .left-menu { +.menu-section { + margin-bottom: 1.5rem; + padding: 0.75rem; border: 1px solid var(--line); - background: rgba(255, 255, 255, 0.55); + border-radius: 0.5rem; + background: rgba(255, 255, 255, 0.3); +} + +.menu-section-header { + font-size: 0.875rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--sea-700); + margin-bottom: 0.5rem; + padding: 0 0.5rem; +} + +:host ::ng-deep .left-menu { + border: 0; + background: transparent; } :host ::ng-deep .left-menu .p-menu-item-link { diff --git a/FrontEnd/src/app/app.ts b/FrontEnd/src/app/app.ts index 5d7e7cc..fcd541a 100644 --- a/FrontEnd/src/app/app.ts +++ b/FrontEnd/src/app/app.ts @@ -17,12 +17,15 @@ export class App implements OnInit { protected readonly configStatus = inject(ConfigurationStatusService); - protected readonly sideMenuItems = computed(() => [ + protected readonly homeMenuItems = computed(() => [ { label: 'Home', icon: 'fa-solid fa-house', routerLink: '/', }, + ]); + + protected readonly configMenuItems = computed(() => [ { label: 'HA Connection', icon: 'fa-solid fa-plug', @@ -39,7 +42,7 @@ export class App implements OnInit { disabled: !this.configStatus.haConnectionConfigured(), }, { - label: 'Energy History', + label: 'Get Energy History', icon: 'fa-solid fa-chart-line', routerLink: '/energy-history', badge: this.configStatus.energyHistoryFetched() ? '✓' : undefined, @@ -48,6 +51,15 @@ export class App implements OnInit { }, ]); + protected readonly analyticsMenuItems = computed(() => [ + { + label: 'Energy History', + icon: 'fa-solid fa-chart-column', + routerLink: '/energy-history-view', + disabled: !this.configStatus.energyHistoryFetched(), + }, + ]); + ngOnInit(): void { this.configStatus.refresh(); } diff --git a/FrontEnd/src/app/pages/energy-history-view/energy-history-view.html b/FrontEnd/src/app/pages/energy-history-view/energy-history-view.html new file mode 100644 index 0000000..bdb615b --- /dev/null +++ b/FrontEnd/src/app/pages/energy-history-view/energy-history-view.html @@ -0,0 +1,66 @@ +
+

Energy History

+ + @if (loading()) { +
Loading energy history data...
+ } @else { +
+ +

{{ selectedYear() }}

+ +
+ +
+ +
+ +
+

Detailed Data

+ + + + + Year + + + Month + + + Type + + + Entity ID + + + Average Value (kWh) + + + + + + {{ item.year }} + {{ item.month }} + {{ getEntityTypeName(item.entityType) }} + {{ item.entityId }} + {{ item.averageValue | number: '1.2-2' }} + + + +
+ } +
diff --git a/FrontEnd/src/app/pages/energy-history-view/energy-history-view.scss b/FrontEnd/src/app/pages/energy-history-view/energy-history-view.scss new file mode 100644 index 0000000..6e7ed1f --- /dev/null +++ b/FrontEnd/src/app/pages/energy-history-view/energy-history-view.scss @@ -0,0 +1,68 @@ + +.energy-history-view { + width: 100%; + max-width: 100%; + + h1 { + font-size: 2rem; + font-weight: 700; + margin-bottom: 1.5rem; + color: var(--sea-900); + } + + h2 { + font-size: 1.5rem; + font-weight: 600; + margin-bottom: 1rem; + color: var(--sea-700); + } + + .loading { + text-align: center; + padding: 2rem; + font-size: 1.1rem; + color: var(--sea-700); + } + + .year-controls { + display: flex; + align-items: center; + justify-content: center; + gap: 1rem; + margin-bottom: 1.5rem; + background: white; + padding: 1rem; + border-radius: 0.5rem; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + + .year-display { + font-size: 1.75rem; + font-weight: 700; + color: var(--sea-900); + margin: 0; + min-width: 5rem; + text-align: center; + } + } + + .chart-container { + background: white; + padding: 1.5rem; + border-radius: 0.5rem; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + margin-bottom: 2rem; + height: 400px; + } + + .table-container { + background: white; + padding: 1.5rem; + border-radius: 0.5rem; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + overflow-x: auto; + width: 100%; + } +} +.p-chart{ + height: 100%; +} \ No newline at end of file diff --git a/FrontEnd/src/app/pages/energy-history-view/energy-history-view.ts b/FrontEnd/src/app/pages/energy-history-view/energy-history-view.ts new file mode 100644 index 0000000..75e72be --- /dev/null +++ b/FrontEnd/src/app/pages/energy-history-view/energy-history-view.ts @@ -0,0 +1,212 @@ +import { Component, computed, inject, OnInit, signal } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TableModule } from 'primeng/table'; +import { ChartModule } from 'primeng/chart'; +import { ButtonModule } from 'primeng/button'; +import { ApiEnergyHistoryService } from '../../../services/api-services/api-energy-history-service/api-energy-history-service'; +import { EnergyHistoryDto } from '../../../models/energy-history-dto'; +import { EnergyEntityTypeEnum } from '../../../enums/energy-entity-type-enum'; + +interface ChartDataPoint { + date: string; + powerConsumption: number; + powerProduction: number; + pv: number; + usedSolar: number; +} + +@Component({ + selector: 'app-energy-history-view', + standalone: true, + imports: [CommonModule, TableModule, ChartModule, ButtonModule], + templateUrl: './energy-history-view.html', + styleUrl: './energy-history-view.scss', +}) +export class EnergyHistoryView implements OnInit { + private readonly apiEnergyHistoryService = inject(ApiEnergyHistoryService); + + readonly energyHistory = signal([]); + readonly allData = signal([]); + readonly chartData = signal({}); + readonly chartOptions = signal({}); + readonly loading = signal(true); + readonly selectedYear = signal(new Date().getFullYear()); + + readonly availableYears = computed(() => { + const years = new Set(this.allData().map(d => d.year)); + return Array.from(years).sort((a, b) => a - b); + }); + + readonly canGoPrevious = computed(() => { + const years = this.availableYears(); + return years.length > 0 && this.selectedYear() > years[0]; + }); + + readonly canGoNext = computed(() => { + const years = this.availableYears(); + return years.length > 0 && this.selectedYear() < years[years.length - 1]; + }); + + readonly EnergyEntityTypeEnum = EnergyEntityTypeEnum; + + async ngOnInit(): Promise { + await this.loadData(); + } + + async loadData(): Promise { + this.loading.set(true); + try { + const data = await this.apiEnergyHistoryService.getEnergyHistory(); + this.allData.set(data); + this.energyHistory.set(data); + + // Set initial year to the most recent year with data + const years = Array.from(new Set(data.map(d => d.year))).sort((a, b) => b - a); + if (years.length > 0) { + this.selectedYear.set(years[0]); + } + + this.updateChartForYear(this.selectedYear()); + } catch (error) { + console.error('Error loading energy history:', error); + } finally { + this.loading.set(false); + } + } + + previousYear(): void { + const years = this.availableYears(); + const currentIndex = years.indexOf(this.selectedYear()); + if (currentIndex > 0) { + this.selectedYear.set(years[currentIndex - 1]); + this.updateChartForYear(this.selectedYear()); + } + } + + nextYear(): void { + const years = this.availableYears(); + const currentIndex = years.indexOf(this.selectedYear()); + if (currentIndex < years.length - 1) { + this.selectedYear.set(years[currentIndex + 1]); + this.updateChartForYear(this.selectedYear()); + } + } + + private updateChartForYear(year: number): void { + const yearData = this.allData().filter(d => d.year === year); + console.log(`Filtering data for year ${year}:`, yearData.length, 'records'); + this.prepareChartData(yearData, year); + } + + private prepareChartData(data: EnergyHistoryDto[], year: number): void { + const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + + // Initialize all 12 months with zero values + const monthlyData: ChartDataPoint[] = Array.from({ length: 12 }, (_, i) => ({ + date: monthNames[i], + powerConsumption: 0, + powerProduction: 0, + pv: 0, + usedSolar: 0, + })); + + // Populate with actual data + data.forEach((item) => { + const monthIndex = item.month - 1; // month is 1-based, array is 0-based + if (monthIndex >= 0 && monthIndex < 12) { + const point = monthlyData[monthIndex]; + switch (item.entityType) { + case EnergyEntityTypeEnum.PowerConsumption: + point.powerConsumption += item.averageValue; + break; + case EnergyEntityTypeEnum.PowerProduction: + point.powerProduction += item.averageValue; + break; + case EnergyEntityTypeEnum.Pv: + point.pv += item.averageValue; + break; + } + } + }); + + // Calculate used solar: PV production - total exported (PowerProduction) + monthlyData.forEach((point) => { + point.usedSolar = point.pv - point.powerProduction; + }); + + // Prepare chart data + this.chartData.set({ + labels: monthlyData.map((d) => d.date), + datasets: [ + { + label: 'Power Consumption', + backgroundColor: '#ef4444', + data: monthlyData.map((d) => d.powerConsumption), + stack: 'energy', + }, + { + label: 'Power Production (Export)', + backgroundColor: '#22c55e', + data: monthlyData.map((d) => d.powerProduction), + stack: 'energy', + }, + { + label: 'PV Production', + backgroundColor: '#f59e0b', + data: monthlyData.map((d) => d.pv), + stack: 'energy', + }, + { + label: 'Used Solar', + backgroundColor: '#3b82f6', + data: monthlyData.map((d) => d.usedSolar), + stack: 'energy', + }, + ], + }); + + this.chartOptions.set({ + responsive: false, + maintainAspectRatio: false, + plugins: { + legend: { + position: 'top', + }, + title: { + display: true, + text: `Energy History (kWh) - ${year}`, + }, + }, + scales: { + x: { + stacked: true, + title: { + display: true, + text: 'Month', + }, + }, + y: { + stacked: true, + title: { + display: true, + text: 'kWh', + }, + beginAtZero: true, + }, + }, + }); + } + + getEntityTypeName(type: EnergyEntityTypeEnum): string { + switch (type) { + case EnergyEntityTypeEnum.PowerConsumption: + return 'Power Consumption'; + case EnergyEntityTypeEnum.PowerProduction: + return 'Power Production'; + case EnergyEntityTypeEnum.Pv: + return 'PV'; + default: + return 'Unknown'; + } + } +} diff --git a/FrontEnd/src/enums/energy-entity-type-enum.ts b/FrontEnd/src/enums/energy-entity-type-enum.ts index 078b43b..e631cd4 100644 --- a/FrontEnd/src/enums/energy-entity-type-enum.ts +++ b/FrontEnd/src/enums/energy-entity-type-enum.ts @@ -1,6 +1,6 @@ export enum EnergyEntityTypeEnum { - PowerConsumption = 0, - PowerProduction = 'PowerProduction', - Pv = 'Pv' + PowerProduction = 0, + PowerConsumption = 1, + Pv = 2 }