A lot fixing, cleaning, refactoring in: Schedule, KnxLink

This commit is contained in:
Patrick Haßel 2021-10-03 14:52:47 +02:00
parent 7562f1426d
commit fc7058c0e0
24 changed files with 615 additions and 364 deletions

View File

@ -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<String, String>... 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<String, String>... entries) {
return create(schedule, ScheduleEntryType.TIME, null, hour, minute, second, entries);
}
private ScheduleEntry createSunrise(final Schedule schedule, final Zenith zenith, final Map.Entry<String, String>... entries) {
return create(schedule, ScheduleEntryType.SUNRISE, zenith, 0, 0, 0, entries);
}
private ScheduleEntry createSunset(final Schedule schedule, final Zenith zenith, final Map.Entry<String, String>... 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<String, String>... 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;
}
}

View File

@ -4,8 +4,8 @@ import de.ph87.homeautomation.property.PropertyService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.transaction.Transactional;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;

View File

@ -4,9 +4,9 @@ import de.ph87.homeautomation.property.PropertyService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
import javax.transaction.Transactional;
@Slf4j @Slf4j
@Service @Service

View File

@ -2,11 +2,10 @@ package de.ph87.homeautomation.knx;
import de.ph87.homeautomation.knx.group.KnxGroupLinkService; import de.ph87.homeautomation.knx.group.KnxGroupLinkService;
import de.ph87.homeautomation.knx.group.KnxGroupWriteService; import de.ph87.homeautomation.knx.group.KnxGroupWriteService;
import de.ph87.homeautomation.shared.AbstractThreadService;
import de.ph87.network.router.Router; import de.ph87.network.router.Router;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import tuwien.auto.calimero.CloseEvent; import tuwien.auto.calimero.CloseEvent;
import tuwien.auto.calimero.DetachEvent; 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.ProcessEvent;
import tuwien.auto.calimero.process.ProcessListener; import tuwien.auto.calimero.process.ProcessListener;
import javax.annotation.PreDestroy;
import java.io.IOException; import java.io.IOException;
import java.net.Inet4Address; import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.time.Duration; import java.time.Duration;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
@Slf4j @Slf4j
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class KnxLinkService implements NetworkLinkListener, ProcessListener { public class KnxThreadService extends AbstractThreadService implements NetworkLinkListener, ProcessListener {
private final KnxGroupWriteService knxGroupWriteService; private final KnxGroupWriteService knxGroupWriteService;
@ -39,57 +35,58 @@ public class KnxLinkService implements NetworkLinkListener, ProcessListener {
private Inet4Address remoteAddress = null; private Inet4Address remoteAddress = null;
private final Thread thread = new Thread(this::run, "knx-sync");
private boolean stop = false;
private KNXNetworkLinkIP link = null; private KNXNetworkLinkIP link = null;
private ProcessCommunicatorImpl processCommunicator = null; private ProcessCommunicatorImpl processCommunicator = null;
private final Object lock = new Object(); private final Object databaseAccessLock = new Object();
@EventListener(ApplicationStartedEvent.class) @Override
public void afterStartup() throws UnknownHostException { protected String getThreadName() {
remoteAddress = (Inet4Address) Inet4Address.getByName("10.0.0.102"); return "KNX";
thread.start();
} }
@PreDestroy @Override
public void preDestroy() { protected void doStart() throws Exception {
stop = true; 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; final KNXNetworkLinkIP copy = link;
if (copy != null) { if (copy != null) {
copy.close(); copy.close();
} }
synchronized (lock) {
lock.notifyAll();
}
} }
private void run() { @Override
try { protected void postStop() {
while (!stop) { // nothing
if (link == null) {
try {
connect();
} catch (KNXException | IOException e) {
error(e);
}
} else {
work();
}
}
} catch (InterruptedException e) {
// ignore
} finally {
log.info("KNX Thread terminated.");
}
} }
private void connect() throws KNXException, InterruptedException, IOException { private void connect() throws KNXException, InterruptedException, IOException {
log.debug("Connecting KNX link..."); final Inet4Address localAddress = Router.getLocalInet4AddressForRemoteAddress(remoteAddress);
final InetAddress localAddress = Router.getLocalInetAddressForRemoteAddress(remoteAddress); log.debug("Connecting KNX link: {} -> {}", localAddress, remoteAddress);
link = KNXNetworkLinkIP.newTunnelingLink(new InetSocketAddress(localAddress, 0), new InetSocketAddress(remoteAddress, 3671), false, new TPSettings()); link = KNXNetworkLinkIP.newTunnelingLink(new InetSocketAddress(localAddress, 0), new InetSocketAddress(remoteAddress, 3671), false, new TPSettings());
link.addLinkListener(this); link.addLinkListener(this);
processCommunicator = new ProcessCommunicatorImpl(link); processCommunicator = new ProcessCommunicatorImpl(link);
@ -97,54 +94,20 @@ public class KnxLinkService implements NetworkLinkListener, ProcessListener {
log.info("KNX link established."); log.info("KNX link established.");
} }
private void work() throws InterruptedException { private long work() throws InterruptedException, KNXException {
try { synchronized (databaseAccessLock) {
if (!knxGroupLinkService.sendNext(processCommunicator) && !knxGroupLinkService.readNext(processCommunicator)) { if (!knxGroupLinkService.sendNext(processCommunicator) && !knxGroupLinkService.readNext(processCommunicator)) {
final ZonedDateTime nextTimestamp = knxGroupLinkService.getNextTimestamp(); final ZonedDateTime nextTimestamp = knxGroupLinkService.getNextTimestamp();
if (nextTimestamp == null) { if (nextTimestamp == null) {
doWait(0); return 0;
} else { } else {
final long waitMs = Duration.between(ZonedDateTime.now(), nextTimestamp).toMillis(); final long waitMs = Duration.between(ZonedDateTime.now(), nextTimestamp).toMillis();
if (waitMs > 0) { if (waitMs > 0) {
doWait(waitMs); return waitMs;
} }
} }
} }
} catch (KNXException e) { return -1;
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();
} }
} }
@ -165,14 +128,16 @@ public class KnxLinkService implements NetworkLinkListener, ProcessListener {
@Override @Override
public void groupReadResponse(final ProcessEvent processEvent) { public void groupReadResponse(final ProcessEvent processEvent) {
log.debug("{}", processEvent); synchronized (databaseAccessLock) {
knxGroupWriteService.updateOrCreate(processEvent.getDestination().getRawAddress(), processEvent.getASDU()); knxGroupWriteService.updateOrCreate(processEvent.getDestination().getRawAddress(), processEvent.getASDU());
}
} }
@Override @Override
public void groupWrite(final ProcessEvent processEvent) { public void groupWrite(final ProcessEvent processEvent) {
log.debug("{}", processEvent); synchronized (databaseAccessLock) {
knxGroupWriteService.updateOrCreate(processEvent.getDestination().getRawAddress(), processEvent.getASDU()); knxGroupWriteService.updateOrCreate(processEvent.getDestination().getRawAddress(), processEvent.getASDU());
}
} }
@Override @Override

View File

@ -8,7 +8,6 @@ import tuwien.auto.calimero.GroupAddress;
import javax.persistence.*; import javax.persistence.*;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.Duration;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
@Getter @Getter
@ -54,10 +53,6 @@ public class KnxGroup {
setAddress(new GroupAddress(rawAddress)); 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) { public void setAddress(final GroupAddress groupAddress) {
this.addressRaw = groupAddress.getRawAddress(); this.addressRaw = groupAddress.getRawAddress();
this.addressStr = groupAddress.toString(); this.addressStr = groupAddress.toString();
@ -67,25 +62,4 @@ public class KnxGroup {
return new GroupAddress(addressRaw); 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));
}
}
}
} }

View File

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

View File

@ -3,6 +3,7 @@ package de.ph87.homeautomation.knx.group;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import tuwien.auto.calimero.GroupAddress; import tuwien.auto.calimero.GroupAddress;
import tuwien.auto.calimero.KNXException; import tuwien.auto.calimero.KNXException;
import tuwien.auto.calimero.KNXFormatException; 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.dptxlator.TranslatorTypes;
import tuwien.auto.calimero.process.ProcessCommunicatorImpl; import tuwien.auto.calimero.process.ProcessCommunicatorImpl;
import javax.transaction.Transactional; import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.util.Optional; import java.util.Optional;
@ -62,17 +64,28 @@ public class KnxGroupLinkService {
processCommunicator.read(createStateDP(knxGroup)); processCommunicator.read(createStateDP(knxGroup));
knxGroup.getRead().setErrorCount(0); knxGroup.getRead().setErrorCount(0);
knxGroup.getRead().setErrorMessage(null); knxGroup.getRead().setErrorMessage(null);
if (knxGroup.getReadInterval() > 0) {
knxGroup.getRead().setNextTimestamp(align(knxGroup.getReadInterval()));
} else {
knxGroup.getRead().setNextTimestamp(null);
}
return true; return true;
} catch (KNXFormatException e) { } catch (KNXFormatException e) {
log.error(e.toString()); log.error(e.toString());
knxGroup.getRead().setErrorCount(knxGroup.getRead().getErrorCount() + 1); knxGroup.getRead().setErrorCount(knxGroup.getRead().getErrorCount() + 1);
knxGroup.getRead().setErrorMessage(e.toString()); knxGroup.getRead().setErrorMessage(e.toString());
} finally { knxGroup.getRead().setNextTimestamp(ZonedDateTime.now().plusSeconds(knxGroup.getRead().getErrorCount()));
knxGroup.updateNextReadTimestamp();
} }
return false; 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) { private StateDP createStateDP(final KnxGroup knxGroup) {
final GroupAddress groupAddress = knxGroup.getAddress(); final GroupAddress groupAddress = knxGroup.getAddress();
final int mainNumber = Integer.parseInt(knxGroup.getDpt().split("\\.", 2)[0]); final int mainNumber = Integer.parseInt(knxGroup.getDpt().split("\\.", 2)[0]);
@ -80,10 +93,21 @@ public class KnxGroupLinkService {
} }
public ZonedDateTime getNextTimestamp() { public ZonedDateTime getNextTimestamp() {
if (knxGroupRepository.findFirstBySend_NextTimestampNotNullOrderBySend_NextTimestampAsc().isPresent()) { final Optional<ZonedDateTime> sendOptional = knxGroupRepository.findFirstBySend_NextTimestampNotNullOrderBySend_NextTimestampAsc().map(KnxGroup::getSend).map(ComInfo::getNextTimestamp);
return ZonedDateTime.now(); final Optional<ZonedDateTime> 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;
} }
} }

