Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

mDNS / UPnP discovery internationalization #2547

Merged
merged 7 commits into from
Nov 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
lolodomo marked this conversation as resolved.
Show resolved Hide resolved
@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();
}
}