A lot fixing, cleaning, refactoring in: Schedule, KnxLink
This commit is contained in:
parent
7562f1426d
commit
fc7058c0e0
106
src/main/java/de/ph87/homeautomation/DemoDataService.java
Normal file
106
src/main/java/de/ph87/homeautomation/DemoDataService.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@ -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<ZonedDateTime> sendOptional = knxGroupRepository.findFirstBySend_NextTimestampNotNullOrderBySend_NextTimestampAsc().map(KnxGroup::getSend).map(ComInfo::getNextTimestamp);
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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<GroupAddress> 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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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<KnxGroup> knxGroupOptional = knxGroupRepository.findByAddressRaw(groupAddress.getRawAddress());
|
||||
if (knxGroupOptional.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
final KnxGroup knxGroup = knxGroupOptional.get();
|
||||
|
||||
final Optional<DPTXlator> 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<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]);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -13,7 +13,7 @@ public class PropertyService {
|
||||
|
||||
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);
|
||||
getOwnerOrThrow(propertyName).setProperty(propertyName, value);
|
||||
}
|
||||
|
||||
@ -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()));
|
||||
}
|
||||
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@ -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:
|
||||
|
||||
@ -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();
|
||||
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
33
src/main/java/de/ph87/homeautomation/shared/Helpers.java
Normal file
33
src/main/java/de/ph87/homeautomation/shared/Helpers.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user