Weather
This commit is contained in:
parent
940e7c465b
commit
e88ff035cf
@ -1,6 +1,8 @@
|
||||
import {ApplicationConfig, provideBrowserGlobalErrorListeners, provideZoneChangeDetection} from '@angular/core';
|
||||
import {provideRouter} from '@angular/router';
|
||||
|
||||
import 'chartjs-adapter-date-fns';
|
||||
|
||||
import {routes} from './app.routes';
|
||||
import {provideHttpClient} from '@angular/common/http';
|
||||
import {stompServiceFactory} from './COMMON';
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
<div class="plots">
|
||||
<div class="plot">
|
||||
<app-weather-component></app-weather-component>
|
||||
</div>
|
||||
@for (plot of plots; track plot.id) {
|
||||
<div class="plot">
|
||||
<app-plot [plot]="plot"></app-plot>
|
||||
|
||||
@ -2,11 +2,13 @@ import {Component} from '@angular/core';
|
||||
import {PlotService} from '../plot/plot.service';
|
||||
import {Plot} from '../plot/Plot';
|
||||
import {PlotComponent} from '../plot/plot/plot.component';
|
||||
import {WeatherComponent} from '../weather/plot/weather-component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-dashboard',
|
||||
imports: [
|
||||
PlotComponent
|
||||
PlotComponent,
|
||||
WeatherComponent
|
||||
],
|
||||
templateUrl: './dashboard.component.html',
|
||||
styleUrl: './dashboard.component.less'
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import {Component, OnDestroy, OnInit} from '@angular/core';
|
||||
import {BarController, BarElement, CategoryScale, Chart, Filler, Legend, LinearScale, LineController, LineElement, PointElement, TimeScale, Title, Tooltip} from 'chart.js';
|
||||
import {SeriesService} from "../../series/series.service";
|
||||
import 'chartjs-adapter-date-fns';
|
||||
import {SeriesType} from '../../series/SeriesType';
|
||||
import {PlotService} from '../plot.service';
|
||||
import {Plot} from '../Plot';
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import {AfterViewInit, Component, ElementRef, Input, OnDestroy, ViewChild} from '@angular/core';
|
||||
import {BarController, BarElement, CategoryScale, Chart, ChartDataset, Filler, Legend, LinearScale, LineController, LineElement, PointElement, TimeScale, Title, Tooltip} from 'chart.js';
|
||||
import 'chartjs-adapter-date-fns';
|
||||
import {toAvg, toBool, toDelta, toMax, toMin} from '../../series/MinMaxAvg';
|
||||
import {SeriesType} from '../../series/SeriesType';
|
||||
import {Plot} from '../Plot';
|
||||
|
||||
25
src/main/angular/src/app/weather/WeatherHour.ts
Normal file
25
src/main/angular/src/app/weather/WeatherHour.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import {validateDate, validateNumber} from '../COMMON';
|
||||
|
||||
export class WeatherHour {
|
||||
|
||||
constructor(
|
||||
readonly date: Date,
|
||||
readonly clouds: number,
|
||||
readonly irradiation: number,
|
||||
readonly precipitation: number,
|
||||
readonly temperature: number,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
static fromJson(json: any): WeatherHour {
|
||||
return new WeatherHour(
|
||||
validateDate(json.date),
|
||||
validateNumber(json.clouds),
|
||||
validateNumber(json.irradiation),
|
||||
validateNumber(json.precipitation),
|
||||
validateNumber(json.temperature),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
<div #container class="container">
|
||||
<canvas #chartCanvas></canvas>
|
||||
</div>
|
||||
@ -0,0 +1,4 @@
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
204
src/main/angular/src/app/weather/plot/weather-component.ts
Normal file
204
src/main/angular/src/app/weather/plot/weather-component.ts
Normal file
@ -0,0 +1,204 @@
|
||||
import {AfterViewInit, Component, ElementRef, ViewChild} from '@angular/core';
|
||||
import {WeatherService} from '../weather-service';
|
||||
import {WeatherHour} from '../WeatherHour';
|
||||
import {Chart} from 'chart.js';
|
||||
|
||||
import {de} from 'date-fns/locale';
|
||||
import {format} from 'date-fns';
|
||||
|
||||
export function toPoint(f: (hour: WeatherHour) => number): (hour: WeatherHour) => { x: number, y: number } {
|
||||
return (hour: WeatherHour) => {
|
||||
return {
|
||||
x: hour.date.getTime(),
|
||||
y: f(hour),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const TEMPERATURE_HIGH_COLOR = '#FF0000';
|
||||
const TEMPERATURE_LOW_COLOR = '#0000FF';
|
||||
const TEMPERATURE_LOW_THRESHOLD = 0;
|
||||
const PRECIPITATION_COLOR = '#0000FF';
|
||||
const CLOUDS_COLOR = '#cccccc';
|
||||
const SUN_COLOR = '#ffc400';
|
||||
|
||||
@Component({
|
||||
selector: 'app-weather-component',
|
||||
imports: [],
|
||||
templateUrl: './weather-component.html',
|
||||
styleUrl: './weather-component.less'
|
||||
})
|
||||
export class WeatherComponent implements AfterViewInit {
|
||||
|
||||
@ViewChild('chartCanvas')
|
||||
protected canvasRef!: ElementRef<HTMLCanvasElement>;
|
||||
|
||||
@ViewChild('container')
|
||||
protected chartContainer!: ElementRef<HTMLDivElement>;
|
||||
|
||||
private chart!: Chart;
|
||||
|
||||
constructor(
|
||||
readonly weatherService: WeatherService,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.chart = new Chart(this.canvasRef.nativeElement, {
|
||||
type: 'line',
|
||||
data: {
|
||||
datasets: [],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
animation: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
},
|
||||
tooltip: {
|
||||
position: 'nearest',
|
||||
mode: 'index',
|
||||
usePointStyle: true,
|
||||
callbacks: {
|
||||
title: function (items) {
|
||||
const date = items[0].parsed.x as unknown as Date;
|
||||
return format(date, 'EE dd.MM. HH:mm', {locale: de});
|
||||
},
|
||||
},
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: "Wetter",
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
type: 'time',
|
||||
time: {
|
||||
unit: "day",
|
||||
displayFormats: {
|
||||
day: "EE dd.MM"
|
||||
}
|
||||
},
|
||||
adapters: {
|
||||
date: {
|
||||
locale: de,
|
||||
}
|
||||
},
|
||||
},
|
||||
y_temperature: {
|
||||
position: "right",
|
||||
title: {
|
||||
display: true,
|
||||
text: "Temperatur [°C]",
|
||||
color: TEMPERATURE_HIGH_COLOR,
|
||||
},
|
||||
ticks: {
|
||||
color: TEMPERATURE_HIGH_COLOR,
|
||||
},
|
||||
min: 0,
|
||||
max: 35,
|
||||
},
|
||||
y_precipitation: {
|
||||
title: {
|
||||
display: true,
|
||||
text: "Niederschlag [mm]",
|
||||
color: PRECIPITATION_COLOR,
|
||||
},
|
||||
ticks: {
|
||||
color: PRECIPITATION_COLOR,
|
||||
},
|
||||
min: 0,
|
||||
max: 15,
|
||||
},
|
||||
y_sun: {
|
||||
position: "right",
|
||||
title: {
|
||||
display: true,
|
||||
text: "Sonne [kWh/m²]",
|
||||
color: SUN_COLOR,
|
||||
},
|
||||
ticks: {
|
||||
color: SUN_COLOR,
|
||||
},
|
||||
min: 0,
|
||||
max: 1000,
|
||||
},
|
||||
y_clouds: {
|
||||
display: false,
|
||||
title: {
|
||||
display: true,
|
||||
text: "Wolkenbedeckung [%]",
|
||||
color: CLOUDS_COLOR,
|
||||
},
|
||||
ticks: {
|
||||
color: CLOUDS_COLOR,
|
||||
},
|
||||
min: 0,
|
||||
max: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
this.weatherService.all(hours => {
|
||||
const now = Date.now();
|
||||
const filtered = hours.filter(h => h.date.getTime() >= now);
|
||||
this.chart.data.datasets.push({
|
||||
label: "Niederschlag",
|
||||
type: "bar",
|
||||
yAxisID: "y_precipitation",
|
||||
data: filtered.map(toPoint(h => h.precipitation)),
|
||||
borderColor: PRECIPITATION_COLOR,
|
||||
backgroundColor: PRECIPITATION_COLOR + '66',
|
||||
borderWidth: 0,
|
||||
pointStyle: "rect",
|
||||
});
|
||||
this.chart.data.datasets.push({
|
||||
label: "Sonne",
|
||||
type: "line",
|
||||
fill: "origin",
|
||||
yAxisID: "y_sun",
|
||||
data: filtered.map(toPoint(h => h.irradiation)),
|
||||
borderColor: SUN_COLOR,
|
||||
backgroundColor: SUN_COLOR + '88',
|
||||
borderWidth: 0,
|
||||
pointRadius: 0,
|
||||
});
|
||||
this.chart.data.datasets.push({
|
||||
label: "Temperatur",
|
||||
type: "line",
|
||||
yAxisID: "y_temperature",
|
||||
data: filtered.map(toPoint(h => h.temperature)),
|
||||
borderColor: function (context) {
|
||||
const value = context.dataset.data[context.dataIndex];
|
||||
return value !== null && typeof value === 'object' && value.hasOwnProperty("y") && value.y >= TEMPERATURE_LOW_THRESHOLD ? TEMPERATURE_HIGH_COLOR : TEMPERATURE_LOW_COLOR;
|
||||
},
|
||||
backgroundColor: TEMPERATURE_HIGH_COLOR + '66',
|
||||
fill: {
|
||||
target: {value: TEMPERATURE_LOW_THRESHOLD},
|
||||
above: TEMPERATURE_HIGH_COLOR + '66',
|
||||
below: TEMPERATURE_LOW_COLOR + '66',
|
||||
},
|
||||
borderWidth: 0,
|
||||
pointRadius: 5,
|
||||
pointStyle: "cross",
|
||||
});
|
||||
this.chart.data.datasets.push({
|
||||
label: "Wolken",
|
||||
type: "line",
|
||||
fill: "origin",
|
||||
yAxisID: "y_clouds",
|
||||
data: filtered.map(toPoint(h => h.clouds)),
|
||||
borderColor: CLOUDS_COLOR,
|
||||
backgroundColor: CLOUDS_COLOR + '88',
|
||||
borderWidth: 0,
|
||||
pointRadius: 0,
|
||||
});
|
||||
this.chart.update();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
20
src/main/angular/src/app/weather/weather-service.ts
Normal file
20
src/main/angular/src/app/weather/weather-service.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {ApiService, CrudService, Next} from '../COMMON';
|
||||
import {WeatherHour} from './WeatherHour';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class WeatherService extends CrudService<WeatherHour> {
|
||||
|
||||
constructor(
|
||||
api: ApiService,
|
||||
) {
|
||||
super(api, ['Weather'], WeatherHour.fromJson);
|
||||
}
|
||||
|
||||
all(next: Next<WeatherHour[]>) {
|
||||
this.getList(['all'], next);
|
||||
}
|
||||
|
||||
}
|
||||
81
src/main/java/de/ph87/data/weather/BrightSkyDto.java
Normal file
81
src/main/java/de/ph87/data/weather/BrightSkyDto.java
Normal file
@ -0,0 +1,81 @@
|
||||
package de.ph87.data.weather;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
import lombok.NonNull;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.List;
|
||||
import java.util.TimeZone;
|
||||
|
||||
@Data
|
||||
@SuppressWarnings("unused")
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class BrightSkyDto {
|
||||
|
||||
private List<Hour> weather;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class Hour {
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
public double getSolar() {
|
||||
return solar == null ? 0.0 : solar;
|
||||
}
|
||||
|
||||
public double getCloud_cover() {
|
||||
return cloud_cover == null ? 0.0 : cloud_cover;
|
||||
}
|
||||
|
||||
public double getPrecipitation() {
|
||||
return precipitation == null ? 0.0 : precipitation;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
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.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@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;
|
||||
|
||||
}
|
||||
24
src/main/java/de/ph87/data/weather/WeatherController.java
Normal file
24
src/main/java/de/ph87/data/weather/WeatherController.java
Normal file
@ -0,0 +1,24 @@
|
||||
package de.ph87.data.weather;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.CrossOrigin;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@CrossOrigin
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("Weather")
|
||||
public class WeatherController {
|
||||
|
||||
private final WeatherService weatherService;
|
||||
|
||||
@GetMapping("all")
|
||||
public List<WeatherHour> all() {
|
||||
return weatherService.all();
|
||||
}
|
||||
|
||||
}
|
||||
36
src/main/java/de/ph87/data/weather/WeatherHour.java
Normal file
36
src/main/java/de/ph87/data/weather/WeatherHour.java
Normal file
@ -0,0 +1,36 @@
|
||||
package de.ph87.data.weather;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NonNull;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
|
||||
@Data
|
||||
@ToString(includeFieldNames = false)
|
||||
public class WeatherHour {
|
||||
|
||||
@NonNull
|
||||
public final ZonedDateTime date;
|
||||
|
||||
@NonNull
|
||||
public final double clouds;
|
||||
|
||||
@NonNull
|
||||
public final double irradiation;
|
||||
|
||||
@NonNull
|
||||
public final double precipitation;
|
||||
|
||||
@NonNull
|
||||
public final double temperature;
|
||||
|
||||
public WeatherHour(@NonNull final BrightSkyDto.Hour dto) {
|
||||
date = dto.getTimestamp();
|
||||
clouds = dto.getCloud_cover();
|
||||
irradiation = dto.getSolar() * 1000;
|
||||
precipitation = dto.getPrecipitation();
|
||||
temperature = (dto.getTemperature());
|
||||
}
|
||||
|
||||
}
|
||||
77
src/main/java/de/ph87/data/weather/WeatherService.java
Normal file
77
src/main/java/de/ph87/data/weather/WeatherService.java
Normal file
@ -0,0 +1,77 @@
|
||||
package de.ph87.data.weather;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.context.event.ApplicationStartedEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@EnableScheduling
|
||||
@RequiredArgsConstructor
|
||||
public class WeatherService {
|
||||
|
||||
private List<WeatherHour> hours = new ArrayList<>();
|
||||
|
||||
private final WeatherConfig weatherConfig;
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@NonNull
|
||||
public List<WeatherHour> all() {
|
||||
return new ArrayList<>(hours);
|
||||
}
|
||||
|
||||
@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<WeatherHour> hours = new ArrayList<>();
|
||||
log.debug("Updating Weather...");
|
||||
for (LocalDate day = first; !day.isAfter(end); day = day.plusDays(1)) {
|
||||
fetchDay(day).getWeather().stream().map(WeatherHour::new).forEach(hours::add);
|
||||
}
|
||||
this.hours = hours;
|
||||
log.info("Weather update complete");
|
||||
} catch (Exception e) {
|
||||
log.error("Failed fetching Weather data: {}", 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