View File

@ -1,7 +1,8 @@
package de.ph87.homeautomation.knx.group; 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.IPropertyOwner;
import de.ph87.homeautomation.property.PropertySetException;
import lombok.Getter; import lombok.Getter;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -10,10 +11,11 @@ import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import tuwien.auto.calimero.GroupAddress; import tuwien.auto.calimero.GroupAddress;
import java.util.Optional;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import static de.ph87.homeautomation.shared.Helpers.quoteOrNull;
@Slf4j @Slf4j
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@ -22,48 +24,43 @@ public class KnxGroupSetService implements IPropertyOwner {
@Getter @Getter
private final Pattern propertyNamePattern = Pattern.compile("^knx\\.group\\.(\\d+)/(\\d+)/(\\d+)$"); 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 KnxGroupWriteService knxGroupWriteService;
private final KnxGroupRepository knxGroupRepository; private final KnxGroupRepository knxGroupRepository;
@EventListener(ApplicationStartedEvent.class) @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() { public void requestAll() {
knxGroupWriteService.markAllForRead(); knxGroupWriteService.markAllForRead();
knxLinkService.notifyActionPending(); knxThreadService.notifyActionPending();
} }
@Override @Override
public void setProperty(final String propertyName, final String value) { public void setProperty(final String propertyName, final String value) throws PropertySetException {
findGroupAddress(propertyName).ifPresent(groupAddress -> { final GroupAddress groupAddress = parseGroupAddress(propertyName);
try {
if (knxGroupWriteService.setSendValue(groupAddress, value)) { if (knxGroupWriteService.setSendValue(groupAddress, value)) {
knxLinkService.notifyActionPending(); knxThreadService.notifyActionPending();
} else { } else {
log.error("No such KnxGroup.address = {}", groupAddress); log.error("No such KnxGroup.address = {}", groupAddress);
} }
}); } catch (KnxGroupFormatException e) {
throw new PropertySetException(propertyName, value, e);
}
} }
@Override @Override
public Boolean readBoolean(final String propertyName) { 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<GroupAddress> findGroupAddress(final String propertyName) { private GroupAddress parseGroupAddress(final String propertyName) {
final Matcher matcher = propertyNamePattern.matcher(propertyName); final Matcher matcher = propertyNamePattern.matcher(propertyName);
if (matcher.matches()) { 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));
} }
} }

