diff --git a/src/main/java/de/ph87/homeautomation/DemoDataService.java b/src/main/java/de/ph87/homeautomation/DemoDataService.java new file mode 100644 index 0000000..37535a1 --- /dev/null +++ b/src/main/java/de/ph87/homeautomation/DemoDataService.java @@ -0,0 +1,106 @@ +package de.ph87.homeautomation; + +import com.luckycatlabs.sunrisesunset.Zenith; +import de.ph87.homeautomation.knx.group.KnxGroupWriteService; +import de.ph87.homeautomation.schedule.PropertyEntry; +import de.ph87.homeautomation.schedule.Schedule; +import de.ph87.homeautomation.schedule.ScheduleRepository; +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.stereotype.Service; +import tuwien.auto.calimero.GroupAddress; + +import javax.annotation.PostConstruct; +import java.time.ZonedDateTime; +import java.util.Arrays; +import java.util.Map; + +@Slf4j +@Service +@RequiredArgsConstructor +public class DemoDataService { + + private static final GroupAddress WOHNZIMMER_ROLLLADEN_POSITION_ANFAHREN_ADDRESS = new GroupAddress(0, 4, 24); + + private static final GroupAddress SCHLAFZIMMER_ROLLLADEN_POSITION_ANFAHREN_ADDRESS = new GroupAddress(0, 3, 3); + + private static final GroupAddress FLUR_ROLLLADEN_POSITION_ANFAHREN_ADDRESS = new GroupAddress(0, 5, 13); + + private static final GroupAddress BADEWANNE_SCHALTEN = new GroupAddress(781); + + private static final GroupAddress BADEWANNE_STATUS = new GroupAddress(782); + + private final KnxGroupWriteService knxGroupWriteService; + + private final ScheduleRepository scheduleRepository; + + @PostConstruct + public void postConstruct() { + knxGroupWriteService.create("Wohnzimmer Rollladen Position Anfahren", WOHNZIMMER_ROLLLADEN_POSITION_ANFAHREN_ADDRESS, "5.001", false); + knxGroupWriteService.create("Schlafzimmer Rollladen Position Anfahren", SCHLAFZIMMER_ROLLLADEN_POSITION_ANFAHREN_ADDRESS, "5.001", false); + knxGroupWriteService.create("Flur Rollladen Position Anfahren", FLUR_ROLLLADEN_POSITION_ANFAHREN_ADDRESS, "5.001", false); + knxGroupWriteService.create("Badewanne Schalten", BADEWANNE_SCHALTEN, "1.001", false); + knxGroupWriteService.create("Badewanne Status", BADEWANNE_STATUS, "1.001", true); + + final Schedule wohnzimmer = new Schedule(); + 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.setName("Rollläden Schlafzimmer"); + createTime(schlafzimmer, 7, 0, 0, new PropertyEntry(SCHLAFZIMMER_ROLLLADEN_POSITION_ANFAHREN_ADDRESS, "0")); + createTime(schlafzimmer, 20, 0, 0, new PropertyEntry(SCHLAFZIMMER_ROLLLADEN_POSITION_ANFAHREN_ADDRESS, "100")); + scheduleRepository.save(schlafzimmer); + + final Schedule flur = new Schedule(); + 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); + +// final Schedule badewanne = new Schedule(); +// badewanne.setName("Badewanne"); +// int seconds = 2; +// createRelative(badewanne, seconds += 2, new PropertyEntry(BADEWANNE_SCHALTEN, "true")); +// createRelative(badewanne, seconds += 2, new PropertyEntry(BADEWANNE_SCHALTEN, "false")); +// createRelative(badewanne, seconds += 2, new PropertyEntry(BADEWANNE_SCHALTEN, "true")); +// createRelative(badewanne, seconds += 2, new PropertyEntry(BADEWANNE_SCHALTEN, "false")); +// scheduleRepository.save(badewanne); + } + + private ScheduleEntry createRelative(final Schedule schedule, final int inSeconds, final Map.Entry... entries) { + final ZonedDateTime now = ZonedDateTime.now().plusSeconds(inSeconds).withNano(0); + return create(schedule, ScheduleEntryType.TIME, null, now.getHour(), now.getMinute(), now.getSecond(), entries); + } + + private ScheduleEntry createTime(final Schedule schedule, final int hour, final int minute, final int second, final Map.Entry... entries) { + return create(schedule, ScheduleEntryType.TIME, null, hour, minute, second, entries); + } + + private ScheduleEntry createSunrise(final Schedule schedule, final Zenith zenith, final Map.Entry... entries) { + return create(schedule, ScheduleEntryType.SUNRISE, zenith, 0, 0, 0, entries); + } + + private ScheduleEntry createSunset(final Schedule schedule, final Zenith zenith, final Map.Entry... entries) { + return create(schedule, ScheduleEntryType.SUNSET, zenith, 0, 0, 0, entries); + } + + private ScheduleEntry create(final Schedule schedule, final ScheduleEntryType type, final Zenith zenith, final int hour, final int minute, final int second, 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); + entry.setSecond(second); + Arrays.stream(entries).forEach(p -> entry.getProperties().put(p.getKey(), p.getValue())); + schedule.getEntries().add(entry); + return entry; + } + +} diff --git a/src/main/java/de/ph87/homeautomation/device/DeviceReadService.java b/src/main/java/de/ph87/homeautomation/device/DeviceReadService.java index f839a97..df076e3 100644 --- a/src/main/java/de/ph87/homeautomation/device/DeviceReadService.java +++ b/src/main/java/de/ph87/homeautomation/device/DeviceReadService.java @@ -4,8 +4,8 @@ import de.ph87.homeautomation.property.PropertyService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; -import javax.transaction.Transactional; import java.util.List; import java.util.stream.Collectors; diff --git a/src/main/java/de/ph87/homeautomation/device/DeviceWriteService.java b/src/main/java/de/ph87/homeautomation/device/DeviceWriteService.java index 8f2cfa5..54cb2ac 100644 --- a/src/main/java/de/ph87/homeautomation/device/DeviceWriteService.java +++ b/src/main/java/de/ph87/homeautomation/device/DeviceWriteService.java @@ -4,9 +4,9 @@ import de.ph87.homeautomation.property.PropertyService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import javax.annotation.PostConstruct; -import javax.transaction.Transactional; @Slf4j @Service diff --git a/src/main/java/de/ph87/homeautomation/knx/KnxLinkService.java b/src/main/java/de/ph87/homeautomation/knx/KnxThreadService.java similarity index 55% rename from src/main/java/de/ph87/homeautomation/knx/KnxLinkService.java rename to src/main/java/de/ph87/homeautomation/knx/KnxThreadService.java index 6a89f75..f76c055 100644 --- a/src/main/java/de/ph87/homeautomation/knx/KnxLinkService.java +++ b/src/main/java/de/ph87/homeautomation/knx/KnxThreadService.java @@ -2,11 +2,10 @@ package de.ph87.homeautomation.knx; import de.ph87.homeautomation.knx.group.KnxGroupLinkService; import de.ph87.homeautomation.knx.group.KnxGroupWriteService; +import de.ph87.homeautomation.shared.AbstractThreadService; import de.ph87.network.router.Router; 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.CloseEvent; import tuwien.auto.calimero.DetachEvent; @@ -19,19 +18,16 @@ import tuwien.auto.calimero.process.ProcessCommunicatorImpl; 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; -import java.net.UnknownHostException; import java.time.Duration; import java.time.ZonedDateTime; @Slf4j @Service @RequiredArgsConstructor -public class KnxLinkService implements NetworkLinkListener, ProcessListener { +public class KnxThreadService extends AbstractThreadService implements NetworkLinkListener, ProcessListener { private final KnxGroupWriteService knxGroupWriteService; @@ -39,57 +35,58 @@ public class KnxLinkService implements NetworkLinkListener, ProcessListener { private Inet4Address remoteAddress = null; - private final Thread thread = new Thread(this::run, "knx-sync"); - - private boolean stop = false; - private KNXNetworkLinkIP link = null; private ProcessCommunicatorImpl processCommunicator = null; - private final Object lock = new Object(); + private final Object databaseAccessLock = new Object(); - @EventListener(ApplicationStartedEvent.class) - public void afterStartup() throws UnknownHostException { - remoteAddress = (Inet4Address) Inet4Address.getByName("10.0.0.102"); - thread.start(); + @Override + protected String getThreadName() { + return "KNX"; } - @PreDestroy - public void preDestroy() { - stop = true; + @Override + protected void doStart() throws Exception { + remoteAddress = (Inet4Address) Inet4Address.getByName("10.0.0.102"); + } + + @Override + protected long doStep() throws InterruptedException { + try { + if (link == null) { + connect(); + return -1; + } else { + return work(); + } + } catch (KNXException | IOException e) { + log.error(e.toString()); + if (link != null) { + link.close(); + link = null; + processCommunicator = null; + } + return 3000; + } + } + + @Override + protected void preStop() { final KNXNetworkLinkIP copy = link; if (copy != null) { copy.close(); } - synchronized (lock) { - lock.notifyAll(); - } } - private void run() { - try { - while (!stop) { - if (link == null) { - try { - connect(); - } catch (KNXException | IOException e) { - error(e); - } - } else { - work(); - } - } - } catch (InterruptedException e) { - // ignore - } finally { - log.info("KNX Thread terminated."); - } + @Override + protected void postStop() { + // nothing } private void connect() throws KNXException, InterruptedException, IOException { - log.debug("Connecting KNX link..."); - final InetAddress localAddress = Router.getLocalInetAddressForRemoteAddress(remoteAddress); + final Inet4Address localAddress = Router.getLocalInet4AddressForRemoteAddress(remoteAddress); + log.debug("Connecting KNX link: {} -> {}", localAddress, remoteAddress); link = KNXNetworkLinkIP.newTunnelingLink(new InetSocketAddress(localAddress, 0), new InetSocketAddress(remoteAddress, 3671), false, new TPSettings()); link.addLinkListener(this); processCommunicator = new ProcessCommunicatorImpl(link); @@ -97,54 +94,20 @@ public class KnxLinkService implements NetworkLinkListener, ProcessListener { log.info("KNX link established."); } - private void work() throws InterruptedException { - try { + private long work() throws InterruptedException, KNXException { + synchronized (databaseAccessLock) { if (!knxGroupLinkService.sendNext(processCommunicator) && !knxGroupLinkService.readNext(processCommunicator)) { final ZonedDateTime nextTimestamp = knxGroupLinkService.getNextTimestamp(); if (nextTimestamp == null) { - doWait(0); + return 0; } else { final long waitMs = Duration.between(ZonedDateTime.now(), nextTimestamp).toMillis(); if (waitMs > 0) { - doWait(waitMs); + return waitMs; } } } - } catch (KNXException e) { - error(e); - } - } - - private void error(final Exception e) throws InterruptedException { - log.error(e.toString()); - cleanUp(); - int ERROR_DELAY_MS = 3000; - doWait(ERROR_DELAY_MS); - } - - private void doWait(final long waitMs) throws InterruptedException { - synchronized (lock) { - log.debug("KNX Thread going to sleep{}...", waitMs > 0 ? " for " + waitMs + "ms" : ""); - if (waitMs > 0) { - lock.wait(waitMs); - } else { - lock.wait(); - } - log.debug("KNX Thread woke up."); - } - } - - private void cleanUp() { - if (link != null) { - link.close(); - link = null; - processCommunicator = null; - } - } - - public void notifyActionPending() { - synchronized (lock) { - lock.notifyAll(); + return -1; } } @@ -165,14 +128,16 @@ public class KnxLinkService implements NetworkLinkListener, ProcessListener { @Override public void groupReadResponse(final ProcessEvent processEvent) { - log.debug("{}", processEvent); - knxGroupWriteService.updateOrCreate(processEvent.getDestination().getRawAddress(), processEvent.getASDU()); + synchronized (databaseAccessLock) { + knxGroupWriteService.updateOrCreate(processEvent.getDestination().getRawAddress(), processEvent.getASDU()); + } } @Override public void groupWrite(final ProcessEvent processEvent) { - log.debug("{}", processEvent); - knxGroupWriteService.updateOrCreate(processEvent.getDestination().getRawAddress(), processEvent.getASDU()); + synchronized (databaseAccessLock) { + knxGroupWriteService.updateOrCreate(processEvent.getDestination().getRawAddress(), processEvent.getASDU()); + } } @Override diff --git a/src/main/java/de/ph87/homeautomation/knx/group/KnxGroup.java b/src/main/java/de/ph87/homeautomation/knx/group/KnxGroup.java index c30b446..e6a6d26 100644 --- a/src/main/java/de/ph87/homeautomation/knx/group/KnxGroup.java +++ b/src/main/java/de/ph87/homeautomation/knx/group/KnxGroup.java @@ -8,7 +8,6 @@ import tuwien.auto.calimero.GroupAddress; import javax.persistence.*; import java.math.BigDecimal; -import java.time.Duration; import java.time.ZonedDateTime; @Getter @@ -54,10 +53,6 @@ public class KnxGroup { setAddress(new GroupAddress(rawAddress)); } - public void setAddress(final int main, final int middle, final int sub) { - setAddress(new GroupAddress(main, middle, sub)); - } - public void setAddress(final GroupAddress groupAddress) { this.addressRaw = groupAddress.getRawAddress(); this.addressStr = groupAddress.toString(); @@ -67,25 +62,4 @@ public class KnxGroup { return new GroupAddress(addressRaw); } - public void setReadInterval(final int readInterval) { - if (readInterval <= 0) { - this.readInterval = 0; - } else { - this.readInterval = readInterval; - if (read.getNextTimestamp() == null || Duration.between(ZonedDateTime.now(), read.getNextTimestamp()).toSeconds() > this.readInterval) { - updateNextReadTimestamp(); - } - } - } - - public void updateNextReadTimestamp() { - if (read.getErrorCount() == 0) { - if (this.readInterval <= 0) { - read.setNextTimestamp(null); - } else { - read.setNextTimestamp(ZonedDateTime.now().plusSeconds(read.getNextTimestamp() == null ? 0 : this.readInterval)); - } - } - } - } diff --git a/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupFormatException.java b/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupFormatException.java new file mode 100644 index 0000000..ae010d7 --- /dev/null +++ b/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupFormatException.java @@ -0,0 +1,11 @@ +package de.ph87.homeautomation.knx.group; + +import static de.ph87.homeautomation.shared.Helpers.quoteOrNull; + +public class KnxGroupFormatException extends Exception { + + public KnxGroupFormatException(final KnxGroup knxGroup, final String value, final String reason) { + super(String.format("Cannot use value %s (%s) to set KnxGroup: %s", quoteOrNull(value), reason, knxGroup)); + } + +} diff --git a/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupLinkService.java b/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupLinkService.java index 958c823..83d824c 100644 --- a/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupLinkService.java +++ b/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupLinkService.java @@ -3,6 +3,7 @@ package de.ph87.homeautomation.knx.group; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import tuwien.auto.calimero.GroupAddress; import tuwien.auto.calimero.KNXException; import tuwien.auto.calimero.KNXFormatException; @@ -10,7 +11,8 @@ import tuwien.auto.calimero.datapoint.StateDP; import tuwien.auto.calimero.dptxlator.TranslatorTypes; import tuwien.auto.calimero.process.ProcessCommunicatorImpl; -import javax.transaction.Transactional; +import java.time.Instant; +import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.Optional; @@ -62,17 +64,28 @@ public class KnxGroupLinkService { processCommunicator.read(createStateDP(knxGroup)); knxGroup.getRead().setErrorCount(0); knxGroup.getRead().setErrorMessage(null); + if (knxGroup.getReadInterval() > 0) { + knxGroup.getRead().setNextTimestamp(align(knxGroup.getReadInterval())); + } else { + knxGroup.getRead().setNextTimestamp(null); + } return true; } catch (KNXFormatException e) { log.error(e.toString()); knxGroup.getRead().setErrorCount(knxGroup.getRead().getErrorCount() + 1); knxGroup.getRead().setErrorMessage(e.toString()); - } finally { - knxGroup.updateNextReadTimestamp(); + knxGroup.getRead().setNextTimestamp(ZonedDateTime.now().plusSeconds(knxGroup.getRead().getErrorCount())); } return false; } + private ZonedDateTime align(final int interval) { + final ZonedDateTime now = ZonedDateTime.now(); + final long nextEpochAlignment = (long) Math.ceil(now.toEpochSecond() / (double) interval) * interval; + final ZonedDateTime nextUTC = ZonedDateTime.ofInstant(Instant.ofEpochSecond(nextEpochAlignment, 0), ZoneId.of("Z")); + return nextUTC.withZoneSameInstant(now.getZone()); + } + private StateDP createStateDP(final KnxGroup knxGroup) { final GroupAddress groupAddress = knxGroup.getAddress(); final int mainNumber = Integer.parseInt(knxGroup.getDpt().split("\\.", 2)[0]); @@ -80,10 +93,21 @@ public class KnxGroupLinkService { } public ZonedDateTime getNextTimestamp() { - if (knxGroupRepository.findFirstBySend_NextTimestampNotNullOrderBySend_NextTimestampAsc().isPresent()) { - return ZonedDateTime.now(); + final Optional sendOptional = knxGroupRepository.findFirstBySend_NextTimestampNotNullOrderBySend_NextTimestampAsc().map(KnxGroup::getSend).map(ComInfo::getNextTimestamp); + final Optional readOptional = knxGroupRepository.findFirstByRead_NextTimestampNotNullOrderByRead_NextTimestampAsc().map(KnxGroup::getRead).map(ComInfo::getNextTimestamp); + if (sendOptional.isEmpty()) { + return readOptional.orElse(null); } - return knxGroupRepository.findFirstByRead_NextTimestampNotNullOrderByRead_NextTimestampAsc().map(KnxGroup::getRead).map(ComInfo::getNextTimestamp).orElse(null); + final ZonedDateTime send = sendOptional.get(); + if (readOptional.isEmpty()) { + return send; + } + final ZonedDateTime read = readOptional.get(); + if (send.isBefore(read)) { + return send; + } + return read; + } } diff --git a/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupSetService.java b/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupSetService.java index 620bd09..5cac6fc 100644 --- a/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupSetService.java +++ b/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupSetService.java @@ -1,7 +1,8 @@ package de.ph87.homeautomation.knx.group; -import de.ph87.homeautomation.knx.KnxLinkService; +import de.ph87.homeautomation.knx.KnxThreadService; import de.ph87.homeautomation.property.IPropertyOwner; +import de.ph87.homeautomation.property.PropertySetException; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -10,10 +11,11 @@ import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; import tuwien.auto.calimero.GroupAddress; -import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; +import static de.ph87.homeautomation.shared.Helpers.quoteOrNull; + @Slf4j @Service @RequiredArgsConstructor @@ -22,48 +24,43 @@ public class KnxGroupSetService implements IPropertyOwner { @Getter private final Pattern propertyNamePattern = Pattern.compile("^knx\\.group\\.(\\d+)/(\\d+)/(\\d+)$"); - private final KnxLinkService knxLinkService; + private final KnxThreadService knxThreadService; private final KnxGroupWriteService knxGroupWriteService; private final KnxGroupRepository knxGroupRepository; @EventListener(ApplicationStartedEvent.class) - public void applicationStarted() { - knxGroupWriteService.knxGroupCreate("Bad Dusche Status", 0, 3, 6, "1.001", true); - knxGroupWriteService.knxGroupCreate("Wohnzimmer Rollladen", 0, 4, 24, "5.001", false); - knxGroupWriteService.knxGroupCreate("Schlafzimmer Rollladen", 0, 3, 3, "5.001", false); - knxGroupWriteService.knxGroupCreate("Flur Rollladen", 0, 5, 13, "5.001", false); - requestAll(); - } - public void requestAll() { knxGroupWriteService.markAllForRead(); - knxLinkService.notifyActionPending(); + knxThreadService.notifyActionPending(); } @Override - public void setProperty(final String propertyName, final String value) { - findGroupAddress(propertyName).ifPresent(groupAddress -> { + public void setProperty(final String propertyName, final String value) throws PropertySetException { + final GroupAddress groupAddress = parseGroupAddress(propertyName); + try { if (knxGroupWriteService.setSendValue(groupAddress, value)) { - knxLinkService.notifyActionPending(); + knxThreadService.notifyActionPending(); } else { log.error("No such KnxGroup.address = {}", groupAddress); } - }); + } catch (KnxGroupFormatException e) { + throw new PropertySetException(propertyName, value, e); + } } @Override public Boolean readBoolean(final String propertyName) { - return findGroupAddress(propertyName).flatMap(groupAddress -> knxGroupRepository.findByAddressRaw(groupAddress.getRawAddress())).map(KnxGroup::getBooleanValue).orElse(null); + return knxGroupRepository.findByAddressRaw(parseGroupAddress(propertyName).getRawAddress()).map(KnxGroup::getBooleanValue).orElse(null); } - private Optional findGroupAddress(final String propertyName) { + private GroupAddress parseGroupAddress(final String propertyName) { final Matcher matcher = propertyNamePattern.matcher(propertyName); if (matcher.matches()) { - return Optional.of(new GroupAddress(Integer.parseInt(matcher.group(1)), Integer.parseInt(matcher.group(2)), Integer.parseInt(matcher.group(3)))); + return new GroupAddress(Integer.parseInt(matcher.group(1)), Integer.parseInt(matcher.group(2)), Integer.parseInt(matcher.group(3))); } - return Optional.empty(); + throw new RuntimeException("Cannot parse GroupAddress from propertyName: " + quoteOrNull(propertyName)); } } 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 16d7b8d..af0e240 100644 --- a/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupWriteService.java +++ b/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupWriteService.java @@ -3,6 +3,8 @@ package de.ph87.homeautomation.knx.group; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; import tuwien.auto.calimero.GroupAddress; import tuwien.auto.calimero.KNXException; import tuwien.auto.calimero.KNXFormatException; @@ -11,14 +13,13 @@ 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.TxType.REQUIRES_NEW) +@Transactional(propagation = Propagation.REQUIRES_NEW) @RequiredArgsConstructor public class KnxGroupWriteService { @@ -27,16 +28,19 @@ public class KnxGroupWriteService { public void updateOrCreate(final int rawAddress, final byte[] data) { final KnxGroup knxGroup = getOrCreate(rawAddress); knxGroup.setValue(data); + knxGroup.setValueTimestamp(ZonedDateTime.now()); knxGroup.setBooleanValue(null); knxGroup.setNumberValue(null); - findTranslator(knxGroup).ifPresent(translator -> { + try { + final DPTXlator translator = findTranslator(knxGroup); translator.setData(data); if (translator instanceof DPTXlatorBoolean) { knxGroup.setBooleanValue(((DPTXlatorBoolean) translator).getValueBoolean()); } // TODO implement all DPTXlator... - }); - knxGroup.setValueTimestamp(ZonedDateTime.now()); + } catch (NoTranslatorException e) { + log.error(e.getMessage()); + } log.debug("KnxGroup updated: {}", knxGroup); } @@ -54,30 +58,35 @@ public class KnxGroupWriteService { knxGroupRepository.findAllByRead_AbleTrue().forEach(knxGroup -> knxGroup.getRead().setNextTimestamp(ZonedDateTime.now())); } - public void knxGroupCreate(final String name, final int main, final int middle, final int sub, final String dpt, final boolean readable) { + public void create(final String name, final int main, final int middle, final int sub, final String dpt, final boolean readable) { + create(name, new GroupAddress(main, middle, sub), dpt, readable); + } + + public void create(final String name, final GroupAddress address, final String dpt, final boolean readable) { final KnxGroup trans = new KnxGroup(); - trans.setAddress(main, middle, sub); + trans.setAddress(address); trans.setDpt(dpt); trans.setName(name); trans.getRead().setAble(readable); knxGroupRepository.save(trans); } - public boolean setSendValue(final GroupAddress groupAddress, final String value) { + public boolean setSendValue(final GroupAddress groupAddress, final String value) throws KnxGroupFormatException { final Optional knxGroupOptional = knxGroupRepository.findByAddressRaw(groupAddress.getRawAddress()); if (knxGroupOptional.isEmpty()) { return false; } final KnxGroup knxGroup = knxGroupOptional.get(); - final Optional translatorOptional = findTranslator(knxGroup); - if (translatorOptional.isEmpty()) { - return false; - } - final DPTXlator translator = translatorOptional.get(); try { + final DPTXlator translator = findTranslator(knxGroup); if (translator instanceof DPTXlatorBoolean) { - ((DPTXlatorBoolean) translator).setValue(Objects.equals(value, "true")); + final boolean isTrue = Objects.equals(value, "true"); + final boolean isFalse = Objects.equals(value, "false"); + if (!isTrue && !isFalse) { + throw new KnxGroupFormatException(knxGroup, value, "Must be \"true\" or \"false\"."); + } + ((DPTXlatorBoolean) translator).setValue(isTrue); } else if (translator instanceof DPTXlator8BitUnsigned) { ((DPTXlator8BitUnsigned) translator).setValue(Integer.parseInt(value)); } else { // TODO implement all DPTXlator... @@ -86,19 +95,20 @@ public class KnxGroupWriteService { knxGroup.setSendValue(translator.getData()); knxGroup.getSend().setNextTimestamp(ZonedDateTime.now()); return true; - } catch (KNXFormatException e) { - log.error(e.toString()); + } catch (NoTranslatorException | KNXFormatException e) { + throw new KnxGroupFormatException(knxGroup, value, e.getMessage()); } - return false; } - private Optional findTranslator(final KnxGroup knxGroup) { + private DPTXlator findTranslator(final KnxGroup knxGroup) throws NoTranslatorException { + if (knxGroup.getDpt() == null) { + throw new NoTranslatorException("Missing DPT"); + } final int mainNumber = Integer.parseInt(knxGroup.getDpt().split("\\.")[0]); try { - return Optional.of(TranslatorTypes.createTranslator(mainNumber, knxGroup.getDpt())); + return TranslatorTypes.createTranslator(mainNumber, knxGroup.getDpt()); } catch (KNXException e) { - log.error(e.toString()); - return Optional.empty(); + throw new NoTranslatorException(e); } } diff --git a/src/main/java/de/ph87/homeautomation/knx/group/NoTranslatorException.java b/src/main/java/de/ph87/homeautomation/knx/group/NoTranslatorException.java new file mode 100644 index 0000000..58bdb3c --- /dev/null +++ b/src/main/java/de/ph87/homeautomation/knx/group/NoTranslatorException.java @@ -0,0 +1,15 @@ +package de.ph87.homeautomation.knx.group; + +import tuwien.auto.calimero.KNXException; + +public class NoTranslatorException extends Exception { + + public NoTranslatorException(final String message) { + super("Cannot create translator: " + message); + } + + public NoTranslatorException(final KNXException e) { + super("Cannot create translator: " + e.toString()); + } + +} diff --git a/src/main/java/de/ph87/homeautomation/property/IPropertyOwner.java b/src/main/java/de/ph87/homeautomation/property/IPropertyOwner.java index cc893a3..6d1c890 100644 --- a/src/main/java/de/ph87/homeautomation/property/IPropertyOwner.java +++ b/src/main/java/de/ph87/homeautomation/property/IPropertyOwner.java @@ -6,7 +6,7 @@ public interface IPropertyOwner { Pattern getPropertyNamePattern(); - void setProperty(final String propertyName, final String value); + void setProperty(final String propertyName, final String value) throws PropertySetException; Boolean readBoolean(String propertyName); diff --git a/src/main/java/de/ph87/homeautomation/property/PropertyService.java b/src/main/java/de/ph87/homeautomation/property/PropertyService.java index d9bb600..d5daf6f 100644 --- a/src/main/java/de/ph87/homeautomation/property/PropertyService.java +++ b/src/main/java/de/ph87/homeautomation/property/PropertyService.java @@ -13,7 +13,7 @@ public class PropertyService { private final Set propertyOwners; - public void set(final String propertyName, final String value) { + public void set(final String propertyName, final String value) throws PropertySetException { log.debug("Setting property \"{}\" => {}", propertyName, value); getOwnerOrThrow(propertyName).setProperty(propertyName, value); } diff --git a/src/main/java/de/ph87/homeautomation/property/PropertySetException.java b/src/main/java/de/ph87/homeautomation/property/PropertySetException.java new file mode 100644 index 0000000..3094304 --- /dev/null +++ b/src/main/java/de/ph87/homeautomation/property/PropertySetException.java @@ -0,0 +1,9 @@ +package de.ph87.homeautomation.property; + +public class PropertySetException extends Exception { + + public PropertySetException(final String propertyName, final String value, final Exception e) { + super(String.format("Failed to set property %s to value %s: %s", propertyName, value, e.getMessage())); + } + +} diff --git a/src/main/java/de/ph87/homeautomation/schedule/Schedule.java b/src/main/java/de/ph87/homeautomation/schedule/Schedule.java index 2e46fe5..d86c799 100644 --- a/src/main/java/de/ph87/homeautomation/schedule/Schedule.java +++ b/src/main/java/de/ph87/homeautomation/schedule/Schedule.java @@ -21,7 +21,7 @@ public class Schedule { @Setter(AccessLevel.NONE) private Long id; - private boolean enabled; + private boolean enabled = true; @Column(nullable = false, unique = true) private String name; diff --git a/src/main/java/de/ph87/homeautomation/schedule/ScheduleService.java b/src/main/java/de/ph87/homeautomation/schedule/ScheduleService.java deleted file mode 100644 index 4ba29c4..0000000 --- a/src/main/java/de/ph87/homeautomation/schedule/ScheduleService.java +++ /dev/null @@ -1,198 +0,0 @@ -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 tuwien.auto.calimero.GroupAddress; - -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 { - - private static final GroupAddress WOHNZIMMER_ROLLLADEN_POSITION_ANFAHREN_ADDRESS = new GroupAddress(0, 4, 24); - - private static final GroupAddress SCHLAFZIMMER_ROLLLADEN_POSITION_ANFAHREN_ADDRESS = new GroupAddress(0, 3, 3); - - private static final GroupAddress FLUR_ROLLLADEN_POSITION_ANFAHREN_ADDRESS = new GroupAddress(0, 5, 13); - - 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/ScheduleThreadService.java b/src/main/java/de/ph87/homeautomation/schedule/ScheduleThreadService.java new file mode 100644 index 0000000..f75513e --- /dev/null +++ b/src/main/java/de/ph87/homeautomation/schedule/ScheduleThreadService.java @@ -0,0 +1,44 @@ +package de.ph87.homeautomation.schedule; + +import de.ph87.homeautomation.shared.AbstractThreadService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.time.Duration; +import java.time.ZonedDateTime; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ScheduleThreadService extends AbstractThreadService { + + private final ScheduleWriteService scheduleWriteService; + + @Override + protected String getThreadName() { + return "SCHEDULER"; + } + + @Override + protected void doStart() throws Exception { + scheduleWriteService.calculateAllNext(); + } + + @Override + protected long doStep() throws InterruptedException { + scheduleWriteService.executeAllDue(); + return scheduleWriteService.getOverallNextTimestamp().map(nextTimestamp -> Duration.between(ZonedDateTime.now(), nextTimestamp).toMillis()).orElse(0L); + } + + @Override + protected void preStop() { + // nothing + } + + @Override + protected void postStop() { + // nothing + } + +} diff --git a/src/main/java/de/ph87/homeautomation/schedule/ScheduleWriteService.java b/src/main/java/de/ph87/homeautomation/schedule/ScheduleWriteService.java new file mode 100644 index 0000000..c23d8a8 --- /dev/null +++ b/src/main/java/de/ph87/homeautomation/schedule/ScheduleWriteService.java @@ -0,0 +1,149 @@ +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.property.PropertySetException; +import de.ph87.homeautomation.schedule.entry.ScheduleEntry; +import de.ph87.homeautomation.schedule.entry.ScheduleEntryRepository; +import de.ph87.homeautomation.schedule.entry.ScheduleEntryType; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.EnableScheduling; +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 +@EnableScheduling +@RequiredArgsConstructor +public class ScheduleWriteService { + + private final Config config; + + private final ScheduleRepository scheduleRepository; + + private final PropertyService propertyService; + + private final ScheduleEntryRepository scheduleEntryRepository; + + public void calculateAllNext() { + final ZonedDateTime now = ZonedDateTime.now(); + scheduleRepository.findAll().forEach(schedule -> schedule.getEntries().forEach(entry -> calculateNext(schedule, entry, now))); + } + + public void executeAllDue() { + final ZonedDateTime now = ZonedDateTime.now(); + scheduleRepository.findAll().forEach(schedule -> executeIfDue(schedule, now)); + } + + private void executeIfDue(final Schedule schedule, final ZonedDateTime now) { + schedule.getEntries().stream() + .filter(entry -> entry.getNextDateTime() != null && !entry.getNextDateTime().isAfter(now)) + .max(Comparator.comparing(ScheduleEntry::getNextDateTime)) + .ifPresent(entry -> { + log.info("Executing ScheduleEntry {}", entry); + calculateNext(schedule, entry, now); + entry.getProperties().forEach(this::applyPropertyMapEntry); + }); + } + + private void applyPropertyMapEntry(final String propertyName, final String value) { + try { + propertyService.set(propertyName, value); + } catch (PropertySetException e) { + log.error(e.getMessage()); + } + } + + 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 = nextForDay(entry, midnight); + while (next != null && (!next.isAfter(now) || !isWeekdayValid(entry, next))) { + log.debug(" -- skipping: {}", next); + midnight = midnight.plusDays(1); + next = nextForDay(entry, midnight); + } + log.debug(" => {}", next); + entry.setNextDateTime(next); + } + + private ZonedDateTime nextForDay(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 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; + } + + public Optional getOverallNextTimestamp() { + return scheduleEntryRepository.findFirstNextDateTimeByNextDateTimeNotNullOrderByNextDateTimeAsc().map(ScheduleEntry::getNextDateTime); + } + +} diff --git a/src/main/java/de/ph87/homeautomation/schedule/entry/ScheduleEntry.java b/src/main/java/de/ph87/homeautomation/schedule/entry/ScheduleEntry.java index 13fe1f4..f6792de 100644 --- a/src/main/java/de/ph87/homeautomation/schedule/entry/ScheduleEntry.java +++ b/src/main/java/de/ph87/homeautomation/schedule/entry/ScheduleEntry.java @@ -45,6 +45,8 @@ public class ScheduleEntry { private int minute; + private int second; + private ZonedDateTime nextDateTime; @ElementCollection @@ -93,6 +95,8 @@ public class ScheduleEntry { builder.append(hour); builder.append(", minute="); builder.append(minute); + builder.append(", second="); + builder.append(second); break; case SUNRISE: case SUNSET: diff --git a/src/main/java/de/ph87/homeautomation/schedule/entry/ScheduleEntryRepository.java b/src/main/java/de/ph87/homeautomation/schedule/entry/ScheduleEntryRepository.java new file mode 100644 index 0000000..8e63227 --- /dev/null +++ b/src/main/java/de/ph87/homeautomation/schedule/entry/ScheduleEntryRepository.java @@ -0,0 +1,11 @@ +package de.ph87.homeautomation.schedule.entry; + +import org.springframework.data.repository.CrudRepository; + +import java.util.Optional; + +public interface ScheduleEntryRepository extends CrudRepository { + + Optional findFirstNextDateTimeByNextDateTimeNotNullOrderByNextDateTimeAsc(); + +} diff --git a/src/main/java/de/ph87/homeautomation/shared/AbstractThreadService.java b/src/main/java/de/ph87/homeautomation/shared/AbstractThreadService.java new file mode 100644 index 0000000..17883aa --- /dev/null +++ b/src/main/java/de/ph87/homeautomation/shared/AbstractThreadService.java @@ -0,0 +1,97 @@ +package de.ph87.homeautomation.shared; + +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 javax.annotation.PreDestroy; + +import static de.ph87.homeautomation.shared.Helpers.dhms; + +@Slf4j +@Service +@RequiredArgsConstructor +public abstract class AbstractThreadService { + + private final Thread thread = new Thread(this::run, getThreadName()); + + private boolean stop = false; + + private final Object lock = new Object(); + + private boolean stopped = false; + + protected abstract String getThreadName(); + + protected abstract void doStart() throws Exception; + + protected abstract long doStep() throws InterruptedException; + + protected abstract void preStop(); + + protected abstract void postStop(); + + @EventListener(ApplicationStartedEvent.class) + public void afterStartup() { + thread.start(); + } + + @PreDestroy + public void preDestroy() { + log.debug("{} stopping...", getThreadName()); + stop = true; + preStop(); + synchronized (lock) { + lock.notifyAll(); + try { + while (!stopped) { + lock.wait(); + } + } catch (InterruptedException e) { + log.error("{}: {}", getThreadName(), e.toString()); + } + } + } + + private void run() { + log.info("{} started.", getThreadName()); + try { + doStart(); + while (!stop) { + + doWait(doStep()); + } + } catch (InterruptedException e) { + // ignore + } catch (Exception e) { + log.error("{} failed to start.", getThreadName(), e); + } finally { + postStop(); + log.info("{} terminated.", getThreadName()); + synchronized (lock) { + lock.notifyAll(); + stopped = true; + } + } + } + + private void doWait(final long waitMs) throws InterruptedException { + if (waitMs < 0) { + return; + } + synchronized (lock) { + log.debug("{} going to sleep{}...", getThreadName(), waitMs > 0 ? " for " + dhms(waitMs) : ""); + lock.wait(waitMs); + log.debug("{} woke up.", getThreadName()); + } + } + + public void notifyActionPending() { + synchronized (lock) { + lock.notifyAll(); + } + } + +} diff --git a/src/main/java/de/ph87/homeautomation/shared/Helpers.java b/src/main/java/de/ph87/homeautomation/shared/Helpers.java new file mode 100644 index 0000000..7be9204 --- /dev/null +++ b/src/main/java/de/ph87/homeautomation/shared/Helpers.java @@ -0,0 +1,33 @@ +package de.ph87.homeautomation.shared; + +public class Helpers { + + public static String quoteOrNull(final String value) { + if (value == null) { + return null; + } + return "\"" + value + "\""; + } + + public static String dhms(long milliseconds) { + final long seconds = milliseconds / 1000; + final long minutes = seconds / 60; + final long hours = minutes / 60; + final long days = hours / 24; + String result = milliseconds % 1000 + "ms"; + if (seconds > 0) { + result = seconds % 60 + "s " + result; + if (minutes > 0) { + result = minutes % 60 + "m " + result; + if (hours > 0) { + result = hours % 60 + "h " + result; + if (days > 0) { + result = days + "d " + result; + } + } + } + } + return result; + } + +} diff --git a/src/main/java/de/ph87/homeautomation/web/WebSocketConfig.java b/src/main/java/de/ph87/homeautomation/web/WebSocketConfig.java index 33baeb2..5d5e3c8 100644 --- a/src/main/java/de/ph87/homeautomation/web/WebSocketConfig.java +++ b/src/main/java/de/ph87/homeautomation/web/WebSocketConfig.java @@ -1,4 +1,4 @@ -package de.ph87.office.web; +package de.ph87.homeautomation.web; import org.springframework.context.annotation.Configuration; import org.springframework.messaging.simp.config.MessageBrokerRegistry; diff --git a/src/main/java/de/ph87/homeautomation/web/WebSocketService.java b/src/main/java/de/ph87/homeautomation/web/WebSocketService.java index 2669f03..8b30ca5 100644 --- a/src/main/java/de/ph87/homeautomation/web/WebSocketService.java +++ b/src/main/java/de/ph87/homeautomation/web/WebSocketService.java @@ -1,5 +1,6 @@ package de.ph87.office.web; +import de.ph87.homeautomation.web.WebSocketConfig; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.lang.NonNull; diff --git a/src/main/java/de/ph87/network/router/Router.java b/src/main/java/de/ph87/network/router/Router.java index 1be9403..565e19d 100644 --- a/src/main/java/de/ph87/network/router/Router.java +++ b/src/main/java/de/ph87/network/router/Router.java @@ -2,7 +2,6 @@ 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; @@ -12,10 +11,10 @@ import java.util.stream.Collectors; public class Router { - public static InetAddress getLocalInetAddressForRemoteAddress(final Inet4Address remoteAddress) throws IOException { + public static Inet4Address getLocalInet4AddressForRemoteAddress(final Inet4Address remoteAddress) throws IOException { final Route route = getRoute(remoteAddress); final NetworkInterface networkInterface = NetworkInterface.getByName(route.iface); - return networkInterface.getInetAddresses().nextElement(); + return (Inet4Address) networkInterface.inetAddresses().filter(address -> address instanceof Inet4Address).findFirst().orElse(null); } public static Route getRoute(final Inet4Address remoteAddress) throws IOException {