←
diff --git a/src/main/angular/src/app/location/detail/location-detail.ts b/src/main/angular/src/app/location/detail/location-detail.ts
index 5844cbf..e0fb83e 100644
--- a/src/main/angular/src/app/location/detail/location-detail.ts
+++ b/src/main/angular/src/app/location/detail/location-detail.ts
@@ -15,6 +15,7 @@ import {SeriesType} from '../../series/SeriesType';
import {DateService} from '../../date.service';
import {LocationPower} from '../power/location-power';
import {ConfigService} from '../../config.service';
+import {WeatherComponent} from '../../weather/plot/weather-component';
export function paramNumberOrNull(params: Params, key: string): number | null {
const param = params[key];
@@ -35,7 +36,8 @@ export function paramNumberOrNull(params: Params, key: string): number | null {
Number,
SeriesSelect,
LocationEnergy,
- LocationPower
+ LocationPower,
+ WeatherComponent
],
templateUrl: './location-detail.html',
styleUrl: './location-detail.less',
diff --git a/src/main/angular/src/app/location/energy/charts/energy-charts.ts b/src/main/angular/src/app/location/energy/charts/energy-charts.ts
index 97ec0f8..071b221 100644
--- a/src/main/angular/src/app/location/energy/charts/energy-charts.ts
+++ b/src/main/angular/src/app/location/energy/charts/energy-charts.ts
@@ -10,8 +10,10 @@ import {formatNumber} from '@angular/common';
const COLOR_BACK_PURCHASE = "#ffb9b9";
const COLOR_BACK_DELIVER = "#ff59ff";
+// noinspection JSUnusedLocalSymbols
const COLOR_BACK_PRODUCE = "#5cbcff";
const COLOR_BACK_SELF = "#60ff8c";
+// noinspection JSUnusedLocalSymbols
const COLOR_BACK_CONSUME = "#ffc07a";
@Component({
@@ -71,7 +73,6 @@ export class EnergyCharts implements OnChanges {
locale: de
},
},
- bounds: 'ticks',
},
y: {},
},
diff --git a/src/main/angular/src/app/weather/WeatherHour.ts b/src/main/angular/src/app/weather/WeatherHour.ts
new file mode 100644
index 0000000..0ab1172
--- /dev/null
+++ b/src/main/angular/src/app/weather/WeatherHour.ts
@@ -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),
+ );
+ }
+
+}
diff --git a/src/main/angular/src/app/weather/plot/weather-component.html b/src/main/angular/src/app/weather/plot/weather-component.html
new file mode 100644
index 0000000..4585a0f
--- /dev/null
+++ b/src/main/angular/src/app/weather/plot/weather-component.html
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/main/angular/src/app/weather/plot/weather-component.less b/src/main/angular/src/app/weather/plot/weather-component.less
new file mode 100644
index 0000000..cbe226c
--- /dev/null
+++ b/src/main/angular/src/app/weather/plot/weather-component.less
@@ -0,0 +1,4 @@
+.container {
+ width: 100%;
+ height: 100%;
+}
diff --git a/src/main/angular/src/app/weather/plot/weather-component.ts b/src/main/angular/src/app/weather/plot/weather-component.ts
new file mode 100644
index 0000000..7c26d51
--- /dev/null
+++ b/src/main/angular/src/app/weather/plot/weather-component.ts
@@ -0,0 +1,238 @@
+import {AfterViewInit, Component, ElementRef, Inject, Input, LOCALE_ID, 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';
+import {Location} from '../../location/Location';
+import {formatNumber} from '@angular/common';
+
+export function toPoint(f: (hour: WeatherHour) => number): (hour: WeatherHour) => { x: number, y: number } {
+ return (hour: WeatherHour) => {
+ return {
+ x: hour.date.getTime(),
+ y: f(hour),
+ };
+ };
+}
+
+export function temperatureColor(value: number | null | undefined) {
+ if (!value) {
+ return "black";
+ } else if (value < 0) {
+ return 'blue';
+ } else if (value < 20) {
+ return '#c1b100';
+ } else if (value < 30) {
+ return '#ff8100';
+ }
+ return TEMPERATURE_HIGH_COLOR;
+}
+
+const TEMPERATURE_HIGH_COLOR = '#FF0000';
+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
;
+
+ @ViewChild('container')
+ protected chartContainer!: ElementRef;
+
+ private chart!: Chart;
+
+ @Input()
+ location!: Location;
+
+ constructor(
+ @Inject(LOCALE_ID) readonly locale: string,
+ readonly weatherService: WeatherService,
+ ) {
+ //
+ }
+
+ ngAfterViewInit(): void {
+ this.chart = new Chart(this.canvasRef.nativeElement, {
+ type: 'line',
+ data: {
+ datasets: [],
+ },
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ animation: false,
+ interaction: {
+ mode: 'index',
+ intersect: false
+ },
+ plugins: {
+ legend: {
+ display: false,
+ },
+ tooltip: {
+ mode: 'index',
+ intersect: false,
+ itemSort: (a, b) => b.datasetIndex - a.datasetIndex,
+ 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});
+ },
+ label: ((ctx: any) => {
+ const groups = /^(?.+)\[(?.+)]$/.exec(ctx.dataset.label || '')?.groups;
+ if (groups) {
+ const value = ctx.parsed.y === null ? '-' : formatNumber(Math.abs(ctx.parsed.y), this.locale, '0.2-2');
+ return `${value} ${groups['unit']} ${groups['name']}`;
+ }
+ return ctx.dataset.label;
+ }) as any,
+ },
+
+ },
+ title: {
+ display: true,
+ text: "Wetter",
+ }
+ },
+ scales: {
+ x: {
+ type: 'time',
+ time: {
+ unit: "day",
+ displayFormats: {
+ day: "EE dd.MM"
+ }
+ },
+ adapters: {
+ date: {
+ locale: de,
+ }
+ },
+ },
+ y_temperature: {
+ display: true,
+ position: "right",
+ title: {
+ display: false,
+ text: "Temperatur [°C]",
+ color: TEMPERATURE_HIGH_COLOR,
+ },
+ ticks: {
+ color: context => temperatureColor(context.tick.value),
+ },
+ min: -10,
+ max: 35,
+ },
+ y_precipitation: {
+ display: false,
+ grid: {
+ display: false,
+ },
+ title: {
+ display: false,
+ text: "Niederschlag [mm]",
+ color: PRECIPITATION_COLOR,
+ },
+ ticks: {
+ color: PRECIPITATION_COLOR,
+ },
+ min: 0,
+ max: 15,
+ },
+ y_sun: {
+ display: false,
+ grid: {
+ display: false,
+ },
+ position: "right",
+ title: {
+ display: false,
+ 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.forLocation(this.location, hours => {
+ const now = Date.now();
+ const filtered = hours.filter(h => h.date.getTime() >= now);
+ this.chart.data.datasets.push({
+ label: "Niederschlag [mm]",
+ categoryPercentage: 1.0,
+ barPercentage: 1.0,
+ 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 [W/m²]",
+ 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 [°C]",
+ type: "line",
+ yAxisID: "y_temperature",
+ data: filtered.map(toPoint(h => h.temperature)),
+ backgroundColor: TEMPERATURE_HIGH_COLOR + '66',
+ borderWidth: 0,
+ pointRadius: 1,
+ pointBackgroundColor: context => temperatureColor(context.parsed.y),
+ pointStyle: "point",
+ });
+ this.chart.data.datasets.push({
+ label: "Bewölkung [%]",
+ 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();
+ });
+ }
+
+}
diff --git a/src/main/angular/src/app/weather/weather-service.ts b/src/main/angular/src/app/weather/weather-service.ts
new file mode 100644
index 0000000..66c87a8
--- /dev/null
+++ b/src/main/angular/src/app/weather/weather-service.ts
@@ -0,0 +1,22 @@
+import {Injectable} from '@angular/core';
+import {ApiService, CrudService, Next, WebsocketService} from '../common';
+import {WeatherHour} from './WeatherHour';
+import {Location} from '../location/Location';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class WeatherService extends CrudService {
+
+ constructor(
+ api: ApiService,
+ ws: WebsocketService,
+ ) {
+ super(api, ws, ['Weather'], WeatherHour.fromJson);
+ }
+
+ forLocation(location: Location, next: Next) {
+ this.getList(['forLocation', location.id], next);
+ }
+
+}
diff --git a/src/main/angular/src/styles.less b/src/main/angular/src/styles.less
index 7f3341b..3cc15f7 100644
--- a/src/main/angular/src/styles.less
+++ b/src/main/angular/src/styles.less
@@ -71,6 +71,7 @@ div {
> .SectionHeading {
display: flex;
color: dimgray;
+ font-size: 80%;
margin-top: -1.25em;
> .SectionHeadingText {
diff --git a/src/main/java/de/ph87/data/weather/WeatherConfig.java b/src/main/java/de/ph87/data/weather/WeatherConfig.java
index 8a05b25..8989332 100644
--- a/src/main/java/de/ph87/data/weather/WeatherConfig.java
+++ b/src/main/java/de/ph87/data/weather/WeatherConfig.java
@@ -11,10 +11,6 @@ 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;
diff --git a/src/main/java/de/ph87/data/weather/WeatherController.java b/src/main/java/de/ph87/data/weather/WeatherController.java
index 1b59384..432981e 100644
--- a/src/main/java/de/ph87/data/weather/WeatherController.java
+++ b/src/main/java/de/ph87/data/weather/WeatherController.java
@@ -3,6 +3,7 @@ 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.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@@ -16,9 +17,9 @@ public class WeatherController {
private final WeatherService weatherService;
- @GetMapping("all")
- public List all() {
- return weatherService.all();
+ @GetMapping("forLocation/{id}")
+ public List forLocation(@PathVariable final long id) {
+ return weatherService.forLocation(id);
}
}
diff --git a/src/main/java/de/ph87/data/weather/WeatherService.java b/src/main/java/de/ph87/data/weather/WeatherService.java
index 0ba744f..c7098ec 100644
--- a/src/main/java/de/ph87/data/weather/WeatherService.java
+++ b/src/main/java/de/ph87/data/weather/WeatherService.java
@@ -1,13 +1,17 @@
package de.ph87.data.weather;
+import de.ph87.data.location.LocationDto;
+import de.ph87.data.location.LocationRepository;
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.http.HttpStatus;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
+import org.springframework.web.server.ResponseStatusException;
import tools.jackson.databind.ObjectMapper;
import java.io.IOException;
@@ -20,7 +24,9 @@ import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
@Slf4j
@Service
@@ -28,43 +34,53 @@ import java.util.List;
@RequiredArgsConstructor
public class WeatherService {
- private List hours = new ArrayList<>();
+ private final LocationRepository locationRepository;
+
+ private final Map> locationHours = new HashMap<>();
private final WeatherConfig weatherConfig;
private final ObjectMapper objectMapper;
@NonNull
- public List all() {
+ public List forLocation(final long id) {
+ final List hours = this.locationHours.get(id);
+ if (hours == null) {
+ throw new ResponseStatusException(HttpStatus.NOT_FOUND);
+ }
return new ArrayList<>(hours);
}
@Scheduled(cron = "0 0 * * * *")
@EventListener(ApplicationStartedEvent.class)
- public void update() {
+ public void updateAll() {
+ locationRepository.findAllDto().forEach(this::update);
+ }
+
+ private void update(@NonNull final LocationDto location) {
try {
final LocalDate today = LocalDate.now();
final LocalDate first = today.minusDays(weatherConfig.getPastDays());
final LocalDate end = today.plusDays(weatherConfig.getFutureDays());
final List hours = new ArrayList<>();
- log.debug("Updating Weather...");
+ log.debug("Updating Weather for Location: {}", location.name);
for (LocalDate day = first; !day.isAfter(end); day = day.plusDays(1)) {
- fetchDay(day).getWeather().stream().map(WeatherHour::new).forEach(hours::add);
+ fetchDay(location, day).getWeather().stream().map(WeatherHour::new).forEach(hours::add);
}
- this.hours = hours;
- log.info("Weather update complete");
+ this.locationHours.put(location.id, hours);
+ log.info("Weather update complete for Location: {}", location.name);
} catch (Exception e) {
- log.error("Failed fetching Weather data: {}", e.toString());
+ log.error("Failed fetching Weather data for Location: {}: {}", location.name, e.toString());
}
}
@NonNull
- public BrightSkyDto fetchDay(@NonNull final LocalDate day) throws IOException {
+ public BrightSkyDto fetchDay(@NonNull final LocationDto location, @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() + "");
+ .replace("{latitude}", location.getLatitude() + "")
+ .replace("{longitude}", location.getLongitude() + "");
final HttpURLConnection connection = (HttpURLConnection) URI.create(url).toURL().openConnection();
final int responseCode = connection.getResponseCode();
final byte[] bytes = connection.getInputStream().readAllBytes();