diff --git a/bundles/org.openhab.core.config.discovery.mdns/src/main/java/org/openhab/core/config/discovery/mdns/internal/MDNSDiscoveryService.java b/bundles/org.openhab.core.config.discovery.mdns/src/main/java/org/openhab/core/config/discovery/mdns/internal/MDNSDiscoveryService.java index 5aef678abf2..e0a5c6c0630 100644 --- a/bundles/org.openhab.core.config.discovery.mdns/src/main/java/org/openhab/core/config/discovery/mdns/internal/MDNSDiscoveryService.java +++ b/bundles/org.openhab.core.config.discovery.mdns/src/main/java/org/openhab/core/config/discovery/mdns/internal/MDNSDiscoveryService.java @@ -29,9 +29,12 @@ import org.openhab.core.config.discovery.DiscoveryResult; import org.openhab.core.config.discovery.DiscoveryService; import org.openhab.core.config.discovery.mdns.MDNSDiscoveryParticipant; +import org.openhab.core.i18n.LocaleProvider; +import org.openhab.core.i18n.TranslationProvider; import org.openhab.core.io.transport.mdns.MDNSClient; import org.openhab.core.thing.ThingTypeUID; import org.openhab.core.thing.ThingUID; +import org.osgi.framework.FrameworkUtil; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Deactivate; @@ -63,10 +66,13 @@ public class MDNSDiscoveryService extends AbstractDiscoveryService implements Se @Activate public MDNSDiscoveryService(final @Nullable Map configProperties, - final @Reference MDNSClient mdnsClient) { + final @Reference MDNSClient mdnsClient, final @Reference TranslationProvider i18nProvider, + final @Reference LocaleProvider localeProvider) { super(5); this.mdnsClient = mdnsClient; + this.i18nProvider = i18nProvider; + this.localeProvider = localeProvider; super.activate(configProperties); @@ -152,7 +158,9 @@ private void scan(boolean isBackground) { for (ServiceInfo service : services) { DiscoveryResult result = participant.createResult(service); if (result != null) { - thingDiscovered(result); + final DiscoveryResult resultNew = getLocalizedDiscoveryResult(result, + FrameworkUtil.getBundle(participant.getClass())); + thingDiscovered(resultNew); } } } @@ -213,7 +221,9 @@ private void considerService(ServiceEvent serviceEvent) { try { DiscoveryResult result = participant.createResult(serviceEvent.getInfo()); if (result != null) { - thingDiscovered(result); + final DiscoveryResult resultNew = getLocalizedDiscoveryResult(result, + FrameworkUtil.getBundle(participant.getClass())); + thingDiscovered(resultNew); } } catch (Exception e) { logger.error("Participant '{}' threw an exception", participant.getClass().getName(), e); diff --git a/bundles/org.openhab.core.config.discovery.upnp/src/main/java/org/openhab/core/config/discovery/upnp/internal/UpnpDiscoveryService.java b/bundles/org.openhab.core.config.discovery.upnp/src/main/java/org/openhab/core/config/discovery/upnp/internal/UpnpDiscoveryService.java index 54d19292bde..44ab46a43ee 100644 --- a/bundles/org.openhab.core.config.discovery.upnp/src/main/java/org/openhab/core/config/discovery/upnp/internal/UpnpDiscoveryService.java +++ b/bundles/org.openhab.core.config.discovery.upnp/src/main/java/org/openhab/core/config/discovery/upnp/internal/UpnpDiscoveryService.java @@ -33,11 +33,14 @@ import org.openhab.core.config.discovery.DiscoveryResult; import org.openhab.core.config.discovery.DiscoveryService; import org.openhab.core.config.discovery.upnp.UpnpDiscoveryParticipant; +import org.openhab.core.i18n.LocaleProvider; +import org.openhab.core.i18n.TranslationProvider; import org.openhab.core.net.CidrAddress; import org.openhab.core.net.NetworkAddressChangeListener; import org.openhab.core.net.NetworkAddressService; import org.openhab.core.thing.ThingTypeUID; import org.openhab.core.thing.ThingUID; +import org.osgi.framework.FrameworkUtil; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Modified; import org.osgi.service.component.annotations.Reference; @@ -104,6 +107,24 @@ protected void unsetNetworkAddressService(NetworkAddressService networkAddressSe networkAddressService.removeNetworkAddressChangeListener(this); } + @Reference + protected void setI18nProvider(TranslationProvider i18nProvider) { + this.i18nProvider = i18nProvider; + } + + protected void unsetI18nProvider(TranslationProvider i18nProvider) { + this.i18nProvider = null; + } + + @Reference + protected void setLocaleProvider(LocaleProvider localeProvider) { + this.localeProvider = localeProvider; + } + + protected void unsetLocaleProvider(LocaleProvider localeProvider) { + this.localeProvider = null; + } + @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) protected void addUpnpDiscoveryParticipant(UpnpDiscoveryParticipant participant) { this.participants.add(participant); @@ -113,7 +134,9 @@ protected void addUpnpDiscoveryParticipant(UpnpDiscoveryParticipant participant) for (RemoteDevice device : devices) { DiscoveryResult result = participant.createResult(device); if (result != null) { - thingDiscovered(result); + final DiscoveryResult resultNew = getLocalizedDiscoveryResult(result, + FrameworkUtil.getBundle(participant.getClass())); + thingDiscovered(resultNew); } } } @@ -169,7 +192,9 @@ public void remoteDeviceAdded(Registry registry, RemoteDevice device) { if (participant.getRemovalGracePeriodSeconds(device) > 0) { cancelRemovalTask(device.getIdentity().getUdn()); } - thingDiscovered(result); + final DiscoveryResult resultNew = getLocalizedDiscoveryResult(result, + FrameworkUtil.getBundle(participant.getClass())); + thingDiscovered(resultNew); } } catch (Exception e) { logger.error("Participant '{}' threw an exception", participant.getClass().getName(), e); diff --git a/bundles/org.openhab.core.config.discovery/src/main/java/org/openhab/core/config/discovery/AbstractDiscoveryService.java b/bundles/org.openhab.core.config.discovery/src/main/java/org/openhab/core/config/discovery/AbstractDiscoveryService.java index ed527c16e9c..3fc5a470c29 100644 --- a/bundles/org.openhab.core.config.discovery/src/main/java/org/openhab/core/config/discovery/AbstractDiscoveryService.java +++ b/bundles/org.openhab.core.config.discovery/src/main/java/org/openhab/core/config/discovery/AbstractDiscoveryService.java @@ -12,6 +12,7 @@ */ package org.openhab.core.config.discovery; +import java.util.Arrays; import java.util.Collection; import java.util.Date; import java.util.HashMap; @@ -243,24 +244,8 @@ protected synchronized void stopScan() { * @param discoveryResult Holds the information needed to identify the discovered device. */ protected void thingDiscovered(final DiscoveryResult discoveryResult) { - final DiscoveryResult discoveryResultNew; - if (i18nProvider != null && localeProvider != null) { - Bundle bundle = FrameworkUtil.getBundle(this.getClass()); - - String defaultLabel = discoveryResult.getLabel(); - - String key = I18nUtil.stripConstantOr(defaultLabel, () -> inferKey(discoveryResult, "label")); - - String label = i18nProvider.getText(bundle, key, defaultLabel, localeProvider.getLocale()); - - discoveryResultNew = DiscoveryResultBuilder.create(discoveryResult.getThingUID()) - .withThingType(discoveryResult.getThingTypeUID()).withBridge(discoveryResult.getBridgeUID()) - .withProperties(discoveryResult.getProperties()) - .withRepresentationProperty(discoveryResult.getRepresentationProperty()).withLabel(label) - .withTTL(discoveryResult.getTimeToLive()).build(); - } else { - discoveryResultNew = discoveryResult; - } + final DiscoveryResult discoveryResultNew = getLocalizedDiscoveryResult(discoveryResult, + FrameworkUtil.getBundle(this.getClass())); for (DiscoveryListener discoveryListener : discoveryListeners) { try { discoveryListener.thingDiscovered(this, discoveryResultNew); @@ -454,4 +439,53 @@ private boolean getAutoDiscoveryEnabled(Object autoDiscoveryEnabled) { private String inferKey(DiscoveryResult discoveryResult, String lastSegment) { return "discovery." + discoveryResult.getThingUID().getAsString().replaceAll(":", ".") + "." + lastSegment; } + + protected DiscoveryResult getLocalizedDiscoveryResult(final DiscoveryResult discoveryResult, + @Nullable Bundle bundle) { + if (i18nProvider != null && localeProvider != null) { + String currentLabel = discoveryResult.getLabel(); + + String key = I18nUtil.stripConstantOr(currentLabel, () -> inferKey(discoveryResult, "label")); + + ParsedKey parsedKey = new ParsedKey(key); + + String label = i18nProvider.getText(bundle, parsedKey.key, currentLabel, localeProvider.getLocale(), + parsedKey.args); + + if (currentLabel.equals(label)) { + return discoveryResult; + } else { + return DiscoveryResultBuilder.create(discoveryResult.getThingUID()) + .withThingType(discoveryResult.getThingTypeUID()).withBridge(discoveryResult.getBridgeUID()) + .withProperties(discoveryResult.getProperties()) + .withRepresentationProperty(discoveryResult.getRepresentationProperty()).withLabel(label) + .withTTL(discoveryResult.getTimeToLive()).build(); + } + } else { + return discoveryResult; + } + } + + /** + * Utility class to parse the key with parameters into the key and optional arguments. + */ + private final class ParsedKey { + + private static final int LIMIT = 2; + + private final String key; + private final Object @Nullable [] args; + + private ParsedKey(String label) { + String[] parts = label.split("\\s+", LIMIT); + this.key = parts[0]; + + if (parts.length == 1) { + this.args = null; + } else { + this.args = Arrays.stream(parts[1].replaceAll("\\[|\\]|\"", "").split(",")) + .filter(s -> s != null && !s.isBlank()).map(s -> s.trim()).toArray(Object[]::new); + } + } + } } diff --git a/bundles/org.openhab.core.config.discovery/src/test/java/org/openhab/core/config/discovery/AbstractDiscoveryServiceTest.java b/bundles/org.openhab.core.config.discovery/src/test/java/org/openhab/core/config/discovery/AbstractDiscoveryServiceTest.java new file mode 100644 index 00000000000..c5712623435 --- /dev/null +++ b/bundles/org.openhab.core.config.discovery/src/test/java/org/openhab/core/config/discovery/AbstractDiscoveryServiceTest.java @@ -0,0 +1,179 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.config.discovery; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsMapContaining.hasEntry; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.util.Collection; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; +import org.junit.jupiter.api.Test; +import org.openhab.core.i18n.LocaleProvider; +import org.openhab.core.i18n.TranslationProvider; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.ThingUID; +import org.osgi.framework.Bundle; + +/** + * Tests the {@link DiscoveryResultBuilder}. + * + * @author Laurent Garnier - Initial contribution + */ +public class AbstractDiscoveryServiceTest implements DiscoveryListener { + + private static final String BINDING_ID = "bindingId"; + private static final ThingUID BRIDGE_UID = new ThingUID(new ThingTypeUID(BINDING_ID, "bridgeTypeId"), "bridgeId"); + private static final ThingTypeUID THING_TYPE_UID = new ThingTypeUID(BINDING_ID, "thingTypeId"); + private static final ThingUID THING_UID1 = new ThingUID(THING_TYPE_UID, BRIDGE_UID, "thingId1"); + private static final ThingUID THING_UID2 = new ThingUID(THING_TYPE_UID, "thingId2"); + private static final ThingUID THING_UID3 = new ThingUID(THING_TYPE_UID, BRIDGE_UID, "thingId3"); + private static final ThingUID THING_UID4 = new ThingUID(THING_TYPE_UID, "thingId4"); + private static final String KEY1 = "key1"; + private static final String KEY2 = "key2"; + private static final String VALUE1 = "value1"; + private static final String VALUE2 = "value2"; + private final Map properties = Map.of(KEY1, VALUE1, KEY2, VALUE2); + private static final String DISCOVERY_THING2_INFERED_KEY = "discovery." + + THING_UID2.getAsString().replaceAll(":", ".") + ".label"; + private static final String DISCOVERY_THING4_INFERED_KEY = "discovery." + + THING_UID4.getAsString().replaceAll(":", ".") + ".label"; + private static final String DISCOVERY_LABEL = "Result Test"; + private static final String DISCOVERY_LABEL_KEY1 = "@text/test"; + private static final String DISCOVERY_LABEL_KEY2 = "@text/test2 [ \"50\", \"number\" ]"; + private static final String PROPERTY_LABEL1 = "Label from property (text key)"; + private static final String PROPERTY_LABEL2 = "Label from property (infered key)"; + private static final String PROPERTY_LABEL3 = "Label from property (parameters 50 and number)"; + + private TranslationProvider i18nProvider = new TranslationProvider() { + @Override + public @Nullable String getText(@Nullable Bundle bundle, @Nullable String key, @Nullable String defaultText, + @Nullable Locale locale, @Nullable Object... arguments) { + if (Locale.ENGLISH.equals(locale)) { + if ("test".equals(key)) { + return PROPERTY_LABEL1; + } else if ("test2".equals(key) && arguments != null && arguments.length == 2 + && "50".equals(arguments[0]) && "number".equals(arguments[1])) { + return PROPERTY_LABEL3; + } else if (DISCOVERY_THING2_INFERED_KEY.equals(key) || DISCOVERY_THING4_INFERED_KEY.equals(key)) { + return PROPERTY_LABEL2; + } + } + return defaultText; + } + + @Override + public @Nullable String getText(@Nullable Bundle bundle, @Nullable String key, @Nullable String defaultText, + @Nullable Locale locale) { + return null; + } + }; + + private LocaleProvider localeProvider = new LocaleProvider() { + @Override + public @NonNull Locale getLocale() { + return Locale.ENGLISH; + } + }; + + class TestDiscoveryService extends AbstractDiscoveryService { + + public TestDiscoveryService(TranslationProvider i18nProvider, LocaleProvider localeProvider) + throws IllegalArgumentException { + super(Set.of(THING_TYPE_UID), 1, false); + this.i18nProvider = i18nProvider; + this.localeProvider = localeProvider; + } + + @Override + protected void startScan() { + // Discovered thing 1 has a hard coded label and no key based on its thing UID defined in the properties + // file => the hard coded label should be considered + DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(THING_UID1).withThingType(THING_TYPE_UID) + .withProperties(properties).withRepresentationProperty(KEY1).withBridge(BRIDGE_UID) + .withLabel(DISCOVERY_LABEL).build(); + thingDiscovered(discoveryResult); + + // Discovered thing 2 has a hard coded label but with a key based on its thing UID defined in the properties + // file => the value from the properties file should be considered + discoveryResult = DiscoveryResultBuilder.create(THING_UID2).withThingType(THING_TYPE_UID) + .withProperties(properties).withRepresentationProperty(KEY1).withLabel(DISCOVERY_LABEL).build(); + thingDiscovered(discoveryResult); + + // Discovered thing 3 has a label referencing an entry in the properties file and no key based on its thing + // UID defined in the properties file => the value from the properties file should be considered + discoveryResult = DiscoveryResultBuilder.create(THING_UID3).withThingType(THING_TYPE_UID) + .withProperties(properties).withRepresentationProperty(KEY1).withBridge(BRIDGE_UID) + .withLabel(DISCOVERY_LABEL_KEY1).build(); + thingDiscovered(discoveryResult); + + // Discovered thing 4 has a label referencing an entry in the properties file and a key based on its thing + // UID defined in the properties file => the value from the properties file (the one referenced by the + // label) should be considered + discoveryResult = DiscoveryResultBuilder.create(THING_UID4).withThingType(THING_TYPE_UID) + .withProperties(properties).withRepresentationProperty(KEY1).withLabel(DISCOVERY_LABEL_KEY2) + .build(); + thingDiscovered(discoveryResult); + } + }; + + private TestDiscoveryService discoveryService; + + @Override + public void thingDiscovered(@NonNull DiscoveryService source, @NonNull DiscoveryResult result) { + assertThat(result.getThingTypeUID(), is(THING_TYPE_UID)); + assertThat(result.getBindingId(), is(BINDING_ID)); + assertThat(result.getProperties().size(), is(2)); + assertThat(result.getProperties(), hasEntry(KEY1, VALUE1)); + assertThat(result.getProperties(), hasEntry(KEY2, VALUE2)); + assertThat(result.getRepresentationProperty(), is(KEY1)); + assertThat(result.getTimeToLive(), is(DiscoveryResult.TTL_UNLIMITED)); + + if (THING_UID1.equals(result.getThingUID())) { + assertThat(result.getBridgeUID(), is(BRIDGE_UID)); + assertThat(result.getLabel(), is(DISCOVERY_LABEL)); + } else if (THING_UID2.equals(result.getThingUID())) { + assertNull(result.getBridgeUID()); + assertThat(result.getLabel(), is(PROPERTY_LABEL2)); + } else if (THING_UID3.equals(result.getThingUID())) { + assertThat(result.getBridgeUID(), is(BRIDGE_UID)); + assertThat(result.getLabel(), is(PROPERTY_LABEL1)); + } else if (THING_UID4.equals(result.getThingUID())) { + assertNull(result.getBridgeUID()); + assertThat(result.getLabel(), is(PROPERTY_LABEL3)); + } + } + + @Override + public void thingRemoved(@NonNull DiscoveryService source, @NonNull ThingUID thingUID) { + } + + @Override + public @Nullable Collection<@NonNull ThingUID> removeOlderResults(@NonNull DiscoveryService source, long timestamp, + @Nullable Collection<@NonNull ThingTypeUID> thingTypeUIDs, @Nullable ThingUID bridgeUID) { + return null; + } + + @Test + public void testDiscoveryResults() { + discoveryService = new TestDiscoveryService(i18nProvider, localeProvider); + discoveryService.addDiscoveryListener(this); + discoveryService.startScan(); + } +}