Skip to content

Commit

Permalink
mDNS / UPnP discovery internationalization (openhab#2547)
Browse files Browse the repository at this point in the history
* mDNS / UPnP discovery internationalization

Related to openhab#2546

Signed-off-by: Laurent Garnier <lg.hc@free.fr>
  • Loading branch information
lolodomo committed Nov 3, 2021
1 parent c4837b2 commit 089b9d2
Show file tree
Hide file tree
Showing 4 changed files with 271 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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);
}
}
}
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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);
}
}
}
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}

0 comments on commit 089b9d2

Please sign in to comment.