View File

@ -3,6 +3,8 @@ package de.ph87.homeautomation.knx.group;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; 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.GroupAddress;
import tuwien.auto.calimero.KNXException; import tuwien.auto.calimero.KNXException;
import tuwien.auto.calimero.KNXFormatException; 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.DPTXlatorBoolean;
import tuwien.auto.calimero.dptxlator.TranslatorTypes; import tuwien.auto.calimero.dptxlator.TranslatorTypes;
import javax.transaction.Transactional;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
@Slf4j @Slf4j
@Service @Service
@Transactional(Transactional.TxType.REQUIRES_NEW) @Transactional(propagation = Propagation.REQUIRES_NEW)
@RequiredArgsConstructor @RequiredArgsConstructor
public class KnxGroupWriteService { public class KnxGroupWriteService {
@ -27,16 +28,19 @@ public class KnxGroupWriteService {
public void updateOrCreate(final int rawAddress, final byte[] data) { public void updateOrCreate(final int rawAddress, final byte[] data) {
final KnxGroup knxGroup = getOrCreate(rawAddress); final KnxGroup knxGroup = getOrCreate(rawAddress);
knxGroup.setValue(data); knxGroup.setValue(data);
knxGroup.setValueTimestamp(ZonedDateTime.now());
knxGroup.setBooleanValue(null); knxGroup.setBooleanValue(null);
knxGroup.setNumberValue(null); knxGroup.setNumberValue(null);
findTranslator(knxGroup).ifPresent(translator -> { try {
final DPTXlator translator = findTranslator(knxGroup);
translator.setData(data); translator.setData(data);
if (translator instanceof DPTXlatorBoolean) { if (translator instanceof DPTXlatorBoolean) {
knxGroup.setBooleanValue(((DPTXlatorBoolean) translator).getValueBoolean()); knxGroup.setBooleanValue(((DPTXlatorBoolean) translator).getValueBoolean());
} }
// TODO implement all DPTXlator... // TODO implement all DPTXlator...
}); } catch (NoTranslatorException e) {
knxGroup.setValueTimestamp(ZonedDateTime.now()); log.error(e.getMessage());
}
log.debug("KnxGroup updated: {}", knxGroup); log.debug("KnxGroup updated: {}", knxGroup);
} }
@ -54,30 +58,35 @@ public class KnxGroupWriteService {
knxGroupRepository.findAllByRead_AbleTrue().forEach(knxGroup -> knxGroup.getRead().setNextTimestamp(ZonedDateTime.now())); 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(); final KnxGroup trans = new KnxGroup();
trans.setAddress(main, middle, sub); trans.setAddress(address);
trans.setDpt(dpt); trans.setDpt(dpt);
trans.setName(name); trans.setName(name);
trans.getRead().setAble(readable); trans.getRead().setAble(readable);
knxGroupRepository.save(trans); 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<KnxGroup> knxGroupOptional = knxGroupRepository.findByAddressRaw(groupAddress.getRawAddress()); final Optional<KnxGroup> knxGroupOptional = knxGroupRepository.findByAddressRaw(groupAddress.getRawAddress());
if (knxGroupOptional.isEmpty()) { if (knxGroupOptional.isEmpty()) {
return false; return false;
} }
final KnxGroup knxGroup = knxGroupOptional.get(); final KnxGroup knxGroup = knxGroupOptional.get();
final Optional<DPTXlator> translatorOptional = findTranslator(knxGroup);
if (translatorOptional.isEmpty()) {
return false;
}
final DPTXlator translator = translatorOptional.get();
try { try {
final DPTXlator translator = findTranslator(knxGroup);
if (translator instanceof DPTXlatorBoolean) { 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) { } else if (translator instanceof DPTXlator8BitUnsigned) {
((DPTXlator8BitUnsigned) translator).setValue(Integer.parseInt(value)); ((DPTXlator8BitUnsigned) translator).setValue(Integer.parseInt(value));
} else { // TODO implement all DPTXlator... } else { // TODO implement all DPTXlator...
@ -86,19 +95,20 @@ public class KnxGroupWriteService {
knxGroup.setSendValue(translator.getData()); knxGroup.setSendValue(translator.getData());
knxGroup.getSend().setNextTimestamp(ZonedDateTime.now()); knxGroup.getSend().setNextTimestamp(ZonedDateTime.now());
return true; return true;
} catch (KNXFormatException e) { } catch (NoTranslatorException | KNXFormatException e) {
log.error(e.toString()); throw new KnxGroupFormatException(knxGroup, value, e.getMessage());
} }
return false;
} }
private Optional<DPTXlator> 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]); final int mainNumber = Integer.parseInt(knxGroup.getDpt().split("\\.")[0]);
try { try {
return Optional.of(TranslatorTypes.createTranslator(mainNumber, knxGroup.getDpt())); return TranslatorTypes.createTranslator(mainNumber, knxGroup.getDpt());
} catch (KNXException e) { } catch (KNXException e) {
log.error(e.toString()); throw new NoTranslatorException(e);
return Optional.empty();
} }
} }

