From 82313a19324af5363164e9982a12451d728641d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Ha=C3=9Fel?= Date: Fri, 1 Oct 2021 12:47:23 +0200 Subject: [PATCH] implemented Schedule --- pom.xml | 9 +- .../homeautomation/BackendApplication.java | 39 ---- .../java/de/ph87/homeautomation/Config.java | 18 ++ .../homeautomation/knx/KnxLinkService.java | 23 +- .../knx/group/KnxGroupSetService.java | 55 +++++ .../knx/group/KnxGroupWriteService.java | 47 ++++- .../property/IPropertyOwner.java | 11 + .../property/PropertyService.java | 27 +++ .../schedule/PropertyEntry.java | 30 +++ .../homeautomation/schedule/Schedule.java | 33 +++ .../schedule/ScheduleRepository.java | 11 + .../schedule/ScheduleService.java | 197 ++++++++++++++++++ .../schedule/entry/ScheduleEntry.java | 108 ++++++++++ .../schedule/entry/ScheduleEntryType.java | 5 + .../java/de/ph87/network/router/Route.java | 90 ++++++++ .../java/de/ph87/network/router/Router.java | 42 ++++ 16 files changed, 693 insertions(+), 52 deletions(-) create mode 100644 src/main/java/de/ph87/homeautomation/Config.java create mode 100644 src/main/java/de/ph87/homeautomation/knx/group/KnxGroupSetService.java create mode 100644 src/main/java/de/ph87/homeautomation/property/IPropertyOwner.java create mode 100644 src/main/java/de/ph87/homeautomation/property/PropertyService.java create mode 100644 src/main/java/de/ph87/homeautomation/schedule/PropertyEntry.java create mode 100644 src/main/java/de/ph87/homeautomation/schedule/Schedule.java create mode 100644 src/main/java/de/ph87/homeautomation/schedule/ScheduleRepository.java create mode 100644 src/main/java/de/ph87/homeautomation/schedule/ScheduleService.java create mode 100644 src/main/java/de/ph87/homeautomation/schedule/entry/ScheduleEntry.java create mode 100644 src/main/java/de/ph87/homeautomation/schedule/entry/ScheduleEntryType.java create mode 100644 src/main/java/de/ph87/network/router/Route.java create mode 100644 src/main/java/de/ph87/network/router/Router.java diff --git a/pom.xml b/pom.xml index a85db42..6692191 100644 --- a/pom.xml +++ b/pom.xml @@ -10,8 +10,8 @@ war - 11 - 11 + 15 + 15 @@ -56,6 +56,11 @@ calimero-core 2.5-M1 + + com.luckycatlabs + SunriseSunsetCalculator + 1.2 + org.springframework.boot diff --git a/src/main/java/de/ph87/homeautomation/BackendApplication.java b/src/main/java/de/ph87/homeautomation/BackendApplication.java index 390a582..944b18a 100644 --- a/src/main/java/de/ph87/homeautomation/BackendApplication.java +++ b/src/main/java/de/ph87/homeautomation/BackendApplication.java @@ -1,54 +1,15 @@ package de.ph87.homeautomation; -import de.ph87.homeautomation.knx.KnxLinkService; -import de.ph87.homeautomation.knx.group.KnxGroup; -import de.ph87.homeautomation.knx.group.KnxGroupRepository; -import de.ph87.homeautomation.knx.group.KnxGroupWriteService; import lombok.RequiredArgsConstructor; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.context.event.ApplicationStartedEvent; -import org.springframework.context.event.EventListener; - -import javax.annotation.PostConstruct; @SpringBootApplication @RequiredArgsConstructor public class BackendApplication { - private final KnxGroupRepository knxGroupRepository; - - private final KnxLinkService knxLinkService; - - private final KnxGroupWriteService knxGroupWriteService; - public static void main(String[] args) { SpringApplication.run(BackendApplication.class); } - @PostConstruct - public void postConstruct() { - knxGroupCreate(0, 0, 1, "1.001"); - knxGroupCreate(0, 3, 6, "1.001"); - - requestAll(); - } - - private void knxGroupCreate(final int main, final int middle, final int sub, final String dpt) { - final KnxGroup trans = new KnxGroup(); - trans.setAddress(main, middle, sub); - trans.setDpt(dpt); - knxGroupRepository.save(trans); - } - - public void requestAll() { - knxGroupWriteService.markAllForRead(); - knxLinkService.notifyActionPending(); - } - - @EventListener(ApplicationStartedEvent.class) - public void applicationStarted() { - - } - } \ No newline at end of file diff --git a/src/main/java/de/ph87/homeautomation/Config.java b/src/main/java/de/ph87/homeautomation/Config.java new file mode 100644 index 0000000..5214b78 --- /dev/null +++ b/src/main/java/de/ph87/homeautomation/Config.java @@ -0,0 +1,18 @@ +package de.ph87.homeautomation; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Data +@Configuration +@ConfigurationProperties(prefix = "de.ph87.homeautomation") +public class Config { + + private double latitude = 49.4085629; + + private double longitude = 6.9645334; + + private String timezone = "Europe/Berlin"; + +} diff --git a/src/main/java/de/ph87/homeautomation/knx/KnxLinkService.java b/src/main/java/de/ph87/homeautomation/knx/KnxLinkService.java index 822eb64..6a89f75 100644 --- a/src/main/java/de/ph87/homeautomation/knx/KnxLinkService.java +++ b/src/main/java/de/ph87/homeautomation/knx/KnxLinkService.java @@ -2,6 +2,7 @@ package de.ph87.homeautomation.knx; import de.ph87.homeautomation.knx.group.KnxGroupLinkService; import de.ph87.homeautomation.knx.group.KnxGroupWriteService; +import de.ph87.network.router.Router; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.context.event.ApplicationStartedEvent; @@ -19,6 +20,7 @@ import tuwien.auto.calimero.process.ProcessEvent; import tuwien.auto.calimero.process.ProcessListener; import javax.annotation.PreDestroy; +import java.io.IOException; import java.net.Inet4Address; import java.net.InetAddress; import java.net.InetSocketAddress; @@ -31,15 +33,13 @@ import java.time.ZonedDateTime; @RequiredArgsConstructor public class KnxLinkService implements NetworkLinkListener, ProcessListener { - public static final int ERROR_DELAY_MS = 3000; - private final KnxGroupWriteService knxGroupWriteService; private final KnxGroupLinkService knxGroupLinkService; - private InetAddress remoteAddress = null; + private Inet4Address remoteAddress = null; - private final Thread thread = new Thread(this::run); + private final Thread thread = new Thread(this::run, "knx-sync"); private boolean stop = false; @@ -51,7 +51,7 @@ public class KnxLinkService implements NetworkLinkListener, ProcessListener { @EventListener(ApplicationStartedEvent.class) public void afterStartup() throws UnknownHostException { - remoteAddress = Inet4Address.getByName("10.0.0.102"); + remoteAddress = (Inet4Address) Inet4Address.getByName("10.0.0.102"); thread.start(); } @@ -73,7 +73,7 @@ public class KnxLinkService implements NetworkLinkListener, ProcessListener { if (link == null) { try { connect(); - } catch (KNXException e) { + } catch (KNXException | IOException e) { error(e); } } else { @@ -87,9 +87,10 @@ public class KnxLinkService implements NetworkLinkListener, ProcessListener { } } - private void connect() throws KNXException, InterruptedException { + private void connect() throws KNXException, InterruptedException, IOException { log.debug("Connecting KNX link..."); - link = KNXNetworkLinkIP.newTunnelingLink(new InetSocketAddress("10.0.0.132", 0), new InetSocketAddress(remoteAddress, 3671), false, new TPSettings()); + final InetAddress localAddress = Router.getLocalInetAddressForRemoteAddress(remoteAddress); + link = KNXNetworkLinkIP.newTunnelingLink(new InetSocketAddress(localAddress, 0), new InetSocketAddress(remoteAddress, 3671), false, new TPSettings()); link.addLinkListener(this); processCommunicator = new ProcessCommunicatorImpl(link); processCommunicator.addProcessListener(this); @@ -114,9 +115,10 @@ public class KnxLinkService implements NetworkLinkListener, ProcessListener { } } - private void error(final KNXException e) throws InterruptedException { + private void error(final Exception e) throws InterruptedException { log.error(e.toString()); cleanUp(); + int ERROR_DELAY_MS = 3000; doWait(ERROR_DELAY_MS); } @@ -169,7 +171,8 @@ public class KnxLinkService implements NetworkLinkListener, ProcessListener { @Override public void groupWrite(final ProcessEvent processEvent) { - // ignore + log.debug("{}", processEvent); + knxGroupWriteService.updateOrCreate(processEvent.getDestination().getRawAddress(), processEvent.getASDU()); } @Override diff --git a/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupSetService.java b/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupSetService.java new file mode 100644 index 0000000..1a661f2 --- /dev/null +++ b/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupSetService.java @@ -0,0 +1,55 @@ +package de.ph87.homeautomation.knx.group; + +import de.ph87.homeautomation.knx.KnxLinkService; +import de.ph87.homeautomation.property.IPropertyOwner; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.event.ApplicationStartedEvent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Service; +import tuwien.auto.calimero.GroupAddress; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@Slf4j +@Service +@RequiredArgsConstructor +public class KnxGroupSetService implements IPropertyOwner { + + @Getter + private final Pattern propertyNamePattern = Pattern.compile("^knx\\.group\\.(\\d+)/(\\d+)/(\\d+)$"); + + private final KnxLinkService knxLinkService; + + private final KnxGroupWriteService knxGroupWriteService; + + @EventListener(ApplicationStartedEvent.class) + public void applicationStarted() { + knxGroupWriteService.knxGroupCreate(0, 0, 1, "1.001", true); + knxGroupWriteService.knxGroupCreate(0, 3, 6, "1.001", true); + knxGroupWriteService.knxGroupCreate(0, 4, 24, "5.001", false); + + requestAll(); + } + + public void requestAll() { + knxGroupWriteService.markAllForRead(); + knxLinkService.notifyActionPending(); + } + + @Override + public void setProperty(final String propertyName, final String value) { + final Matcher matcher = propertyNamePattern.matcher(propertyName); + if (matcher.matches()) { + final GroupAddress groupAddress = new GroupAddress(Integer.parseInt(matcher.group(1)), Integer.parseInt(matcher.group(2)), Integer.parseInt(matcher.group(3))); + if (knxGroupWriteService.setSendValue(groupAddress, value)) { + knxLinkService.notifyActionPending(); + } else { + log.error("No such KnxGroup.address = {}", groupAddress); + } + } + } + +} diff --git a/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupWriteService.java b/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupWriteService.java index 1b507b4..c75a601 100644 --- a/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupWriteService.java +++ b/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupWriteService.java @@ -3,13 +3,21 @@ package de.ph87.homeautomation.knx.group; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import tuwien.auto.calimero.GroupAddress; +import tuwien.auto.calimero.KNXException; +import tuwien.auto.calimero.dptxlator.DPTXlator; +import tuwien.auto.calimero.dptxlator.DPTXlator8BitUnsigned; +import tuwien.auto.calimero.dptxlator.DPTXlatorBoolean; +import tuwien.auto.calimero.dptxlator.TranslatorTypes; import javax.transaction.Transactional; import java.time.ZonedDateTime; +import java.util.Objects; +import java.util.Optional; @Slf4j @Service -@Transactional +@Transactional(Transactional.TxType.REQUIRES_NEW) @RequiredArgsConstructor public class KnxGroupWriteService { @@ -36,4 +44,41 @@ public class KnxGroupWriteService { knxGroupRepository.findAllByRead_AbleTrue().forEach(knxGroup -> knxGroup.getRead().setNextTimestamp(ZonedDateTime.now())); } + public void knxGroupCreate(final int main, final int middle, final int sub, final String dpt, final boolean readable) { + final KnxGroup trans = new KnxGroup(); + trans.setAddress(main, middle, sub); + trans.setDpt(dpt); + trans.getRead().setAble(readable); + knxGroupRepository.save(trans); + } + + public boolean setSendValue(final GroupAddress groupAddress, final String value) { + final Optional knxGroupOptional = knxGroupRepository.findByAddressRaw(groupAddress.getRawAddress()); + if (knxGroupOptional.isEmpty()) { + return false; + } + final KnxGroup knxGroup = knxGroupOptional.get(); + getTranslator(knxGroup, value).ifPresent(translator -> knxGroup.setSendValue(translator.getData())); + knxGroup.getSend().setNextTimestamp(ZonedDateTime.now()); + return true; + } + + private Optional getTranslator(final KnxGroup knxGroup, final String value) { + final int mainNumber = Integer.parseInt(knxGroup.getDpt().split("\\.")[0]); + try { + final DPTXlator translator = TranslatorTypes.createTranslator(mainNumber, knxGroup.getDpt()); + if (translator instanceof DPTXlatorBoolean) { + ((DPTXlatorBoolean) translator).setValue(Objects.equals(value, "true")); + } else if (translator instanceof DPTXlator8BitUnsigned) { + ((DPTXlator8BitUnsigned) translator).setValue(Integer.parseInt(value)); + } else { + translator.setValue(value); + } + return Optional.of(translator); + } catch (KNXException e) { + log.error(e.toString()); + return Optional.empty(); + } + } + } diff --git a/src/main/java/de/ph87/homeautomation/property/IPropertyOwner.java b/src/main/java/de/ph87/homeautomation/property/IPropertyOwner.java new file mode 100644 index 0000000..7cfef0c --- /dev/null +++ b/src/main/java/de/ph87/homeautomation/property/IPropertyOwner.java @@ -0,0 +1,11 @@ +package de.ph87.homeautomation.property; + +import java.util.regex.Pattern; + +public interface IPropertyOwner { + + Pattern getPropertyNamePattern(); + + void setProperty(final String propertyName, final String value); + +} diff --git a/src/main/java/de/ph87/homeautomation/property/PropertyService.java b/src/main/java/de/ph87/homeautomation/property/PropertyService.java new file mode 100644 index 0000000..7efdd9f --- /dev/null +++ b/src/main/java/de/ph87/homeautomation/property/PropertyService.java @@ -0,0 +1,27 @@ +package de.ph87.homeautomation.property; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.Optional; +import java.util.Set; + +@Slf4j +@Service +@RequiredArgsConstructor +public class PropertyService { + + private final Set propertyOwners; + + public void set(final String propertyName, final String value) { + log.info("Setting property \"{}\" => {}", propertyName, value); + final Optional iPropertyOwnerOptional = propertyOwners.stream().filter(iPropertyOwner -> iPropertyOwner.getPropertyNamePattern().matcher(propertyName).matches()).findFirst(); + if (iPropertyOwnerOptional.isEmpty()) { + log.error("No IPropertyOwner found for name: {}", propertyName); + return; + } + iPropertyOwnerOptional.get().setProperty(propertyName, value); + } + +} diff --git a/src/main/java/de/ph87/homeautomation/schedule/PropertyEntry.java b/src/main/java/de/ph87/homeautomation/schedule/PropertyEntry.java new file mode 100644 index 0000000..fcad0ea --- /dev/null +++ b/src/main/java/de/ph87/homeautomation/schedule/PropertyEntry.java @@ -0,0 +1,30 @@ +package de.ph87.homeautomation.schedule; + +import lombok.Data; +import tuwien.auto.calimero.GroupAddress; + +import java.util.Map; + +@Data +public class PropertyEntry implements Map.Entry { + + private final String key; + + private String value; + + public PropertyEntry(final int rawGroupAddress, final String value) { + this.key = "knx.group." + new GroupAddress(rawGroupAddress); + this.value = value; + } + + public PropertyEntry(final String propertyName, final String value) { + this.key = propertyName; + this.value = value; + } + + public String setValue(final String value) { + this.value = value; + return value; + } + +} diff --git a/src/main/java/de/ph87/homeautomation/schedule/Schedule.java b/src/main/java/de/ph87/homeautomation/schedule/Schedule.java new file mode 100644 index 0000000..2e46fe5 --- /dev/null +++ b/src/main/java/de/ph87/homeautomation/schedule/Schedule.java @@ -0,0 +1,33 @@ +package de.ph87.homeautomation.schedule; + +import de.ph87.homeautomation.schedule.entry.ScheduleEntry; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import javax.persistence.*; +import java.util.HashSet; +import java.util.Set; + +@Getter +@Setter +@ToString +@Entity +public class Schedule { + + @Id + @GeneratedValue + @Setter(AccessLevel.NONE) + private Long id; + + private boolean enabled; + + @Column(nullable = false, unique = true) + private String name; + + @ToString.Exclude + @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) + private Set entries = new HashSet<>(); + +} diff --git a/src/main/java/de/ph87/homeautomation/schedule/ScheduleRepository.java b/src/main/java/de/ph87/homeautomation/schedule/ScheduleRepository.java new file mode 100644 index 0000000..728511d --- /dev/null +++ b/src/main/java/de/ph87/homeautomation/schedule/ScheduleRepository.java @@ -0,0 +1,11 @@ +package de.ph87.homeautomation.schedule; + +import org.springframework.data.repository.CrudRepository; + +import java.util.List; + +public interface ScheduleRepository extends CrudRepository { + + List findAll(); + +} diff --git a/src/main/java/de/ph87/homeautomation/schedule/ScheduleService.java b/src/main/java/de/ph87/homeautomation/schedule/ScheduleService.java new file mode 100644 index 0000000..5e462a2 --- /dev/null +++ b/src/main/java/de/ph87/homeautomation/schedule/ScheduleService.java @@ -0,0 +1,197 @@ +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.property.PropertyService; +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.event.EventListener; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import javax.annotation.PostConstruct; +import javax.transaction.Transactional; +import java.time.Instant; +import java.time.ZonedDateTime; +import java.util.*; + +@Slf4j +@Service +@Transactional +@EnableScheduling +@RequiredArgsConstructor +public class ScheduleService { + + public static final int WOHNZIMMER_ROLLLADEN_POSITION_ANFAHREN_ADDRESS = 1048; + + public static final int SCHLAFZIMMER_ROLLLADEN_POSITION_ANFAHREN_ADDRESS = 771; + + public static final int FLUR_ROLLLADEN_POSITION_ANFAHREN_ADDRESS = 1293; + + private final Config config; + + private final ScheduleRepository scheduleRepository; + + private final PropertyService propertyService; + + @PostConstruct + public void postConstruct() { + final Schedule wohnzimmer = new Schedule(); + wohnzimmer.setEnabled(true); + wohnzimmer.setName("Rollläden Wohnzimmer"); + createSunrise(wohnzimmer, Zenith.OFFICIAL, new PropertyEntry(WOHNZIMMER_ROLLLADEN_POSITION_ANFAHREN_ADDRESS, "0")); + createSunset(wohnzimmer, Zenith.OFFICIAL, new PropertyEntry(WOHNZIMMER_ROLLLADEN_POSITION_ANFAHREN_ADDRESS, "100")); + scheduleRepository.save(wohnzimmer); + + final Schedule schlafzimmer = new Schedule(); + schlafzimmer.setEnabled(true); + schlafzimmer.setName("Rollläden Schlafzimmer"); + createTime(schlafzimmer, 7, 0, new PropertyEntry(SCHLAFZIMMER_ROLLLADEN_POSITION_ANFAHREN_ADDRESS, "0")); + createTime(schlafzimmer, 20, 0, new PropertyEntry(SCHLAFZIMMER_ROLLLADEN_POSITION_ANFAHREN_ADDRESS, "100")); + scheduleRepository.save(schlafzimmer); + + final Schedule flur = new Schedule(); + flur.setEnabled(true); + flur.setName("Rollläden Flur"); + createSunrise(flur, Zenith.NAUTICAL, new PropertyEntry(FLUR_ROLLLADEN_POSITION_ANFAHREN_ADDRESS, "0")); + createSunset(flur, Zenith.NAUTICAL, new PropertyEntry(FLUR_ROLLLADEN_POSITION_ANFAHREN_ADDRESS, "100")); + scheduleRepository.save(flur); + } + + private ScheduleEntry createTime(final Schedule schedule, final Map.Entry... entries) { + final ZonedDateTime dateTime = ZonedDateTime.now().plusMinutes(1).withSecond(0).withNano(0); + return create(schedule, ScheduleEntryType.TIME, null, dateTime.getHour(), dateTime.getMinute(), entries); + } + + private ScheduleEntry createTime(final Schedule schedule, final int hour, final int minute, final Map.Entry... entries) { + return create(schedule, ScheduleEntryType.TIME, null, hour, minute, entries); + } + + private ScheduleEntry createSunrise(final Schedule schedule, final Zenith zenith, final Map.Entry... entries) { + return create(schedule, ScheduleEntryType.SUNRISE, zenith, 0, 0, entries); + } + + private ScheduleEntry createSunset(final Schedule schedule, final Zenith zenith, final Map.Entry... entries) { + return create(schedule, ScheduleEntryType.SUNSET, zenith, 0, 0, entries); + } + + private ScheduleEntry create(final Schedule schedule, final ScheduleEntryType type, final Zenith zenith, final int hour, final int minute, final Map.Entry... entries) { + final ScheduleEntry entry = new ScheduleEntry(); + entry.setType(type); + if (zenith != null) { + entry.setZenith(zenith.degrees().doubleValue()); + } + entry.setHour(hour); + entry.setMinute(minute); + Arrays.stream(entries).forEach(p -> entry.getProperties().put(p.getKey(), p.getValue())); + schedule.getEntries().add(entry); + return entry; + } + + @EventListener(ApplicationStartedEvent.class) + public void startup() { + final ZonedDateTime now = ZonedDateTime.now(); + scheduleRepository.findAll().forEach(schedule -> schedule.getEntries().forEach(entry -> calculateNext(schedule, entry, now))); + } + + @Scheduled(initialDelay = 1000, fixedRate = 1000) + public void execute() { + final ZonedDateTime now = ZonedDateTime.now(); + scheduleRepository.findAll().forEach(schedule -> execute(schedule, now)); + } + + private void execute(final Schedule schedule, final ZonedDateTime now) { + schedule.getEntries().stream() + .filter(entry -> entry.getNextDateTime() != null && !entry.getNextDateTime().isAfter(now)) // TODO check nextTimestamp with an threshold of +/-30 sec (= half execution interval)!? + .max(Comparator.comparing(ScheduleEntry::getNextDateTime)) + .ifPresent(entry -> { + log.info("Executing ScheduleEntry {}", entry); + calculateNext(schedule, entry, now); + entry.getProperties().forEach(propertyService::set); + }); + } + + private void calculateNext(final Schedule schedule, final ScheduleEntry entry, final ZonedDateTime now) { + log.debug("calculateNext \"{}\", {}:", schedule.getName(), entry); + if (!entry.isEnabled() || !isAnyWeekdayEnabled(entry)) { + entry.setNextDateTime(null); + return; + } + ZonedDateTime midnight = now.withHour(0).withMinute(0).withSecond(0).withNano(0); + ZonedDateTime next = calculateNextForDay(entry, midnight); + log.debug(" - {}", next); + while (next != null && (!next.isAfter(now) || !isWeekdayValid(entry, next))) { // TODO check nextTimestamp with an threshold of +/-30 sec (= half execution interval)!? + midnight = midnight.plusDays(1); + next = calculateNextForDay(entry, midnight); + log.debug(" - {}", next); + } + log.debug("done"); + entry.setNextDateTime(next); + } + + private ZonedDateTime calculateNextForDay(final ScheduleEntry entry, final ZonedDateTime midnight) { + switch (entry.getType()) { + case TIME: + return midnight.withHour(entry.getHour()).withMinute(entry.getMinute()); + case SUNRISE: + case SUNSET: + return calculateNextModeAstro(entry, midnight); + default: + log.error("AstroEvent not implemented: {}", entry.getType()); + break; + } + return null; + } + + private ZonedDateTime calculateNextModeAstro(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 = calculateNextModeAstro(calculator, entry.getType(), new Zenith(entry.getZenith()), calendar); + if (nextCalendar == null) { + return null; + } + return ZonedDateTime.ofInstant(Instant.ofEpochMilli(nextCalendar.getTimeInMillis()), midnight.getZone()); + } + + private Calendar calculateNextModeAstro(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 isWeekdayValid(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; + } + +} diff --git a/src/main/java/de/ph87/homeautomation/schedule/entry/ScheduleEntry.java b/src/main/java/de/ph87/homeautomation/schedule/entry/ScheduleEntry.java new file mode 100644 index 0000000..13fe1f4 --- /dev/null +++ b/src/main/java/de/ph87/homeautomation/schedule/entry/ScheduleEntry.java @@ -0,0 +1,108 @@ +package de.ph87.homeautomation.schedule.entry; + +import com.luckycatlabs.sunrisesunset.Zenith; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; + +import javax.persistence.*; +import java.time.ZonedDateTime; +import java.util.HashMap; +import java.util.Map; + +@Getter +@Setter +@Entity +public class ScheduleEntry { + + @Id + @GeneratedValue + @Setter(AccessLevel.NONE) + private Long id; + + private boolean enabled = true; + + private boolean monday = true; + + private boolean tuesday = true; + + private boolean wednesday = true; + + private boolean thursday = true; + + private boolean friday = true; + + private boolean saturday = true; + + private boolean sunday = true; + + @Column(nullable = false) + private ScheduleEntryType type = null; + + private double zenith = Zenith.CIVIL.degrees().doubleValue(); + + private int hour; + + private int minute; + + private ZonedDateTime nextDateTime; + + @ElementCollection + private Map properties = new HashMap<>(); + + public void setWorkday(final boolean enabled) { + monday = enabled; + tuesday = enabled; + wednesday = enabled; + thursday = enabled; + friday = enabled; + } + + public void setWeekend(final boolean enabled) { + saturday = enabled; + sunday = enabled; + } + + public void setEveryday(final boolean enabled) { + setWorkday(enabled); + setWeekend(enabled); + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append(getClass().getSimpleName()); + builder.append("(id="); + builder.append(id); + builder.append(", enabled="); + builder.append(enabled); + builder.append(", type="); + builder.append(type); + builder.append(", weekdays="); + builder.append(monday ? 1 : 0); + builder.append(tuesday ? 1 : 0); + builder.append(wednesday ? 1 : 0); + builder.append(thursday ? 1 : 0); + builder.append(friday ? 1 : 0); + builder.append(saturday ? 1 : 0); + builder.append(sunday ? 1 : 0); + if (type != null) { + switch (type) { + case TIME: + builder.append(", hour="); + builder.append(hour); + builder.append(", minute="); + builder.append(minute); + break; + case SUNRISE: + case SUNSET: + builder.append(", zenith="); + builder.append(zenith); + break; + } + } + builder.append(")"); + return builder.toString(); + } + +} diff --git a/src/main/java/de/ph87/homeautomation/schedule/entry/ScheduleEntryType.java b/src/main/java/de/ph87/homeautomation/schedule/entry/ScheduleEntryType.java new file mode 100644 index 0000000..86b65b2 --- /dev/null +++ b/src/main/java/de/ph87/homeautomation/schedule/entry/ScheduleEntryType.java @@ -0,0 +1,5 @@ +package de.ph87.homeautomation.schedule.entry; + +public enum ScheduleEntryType { + TIME, SUNRISE, SUNSET +} diff --git a/src/main/java/de/ph87/network/router/Route.java b/src/main/java/de/ph87/network/router/Route.java new file mode 100644 index 0000000..d6ee64c --- /dev/null +++ b/src/main/java/de/ph87/network/router/Route.java @@ -0,0 +1,90 @@ +package de.ph87.network.router; + +import java.net.Inet4Address; +import java.net.UnknownHostException; +import java.nio.ByteBuffer; +import java.util.Objects; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class Route { + + private static final Pattern regex = Pattern.compile("^([\\d.]+)\\s+([\\d.]+)\\s+([\\d.]+)\\s+(\\S+)\\s+(\\d+)\\s+(\\d+)\\s+(\\d+)\\s+(\\S+)$"); + + public final Inet4Address network; + + public final Inet4Address router; + + public final Inet4Address netmask; + + public final int metric; + + public final String iface; + + public final int networkInt; + + public final int routerInt; + + public final int netmaskInt; + + public final Inet4Address addressFirst; + + public final int addressFirstInt; + + public final Inet4Address addressLast; + + public final int addressLastInt; + + public Route(final Inet4Address network, final Inet4Address router, final Inet4Address netmask, final int metric, final String iface) throws UnknownHostException { + this.network = network; + this.router = router; + this.netmask = netmask; + this.metric = metric; + this.iface = iface; + + this.networkInt = toInt(network); + this.routerInt = toInt(router); + this.netmaskInt = toInt(netmask); + this.addressFirstInt = networkInt & netmaskInt; + this.addressLastInt = networkInt | ~netmaskInt; + this.addressFirst = fromInt(addressFirstInt); + this.addressLast = fromInt(addressLastInt); + } + + public boolean matches(final Inet4Address address) { + final int addressInt = toInt(address); + return addressFirstInt <= addressInt && addressInt <= addressLastInt; + } + + private static int toInt(final Inet4Address address) { + return ByteBuffer.wrap(address.getAddress()).getInt(); + } + + private static Inet4Address fromInt(final int value) throws UnknownHostException { + return (Inet4Address) Inet4Address.getByAddress(ByteBuffer.allocate(4).putInt(value).array()); + } + + public static Optional parse(final String line) { + final Matcher matcher = regex.matcher(line); + try { + if (matcher.matches()) { + final String networkString = matcher.group(1); + final Inet4Address network = (Inet4Address) Inet4Address.getByName(Objects.equals(networkString, "default") ? "0.0.0.0" : networkString); + final Inet4Address router = (Inet4Address) Inet4Address.getByName(matcher.group(2)); + final Inet4Address netmask = (Inet4Address) Inet4Address.getByName(matcher.group(3)); + final int metric = Integer.parseInt(matcher.group(5)); + final String iface = matcher.group(8); + return Optional.of(new Route(network, router, netmask, metric, iface)); + } + } catch (UnknownHostException e) { + e.printStackTrace(); + } + return Optional.empty(); + } + + public boolean isDefault() { + return network.isAnyLocalAddress() && netmask.isAnyLocalAddress(); + } + +} diff --git a/src/main/java/de/ph87/network/router/Router.java b/src/main/java/de/ph87/network/router/Router.java new file mode 100644 index 0000000..1be9403 --- /dev/null +++ b/src/main/java/de/ph87/network/router/Router.java @@ -0,0 +1,42 @@ +package de.ph87.network.router; + +import java.io.IOException; +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class Router { + + public static InetAddress getLocalInetAddressForRemoteAddress(final Inet4Address remoteAddress) throws IOException { + final Route route = getRoute(remoteAddress); + final NetworkInterface networkInterface = NetworkInterface.getByName(route.iface); + return networkInterface.getInetAddresses().nextElement(); + } + + public static Route getRoute(final Inet4Address remoteAddress) throws IOException { + final List routes = execute(Route::parse, "route", "-n"); + final Optional routeOptional = routes.stream() + .filter(r -> !r.isDefault()) + .filter(r -> r.matches(remoteAddress)) + .reduce((a, b) -> a.metric < b.metric ? a : b) + .or(() -> routes.stream() + .filter(Route::isDefault) + .reduce((a, b) -> a.metric < b.metric ? a : b) + ); + if (routeOptional.isEmpty()) { + throw new IOException("No route found for remoteAddress: " + remoteAddress); + } + return routeOptional.get(); + } + + private static List execute(final Function> parser, final String... commands) throws IOException { + final String output = new String(new ProcessBuilder(commands).start().getInputStream().readAllBytes()); + return Arrays.stream(output.split("\\n")).map(parser).filter(Optional::isPresent).map(Optional::get).collect(Collectors.toList()); + } + +}