KnxLinkService working now

This commit is contained in:
Patrick Haßel 2021-09-30 16:26:26 +02:00
commit 28cf4eed2e
10 changed files with 679 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
/target/
/.idea/
/.jpb/
/application.properties

160
pom.xml Normal file
View File

@ -0,0 +1,160 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>de.ph87</groupId>
<artifactId>Homeautomation</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.6.RELEASE</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.github.calimero</groupId>
<artifactId>calimero-core</artifactId>
<version>2.5-M1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<version>1.10.3</version>
<configuration>
<workingDirectory>src/main/angular</workingDirectory>
</configuration>
<executions>
<execution>
<id>install-node-and-npm</id>
<goals>
<goal>install-node-and-npm</goal>
</goals>
<configuration>
<nodeVersion>v14.15.5</nodeVersion>
</configuration>
</execution>
<execution>
<id>npm install</id>
<goals>
<goal>npm</goal>
</goals>
<configuration>
<arguments>install</arguments>
</configuration>
</execution>
<execution>
<id>npm build</id>
<phase>generate-resources</phase>
<goals>
<goal>npm</goal>
</goals>
<configuration>
<arguments>run build</arguments>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<executions>
<execution>
<id>copy-resources</id>
<phase>process-resources</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>target/classes/resources/</outputDirectory>
<resources>
<resource>
<directory>src/main/angular/dist/angular/</directory>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
<configuration>
<classifier>spring-boot</classifier>
<mainClass>
de.ph87.de.ph87.homeautomation.BackendApplication
</mainClass>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>11</source>
<target>11</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,54 @@
package de.ph87.homeautomation;
import de.ph87.homeautomation.knx.KnxLinkService;
import de.ph87.homeautomation.knx.group.KnxGroup;
import de.ph87.homeautomation.knx.group.KnxGroupRepository;
import de.ph87.homeautomation.knx.group.KnxGroupWriteService;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.event.EventListener;
import javax.annotation.PostConstruct;
@SpringBootApplication
@RequiredArgsConstructor
public class BackendApplication {
private final KnxGroupRepository knxGroupRepository;
private final KnxLinkService knxLinkService;
private final KnxGroupWriteService knxGroupWriteService;
public static void main(String[] args) {
SpringApplication.run(BackendApplication.class);
}
@PostConstruct
public void postConstruct() {
knxGroupCreate(0, 0, 1, "1.001");
knxGroupCreate(0, 3, 6, "1.001");
requestAll();
}
private void knxGroupCreate(final int main, final int middle, final int sub, final String dpt) {
final KnxGroup trans = new KnxGroup();
trans.setAddress(main, middle, sub);
trans.setDpt(dpt);
knxGroupRepository.save(trans);
}
public void requestAll() {
knxGroupWriteService.markAllForRead();
knxLinkService.notifyActionPending();
}
@EventListener(ApplicationStartedEvent.class)
public void applicationStarted() {
}
}

View File

@ -0,0 +1,185 @@
package de.ph87.homeautomation.knx;
import de.ph87.homeautomation.knx.group.KnxGroupLinkService;
import de.ph87.homeautomation.knx.group.KnxGroupWriteService;
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;
import tuwien.auto.calimero.FrameEvent;
import tuwien.auto.calimero.KNXException;
import tuwien.auto.calimero.link.KNXNetworkLinkIP;
import tuwien.auto.calimero.link.NetworkLinkListener;
import tuwien.auto.calimero.link.medium.TPSettings;
import tuwien.auto.calimero.process.ProcessCommunicatorImpl;
import tuwien.auto.calimero.process.ProcessEvent;
import tuwien.auto.calimero.process.ProcessListener;
import javax.annotation.PreDestroy;
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 static final int ERROR_DELAY_MS = 3000;
private final KnxGroupWriteService knxGroupWriteService;
private final KnxGroupLinkService knxGroupLinkService;
private InetAddress remoteAddress = null;
private final Thread thread = new Thread(this::run);
private boolean stop = false;
private KNXNetworkLinkIP link = null;
private ProcessCommunicatorImpl processCommunicator = null;
private final Object lock = new Object();
@EventListener(ApplicationStartedEvent.class)
public void afterStartup() throws UnknownHostException {
remoteAddress = Inet4Address.getByName("10.0.0.102");
thread.start();
}
@PreDestroy
public void preDestroy() {
stop = true;
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 e) {
error(e);
}
} else {
work();
}
}
} catch (InterruptedException e) {
// ignore
} finally {
log.info("KNX Thread terminated.");
}
}
private void connect() throws KNXException, InterruptedException {
log.debug("Connecting KNX link...");
link = KNXNetworkLinkIP.newTunnelingLink(new InetSocketAddress("10.0.0.132", 0), new InetSocketAddress(remoteAddress, 3671), false, new TPSettings());
link.addLinkListener(this);
processCommunicator = new ProcessCommunicatorImpl(link);
processCommunicator.addProcessListener(this);
log.info("KNX link established.");
}
private void work() throws InterruptedException {
try {
if (!knxGroupLinkService.sendNext(processCommunicator) && !knxGroupLinkService.readNext(processCommunicator)) {
final ZonedDateTime nextTimestamp = knxGroupLinkService.getNextTimestamp();
if (nextTimestamp == null) {
doWait(0);
} else {
final long waitMs = Duration.between(ZonedDateTime.now(), nextTimestamp).toMillis();
if (waitMs > 0) {
doWait(waitMs);
}
}
}
} catch (KNXException e) {
error(e);
}
}
private void error(final KNXException e) throws InterruptedException {
log.error(e.toString());
cleanUp();
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();
}
}
@Override
public void confirmation(final FrameEvent e) {
// ignore
}
@Override
public void indication(final FrameEvent e) {
// ignore
}
@Override
public void linkClosed(final CloseEvent e) {
// ignore
}
@Override
public void groupReadResponse(final ProcessEvent processEvent) {
log.debug("{}", processEvent);
knxGroupWriteService.updateOrCreate(processEvent.getDestination().getRawAddress(), processEvent.getASDU());
}
@Override
public void groupWrite(final ProcessEvent processEvent) {
// ignore
}
@Override
public void groupReadRequest(final ProcessEvent processEvent) {
// ignore
}
@Override
public void detached(final DetachEvent detachEvent) {
// ignore
}
}

