ADD: Records

This commit is contained in:
sebastian 2026-07-01 19:35:50 +02:00
parent f8e59e3649
commit b6c0c50968
16 changed files with 490 additions and 1 deletions

View File

@ -12,10 +12,13 @@ model/hTTPValidationError.ts
model/indoorMeasurementCreateRequest.ts
model/locationInner.ts
model/measurementListResponse.ts
model/measurementRecord.ts
model/measurementRecordItem.ts
model/measurementResolution.ts
model/measurementResponse.ts
model/models.ts
model/outdoorMeasurementCreateRequest.ts
model/recordResponse.ts
model/stationCreateRequest.ts
model/stationCreateResponse.ts
model/stationListResponse.ts

View File

@ -27,6 +27,8 @@ import { MeasurementResolution } from '../model/measurementResolution';
// @ts-ignore
import { OutdoorMeasurementCreateRequest } from '../model/outdoorMeasurementCreateRequest';
// @ts-ignore
import { RecordResponse } from '../model/recordResponse';
// @ts-ignore
import { StationCreateRequest } from '../model/stationCreateRequest';
// @ts-ignore
import { StationCreateResponse } from '../model/stationCreateResponse';
@ -422,6 +424,81 @@ export class DefaultService extends BaseService {
);
}
/**
* Get Records
* @endpoint get /measurements/records
* @param stationIds
* @param fromTimestamp
* @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
* @param reportProgress flag to report request and response progress.
* @param options additional options
*/
public getRecordsMeasurementsRecordsGet(stationIds?: Array<number>, fromTimestamp?: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable<RecordResponse>;
public getRecordsMeasurementsRecordsGet(stationIds?: Array<number>, fromTimestamp?: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable<HttpResponse<RecordResponse>>;
public getRecordsMeasurementsRecordsGet(stationIds?: Array<number>, fromTimestamp?: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable<HttpEvent<RecordResponse>>;
public getRecordsMeasurementsRecordsGet(stationIds?: Array<number>, fromTimestamp?: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable<any> {
let localVarQueryParameters = new OpenApiHttpParams(this.encoder);
localVarQueryParameters = this.addToHttpParams(
localVarQueryParameters,
'station_ids',
<any>stationIds,
QueryParamStyle.Form,
true,
);
localVarQueryParameters = this.addToHttpParams(
localVarQueryParameters,
'from_timestamp',
<any>fromTimestamp,
QueryParamStyle.Form,
true,
);
let localVarHeaders = this.defaultHeaders;
const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([
'application/json'
]);
if (localVarHttpHeaderAcceptSelected !== undefined) {
localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected);
}
const localVarHttpContext: HttpContext = options?.context ?? new HttpContext();
const localVarTransferCache: boolean = options?.transferCache ?? true;
let responseType_: 'text' | 'json' | 'blob' = 'json';
if (localVarHttpHeaderAcceptSelected) {
if (localVarHttpHeaderAcceptSelected.startsWith('text')) {
responseType_ = 'text';
} else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) {
responseType_ = 'json';
} else {
responseType_ = 'blob';
}
}
let localVarPath = `/measurements/records`;
const { basePath, withCredentials } = this.configuration;
return this.httpClient.request<RecordResponse>('get', `${basePath}${localVarPath}`,
{
context: localVarHttpContext,
params: localVarQueryParameters.toHttpParams(),
responseType: <any>responseType_,
...(withCredentials ? { withCredentials } : {}),
headers: localVarHeaders,
observe: observe,
...(localVarTransferCache !== undefined ? { transferCache: localVarTransferCache } : {}),
reportProgress: reportProgress
}
);
}
/**
* List Stations
* @endpoint get /stations/list

View File

@ -0,0 +1,17 @@
/**
* FastAPI
*
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import { MeasurementRecordItem } from './measurementRecordItem';
export interface MeasurementRecord {
min_value: MeasurementRecordItem;
max_value: MeasurementRecordItem;
}

View File

@ -0,0 +1,18 @@
/**
* FastAPI
*
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import { StationListResponse } from './stationListResponse';
export interface MeasurementRecordItem {
recordValue: number;
timestamp: string;
station: StationListResponse;
}

View File

@ -2,9 +2,12 @@ export * from './hTTPValidationError';
export * from './indoorMeasurementCreateRequest';
export * from './locationInner';
export * from './measurementListResponse';
export * from './measurementRecord';
export * from './measurementRecordItem';
export * from './measurementResolution';
export * from './measurementResponse';
export * from './outdoorMeasurementCreateRequest';
export * from './recordResponse';
export * from './stationCreateRequest';
export * from './stationCreateResponse';
export * from './stationListResponse';

View File

@ -0,0 +1,21 @@
/**
* FastAPI
*
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import { MeasurementRecord } from './measurementRecord';
export interface RecordResponse {
inner_temperature_record: MeasurementRecord;
outer_temperature_record: MeasurementRecord;
inner_humidity_record: MeasurementRecord;
outer_humidity_record: MeasurementRecord;
pressure_record: MeasurementRecord;
night_temperature_record: MeasurementRecord;
}

View File

@ -28,6 +28,7 @@
<mat-drawer-content>
<app-wettter-overview></app-wettter-overview>
<app-diagrams #diagrams></app-diagrams>
<app-records></app-records>
</mat-drawer-content>
</mat-drawer-container>

View File

@ -8,10 +8,11 @@ import {StationList} from './station-list/station-list';
import {WettterOverview} from './components/wettter-overview/wettter-overview';
import {TemperatureComponent, TimeResolution} from './components/diagrams/temperature/temperature';
import {Diagrams} from './components/diagrams/diagrams';
import {Records} from './components/records/records';
@Component({
selector: 'app-root',
imports: [RouterOutlet, MatToolbar, MatIcon, MatIconButton, RouterLink, MatButton, MatDrawerContainer, MatDrawer, MatDrawerContent, StationList, WettterOverview, TemperatureComponent, Diagrams],
imports: [RouterOutlet, MatToolbar, MatIcon, MatIconButton, RouterLink, MatButton, MatDrawerContainer, MatDrawer, MatDrawerContent, StationList, WettterOverview, Diagrams, Records],
templateUrl: './app.html',
styleUrl: './app.css'
})

View File

@ -0,0 +1,72 @@
mat-card {
background: #1e1e1e !important;
color: #fff;
border-radius: 14px !important;
padding: 0 !important;
box-shadow:
0 4px 10px rgba(0, 0, 0, .35),
inset 0 1px 0 rgba(255,255,255,.04) !important;
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) !important;
}
}
mat-card-content {
padding: 18px !important;
display: flex;
flex-direction: column;
gap: 4px;
.record-icon {
font-size: 28px;
width: 28px;
height: 28px;
margin-bottom: 4px;
}
.record-value {
font-size: 1.8rem;
font-weight: 500;
line-height: 1.1;
margin: 4px 0;
}
.record-date {
font-size: 0.78rem;
opacity: 0.6;
}
.record-station {
display: flex;
align-items: center;
gap: 6px;
margin-top: 6px;
font-size: 0.82rem;
.dot {
width: 7px;
height: 7px;
border-radius: 50%;
flex-shrink: 0;
&.orange { background: #eda100; }
&.blue { background: #2a78d6; }
&.purple { background: #4a3aa7; }
}
}
}
.record-label {
opacity: 0.6;
}
.max-record { color: #e34948; }
.min-record { color: #2a78d6; }
.green-record { color: #25a532; }
.night-record { color: #7300ed; }
.white-record { color: #ffffff; }

View File

@ -0,0 +1,22 @@
<mat-card>
<mat-card-header>
<mat-card-title>
@if(measurementType() === 'temperature') {
@if(recordType() === 'max') {
<mat-icon [ngClass]="recordIconClass" fontSet="material-symbols-outlined">thermometer</mat-icon>
} @else {
<mat-icon [ngClass]="recordIconClass" fontSet="material-symbols-outlined">snowflake</mat-icon>
}
}
<span class="record-label">{{recordLabel}}</span>
</mat-card-title>
</mat-card-header>
<mat-card-content>
<p [ngClass]="recordValueClass">{{ recordValue }}{{recordUnit}}</p>
<p class="record-date">{{ recordTimeStamp | date: 'dd.MM.yyyy HH:mm' }}</p>
<div class="record-station">
<span class="dot orange"></span>
<span style="color: orange">{{ record().max_value.station.name }}</span>
</div>
</mat-card-content>
</mat-card>

View File

@ -0,0 +1,22 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RecordCard } from './record-card';
describe('RecordCard', () => {
let component: RecordCard;
let fixture: ComponentFixture<RecordCard>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [RecordCard],
}).compileComponents();
fixture = TestBed.createComponent(RecordCard);
component = fixture.componentInstance;
await fixture.whenStable();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,113 @@
import {ChangeDetectorRef, Component, input, Input, OnInit} from '@angular/core';
import {MeasurementRecord, RecordResponse} from '../../../api';
import {MatIcon} from '@angular/material/icon';
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card';
import {DatePipe, NgClass} from '@angular/common';
@Component({
selector: 'app-record-card',
imports: [
MatIcon,
MatCard,
MatCardHeader,
MatCardTitle,
MatCardContent,
DatePipe,
NgClass
],
templateUrl: './record-card.html',
styleUrl: './record-card.css',
})
export class RecordCard {
recordType = input.required<'min' | 'max'>();
record = input.required<MeasurementRecord>();
measurementType = input.required<'temperature' | 'humidity' | 'pressure'>();
variant = input.required<'red-blue' | 'green' | 'white' | 'night'>()
predifenedRecordLabel = input<string | null>()
get recordIconClass() {
if(this.variant() === 'red-blue') {
if(this.recordType() === 'min') {
return 'record-icon min-record'
} else {
return 'record-icon max-record';
}
} else if(this.variant() === 'green') {
return 'record-icon green-record';
} else if(this.variant() === 'white') {
return 'record-icon white-record';
} else if(this.variant() === 'night') {
return 'record-icon night-record';
}
return ""
}
get recordValueClass() {
if(this.variant() === 'red-blue') {
if(this.recordType() === 'min') {
return 'record-value min-record'
} else {
return 'record-value max-record';
}
} else if(this.variant() === 'green') {
return 'record-value green-record';
} else if(this.variant() === 'white') {
return 'record-value white-record';
} else if(this.variant() === 'night') {
return 'record-value night-record';
}
return ""
}
get recordLabel() {
if(this.predifenedRecordLabel()) {
return this.predifenedRecordLabel();
}
let recordType = ''
if(this.recordType() === 'max') {
recordType = 'Höchste'
} else {
recordType = 'Niedrigste'
}
let measurementType = ''
if(this.measurementType() === 'humidity') {
measurementType = 'Luftfeuchtigkeit'
} else if(this.measurementType() === 'pressure') {
measurementType = 'Luftdruck'
} else {
measurementType = 'Temperatur'
}
return recordType + ' ' + measurementType
}
get recordUnit() {
let unit = ''
if(this.measurementType() === 'humidity') {
return '%'
} else if(this.measurementType() === 'pressure') {
return 'hPa'
} else {
return '°C'
}
}
get recordValue() {
if(this.recordType() === 'min') {
return this.record().min_value.recordValue
} else {
return this.record().max_value.recordValue
}
}
get recordTimeStamp() {
if(this.recordType() === 'min') {
return this.record().min_value.timestamp
} else {
return this.record().max_value.timestamp
}
}
}

View File

@ -0,0 +1,6 @@
.records-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin: 2em;
}

View File

@ -0,0 +1,18 @@
<div class="records-grid">
@if(unifiedTemperatureRecord) {
<app-record-card [measurementType]="'temperature'" [record]="unifiedTemperatureRecord!" [recordType]="'max'"
[variant]="'red-blue'"></app-record-card>
<app-record-card [measurementType]="'temperature'" [record]="unifiedTemperatureRecord!" [recordType]="'min'"
[variant]="'red-blue'"></app-record-card>
}
@if(records) {
<app-record-card [measurementType]="'humidity'" [record]="records.inner_humidity_record" [recordType]="'max'"
[variant]="'green'"></app-record-card>
<app-record-card [measurementType]="'temperature'" [record]="records.night_temperature_record" [recordType]="'max'"
[variant]="'night'" [predifenedRecordLabel]="'Wärmste Nacht (innen)'"></app-record-card>
<app-record-card [measurementType]="'pressure'" [record]="records.pressure_record" [recordType]="'max'"
[variant]="'white'"></app-record-card>
<app-record-card [measurementType]="'pressure'" [record]="records.pressure_record" [recordType]="'min'"
[variant]="'white'"></app-record-card>
}
</div>

View File

@ -0,0 +1,22 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Records } from './records';
describe('Records', () => {
let component: Records;
let fixture: ComponentFixture<Records>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [Records],
}).compileComponents();
fixture = TestBed.createComponent(Records);
component = fixture.componentInstance;
await fixture.whenStable();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,73 @@
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit} from '@angular/core';
import {DefaultService, MeasurementRecord, MeasurementRecordItem, RecordResponse} from '../../api';
import {addYears, startOfMonth, startOfWeek, startOfYear} from 'date-fns';
import {RecordCard} from './record-card/record-card';
export type RecordTimeInterval = 'All' | 'Year' | 'Month' | 'Week'
@Component({
selector: 'app-records',
imports: [
RecordCard
],
templateUrl: './records.html',
styleUrl: './records.css',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class Records implements OnInit {
timeInterval: RecordTimeInterval = 'All';
station_ids: number[] = [];
records: RecordResponse | null = null;
unifiedTemperatureRecord: MeasurementRecord | null = null;
constructor(private apiService: DefaultService, private cdr: ChangeDetectorRef) {
}
ngOnInit() {
let from_timestamp : undefined | string = undefined
if (this.timeInterval === 'All') {
from_timestamp = undefined
} else if(this.timeInterval === 'Year') {
const currentDate = new Date();
from_timestamp = startOfYear(currentDate).toISOString()
} else if(this.timeInterval === 'Month') {
const currentDate = new Date();
from_timestamp = startOfMonth(currentDate).toISOString()
} else {
const currentDate = new Date();
from_timestamp = startOfWeek(currentDate, { weekStartsOn: 1}).toISOString()
}
this.apiService.getRecordsMeasurementsRecordsGet(undefined, from_timestamp).subscribe(
{
next: records => {
this.records = records;
this.unifiedTemperatureRecord = {
min_value: this.getLowestMeasurement(records.inner_temperature_record, records.outer_temperature_record).min_value,
max_value: this.getHighestMeasurement(records.inner_temperature_record, records.outer_temperature_record).max_value,
}
console.log(this.unifiedTemperatureRecord)
this.cdr.detectChanges()
}
}
)
}
getHighestMeasurement(a: MeasurementRecord, b: MeasurementRecord) {
if(a.max_value > b.max_value) {
return a
} else {
return b
}
}
getLowestMeasurement(a: MeasurementRecord, b: MeasurementRecord) {
if(a.min_value > b.min_value) {
return a
} else {
return b
}
}
}