View File

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

View File

@ -6,7 +6,7 @@ public interface IPropertyOwner {
Pattern getPropertyNamePattern(); Pattern getPropertyNamePattern();
void setProperty(final String propertyName, final String value); void setProperty(final String propertyName, final String value) throws PropertySetException;
Boolean readBoolean(String propertyName); Boolean readBoolean(String propertyName);

View File

@ -13,7 +13,7 @@ public class PropertyService {
private final Set<IPropertyOwner> propertyOwners; private final Set<IPropertyOwner> 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); log.debug("Setting property \"{}\" => {}", propertyName, value);
getOwnerOrThrow(propertyName).setProperty(propertyName, value); getOwnerOrThrow(propertyName).setProperty(propertyName, value);
} }

View File

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

View File

@ -21,7 +21,7 @@ public class Schedule {
@Setter(AccessLevel.NONE) @Setter(AccessLevel.NONE)
private Long id; private Long id;
private boolean enabled; private boolean enabled = true;
@Column(nullable = false, unique = true) @Column(nullable = false, unique = true)
private String name; private String name;

View File

@ -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<String, String>... 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<String, String>... entries) {
return create(schedule, ScheduleEntryType.TIME, null, hour, minute, entries);
}
private ScheduleEntry createSunrise(final Schedule schedule, final Zenith zenith, final Map.Entry<String, String>... entries) {
return create(schedule, ScheduleEntryType.SUNRISE, zenith, 0, 0, entries);
}
private ScheduleEntry createSunset(final Schedule schedule, final Zenith zenith, final Map.Entry<String, String>... 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<String, String>... 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;
}
}

