implemented Schedule
This commit is contained in:
parent
28cf4eed2e
commit
82313a1932
9
pom.xml
9
pom.xml
@ -10,8 +10,8 @@
|
|||||||
<packaging>war</packaging>
|
<packaging>war</packaging>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<maven.compiler.source>11</maven.compiler.source>
|
<maven.compiler.source>15</maven.compiler.source>
|
||||||
<maven.compiler.target>11</maven.compiler.target>
|
<maven.compiler.target>15</maven.compiler.target>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<parent>
|
<parent>
|
||||||
@ -56,6 +56,11 @@
|
|||||||
<artifactId>calimero-core</artifactId>
|
<artifactId>calimero-core</artifactId>
|
||||||
<version>2.5-M1</version>
|
<version>2.5-M1</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.luckycatlabs</groupId>
|
||||||
|
<artifactId>SunriseSunsetCalculator</artifactId>
|
||||||
|
<version>1.2</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
|||||||
@ -1,54 +1,15 @@
|
|||||||
package de.ph87.homeautomation;
|
package de.ph87.homeautomation;
|
||||||
|
|
||||||
import de.ph87.homeautomation.knx.KnxLinkService;
|
|
||||||
import de.ph87.homeautomation.knx.group.KnxGroup;
|
|
||||||
import de.ph87.homeautomation.knx.group.KnxGroupRepository;
|
|
||||||
import de.ph87.homeautomation.knx.group.KnxGroupWriteService;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
import org.springframework.boot.context.event.ApplicationStartedEvent;
|
|
||||||
import org.springframework.context.event.EventListener;
|
|
||||||
|
|
||||||
import javax.annotation.PostConstruct;
|
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class BackendApplication {
|
public class BackendApplication {
|
||||||
|
|
||||||
private final KnxGroupRepository knxGroupRepository;
|
|
||||||
|
|
||||||
private final KnxLinkService knxLinkService;
|
|
||||||
|
|
||||||
private final KnxGroupWriteService knxGroupWriteService;
|
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
SpringApplication.run(BackendApplication.class);
|
SpringApplication.run(BackendApplication.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostConstruct
|
|
||||||
public void postConstruct() {
|
|
||||||
knxGroupCreate(0, 0, 1, "1.001");
|
|
||||||
knxGroupCreate(0, 3, 6, "1.001");
|
|
||||||
|
|
||||||
requestAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void knxGroupCreate(final int main, final int middle, final int sub, final String dpt) {
|
|
||||||
final KnxGroup trans = new KnxGroup();
|
|
||||||
trans.setAddress(main, middle, sub);
|
|
||||||
trans.setDpt(dpt);
|
|
||||||
knxGroupRepository.save(trans);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void requestAll() {
|
|
||||||
knxGroupWriteService.markAllForRead();
|
|
||||||
knxLinkService.notifyActionPending();
|
|
||||||
}
|
|
||||||
|
|
||||||
@EventListener(ApplicationStartedEvent.class)
|
|
||||||
public void applicationStarted() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
18
src/main/java/de/ph87/homeautomation/Config.java
Normal file
18
src/main/java/de/ph87/homeautomation/Config.java
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package de.ph87.homeautomation;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Configuration
|
||||||
|
@ConfigurationProperties(prefix = "de.ph87.homeautomation")
|
||||||
|
public class Config {
|
||||||
|
|
||||||
|
private double latitude = 49.4085629;
|
||||||
|
|
||||||
|
private double longitude = 6.9645334;
|
||||||
|
|
||||||
|
private String timezone = "Europe/Berlin";
|
||||||
|
|
||||||
|
}
|
||||||
@ -2,6 +2,7 @@ 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.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.boot.context.event.ApplicationStartedEvent;
|
||||||
@ -19,6 +20,7 @@ import tuwien.auto.calimero.process.ProcessEvent;
|
|||||||
import tuwien.auto.calimero.process.ProcessListener;
|
import tuwien.auto.calimero.process.ProcessListener;
|
||||||
|
|
||||||
import javax.annotation.PreDestroy;
|
import javax.annotation.PreDestroy;
|
||||||
|
import java.io.IOException;
|
||||||
import java.net.Inet4Address;
|
import java.net.Inet4Address;
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
@ -31,15 +33,13 @@ import java.time.ZonedDateTime;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class KnxLinkService implements NetworkLinkListener, ProcessListener {
|
public class KnxLinkService implements NetworkLinkListener, ProcessListener {
|
||||||
|
|
||||||
public static final int ERROR_DELAY_MS = 3000;
|
|
||||||
|
|
||||||
private final KnxGroupWriteService knxGroupWriteService;
|
private final KnxGroupWriteService knxGroupWriteService;
|
||||||
|
|
||||||
private final KnxGroupLinkService knxGroupLinkService;
|
private final KnxGroupLinkService knxGroupLinkService;
|
||||||
|
|
||||||
private InetAddress remoteAddress = null;
|
private Inet4Address remoteAddress = null;
|
||||||
|
|
||||||
private final Thread thread = new Thread(this::run);
|
private final Thread thread = new Thread(this::run, "knx-sync");
|
||||||
|
|
||||||
private boolean stop = false;
|
private boolean stop = false;
|
||||||
|
|
||||||
@ -51,7 +51,7 @@ public class KnxLinkService implements NetworkLinkListener, ProcessListener {
|
|||||||
|
|
||||||
@EventListener(ApplicationStartedEvent.class)
|
@EventListener(ApplicationStartedEvent.class)
|
||||||
public void afterStartup() throws UnknownHostException {
|
public void afterStartup() throws UnknownHostException {
|
||||||
remoteAddress = Inet4Address.getByName("10.0.0.102");
|
remoteAddress = (Inet4Address) Inet4Address.getByName("10.0.0.102");
|
||||||
thread.start();
|
thread.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,7 +73,7 @@ public class KnxLinkService implements NetworkLinkListener, ProcessListener {
|
|||||||
if (link == null) {
|
if (link == null) {
|
||||||
try {
|
try {
|
||||||
connect();
|
connect();
|
||||||
} catch (KNXException e) {
|
} catch (KNXException | IOException e) {
|
||||||
error(e);
|
error(e);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -87,9 +87,10 @@ public class KnxLinkService implements NetworkLinkListener, ProcessListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void connect() throws KNXException, InterruptedException {
|
private void connect() throws KNXException, InterruptedException, IOException {
|
||||||
log.debug("Connecting KNX link...");
|
log.debug("Connecting KNX link...");
|
||||||
link = KNXNetworkLinkIP.newTunnelingLink(new InetSocketAddress("10.0.0.132", 0), new InetSocketAddress(remoteAddress, 3671), false, new TPSettings());
|
final InetAddress localAddress = Router.getLocalInetAddressForRemoteAddress(remoteAddress);
|
||||||
|
link = KNXNetworkLinkIP.newTunnelingLink(new InetSocketAddress(localAddress, 0), new InetSocketAddress(remoteAddress, 3671), false, new TPSettings());
|
||||||
link.addLinkListener(this);
|
link.addLinkListener(this);
|
||||||
processCommunicator = new ProcessCommunicatorImpl(link);
|
processCommunicator = new ProcessCommunicatorImpl(link);
|
||||||
processCommunicator.addProcessListener(this);
|
processCommunicator.addProcessListener(this);
|
||||||
@ -114,9 +115,10 @@ public class KnxLinkService implements NetworkLinkListener, ProcessListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void error(final KNXException e) throws InterruptedException {
|
private void error(final Exception e) throws InterruptedException {
|
||||||
log.error(e.toString());
|
log.error(e.toString());
|
||||||
cleanUp();
|
cleanUp();
|
||||||
|
int ERROR_DELAY_MS = 3000;
|
||||||
doWait(ERROR_DELAY_MS);
|
doWait(ERROR_DELAY_MS);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,7 +171,8 @@ public class KnxLinkService implements NetworkLinkListener, ProcessListener {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void groupWrite(final ProcessEvent processEvent) {
|
public void groupWrite(final ProcessEvent processEvent) {
|
||||||
// ignore
|
log.debug("{}", processEvent);
|
||||||
|
knxGroupWriteService.updateOrCreate(processEvent.getDestination().getRawAddress(), processEvent.getASDU());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@ -0,0 +1,55 @@
|
|||||||
|
package de.ph87.homeautomation.knx.group;
|
||||||
|
|
||||||
|
import de.ph87.homeautomation.knx.KnxLinkService;
|
||||||
|
import de.ph87.homeautomation.property.IPropertyOwner;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.boot.context.event.ApplicationStartedEvent;
|
||||||
|
import org.springframework.context.event.EventListener;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import tuwien.auto.calimero.GroupAddress;
|
||||||
|
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class KnxGroupSetService implements IPropertyOwner {
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private final Pattern propertyNamePattern = Pattern.compile("^knx\\.group\\.(\\d+)/(\\d+)/(\\d+)$");
|
||||||
|
|
||||||
|
private final KnxLinkService knxLinkService;
|
||||||
|
|
||||||
|
private final KnxGroupWriteService knxGroupWriteService;
|
||||||
|
|
||||||
|
@EventListener(ApplicationStartedEvent.class)
|
||||||
|
public void applicationStarted() {
|
||||||
|
knxGroupWriteService.knxGroupCreate(0, 0, 1, "1.001", true);
|
||||||
|
knxGroupWriteService.knxGroupCreate(0, 3, 6, "1.001", true);
|
||||||
|
knxGroupWriteService.knxGroupCreate(0, 4, 24, "5.001", false);
|
||||||
|
|
||||||
|
requestAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void requestAll() {
|
||||||
|
knxGroupWriteService.markAllForRead();
|
||||||
|
knxLinkService.notifyActionPending();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setProperty(final String propertyName, final String value) {
|
||||||
|
final Matcher matcher = propertyNamePattern.matcher(propertyName);
|
||||||
|
if (matcher.matches()) {
|
||||||
|
final GroupAddress groupAddress = new GroupAddress(Integer.parseInt(matcher.group(1)), Integer.parseInt(matcher.group(2)), Integer.parseInt(matcher.group(3)));
|
||||||
|
if (knxGroupWriteService.setSendValue(groupAddress, value)) {
|
||||||
|
knxLinkService.notifyActionPending();
|
||||||
|
} else {
|
||||||
|
log.error("No such KnxGroup.address = {}", groupAddress);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -3,13 +3,21 @@ 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 tuwien.auto.calimero.GroupAddress;
|
||||||
|
import tuwien.auto.calimero.KNXException;
|
||||||
|
import tuwien.auto.calimero.dptxlator.DPTXlator;
|
||||||
|
import tuwien.auto.calimero.dptxlator.DPTXlator8BitUnsigned;
|
||||||
|
import tuwien.auto.calimero.dptxlator.DPTXlatorBoolean;
|
||||||
|
import tuwien.auto.calimero.dptxlator.TranslatorTypes;
|
||||||
|
|
||||||
import javax.transaction.Transactional;
|
import javax.transaction.Transactional;
|
||||||
import java.time.ZonedDateTime;
|
import java.time.ZonedDateTime;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@Transactional
|
@Transactional(Transactional.TxType.REQUIRES_NEW)
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class KnxGroupWriteService {
|
public class KnxGroupWriteService {
|
||||||
|
|
||||||
@ -36,4 +44,41 @@ 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 int main, final int middle, final int sub, final String dpt, final boolean readable) {
|
||||||
|
final KnxGroup trans = new KnxGroup();
|
||||||
|
trans.setAddress(main, middle, sub);
|
||||||
|
trans.setDpt(dpt);
|
||||||
|
trans.getRead().setAble(readable);
|
||||||
|
knxGroupRepository.save(trans);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean setSendValue(final GroupAddress groupAddress, final String value) {
|
||||||
|
final Optional<KnxGroup> knxGroupOptional = knxGroupRepository.findByAddressRaw(groupAddress.getRawAddress());
|
||||||
|
if (knxGroupOptional.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final KnxGroup knxGroup = knxGroupOptional.get();
|
||||||
|
getTranslator(knxGroup, value).ifPresent(translator -> knxGroup.setSendValue(translator.getData()));
|
||||||
|
knxGroup.getSend().setNextTimestamp(ZonedDateTime.now());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<DPTXlator> getTranslator(final KnxGroup knxGroup, final String value) {
|
||||||
|
final int mainNumber = Integer.parseInt(knxGroup.getDpt().split("\\.")[0]);
|
||||||
|
try {
|
||||||
|
final DPTXlator translator = TranslatorTypes.createTranslator(mainNumber, knxGroup.getDpt());
|
||||||
|
if (translator instanceof DPTXlatorBoolean) {
|
||||||
|
((DPTXlatorBoolean) translator).setValue(Objects.equals(value, "true"));
|
||||||
|
} else if (translator instanceof DPTXlator8BitUnsigned) {
|
||||||
|
((DPTXlator8BitUnsigned) translator).setValue(Integer.parseInt(value));
|
||||||
|
} else {
|
||||||
|
translator.setValue(value);
|
||||||
|
}
|
||||||
|
return Optional.of(translator);
|
||||||
|
} catch (KNXException e) {
|
||||||
|
log.error(e.toString());
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,11 @@
|
|||||||
|
package de.ph87.homeautomation.property;
|
||||||
|
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
public interface IPropertyOwner {
|
||||||
|
|
||||||
|
Pattern getPropertyNamePattern();
|
||||||
|
|
||||||
|
void setProperty(final String propertyName, final String value);
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
package de.ph87.homeautomation.property;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class PropertyService {
|
||||||
|
|
||||||
|
private final Set<IPropertyOwner> propertyOwners;
|
||||||
|
|
||||||
|
public void set(final String propertyName, final String value) {
|
||||||
|
log.info("Setting property \"{}\" => {}", propertyName, value);
|
||||||
|
final Optional<IPropertyOwner> iPropertyOwnerOptional = propertyOwners.stream().filter(iPropertyOwner -> iPropertyOwner.getPropertyNamePattern().matcher(propertyName).matches()).findFirst();
|
||||||
|
if (iPropertyOwnerOptional.isEmpty()) {
|
||||||
|
log.error("No IPropertyOwner found for name: {}", propertyName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
iPropertyOwnerOptional.get().setProperty(propertyName, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
package de.ph87.homeautomation.schedule;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import tuwien.auto.calimero.GroupAddress;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class PropertyEntry implements Map.Entry<String, String> {
|
||||||
|
|
||||||
|
private final String key;
|
||||||
|
|
||||||
|
private String value;
|
||||||
|
|
||||||
|
public PropertyEntry(final int rawGroupAddress, final String value) {
|
||||||
|
this.key = "knx.group." + new GroupAddress(rawGroupAddress);
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PropertyEntry(final String propertyName, final String value) {
|
||||||
|
this.key = propertyName;
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String setValue(final String value) {
|
||||||
|
this.value = value;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
33
src/main/java/de/ph87/homeautomation/schedule/Schedule.java
Normal file
33
src/main/java/de/ph87/homeautomation/schedule/Schedule.java
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package de.ph87.homeautomation.schedule;
|
||||||
|
|
||||||
|
import de.ph87.homeautomation.schedule.entry.ScheduleEntry;
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
import javax.persistence.*;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@ToString
|
||||||
|
@Entity
|
||||||
|
public class Schedule {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue
|
||||||
|
@Setter(AccessLevel.NONE)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private boolean enabled;
|
||||||
|
|
||||||
|
@Column(nullable = false, unique = true)
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@ToString.Exclude
|
||||||
|
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
|
||||||
|
private Set<ScheduleEntry> entries = new HashSet<>();
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
package de.ph87.homeautomation.schedule;
|
||||||
|
|
||||||
|
import org.springframework.data.repository.CrudRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface ScheduleRepository extends CrudRepository<Schedule, Long> {
|
||||||
|
|
||||||
|
List<Schedule> findAll();
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,197 @@
|
|||||||
|
package de.ph87.homeautomation.schedule;
|
||||||
|
|
||||||
|
import com.luckycatlabs.sunrisesunset.Zenith;
|
||||||
|
import com.luckycatlabs.sunrisesunset.calculator.SolarEventCalculator;
|
||||||
|
import com.luckycatlabs.sunrisesunset.dto.Location;
|
||||||
|
import de.ph87.homeautomation.Config;
|
||||||
|
import de.ph87.homeautomation.property.PropertyService;
|
||||||
|
import de.ph87.homeautomation.schedule.entry.ScheduleEntry;
|
||||||
|
import de.ph87.homeautomation.schedule.entry.ScheduleEntryType;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.boot.context.event.ApplicationStartedEvent;
|
||||||
|
import org.springframework.context.event.EventListener;
|
||||||
|
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import javax.annotation.PostConstruct;
|
||||||
|
import javax.transaction.Transactional;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@Transactional
|
||||||
|
@EnableScheduling
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ScheduleService {
|
||||||
|
|
||||||
|
public static final int WOHNZIMMER_ROLLLADEN_POSITION_ANFAHREN_ADDRESS = 1048;
|
||||||
|
|
||||||
|
public static final int SCHLAFZIMMER_ROLLLADEN_POSITION_ANFAHREN_ADDRESS = 771;
|
||||||
|
|
||||||
|
public static final int FLUR_ROLLLADEN_POSITION_ANFAHREN_ADDRESS = 1293;
|
||||||
|
|
||||||
|
private final Config config;
|
||||||
|
|
||||||
|
private final ScheduleRepository scheduleRepository;
|
||||||
|
|
||||||
|
private final PropertyService propertyService;
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void postConstruct() {
|
||||||
|
final Schedule wohnzimmer = new Schedule();
|
||||||
|
wohnzimmer.setEnabled(true);
|
||||||
|
wohnzimmer.setName("Rollläden Wohnzimmer");
|
||||||
|
createSunrise(wohnzimmer, Zenith.OFFICIAL, new PropertyEntry(WOHNZIMMER_ROLLLADEN_POSITION_ANFAHREN_ADDRESS, "0"));
|
||||||
|
createSunset(wohnzimmer, Zenith.OFFICIAL, new PropertyEntry(WOHNZIMMER_ROLLLADEN_POSITION_ANFAHREN_ADDRESS, "100"));
|
||||||
|
scheduleRepository.save(wohnzimmer);
|
||||||
|
|
||||||
|
final Schedule schlafzimmer = new Schedule();
|
||||||
|
schlafzimmer.setEnabled(true);
|
||||||
|
schlafzimmer.setName("Rollläden Schlafzimmer");
|
||||||
|
createTime(schlafzimmer, 7, 0, new PropertyEntry(SCHLAFZIMMER_ROLLLADEN_POSITION_ANFAHREN_ADDRESS, "0"));
|
||||||
|
createTime(schlafzimmer, 20, 0, new PropertyEntry(SCHLAFZIMMER_ROLLLADEN_POSITION_ANFAHREN_ADDRESS, "100"));
|
||||||
|
scheduleRepository.save(schlafzimmer);
|
||||||
|
|
||||||
|
final Schedule flur = new Schedule();
|
||||||
|
flur.setEnabled(true);
|
||||||
|
flur.setName("Rollläden Flur");
|
||||||
|
createSunrise(flur, Zenith.NAUTICAL, new PropertyEntry(FLUR_ROLLLADEN_POSITION_ANFAHREN_ADDRESS, "0"));
|
||||||
|
createSunset(flur, Zenith.NAUTICAL, new PropertyEntry(FLUR_ROLLLADEN_POSITION_ANFAHREN_ADDRESS, "100"));
|
||||||
|
scheduleRepository.save(flur);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ScheduleEntry createTime(final Schedule schedule, final Map.Entry<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,108 @@
|
|||||||
|
package de.ph87.homeautomation.schedule.entry;
|
||||||
|
|
||||||
|
import com.luckycatlabs.sunrisesunset.Zenith;
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
import javax.persistence.*;
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@Entity
|
||||||
|
public class ScheduleEntry {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue
|
||||||
|
@Setter(AccessLevel.NONE)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private boolean enabled = true;
|
||||||
|
|
||||||
|
private boolean monday = true;
|
||||||
|
|
||||||
|
private boolean tuesday = true;
|
||||||
|
|
||||||
|
private boolean wednesday = true;
|
||||||
|
|
||||||
|
private boolean thursday = true;
|
||||||
|
|
||||||
|
private boolean friday = true;
|
||||||
|
|
||||||
|
private boolean saturday = true;
|
||||||
|
|
||||||
|
private boolean sunday = true;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private ScheduleEntryType type = null;
|
||||||
|
|
||||||
|
private double zenith = Zenith.CIVIL.degrees().doubleValue();
|
||||||
|
|
||||||
|
private int hour;
|
||||||
|
|
||||||
|
private int minute;
|
||||||
|
|
||||||
|
private ZonedDateTime nextDateTime;
|
||||||
|
|
||||||
|
@ElementCollection
|
||||||
|
private Map<String, String> properties = new HashMap<>();
|
||||||
|
|
||||||
|
public void setWorkday(final boolean enabled) {
|
||||||
|
monday = enabled;
|
||||||
|
tuesday = enabled;
|
||||||
|
wednesday = enabled;
|
||||||
|
thursday = enabled;
|
||||||
|
friday = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setWeekend(final boolean enabled) {
|
||||||
|
saturday = enabled;
|
||||||
|
sunday = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEveryday(final boolean enabled) {
|
||||||
|
setWorkday(enabled);
|
||||||
|
setWeekend(enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
final StringBuilder builder = new StringBuilder();
|
||||||
|
builder.append(getClass().getSimpleName());
|
||||||
|
builder.append("(id=");
|
||||||
|
builder.append(id);
|
||||||
|
builder.append(", enabled=");
|
||||||
|
builder.append(enabled);
|
||||||
|
builder.append(", type=");
|
||||||
|
builder.append(type);
|
||||||
|
builder.append(", weekdays=");
|
||||||
|
builder.append(monday ? 1 : 0);
|
||||||
|
builder.append(tuesday ? 1 : 0);
|
||||||
|
builder.append(wednesday ? 1 : 0);
|
||||||
|
builder.append(thursday ? 1 : 0);
|
||||||
|
builder.append(friday ? 1 : 0);
|
||||||
|
builder.append(saturday ? 1 : 0);
|
||||||
|
builder.append(sunday ? 1 : 0);
|
||||||
|
if (type != null) {
|
||||||
|
switch (type) {
|
||||||
|
case TIME:
|
||||||
|
builder.append(", hour=");
|
||||||
|
builder.append(hour);
|
||||||
|
builder.append(", minute=");
|
||||||
|
builder.append(minute);
|
||||||
|
break;
|
||||||
|
case SUNRISE:
|
||||||
|
case SUNSET:
|
||||||
|
builder.append(", zenith=");
|
||||||
|
builder.append(zenith);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
builder.append(")");
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
package de.ph87.homeautomation.schedule.entry;
|
||||||
|
|
||||||
|
public enum ScheduleEntryType {
|
||||||
|
TIME, SUNRISE, SUNSET
|
||||||
|
}
|
||||||
90
src/main/java/de/ph87/network/router/Route.java
Normal file
90
src/main/java/de/ph87/network/router/Route.java
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
package de.ph87.network.router;
|
||||||
|
|
||||||
|
import java.net.Inet4Address;
|
||||||
|
import java.net.UnknownHostException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
public class Route {
|
||||||
|
|
||||||
|
private static final Pattern regex = Pattern.compile("^([\\d.]+)\\s+([\\d.]+)\\s+([\\d.]+)\\s+(\\S+)\\s+(\\d+)\\s+(\\d+)\\s+(\\d+)\\s+(\\S+)$");
|
||||||
|
|
||||||
|
public final Inet4Address network;
|
||||||
|
|
||||||
|
public final Inet4Address router;
|
||||||
|
|
||||||
|
public final Inet4Address netmask;
|
||||||
|
|
||||||
|
public final int metric;
|
||||||
|
|
||||||
|
public final String iface;
|
||||||
|
|
||||||
|
public final int networkInt;
|
||||||
|
|
||||||
|
public final int routerInt;
|
||||||
|
|
||||||
|
public final int netmaskInt;
|
||||||
|
|
||||||
|
public final Inet4Address addressFirst;
|
||||||
|
|
||||||
|
public final int addressFirstInt;
|
||||||
|
|
||||||
|
public final Inet4Address addressLast;
|
||||||
|
|
||||||
|
public final int addressLastInt;
|
||||||
|
|
||||||
|
public Route(final Inet4Address network, final Inet4Address router, final Inet4Address netmask, final int metric, final String iface) throws UnknownHostException {
|
||||||
|
this.network = network;
|
||||||
|
this.router = router;
|
||||||
|
this.netmask = netmask;
|
||||||
|
this.metric = metric;
|
||||||
|
this.iface = iface;
|
||||||
|
|
||||||
|
this.networkInt = toInt(network);
|
||||||
|
this.routerInt = toInt(router);
|
||||||
|
this.netmaskInt = toInt(netmask);
|
||||||
|
this.addressFirstInt = networkInt & netmaskInt;
|
||||||
|
this.addressLastInt = networkInt | ~netmaskInt;
|
||||||
|
this.addressFirst = fromInt(addressFirstInt);
|
||||||
|
this.addressLast = fromInt(addressLastInt);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean matches(final Inet4Address address) {
|
||||||
|
final int addressInt = toInt(address);
|
||||||
|
return addressFirstInt <= addressInt && addressInt <= addressLastInt;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int toInt(final Inet4Address address) {
|
||||||
|
return ByteBuffer.wrap(address.getAddress()).getInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Inet4Address fromInt(final int value) throws UnknownHostException {
|
||||||
|
return (Inet4Address) Inet4Address.getByAddress(ByteBuffer.allocate(4).putInt(value).array());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Optional<Route> parse(final String line) {
|
||||||
|
final Matcher matcher = regex.matcher(line);
|
||||||
|
try {
|
||||||
|
if (matcher.matches()) {
|
||||||
|
final String networkString = matcher.group(1);
|
||||||
|
final Inet4Address network = (Inet4Address) Inet4Address.getByName(Objects.equals(networkString, "default") ? "0.0.0.0" : networkString);
|
||||||
|
final Inet4Address router = (Inet4Address) Inet4Address.getByName(matcher.group(2));
|
||||||
|
final Inet4Address netmask = (Inet4Address) Inet4Address.getByName(matcher.group(3));
|
||||||
|
final int metric = Integer.parseInt(matcher.group(5));
|
||||||
|
final String iface = matcher.group(8);
|
||||||
|
return Optional.of(new Route(network, router, netmask, metric, iface));
|
||||||
|
}
|
||||||
|
} catch (UnknownHostException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isDefault() {
|
||||||
|
return network.isAnyLocalAddress() && netmask.isAnyLocalAddress();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
42
src/main/java/de/ph87/network/router/Router.java
Normal file
42
src/main/java/de/ph87/network/router/Router.java
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
package de.ph87.network.router;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.Inet4Address;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.NetworkInterface;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public class Router {
|
||||||
|
|
||||||
|
public static InetAddress getLocalInetAddressForRemoteAddress(final Inet4Address remoteAddress) throws IOException {
|
||||||
|
final Route route = getRoute(remoteAddress);
|
||||||
|
final NetworkInterface networkInterface = NetworkInterface.getByName(route.iface);
|
||||||
|
return networkInterface.getInetAddresses().nextElement();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Route getRoute(final Inet4Address remoteAddress) throws IOException {
|
||||||
|
final List<Route> routes = execute(Route::parse, "route", "-n");
|
||||||
|
final Optional<Route> routeOptional = routes.stream()
|
||||||
|
.filter(r -> !r.isDefault())
|
||||||
|
.filter(r -> r.matches(remoteAddress))
|
||||||
|
.reduce((a, b) -> a.metric < b.metric ? a : b)
|
||||||
|
.or(() -> routes.stream()
|
||||||
|
.filter(Route::isDefault)
|
||||||
|
.reduce((a, b) -> a.metric < b.metric ? a : b)
|
||||||
|
);
|
||||||
|
if (routeOptional.isEmpty()) {
|
||||||
|
throw new IOException("No route found for remoteAddress: " + remoteAddress);
|
||||||
|
}
|
||||||
|
return routeOptional.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T> List<T> execute(final Function<String, Optional<T>> parser, final String... commands) throws IOException {
|
||||||
|
final String output = new String(new ProcessBuilder(commands).start().getInputStream().readAllBytes());
|
||||||
|
return Arrays.stream(output.split("\\n")).map(parser).filter(Optional::isPresent).map(Optional::get).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user