commit 28cf4eed2ef77d1296c85186c6ec20208d9b4672 Author: Patrick Haßel Date: Thu Sep 30 16:26:26 2021 +0200 KnxLinkService working now diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a263d40 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/target/ +/.idea/ +/.jpb/ +/application.properties diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..a85db42 --- /dev/null +++ b/pom.xml @@ -0,0 +1,160 @@ + + + 4.0.0 + + de.ph87 + Homeautomation + 1.0-SNAPSHOT + war + + + 11 + 11 + + + + org.springframework.boot + spring-boot-starter-parent + 2.3.6.RELEASE + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-configuration-processor + + + org.springframework.boot + spring-boot-starter-websocket + + + + com.h2database + h2 + + + org.projectlombok + lombok + + + com.github.calimero + calimero-core + 2.5-M1 + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + + + com.github.eirslett + frontend-maven-plugin + 1.10.3 + + src/main/angular + + + + install-node-and-npm + + install-node-and-npm + + + v14.15.5 + + + + npm install + + npm + + + install + + + + npm build + generate-resources + + npm + + + run build + + + + + + + maven-resources-plugin + + + copy-resources + process-resources + + copy-resources + + + target/classes/resources/ + + + src/main/angular/dist/angular/ + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + spring-boot + + de.ph87.de.ph87.homeautomation.BackendApplication + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 11 + 11 + + + + + + + diff --git a/src/main/java/de/ph87/homeautomation/BackendApplication.java b/src/main/java/de/ph87/homeautomation/BackendApplication.java new file mode 100644 index 0000000..390a582 --- /dev/null +++ b/src/main/java/de/ph87/homeautomation/BackendApplication.java @@ -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() { + + } + +} \ No newline at end of file diff --git a/src/main/java/de/ph87/homeautomation/knx/KnxLinkService.java b/src/main/java/de/ph87/homeautomation/knx/KnxLinkService.java new file mode 100644 index 0000000..822eb64 --- /dev/null +++ b/src/main/java/de/ph87/homeautomation/knx/KnxLinkService.java @@ -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 + } + +} diff --git a/src/main/java/de/ph87/homeautomation/knx/group/ComInfo.java b/src/main/java/de/ph87/homeautomation/knx/group/ComInfo.java new file mode 100644 index 0000000..b4d09de --- /dev/null +++ b/src/main/java/de/ph87/homeautomation/knx/group/ComInfo.java @@ -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; + +} \ No newline at end of file diff --git a/src/main/java/de/ph87/homeautomation/knx/group/KnxGroup.java b/src/main/java/de/ph87/homeautomation/knx/group/KnxGroup.java new file mode 100644 index 0000000..caa7414 --- /dev/null +++ b/src/main/java/de/ph87/homeautomation/knx/group/KnxGroup.java @@ -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)); + } + } + } + +} diff --git a/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupLinkService.java b/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupLinkService.java new file mode 100644 index 0000000..958c823 --- /dev/null +++ b/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupLinkService.java @@ -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 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 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); + } + +} diff --git a/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupRepository.java b/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupRepository.java new file mode 100644 index 0000000..b0c3c8e --- /dev/null +++ b/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupRepository.java @@ -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 { + + Optional findByAddressRaw(int rawAddress); + + List findAll(); + + Optional findFirstBySend_NextTimestampNotNullOrderBySend_NextTimestampAsc(); + + Optional findFirstByRead_NextTimestampLessThanEqualOrderByRead_NextTimestampAsc(ZonedDateTime timestamp); + + List findAllByRead_AbleTrue(); + + Optional findFirstByRead_NextTimestampNotNullOrderByRead_NextTimestampAsc(); + +} diff --git a/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupWriteService.java b/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupWriteService.java new file mode 100644 index 0000000..1b507b4 --- /dev/null +++ b/src/main/java/de/ph87/homeautomation/knx/group/KnxGroupWriteService.java @@ -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())); + } + +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..264df89 --- /dev/null +++ b/src/main/resources/application.properties @@ -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