View File

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

View File

@ -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<ZonedDateTime> getOverallNextTimestamp() {
return scheduleEntryRepository.findFirstNextDateTimeByNextDateTimeNotNullOrderByNextDateTimeAsc().map(ScheduleEntry::getNextDateTime);
}
}

View File

@ -45,6 +45,8 @@ public class ScheduleEntry {
private int minute; private int minute;
private int second;
private ZonedDateTime nextDateTime; private ZonedDateTime nextDateTime;
@ElementCollection @ElementCollection
@ -93,6 +95,8 @@ public class ScheduleEntry {
builder.append(hour); builder.append(hour);
builder.append(", minute="); builder.append(", minute=");
builder.append(minute); builder.append(minute);
builder.append(", second=");
builder.append(second);
break; break;
case SUNRISE: case SUNRISE:
case SUNSET: case SUNSET:

View File

@ -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<ScheduleEntry, Long> {
Optional<ScheduleEntry> findFirstNextDateTimeByNextDateTimeNotNullOrderByNextDateTimeAsc();
}

View File

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

View File

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

View File

@ -1,4 +1,4 @@
package de.ph87.office.web; package de.ph87.homeautomation.web;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry; import org.springframework.messaging.simp.config.MessageBrokerRegistry;

View File

@ -1,5 +1,6 @@
package de.ph87.office.web; package de.ph87.office.web;
import de.ph87.homeautomation.web.WebSocketConfig;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.lang.NonNull; import org.springframework.lang.NonNull;

View File

@ -2,7 +2,6 @@ package de.ph87.network.router;
import java.io.IOException; import java.io.IOException;
import java.net.Inet4Address; import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface; import java.net.NetworkInterface;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
@ -12,10 +11,10 @@ import java.util.stream.Collectors;
public class Router { 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 Route route = getRoute(remoteAddress);
final NetworkInterface networkInterface = NetworkInterface.getByName(route.iface); 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 { public static Route getRoute(final Inet4Address remoteAddress) throws IOException {