Weather
This commit is contained in:
parent
2882184a82
commit
906be87e50
@ -4,7 +4,7 @@ import {map, Subscription} from 'rxjs';
|
|||||||
import {StompService} from '@stomp/ng2-stompjs';
|
import {StompService} from '@stomp/ng2-stompjs';
|
||||||
import {FromJson, Next} from './types';
|
import {FromJson, Next} from './types';
|
||||||
|
|
||||||
const DEV_TO_PROD = true;
|
const DEV_TO_PROD = false;
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import {FromJson} from './types';
|
||||||
|
|
||||||
export function validateString(value: any): string {
|
export function validateString(value: any): string {
|
||||||
if (typeof value !== 'string') {
|
if (typeof value !== 'string') {
|
||||||
throw new Error('Not a string: ' + value);
|
throw new Error('Not a string: ' + value);
|
||||||
@ -11,3 +13,11 @@ export function validateNumber(value: any): number {
|
|||||||
}
|
}
|
||||||
return value as number;
|
return value as number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function validateDate(value: any): Date {
|
||||||
|
return new Date(validateString(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateList<T>(value: any[], fromJson: FromJson<T>): T[] {
|
||||||
|
return value.map(fromJson);
|
||||||
|
}
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
<app-weather-diagram></app-weather-diagram>
|
||||||
|
|
||||||
<app-electro-power></app-electro-power>
|
<app-electro-power></app-electro-power>
|
||||||
|
|
||||||
<app-electro-energy></app-electro-energy>
|
<app-electro-energy></app-electro-energy>
|
||||||
|
|||||||
@ -1,12 +1,14 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import {ElectroEnergyComponent} from "../electro/energy/electro-energy.component";
|
import {ElectroEnergyComponent} from "../electro/energy/electro-energy.component";
|
||||||
import {ElectroPowerComponent} from "../electro/power/electro-power.component";
|
import {ElectroPowerComponent} from "../electro/power/electro-power.component";
|
||||||
|
import {WeatherDiagramComponent} from '../weather/weather-diagram/weather-diagram.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-dashboard',
|
selector: 'app-dashboard',
|
||||||
imports: [
|
imports: [
|
||||||
ElectroEnergyComponent,
|
ElectroEnergyComponent,
|
||||||
ElectroPowerComponent
|
ElectroPowerComponent,
|
||||||
|
WeatherDiagramComponent
|
||||||
],
|
],
|
||||||
templateUrl: './dashboard.component.html',
|
templateUrl: './dashboard.component.html',
|
||||||
styleUrl: './dashboard.component.less'
|
styleUrl: './dashboard.component.less'
|
||||||
|
|||||||
@ -1,17 +1,17 @@
|
|||||||
<div class="bar">
|
<div class="bar">
|
||||||
<div class="part purchase" *ngIf="barPurchasePercent" [style.width]="barPurchasePercent.value + '%'">
|
<div class="part purchase" *ngIf="barPurchasePercent" [style.width]="barPurchasePercent.formatted2">
|
||||||
<div class="text">
|
<div class="text">
|
||||||
{{ _purchase?.formatted }}<br>
|
{{ _purchase?.formatted }}<br>
|
||||||
{{ purchasePercent?.formatted }}
|
{{ purchasePercent?.formatted }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="part self" *ngIf="barSelfPercent" [style.width]="barSelfPercent.value + '%'">
|
<div class="part self" *ngIf="barSelfPercent" [style.width]="barSelfPercent.formatted2">
|
||||||
<div class="text">
|
<div class="text">
|
||||||
{{ _self?.formatted }}<br>
|
{{ _self?.formatted }}<br>
|
||||||
{{ selfPercent?.formatted }}
|
{{ selfPercent?.formatted }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="part delivery" *ngIf="barDeliveryPercent" [style.width]="barDeliveryPercent.value + '%'">
|
<div class="part delivery" *ngIf="barDeliveryPercent" [style.width]="barDeliveryPercent.formatted2">
|
||||||
<div class="text">
|
<div class="text">
|
||||||
{{ _delivery?.formatted }}<br>
|
{{ _delivery?.formatted }}<br>
|
||||||
{{ deliveryPercent?.formatted }}
|
{{ deliveryPercent?.formatted }}
|
||||||
|
|||||||
@ -12,6 +12,14 @@ export class Unit {
|
|||||||
|
|
||||||
static readonly PERCENT = new Unit('PERCENT', "%");
|
static readonly PERCENT = new Unit('PERCENT', "%");
|
||||||
|
|
||||||
|
static readonly CLOUD_COVER_PERCENT = new Unit('CLOUD_COVER_PERCENT', '%');
|
||||||
|
|
||||||
|
static readonly IRRADIATION_WH_M2 = new Unit('IRRADIATION_WH_M2', 'Wh/m²');
|
||||||
|
|
||||||
|
static readonly IRRADIATION_KWH_M2 = new Unit('IRRADIATION_KWH_M2', 'kWh/m²');
|
||||||
|
|
||||||
|
static readonly PRECIPITATION_MM = new Unit('PRECIPITATION_MM', 'mm');
|
||||||
|
|
||||||
private constructor(
|
private constructor(
|
||||||
readonly name: string,
|
readonly name: string,
|
||||||
readonly unit: string,
|
readonly unit: string,
|
||||||
|
|||||||
@ -11,8 +11,22 @@ export class Value {
|
|||||||
//
|
//
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static fromJson2(json: any, locale: string): Value {
|
||||||
|
return new Value(
|
||||||
|
validateNumber(json['value']),
|
||||||
|
Unit.fromJson(json['unit']),
|
||||||
|
validateNumber(json['decimals']),
|
||||||
|
locale
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
static fromJson(value: any, unit: Unit, decimals: number, locale: string): Value {
|
static fromJson(value: any, unit: Unit, decimals: number, locale: string): Value {
|
||||||
return new Value(validateNumber(value), unit, decimals, locale);
|
return new Value(
|
||||||
|
validateNumber(value),
|
||||||
|
unit,
|
||||||
|
decimals,
|
||||||
|
locale
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get zero(): boolean {
|
get zero(): boolean {
|
||||||
@ -20,7 +34,15 @@ export class Value {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get formatted(): string {
|
get formatted(): string {
|
||||||
return `${this.value.toLocaleString(this.locale, {maximumFractionDigits: this.decimals, minimumFractionDigits: this.decimals})} ${this.unit.unit}`;
|
return `${(this.localeString)} ${this.unit.unit}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
get formatted2(): string {
|
||||||
|
return `${(this.localeString)}${this.unit.unit}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
get localeString(): string {
|
||||||
|
return this.value.toLocaleString(this.locale, {maximumFractionDigits: this.decimals, minimumFractionDigits: this.decimals});
|
||||||
}
|
}
|
||||||
|
|
||||||
negate() {
|
negate() {
|
||||||
@ -55,11 +77,12 @@ export class Value {
|
|||||||
return new Value(0, this.unit, this.decimals, this.locale);
|
return new Value(0, this.unit, this.decimals, this.locale);
|
||||||
}
|
}
|
||||||
|
|
||||||
percent(other: Value | undefined): Value | undefined {
|
percent(other: Value | number | undefined): Value | undefined {
|
||||||
if (!other || other.value === 0) {
|
const v = other instanceof Value ? other.value : typeof other === "number" ? other : 0;
|
||||||
|
if (v === 0) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
return new Value(this.value / other.value * 100, Unit.PERCENT, 0, this.locale);
|
return new Value(this.value / v * 100, Unit.PERCENT, 0, this.locale);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,27 @@
|
|||||||
|
import {Value} from "../../value/Value";
|
||||||
|
import {validateDate, validateList} from "../../core/validators";
|
||||||
|
import {WeatherHour} from "./WeatherHour";
|
||||||
|
|
||||||
|
export class WeatherDay {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly date: Date,
|
||||||
|
readonly hours: WeatherHour[],
|
||||||
|
readonly clouds: Value,
|
||||||
|
readonly irradiation: Value,
|
||||||
|
readonly precipitation: Value,
|
||||||
|
) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromJson(json: any, locale: string): WeatherDay {
|
||||||
|
return new WeatherDay(
|
||||||
|
validateDate(json['date']),
|
||||||
|
validateList(json['hours'], hour => WeatherHour.fromJson(hour, locale)),
|
||||||
|
Value.fromJson2(json['clouds'], locale),
|
||||||
|
Value.fromJson2(json['irradiation'], locale),
|
||||||
|
Value.fromJson2(json['precipitation'], locale),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
import {Value} from '../../value/Value';
|
||||||
|
import {validateDate} from '../../core/validators';
|
||||||
|
|
||||||
|
export class WeatherHour {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly date: Date,
|
||||||
|
readonly clouds: Value,
|
||||||
|
readonly irradiation: Value,
|
||||||
|
readonly precipitation: Value,
|
||||||
|
) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromJson(json: any, locale: string): WeatherHour {
|
||||||
|
return new WeatherHour(
|
||||||
|
validateDate(json['date']),
|
||||||
|
Value.fromJson2(json['clouds'], locale),
|
||||||
|
Value.fromJson2(json['irradiation'], locale),
|
||||||
|
Value.fromJson2(json['precipitation'], locale),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
<div class="numberTable">
|
||||||
|
<div class="title">
|
||||||
|
Wetter
|
||||||
|
</div>
|
||||||
|
<div class="day">
|
||||||
|
<div class="hour" *ngFor="let hour of hours">
|
||||||
|
<div class="bar clouds" [style.height]="clouds(hour)"></div>
|
||||||
|
<div class="bar precipitation" [style.height]="precipitation(hour)"></div>
|
||||||
|
<div class="bar irradiation" [style.height]="irradiation(hour)"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
.day {
|
||||||
|
display: flex;
|
||||||
|
height: 3em;
|
||||||
|
background-color: #2d4255;
|
||||||
|
|
||||||
|
.hour {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
width: 100%; // any width will do (flex cares about real with)
|
||||||
|
|
||||||
|
.bar {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clouds {
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.irradiation {
|
||||||
|
background-color: yellow;
|
||||||
|
}
|
||||||
|
|
||||||
|
.precipitation {
|
||||||
|
background-color: blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,77 @@
|
|||||||
|
import {Component, OnInit} from '@angular/core';
|
||||||
|
import {NgForOf} from '@angular/common';
|
||||||
|
import {WeatherHour} from './WeatherHour';
|
||||||
|
import {WeatherService} from './weather.service';
|
||||||
|
import {WeatherDay} from './WeatherDay';
|
||||||
|
|
||||||
|
const PAST_HOURS_COUNT = 0;
|
||||||
|
|
||||||
|
const DAY_COUNT = 7;
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-weather-diagram',
|
||||||
|
imports: [
|
||||||
|
NgForOf
|
||||||
|
],
|
||||||
|
templateUrl: './weather-diagram.component.html',
|
||||||
|
styleUrl: './weather-diagram.component.less'
|
||||||
|
})
|
||||||
|
export class WeatherDiagramComponent implements OnInit {
|
||||||
|
|
||||||
|
protected days: WeatherDay[] = [];
|
||||||
|
|
||||||
|
protected hours: WeatherHour[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly weatherService: WeatherService,
|
||||||
|
) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.weatherService.all(all => {
|
||||||
|
this.days = all;
|
||||||
|
this.updateHours();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
clouds(hour: WeatherHour): string {
|
||||||
|
return (hour.clouds?.value || 0) + '%';
|
||||||
|
}
|
||||||
|
|
||||||
|
irradiation(hour: WeatherHour): string {
|
||||||
|
return (hour.irradiation.percent(1000)?.value || 0) + '%';
|
||||||
|
}
|
||||||
|
|
||||||
|
precipitation(hour: WeatherHour) {
|
||||||
|
return (hour.precipitation.percent(15)?.value || 0) + '%';
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateHours() {
|
||||||
|
const nowHour = new Date();
|
||||||
|
nowHour.setMinutes(0);
|
||||||
|
nowHour.setSeconds(0);
|
||||||
|
nowHour.setMilliseconds(0);
|
||||||
|
|
||||||
|
const firstHour = new Date(nowHour);
|
||||||
|
firstHour.setHours(firstHour.getHours() - PAST_HOURS_COUNT);
|
||||||
|
|
||||||
|
const endHour = new Date(firstHour);
|
||||||
|
endHour.setHours(endHour.getHours() + 24 * DAY_COUNT);
|
||||||
|
|
||||||
|
this.hours = [];
|
||||||
|
const currentHour = new Date(firstHour);
|
||||||
|
for (const day of this.days) {
|
||||||
|
for (const hour of day.hours) {
|
||||||
|
if (hour.date.getTime() === currentHour.getTime()) {
|
||||||
|
this.hours.push(hour);
|
||||||
|
currentHour.setHours(currentHour.getHours() + 1);
|
||||||
|
if (currentHour.getTime() >= endHour.getTime()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
import {Inject, Injectable, LOCALE_ID} from '@angular/core';
|
||||||
|
import {ApiService} from '../../core/api.service';
|
||||||
|
import {Next} from '../../core/types';
|
||||||
|
import {WeatherDay} from './WeatherDay';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class WeatherService {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly api: ApiService,
|
||||||
|
@Inject(LOCALE_ID) readonly locale: string,
|
||||||
|
) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
all(next: Next<WeatherDay[]>) {
|
||||||
|
return this.api.getList(['Weather', 'all'], json => WeatherDay.fromJson(json, this.locale), next);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -38,6 +38,13 @@ public enum Unit {
|
|||||||
SUN_DC("Δ°C"),
|
SUN_DC("Δ°C"),
|
||||||
|
|
||||||
UNIT_PERCENT("%"),
|
UNIT_PERCENT("%"),
|
||||||
|
|
||||||
|
CLOUD_COVER_PERCENT("%"),
|
||||||
|
|
||||||
|
IRRADIATION_WH_M2("Wh/m²"),
|
||||||
|
IRRADIATION_KWH_M2("kWh/m²", 1000, IRRADIATION_WH_M2),
|
||||||
|
|
||||||
|
PRECIPITATION_MM("mm"),
|
||||||
;
|
;
|
||||||
|
|
||||||
public final String unit;
|
public final String unit;
|
||||||
|
|||||||
@ -2,17 +2,31 @@ package de.ph87.data.value;
|
|||||||
|
|
||||||
import lombok.*;
|
import lombok.*;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.function.*;
|
||||||
|
|
||||||
|
@Data
|
||||||
public class Value {
|
public class Value {
|
||||||
|
|
||||||
public final double value;
|
public final double value;
|
||||||
|
|
||||||
public final Unit unit;
|
public final Unit unit;
|
||||||
|
|
||||||
|
public final int decimals;
|
||||||
|
|
||||||
public Value(final double value, @NonNull final Unit unit) {
|
public Value(final double value, @NonNull final Unit unit) {
|
||||||
this.value = value;
|
this.value = value;
|
||||||
this.unit = unit;
|
this.unit = unit;
|
||||||
|
this.decimals = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Value(final double value, @NonNull final Unit unit, final int decimals) {
|
||||||
|
this.value = value;
|
||||||
|
this.unit = unit;
|
||||||
|
this.decimals = decimals;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
public Value as(@NonNull final Unit target) {
|
public Value as(@NonNull final Unit target) {
|
||||||
if (this.unit == target) {
|
if (this.unit == target) {
|
||||||
return this;
|
return this;
|
||||||
@ -23,4 +37,21 @@ public class Value {
|
|||||||
return new Value(value * this.unit.factor / target.factor, target);
|
return new Value(value * this.unit.factor / target.factor, target);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static <T> Value sum(@NonNull final List<T> hours, @NonNull final Function<T, Value> map, @NonNull final Unit unit, final int decimals) {
|
||||||
|
final double sum = hours.stream().map(map).map(v -> v.as(unit)).map(Value::getValue).reduce(Double::sum).orElse(0.0);
|
||||||
|
return new Value(sum, unit, decimals);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static <T> Value avg(@NonNull final List<T> hours, @NonNull final Function<T, Value> map, @NonNull final Unit unit, final int decimals) {
|
||||||
|
final double avg = sum(hours, map, unit, decimals).as(unit).value / hours.size();
|
||||||
|
return new Value(avg, unit, decimals);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "%%.%df%%s".formatted(decimals).formatted(value, unit.unit);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
65
src/main/java/de/ph87/data/weather/BrightSkyDto.java
Normal file
65
src/main/java/de/ph87/data/weather/BrightSkyDto.java
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
package de.ph87.data.weather;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.*;
|
||||||
|
import lombok.*;
|
||||||
|
|
||||||
|
import java.time.*;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public class BrightSkyDto {
|
||||||
|
|
||||||
|
private List<Weather> weather;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@ToString
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public static class Weather {
|
||||||
|
|
||||||
|
private ZonedDateTime timestamp;
|
||||||
|
|
||||||
|
private int source_id;
|
||||||
|
|
||||||
|
private Double precipitation;
|
||||||
|
|
||||||
|
private Double pressure_msl;
|
||||||
|
|
||||||
|
private Double sunshine;
|
||||||
|
|
||||||
|
private Double temperature;
|
||||||
|
|
||||||
|
private Double wind_direction;
|
||||||
|
|
||||||
|
private Double wind_speed;
|
||||||
|
|
||||||
|
private Integer cloud_cover;
|
||||||
|
|
||||||
|
private Double dew_point;
|
||||||
|
|
||||||
|
private Double relative_humidity;
|
||||||
|
|
||||||
|
private Double visibility;
|
||||||
|
|
||||||
|
private Double wind_gust_direction;
|
||||||
|
|
||||||
|
private Double wind_gust_speed;
|
||||||
|
|
||||||
|
private String condition;
|
||||||
|
|
||||||
|
private Double precipitation_probability;
|
||||||
|
|
||||||
|
private Double precipitation_probability_6h;
|
||||||
|
|
||||||
|
private Double solar;
|
||||||
|
|
||||||
|
private String icon;
|
||||||
|
|
||||||
|
public void setTimestamp(@NonNull final ZonedDateTime timestamp) {
|
||||||
|
this.timestamp = timestamp.withZoneSameInstant(TimeZone.getDefault().toZoneId());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
22
src/main/java/de/ph87/data/weather/WeatherConfig.java
Normal file
22
src/main/java/de/ph87/data/weather/WeatherConfig.java
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package de.ph87.data.weather;
|
||||||
|
|
||||||
|
import lombok.*;
|
||||||
|
import org.springframework.boot.context.properties.*;
|
||||||
|
import org.springframework.stereotype.*;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Component
|
||||||
|
@ConfigurationProperties(prefix = "de.ph87.data.weather")
|
||||||
|
public class WeatherConfig {
|
||||||
|
|
||||||
|
private String urlPattern = "https://api.brightsky.dev/weather?date={date}&lat={latitude}&lon={longitude}&units=dwd";
|
||||||
|
|
||||||
|
private double latitude = 49.320789191091194;
|
||||||
|
|
||||||
|
private double longitude = 7.102111982262271;
|
||||||
|
|
||||||
|
private int pastDays = 9;
|
||||||
|
|
||||||
|
private int futureDays = 9;
|
||||||
|
|
||||||
|
}
|
||||||
25
src/main/java/de/ph87/data/weather/WeatherController.java
Normal file
25
src/main/java/de/ph87/data/weather/WeatherController.java
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package de.ph87.data.weather;
|
||||||
|
|
||||||
|
import lombok.*;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@RequestMapping("Weather")
|
||||||
|
public class WeatherController {
|
||||||
|
|
||||||
|
private final WeatherService weatherService;
|
||||||
|
|
||||||
|
@GetMapping("update")
|
||||||
|
public void update() {
|
||||||
|
weatherService.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("all")
|
||||||
|
public List<WeatherDay> all() {
|
||||||
|
return weatherService.all();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
79
src/main/java/de/ph87/data/weather/WeatherDay.java
Normal file
79
src/main/java/de/ph87/data/weather/WeatherDay.java
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
package de.ph87.data.weather;
|
||||||
|
|
||||||
|
import de.ph87.data.value.Value;
|
||||||
|
import de.ph87.data.value.*;
|
||||||
|
import lombok.*;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.time.*;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.*;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@ToString(includeFieldNames = false)
|
||||||
|
public class WeatherDay {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final LocalDate date;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@ToString.Exclude
|
||||||
|
public final List<Hour> hours;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final Value clouds;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final Value irradiation;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final Value precipitation;
|
||||||
|
|
||||||
|
public WeatherDay(@NonNull final BrightSkyDto dto) throws IOException {
|
||||||
|
if (dto.getWeather().size() != 25) {
|
||||||
|
throw new IOException("Expected 25 hours. But received: %d:\n %s".formatted(dto.getWeather().size(), dto.getWeather().stream().map(BrightSkyDto.Weather::toString).collect(Collectors.joining("\n "))));
|
||||||
|
}
|
||||||
|
date = dto.getWeather().getFirst().getTimestamp().toLocalDate();
|
||||||
|
hours = dto.getWeather().stream().map(Hour::new).filter(h -> h.date.toLocalDate().equals(date)).toList();
|
||||||
|
clouds = Value.avg(hours, Hour::getClouds, Unit.CLOUD_COVER_PERCENT, 0);
|
||||||
|
irradiation = Value.sum(hours, Hour::getIrradiation, Unit.IRRADIATION_KWH_M2, 1);
|
||||||
|
precipitation = Value.sum(hours, Hour::getPrecipitation, Unit.PRECIPITATION_MM, 0);
|
||||||
|
validate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validate() throws IOException {
|
||||||
|
ZonedDateTime date = ZonedDateTime.of(this.date, LocalTime.MIDNIGHT, ZoneId.systemDefault());
|
||||||
|
for (final Hour hour : hours) {
|
||||||
|
if (hour.date.compareTo(date) != 0) {
|
||||||
|
throw new IOException("Invalid Hour-Date: expected=%s, actual=%s".formatted(date, hour.date));
|
||||||
|
}
|
||||||
|
date = date.plusHours(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@ToString(includeFieldNames = false)
|
||||||
|
public static class Hour {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final ZonedDateTime date;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final Value clouds;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final Value irradiation;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final Value precipitation;
|
||||||
|
|
||||||
|
public Hour(@NonNull final BrightSkyDto.Weather dto) {
|
||||||
|
date = dto.getTimestamp();
|
||||||
|
clouds = new Value(dto.getCloud_cover(), Unit.CLOUD_COVER_PERCENT);
|
||||||
|
irradiation = new Value(dto.getSolar() * 1000, Unit.IRRADIATION_WH_M2);
|
||||||
|
precipitation = new Value(dto.getPrecipitation(), Unit.PRECIPITATION_MM);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
76
src/main/java/de/ph87/data/weather/WeatherService.java
Normal file
76
src/main/java/de/ph87/data/weather/WeatherService.java
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
package de.ph87.data.weather;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.*;
|
||||||
|
import lombok.*;
|
||||||
|
import lombok.extern.slf4j.*;
|
||||||
|
import org.springframework.boot.context.event.*;
|
||||||
|
import org.springframework.context.event.EventListener;
|
||||||
|
import org.springframework.scheduling.annotation.*;
|
||||||
|
import org.springframework.stereotype.*;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.net.*;
|
||||||
|
import java.nio.charset.*;
|
||||||
|
import java.time.*;
|
||||||
|
import java.time.format.*;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@EnableScheduling
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class WeatherService {
|
||||||
|
|
||||||
|
private List<WeatherDay> days = new ArrayList<>();
|
||||||
|
|
||||||
|
private final WeatherConfig weatherConfig;
|
||||||
|
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public List<WeatherDay> all() {
|
||||||
|
return new ArrayList<>(days);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Scheduled(cron = "0 0 * * * *")
|
||||||
|
@EventListener(ApplicationStartedEvent.class)
|
||||||
|
public void update() {
|
||||||
|
try {
|
||||||
|
final LocalDate today = LocalDate.now();
|
||||||
|
final LocalDate first = today.minusDays(weatherConfig.getPastDays());
|
||||||
|
final LocalDate end = today.plusDays(weatherConfig.getFutureDays());
|
||||||
|
final List<WeatherDay> newDays = new ArrayList<>();
|
||||||
|
log.debug("Updating Weather...");
|
||||||
|
for (LocalDate day = first; !day.isAfter(end); day = day.plusDays(1)) {
|
||||||
|
final WeatherDay weatherDay = new WeatherDay(fetchDay(day));
|
||||||
|
newDays.add(weatherDay);
|
||||||
|
if (log.isDebugEnabled()) {
|
||||||
|
log.debug(" {}:", weatherDay);
|
||||||
|
for (WeatherDay.Hour hour : weatherDay.getHours()) {
|
||||||
|
log.debug(" %s: %4s clouds, %9s, %4s".formatted(hour.date.toLocalTime(), hour.getClouds(), hour.getIrradiation(), hour.getPrecipitation()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
days = newDays;
|
||||||
|
log.info("Weather update complete");
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error(e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public BrightSkyDto fetchDay(@NonNull final LocalDate day) throws IOException {
|
||||||
|
final String url = weatherConfig.getUrlPattern()
|
||||||
|
.replace("{date}", ZonedDateTime.of(day, LocalTime.MIDNIGHT, ZoneId.systemDefault()).format(DateTimeFormatter.ISO_OFFSET_DATE_TIME))
|
||||||
|
.replace("{latitude}", weatherConfig.getLatitude() + "")
|
||||||
|
.replace("{longitude}", weatherConfig.getLongitude() + "");
|
||||||
|
final HttpURLConnection connection = (HttpURLConnection) URI.create(url).toURL().openConnection();
|
||||||
|
final int responseCode = connection.getResponseCode();
|
||||||
|
final byte[] bytes = connection.getInputStream().readAllBytes();
|
||||||
|
if (responseCode / 100 != 2) {
|
||||||
|
throw new IOException("responseCode=%d, message: %s".formatted(responseCode, new String(bytes, StandardCharsets.UTF_8)));
|
||||||
|
}
|
||||||
|
return objectMapper.readValue(bytes, BrightSkyDto.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user