mDNS / UPnP discovery internationalization (#2547)

* mDNS / UPnP discovery internationalization

Related to #2546

Signed-off-by: Laurent Garnier <lg.hc@free.fr>
This commit is contained in:
lolodomo
2021-11-03 21:35:13 +01:00
committed by GitHub
parent c4837b2c8b
commit 089b9d22ce
4 changed files with 271 additions and 23 deletions
@@ -29,9 +29,12 @@ import org.openhab.core.config.discovery.AbstractDiscoveryService;
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<String, Object> 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 @@ public class MDNSDiscoveryService extends AbstractDiscoveryService implements Se
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 @@ public class MDNSDiscoveryService extends AbstractDiscoveryService implements Se
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);
@@ -33,11 +33,14 @@ import org.openhab.core.config.discovery.AbstractDiscoveryService;
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 @@ public class UpnpDiscoveryService extends AbstractDiscoveryService
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 @@ public class UpnpDiscoveryService extends AbstractDiscoveryService
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 class UpnpDiscoveryService extends AbstractDiscoveryService
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);
@@ -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 @@ public abstract class AbstractDiscoveryService implements DiscoveryService {
* @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 @@ public abstract class AbstractDiscoveryService implements DiscoveryService {
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);
}
}
}
}
@@ -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<String, Object> 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();
}
}