View File

@ -0,0 +1,24 @@
package de.ph87.homeautomation.knx.group;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import javax.persistence.Embeddable;
import java.time.ZonedDateTime;
@Getter
@Setter
@ToString
@Embeddable
public class ComInfo {
private boolean able = true;
private ZonedDateTime nextTimestamp = null;
private int errorCount = 0;
private String errorMessage = null;
}

View File

@ -0,0 +1,84 @@
package de.ph87.homeautomation.knx.group;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import tuwien.auto.calimero.GroupAddress;
import javax.persistence.*;
import java.time.Duration;
import java.time.ZonedDateTime;
@Getter
@Setter
@ToString
@Entity
public class KnxGroup {
@Id
@GeneratedValue
@Setter(AccessLevel.NONE)
private Long id;
@Column(unique = true)
private int addressRaw;
@Column(unique = true)
private String addressStr;
private String dpt;
private byte[] value;
private ZonedDateTime valueTimestamp;
private byte[] sendValue;
private int readInterval;
@Embedded
private ComInfo read = new ComInfo();
@Embedded
private ComInfo send = new ComInfo();
public void setAddress(final int rawAddress) {
setAddress(new GroupAddress(rawAddress));
}
public void setAddress(final int main, final int middle, final int sub) {
setAddress(new GroupAddress(main, middle, sub));
}
public void setAddress(final GroupAddress groupAddress) {
this.addressRaw = groupAddress.getRawAddress();
this.addressStr = groupAddress.toString();
}
public GroupAddress getAddress() {
return new GroupAddress(addressRaw);
}
public void setReadInterval(final int readInterval) {
if (readInterval <= 0) {
this.readInterval = 0;
} else {
this.readInterval = readInterval;
if (read.getNextTimestamp() == null || Duration.between(ZonedDateTime.now(), read.getNextTimestamp()).toSeconds() > this.readInterval) {
updateNextReadTimestamp();
}
}
}
public void updateNextReadTimestamp() {
if (read.getErrorCount() == 0) {
if (this.readInterval <= 0) {
read.setNextTimestamp(null);
} else {
read.setNextTimestamp(ZonedDateTime.now().plusSeconds(read.getNextTimestamp() == null ? 0 : this.readInterval));
}
}
}
}

View File

