From 0c30d90757986c9814d84c9c2729f0ef5488e4c9 Mon Sep 17 00:00:00 2001 From: Christoph Weitkamp Date: Mon, 23 Nov 2020 01:22:22 +0100 Subject: [PATCH] [tr064] Improvements of Phonebook Profile (#9054) * Improvements of Phonebook Profile Signed-off-by: Christoph Weitkamp --- ...a => AVMFritzTlsTrustManagerProvider.java} | 2 +- .../internal/phonebook/PhonebookProfile.java | 107 +++++++++--- .../phonebook/PhonebookProfileFactory.java | 68 ++++++-- .../phonebook/Tr064PhonebookImpl.java | 9 +- .../OH-INF/config/phonebookProfile.xml | 14 +- .../resources/OH-INF/i18n/tr064_de.properties | 7 + .../phonebook/PhonebookProfileTest.java | 163 ++++++++++++++++++ 7 files changed, 324 insertions(+), 46 deletions(-) rename bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/{AvmFritzTlsTrustManagerProvider.java => AVMFritzTlsTrustManagerProvider.java} (94%) create mode 100644 bundles/org.openhab.binding.tr064/src/main/resources/OH-INF/i18n/tr064_de.properties create mode 100644 bundles/org.openhab.binding.tr064/src/test/java/org/openhab/binding/tr064/internal/phonebook/PhonebookProfileTest.java diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/AvmFritzTlsTrustManagerProvider.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/AVMFritzTlsTrustManagerProvider.java similarity index 94% rename from bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/AvmFritzTlsTrustManagerProvider.java rename to bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/AVMFritzTlsTrustManagerProvider.java index 1f8f24f0cd3..12173333b6f 100644 --- a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/AvmFritzTlsTrustManagerProvider.java +++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/AVMFritzTlsTrustManagerProvider.java @@ -26,7 +26,7 @@ import org.osgi.service.component.annotations.Component; */ @Component @NonNullByDefault -public class AvmFritzTlsTrustManagerProvider implements TlsTrustManagerProvider { +public class AVMFritzTlsTrustManagerProvider implements TlsTrustManagerProvider { @Override public String getHostName() { diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/PhonebookProfile.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/PhonebookProfile.java index 33bc639f27d..89dcaf9a4f3 100644 --- a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/PhonebookProfile.java +++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/PhonebookProfile.java @@ -12,18 +12,27 @@ */ package org.openhab.binding.tr064.internal.phonebook; +import java.math.BigDecimal; import java.util.Map; import java.util.Optional; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.config.core.Configuration; +import org.openhab.core.library.CoreItemFactory; +import org.openhab.core.library.types.StringListType; import org.openhab.core.library.types.StringType; import org.openhab.core.thing.ThingUID; -import org.openhab.core.thing.profiles.*; +import org.openhab.core.thing.profiles.ProfileCallback; +import org.openhab.core.thing.profiles.ProfileContext; +import org.openhab.core.thing.profiles.ProfileType; +import org.openhab.core.thing.profiles.ProfileTypeBuilder; +import org.openhab.core.thing.profiles.ProfileTypeUID; +import org.openhab.core.thing.profiles.StateProfile; import org.openhab.core.transform.TransformationService; import org.openhab.core.types.Command; import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; import org.openhab.core.util.UIDUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -37,11 +46,15 @@ import org.slf4j.LoggerFactory; public class PhonebookProfile implements StateProfile { public static final ProfileTypeUID PHONEBOOK_PROFILE_TYPE_UID = new ProfileTypeUID( TransformationService.TRANSFORM_PROFILE_SCOPE, "PHONEBOOK"); - public static final ProfileType PHONEBOOK_PROFILE_TYPE = ProfileTypeBuilder - .newState(PhonebookProfile.PHONEBOOK_PROFILE_TYPE_UID, "Phonebook").build(); + public static final ProfileType PHONEBOOK_PROFILE_TYPE = ProfileTypeBuilder // + .newState(PHONEBOOK_PROFILE_TYPE_UID, "Phonebook") // + .withSupportedItemTypesOfChannel(CoreItemFactory.CALL, CoreItemFactory.STRING) // + .withSupportedItemTypes(CoreItemFactory.STRING) // + .build(); public static final String PHONEBOOK_PARAM = "phonebook"; - private static final String MATCH_COUNT_PARAM = "matchCount"; + public static final String MATCH_COUNT_PARAM = "matchCount"; + public static final String PHONE_NUMBER_INDEX_PARAM = "phoneNumberIndex"; private final Logger logger = LoggerFactory.getLogger(PhonebookProfile.class); @@ -51,6 +64,7 @@ public class PhonebookProfile implements StateProfile { private final @Nullable ThingUID thingUID; private final Map phonebookProviders; private final int matchCount; + private final int phoneNumberIndex; public PhonebookProfile(ProfileCallback callback, ProfileContext context, Map phonebookProviders) { @@ -60,32 +74,44 @@ public class PhonebookProfile implements StateProfile { Configuration configuration = context.getConfiguration(); Object phonebookParam = configuration.get(PHONEBOOK_PARAM); Object matchCountParam = configuration.get(MATCH_COUNT_PARAM); + Object phoneNumberIndexParam = configuration.get(PHONE_NUMBER_INDEX_PARAM); - logger.debug("Profile configured with '{}'='{}', '{}'='{}'", PHONEBOOK_PARAM, phonebookParam, MATCH_COUNT_PARAM, - matchCountParam); + logger.debug("Profile configured with '{}'='{}', '{}'='{}', '{}'='{}'", PHONEBOOK_PARAM, phonebookParam, + MATCH_COUNT_PARAM, matchCountParam, PHONE_NUMBER_INDEX_PARAM, phoneNumberIndexParam); ThingUID thingUID; String phonebookName = null; int matchCount = 0; + int phoneNumberIndex = 0; try { - if (!(phonebookParam instanceof String) - || ((matchCountParam != null) && !(matchCountParam instanceof String))) { - throw new IllegalArgumentException("Parameters need to be Strings"); + if (!(phonebookParam instanceof String)) { + throw new IllegalArgumentException("Parameter 'phonebook' need to be a String"); } String[] phonebookParams = ((String) phonebookParam).split(":"); if (phonebookParams.length > 2) { - throw new IllegalArgumentException("Could not split 'phonebook' parameter"); + throw new IllegalArgumentException("Cannot split 'phonebook' parameter"); } thingUID = new ThingUID(UIDUtils.decode(phonebookParams[0])); if (phonebookParams.length == 2) { phonebookName = UIDUtils.decode(phonebookParams[1]); } if (matchCountParam != null) { - matchCount = Integer.parseInt((String) matchCountParam); + if (matchCountParam instanceof BigDecimal) { + matchCount = ((BigDecimal) matchCountParam).intValue(); + } else if (matchCountParam instanceof String) { + matchCount = Integer.parseInt((String) matchCountParam); + } + } + if (phoneNumberIndexParam != null) { + if (phoneNumberIndexParam instanceof BigDecimal) { + phoneNumberIndex = ((BigDecimal) phoneNumberIndexParam).intValue(); + } else if (phoneNumberIndexParam instanceof String) { + phoneNumberIndex = Integer.parseInt((String) phoneNumberIndexParam); + } } } catch (IllegalArgumentException e) { - logger.warn("Could not initialize PHONEBOOK transformation profile: {}. Profile will be inactive.", + logger.warn("Cannot initialize PHONEBOOK transformation profile: {}. Profile will be inactive.", e.getMessage()); thingUID = null; } @@ -93,6 +119,7 @@ public class PhonebookProfile implements StateProfile { this.thingUID = thingUID; this.phonebookName = phonebookName; this.matchCount = matchCount; + this.phoneNumberIndex = phoneNumberIndex; } @Override @@ -105,29 +132,53 @@ public class PhonebookProfile implements StateProfile { @Override public void onStateUpdateFromHandler(State state) { + if (state instanceof UnDefType) { + // we cannot adjust UNDEF or NULL values, thus we simply apply them without reporting an error or warning + callback.sendUpdate(state); + } if (state instanceof StringType) { - PhonebookProvider provider = phonebookProviders.get(thingUID); - if (provider == null) { - logger.warn("Could not get phonebook provider with thing UID '{}'.", thingUID); - return; - } - final String phonebookName = this.phonebookName; - Optional match; - if (phonebookName != null) { - match = provider.getPhonebookByName(phonebookName).or(() -> { - logger.warn("Could not get phonebook '{}' from provider '{}'", phonebookName, thingUID); - return Optional.empty(); - }).flatMap(phonebook -> phonebook.lookupNumber(state.toString(), matchCount)); - } else { - match = provider.getPhonebooks().stream().map(p -> p.lookupNumber(state.toString(), matchCount)) - .filter(Optional::isPresent).map(Optional::get).findAny(); - } + Optional match = resolveNumber(state.toString()); State newState = match.map(name -> (State) new StringType(name)).orElse(state); if (newState == state) { logger.debug("Number '{}' not found in phonebook '{}' from provider '{}'", state, phonebookName, thingUID); } callback.sendUpdate(newState); + } else if (state instanceof StringListType) { + StringListType stringList = (StringListType) state; + try { + String phoneNumber = stringList.getValue(phoneNumberIndex); + Optional match = resolveNumber(phoneNumber); + final State newState; + if (match.isPresent()) { + newState = new StringType(match.get()); + } else { + logger.debug("Number '{}' not found in phonebook '{}' from provider '{}'", phoneNumber, + phonebookName, thingUID); + newState = new StringType(phoneNumber); + } + callback.sendUpdate(newState); + } catch (IllegalArgumentException e) { + logger.debug("StringListType does not contain a number at index {}", phoneNumberIndex); + } + } + } + + private Optional resolveNumber(String phoneNumber) { + PhonebookProvider provider = phonebookProviders.get(thingUID); + if (provider == null) { + logger.warn("Could not get phonebook provider with thing UID '{}'.", thingUID); + return Optional.empty(); + } + final String phonebookName = this.phonebookName; + if (phonebookName != null) { + return provider.getPhonebookByName(phonebookName).or(() -> { + logger.warn("Could not get phonebook '{}' from provider '{}'", phonebookName, thingUID); + return Optional.empty(); + }).flatMap(phonebook -> phonebook.lookupNumber(phoneNumber, matchCount)); + } else { + return provider.getPhonebooks().stream().map(p -> p.lookupNumber(phoneNumber, matchCount)) + .filter(Optional::isPresent).map(Optional::get).findAny(); } } diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/PhonebookProfileFactory.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/PhonebookProfileFactory.java index 054e09c0cfc..f70b51d95f9 100644 --- a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/PhonebookProfileFactory.java +++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/PhonebookProfileFactory.java @@ -12,19 +12,23 @@ */ package org.openhab.binding.tr064.internal.phonebook; +import static java.util.Comparator.comparing; + import java.net.URI; +import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; +import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; -import java.util.stream.Stream; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.config.core.ConfigOptionProvider; import org.openhab.core.config.core.ParameterOption; +import org.openhab.core.i18n.LocalizedKey; import org.openhab.core.thing.ThingUID; import org.openhab.core.thing.profiles.Profile; import org.openhab.core.thing.profiles.ProfileCallback; @@ -33,8 +37,13 @@ import org.openhab.core.thing.profiles.ProfileFactory; import org.openhab.core.thing.profiles.ProfileType; import org.openhab.core.thing.profiles.ProfileTypeProvider; import org.openhab.core.thing.profiles.ProfileTypeUID; +import org.openhab.core.thing.profiles.i18n.ProfileTypeI18nLocalizationService; +import org.openhab.core.util.BundleResolver; import org.openhab.core.util.UIDUtils; +import org.osgi.framework.Bundle; +import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -50,6 +59,19 @@ public class PhonebookProfileFactory implements ProfileFactory, ProfileTypeProvi private final Logger logger = LoggerFactory.getLogger(PhonebookProfileFactory.class); private final Map phonebookProviders = new ConcurrentHashMap<>(); + private final Map localizedProfileTypeCache = new ConcurrentHashMap<>(); + + private final ProfileTypeI18nLocalizationService profileTypeI18nLocalizationService; + private final Bundle bundle; + + @Activate + public PhonebookProfileFactory( + final @Reference ProfileTypeI18nLocalizationService profileTypeI18nLocalizationService, + final @Reference BundleResolver bundleResolver) { + this.profileTypeI18nLocalizationService = profileTypeI18nLocalizationService; + this.bundle = bundleResolver.resolveBundle(PhonebookProfileFactory.class); + } + @Override public @Nullable Profile createProfile(ProfileTypeUID profileTypeUID, ProfileCallback callback, ProfileContext profileContext) { @@ -58,12 +80,31 @@ public class PhonebookProfileFactory implements ProfileFactory, ProfileTypeProvi @Override public Collection getSupportedProfileTypeUIDs() { - return Collections.singleton(PhonebookProfile.PHONEBOOK_PROFILE_TYPE_UID); + return Set.of(PhonebookProfile.PHONEBOOK_PROFILE_TYPE_UID); } @Override public Collection getProfileTypes(@Nullable Locale locale) { - return Collections.singleton(PhonebookProfile.PHONEBOOK_PROFILE_TYPE); + return Set.of(createLocalizedProfileType(PhonebookProfile.PHONEBOOK_PROFILE_TYPE, locale)); + } + + private ProfileType createLocalizedProfileType(ProfileType profileType, @Nullable Locale locale) { + final LocalizedKey localizedKey = new LocalizedKey(profileType.getUID(), + locale != null ? locale.toLanguageTag() : null); + + final ProfileType cachedlocalizedProfileType = localizedProfileTypeCache.get(localizedKey); + if (cachedlocalizedProfileType != null) { + return cachedlocalizedProfileType; + } + + final ProfileType localizedProfileType = profileTypeI18nLocalizationService.createLocalizedProfileType(bundle, + profileType, locale); + if (localizedProfileType != null) { + localizedProfileTypeCache.put(localizedKey, localizedProfileType); + return localizedProfileType; + } else { + return profileType; + } } /** @@ -90,16 +131,17 @@ public class PhonebookProfileFactory implements ProfileFactory, ProfileTypeProvi } } - private Stream createPhonebookList(Map.Entry entry) { + private List createPhonebookList(Map.Entry entry) { String thingUid = UIDUtils.encode(entry.getKey().toString()); String thingName = entry.getValue().getFriendlyName(); - Stream parameterOptions = entry.getValue().getPhonebooks().stream() + List parameterOptions = entry.getValue().getPhonebooks().stream() .map(phonebook -> new ParameterOption(thingUid + ":" + UIDUtils.encode(phonebook.getName()), - thingName + " " + phonebook.getName())); + thingName + " - " + phonebook.getName())) + .collect(Collectors.toList()); - if (parameterOptions.count() > 0) { - return Stream.concat(Stream.of(new ParameterOption(thingUid, thingName)), parameterOptions); + if (parameterOptions.size() > 0) { + parameterOptions.add(new ParameterOption(thingUid, thingName)); } return parameterOptions; @@ -110,8 +152,12 @@ public class PhonebookProfileFactory implements ProfileFactory, ProfileTypeProvi @Nullable Locale locale) { if (uri.getSchemeSpecificPart().equals(PhonebookProfile.PHONEBOOK_PROFILE_TYPE_UID.toString()) && s.equals(PhonebookProfile.PHONEBOOK_PARAM)) { - return phonebookProviders.entrySet().stream().flatMap(this::createPhonebookList) - .collect(Collectors.toSet()); + List parameterOptions = new ArrayList<>(); + for (Map.Entry entry : phonebookProviders.entrySet()) { + parameterOptions.addAll(createPhonebookList(entry)); + } + parameterOptions.sort(comparing(o -> o.getLabel())); + return parameterOptions; } return null; } diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/Tr064PhonebookImpl.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/Tr064PhonebookImpl.java index bf105c313d3..a020c72dce5 100644 --- a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/Tr064PhonebookImpl.java +++ b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/Tr064PhonebookImpl.java @@ -88,9 +88,12 @@ public class Tr064PhonebookImpl implements Phonebook { @Override public Optional lookupNumber(String number, int matchCount) { - String matchString = matchCount < number.length() ? number.substring(number.length() - matchCount) : number; - logger.trace("matchString for {} is {}", number, matchString); - return phonebook.keySet().stream().filter(n -> n.endsWith(matchString)).findAny().map(phonebook::get); + String matchString = matchCount > 0 && matchCount < number.length() + ? number.substring(number.length() - matchCount) + : number; + logger.trace("matchString for '{}' is '{}'", number, matchString); + return matchString.isBlank() ? Optional.empty() + : phonebook.keySet().stream().filter(n -> n.endsWith(matchString)).findFirst().map(phonebook::get); } @Override diff --git a/bundles/org.openhab.binding.tr064/src/main/resources/OH-INF/config/phonebookProfile.xml b/bundles/org.openhab.binding.tr064/src/main/resources/OH-INF/config/phonebookProfile.xml index f603d7d2118..462e36cf962 100644 --- a/bundles/org.openhab.binding.tr064/src/main/resources/OH-INF/config/phonebookProfile.xml +++ b/bundles/org.openhab.binding.tr064/src/main/resources/OH-INF/config/phonebookProfile.xml @@ -3,14 +3,22 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0" xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd"> + - - The name of the the phonebook + + The name of the the phone book - + The number of digits matching the incoming value, counted from far right (default is 0 = all matching) + 0 + + + + The index of the phone number to be resolved from a CallItem state (StringListType), 0 or 1 (default is + 0) + 0 diff --git a/bundles/org.openhab.binding.tr064/src/main/resources/OH-INF/i18n/tr064_de.properties b/bundles/org.openhab.binding.tr064/src/main/resources/OH-INF/i18n/tr064_de.properties new file mode 100644 index 00000000000..ba097de0a2a --- /dev/null +++ b/bundles/org.openhab.binding.tr064/src/main/resources/OH-INF/i18n/tr064_de.properties @@ -0,0 +1,7 @@ +profile-type.transform.PHONEBOOK.label = Telefonbuch +profile.config.transform.PHONEBOOK.phonebook.label = Telefonbuch +profile.config.transform.PHONEBOOK.phonebook.description = Der Name des Telefonbuches. +profile.config.transform.PHONEBOOK.matchCount.label = Übereinstimmungen +profile.config.transform.PHONEBOOK.matchCount.description = Die Anzahl der Ziffern, die mit dem eingehenden Wert übereinstimmen, von rechts gezählt (Vorgabe ist 0 = alle müssen übereinstimmen). +profile.config.transform.PHONEBOOK.phoneNumberIndex.label = Telefonnummern-Index +profile.config.transform.PHONEBOOK.phoneNumberIndex.description = Der Index der Telefonnummer, die aus einem CallItem-State (StringListType) aufgelöst werden soll, 0 oder 1 (Vorgabe ist 0). diff --git a/bundles/org.openhab.binding.tr064/src/test/java/org/openhab/binding/tr064/internal/phonebook/PhonebookProfileTest.java b/bundles/org.openhab.binding.tr064/src/test/java/org/openhab/binding/tr064/internal/phonebook/PhonebookProfileTest.java new file mode 100644 index 00000000000..b51f665a1a0 --- /dev/null +++ b/bundles/org.openhab.binding.tr064/src/test/java/org/openhab/binding/tr064/internal/phonebook/PhonebookProfileTest.java @@ -0,0 +1,163 @@ +/** + * Copyright (c) 2010-2020 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.binding.tr064.internal.phonebook; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; +import static org.mockito.MockitoAnnotations.openMocks; +import static org.openhab.binding.tr064.internal.Tr064BindingConstants.*; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mock; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.library.types.StringListType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.profiles.ProfileCallback; +import org.openhab.core.thing.profiles.ProfileContext; +import org.openhab.core.thing.profiles.StateProfile; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; +import org.openhab.core.util.UIDUtils; + +/** + * + * @author Christoph Weitkamp - Initial contribution + */ +class PhonebookProfileTest { + + private static final String INTERNAL_PHONE_NUMBER = "999"; + private static final String OTHER_PHONE_NUMBER = "555-456"; + private static final String JOHN_DOES_PHONE_NUMBER = "12345"; + private static final String JOHN_DOES_NAME = "John Doe"; + private static final ThingUID THING_UID = new ThingUID(BINDING_ID, THING_TYPE_FRITZBOX.getId(), "test"); + private static final String MY_PHONEBOOK = UIDUtils.encode(THING_UID.getAsString()) + ":MyPhonebook"; + + @NonNullByDefault + public static class ParameterSet { + public final State state; + public final State resultingState; + public final @Nullable Object matchCount; + public final @Nullable Object phoneNumberIndex; + + public ParameterSet(State state, State resultingState, @Nullable Object matchCount, + @Nullable Object phoneNumberIndex) { + this.state = state; + this.resultingState = resultingState; + this.matchCount = matchCount; + this.phoneNumberIndex = phoneNumberIndex; + } + } + + public static Collection parameters() { + return Arrays.asList(new Object[][] { // + { new ParameterSet(UnDefType.UNDEF, UnDefType.UNDEF, null, null) }, // + { new ParameterSet(new StringType(JOHN_DOES_PHONE_NUMBER), new StringType(JOHN_DOES_NAME), null, + null) }, // + { new ParameterSet(new StringType(JOHN_DOES_PHONE_NUMBER), new StringType(JOHN_DOES_NAME), + BigDecimal.ONE, null) }, // + { new ParameterSet(new StringType(JOHN_DOES_PHONE_NUMBER), new StringType(JOHN_DOES_NAME), "3", null) }, // + { new ParameterSet(new StringListType(JOHN_DOES_PHONE_NUMBER, INTERNAL_PHONE_NUMBER), + new StringType(JOHN_DOES_NAME), null, null) }, // + { new ParameterSet(new StringListType(JOHN_DOES_PHONE_NUMBER, INTERNAL_PHONE_NUMBER), + new StringType(JOHN_DOES_NAME), null, BigDecimal.ZERO) }, // + { new ParameterSet(new StringListType(INTERNAL_PHONE_NUMBER, JOHN_DOES_PHONE_NUMBER), + new StringType(JOHN_DOES_NAME), null, BigDecimal.ONE) }, // + { new ParameterSet(new StringType(OTHER_PHONE_NUMBER), new StringType(OTHER_PHONE_NUMBER), null, + null) }, // + { new ParameterSet(new StringListType(OTHER_PHONE_NUMBER, INTERNAL_PHONE_NUMBER), + new StringType(OTHER_PHONE_NUMBER), null, null) }, // + { new ParameterSet(new StringListType(OTHER_PHONE_NUMBER, INTERNAL_PHONE_NUMBER), + new StringType(OTHER_PHONE_NUMBER), null, BigDecimal.ZERO) }, // + { new ParameterSet(new StringListType(INTERNAL_PHONE_NUMBER, OTHER_PHONE_NUMBER), + new StringType(OTHER_PHONE_NUMBER), null, BigDecimal.ONE) }, // + }); + } + + private AutoCloseable mocksCloseable; + + private @Mock ProfileCallback mockCallback; + private @Mock ProfileContext mockContext; + private @Mock PhonebookProvider mockPhonebookProvider; + + @NonNullByDefault + private final Phonebook phonebook = new Phonebook() { + @Override + public Optional lookupNumber(String number, int matchCount) { + switch (number) { + case JOHN_DOES_PHONE_NUMBER: + return Optional.of(JOHN_DOES_NAME); + default: + return Optional.empty(); + } + } + + @Override + public String getName() { + return MY_PHONEBOOK; + } + }; + + @BeforeEach + public void setup() { + mocksCloseable = openMocks(this); + + when(mockPhonebookProvider.getPhonebookByName(any(String.class))).thenReturn(Optional.of(phonebook)); + when(mockPhonebookProvider.getPhonebooks()).thenReturn(Set.of(phonebook)); + } + + @AfterEach + public void afterEach() throws Exception { + mocksCloseable.close(); + } + + @ParameterizedTest + @MethodSource("parameters") + public void testPhonebookProfileResolvesPhoneNumber(ParameterSet parameterSet) { + StateProfile profile = initProfile(MY_PHONEBOOK, parameterSet.matchCount, parameterSet.phoneNumberIndex); + verifySendUpdate(profile, parameterSet.state, parameterSet.resultingState); + } + + private StateProfile initProfile(Object phonebookName, @Nullable Object matchCount, + @Nullable Object phoneNumberIndex) { + Map properties = new HashMap<>(); + properties.put(PhonebookProfile.PHONEBOOK_PARAM, phonebookName); + if (matchCount != null) { + properties.put(PhonebookProfile.MATCH_COUNT_PARAM, matchCount); + } + if (phoneNumberIndex != null) { + properties.put(PhonebookProfile.PHONE_NUMBER_INDEX_PARAM, phoneNumberIndex); + } + when(mockContext.getConfiguration()).thenReturn(new Configuration(properties)); + return new PhonebookProfile(mockCallback, mockContext, Map.of(THING_UID, mockPhonebookProvider)); + } + + private void verifySendUpdate(StateProfile profile, State state, State expectedState) { + reset(mockCallback); + profile.onStateUpdateFromHandler(state); + verify(mockCallback, times(1)).sendUpdate(eq(expectedState)); + } +}