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());
+ }
+
+}