@ -0,0 +1,89 @@
package de.ph87.homeautomation.knx.group;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import tuwien.auto.calimero.GroupAddress;
import tuwien.auto.calimero.KNXException;
import tuwien.auto.calimero.KNXFormatException;
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.ZonedDateTime;
import java.util.Optional;
@Slf4j
@Service
@Transactional
@RequiredArgsConstructor
public class KnxGroupLinkService {
private final KnxGroupRepository knxGroupRepository;
public boolean sendNext(final ProcessCommunicatorImpl processCommunicator) throws KNXException {
final Optional<KnxGroup> knxGroupOptional = knxGroupRepository.findFirstBySend_NextTimestampNotNullOrderBySend_NextTimestampAsc();
if (knxGroupOptional.isEmpty()) {
return false;
}
return send(processCommunicator, knxGroupOptional.get());
}
public boolean readNext(final ProcessCommunicatorImpl processCommunicator) throws KNXException, InterruptedException {
final Optional<KnxGroup> knxGroupOptional = knxGroupRepository.findFirstByRead_NextTimestampLessThanEqualOrderByRead_NextTimestampAsc(ZonedDateTime.now());
if (knxGroupOptional.isEmpty()) {
return false;
}
return read(processCommunicator, knxGroupOptional.get());
}
private boolean send(final ProcessCommunicatorImpl processCommunicator, final KnxGroup knxGroup) throws KNXException {
try {
log.debug("Sending KnxGroup: {}", knxGroup);
processCommunicator.write(knxGroup.getAddress(), TranslatorTypes.createTranslator(knxGroup.getDpt(), knxGroup.getSendValue()));
knxGroup.getSend().setErrorCount(0);
knxGroup.getSend().setErrorMessage(null);
knxGroup.getSend().setNextTimestamp(null);
log.debug("Successfully sent KnxGroup: {}", knxGroup);
return true;
} catch (KNXFormatException e) {
log.error(e.toString());
knxGroup.getSend().setErrorCount(knxGroup.getSend().getErrorCount() + 1);
knxGroup.getSend().setErrorMessage(e.toString());
knxGroup.getSend().setNextTimestamp(ZonedDateTime.now().plusSeconds(knxGroup.getSend().getErrorCount()));
}
return false;
}
private boolean read(final ProcessCommunicatorImpl processCommunicator, final KnxGroup knxGroup) throws KNXException, InterruptedException {
try {
log.debug("Reading KnxGroup: {}", knxGroup);
processCommunicator.read(createStateDP(knxGroup));
knxGroup.getRead().setErrorCount(0);
knxGroup.getRead().setErrorMessage(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();
}
return false;
}
private StateDP createStateDP(final KnxGroup knxGroup) {
final GroupAddress groupAddress = knxGroup.getAddress();
final int mainNumber = Integer.parseInt(knxGroup.getDpt().split("\\.", 2)[0]);
return new StateDP(groupAddress, groupAddress.toString(), mainNumber, knxGroup.getDpt());
}
public ZonedDateTime getNextTimestamp() {
if (knxGroupRepository.findFirstBySend_NextTimestampNotNullOrderBySend_NextTimestampAsc().isPresent()) {
return ZonedDateTime.now();
}
return knxGroupRepository.findFirstByRead_NextTimestampNotNullOrderByRead_NextTimestampAsc().map(KnxGroup::getRead).map(ComInfo::getNextTimestamp).orElse(null);
}
}

View File

@ -0,0 +1,23 @@
package de.ph87.homeautomation.knx.group;
import org.springframework.data.repository.CrudRepository;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Optional;
public interface KnxGroupRepository extends CrudRepository<KnxGroup, Long> {
Optional<KnxGroup> findByAddressRaw(int rawAddress);
List<KnxGroup> findAll();
Optional<KnxGroup> findFirstBySend_NextTimestampNotNullOrderBySend_NextTimestampAsc();
Optional<KnxGroup> findFirstByRead_NextTimestampLessThanEqualOrderByRead_NextTimestampAsc(ZonedDateTime timestamp);
List<KnxGroup> findAllByRead_AbleTrue();
Optional<KnxGroup> findFirstByRead_NextTimestampNotNullOrderByRead_NextTimestampAsc();
}

View File

@ -0,0 +1,39 @@
package de.ph87.homeautomation.knx.group;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.transaction.Transactional;
import java.time.ZonedDateTime;
@Slf4j
@Service
@Transactional
@RequiredArgsConstructor
public class KnxGroupWriteService {
private final KnxGroupRepository knxGroupRepository;
public void updateOrCreate(final int rawAddress, final byte[] data) {
final KnxGroup knxGroup = getOrCreate(rawAddress);
knxGroup.setValue(data);
knxGroup.setValueTimestamp(ZonedDateTime.now());
log.debug("KnxGroup updated: {}", knxGroup);
}
private KnxGroup getOrCreate(final int rawAddress) {
return knxGroupRepository.findByAddressRaw(rawAddress).orElseGet(() -> {
final KnxGroup trans = new KnxGroup();
trans.setAddress(rawAddress);
final KnxGroup saved = knxGroupRepository.save(trans);
log.info("KnxGroup created: {}", saved);
return saved;
});
}
public void markAllForRead() {
knxGroupRepository.findAllByRead_AbleTrue().forEach(knxGroup -> knxGroup.getRead().setNextTimestamp(ZonedDateTime.now()));
}
}

View File

@ -0,0 +1,17 @@
logging.level.root=WARN
logging.level.de.ph87=INFO
#-
spring.datasource.url=jdbc:h2:./Homeautomation;AUTO_SERVER=TRUE;DB_CLOSE_ON_EXIT=FALSE
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
#-
spring.jpa.hibernate.naming.implicit-strategy=org.hibernate.boot.model.naming.ImplicitNamingStrategyComponentPathImpl
spring.jpa.hibernate.ddl-auto=update
spring.jpa.open-in-view=false
#-
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
#-
spring.jackson.serialization.indent_output=true
#-
spring.main.banner-mode=off