136 lines
5.3 KiB
Java
136 lines
5.3 KiB
Java
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.entry.ScheduleEntry;
|
|
import de.ph87.homeautomation.schedule.entry.ScheduleEntryType;
|
|
import lombok.RequiredArgsConstructor;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import org.springframework.boot.context.event.ApplicationStartedEvent;
|
|
import org.springframework.context.ApplicationEventPublisher;
|
|
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
|
|
@Service
|
|
@Transactional
|
|
@RequiredArgsConstructor
|
|
public class ScheduleCalculationService {
|
|
|
|
private final Config config;
|
|
|
|
private final ScheduleReadService scheduleReadService;
|
|
|
|
private final ApplicationEventPublisher eventPublisher;
|
|
|
|
@EventListener(ApplicationStartedEvent.class)
|
|
public void calculateAllNext() {
|
|
final ZonedDateTime now = ZonedDateTime.now();
|
|
scheduleReadService.findAll().forEach(schedule -> calculateSchedule(schedule, now));
|
|
}
|
|
|
|
public void calculateSchedule(final Schedule schedule, final ZonedDateTime now) {
|
|
schedule.getEntries().forEach(scheduleEntry -> calculateEntry(schedule, scheduleEntry, now));
|
|
final Optional<ScheduleEntry> nextEntry = schedule.getEntries().stream()
|
|
.filter(entry -> entry.getNextFuzzyTimestamp() != null && entry.getNextFuzzyTimestamp().isAfter(now))
|
|
.min(Comparator.comparing(ScheduleEntry::getNextFuzzyTimestamp));
|
|
if (nextEntry.isEmpty()) {
|
|
log.info("No next schedule for \"{}\"", schedule.getTitle());
|
|
} else {
|
|
log.info("Next schedule for \"{}\": {}", schedule.getTitle(), nextEntry.get().getNextFuzzyTimestamp());
|
|
}
|
|
eventPublisher.publishEvent(new ScheduleThreadWakeUpEvent());
|
|
}
|
|
|
|
private void calculateEntry(final Schedule schedule, final ScheduleEntry entry, final ZonedDateTime now) {
|
|
log.debug("calculateNext \"{}\", {}:", schedule.getTitle(), entry);
|
|
if (!schedule.isEnabled() || !entry.isEnabled() || !isAnyWeekdayEnabled(entry)) {
|
|
entry.setNextClearTimestamp(null);
|
|
return;
|
|
}
|
|
ZonedDateTime midnight = now.withHour(0).withMinute(0).withSecond(0).withNano(0);
|
|
ZonedDateTime next = calculateEntryForDay(entry, midnight);
|
|
while (next != null && (!next.isAfter(now) || !isAfterLast(entry, next) || !isWeekdayEnabled(entry, next))) {
|
|
log.debug(" -- skipping: next={}", next);
|
|
midnight = midnight.plusDays(1);
|
|
next = calculateEntryForDay(entry, midnight);
|
|
}
|
|
log.debug(" => {}", next);
|
|
entry.setNextClearTimestamp(next);
|
|
}
|
|
|
|
private boolean isAfterLast(final ScheduleEntry entry, final ZonedDateTime next) {
|
|
return entry.getLastClearTimestamp() == null || next.isAfter(entry.getLastClearTimestamp());
|
|
}
|
|
|
|
private ZonedDateTime calculateEntryForDay(final ScheduleEntry entry, final ZonedDateTime midnight) {
|
|
switch (entry.getType()) {
|
|
case TIME:
|
|
return midnight.withHour(entry.getHour()).withMinute(entry.getMinute()).withSecond(entry.getSecond());
|
|
case SUNRISE:
|
|
case SUNSET:
|
|
return astroNext(entry, midnight);
|
|
default:
|
|
log.error("AstroEvent not implemented: {}", entry.getType());
|
|
break;
|
|
}
|
|
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;
|
|
}
|
|
|
|
}
|