ADD: Temperatur and Weather Overview
This commit is contained in:
parent
9bcd4e2e39
commit
af273889d8
33
package-lock.json
generated
33
package-lock.json
generated
@ -16,6 +16,8 @@
|
|||||||
"@angular/material": "^22.0.2",
|
"@angular/material": "^22.0.2",
|
||||||
"@angular/platform-browser": "^22.0.0",
|
"@angular/platform-browser": "^22.0.0",
|
||||||
"@angular/router": "^22.0.0",
|
"@angular/router": "^22.0.0",
|
||||||
|
"date-fns": "^4.4.0",
|
||||||
|
"ng-apexcharts": "^2.4.0",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
"tslib": "^2.3.0"
|
"tslib": "^2.3.0"
|
||||||
},
|
},
|
||||||
@ -4533,6 +4535,13 @@
|
|||||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/apexcharts": {
|
||||||
|
"version": "5.15.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-5.15.2.tgz",
|
||||||
|
"integrity": "sha512-qbd+ehDiRxo++wAqaGLmJcd683ktulTqoku4WYYyQ3XOgJIn4yhOQGV27KcdK7c2yhwxblyh4mgMm8ukI0wC0Q==",
|
||||||
|
"license": "SEE LICENSE IN LICENSE",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/assertion-error": {
|
"node_modules/assertion-error": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
||||||
@ -5308,6 +5317,16 @@
|
|||||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/date-fns": {
|
||||||
|
"version": "4.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.4.0.tgz",
|
||||||
|
"integrity": "sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/kossnocorp"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.3",
|
"version": "4.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
@ -7349,6 +7368,20 @@
|
|||||||
"node": ">= 0.4.0"
|
"node": ">= 0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ng-apexcharts": {
|
||||||
|
"version": "2.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ng-apexcharts/-/ng-apexcharts-2.4.0.tgz",
|
||||||
|
"integrity": "sha512-bNXIOQrux9ZXnpm4kxbW8M6AA4ENAEOpR31ZoaeBkc0SrfOMw1nxRN5ORkStOQFVQF5Wf5qivAA9KIgU7Ol26A==",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.8.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@angular/common": ">=20.0.0",
|
||||||
|
"@angular/core": ">=20.0.0",
|
||||||
|
"apexcharts": "^5.10.3",
|
||||||
|
"rxjs": "^7.8.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/node-addon-api": {
|
"node_modules/node-addon-api": {
|
||||||
"version": "6.1.0",
|
"version": "6.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz",
|
||||||
|
|||||||
@ -19,6 +19,8 @@
|
|||||||
"@angular/material": "^22.0.2",
|
"@angular/material": "^22.0.2",
|
||||||
"@angular/platform-browser": "^22.0.0",
|
"@angular/platform-browser": "^22.0.0",
|
||||||
"@angular/router": "^22.0.0",
|
"@angular/router": "^22.0.0",
|
||||||
|
"date-fns": "^4.4.0",
|
||||||
|
"ng-apexcharts": "^2.4.0",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
"tslib": "^2.3.0"
|
"tslib": "^2.3.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -25,7 +25,10 @@
|
|||||||
<mat-drawer mode="side" opened>
|
<mat-drawer mode="side" opened>
|
||||||
<app-station-list></app-station-list>
|
<app-station-list></app-station-list>
|
||||||
</mat-drawer>
|
</mat-drawer>
|
||||||
<mat-drawer-content>Main Cointent</mat-drawer-content>
|
<mat-drawer-content>
|
||||||
|
<app-wettter-overview></app-wettter-overview>
|
||||||
|
<app-temperature></app-temperature>
|
||||||
|
</mat-drawer-content>
|
||||||
</mat-drawer-container>
|
</mat-drawer-container>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -5,10 +5,12 @@ import {MatIcon} from '@angular/material/icon';
|
|||||||
import {MatButton, MatIconButton} from '@angular/material/button';
|
import {MatButton, MatIconButton} from '@angular/material/button';
|
||||||
import {MatDrawer, MatDrawerContainer, MatDrawerContent} from '@angular/material/sidenav';
|
import {MatDrawer, MatDrawerContainer, MatDrawerContent} from '@angular/material/sidenav';
|
||||||
import {StationList} from './station-list/station-list';
|
import {StationList} from './station-list/station-list';
|
||||||
|
import {WettterOverview} from './components/wettter-overview/wettter-overview';
|
||||||
|
import {TemperatureComponent} from './components/diagrams/temperature/temperature';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
imports: [RouterOutlet, MatToolbar, MatIcon, MatIconButton, RouterLink, MatButton, MatDrawerContainer, MatDrawer, MatDrawerContent, StationList],
|
imports: [RouterOutlet, MatToolbar, MatIcon, MatIconButton, RouterLink, MatButton, MatDrawerContainer, MatDrawer, MatDrawerContent, StationList, WettterOverview, TemperatureComponent],
|
||||||
templateUrl: './app.html',
|
templateUrl: './app.html',
|
||||||
styleUrl: './app.css'
|
styleUrl: './app.css'
|
||||||
})
|
})
|
||||||
|
|||||||
21
src/app/components/diagrams/temperature/temperature.html
Normal file
21
src/app/components/diagrams/temperature/temperature.html
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
@if(measurements) {
|
||||||
|
<mat-card>
|
||||||
|
<mat-card-header>
|
||||||
|
<mat-card-title>Temperaturverlauf</mat-card-title>
|
||||||
|
</mat-card-header>
|
||||||
|
<mat-card-content>
|
||||||
|
<apx-chart
|
||||||
|
[series]="chartOptions.series!"
|
||||||
|
[chart]="chartOptions.chart!"
|
||||||
|
[xaxis]="chartOptions.xaxis!"
|
||||||
|
[yaxis]="chartOptions.yaxis!"
|
||||||
|
[stroke]="chartOptions.stroke!"
|
||||||
|
[grid]="chartOptions.grid!"
|
||||||
|
[tooltip]="chartOptions.tooltip!"
|
||||||
|
[legend]="chartOptions.legend!"
|
||||||
|
[theme]="chartOptions.theme!"
|
||||||
|
/>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
|
||||||
|
}
|
||||||
22
src/app/components/diagrams/temperature/temperature.spec.ts
Normal file
22
src/app/components/diagrams/temperature/temperature.spec.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { Temperature } from './temperature';
|
||||||
|
|
||||||
|
describe('Temperature', () => {
|
||||||
|
let component: Temperature;
|
||||||
|
let fixture: ComponentFixture<Temperature>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [Temperature],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(Temperature);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
await fixture.whenStable();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
120
src/app/components/diagrams/temperature/temperature.ts
Normal file
120
src/app/components/diagrams/temperature/temperature.ts
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import {ChangeDetectorRef, Component, Input, OnInit} from '@angular/core';
|
||||||
|
import {DefaultService, MeasurementListResponse, MeasurementResolution, StationListResponse} from '../../../api';
|
||||||
|
import {addDays, addHours, addMonths, addYears, startOfDay} from 'date-fns';
|
||||||
|
import {ApexOptions, ChartComponent} from 'ng-apexcharts';
|
||||||
|
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card';
|
||||||
|
import {MatProgressSpinner} from '@angular/material/progress-spinner';
|
||||||
|
|
||||||
|
export type TimeResolution = '24h' | '7d' | '30d' | '1Y' | 'All'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-temperature',
|
||||||
|
imports: [
|
||||||
|
ChartComponent,
|
||||||
|
MatCard,
|
||||||
|
MatCardContent,
|
||||||
|
MatCardHeader,
|
||||||
|
MatCardTitle,
|
||||||
|
],
|
||||||
|
templateUrl: './temperature.html',
|
||||||
|
styleUrl: './temperature.css',
|
||||||
|
})
|
||||||
|
export class TemperatureComponent implements OnInit {
|
||||||
|
@Input() timeResolution: TimeResolution = '7d';
|
||||||
|
@Input() activeStations: StationListResponse[] = [];
|
||||||
|
|
||||||
|
resolution: MeasurementResolution = MeasurementResolution.Daily;
|
||||||
|
measurements: MeasurementListResponse | undefined;
|
||||||
|
chartOptions: ApexOptions = {};
|
||||||
|
|
||||||
|
constructor(private api: DefaultService, private cdr: ChangeDetectorRef) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
if (this.activeStations.length === 0) {
|
||||||
|
this.api.listStationsStationsListGet().subscribe(stations => {
|
||||||
|
this.activeStations = stations;
|
||||||
|
this.fetchMeasurements();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.fetchMeasurements();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get timeIntervalStart(): string | undefined {
|
||||||
|
const now = new Date();
|
||||||
|
const map: Record<TimeResolution, Date | undefined> = {
|
||||||
|
'24h': addHours(now, -24),
|
||||||
|
'7d': startOfDay(addDays(now, -7)),
|
||||||
|
'30d': startOfDay(addMonths(now, -1)),
|
||||||
|
'1Y': startOfDay(addYears(now, -1)),
|
||||||
|
'All': undefined
|
||||||
|
};
|
||||||
|
return map[this.timeResolution]?.toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchMeasurements() {
|
||||||
|
const stationIds = this.activeStations.map(s => s.id);
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
|
this.api.getMeasurementsMeasurementsGet(
|
||||||
|
stationIds, undefined, this.timeIntervalStart, now, this.resolution
|
||||||
|
).subscribe(data => {
|
||||||
|
this.measurements = data;
|
||||||
|
this.buildChart();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
buildChart() {
|
||||||
|
if (!this.measurements) return;
|
||||||
|
|
||||||
|
this.chartOptions = {
|
||||||
|
chart: {
|
||||||
|
type: 'line',
|
||||||
|
height: 280,
|
||||||
|
toolbar: { show: false },
|
||||||
|
animations: { enabled: true },
|
||||||
|
background: 'transparent',
|
||||||
|
},
|
||||||
|
theme: { mode: 'dark' },
|
||||||
|
stroke: {
|
||||||
|
curve: 'smooth',
|
||||||
|
width: 2,
|
||||||
|
},
|
||||||
|
series: this.measurements.stations.map(s => ({
|
||||||
|
name: `${s.station.name} ${s.indoor ? '(innen)' : '(außen)'}`,
|
||||||
|
data: s.measurements.map(m => ({
|
||||||
|
x: new Date(m.timestamp).getTime(),
|
||||||
|
y: m.temperature
|
||||||
|
}))
|
||||||
|
})),
|
||||||
|
xaxis: {
|
||||||
|
type: 'datetime',
|
||||||
|
labels: {
|
||||||
|
datetimeUTC: false,
|
||||||
|
style: { colors: '#9e9e9e', fontSize: '11px' }
|
||||||
|
},
|
||||||
|
axisBorder: { show: false },
|
||||||
|
axisTicks: { show: false },
|
||||||
|
},
|
||||||
|
yaxis: {
|
||||||
|
labels: {
|
||||||
|
style: { colors: '#9e9e9e', fontSize: '11px' },
|
||||||
|
formatter: (val: number) => `${val.toFixed(1)}°`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
borderColor: 'rgba(255,255,255,0.06)',
|
||||||
|
strokeDashArray: 4,
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
theme: 'dark',
|
||||||
|
x: { format: 'dd.MM.yyyy HH:mm' },
|
||||||
|
y: { formatter: (val: number) => `${val.toFixed(1)} °C` }
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
labels: { colors: '#9e9e9e' }
|
||||||
|
},
|
||||||
|
};
|
||||||
|
this.cdr.detectChanges()
|
||||||
|
}
|
||||||
|
}
|
||||||
85
src/app/components/wettter-overview/wettter-overview.css
Normal file
85
src/app/components/wettter-overview/wettter-overview.css
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
margin: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
background: #1e1e1e !important;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 18px;
|
||||||
|
box-shadow:
|
||||||
|
0 4px 10px rgba(0, 0, 0, .35),
|
||||||
|
inset 0 1px 0 rgba(255,255,255,.04);
|
||||||
|
transition: all .2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow:
|
||||||
|
0 8px 18px rgba(0,0,0,.45),
|
||||||
|
inset 0 1px 0 rgba(255,255,255,.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
color: #9e9e9e;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label mat-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
color: #8a8a8a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1;
|
||||||
|
letter-spacing: -0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change {
|
||||||
|
margin-top: 10px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
font-size: .82rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change mat-icon {
|
||||||
|
font-size: 15px;
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.positive {
|
||||||
|
color: #37d67a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.negative {
|
||||||
|
color: #ff6b6b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.neutral {
|
||||||
|
color: #9e9e9e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.forecast {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: .78rem;
|
||||||
|
color: #9e9e9e;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
85
src/app/components/wettter-overview/wettter-overview.html
Normal file
85
src/app/components/wettter-overview/wettter-overview.html
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
<div class="stats-grid">
|
||||||
|
<mat-card class="stat-card">
|
||||||
|
<div class="label">
|
||||||
|
<mat-icon>thermostat</mat-icon>
|
||||||
|
Außen
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="value">{{out_temperature}} °C</div>
|
||||||
|
|
||||||
|
@if (outer_temperature_diff) {
|
||||||
|
@if (outer_temperature_diff > 0) {
|
||||||
|
<div class="change negative">
|
||||||
|
<mat-icon>arrow_upward</mat-icon>
|
||||||
|
{{outer_temperature_diff | number : '1.1-2'}}° vs. gestern
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<div class="change positive">
|
||||||
|
<mat-icon>arrow_downward</mat-icon>
|
||||||
|
{{outer_temperature_diff | number : '1.1-2'}}° vs. gestern
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</mat-card>
|
||||||
|
|
||||||
|
<mat-card class="stat-card">
|
||||||
|
<div class="label">
|
||||||
|
<mat-icon>home</mat-icon>
|
||||||
|
Innen
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="value">{{inner_temperature | number: '1.1-2'}} °C</div>
|
||||||
|
|
||||||
|
@if (inner_temperature_diff) {
|
||||||
|
@if (inner_temperature_diff > 0) {
|
||||||
|
<div class="change negative">
|
||||||
|
<mat-icon>arrow_upward</mat-icon>
|
||||||
|
{{inner_temperature_diff | number : '1.1-2'}}° vs. gestern
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<div class="change positive">
|
||||||
|
<mat-icon>arrow_downward</mat-icon>
|
||||||
|
{{inner_temperature_diff | number : '1.1-2'}}° vs. gestern
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</mat-card>
|
||||||
|
|
||||||
|
<mat-card class="stat-card">
|
||||||
|
<div class="label">
|
||||||
|
<mat-icon>water_drop</mat-icon>
|
||||||
|
Luftfeucht.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="value">{{humidity | number: '1.1-2'}} %</div>
|
||||||
|
|
||||||
|
@if (humidity_diff) {
|
||||||
|
@if (humidity_diff > 0) {
|
||||||
|
<div class="change negative">
|
||||||
|
<mat-icon>arrow_upward</mat-icon>
|
||||||
|
{{humidity_diff | number : '1.1-2'}}% vs. gestern
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<div class="change positive">
|
||||||
|
<mat-icon>arrow_downward</mat-icon>
|
||||||
|
{{humidity_diff | number : '1.1-2'}}% vs. gestern
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</mat-card>
|
||||||
|
|
||||||
|
<mat-card class="stat-card">
|
||||||
|
<div class="label">
|
||||||
|
<mat-icon>speed</mat-icon>
|
||||||
|
Luftdruck
|
||||||
|
</div>
|
||||||
|
<div class="value">{{pressure}} hPa</div>
|
||||||
|
@if(pressure) {
|
||||||
|
<div class="change neutral">
|
||||||
|
<mat-icon>{{pressureTrendIcon}}</mat-icon>
|
||||||
|
<div class="forecast">{{weatherForecast}}</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
22
src/app/components/wettter-overview/wettter-overview.spec.ts
Normal file
22
src/app/components/wettter-overview/wettter-overview.spec.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { WettterOverview } from './wettter-overview';
|
||||||
|
|
||||||
|
describe('WettterOverview', () => {
|
||||||
|
let component: WettterOverview;
|
||||||
|
let fixture: ComponentFixture<WettterOverview>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [WettterOverview],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(WettterOverview);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
await fixture.whenStable();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
147
src/app/components/wettter-overview/wettter-overview.ts
Normal file
147
src/app/components/wettter-overview/wettter-overview.ts
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
import {ChangeDetectorRef, Component, OnInit} from '@angular/core';
|
||||||
|
import {DefaultService, MeasurementListResponse, MeasurementResolution, StationMeasurementResponse} from '../../api';
|
||||||
|
import {MatCard} from '@angular/material/card';
|
||||||
|
import {MatIcon} from '@angular/material/icon';
|
||||||
|
import {addDays, endOfDay, startOfDay} from 'date-fns';
|
||||||
|
import {DecimalPipe} from '@angular/common';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-wettter-overview',
|
||||||
|
imports: [
|
||||||
|
MatCard,
|
||||||
|
MatIcon,
|
||||||
|
DecimalPipe
|
||||||
|
],
|
||||||
|
templateUrl: './wettter-overview.html',
|
||||||
|
styleUrl: './wettter-overview.css',
|
||||||
|
})
|
||||||
|
export class WettterOverview implements OnInit {
|
||||||
|
|
||||||
|
last_measurement: MeasurementListResponse | undefined
|
||||||
|
out_temperature: number | undefined
|
||||||
|
inner_temperature: number | undefined
|
||||||
|
humidity: number | undefined
|
||||||
|
pressure: number | undefined
|
||||||
|
|
||||||
|
outer_temperature_diff: number | undefined
|
||||||
|
inner_temperature_diff: number | undefined
|
||||||
|
humidity_diff: number | undefined
|
||||||
|
pressureDiff: number = 0
|
||||||
|
|
||||||
|
change_class(yesterday_val: number | undefined, today_val: number | undefined) {
|
||||||
|
if (yesterday_val && today_val) {
|
||||||
|
return yesterday_val <= today_val ? 'negative' : 'positive'
|
||||||
|
}
|
||||||
|
return 'neutral'
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(private apiService: DefaultService, private cdr: ChangeDetectorRef) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
const today = startOfDay(new Date());
|
||||||
|
const yesterdayStart = addDays(today, -1);
|
||||||
|
const yesterdayEnd = endOfDay(yesterdayStart);
|
||||||
|
|
||||||
|
this.apiService.getMeasurementsMeasurementsGet(undefined, undefined, undefined, undefined, MeasurementResolution.Raw, 1).subscribe(measurements => {
|
||||||
|
const outdoor_stations = measurements.stations.filter(s => !s.indoor)
|
||||||
|
this.out_temperature = this.computeAvgTemperature(outdoor_stations, false)
|
||||||
|
this.inner_temperature = this.computeAvgTemperature(measurements.stations, true)
|
||||||
|
|
||||||
|
this.humidity = this.computeAvgHumidity(measurements.stations, true)
|
||||||
|
this.pressure = this.computeAvgPressure(measurements.stations, false)
|
||||||
|
|
||||||
|
this.apiService.getMeasurementsMeasurementsGet(undefined, undefined, yesterdayStart.toISOString(), yesterdayEnd.toISOString(), MeasurementResolution.Daily, 1).subscribe(measurements => {
|
||||||
|
const outdoor_stations = measurements.stations.filter(s => !s.indoor)
|
||||||
|
|
||||||
|
const yesterday_outer_temperature = this.computeAvgTemperature(outdoor_stations, false)
|
||||||
|
this.outer_temperature_diff = yesterday_outer_temperature - this.out_temperature!
|
||||||
|
|
||||||
|
const yesterday_inner_temperature = this.computeAvgTemperature(measurements.stations, true)
|
||||||
|
this.inner_temperature_diff = yesterday_inner_temperature - this.inner_temperature!
|
||||||
|
|
||||||
|
const yesterday_humidity = this.computeAvgHumidity(measurements.stations, true)
|
||||||
|
this.humidity_diff = yesterday_humidity - this.humidity!
|
||||||
|
|
||||||
|
this.cdr.detectChanges();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
this.apiService.listStationsStationsListGet().subscribe(stations => {
|
||||||
|
const outer_stations = stations.find(s => !s.indoor_only)
|
||||||
|
if (outer_stations) {
|
||||||
|
const now = new Date()
|
||||||
|
const threeHoursAgo = addDays(now, -3)
|
||||||
|
|
||||||
|
this.apiService.getMeasurementsMeasurementsGet([outer_stations.id], false, threeHoursAgo.toISOString(), now.toISOString(), MeasurementResolution.Hourly).subscribe(measurements => {
|
||||||
|
const station_measurements = measurements.stations[0].measurements
|
||||||
|
station_measurements.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime())
|
||||||
|
|
||||||
|
const recent = station_measurements[station_measurements.length - 1].pressure; // neueste
|
||||||
|
const older = station_measurements[0].pressure; // älteste
|
||||||
|
|
||||||
|
if (recent == null || older == null) {
|
||||||
|
this.pressureDiff = 0
|
||||||
|
} else {
|
||||||
|
this.pressureDiff = recent - older
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
get pressureTrendIcon(): string {
|
||||||
|
if (this.pressureDiff > 1.5) return 'trending_up';
|
||||||
|
if (this.pressureDiff < -1.5) return 'trending_down';
|
||||||
|
return 'trending_flat';
|
||||||
|
}
|
||||||
|
|
||||||
|
get pressureTrend(): string {
|
||||||
|
if (this.pressureDiff > 1.5) return `+${this.pressureDiff.toFixed(1)} hPa · steigend`;
|
||||||
|
if (this.pressureDiff < -1.5) return `${this.pressureDiff.toFixed(1)} hPa · fallend`;
|
||||||
|
return 'stabil';
|
||||||
|
}
|
||||||
|
|
||||||
|
get pressureTrendClass(): string {
|
||||||
|
if (this.pressureDiff > 1.5) return 'positive';
|
||||||
|
if (this.pressureDiff < -1.5) return 'negative';
|
||||||
|
return 'neutral';
|
||||||
|
}
|
||||||
|
|
||||||
|
get weatherForecast(): string {
|
||||||
|
return this.getWeatherForecast(this.pressure!, this.pressureDiff);
|
||||||
|
}
|
||||||
|
|
||||||
|
getWeatherForecast(current: number, diff: number): string {
|
||||||
|
const absDiff = Math.abs(diff);
|
||||||
|
const rising = diff > 0;
|
||||||
|
|
||||||
|
if (absDiff <= 1.5) {
|
||||||
|
return current > 1015 ? '☀️ Beständig schön' :
|
||||||
|
current < 1000 ? '🌧️ Beständig trüb' : '🌤️ Gleichbleibendes Wetter';
|
||||||
|
}
|
||||||
|
if (absDiff <= 3) {
|
||||||
|
return rising ? '🌤️ Wetterbesserung möglich' : '🌦️ Wetterverschlechterung möglich';
|
||||||
|
}
|
||||||
|
if (absDiff <= 6) {
|
||||||
|
return rising ? '☀️ Wetterbesserung wahrscheinlich' : '🌧️ Regen wahrscheinlich';
|
||||||
|
}
|
||||||
|
// > 6 hPa
|
||||||
|
return rising ? '☀️ Markante Wetterbesserung' : '⛈️ Sturm möglich';
|
||||||
|
}
|
||||||
|
|
||||||
|
computeAvgTemperature(stations: StationMeasurementResponse[], inner_measurements: boolean) {
|
||||||
|
const temperatures = stations.filter(s => s.indoor === inner_measurements).map(s => s.measurements[0].temperature);
|
||||||
|
return temperatures.reduce((sum, t) => sum + t, 0) / temperatures.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
computeAvgHumidity(stations: StationMeasurementResponse[], inner_measurements: boolean) {
|
||||||
|
const humidity = stations.filter(s => s.indoor === inner_measurements).map(s => s.measurements[0].humidity ? s.measurements[0].humidity : 0);
|
||||||
|
return humidity.reduce((sum, t) => sum + t, 0) / humidity.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
computeAvgPressure(stations: StationMeasurementResponse[], inner_measurements: boolean) {
|
||||||
|
const pressure = stations.filter(s=> s.indoor === inner_measurements).map(s => s.measurements[0].pressure ? s.measurements[0].pressure : 0)
|
||||||
|
return pressure.reduce((sum, t) => sum + t, 0) / pressure.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user