From f9526bd80769761152585d5d9c877e9449bbb296 Mon Sep 17 00:00:00 2001 From: Thimo Seitz Date: Thu, 26 Feb 2026 23:13:42 +0100 Subject: [PATCH 01/23] mDNS: add IP/interface config & NetUtils Add support for configuring the IP address or network interface to use for Avahi/mDNS publishing. NetUtils: introduce detectIpAddressFromInterface() and listNetworkInterfaceNames() helpers to resolve IPv4 addresses and enumerate interfaces. MDnsAvahi: add config-driven resolution (ip-address, ip-interface), fall back to the service-provided IP, and log warnings when an interface is missing or has no IPv4 address. Update reference.conf to expose new mdns.ip-address and mdns.ip-interface settings. --- .../uni_meter/common/utils/NetUtils.java | 50 ++++++++++++++++++ .../deigmueller/uni_meter/mdns/MDnsAvahi.java | 52 ++++++++++++++++--- src/main/resources/reference.conf | 18 ++++--- 3 files changed, 104 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/deigmueller/uni_meter/common/utils/NetUtils.java b/src/main/java/com/deigmueller/uni_meter/common/utils/NetUtils.java index 72f1d64..933eca9 100644 --- a/src/main/java/com/deigmueller/uni_meter/common/utils/NetUtils.java +++ b/src/main/java/com/deigmueller/uni_meter/common/utils/NetUtils.java @@ -4,6 +4,7 @@ import org.jetbrains.annotations.Nullable; import java.net.DatagramSocket; +import java.net.Inet4Address; import java.net.InetAddress; import java.net.NetworkInterface; import java.util.Iterator; @@ -20,6 +21,55 @@ public class NetUtils { return "127.0.0.1"; } } + + /** + * Detects the IP address associated with the specified network interface. + * @param interfaceName the name of the network interface (e.g., "eth0", "wlan0") + * @return the IP address as a string, or null if the interface is not found, not up, or has no valid IPv4 address + */ + public static @Nullable String detectIpAddressFromInterface(@NotNull String interfaceName) { + try { + NetworkInterface networkInterface = NetworkInterface.getByName(interfaceName); + if (networkInterface == null || !networkInterface.isUp()) { + return null; + } + + Iterator iterator = networkInterface.getInetAddresses().asIterator(); + while (iterator.hasNext()) { + InetAddress address = iterator.next(); + if (address instanceof Inet4Address && !address.isLoopbackAddress()) { + return address.getHostAddress(); + } + } + } catch (Exception e) { + // ignore + } + + return null; + } + + /** + * Lists the names of all network interfaces available on the system. + * @return a list of network interface names, sorted alphabetically + */ + public static @NotNull List listNetworkInterfaceNames() { + Set names = new TreeSet<>(); + + try { + Iterator iterator = NetworkInterface.getNetworkInterfaces().asIterator(); + while (iterator.hasNext()) { + try { + names.add(iterator.next().getName()); + } catch (Exception e) { + // ignore + } + } + } catch (Exception e) { + // ignore + } + + return names.stream().toList(); + } public static @Nullable String detectPrimaryMacAddress() { Set macAddresses = new TreeSet<>(); diff --git a/src/main/java/com/deigmueller/uni_meter/mdns/MDnsAvahi.java b/src/main/java/com/deigmueller/uni_meter/mdns/MDnsAvahi.java index b41c8d1..7be0dd3 100644 --- a/src/main/java/com/deigmueller/uni_meter/mdns/MDnsAvahi.java +++ b/src/main/java/com/deigmueller/uni_meter/mdns/MDnsAvahi.java @@ -1,5 +1,6 @@ package com.deigmueller.uni_meter.mdns; +import com.deigmueller.uni_meter.common.utils.NetUtils; import com.typesafe.config.Config; import org.apache.commons.lang3.StringUtils; import org.apache.pekko.actor.typed.Behavior; @@ -8,7 +9,6 @@ import org.apache.pekko.actor.typed.javadsl.Behaviors; import org.apache.pekko.actor.typed.javadsl.ReceiveBuilder; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import scala.concurrent.ExecutionContextExecutor; @@ -18,9 +18,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.time.Duration; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.concurrent.CompletableFuture; public class MDnsAvahi extends MDnsKind { @@ -34,6 +32,8 @@ public class MDnsAvahi extends MDnsKind { private final Set registeredServers = new HashSet<>(); private final Set startedPublishers = new HashSet<>(); private final boolean enableAvahiPublish; + private final String configuredIpAddress; + private final String configuredIpInterface; private String avahiPublishBinary; public static Behavior create(@NotNull Config config) { @@ -45,6 +45,8 @@ protected MDnsAvahi(@NotNull ActorContext context, super(context, config); enableAvahiPublish = config.getBoolean("enable-avahi-publish"); + configuredIpAddress = StringUtils.trimToEmpty(config.getString("ip-address")); + configuredIpInterface = StringUtils.trimToEmpty(config.getString("ip-interface")); avahiPublishBinary = config.getString("avahi-publish"); if (enableAvahiPublish && StringUtils.isBlank(avahiPublishBinary)) { findAvahiPublishBinary(); @@ -179,11 +181,45 @@ private Behavior onAvahiPublishFailed(AvahiPublishFailed message) { private void registerServer(@NotNull RegisterService registerService) { LOGGER.trace("MDnsAvahi.registerServer()"); - if (registeredServers.add(new NameAndIpAddress(registerService.name(), registerService.ipAddress()))) { - if (enableAvahiPublish && !StringUtils.isAllBlank(avahiPublishBinary)) { - startAvahiPublish(registerService.server(), registerService.ipAddress()); + String ipAddress = resolvePublishIpAddress(registerService); + + if (registeredServers.add(new NameAndIpAddress(registerService.name(), ipAddress)) // + && enableAvahiPublish // + && !StringUtils.isAllBlank(avahiPublishBinary)) { + startAvahiPublish(registerService.server(), ipAddress); + } + + } + + /** + * Resolve the IP address to publish for the specified server, based on the configuration and the server information + * @param registerService Server for which to resolve the publish IP address + * @return Resolved IP address to publish for the specified server + */ + private @NotNull String resolvePublishIpAddress(@NotNull RegisterService registerService) { + if (StringUtils.isNotBlank(configuredIpAddress)) { + return configuredIpAddress; + } + + if (StringUtils.isNotBlank(configuredIpInterface)) { + List availableInterfaces = NetUtils.listNetworkInterfaceNames(); + if (!availableInterfaces.contains(configuredIpInterface)) { + LOGGER.warn("configured mdns interface '{}' not found. available interfaces: {}", + configuredIpInterface, + String.join(", ", availableInterfaces)); + return registerService.ipAddress(); } + + String ipAddress = NetUtils.detectIpAddressFromInterface(configuredIpInterface); + if (StringUtils.isNotBlank(ipAddress)) { + return ipAddress; + } + LOGGER.warn("failed to resolve IPv4 address for mdns interface '{}', falling back to {}", + configuredIpInterface, + registerService.ipAddress()); } + + return registerService.ipAddress(); } private void startAvahiPublishing() { @@ -307,4 +343,4 @@ private enum RestartAvahiPublish implements Command { } private record NameAndIpAddress(String name, String ipAddress) {} -} \ No newline at end of file +} diff --git a/src/main/resources/reference.conf b/src/main/resources/reference.conf index fe880cd..13a81f3 100644 --- a/src/main/resources/reference.conf +++ b/src/main/resources/reference.conf @@ -21,13 +21,15 @@ uni-meter { bind-retry-backoff = 15s } - mdns { - type = "auto" - url = ${?UNI_HA_URL} - access-token = ${?UNI_HA_ACCESS_TOKEN} - enable-avahi-publish = true - avahi-publish = "" - } + mdns { + type = "auto" + ip-address = "" + ip-interface = "" + url = ${?UNI_HA_URL} + access-token = ${?UNI_HA_ACCESS_TOKEN} + enable-avahi-publish = true + avahi-publish = "" + } output-devices { common { @@ -358,4 +360,4 @@ pekko.http { server { remote-address-header = on } -} \ No newline at end of file +} From 5c4bcf1d3ff2f4c09cb0f170532f03213acae703 Mon Sep 17 00:00:00 2001 From: Thimo Seitz Date: Thu, 26 Feb 2026 23:16:34 +0100 Subject: [PATCH 02/23] Update MDnsAvahi.java --- .../deigmueller/uni_meter/mdns/MDnsAvahi.java | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/deigmueller/uni_meter/mdns/MDnsAvahi.java b/src/main/java/com/deigmueller/uni_meter/mdns/MDnsAvahi.java index 7be0dd3..d701939 100644 --- a/src/main/java/com/deigmueller/uni_meter/mdns/MDnsAvahi.java +++ b/src/main/java/com/deigmueller/uni_meter/mdns/MDnsAvahi.java @@ -1,7 +1,19 @@ package com.deigmueller.uni_meter.mdns; -import com.deigmueller.uni_meter.common.utils.NetUtils; -import com.typesafe.config.Config; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Duration; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; import org.apache.commons.lang3.StringUtils; import org.apache.pekko.actor.typed.Behavior; import org.apache.pekko.actor.typed.DispatcherSelector; @@ -9,18 +21,13 @@ import org.apache.pekko.actor.typed.javadsl.Behaviors; import org.apache.pekko.actor.typed.javadsl.ReceiveBuilder; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.deigmueller.uni_meter.common.utils.NetUtils; +import com.typesafe.config.Config; import scala.concurrent.ExecutionContextExecutor; -import java.io.*; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.time.Duration; -import java.util.*; -import java.util.concurrent.CompletableFuture; - public class MDnsAvahi extends MDnsKind { // Class members private static final Logger LOGGER = LoggerFactory.getLogger("uni-meter.mdns.avahi"); From bffc3ca0d56d17f27e7cbdde4cf5e6f489a65e29 Mon Sep 17 00:00:00 2001 From: Thimo Seitz Date: Thu, 26 Feb 2026 23:26:44 +0100 Subject: [PATCH 03/23] Upgrade templating plugin; relocate Version Bump templating-maven-plugin from 1.0.0 to 3.1.0 in pom.xml. Rename/move Version.java into src/main/java-templates/com/deigmueller/uni_meter/application/ so the template file resides in the package directory (no content changes). Fixes compile failure in eclipse --- pom.xml | 2 +- .../{ => com/deigmueller/uni_meter/application}/Version.java | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/main/java-templates/{ => com/deigmueller/uni_meter/application}/Version.java (100%) diff --git a/pom.xml b/pom.xml index b43c07c..e8b6508 100644 --- a/pom.xml +++ b/pom.xml @@ -230,7 +230,7 @@ org.codehaus.mojo templating-maven-plugin - 1.0.0 + 3.1.0 generate-version-class diff --git a/src/main/java-templates/Version.java b/src/main/java-templates/com/deigmueller/uni_meter/application/Version.java similarity index 100% rename from src/main/java-templates/Version.java rename to src/main/java-templates/com/deigmueller/uni_meter/application/Version.java From 8e7f3ea84d37cf66e0fcbc5ad6e14eb2567ba9d4 Mon Sep 17 00:00:00 2001 From: Thimo Seitz Date: Thu, 26 Feb 2026 23:42:02 +0100 Subject: [PATCH 04/23] Update NetUtils.java --- .../uni_meter/common/utils/NetUtils.java | 98 +++++++++++-------- 1 file changed, 55 insertions(+), 43 deletions(-) diff --git a/src/main/java/com/deigmueller/uni_meter/common/utils/NetUtils.java b/src/main/java/com/deigmueller/uni_meter/common/utils/NetUtils.java index 933eca9..0202310 100644 --- a/src/main/java/com/deigmueller/uni_meter/common/utils/NetUtils.java +++ b/src/main/java/com/deigmueller/uni_meter/common/utils/NetUtils.java @@ -1,16 +1,17 @@ package com.deigmueller.uni_meter.common.utils; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - import java.net.DatagramSocket; import java.net.Inet4Address; import java.net.InetAddress; import java.net.NetworkInterface; +import java.net.SocketException; +import java.util.Collections; import java.util.Iterator; import java.util.List; -import java.util.Set; -import java.util.TreeSet; +import java.util.Objects; +import org.apache.commons.lang3.stream.Streams; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; public class NetUtils { public static @NotNull String detectPrimaryIpAddress() { @@ -53,52 +54,63 @@ public class NetUtils { * @return a list of network interface names, sorted alphabetically */ public static @NotNull List listNetworkInterfaceNames() { - Set names = new TreeSet<>(); + try { + return Streams.of(NetworkInterface.getNetworkInterfaces()).map(NetworkInterface::getName).sorted().toList(); + } catch (SocketException e) { + // We don't care about the exception, we just want to return an empty list in this case + } + return Collections.emptyList(); + } + /** + * Checks if the given network interface is a loopback or virtual interface. + * @param networkInterface the network interface to check + * @return true if the interface is a loopback or virtual interface, false otherwise + */ + private static boolean isLoopbackOrVirtual(NetworkInterface networkInterface) { try { - Iterator iterator = NetworkInterface.getNetworkInterfaces().asIterator(); - while (iterator.hasNext()) { - try { - names.add(iterator.next().getName()); - } catch (Exception e) { - // ignore - } - } + return networkInterface.isLoopback() || networkInterface.isVirtual(); } catch (Exception e) { - // ignore + // We don't care + return false; } - - return names.stream().toList(); } - public static @Nullable String detectPrimaryMacAddress() { - Set macAddresses = new TreeSet<>(); - + /** + * Converts the hardware address (MAC address) of a network interface to a string representation. + * @param networkInterface the network interface whose hardware address is to be converted + * @return the string representation of the hardware address, or null if the hardware address cannot be retrieved + */ + public static String hardwareAddressToString(NetworkInterface networkInterface ) { try { - Iterator iterator = NetworkInterface.getNetworkInterfaces().asIterator(); - while (iterator.hasNext()) { - try { - NetworkInterface networkInterface = iterator.next(); - if (networkInterface.isUp() && !networkInterface.isLoopback() && !networkInterface.isVirtual()) { - byte[] macAddress = networkInterface.getHardwareAddress(); - if (macAddress != null) { - StringBuilder macAddressString = new StringBuilder(); - for (byte address : macAddress) { - macAddressString.append(String.format("%02X", address)); - } - macAddresses.add(macAddressString.toString()); - } - } - } catch (Exception e) { - // ignore - } + byte[] macAddress = networkInterface.getHardwareAddress(); + StringBuilder macAddressString = new StringBuilder(); + for (byte address : macAddress) { + macAddressString.append(String.format("%02X", address)); } - } catch (Exception e) { - // ignore + return macAddressString.toString(); + } catch(SocketException e) { + return null; + } + } + + /** + * Detects the primary MAC address of the system by iterating through all network interfaces and returning the last valid MAC address found. + * Loopback and virtual interfaces are ignored. + * @return the primary MAC address as a string, or null if no valid MAC address is found + */ + + public static @Nullable String detectPrimaryMacAddress() { + try { + var foundAddresses = Streams.of(NetworkInterface.getNetworkInterfaces()) // + .filter(n -> !isLoopbackOrVirtual(n)) // filter out loopback and virtual interfaces + .map(NetUtils::hardwareAddressToString) // convert to string representation of the MAC address + .filter(Objects::nonNull) // filter out interfaces without a hardware address + .toList(); + return foundAddresses.isEmpty() ? null : foundAddresses.get(foundAddresses.size() - 1); + } catch (SocketException e) { + // We dont't care + return null; } - - List list = macAddresses.stream().toList(); - - return list.isEmpty() ? null : list.get(list.size() - 1); } } From 748a2500f0536f39c6dbd6b7476486bae14174d6 Mon Sep 17 00:00:00 2001 From: Thimo Seitz Date: Thu, 26 Feb 2026 23:42:10 +0100 Subject: [PATCH 05/23] Update BareMetal.md --- doc/install/BareMetal.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/doc/install/BareMetal.md b/doc/install/BareMetal.md index ac800fa..e9c7369 100644 --- a/doc/install/BareMetal.md +++ b/doc/install/BareMetal.md @@ -168,5 +168,18 @@ sudo systemctl start avahi-daemon Starting with `uni-meter` version 1.1.5, a running avahi daemon is automatically detected and all necessary configuration files will be automatically created in `/etc/avahi/service`. +If the host has multiple network interfaces, you can force which IPv4 address is announced via mDNS. +`ip-address` has the highest priority. If it is empty and `ip-interface` is set, `uni-meter` resolves the IPv4 +address of that interface. If both are empty (default), the output device address is used. + +```hocon +uni-meter { + mdns { + type = "auto" + ip-address = "" + ip-interface = "" + } +} +``` From ad2bec51862471233d036ce092239c7f469d3dbd Mon Sep 17 00:00:00 2001 From: Thimo Seitz Date: Thu, 26 Feb 2026 23:50:38 +0100 Subject: [PATCH 06/23] Update NetUtils.java --- .../uni_meter/common/utils/NetUtils.java | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/deigmueller/uni_meter/common/utils/NetUtils.java b/src/main/java/com/deigmueller/uni_meter/common/utils/NetUtils.java index 0202310..4e340d7 100644 --- a/src/main/java/com/deigmueller/uni_meter/common/utils/NetUtils.java +++ b/src/main/java/com/deigmueller/uni_meter/common/utils/NetUtils.java @@ -6,7 +6,6 @@ import java.net.NetworkInterface; import java.net.SocketException; import java.util.Collections; -import java.util.Iterator; import java.util.List; import java.util.Objects; import org.apache.commons.lang3.stream.Streams; @@ -34,18 +33,14 @@ public class NetUtils { if (networkInterface == null || !networkInterface.isUp()) { return null; } - - Iterator iterator = networkInterface.getInetAddresses().asIterator(); - while (iterator.hasNext()) { - InetAddress address = iterator.next(); - if (address instanceof Inet4Address && !address.isLoopbackAddress()) { - return address.getHostAddress(); - } - } + return Streams.of(networkInterface.getInetAddresses()) // + .filter(Inet4Address.class::isInstance) // + .filter(a -> !a.isLoopbackAddress()) // + .map(InetAddress::getHostAddress) // + .findFirst().orElse(null); } catch (Exception e) { - // ignore + // We don't care } - return null; } From 67ff1c3ce63ffb715aafa96945bdb43ec404a245 Mon Sep 17 00:00:00 2001 From: Thimo Seitz Date: Thu, 26 Feb 2026 23:51:47 +0100 Subject: [PATCH 07/23] Update NetUtils.java --- .../uni_meter/common/utils/NetUtils.java | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/main/java/com/deigmueller/uni_meter/common/utils/NetUtils.java b/src/main/java/com/deigmueller/uni_meter/common/utils/NetUtils.java index 4e340d7..dcf2ffb 100644 --- a/src/main/java/com/deigmueller/uni_meter/common/utils/NetUtils.java +++ b/src/main/java/com/deigmueller/uni_meter/common/utils/NetUtils.java @@ -22,6 +22,25 @@ public class NetUtils { } } + /** + * Detects the primary MAC address of the system by iterating through all network interfaces and returning the last valid MAC address found. + * Loopback and virtual interfaces are ignored. + * @return the primary MAC address as a string, or null if no valid MAC address is found + */ + public static @Nullable String detectPrimaryMacAddress() { + try { + var foundAddresses = Streams.of(NetworkInterface.getNetworkInterfaces()) // + .filter(n -> !isLoopbackOrVirtual(n)) // filter out loopback and virtual interfaces + .map(NetUtils::hardwareAddressToString) // convert to string representation of the MAC address + .filter(Objects::nonNull) // filter out interfaces without a hardware address + .toList(); + return foundAddresses.isEmpty() ? null : foundAddresses.get(foundAddresses.size() - 1); + } catch (SocketException e) { + // We dont't care + return null; + } + } + /** * Detects the IP address associated with the specified network interface. * @param interfaceName the name of the network interface (e.g., "eth0", "wlan0") @@ -89,23 +108,4 @@ public static String hardwareAddressToString(NetworkInterface networkInterface ) } } - /** - * Detects the primary MAC address of the system by iterating through all network interfaces and returning the last valid MAC address found. - * Loopback and virtual interfaces are ignored. - * @return the primary MAC address as a string, or null if no valid MAC address is found - */ - - public static @Nullable String detectPrimaryMacAddress() { - try { - var foundAddresses = Streams.of(NetworkInterface.getNetworkInterfaces()) // - .filter(n -> !isLoopbackOrVirtual(n)) // filter out loopback and virtual interfaces - .map(NetUtils::hardwareAddressToString) // convert to string representation of the MAC address - .filter(Objects::nonNull) // filter out interfaces without a hardware address - .toList(); - return foundAddresses.isEmpty() ? null : foundAddresses.get(foundAddresses.size() - 1); - } catch (SocketException e) { - // We dont't care - return null; - } - } } From 30773e41f51a1390f63f2c308891537fa4c9c7ec Mon Sep 17 00:00:00 2001 From: Thimo Seitz Date: Fri, 27 Feb 2026 00:00:44 +0100 Subject: [PATCH 08/23] Create some unit tests --- pom.xml | 24 ++++- .../uni_meter/common/utils/NetUtils.java | 2 +- .../uni_meter/common/utils/NetUtilsTest.java | 36 +++++++ .../uni_meter/mdns/MDnsAvahiTest.java | 102 ++++++++++++++++++ 4 files changed, 162 insertions(+), 2 deletions(-) create mode 100644 src/test/java/com/deigmueller/uni_meter/common/utils/NetUtilsTest.java create mode 100644 src/test/java/com/deigmueller/uni_meter/mdns/MDnsAvahiTest.java diff --git a/pom.xml b/pom.xml index e8b6508..29b3188 100644 --- a/pom.xml +++ b/pom.xml @@ -22,6 +22,8 @@ 1.0.2 2.13 5.8.2 + 5.21.0 + 3.5.5 @@ -168,6 +170,18 @@ ${pekko.version} test + + org.mockito + mockito-core + ${mockito.version} + test + + + org.mockito + mockito-junit-jupiter + ${mockito.version} + test + @@ -226,7 +240,15 @@ - + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + + @{argLine} -XX:+EnableDynamicAgentLoading + + org.codehaus.mojo templating-maven-plugin diff --git a/src/main/java/com/deigmueller/uni_meter/common/utils/NetUtils.java b/src/main/java/com/deigmueller/uni_meter/common/utils/NetUtils.java index dcf2ffb..40fd971 100644 --- a/src/main/java/com/deigmueller/uni_meter/common/utils/NetUtils.java +++ b/src/main/java/com/deigmueller/uni_meter/common/utils/NetUtils.java @@ -89,7 +89,7 @@ private static boolean isLoopbackOrVirtual(NetworkInterface networkInterface) { return false; } } - + /** * Converts the hardware address (MAC address) of a network interface to a string representation. * @param networkInterface the network interface whose hardware address is to be converted diff --git a/src/test/java/com/deigmueller/uni_meter/common/utils/NetUtilsTest.java b/src/test/java/com/deigmueller/uni_meter/common/utils/NetUtilsTest.java new file mode 100644 index 0000000..17f7180 --- /dev/null +++ b/src/test/java/com/deigmueller/uni_meter/common/utils/NetUtilsTest.java @@ -0,0 +1,36 @@ +package com.deigmueller.uni_meter.common.utils; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +class NetUtilsTest { + + @Test + @DisplayName("detectIpAddressFromInterface returns null for unknown interface") + void detectIpAddressFromInterfaceReturnsNullForUnknownInterface() { + String ipAddress = NetUtils.detectIpAddressFromInterface("definitely-not-a-real-interface-1234"); + + assertThat(ipAddress, is((String) null)); + } + + @Test + @DisplayName("listNetworkInterfaceNames returns a sorted list") + void listNetworkInterfaceNamesReturnsSortedList() { + List interfaceNames = NetUtils.listNetworkInterfaceNames(); + + assertThat(interfaceNames, is(notNullValue())); + + List sortedInterfaceNames = new ArrayList<>(interfaceNames); + Collections.sort(sortedInterfaceNames); + + assertThat(interfaceNames, is(sortedInterfaceNames)); + } +} diff --git a/src/test/java/com/deigmueller/uni_meter/mdns/MDnsAvahiTest.java b/src/test/java/com/deigmueller/uni_meter/mdns/MDnsAvahiTest.java new file mode 100644 index 0000000..6e28830 --- /dev/null +++ b/src/test/java/com/deigmueller/uni_meter/mdns/MDnsAvahiTest.java @@ -0,0 +1,102 @@ +package com.deigmueller.uni_meter.mdns; + +import com.deigmueller.uni_meter.common.utils.NetUtils; +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; +import org.apache.pekko.actor.typed.javadsl.ActorContext; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import java.lang.reflect.Method; +import java.util.List; +import java.util.Map; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +class MDnsAvahiTest { + + @Test + @DisplayName("resolvePublishIpAddress uses configured ip-address with highest priority") + void resolvePublishIpAddressUsesConfiguredIpAddress() throws Exception { + MDnsAvahi mdnsAvahi = createMdnsAvahi(" 192.168.1.10 ", ""); + + String result = invokeResolvePublishIpAddress(mdnsAvahi, createRegisterService("10.0.0.5")); + + assertThat(result, is("192.168.1.10")); + } + + @Test + @DisplayName("resolvePublishIpAddress resolves IP from configured interface") + void resolvePublishIpAddressUsesConfiguredIpInterface() throws Exception { + MDnsAvahi mdnsAvahi = createMdnsAvahi("", "eth0"); + + try (MockedStatic mockedNetUtils = Mockito.mockStatic(NetUtils.class)) { + mockedNetUtils.when(NetUtils::listNetworkInterfaceNames).thenReturn(List.of("eth0", "wlan0")); + mockedNetUtils.when(() -> NetUtils.detectIpAddressFromInterface("eth0")).thenReturn("192.168.178.22"); + + String result = invokeResolvePublishIpAddress(mdnsAvahi, createRegisterService("10.0.0.5")); + + assertThat(result, is("192.168.178.22")); + } + } + + @Test + @DisplayName("resolvePublishIpAddress falls back when configured interface is missing") + void resolvePublishIpAddressFallsBackForMissingInterface() throws Exception { + MDnsAvahi mdnsAvahi = createMdnsAvahi("", "eth9"); + + try (MockedStatic mockedNetUtils = Mockito.mockStatic(NetUtils.class)) { + mockedNetUtils.when(NetUtils::listNetworkInterfaceNames).thenReturn(List.of("eth0", "wlan0")); + + String result = invokeResolvePublishIpAddress(mdnsAvahi, createRegisterService("10.0.0.5")); + + assertThat(result, is("10.0.0.5")); + } + } + + @Test + @DisplayName("resolvePublishIpAddress falls back when interface has no IPv4 address") + void resolvePublishIpAddressFallsBackForInterfaceWithoutIpv4() throws Exception { + MDnsAvahi mdnsAvahi = createMdnsAvahi("", "eth0"); + + try (MockedStatic mockedNetUtils = Mockito.mockStatic(NetUtils.class)) { + mockedNetUtils.when(NetUtils::listNetworkInterfaceNames).thenReturn(List.of("eth0", "wlan0")); + mockedNetUtils.when(() -> NetUtils.detectIpAddressFromInterface("eth0")).thenReturn(null); + + String result = invokeResolvePublishIpAddress(mdnsAvahi, createRegisterService("10.0.0.5")); + + assertThat(result, is("10.0.0.5")); + } + } + + @Test + @DisplayName("resolvePublishIpAddress falls back to registerService ip when nothing is configured") + void resolvePublishIpAddressFallsBackToServiceIp() throws Exception { + MDnsAvahi mdnsAvahi = createMdnsAvahi("", ""); + + String result = invokeResolvePublishIpAddress(mdnsAvahi, createRegisterService("10.0.0.5")); + + assertThat(result, is("10.0.0.5")); + } + + private static MDnsAvahi createMdnsAvahi(String ipAddress, String ipInterface) { + Config config = + ConfigFactory.parseString("enable-avahi-publish = false\n" + "ip-address = \"" + ipAddress + "\"\n" + "ip-interface = \"" + ipInterface + "\"\n" + "avahi-publish = \"\""); + @SuppressWarnings("unchecked") + ActorContext context = Mockito.mock(ActorContext.class); + return new MDnsAvahi(context, config); + } + + private static MDnsKind.RegisterService createRegisterService(String ipAddress) { + return new MDnsKind.RegisterService("_shelly._tcp", "shellypro3em-123456", 80, Map.of("app", "shellypro3em"), "shellypro3em-123456", ipAddress); + } + + private static String invokeResolvePublishIpAddress(MDnsAvahi mdnsAvahi, MDnsKind.RegisterService registerService) throws Exception { + Method resolveMethod = MDnsAvahi.class.getDeclaredMethod("resolvePublishIpAddress", MDnsKind.RegisterService.class); + resolveMethod.setAccessible(true); + return (String) resolveMethod.invoke(mdnsAvahi, registerService); + } +} From 741a5cbd11bdd34f14c43b44b5b4db54db170be5 Mon Sep 17 00:00:00 2001 From: Thimo Seitz Date: Wed, 18 Mar 2026 17:02:47 +0100 Subject: [PATCH 09/23] Update pom.xml Remove @{argLine} --- pom.xml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 29b3188..eb73b98 100644 --- a/pom.xml +++ b/pom.xml @@ -24,6 +24,7 @@ 5.8.2 5.21.0 3.5.5 + 3.1.0 @@ -246,13 +247,13 @@ ${maven-surefire-plugin.version} - @{argLine} -XX:+EnableDynamicAgentLoading + -XX:+EnableDynamicAgentLoading org.codehaus.mojo templating-maven-plugin - 3.1.0 + ${templating-maven-plugin.version} generate-version-class From 86ded622f496a4a2aadad24f5ff227a7834baca7 Mon Sep 17 00:00:00 2001 From: Thimo Seitz Date: Wed, 18 Mar 2026 17:16:10 +0100 Subject: [PATCH 10/23] Move mDNS IP resolution to OutputDevice Centralize announced IP resolution logic into OutputDevice (resolveAnnouncedIpAddress) and remove the old resolvePublishIpAddress from MDnsAvahi. Devices (EcoTracker, ShellyPro3EM) now use getAnnouncedIpAddress() instead of calling NetUtils.detectPrimaryIpAddress() directly. MDnsAvahi no longer keeps configured ip/interface fields and uses the RegisterService IP when publishing; avahi resolution logic was consolidated and NetUtils usage is mocked in updated tests. This reduces duplication and provides a consistent fallback order: configured ip-address -> ip-interface (if valid) -> primary detected IP. --- .../deigmueller/uni_meter/mdns/MDnsAvahi.java | 40 +-------- .../uni_meter/output/OutputDevice.java | 37 ++++++++- .../output/device/eco_tracker/EcoTracker.java | 4 +- .../output/device/shelly/ShellyPro3EM.java | 6 +- .../uni_meter/mdns/MDnsAvahiTest.java | 81 +++++++++---------- 5 files changed, 78 insertions(+), 90 deletions(-) diff --git a/src/main/java/com/deigmueller/uni_meter/mdns/MDnsAvahi.java b/src/main/java/com/deigmueller/uni_meter/mdns/MDnsAvahi.java index d701939..0b6974a 100644 --- a/src/main/java/com/deigmueller/uni_meter/mdns/MDnsAvahi.java +++ b/src/main/java/com/deigmueller/uni_meter/mdns/MDnsAvahi.java @@ -10,7 +10,6 @@ import java.nio.file.Paths; import java.time.Duration; import java.util.HashSet; -import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.CompletableFuture; @@ -24,7 +23,6 @@ import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.deigmueller.uni_meter.common.utils.NetUtils; import com.typesafe.config.Config; import scala.concurrent.ExecutionContextExecutor; @@ -39,8 +37,6 @@ public class MDnsAvahi extends MDnsKind { private final Set registeredServers = new HashSet<>(); private final Set startedPublishers = new HashSet<>(); private final boolean enableAvahiPublish; - private final String configuredIpAddress; - private final String configuredIpInterface; private String avahiPublishBinary; public static Behavior create(@NotNull Config config) { @@ -52,8 +48,6 @@ protected MDnsAvahi(@NotNull ActorContext context, super(context, config); enableAvahiPublish = config.getBoolean("enable-avahi-publish"); - configuredIpAddress = StringUtils.trimToEmpty(config.getString("ip-address")); - configuredIpInterface = StringUtils.trimToEmpty(config.getString("ip-interface")); avahiPublishBinary = config.getString("avahi-publish"); if (enableAvahiPublish && StringUtils.isBlank(avahiPublishBinary)) { findAvahiPublishBinary(); @@ -188,45 +182,13 @@ private Behavior onAvahiPublishFailed(AvahiPublishFailed message) { private void registerServer(@NotNull RegisterService registerService) { LOGGER.trace("MDnsAvahi.registerServer()"); - String ipAddress = resolvePublishIpAddress(registerService); + String ipAddress = registerService.ipAddress(); if (registeredServers.add(new NameAndIpAddress(registerService.name(), ipAddress)) // && enableAvahiPublish // && !StringUtils.isAllBlank(avahiPublishBinary)) { startAvahiPublish(registerService.server(), ipAddress); } - - } - - /** - * Resolve the IP address to publish for the specified server, based on the configuration and the server information - * @param registerService Server for which to resolve the publish IP address - * @return Resolved IP address to publish for the specified server - */ - private @NotNull String resolvePublishIpAddress(@NotNull RegisterService registerService) { - if (StringUtils.isNotBlank(configuredIpAddress)) { - return configuredIpAddress; - } - - if (StringUtils.isNotBlank(configuredIpInterface)) { - List availableInterfaces = NetUtils.listNetworkInterfaceNames(); - if (!availableInterfaces.contains(configuredIpInterface)) { - LOGGER.warn("configured mdns interface '{}' not found. available interfaces: {}", - configuredIpInterface, - String.join(", ", availableInterfaces)); - return registerService.ipAddress(); - } - - String ipAddress = NetUtils.detectIpAddressFromInterface(configuredIpInterface); - if (StringUtils.isNotBlank(ipAddress)) { - return ipAddress; - } - LOGGER.warn("failed to resolve IPv4 address for mdns interface '{}', falling back to {}", - configuredIpInterface, - registerService.ipAddress()); - } - - return registerService.ipAddress(); } private void startAvahiPublishing() { diff --git a/src/main/java/com/deigmueller/uni_meter/output/OutputDevice.java b/src/main/java/com/deigmueller/uni_meter/output/OutputDevice.java index 8c33e85..47a46e3 100644 --- a/src/main/java/com/deigmueller/uni_meter/output/OutputDevice.java +++ b/src/main/java/com/deigmueller/uni_meter/output/OutputDevice.java @@ -1,6 +1,7 @@ package com.deigmueller.uni_meter.output; import com.deigmueller.uni_meter.application.UniMeter; +import com.deigmueller.uni_meter.common.utils.NetUtils; import com.deigmueller.uni_meter.mdns.MDnsRegistrator; import com.fasterxml.jackson.annotation.JsonInclude; import com.typesafe.config.Config; @@ -8,6 +9,7 @@ import lombok.AccessLevel; import lombok.Getter; import lombok.Setter; +import org.apache.commons.lang3.StringUtils; import org.apache.pekko.actor.typed.ActorRef; import org.apache.pekko.actor.typed.Behavior; import org.apache.pekko.actor.typed.PostStop; @@ -47,6 +49,7 @@ public abstract class OutputDevice extends AbstractBehavior clientContexts = new HashMap<>(); private Instant offUntil = Instant.MIN; @@ -86,6 +89,7 @@ protected OutputDevice(@NotNull ActorContext context, this.defaultFrequency = config.getDouble("default-frequency"); this.defaultClientPowerFactor = config.getDouble("default-client-power-factor"); this.usageConstraintInitDuration = config.getDuration("usage-constraint-init-duration"); + this.announcedIpAddress = resolveAnnouncedIpAddress(); initPowerOffsets(config); @@ -530,7 +534,38 @@ protected Map getParameters(@NotNull Map parameter protected abstract Route createRoute(); protected abstract void eventPowerDataChanged(); - + + protected @NotNull String resolveAnnouncedIpAddress() { + Config mdnsConfig = getContext().getSystem().settings().config().getConfig("uni-meter.mdns"); + return resolveAnnouncedIpAddress(mdnsConfig, logger); + } + + public static @NotNull String resolveAnnouncedIpAddress(@NotNull Config mdnsConfig, @NotNull Logger logger) { + String configuredIpAddress = StringUtils.trimToEmpty(mdnsConfig.getString("ip-address")); + if (StringUtils.isNotBlank(configuredIpAddress)) { + return configuredIpAddress; + } + + String configuredIpInterface = StringUtils.trimToEmpty(mdnsConfig.getString("ip-interface")); + if (StringUtils.isNotBlank(configuredIpInterface)) { + List availableInterfaces = NetUtils.listNetworkInterfaceNames(); + if (!availableInterfaces.contains(configuredIpInterface)) { + logger.warn("configured mdns interface '{}' not found. available interfaces: {}", + configuredIpInterface, + String.join(", ", availableInterfaces)); + } else { + String ipAddress = NetUtils.detectIpAddressFromInterface(configuredIpInterface); + if (StringUtils.isNotBlank(ipAddress)) { + return ipAddress; + } + logger.warn("failed to resolve IPv4 address for mdns interface '{}', falling back to primary address", + configuredIpInterface); + } + } + + return NetUtils.detectPrimaryIpAddress(); + } + protected void initPowerOffsets(@NotNull Config config) { offsetPhase0 = config.getDouble("power-offset-l1"); offsetPhase1 = config.getDouble("power-offset-l2"); diff --git a/src/main/java/com/deigmueller/uni_meter/output/device/eco_tracker/EcoTracker.java b/src/main/java/com/deigmueller/uni_meter/output/device/eco_tracker/EcoTracker.java index 7611a12..9ba2e20 100644 --- a/src/main/java/com/deigmueller/uni_meter/output/device/eco_tracker/EcoTracker.java +++ b/src/main/java/com/deigmueller/uni_meter/output/device/eco_tracker/EcoTracker.java @@ -222,7 +222,7 @@ protected void registerMDns() { logger.trace("EcoTracer.registerMDns()"); Map txtRecords = new HashMap<>(); - txtRecords.put("ip", NetUtils.detectPrimaryIpAddress()); + txtRecords.put("ip", getAnnouncedIpAddress()); ConfigObject mdnsObject = getConfig().getObject("mdns"); mdnsObject.forEach((key, value) -> txtRecords.put(key, value.unwrapped().toString())); @@ -234,7 +234,7 @@ protected void registerMDns() { getBindPort(), txtRecords, getDefaultHostname() + ".local", - NetUtils.detectPrimaryIpAddress() + getAnnouncedIpAddress() ) ); } diff --git a/src/main/java/com/deigmueller/uni_meter/output/device/shelly/ShellyPro3EM.java b/src/main/java/com/deigmueller/uni_meter/output/device/shelly/ShellyPro3EM.java index 2ce50e0..8aebec7 100644 --- a/src/main/java/com/deigmueller/uni_meter/output/device/shelly/ShellyPro3EM.java +++ b/src/main/java/com/deigmueller/uni_meter/output/device/shelly/ShellyPro3EM.java @@ -1444,7 +1444,7 @@ protected void registerMDns() { ConfigObject mdnsObject = getConfig().getObject("mdns"); mdnsObject.forEach((key, value) -> txtRecords.put(key, value.unwrapped().toString())); - String primaryIpAddress = NetUtils.detectPrimaryIpAddress(); + String announcedIpAddress = getAnnouncedIpAddress(); getMdnsRegistrator().tell( new MDnsRegistrator.RegisterService( @@ -1453,7 +1453,7 @@ protected void registerMDns() { getBindPort(), txtRecords, getDefaultHostname() + ".local", - primaryIpAddress + announcedIpAddress ) ); @@ -1464,7 +1464,7 @@ protected void registerMDns() { getBindPort(), txtRecords, getDefaultHostname() + ".local", - primaryIpAddress + announcedIpAddress ) ); } diff --git a/src/test/java/com/deigmueller/uni_meter/mdns/MDnsAvahiTest.java b/src/test/java/com/deigmueller/uni_meter/mdns/MDnsAvahiTest.java index 6e28830..c400ff0 100644 --- a/src/test/java/com/deigmueller/uni_meter/mdns/MDnsAvahiTest.java +++ b/src/test/java/com/deigmueller/uni_meter/mdns/MDnsAvahiTest.java @@ -1,102 +1,93 @@ package com.deigmueller.uni_meter.mdns; import com.deigmueller.uni_meter.common.utils.NetUtils; +import com.deigmueller.uni_meter.output.OutputDevice; import com.typesafe.config.Config; import com.typesafe.config.ConfigFactory; -import org.apache.pekko.actor.typed.javadsl.ActorContext; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.mockito.MockedStatic; import org.mockito.Mockito; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import java.lang.reflect.Method; import java.util.List; -import java.util.Map; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; class MDnsAvahiTest { + private static final Logger LOGGER = LoggerFactory.getLogger(MDnsAvahiTest.class); @Test - @DisplayName("resolvePublishIpAddress uses configured ip-address with highest priority") - void resolvePublishIpAddressUsesConfiguredIpAddress() throws Exception { - MDnsAvahi mdnsAvahi = createMdnsAvahi(" 192.168.1.10 ", ""); + @DisplayName("resolveAnnouncedIpAddress uses configured ip-address with highest priority") + void resolveAnnouncedIpAddressUsesConfiguredIpAddress() { + try (MockedStatic mockedNetUtils = Mockito.mockStatic(NetUtils.class)) { + mockedNetUtils.when(NetUtils::detectPrimaryIpAddress).thenReturn("10.0.0.5"); - String result = invokeResolvePublishIpAddress(mdnsAvahi, createRegisterService("10.0.0.5")); + String result = resolveAnnouncedIpAddress(" 192.168.1.10 ", ""); - assertThat(result, is("192.168.1.10")); + assertThat(result, is("192.168.1.10")); + } } @Test - @DisplayName("resolvePublishIpAddress resolves IP from configured interface") - void resolvePublishIpAddressUsesConfiguredIpInterface() throws Exception { - MDnsAvahi mdnsAvahi = createMdnsAvahi("", "eth0"); - + @DisplayName("resolveAnnouncedIpAddress resolves IP from configured interface") + void resolveAnnouncedIpAddressUsesConfiguredIpInterface() { try (MockedStatic mockedNetUtils = Mockito.mockStatic(NetUtils.class)) { mockedNetUtils.when(NetUtils::listNetworkInterfaceNames).thenReturn(List.of("eth0", "wlan0")); mockedNetUtils.when(() -> NetUtils.detectIpAddressFromInterface("eth0")).thenReturn("192.168.178.22"); - String result = invokeResolvePublishIpAddress(mdnsAvahi, createRegisterService("10.0.0.5")); + String result = resolveAnnouncedIpAddress("", "eth0"); assertThat(result, is("192.168.178.22")); } } @Test - @DisplayName("resolvePublishIpAddress falls back when configured interface is missing") - void resolvePublishIpAddressFallsBackForMissingInterface() throws Exception { - MDnsAvahi mdnsAvahi = createMdnsAvahi("", "eth9"); - + @DisplayName("resolveAnnouncedIpAddress falls back to primary IP when configured interface is missing") + void resolveAnnouncedIpAddressFallsBackForMissingInterface() { try (MockedStatic mockedNetUtils = Mockito.mockStatic(NetUtils.class)) { mockedNetUtils.when(NetUtils::listNetworkInterfaceNames).thenReturn(List.of("eth0", "wlan0")); + mockedNetUtils.when(NetUtils::detectPrimaryIpAddress).thenReturn("10.0.0.5"); - String result = invokeResolvePublishIpAddress(mdnsAvahi, createRegisterService("10.0.0.5")); + String result = resolveAnnouncedIpAddress("", "eth9"); assertThat(result, is("10.0.0.5")); } } @Test - @DisplayName("resolvePublishIpAddress falls back when interface has no IPv4 address") - void resolvePublishIpAddressFallsBackForInterfaceWithoutIpv4() throws Exception { - MDnsAvahi mdnsAvahi = createMdnsAvahi("", "eth0"); - + @DisplayName("resolveAnnouncedIpAddress falls back to primary IP when interface has no IPv4 address") + void resolveAnnouncedIpAddressFallsBackForInterfaceWithoutIpv4() { try (MockedStatic mockedNetUtils = Mockito.mockStatic(NetUtils.class)) { mockedNetUtils.when(NetUtils::listNetworkInterfaceNames).thenReturn(List.of("eth0", "wlan0")); mockedNetUtils.when(() -> NetUtils.detectIpAddressFromInterface("eth0")).thenReturn(null); + mockedNetUtils.when(NetUtils::detectPrimaryIpAddress).thenReturn("10.0.0.5"); - String result = invokeResolvePublishIpAddress(mdnsAvahi, createRegisterService("10.0.0.5")); + String result = resolveAnnouncedIpAddress("", "eth0"); assertThat(result, is("10.0.0.5")); } } @Test - @DisplayName("resolvePublishIpAddress falls back to registerService ip when nothing is configured") - void resolvePublishIpAddressFallsBackToServiceIp() throws Exception { - MDnsAvahi mdnsAvahi = createMdnsAvahi("", ""); - - String result = invokeResolvePublishIpAddress(mdnsAvahi, createRegisterService("10.0.0.5")); - - assertThat(result, is("10.0.0.5")); - } + @DisplayName("resolveAnnouncedIpAddress falls back to primary IP when nothing is configured") + void resolveAnnouncedIpAddressFallsBackToPrimaryIp() { + try (MockedStatic mockedNetUtils = Mockito.mockStatic(NetUtils.class)) { + mockedNetUtils.when(NetUtils::detectPrimaryIpAddress).thenReturn("10.0.0.5"); - private static MDnsAvahi createMdnsAvahi(String ipAddress, String ipInterface) { - Config config = - ConfigFactory.parseString("enable-avahi-publish = false\n" + "ip-address = \"" + ipAddress + "\"\n" + "ip-interface = \"" + ipInterface + "\"\n" + "avahi-publish = \"\""); - @SuppressWarnings("unchecked") - ActorContext context = Mockito.mock(ActorContext.class); - return new MDnsAvahi(context, config); - } + String result = resolveAnnouncedIpAddress("", ""); - private static MDnsKind.RegisterService createRegisterService(String ipAddress) { - return new MDnsKind.RegisterService("_shelly._tcp", "shellypro3em-123456", 80, Map.of("app", "shellypro3em"), "shellypro3em-123456", ipAddress); + assertThat(result, is("10.0.0.5")); + } } - private static String invokeResolvePublishIpAddress(MDnsAvahi mdnsAvahi, MDnsKind.RegisterService registerService) throws Exception { - Method resolveMethod = MDnsAvahi.class.getDeclaredMethod("resolvePublishIpAddress", MDnsKind.RegisterService.class); - resolveMethod.setAccessible(true); - return (String) resolveMethod.invoke(mdnsAvahi, registerService); + private static String resolveAnnouncedIpAddress(String ipAddress, String ipInterface) { + Config mdnsConfig = ConfigFactory.parseString(""" + ip-address = "%s" + ip-interface = "%s" + """.formatted(ipAddress, ipInterface)); + return OutputDevice.resolveAnnouncedIpAddress(mdnsConfig, LOGGER); } -} +} From 47cdaf5c7f6693f2a51d8bba680c2fb6780bc266 Mon Sep 17 00:00:00 2001 From: Thimo Seitz Date: Wed, 18 Mar 2026 17:41:31 +0100 Subject: [PATCH 11/23] Add mDNS IP fallback and resolve overloads Refactor how the announced mDNS IP is resolved: introduce a new static overload resolveAnnouncedIpAddress(..., fallbackIpAddress) and an instance helper resolveMdnsFallbackIpAddress() that prefers a configured bind interface (if not wildcard) before falling back to NetUtils.detectPrimaryIpAddress(). The instance method now passes that fallback into the static resolver. Also adjust the log text when interface resolution fails. This ensures a configurable and deterministic fallback IP for mDNS announcements. --- .../uni_meter/output/OutputDevice.java | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/deigmueller/uni_meter/output/OutputDevice.java b/src/main/java/com/deigmueller/uni_meter/output/OutputDevice.java index 47a46e3..59c0704 100644 --- a/src/main/java/com/deigmueller/uni_meter/output/OutputDevice.java +++ b/src/main/java/com/deigmueller/uni_meter/output/OutputDevice.java @@ -537,10 +537,16 @@ protected Map getParameters(@NotNull Map parameter protected @NotNull String resolveAnnouncedIpAddress() { Config mdnsConfig = getContext().getSystem().settings().config().getConfig("uni-meter.mdns"); - return resolveAnnouncedIpAddress(mdnsConfig, logger); + return resolveAnnouncedIpAddress(mdnsConfig, logger, resolveMdnsFallbackIpAddress()); } public static @NotNull String resolveAnnouncedIpAddress(@NotNull Config mdnsConfig, @NotNull Logger logger) { + return resolveAnnouncedIpAddress(mdnsConfig, logger, NetUtils.detectPrimaryIpAddress()); + } + + public static @NotNull String resolveAnnouncedIpAddress(@NotNull Config mdnsConfig, + @NotNull Logger logger, + @NotNull String fallbackIpAddress) { String configuredIpAddress = StringUtils.trimToEmpty(mdnsConfig.getString("ip-address")); if (StringUtils.isNotBlank(configuredIpAddress)) { return configuredIpAddress; @@ -558,11 +564,22 @@ protected Map getParameters(@NotNull Map parameter if (StringUtils.isNotBlank(ipAddress)) { return ipAddress; } - logger.warn("failed to resolve IPv4 address for mdns interface '{}', falling back to primary address", + logger.warn("failed to resolve IPv4 address for mdns interface '{}', falling back to configured default address", configuredIpInterface); } } + return fallbackIpAddress; + } + + private @NotNull String resolveMdnsFallbackIpAddress() { + if (config.hasPath("interface")) { + String bindAddress = StringUtils.trimToEmpty(config.getString("interface")); + if (StringUtils.isNotBlank(bindAddress) && !"0.0.0.0".equals(bindAddress) && !"::".equals(bindAddress)) { + return bindAddress; + } + } + return NetUtils.detectPrimaryIpAddress(); } From 58bf0df4a302bf906163d35260393c9990c2a05b Mon Sep 17 00:00:00 2001 From: Thimo Seitz Date: Wed, 18 Mar 2026 17:44:53 +0100 Subject: [PATCH 12/23] Update OutputDevice.java --- .../deigmueller/uni_meter/output/OutputDevice.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/deigmueller/uni_meter/output/OutputDevice.java b/src/main/java/com/deigmueller/uni_meter/output/OutputDevice.java index 59c0704..e1a745a 100644 --- a/src/main/java/com/deigmueller/uni_meter/output/OutputDevice.java +++ b/src/main/java/com/deigmueller/uni_meter/output/OutputDevice.java @@ -28,6 +28,7 @@ import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; import java.util.*; +import java.util.function.Supplier; /** * Represents an output device responsible for handling power and energy data, @@ -530,23 +531,23 @@ protected Map getParameters(@NotNull Map parameter return parameters; } - + protected abstract Route createRoute(); - + protected abstract void eventPowerDataChanged(); protected @NotNull String resolveAnnouncedIpAddress() { Config mdnsConfig = getContext().getSystem().settings().config().getConfig("uni-meter.mdns"); - return resolveAnnouncedIpAddress(mdnsConfig, logger, resolveMdnsFallbackIpAddress()); + return resolveAnnouncedIpAddress(mdnsConfig, logger, this::resolveMdnsFallbackIpAddress); } public static @NotNull String resolveAnnouncedIpAddress(@NotNull Config mdnsConfig, @NotNull Logger logger) { - return resolveAnnouncedIpAddress(mdnsConfig, logger, NetUtils.detectPrimaryIpAddress()); + return resolveAnnouncedIpAddress(mdnsConfig, logger, NetUtils::detectPrimaryIpAddress); } public static @NotNull String resolveAnnouncedIpAddress(@NotNull Config mdnsConfig, @NotNull Logger logger, - @NotNull String fallbackIpAddress) { + @NotNull Supplier fallbackIpAddressSupplier) { String configuredIpAddress = StringUtils.trimToEmpty(mdnsConfig.getString("ip-address")); if (StringUtils.isNotBlank(configuredIpAddress)) { return configuredIpAddress; @@ -569,7 +570,7 @@ protected Map getParameters(@NotNull Map parameter } } - return fallbackIpAddress; + return fallbackIpAddressSupplier.get(); } private @NotNull String resolveMdnsFallbackIpAddress() { From 5fed43f3635f6798313f7af53afcfacc93c8b230 Mon Sep 17 00:00:00 2001 From: Thimo Seitz Date: Wed, 18 Mar 2026 17:46:02 +0100 Subject: [PATCH 13/23] Update NetUtils.java --- .../com/deigmueller/uni_meter/common/utils/NetUtils.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/deigmueller/uni_meter/common/utils/NetUtils.java b/src/main/java/com/deigmueller/uni_meter/common/utils/NetUtils.java index 40fd971..61a590f 100644 --- a/src/main/java/com/deigmueller/uni_meter/common/utils/NetUtils.java +++ b/src/main/java/com/deigmueller/uni_meter/common/utils/NetUtils.java @@ -95,9 +95,12 @@ private static boolean isLoopbackOrVirtual(NetworkInterface networkInterface) { * @param networkInterface the network interface whose hardware address is to be converted * @return the string representation of the hardware address, or null if the hardware address cannot be retrieved */ - public static String hardwareAddressToString(NetworkInterface networkInterface ) { + public static @Nullable String hardwareAddressToString(NetworkInterface networkInterface ) { try { byte[] macAddress = networkInterface.getHardwareAddress(); + if (macAddress == null || macAddress.length == 0) { + return null; + } StringBuilder macAddressString = new StringBuilder(); for (byte address : macAddress) { macAddressString.append(String.format("%02X", address)); From b3cd78effbd89414a3b4070d5c76e20c7b8d5c34 Mon Sep 17 00:00:00 2001 From: Thimo Seitz Date: Wed, 18 Mar 2026 17:54:34 +0100 Subject: [PATCH 14/23] Update NetUtils.java --- .../uni_meter/common/utils/NetUtils.java | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/deigmueller/uni_meter/common/utils/NetUtils.java b/src/main/java/com/deigmueller/uni_meter/common/utils/NetUtils.java index 61a590f..715f872 100644 --- a/src/main/java/com/deigmueller/uni_meter/common/utils/NetUtils.java +++ b/src/main/java/com/deigmueller/uni_meter/common/utils/NetUtils.java @@ -8,6 +8,8 @@ import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.TreeSet; +import java.util.stream.Collectors; import org.apache.commons.lang3.stream.Streams; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -30,11 +32,11 @@ public class NetUtils { public static @Nullable String detectPrimaryMacAddress() { try { var foundAddresses = Streams.of(NetworkInterface.getNetworkInterfaces()) // - .filter(n -> !isLoopbackOrVirtual(n)) // filter out loopback and virtual interfaces - .map(NetUtils::hardwareAddressToString) // convert to string representation of the MAC address - .filter(Objects::nonNull) // filter out interfaces without a hardware address - .toList(); - return foundAddresses.isEmpty() ? null : foundAddresses.get(foundAddresses.size() - 1); + .filter(n -> !isLoopbackOrVirtual(n)) // filter out loopback and virtual interfaces + .map(NetUtils::hardwareAddressToString) // convert to string representation of the MAC address + .filter(Objects::nonNull) // filter out interfaces without a hardware address + .collect(Collectors.toCollection(TreeSet::new)); // collect the results + return foundAddresses.isEmpty() ? null : foundAddresses.last(); } catch (SocketException e) { // We dont't care return null; @@ -95,7 +97,7 @@ private static boolean isLoopbackOrVirtual(NetworkInterface networkInterface) { * @param networkInterface the network interface whose hardware address is to be converted * @return the string representation of the hardware address, or null if the hardware address cannot be retrieved */ - public static @Nullable String hardwareAddressToString(NetworkInterface networkInterface ) { + private static @Nullable String hardwareAddressToString(NetworkInterface networkInterface) { try { byte[] macAddress = networkInterface.getHardwareAddress(); if (macAddress == null || macAddress.length == 0) { From 7c0413dcf113f296508714398fa0aeeeca3bd56c Mon Sep 17 00:00:00 2001 From: Thimo Seitz Date: Wed, 18 Mar 2026 18:00:05 +0100 Subject: [PATCH 15/23] Update OutputDevice.java --- .../com/deigmueller/uni_meter/output/OutputDevice.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/deigmueller/uni_meter/output/OutputDevice.java b/src/main/java/com/deigmueller/uni_meter/output/OutputDevice.java index e1a745a..36722f2 100644 --- a/src/main/java/com/deigmueller/uni_meter/output/OutputDevice.java +++ b/src/main/java/com/deigmueller/uni_meter/output/OutputDevice.java @@ -10,6 +10,7 @@ import lombok.Getter; import lombok.Setter; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Strings; import org.apache.pekko.actor.typed.ActorRef; import org.apache.pekko.actor.typed.Behavior; import org.apache.pekko.actor.typed.PostStop; @@ -38,6 +39,10 @@ @Getter(AccessLevel.PROTECTED) @Setter(AccessLevel.PROTECTED) public abstract class OutputDevice extends AbstractBehavior { + + private static final String UNSPECIFIED_IPV4_ADRESS = "0.0.0.0"; + private static final String UNSPECIFIED_IPV6_ADRESS = "::"; + // Instance members protected final Logger logger = LoggerFactory.getLogger("uni-meter.output"); protected final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); @@ -576,7 +581,8 @@ protected Map getParameters(@NotNull Map parameter private @NotNull String resolveMdnsFallbackIpAddress() { if (config.hasPath("interface")) { String bindAddress = StringUtils.trimToEmpty(config.getString("interface")); - if (StringUtils.isNotBlank(bindAddress) && !"0.0.0.0".equals(bindAddress) && !"::".equals(bindAddress)) { + if (StringUtils.isNotBlank(bindAddress) // + && !Strings.CS.equalsAny(bindAddress, UNSPECIFIED_IPV4_ADRESS, UNSPECIFIED_IPV6_ADRESS)) { return bindAddress; } } From b384672d1472be81fe18ef7fdcc5bda1de64d51a Mon Sep 17 00:00:00 2001 From: Thimo Seitz Date: Wed, 18 Mar 2026 18:07:31 +0100 Subject: [PATCH 16/23] Update OutputDevice.java --- .../java/com/deigmueller/uni_meter/output/OutputDevice.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/deigmueller/uni_meter/output/OutputDevice.java b/src/main/java/com/deigmueller/uni_meter/output/OutputDevice.java index 36722f2..3c62790 100644 --- a/src/main/java/com/deigmueller/uni_meter/output/OutputDevice.java +++ b/src/main/java/com/deigmueller/uni_meter/output/OutputDevice.java @@ -541,7 +541,7 @@ protected Map getParameters(@NotNull Map parameter protected abstract void eventPowerDataChanged(); - protected @NotNull String resolveAnnouncedIpAddress() { + private @NotNull String resolveAnnouncedIpAddress() { Config mdnsConfig = getContext().getSystem().settings().config().getConfig("uni-meter.mdns"); return resolveAnnouncedIpAddress(mdnsConfig, logger, this::resolveMdnsFallbackIpAddress); } From f82bd28e25138c30aa9521c905e81ed4b5ae2eb0 Mon Sep 17 00:00:00 2001 From: Thimo Seitz Date: Wed, 18 Mar 2026 18:08:30 +0100 Subject: [PATCH 17/23] Rename MDnsAvahiTest to OutputDeviceTest Move and rename the test from mdns/MDnsAvahiTest to output/OutputDeviceTest (package changed to com.deigmueller.uni_meter.output). --- .../OutputDeviceTest.java} | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) rename src/test/java/com/deigmueller/uni_meter/{mdns/MDnsAvahiTest.java => output/OutputDeviceTest.java} (94%) diff --git a/src/test/java/com/deigmueller/uni_meter/mdns/MDnsAvahiTest.java b/src/test/java/com/deigmueller/uni_meter/output/OutputDeviceTest.java similarity index 94% rename from src/test/java/com/deigmueller/uni_meter/mdns/MDnsAvahiTest.java rename to src/test/java/com/deigmueller/uni_meter/output/OutputDeviceTest.java index c400ff0..ec24f88 100644 --- a/src/test/java/com/deigmueller/uni_meter/mdns/MDnsAvahiTest.java +++ b/src/test/java/com/deigmueller/uni_meter/output/OutputDeviceTest.java @@ -1,23 +1,21 @@ -package com.deigmueller.uni_meter.mdns; +package com.deigmueller.uni_meter.output; -import com.deigmueller.uni_meter.common.utils.NetUtils; -import com.deigmueller.uni_meter.output.OutputDevice; -import com.typesafe.config.Config; -import com.typesafe.config.ConfigFactory; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.mockito.MockedStatic; import org.mockito.Mockito; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.deigmueller.uni_meter.common.utils.NetUtils; +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; -import java.util.List; - -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; +class OutputDeviceTest { -class MDnsAvahiTest { - private static final Logger LOGGER = LoggerFactory.getLogger(MDnsAvahiTest.class); + private static final Logger LOGGER = LoggerFactory.getLogger(OutputDeviceTest.class); @Test @DisplayName("resolveAnnouncedIpAddress uses configured ip-address with highest priority") From 6af6d27be9b9721f67e50c8d3ee34f5da33ded6f Mon Sep 17 00:00:00 2001 From: Thimo Seitz Date: Wed, 18 Mar 2026 18:30:24 +0100 Subject: [PATCH 18/23] Use static logger and simplify IP resolver API Add a class-level static LOGGER and centralize warning logs to it. Simplify resolveAnnouncedIpAddress API by removing the Logger parameter and switching the instance resolver to call the new static signature; make the instance resolver protected. Update log calls to use the static LOGGER and adjust unit tests to use the new method signature (pass the fallback Supplier instead of a Logger). This centralizes logging and reduces parameter passing for IP-address resolution. --- .../uni_meter/output/OutputDevice.java | 14 +++++--------- .../uni_meter/output/OutputDeviceTest.java | 15 ++++++--------- 2 files changed, 11 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/deigmueller/uni_meter/output/OutputDevice.java b/src/main/java/com/deigmueller/uni_meter/output/OutputDevice.java index 3c62790..0df4160 100644 --- a/src/main/java/com/deigmueller/uni_meter/output/OutputDevice.java +++ b/src/main/java/com/deigmueller/uni_meter/output/OutputDevice.java @@ -42,6 +42,7 @@ public abstract class OutputDevice extends AbstractBehavior getParameters(@NotNull Map parameter protected abstract void eventPowerDataChanged(); - private @NotNull String resolveAnnouncedIpAddress() { + protected @NotNull String resolveAnnouncedIpAddress() { Config mdnsConfig = getContext().getSystem().settings().config().getConfig("uni-meter.mdns"); - return resolveAnnouncedIpAddress(mdnsConfig, logger, this::resolveMdnsFallbackIpAddress); - } - - public static @NotNull String resolveAnnouncedIpAddress(@NotNull Config mdnsConfig, @NotNull Logger logger) { - return resolveAnnouncedIpAddress(mdnsConfig, logger, NetUtils::detectPrimaryIpAddress); + return resolveAnnouncedIpAddress(mdnsConfig, this::resolveMdnsFallbackIpAddress); } public static @NotNull String resolveAnnouncedIpAddress(@NotNull Config mdnsConfig, - @NotNull Logger logger, @NotNull Supplier fallbackIpAddressSupplier) { String configuredIpAddress = StringUtils.trimToEmpty(mdnsConfig.getString("ip-address")); if (StringUtils.isNotBlank(configuredIpAddress)) { @@ -562,7 +558,7 @@ protected Map getParameters(@NotNull Map parameter if (StringUtils.isNotBlank(configuredIpInterface)) { List availableInterfaces = NetUtils.listNetworkInterfaceNames(); if (!availableInterfaces.contains(configuredIpInterface)) { - logger.warn("configured mdns interface '{}' not found. available interfaces: {}", + LOGGER.warn("configured mdns interface '{}' not found. available interfaces: {}", configuredIpInterface, String.join(", ", availableInterfaces)); } else { @@ -570,7 +566,7 @@ protected Map getParameters(@NotNull Map parameter if (StringUtils.isNotBlank(ipAddress)) { return ipAddress; } - logger.warn("failed to resolve IPv4 address for mdns interface '{}', falling back to configured default address", + LOGGER.warn("failed to resolve IPv4 address for mdns interface '{}', falling back to configured default address", configuredIpInterface); } } diff --git a/src/test/java/com/deigmueller/uni_meter/output/OutputDeviceTest.java b/src/test/java/com/deigmueller/uni_meter/output/OutputDeviceTest.java index ec24f88..61dd93e 100644 --- a/src/test/java/com/deigmueller/uni_meter/output/OutputDeviceTest.java +++ b/src/test/java/com/deigmueller/uni_meter/output/OutputDeviceTest.java @@ -7,16 +7,12 @@ import org.junit.jupiter.api.Test; import org.mockito.MockedStatic; import org.mockito.Mockito; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import com.deigmueller.uni_meter.common.utils.NetUtils; import com.typesafe.config.Config; import com.typesafe.config.ConfigFactory; class OutputDeviceTest { - private static final Logger LOGGER = LoggerFactory.getLogger(OutputDeviceTest.class); - @Test @DisplayName("resolveAnnouncedIpAddress uses configured ip-address with highest priority") void resolveAnnouncedIpAddressUsesConfiguredIpAddress() { @@ -83,9 +79,10 @@ void resolveAnnouncedIpAddressFallsBackToPrimaryIp() { private static String resolveAnnouncedIpAddress(String ipAddress, String ipInterface) { Config mdnsConfig = ConfigFactory.parseString(""" - ip-address = "%s" - ip-interface = "%s" - """.formatted(ipAddress, ipInterface)); - return OutputDevice.resolveAnnouncedIpAddress(mdnsConfig, LOGGER); + ip-address = "%s" + ip-interface = "%s" + """.formatted(ipAddress, ipInterface)); + + return OutputDevice.resolveAnnouncedIpAddress(mdnsConfig, NetUtils::detectPrimaryIpAddress); } -} +} From de84c8ad65e9e6952d35a3b82af70097bb1a787b Mon Sep 17 00:00:00 2001 From: Thimo Seitz Date: Wed, 18 Mar 2026 18:44:48 +0100 Subject: [PATCH 19/23] Update OutputDeviceTest.java --- .../uni_meter/output/OutputDeviceTest.java | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) diff --git a/src/test/java/com/deigmueller/uni_meter/output/OutputDeviceTest.java b/src/test/java/com/deigmueller/uni_meter/output/OutputDeviceTest.java index 61dd93e..be42346 100644 --- a/src/test/java/com/deigmueller/uni_meter/output/OutputDeviceTest.java +++ b/src/test/java/com/deigmueller/uni_meter/output/OutputDeviceTest.java @@ -1,18 +1,43 @@ package com.deigmueller.uni_meter.output; import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.text.IsEmptyString.isEmptyOrNullString; import java.util.List; +import org.apache.pekko.actor.typed.ActorRef; +import org.apache.pekko.actor.typed.Behavior; +import org.apache.pekko.actor.typed.javadsl.ActorContext; +import org.apache.pekko.actor.typed.javadsl.Behaviors; +import org.apache.pekko.actor.testkit.typed.javadsl.ActorTestKit; +import org.apache.pekko.http.javadsl.server.Directives; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.mockito.MockedStatic; import org.mockito.Mockito; +import com.deigmueller.uni_meter.application.UniMeter; import com.deigmueller.uni_meter.common.utils.NetUtils; +import com.deigmueller.uni_meter.mdns.MDnsRegistrator; import com.typesafe.config.Config; import com.typesafe.config.ConfigFactory; class OutputDeviceTest { + private ActorTestKit testKit; + + @AfterEach + void tearDown() { + if (testKit != null) { + testKit.shutdownTestKit(); + } + } + + // --------------------------------------------------------------------------- + // Tests for the static resolveAnnouncedIpAddress(Config, Supplier) overload + // --------------------------------------------------------------------------- + @Test @DisplayName("resolveAnnouncedIpAddress uses configured ip-address with highest priority") void resolveAnnouncedIpAddressUsesConfiguredIpAddress() { @@ -85,4 +110,109 @@ private static String resolveAnnouncedIpAddress(String ipAddress, String ipInter return OutputDevice.resolveAnnouncedIpAddress(mdnsConfig, NetUtils::detectPrimaryIpAddress); } + + // --------------------------------------------------------------------------- + // Tests for the instance resolveAnnouncedIpAddress() method + // (reads uni-meter.mdns from ActorSystem config and calls resolveMdnsFallbackIpAddress) + // --------------------------------------------------------------------------- + + @Test + @DisplayName("instance resolveAnnouncedIpAddress uses configured ip-address from system config") + void instanceResolveAnnouncedIpAddressUsesConfiguredIpAddress() { + Config systemConfig = buildSystemConfig("192.168.1.10", "", "0.0.0.0"); + testKit = ActorTestKit.create(systemConfig); + String result = spawnAndResolve(testKit, systemConfig); + assertThat(result, is("192.168.1.10")); + } + + @Test + @DisplayName("instance resolveAnnouncedIpAddress falls back to device interface address") + void instanceResolveAnnouncedIpAddressUsesDeviceInterface() { + Config systemConfig = buildSystemConfig("", "", "192.168.178.50"); + testKit = ActorTestKit.create(systemConfig); + String result = spawnAndResolve(testKit, systemConfig); + assertThat(result, is("192.168.178.50")); + } + + @Test + @DisplayName("instance resolveAnnouncedIpAddress falls back to primary IP when interface is unspecified") + void instanceResolveAnnouncedIpAddressFallsBackToPrimaryIp() { + Config systemConfig = buildSystemConfig("", "", "0.0.0.0"); + testKit = ActorTestKit.create(systemConfig); + String result = spawnAndResolve(testKit, systemConfig); + // The exact IP depends on the machine, but it must be a non-empty address + assertThat(result, is(not(isEmptyOrNullString()))); + } + + /** + * Builds a minimal system config with the given mdns ip-address, ip-interface + * and the device-level interface (used by resolveMdnsFallbackIpAddress). + */ + private static Config buildSystemConfig(String mdnsIpAddress, String mdnsIpInterface, String deviceInterface) { + return ConfigFactory.parseString(""" + uni-meter.mdns { + ip-address = "%s" + ip-interface = "%s" + } + test-output-device { + interface = "%s" + forget-interval = 1m + default-voltage = 230 + default-frequency = 50 + default-client-power-factor = 1.0 + power-offset-total = 0 + power-offset-l1 = 0 + power-offset-l2 = 0 + power-offset-l3 = 0 + usage-constraint-init-duration = 60s + } + """.formatted(mdnsIpAddress, mdnsIpInterface, deviceInterface)).withFallback(ConfigFactory.load()); + } + + /** + * Spawns a TestOutputDevice actor inside the given testKit, lets it capture + * its own resolveAnnouncedIpAddress() result and returns it. + */ + private static String spawnAndResolve(ActorTestKit testKit, Config systemConfig) { + Config deviceConfig = systemConfig.getConfig("test-output-device"); + ActorRef controllerDummy = testKit.createTestProbe(UniMeter.Command.class).ref(); + ActorRef mdnsDummy = testKit.createTestProbe(MDnsRegistrator.Command.class).ref(); + + var resultProbe = testKit.createTestProbe(CaptureResult.class); + testKit.spawn(TestOutputDevice.create(controllerDummy, mdnsDummy, deviceConfig, resultProbe.ref())); + + return resultProbe.receiveMessage().ipAddress(); + } + + /** Simple message to capture the resolved IP address out of the actor. */ + record CaptureResult(String ipAddress) { + } + + /** Minimal concrete OutputDevice subclass for testing. */ + static class TestOutputDevice extends OutputDevice { + + static Behavior create(ActorRef controller, ActorRef mdnsRegistrator, Config config, ActorRef replyTo) { + return Behaviors.setup(ctx -> new TestOutputDevice(ctx, controller, mdnsRegistrator, config, replyTo)); + } + + private TestOutputDevice(ActorContext context, ActorRef controller, ActorRef mdnsRegistrator, Config config, + ActorRef replyTo) { + super(context, controller, mdnsRegistrator, config, (logger, cfgList, map) -> { + }); + // resolveAnnouncedIpAddress() is called inside the parent constructor via + // this.announcedIpAddress = resolveAnnouncedIpAddress(); – capture it here. + replyTo.tell(new CaptureResult(getAnnouncedIpAddress())); + } + + @Override + protected @NotNull org.apache.pekko.http.javadsl.server.Route createRoute() { + return Directives.reject(); + } + + @Override + protected void eventPowerDataChanged() { + // NOOP + } + + } } From 875c0dd005537f8eb5aff821ed2a10c71914d8de Mon Sep 17 00:00:00 2001 From: Thimo Seitz Date: Wed, 18 Mar 2026 18:56:43 +0100 Subject: [PATCH 20/23] Update MDnsAvahi.java --- .../deigmueller/uni_meter/mdns/MDnsAvahi.java | 36 +++++++++---------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/src/main/java/com/deigmueller/uni_meter/mdns/MDnsAvahi.java b/src/main/java/com/deigmueller/uni_meter/mdns/MDnsAvahi.java index 0b6974a..a650480 100644 --- a/src/main/java/com/deigmueller/uni_meter/mdns/MDnsAvahi.java +++ b/src/main/java/com/deigmueller/uni_meter/mdns/MDnsAvahi.java @@ -1,18 +1,6 @@ package com.deigmueller.uni_meter.mdns; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import java.io.InputStreamReader; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.time.Duration; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.CompletableFuture; +import com.typesafe.config.Config; import org.apache.commons.lang3.StringUtils; import org.apache.pekko.actor.typed.Behavior; import org.apache.pekko.actor.typed.DispatcherSelector; @@ -23,9 +11,18 @@ import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.typesafe.config.Config; import scala.concurrent.ExecutionContextExecutor; +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Duration; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; + public class MDnsAvahi extends MDnsKind { // Class members private static final Logger LOGGER = LoggerFactory.getLogger("uni-meter.mdns.avahi"); @@ -182,12 +179,10 @@ private Behavior onAvahiPublishFailed(AvahiPublishFailed message) { private void registerServer(@NotNull RegisterService registerService) { LOGGER.trace("MDnsAvahi.registerServer()"); - String ipAddress = registerService.ipAddress(); - - if (registeredServers.add(new NameAndIpAddress(registerService.name(), ipAddress)) // - && enableAvahiPublish // - && !StringUtils.isAllBlank(avahiPublishBinary)) { - startAvahiPublish(registerService.server(), ipAddress); + if (registeredServers.add(new NameAndIpAddress(registerService.name(), registerService.ipAddress()))) { + if (enableAvahiPublish && !StringUtils.isAllBlank(avahiPublishBinary)) { + startAvahiPublish(registerService.server(), registerService.ipAddress()); + } } } @@ -313,3 +308,4 @@ private enum RestartAvahiPublish implements Command { private record NameAndIpAddress(String name, String ipAddress) {} } + From 4d76955c9df273347decbb8107b99d47ebb4b4d6 Mon Sep 17 00:00:00 2001 From: Thimo Seitz Date: Wed, 18 Mar 2026 19:35:11 +0100 Subject: [PATCH 21/23] Use output-device interface for mDNS address Move mDNS address selection from the global mdns config to the output-device `interface` setting and update docs accordingly. OutputDevice.resolveAnnouncedIpAddress now reads the device `interface`, treats literal IPs as addresses, resolves interface names via NetUtils.detectIpAddressFromInterface, and falls back to NetUtils.detectPrimaryIpAddress. NetUtils was adjusted to prefer IPv4 addresses but return any address if no IPv4 is found. Updated reference.conf, installation and output docs, and adapted unit tests to the new config layout and behavior. --- README.md | 6 +- doc/install/BareMetal.md | 17 ++-- doc/output/Common.md | 19 +++++ .../uni_meter/common/utils/NetUtils.java | 15 ++-- .../uni_meter/output/OutputDevice.java | 43 +++++------ src/main/resources/reference.conf | 12 ++- .../uni_meter/output/OutputDeviceTest.java | 77 ++++++++++--------- 7 files changed, 106 insertions(+), 83 deletions(-) diff --git a/README.md b/README.md index 50e9ea4..d647fc3 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,11 @@ uni-meter { Some sample configurations for different devices can be found [here](https://github.com/sdeigm/uni-meter/tree/main/samples). +If you use mDNS, the announced IP address is derived from the selected output device `interface` configuration. +If `interface` contains an IPv4 or IPv6 address, `uni-meter` uses it directly. Otherwise, it is treated as a network +interface name and `uni-meter` tries to resolve an IP address from it, preferring IPv4 over IPv6. If that does not +work, `uni-meter` falls back to the detected primary IPv4 address. + ## Output device configuration To configure the output device, follow the instructions in these sections: @@ -167,4 +172,3 @@ type. ``` A restart is necessary for these changes to take effect. - diff --git a/doc/install/BareMetal.md b/doc/install/BareMetal.md index e9c7369..be6bb6a 100644 --- a/doc/install/BareMetal.md +++ b/doc/install/BareMetal.md @@ -168,18 +168,17 @@ sudo systemctl start avahi-daemon Starting with `uni-meter` version 1.1.5, a running avahi daemon is automatically detected and all necessary configuration files will be automatically created in `/etc/avahi/service`. -If the host has multiple network interfaces, you can force which IPv4 address is announced via mDNS. -`ip-address` has the highest priority. If it is empty and `ip-interface` is set, `uni-meter` resolves the IPv4 -address of that interface. If both are empty (default), the output device address is used. +The IP address announced via mDNS is derived from the configuration of the selected output device. +If `interface` contains an IPv4 or IPv6 address, `uni-meter` uses it directly. Otherwise, it is treated as a network +interface name and `uni-meter` tries to resolve an IP address from it, preferring IPv4 over IPv6. If that does not +work, `uni-meter` falls back to the detected primary IPv4 address of the host. ```hocon uni-meter { - mdns { - type = "auto" - ip-address = "" - ip-interface = "" + output-devices { + shelly-pro3em { + interface = "eth0" + } } } ``` - - diff --git a/doc/output/Common.md b/doc/output/Common.md index f372bed..5be854f 100644 --- a/doc/output/Common.md +++ b/doc/output/Common.md @@ -112,3 +112,22 @@ uni-meter { } } ``` + +## Configuring the announced mDNS address + +If mDNS is used, the announced IP address is derived from the selected output device configuration. +If `interface` contains an IPv4 or IPv6 address, `uni-meter` uses it directly. Otherwise, it is treated as a network +interface name and `uni-meter` tries to resolve an IP address from it, preferring IPv4 over IPv6. If that does not +work, `uni-meter` falls back to the detected primary IPv4 address. + +```hocon +uni-meter { + #... + output-devices { + #... + shelly-pro3em { + interface = "eth0" + } + } +} +``` diff --git a/src/main/java/com/deigmueller/uni_meter/common/utils/NetUtils.java b/src/main/java/com/deigmueller/uni_meter/common/utils/NetUtils.java index 715f872..78ff982 100644 --- a/src/main/java/com/deigmueller/uni_meter/common/utils/NetUtils.java +++ b/src/main/java/com/deigmueller/uni_meter/common/utils/NetUtils.java @@ -1,7 +1,6 @@ package com.deigmueller.uni_meter.common.utils; import java.net.DatagramSocket; -import java.net.Inet4Address; import java.net.InetAddress; import java.net.NetworkInterface; import java.net.SocketException; @@ -46,7 +45,7 @@ public class NetUtils { /** * Detects the IP address associated with the specified network interface. * @param interfaceName the name of the network interface (e.g., "eth0", "wlan0") - * @return the IP address as a string, or null if the interface is not found, not up, or has no valid IPv4 address + * @return the IP address as a string, or null if the interface is not found, not up, or has no valid IP address */ public static @Nullable String detectIpAddressFromInterface(@NotNull String interfaceName) { try { @@ -54,11 +53,15 @@ public class NetUtils { if (networkInterface == null || !networkInterface.isUp()) { return null; } - return Streams.of(networkInterface.getInetAddresses()) // - .filter(Inet4Address.class::isInstance) // + List addresses = Streams.of(networkInterface.getInetAddresses()) // .filter(a -> !a.isLoopbackAddress()) // - .map(InetAddress::getHostAddress) // - .findFirst().orElse(null); + .toList(); + return addresses.stream() + .filter(address -> address.getAddress().length == 4) + .findFirst() + .or(() -> addresses.stream().findFirst()) + .map(InetAddress::getHostAddress) + .orElse(null); } catch (Exception e) { // We don't care } diff --git a/src/main/java/com/deigmueller/uni_meter/output/OutputDevice.java b/src/main/java/com/deigmueller/uni_meter/output/OutputDevice.java index 0df4160..65248ff 100644 --- a/src/main/java/com/deigmueller/uni_meter/output/OutputDevice.java +++ b/src/main/java/com/deigmueller/uni_meter/output/OutputDevice.java @@ -543,47 +543,42 @@ protected Map getParameters(@NotNull Map parameter protected abstract void eventPowerDataChanged(); protected @NotNull String resolveAnnouncedIpAddress() { - Config mdnsConfig = getContext().getSystem().settings().config().getConfig("uni-meter.mdns"); - return resolveAnnouncedIpAddress(mdnsConfig, this::resolveMdnsFallbackIpAddress); + return resolveAnnouncedIpAddress(config, NetUtils::detectPrimaryIpAddress); } - public static @NotNull String resolveAnnouncedIpAddress(@NotNull Config mdnsConfig, + public static @NotNull String resolveAnnouncedIpAddress(@NotNull Config outputDeviceConfig, @NotNull Supplier fallbackIpAddressSupplier) { - String configuredIpAddress = StringUtils.trimToEmpty(mdnsConfig.getString("ip-address")); - if (StringUtils.isNotBlank(configuredIpAddress)) { - return configuredIpAddress; - } + String configuredInterface = StringUtils.trimToEmpty(outputDeviceConfig.getString("interface")); + if (StringUtils.isNotBlank(configuredInterface) + && !Strings.CS.equalsAny(configuredInterface, UNSPECIFIED_IPV4_ADRESS, UNSPECIFIED_IPV6_ADRESS)) { + if (isIpAddress(configuredInterface)) { + return configuredInterface; + } - String configuredIpInterface = StringUtils.trimToEmpty(mdnsConfig.getString("ip-interface")); - if (StringUtils.isNotBlank(configuredIpInterface)) { List availableInterfaces = NetUtils.listNetworkInterfaceNames(); - if (!availableInterfaces.contains(configuredIpInterface)) { - LOGGER.warn("configured mdns interface '{}' not found. available interfaces: {}", - configuredIpInterface, + if (!availableInterfaces.contains(configuredInterface)) { + LOGGER.warn("configured output-device interface '{}' not found. available interfaces: {}", + configuredInterface, String.join(", ", availableInterfaces)); } else { - String ipAddress = NetUtils.detectIpAddressFromInterface(configuredIpInterface); + String ipAddress = NetUtils.detectIpAddressFromInterface(configuredInterface); if (StringUtils.isNotBlank(ipAddress)) { return ipAddress; } - LOGGER.warn("failed to resolve IPv4 address for mdns interface '{}', falling back to configured default address", - configuredIpInterface); + LOGGER.warn("failed to resolve IP address for output-device interface '{}', falling back to configured default address", + configuredInterface); } } return fallbackIpAddressSupplier.get(); } - private @NotNull String resolveMdnsFallbackIpAddress() { - if (config.hasPath("interface")) { - String bindAddress = StringUtils.trimToEmpty(config.getString("interface")); - if (StringUtils.isNotBlank(bindAddress) // - && !Strings.CS.equalsAny(bindAddress, UNSPECIFIED_IPV4_ADRESS, UNSPECIFIED_IPV6_ADRESS)) { - return bindAddress; - } + private static boolean isIpAddress(@NotNull String value) { + try { + return InetAddress.getByName(value) != null; + } catch (Exception e) { + return false; } - - return NetUtils.detectPrimaryIpAddress(); } protected void initPowerOffsets(@NotNull Config config) { diff --git a/src/main/resources/reference.conf b/src/main/resources/reference.conf index 13a81f3..20bba84 100644 --- a/src/main/resources/reference.conf +++ b/src/main/resources/reference.conf @@ -23,8 +23,6 @@ uni-meter { mdns { type = "auto" - ip-address = "" - ip-interface = "" url = ${?UNI_HA_URL} access-token = ${?UNI_HA_ACCESS_TOKEN} enable-avahi-publish = true @@ -42,11 +40,11 @@ uni-meter { power-offset-l2 = 0 power-offset-l3 = 0 - default-client-power-factor = 1.0 - client-contexts = [] - - usage-constraint-init-duration = 60s - } + default-client-power-factor = 1.0 + client-contexts = [] + + usage-constraint-init-duration = 60s + } shelly-pro3em { type = "ShellyPro3EM" diff --git a/src/test/java/com/deigmueller/uni_meter/output/OutputDeviceTest.java b/src/test/java/com/deigmueller/uni_meter/output/OutputDeviceTest.java index be42346..be7904d 100644 --- a/src/test/java/com/deigmueller/uni_meter/output/OutputDeviceTest.java +++ b/src/test/java/com/deigmueller/uni_meter/output/OutputDeviceTest.java @@ -39,25 +39,37 @@ void tearDown() { // --------------------------------------------------------------------------- @Test - @DisplayName("resolveAnnouncedIpAddress uses configured ip-address with highest priority") - void resolveAnnouncedIpAddressUsesConfiguredIpAddress() { + @DisplayName("resolveAnnouncedIpAddress uses configured interface when it is an IPv4 address") + void resolveAnnouncedIpAddressUsesConfiguredIpv4Address() { try (MockedStatic mockedNetUtils = Mockito.mockStatic(NetUtils.class)) { mockedNetUtils.when(NetUtils::detectPrimaryIpAddress).thenReturn("10.0.0.5"); - String result = resolveAnnouncedIpAddress(" 192.168.1.10 ", ""); + String result = resolveAnnouncedIpAddress(" 192.168.1.10 "); assertThat(result, is("192.168.1.10")); } } @Test - @DisplayName("resolveAnnouncedIpAddress resolves IP from configured interface") - void resolveAnnouncedIpAddressUsesConfiguredIpInterface() { + @DisplayName("resolveAnnouncedIpAddress uses configured interface when it is an IPv6 address") + void resolveAnnouncedIpAddressUsesConfiguredIpv6Address() { + try (MockedStatic mockedNetUtils = Mockito.mockStatic(NetUtils.class)) { + mockedNetUtils.when(NetUtils::detectPrimaryIpAddress).thenReturn("10.0.0.5"); + + String result = resolveAnnouncedIpAddress(" 2001:db8::10 "); + + assertThat(result, is("2001:db8::10")); + } + } + + @Test + @DisplayName("resolveAnnouncedIpAddress resolves IP from configured interface name") + void resolveAnnouncedIpAddressUsesConfiguredInterfaceName() { try (MockedStatic mockedNetUtils = Mockito.mockStatic(NetUtils.class)) { mockedNetUtils.when(NetUtils::listNetworkInterfaceNames).thenReturn(List.of("eth0", "wlan0")); mockedNetUtils.when(() -> NetUtils.detectIpAddressFromInterface("eth0")).thenReturn("192.168.178.22"); - String result = resolveAnnouncedIpAddress("", "eth0"); + String result = resolveAnnouncedIpAddress("eth0"); assertThat(result, is("192.168.178.22")); } @@ -70,21 +82,21 @@ void resolveAnnouncedIpAddressFallsBackForMissingInterface() { mockedNetUtils.when(NetUtils::listNetworkInterfaceNames).thenReturn(List.of("eth0", "wlan0")); mockedNetUtils.when(NetUtils::detectPrimaryIpAddress).thenReturn("10.0.0.5"); - String result = resolveAnnouncedIpAddress("", "eth9"); + String result = resolveAnnouncedIpAddress("eth9"); assertThat(result, is("10.0.0.5")); } } @Test - @DisplayName("resolveAnnouncedIpAddress falls back to primary IP when interface has no IPv4 address") - void resolveAnnouncedIpAddressFallsBackForInterfaceWithoutIpv4() { + @DisplayName("resolveAnnouncedIpAddress falls back to primary IP when interface has no IP address") + void resolveAnnouncedIpAddressFallsBackForInterfaceWithoutIpAddress() { try (MockedStatic mockedNetUtils = Mockito.mockStatic(NetUtils.class)) { mockedNetUtils.when(NetUtils::listNetworkInterfaceNames).thenReturn(List.of("eth0", "wlan0")); mockedNetUtils.when(() -> NetUtils.detectIpAddressFromInterface("eth0")).thenReturn(null); mockedNetUtils.when(NetUtils::detectPrimaryIpAddress).thenReturn("10.0.0.5"); - String result = resolveAnnouncedIpAddress("", "eth0"); + String result = resolveAnnouncedIpAddress("eth0"); assertThat(result, is("10.0.0.5")); } @@ -96,64 +108,57 @@ void resolveAnnouncedIpAddressFallsBackToPrimaryIp() { try (MockedStatic mockedNetUtils = Mockito.mockStatic(NetUtils.class)) { mockedNetUtils.when(NetUtils::detectPrimaryIpAddress).thenReturn("10.0.0.5"); - String result = resolveAnnouncedIpAddress("", ""); + String result = resolveAnnouncedIpAddress(""); assertThat(result, is("10.0.0.5")); } } - private static String resolveAnnouncedIpAddress(String ipAddress, String ipInterface) { - Config mdnsConfig = ConfigFactory.parseString(""" - ip-address = "%s" - ip-interface = "%s" - """.formatted(ipAddress, ipInterface)); + private static String resolveAnnouncedIpAddress(String configuredInterface) { + Config outputDeviceConfig = ConfigFactory.parseString(""" + interface = "%s" + """.formatted(configuredInterface)); - return OutputDevice.resolveAnnouncedIpAddress(mdnsConfig, NetUtils::detectPrimaryIpAddress); + return OutputDevice.resolveAnnouncedIpAddress(outputDeviceConfig, NetUtils::detectPrimaryIpAddress); } // --------------------------------------------------------------------------- // Tests for the instance resolveAnnouncedIpAddress() method - // (reads uni-meter.mdns from ActorSystem config and calls resolveMdnsFallbackIpAddress) + // (reads interface from the output-device config) // --------------------------------------------------------------------------- @Test - @DisplayName("instance resolveAnnouncedIpAddress uses configured ip-address from system config") - void instanceResolveAnnouncedIpAddressUsesConfiguredIpAddress() { - Config systemConfig = buildSystemConfig("192.168.1.10", "", "0.0.0.0"); + @DisplayName("instance resolveAnnouncedIpAddress uses configured IPv4 address from output-device config") + void instanceResolveAnnouncedIpAddressUsesConfiguredIpv4Address() { + Config systemConfig = buildSystemConfig("192.168.1.10"); testKit = ActorTestKit.create(systemConfig); String result = spawnAndResolve(testKit, systemConfig); assertThat(result, is("192.168.1.10")); } @Test - @DisplayName("instance resolveAnnouncedIpAddress falls back to device interface address") - void instanceResolveAnnouncedIpAddressUsesDeviceInterface() { - Config systemConfig = buildSystemConfig("", "", "192.168.178.50"); + @DisplayName("instance resolveAnnouncedIpAddress uses configured IPv6 address from output-device config") + void instanceResolveAnnouncedIpAddressUsesConfiguredIpv6Address() { + Config systemConfig = buildSystemConfig("2001:db8::10"); testKit = ActorTestKit.create(systemConfig); String result = spawnAndResolve(testKit, systemConfig); - assertThat(result, is("192.168.178.50")); + assertThat(result, is("2001:db8::10")); } @Test @DisplayName("instance resolveAnnouncedIpAddress falls back to primary IP when interface is unspecified") - void instanceResolveAnnouncedIpAddressFallsBackToPrimaryIp() { - Config systemConfig = buildSystemConfig("", "", "0.0.0.0"); + void instanceResolveAnnouncedIpAddressFallsBackToPrimaryIpForUnspecifiedAddress() { + Config systemConfig = buildSystemConfig("0.0.0.0"); testKit = ActorTestKit.create(systemConfig); String result = spawnAndResolve(testKit, systemConfig); - // The exact IP depends on the machine, but it must be a non-empty address assertThat(result, is(not(isEmptyOrNullString()))); } /** - * Builds a minimal system config with the given mdns ip-address, ip-interface - * and the device-level interface (used by resolveMdnsFallbackIpAddress). + * Builds a minimal system config with the given output-device interface. */ - private static Config buildSystemConfig(String mdnsIpAddress, String mdnsIpInterface, String deviceInterface) { + private static Config buildSystemConfig(String deviceInterface) { return ConfigFactory.parseString(""" - uni-meter.mdns { - ip-address = "%s" - ip-interface = "%s" - } test-output-device { interface = "%s" forget-interval = 1m @@ -166,7 +171,7 @@ interface = "%s" power-offset-l3 = 0 usage-constraint-init-duration = 60s } - """.formatted(mdnsIpAddress, mdnsIpInterface, deviceInterface)).withFallback(ConfigFactory.load()); + """.formatted(deviceInterface)).withFallback(ConfigFactory.load()); } /** From 663ccdc8d6016c6c6bd94c6d3ab5d98bf0de432c Mon Sep 17 00:00:00 2001 From: Thimo Seitz Date: Wed, 18 Mar 2026 19:38:45 +0100 Subject: [PATCH 22/23] Update NetUtils.java --- .../deigmueller/uni_meter/common/utils/NetUtils.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/deigmueller/uni_meter/common/utils/NetUtils.java b/src/main/java/com/deigmueller/uni_meter/common/utils/NetUtils.java index 78ff982..9fe3fbe 100644 --- a/src/main/java/com/deigmueller/uni_meter/common/utils/NetUtils.java +++ b/src/main/java/com/deigmueller/uni_meter/common/utils/NetUtils.java @@ -51,16 +51,16 @@ public class NetUtils { try { NetworkInterface networkInterface = NetworkInterface.getByName(interfaceName); if (networkInterface == null || !networkInterface.isUp()) { - return null; + return null; // interface not found or not up return null } List addresses = Streams.of(networkInterface.getInetAddresses()) // - .filter(a -> !a.isLoopbackAddress()) // + .filter(a -> !a.isLoopbackAddress()) // filter out loopback addresses .toList(); return addresses.stream() - .filter(address -> address.getAddress().length == 4) - .findFirst() - .or(() -> addresses.stream().findFirst()) - .map(InetAddress::getHostAddress) + .filter(address -> address.getAddress().length == 4) // prefer IPv4 addresses + .findFirst() // + .or(() -> addresses.stream().findFirst()) // if no IPv4 address is found, return the first available address (which may be IPv6) + .map(InetAddress::getHostAddress) // .orElse(null); } catch (Exception e) { // We don't care From 9450064354946d39018e3b6f5911d4cca86e1b17 Mon Sep 17 00:00:00 2001 From: Thimo Seitz Date: Wed, 18 Mar 2026 19:43:36 +0100 Subject: [PATCH 23/23] Update reference.conf --- src/main/resources/reference.conf | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/main/resources/reference.conf b/src/main/resources/reference.conf index 76630ed..5d2b6ba 100644 --- a/src/main/resources/reference.conf +++ b/src/main/resources/reference.conf @@ -21,13 +21,13 @@ uni-meter { bind-retry-backoff = 15s } - mdns { - type = "auto" - url = ${?UNI_HA_URL} - access-token = ${?UNI_HA_ACCESS_TOKEN} - enable-avahi-publish = true - avahi-publish = "" - } + mdns { + type = "auto" + url = ${?UNI_HA_URL} + access-token = ${?UNI_HA_ACCESS_TOKEN} + enable-avahi-publish = true + avahi-publish = "" + } output-devices { common { @@ -40,11 +40,11 @@ uni-meter { power-offset-l2 = 0 power-offset-l3 = 0 - default-client-power-factor = 1.0 - client-contexts = [] - - usage-constraint-init-duration = 60s - } + default-client-power-factor = 1.0 + client-contexts = [] + + usage-constraint-init-duration = 60s + } shelly-pro3em { type = "ShellyPro3EM" @@ -359,4 +359,4 @@ pekko.http { server { remote-address-header = on } -} +} \ No newline at end of file