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 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; } }