Provide detailed Sunrise/-set times per Schedule from Server

This commit is contained in:
Patrick Haßel 2022-11-30 11:37:03 +01:00
parent 0b146b5972
commit fd5ceced45
25 changed files with 432 additions and 126 deletions

View File

@ -0,0 +1,25 @@
import {validateDateAllowNull, validateNumberNotNull, validateStringNullToEmpty} from "../validators";
export class Astro {
constructor(
readonly zenith: number,
readonly sunrise: Date,
readonly sunset: Date,
readonly sunriseName: string,
readonly sunsetName: string,
) {
// nothing
}
static fromJson(json: any): Astro {
return new Astro(
validateNumberNotNull(json['zenith']),
validateDateAllowNull(json['sunrise']),
validateDateAllowNull(json['sunset']),
validateStringNullToEmpty(json['sunriseName']),
validateStringNullToEmpty(json['sunsetName']),
);
}
}

View File

@ -1,17 +1,22 @@
import {validateBooleanNotNull, validateListOrEmpty, validateNumberNotNull, validateStringNotEmptyNotNull} from "../validators";
import {ScheduleEntry} from "./entry/ScheduleEntry";
import {Astro} from "../astro/Astro";
export class Schedule {
readonly next?: ScheduleEntry;
readonly last?: ScheduleEntry;
constructor(
readonly id: number,
readonly enabled: boolean,
readonly title: string,
readonly entries: ScheduleEntry[],
readonly astros: Astro[],
) {
this.next = entries.filter(e => e.nextFuzzyTimestamp).sort((a, b) => a.nextFuzzyTimestamp.date.getTime() - b.nextFuzzyTimestamp.date.getTime())[0];
this.last = entries.filter(e => e.lastFuzzyTimestamp).sort((a, b) => b.nextFuzzyTimestamp.date.getTime() - a.nextFuzzyTimestamp.date.getTime())[0];
}
static fromJson(json: any): Schedule {
@ -20,6 +25,7 @@ export class Schedule {
validateBooleanNotNull(json['enabled']),
validateStringNotEmptyNotNull(json['title']),
validateListOrEmpty(json['entries'], ScheduleEntry.fromJson, ScheduleEntry.comparePosition),
validateListOrEmpty(json['astros'], Astro.fromJson),
);
}

View File

@ -29,6 +29,7 @@ export class ScheduleEntry {
readonly lastClearTimestamp: Timestamp | null,
readonly nextClearTimestamp: Timestamp | null,
readonly nextFuzzyTimestamp: Timestamp | null,
readonly lastFuzzyTimestamp: Timestamp | null,
readonly property: Property | null,
readonly value: number,
readonly bulk: Bulk | null,
@ -57,6 +58,7 @@ export class ScheduleEntry {
Timestamp.fromDateOrNull(validateDateAllowNull(json['lastClearTimestamp'])),
Timestamp.fromDateOrNull(validateDateAllowNull(json['nextClearTimestamp'])),
Timestamp.fromDateOrNull(validateDateAllowNull(json['nextFuzzyTimestamp'])),
Timestamp.fromDateOrNull(validateDateAllowNull(json['lastFuzzyTimestamp'])),
Property.fromJsonAllowNull(json['property']),
validateNumberNotNull(json['value']),
Bulk.fromJsonOrNull(json['bulk']),

View File

@ -17,6 +17,8 @@ import {PropertyListComponent} from './pages/property/list/property-list.compone
import {ChannelListComponent} from './pages/channel/list/channel-list.component';
import {BulkListComponent} from './pages/bulk/list/bulk-list.component';
import {BulkEditorComponent} from './pages/bulk/editor/bulk-editor.component';
import {LeftPadDirective} from './pipes/left-pad.directive';
import {EntryValueComponent} from './shared/entry-value/entry-value.component';
@NgModule({
declarations: [
@ -33,6 +35,8 @@ import {BulkEditorComponent} from './pages/bulk/editor/bulk-editor.component';
DeviceListComponent,
BulkListComponent,
BulkEditorComponent,
LeftPadDirective,
EntryValueComponent,
],
imports: [
BrowserModule,

View File

@ -55,23 +55,11 @@
<ng-container *ngIf="entry.type === 'SUNRISE' || entry.type === 'SUNSET'">
<td>
<select [ngModel]="entry.zenith" (ngModelChange)="set(entry, 'zenith', $event)">
<option value="87">
[&nbsp;87°]
<ng-container *ngIf="entry.type === 'SUNRISE'">Nach Sonnenaufgang</ng-container>
<ng-container *ngIf="entry.type === 'SUNSET'">Vor Sonnenuntergang</ng-container>
<option *ngFor="let event of schedule.astros; let index = index" [value]="event.zenith">
[{{event.zenith | number:'0.1-1' | leftPad:5}}°, {{(entry.type === 'SUNRISE' ? event.sunrise : event.sunset) | date:'HH:mm'}}]&nbsp;{{entry.type === 'SUNRISE' ? event.sunriseName : event.sunsetName}}
</option>
<option value="90.8333">
[&nbsp;90°]
<ng-container *ngIf="entry.type === 'SUNRISE'">Sonnenaufgang</ng-container>
<ng-container *ngIf="entry.type === 'SUNSET'">Sonnenuntergang</ng-container>
</option>
<option value="93">[&nbsp;93°]</option>
<option value="96">[&nbsp;96°] Bürgerliche Dämmerung</option>
<option value="99">[&nbsp;99°]</option>
<option value="102">[102°] Nautische Dämmerung</option>
<option value="105">[105°]</option>
<option value="108">[108°] Astronomische Dämmerung</option>
</select>
</td>
</ng-container>
<td *ngIf="entry.type !== 'SUNRISE' && entry.type !== 'SUNSET'" class="empty"></td>
@ -85,7 +73,7 @@
<td class="middle">:</td>
<td class="last">
<select [ngModel]="entry.minute" (ngModelChange)="set(entry, 'minute', $event)">
<option *ngFor="let _ of [].constructor(60); let value = index" [ngValue]="value">{{value | number:'2.0'}}</option>
<option *ngFor="let _ of [].constructor(12); let value = index" [ngValue]="value * 5">{{value * 5 | number:'2.0'}}</option>
</select>
</td>
</ng-container>
@ -132,44 +120,10 @@
<td>
<app-search [searchService]="propertyService" [initial]="entry.property?.id" (valueChange)="set(entry, 'property', $event)"></app-search>
</td>
<ng-container [ngSwitch]="entry.property?.type">
<td *ngSwitchCase="'BOOLEAN'" [class.true]="entry.value" [class.false]="!entry.value" (click)="set(entry, 'value', entry.value > 0 ? 0 : 1)">
{{entry.value ? "An" : "Aus"}}
</td>
<td *ngSwitchCase="'SHUTTER'" [class.true]="entry.value === 0" [class.false]="entry.value === 100" [class.tristate]="0 < entry.value && entry.value < 100">
<select [ngModel]="entry.value" (ngModelChange)="set(entry, 'value', $event)">
<option [ngValue]="0">100% Offen</option>
<option [ngValue]="35">&nbsp;50%</option>
<option [ngValue]="55">&nbsp;75%</option>
<option [ngValue]="75">&nbsp;90% Sonnenschutz</option>
<option [ngValue]="85">100% Schlitze</option>
<option [ngValue]="100">100% Geschlossen</option>
</select>
</td>
<td *ngSwitchCase="'BRIGHTNESS_PERCENT'" [class.true]="entry.value" [class.false]="!entry.value" [class.tristate]="0 < entry.value && entry.value < 100">
<select [ngModel]="entry.value" (ngModelChange)="set(entry, 'value', $event)">
<option *ngFor="let _ of [].constructor(21); let value = index" [ngValue]="value * 5">{{value * 5}}%</option>
</select>
</td>
<td *ngSwitchCase="'COLOR_TEMPERATURE'" [class.true]="entry.value" [class.false]="!entry.value" [class.tristate]="0 < entry.value && entry.value < 100">
<select [ngModel]="entry.value" (ngModelChange)="set(entry, 'value', $event)">
<option *ngFor="let _ of [].constructor(21); let value = index" [ngValue]="value * 5">{{value * 5}}%</option>
</select>
</td>
<td *ngSwitchCase="'LUX'" [class.true]="entry.value" [class.false]="!entry.value" [class.tristate]="0 < entry.value && entry.value < 100">
<select [ngModel]="entry.value" (ngModelChange)="set(entry, 'value', $event)">
<option *ngFor="let _ of [].constructor(21); let value = index" [ngValue]="value * 5">{{value * 5}}%</option>
</select>
</td>
<td *ngSwitchCase="'SCENE'">
<select [ngModel]="entry.value" (ngModelChange)="set(entry, 'value', $event)">
<option *ngFor="let scene of scenes" [ngValue]="scene.number">#{{scene.number | number:'2.0-0'}} {{scene.title}}</option>
</select>
</td>
<td *ngSwitchDefault class="empty">
&nbsp;
</td>
</ng-container>
<td>
<app-entry-value [entry]="entry" [allowChange]="true" (onSet)="set(entry, $event.key, $event.value)"></app-entry-value>
</td>
<td>
<app-search [searchService]="bulkService" [initial]="entry.bulk?.id" (valueChange)="set(entry, 'bulk', $event)"></app-search>

View File

@ -6,8 +6,6 @@ import {ScheduleEntryService} from "../../../api/schedule/entry/schedule-entry.s
import {faArrowAltCircleDown, faArrowAltCircleUp, faCheckCircle, faCircle, faTimesCircle} from '@fortawesome/free-regular-svg-icons';
import {ActivatedRoute, Router} from "@angular/router";
import {PropertyService} from "../../../api/property/property.service";
import {Scene} from "../../../api/scene/Scene";
import {SceneService} from "../../../api/scene/scene.service";
import {BulkService} from "../../../api/bulk/BulkService";
import {Update} from "../../../api/Update";
import {NO_OP} from "../../../api/api.service";
@ -35,8 +33,6 @@ export class ScheduleEditorComponent implements OnInit {
schedule!: Schedule;
scenes: Scene[] = [];
constructor(
readonly router: Router,
readonly activatedRoute: ActivatedRoute,
@ -44,14 +40,12 @@ export class ScheduleEditorComponent implements OnInit {
readonly scheduleEntryService: ScheduleEntryService,
readonly propertyService: PropertyService,
readonly bulkService: BulkService,
readonly sceneService: SceneService,
) {
// nothing
}
ngOnInit(): void {
this.scheduleService.subscribe(update => this.update(update));
this.sceneService.findAll(scenes => this.scenes = scenes);
this.activatedRoute.params.subscribe(params => this.scheduleService.getById(params['id'], schedule => this.schedule = schedule));
}

View File

@ -2,9 +2,10 @@
<tr>
<th>&nbsp;</th>
<th>Bezeichnung</th>
<th colspan="3">Zeitpunkt</th>
<th colspan="3">Nächste</th>
<th colspan="3">Eigenschaft</th>
<th>Massenausführung</th>
<th colspan="3">Letzte</th>
<th>&nbsp;</th>
</tr>
<tr *ngFor="let schedule of schedules; trackBy: Schedule.trackBy">
@ -24,12 +25,18 @@
<td class="number first" [class.empty]="!schedule.next?.property">{{schedule.next?.property?.title}}</td>
<td class="number middle" [class.empty]="!schedule.next?.property">&nbsp;=&nbsp;</td>
<td class="number last" [class.empty]="!schedule.next?.property">{{schedule.next?.value}}</td>
<td class="number last" [class.empty]="!schedule.next?.property">
<app-entry-value *ngIf="schedule.next" [entry]="schedule.next" [allowChange]="false"></app-entry-value>
</td>
<td [class.empty]="!schedule.next?.bulk">
{{schedule.next?.bulk?.name}}
</td>
<td class="number first" [class.empty]="!schedule.last?.lastFuzzyTimestamp">{{schedule.last?.lastFuzzyTimestamp.dayName}}</td>
<td class="number middle" [class.empty]="!schedule.last?.lastFuzzyTimestamp">:&nbsp;</td>
<td class="number last" [class.empty]="!schedule.last?.lastFuzzyTimestamp">{{schedule.last?.lastFuzzyTimestamp.timeString}}</td>
<td class="delete" (click)="delete(schedule)">
<fa-icon title="Löschen" [icon]="faTimes"></fa-icon>
</td>

View File

@ -0,0 +1,20 @@
import {Pipe, PipeTransform} from '@angular/core';
@Pipe({
name: 'leftPad',
})
export class LeftPadDirective implements PipeTransform {
constructor() {
}
transform(value: any, count: number): any {
let result = "" + value;
const rest: number = count - result.length;
if (rest > 0) {
result = " ".repeat(rest) + result;
}
return result;
}
}

View File

@ -0,0 +1,46 @@
<ng-container [ngSwitch]="entry.property?.type">
<div *ngSwitchCase="'BOOLEAN'" [class.true]="entry.value" [class.false]="!entry.value" (click)="set('value', entry.value > 0 ? 0 : 1)">
{{entry.value ? "An" : "Aus"}}
</div>
<div *ngSwitchCase="'SHUTTER'" [class.true]="entry.value === 0" [class.false]="entry.value === 100" [class.tristate]="0 < entry.value && entry.value < 100">
<select [ngModel]="entry.value" (ngModelChange)="set('value', $event)" [disabled]="!allowChange">
<option [ngValue]="0">100% Offen</option>
<option [ngValue]="35">&nbsp;50%</option>
<option [ngValue]="55">&nbsp;75%</option>
<option [ngValue]="75">&nbsp;90% Sonnenschutz</option>
<option [ngValue]="85">100% Schlitze</option>
<option [ngValue]="100">100% Geschlossen</option>
</select>
</div>
<div *ngSwitchCase="'BRIGHTNESS_PERCENT'" [class.true]="entry.value" [class.false]="!entry.value" [class.tristate]="0 < entry.value && entry.value < 100">
<select [ngModel]="entry.value" (ngModelChange)="set('value', $event)" [disabled]="!allowChange">
<option *ngFor="let _ of [].constructor(21); let value = index" [ngValue]="value * 5">{{value * 5}}%</option>
</select>
</div>
<div *ngSwitchCase="'COLOR_TEMPERATURE'" [class.true]="entry.value" [class.false]="!entry.value" [class.tristate]="0 < entry.value && entry.value < 100">
<select [ngModel]="entry.value" (ngModelChange)="set('value', $event)" [disabled]="!allowChange">
<option *ngFor="let _ of [].constructor(21); let value = index" [ngValue]="value * 5">{{value * 5}}%</option>
</select>
</div>
<div *ngSwitchCase="'LUX'" [class.true]="entry.value" [class.false]="!entry.value" [class.tristate]="0 < entry.value && entry.value < 100">
<select [ngModel]="entry.value" (ngModelChange)="set('value', $event)" [disabled]="!allowChange">
<option *ngFor="let _ of [].constructor(21); let value = index" [ngValue]="value * 5">{{value * 5}}%</option>
</select>
</div>
<div *ngSwitchCase="'SCENE'">
<select [ngModel]="entry.value" (ngModelChange)="set('value', $event)" [disabled]="!allowChange">
<option *ngFor="let scene of scenes" [ngValue]="scene.number">#{{scene.number | number:'2.0-0'}} {{scene.title}}</option>
</select>
</div>
<div *ngSwi
tchDefault class="empty">
&nbsp;
</div>
</ng-container>

View File

@ -0,0 +1,51 @@
import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
import {ScheduleEntry} from "../../api/schedule/entry/ScheduleEntry";
import {Scene} from "../../api/scene/Scene";
import {SceneService} from "../../api/scene/scene.service";
export class OnSet {
constructor(
readonly key: string,
readonly value: any,
) {
// nothing
}
}
@Component({
selector: 'app-entry-value',
templateUrl: './entry-value.component.html',
styleUrls: ['./entry-value.component.less']
})
export class EntryValueComponent implements OnInit {
@Input()
entry!: ScheduleEntry;
@Input()
allowChange: boolean = false;
@Output()
onSet: EventEmitter<OnSet> = new EventEmitter<OnSet>();
scenes: Scene[] = [];
constructor(
readonly sceneService: SceneService,) {
// nothing
}
ngOnInit(): void {
this.sceneService.findAll(scenes => this.scenes = scenes);
}
set(key: string, value: any): void {
if (!this.allowChange) {
return;
}
this.onSet.emit(new OnSet(key, value));
}
}

View File

@ -4,10 +4,12 @@
import {getBaseUrl} from "./UrlHelper";
const PROD: boolean = false;
export const environment = {
production: false,
restBase: getBaseUrl('http', 8080),
websocketBase: getBaseUrl('ws', 8080),
restBase: PROD ? 'http://10.0.0.50:8082' : getBaseUrl('http', 8080),
websocketBase: PROD ? 'ws://10.0.0.50:8082' : getBaseUrl('ws', 8080),
};
/*

View File

@ -1,9 +1,6 @@
package de.ph87.homeautomation.schedule;
import com.luckycatlabs.sunrisesunset.Zenith;
import com.luckycatlabs.sunrisesunset.calculator.SolarEventCalculator;
import com.luckycatlabs.sunrisesunset.dto.Location;
import de.ph87.homeautomation.Config;
import de.ph87.homeautomation.schedule.astro.AstroCalculator;
import de.ph87.homeautomation.schedule.entry.ScheduleEntry;
import de.ph87.homeautomation.schedule.entry.ScheduleEntryType;
import lombok.RequiredArgsConstructor;
@ -14,11 +11,8 @@ import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.Instant;
import java.time.ZonedDateTime;
import java.util.Calendar;
import java.util.Comparator;
import java.util.GregorianCalendar;
import java.util.Optional;
@Slf4j
@ -27,14 +21,14 @@ import java.util.Optional;
@RequiredArgsConstructor
public class ScheduleCalculator {
private final Config config;
private final ScheduleReader scheduleReader;
private final ApplicationEventPublisher applicationEventPublisher;
private final ScheduleMapper scheduleMapper;
private final AstroCalculator astroCalculator;
@EventListener(ApplicationStartedEvent.class)
public void calculateAllNext() {
final ZonedDateTime now = ZonedDateTime.now();
@ -82,7 +76,8 @@ public class ScheduleCalculator {
return midnight.withHour(entry.getHour()).withMinute(entry.getMinute()).withSecond(entry.getSecond());
case SUNRISE:
case SUNSET:
return astroNext(entry, midnight);
final boolean sunrise = entry.getType() == ScheduleEntryType.SUNRISE;
return astroCalculator.forDay(midnight, sunrise, entry.getZenith());
default:
log.error("AstroEvent not implemented: {}", entry.getType());
break;
@ -90,49 +85,20 @@ public class ScheduleCalculator {
return null;
}
private ZonedDateTime astroNext(final ScheduleEntry entry, ZonedDateTime midnight) {
final Location location = new Location(config.getLatitude(), config.getLongitude());
final SolarEventCalculator calculator = new SolarEventCalculator(location, config.getTimezone());
final Calendar calendar = GregorianCalendar.from(midnight);
final Calendar nextCalendar = astroNext(calculator, entry.getType(), new Zenith(entry.getZenith()), calendar);
if (nextCalendar == null) {
return null;
}
return ZonedDateTime.ofInstant(Instant.ofEpochMilli(nextCalendar.getTimeInMillis()), midnight.getZone());
}
private Calendar astroNext(final SolarEventCalculator calculator, final ScheduleEntryType type, final Zenith solarZenith, final Calendar calendar) {
switch (type) {
case SUNRISE:
return calculator.computeSunriseCalendar(solarZenith, calendar);
case SUNSET:
return calculator.computeSunsetCalendar(solarZenith, calendar);
}
return null;
}
private boolean isAnyWeekdayEnabled(final ScheduleEntry entry) {
return entry.isMonday() || entry.isTuesday() || entry.isWednesday() || entry.isThursday() || entry.isFriday() || entry.isSaturday() || entry.isSunday();
}
private boolean isWeekdayEnabled(final ScheduleEntry entry, final ZonedDateTime value) {
switch (value.getDayOfWeek()) {
case MONDAY:
return entry.isMonday();
case TUESDAY:
return entry.isTuesday();
case WEDNESDAY:
return entry.isWednesday();
case THURSDAY:
return entry.isThursday();
case FRIDAY:
return entry.isFriday();
case SATURDAY:
return entry.isSaturday();
case SUNDAY:
return entry.isSunday();
}
return false;
return switch (value.getDayOfWeek()) {
case MONDAY -> entry.isMonday();
case TUESDAY -> entry.isTuesday();
case WEDNESDAY -> entry.isWednesday();
case THURSDAY -> entry.isThursday();
case FRIDAY -> entry.isFriday();
case SATURDAY -> entry.isSaturday();
case SUNDAY -> entry.isSunday();
};
}
}

View File

@ -1,6 +1,5 @@
package de.ph87.homeautomation.schedule;
import de.ph87.homeautomation.property.PropertyReader;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@ -15,8 +14,6 @@ public class ScheduleController {
private final ScheduleWriter scheduleWriter;
private final PropertyReader propertyReader;
@GetMapping("findAll")
public List<ScheduleDto> findAll() {
return scheduleReader.findAllDtos();

View File

@ -1,27 +1,32 @@
package de.ph87.homeautomation.schedule;
import de.ph87.homeautomation.schedule.astro.AstroDto;
import de.ph87.homeautomation.schedule.entry.ScheduleEntryDto;
import lombok.Getter;
import java.io.Serializable;
import java.util.List;
import java.util.Set;
@Getter
public class ScheduleDto implements Serializable {
public final long id;
private final long id;
public final boolean enabled;
private final boolean enabled;
public final String title;
private final String title;
public final Set<ScheduleEntryDto> entries;
private final Set<ScheduleEntryDto> entries;
public ScheduleDto(final Schedule schedule, final Set<ScheduleEntryDto> entries) {
private final List<AstroDto> astros;
public ScheduleDto(final Schedule schedule, final Set<ScheduleEntryDto> entries, final List<AstroDto> astros) {
this.id = schedule.getId();
this.enabled = schedule.isEnabled();
this.title = schedule.getTitle();
this.entries = entries;
this.astros = astros;
}
}

View File

@ -48,6 +48,7 @@ public class ScheduleExecutor {
bulkExecutor.execute(entry.getBulk());
}
entry.setLastClearTimestamp(entry.getNextClearTimestamp());
entry.setLastFuzzyTimestamp(entry.getNextFuzzyTimestamp());
scheduleWriter.notifyChanged(schedule);
}

View File

@ -1,5 +1,7 @@
package de.ph87.homeautomation.schedule;
import de.ph87.homeautomation.schedule.astro.AstroDto;
import de.ph87.homeautomation.schedule.astro.AstroService;
import de.ph87.homeautomation.schedule.entry.ScheduleEntryDto;
import de.ph87.homeautomation.schedule.entry.ScheduleEntryMapper;
import de.ph87.homeautomation.web.WebSocketService;
@ -8,6 +10,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@ -21,15 +24,17 @@ public class ScheduleMapper {
private final WebSocketService webSocketService;
private final AstroService astroService;
public ScheduleDto toDto(final Schedule schedule) {
final Set<ScheduleEntryDto> entries = schedule.getEntries().stream().map(scheduleEntryMapper::toDto).collect(Collectors.toSet());
return new ScheduleDto(schedule, entries);
final List<AstroDto> astros = astroService.findAllNext();
return new ScheduleDto(schedule, entries, astros);
}
public ScheduleDto publish(final Schedule schedule, final boolean existing) {
public void publish(final Schedule schedule, final boolean existing) {
final ScheduleDto dto = toDto(schedule);
webSocketService.send(ScheduleDto.class, dto, existing);
return dto;
}
}

View File

@ -0,0 +1,45 @@
package de.ph87.homeautomation.schedule.astro;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import javax.persistence.*;
@Entity
@Getter
@ToString
@NoArgsConstructor
public class Astro {
@Id
@GeneratedValue
private long id;
@Version
private long version;
@Setter
private boolean enabled = true;
@Setter
private String error;
@Column(unique = true)
private double zenith;
@Column(nullable = false)
private String name;
private String differentNameForSunset;
public Astro(final double zenith, @NonNull final String name, @Nullable final String differentNameForSunset) {
this.zenith = zenith;
this.name = name;
this.differentNameForSunset = differentNameForSunset;
}
}

View File

@ -0,0 +1,51 @@
package de.ph87.homeautomation.schedule.astro;
import com.luckycatlabs.sunrisesunset.Zenith;
import com.luckycatlabs.sunrisesunset.calculator.SolarEventCalculator;
import com.luckycatlabs.sunrisesunset.dto.Location;
import de.ph87.homeautomation.Config;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.time.Instant;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Calendar;
import java.util.GregorianCalendar;
@Service
@RequiredArgsConstructor
public class AstroCalculator {
private final Config config;
public ZonedDateTime next(final ZonedDateTime now, final boolean sunrise, final double zenith) {
ZonedDateTime day = now.truncatedTo(ChronoUnit.DAYS);
ZonedDateTime next;
do {
next = forDay(day, sunrise, zenith);
day = day.plusDays(1);
} while (next != null && !next.isAfter(now));
return next;
}
public ZonedDateTime forDay(final ZonedDateTime midnight, final boolean sunrise, final double zenith) {
final Location location = new Location(config.getLatitude(), config.getLongitude());
final SolarEventCalculator calculator = new SolarEventCalculator(location, config.getTimezone());
final Calendar calendar = GregorianCalendar.from(midnight);
final Calendar nextCalendar = forDay(calculator, sunrise, new Zenith(zenith), calendar);
if (nextCalendar == null) {
return null;
}
return ZonedDateTime.ofInstant(Instant.ofEpochMilli(nextCalendar.getTimeInMillis()), midnight.getZone());
}
private Calendar forDay(final SolarEventCalculator calculator, final boolean sunrise, final Zenith solarZenith, final Calendar calendar) {
if (sunrise) {
return calculator.computeSunriseCalendar(solarZenith, calendar);
} else {
return calculator.computeSunsetCalendar(solarZenith, calendar);
}
}
}

View File

@ -0,0 +1,37 @@
package de.ph87.homeautomation.schedule.astro;
import lombok.Getter;
import lombok.ToString;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import java.time.ZonedDateTime;
@Getter
@ToString
public class AstroDto {
@NonNull
private final double zenith;
@NonNull
private final ZonedDateTime sunrise;
@NonNull
private final ZonedDateTime sunset;
@NonNull
private final String sunriseName;
@Nullable
private final String sunsetName;
public AstroDto(@NonNull final Astro astro, @NonNull final ZonedDateTime sunrise, @NonNull final ZonedDateTime sunset) {
this.zenith = astro.getZenith();
this.sunrise = sunrise;
this.sunset = sunset;
this.sunriseName = astro.getName();
this.sunsetName = astro.getDifferentNameForSunset() == null ? astro.getName() : astro.getDifferentNameForSunset();
}
}

View File

@ -0,0 +1,11 @@
package de.ph87.homeautomation.schedule.astro;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface AstroRepository extends JpaRepository<Astro, Long> {
List<Astro> findAllByEnabledTrue();
}

View File

@ -0,0 +1,71 @@
package de.ph87.homeautomation.schedule.astro;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Objects;
@RestController
@Transactional
@RequestMapping("Astro")
@RequiredArgsConstructor
public class AstroService {
private final AstroRepository astroRepository;
private final AstroCalculator astroCalculator;
@EventListener(ApplicationStartedEvent.class)
public void startup() {
if (astroRepository.count() == 0) {
astroRepository.save(new Astro(71.0000, "Aufgang +++++", "Untergang +++++"));
astroRepository.save(new Astro(75.0000, "Aufgang ++++", "Untergang ++++"));
astroRepository.save(new Astro(80.0000, "Aufgang +++", "Untergang +++"));
astroRepository.save(new Astro(85.0000, "Aufgang ++", "Untergang ++"));
astroRepository.save(new Astro(90.0000, "Aufgang +", "Untergang +"));
astroRepository.save(new Astro(90.8333, "Aufgang", "Untergang"));
astroRepository.save(new Astro(92.0000, "Aufgang -", null));
astroRepository.save(new Astro(93.0000, "Aufgang --", null));
astroRepository.save(new Astro(94.0000, "Bürgerlich ++", null));
astroRepository.save(new Astro(95.0000, "Bürgerlich +", null));
astroRepository.save(new Astro(96.0000, "Bürgerlich", null));
astroRepository.save(new Astro(97.0000, "Bürgerlich -", null));
astroRepository.save(new Astro(98.0000, "Bürgerlich --", null));
astroRepository.save(new Astro(99.0000, "Bürgerlich ---", null));
astroRepository.save(new Astro(100.000, "Nautisch ++", null));
astroRepository.save(new Astro(101.000, "Nautisch +", null));
astroRepository.save(new Astro(102.000, "Nautisch", null));
astroRepository.save(new Astro(103.000, "Nautisch -", null));
astroRepository.save(new Astro(104.000, "Nautisch --", null));
astroRepository.save(new Astro(105.000, "Nautisch ---", null));
astroRepository.save(new Astro(106.000, "Astronomisch ++", null));
astroRepository.save(new Astro(107.000, "Astronomisch +", null));
astroRepository.save(new Astro(108.000, "Astronomisch", null));
astroRepository.save(new Astro(110.000, "Astronomisch -", null));
astroRepository.save(new Astro(120.000, "Astronomisch --", null));
}
}
public List<AstroDto> findAllNext() {
final ZonedDateTime now = ZonedDateTime.now();
return astroRepository.findAllByEnabledTrue().stream().map(astro -> next(now, astro)).filter(Objects::nonNull).toList();
}
private AstroDto next(final ZonedDateTime now, final Astro astro) {
final ZonedDateTime sunrise = astroCalculator.next(now, true, astro.getZenith());
final ZonedDateTime sunset = astroCalculator.next(now, false, astro.getZenith());
if (sunrise == null || sunset == null) {
astro.setEnabled(false);
astro.setError("sunrise (%s) or sunset (%s) NULL for %s".formatted(sunrise, sunset, now));
return null;
}
return new AstroDto(astro, sunrise, sunset);
}
}

View File

@ -74,11 +74,13 @@ public class ScheduleEntry {
private ZonedDateTime nextClearTimestamp;
private ZonedDateTime lastClearTimestamp;
@Setter(AccessLevel.NONE)
private ZonedDateTime nextFuzzyTimestamp;
private ZonedDateTime lastClearTimestamp;
private ZonedDateTime lastFuzzyTimestamp;
public ScheduleEntry(final Schedule schedule) {
this.schedule = schedule;
this.position = schedule.getEntries().size();

View File

@ -48,6 +48,8 @@ public class ScheduleEntryDto implements Serializable {
public final ZonedDateTime nextFuzzyTimestamp;
public final ZonedDateTime lastFuzzyTimestamp;
public final PropertyDto property;
public final double value;
@ -74,6 +76,7 @@ public class ScheduleEntryDto implements Serializable {
this.nextClearTimestamp = entry.getNextClearTimestamp();
this.lastClearTimestamp = entry.getLastClearTimestamp();
this.nextFuzzyTimestamp = entry.getNextFuzzyTimestamp();
this.lastFuzzyTimestamp = entry.getLastFuzzyTimestamp();
this.property = property;
this.value = entry.getValue();
this.bulk = bulk;

View File

@ -4,4 +4,5 @@ cd "$(dirname "$0")" || exit 1
scp target/Homeautomation-1.0-SNAPSHOT.jar media@10.0.0.50:/home/media/java/Homeautomation/Homeautomation.jar.update &&
git tag "$(date +'deploy---%Y-%m-%d---%H-%M-%S')" &&
curl -m 2 -s http://10.0.0.50:8082/server/shutdown
curl -m 2 -s http://10.0.0.50:8082/server/shutdown &&
exit 0