]> git.basschouten.com Git - openhab-addons.git/commitdiff
[tr064] Improvements of Phonebook Profile (#9054)
authorChristoph Weitkamp <github@christophweitkamp.de>
Mon, 23 Nov 2020 00:22:22 +0000 (01:22 +0100)
committerGitHub <noreply@github.com>
Mon, 23 Nov 2020 00:22:22 +0000 (16:22 -0800)
* Improvements of Phonebook Profile

Signed-off-by: Christoph Weitkamp <github@christophweitkamp.de>
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/AVMFritzTlsTrustManagerProvider.java [new file with mode: 0644]
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/AvmFritzTlsTrustManagerProvider.java [deleted file]
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/PhonebookProfile.java
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/PhonebookProfileFactory.java
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/phonebook/Tr064PhonebookImpl.java
bundles/org.openhab.binding.tr064/src/main/resources/OH-INF/config/phonebookProfile.xml
bundles/org.openhab.binding.tr064/src/main/resources/OH-INF/i18n/tr064_de.properties [new file with mode: 0644]
bundles/org.openhab.binding.tr064/src/test/java/org/openhab/binding/tr064/internal/phonebook/PhonebookProfileTest.java [new file with mode: 0644]

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
new file mode 100644 (file)
index 0000000..1217333
--- /dev/null
@@ -0,0 +1,40 @@
+/**
+ * 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;
+
+import javax.net.ssl.X509ExtendedTrustManager;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.io.net.http.TlsTrustManagerProvider;
+import org.openhab.core.io.net.http.TrustAllTrustManager;
+import org.osgi.service.component.annotations.Component;
+
+/**
+ * Provides a TrustManager to allow secure connections to any FRITZ!Box
+ *
+ * @author Christoph Weitkamp - Initial Contribution
+ */
+@Component
+@NonNullByDefault
+public class AVMFritzTlsTrustManagerProvider implements TlsTrustManagerProvider {
+
+    @Override
+    public String getHostName() {
+        return "fritz.box";
+    }
+
+    @Override
+    public X509ExtendedTrustManager getTrustManager() {
+        return TrustAllTrustManager.getInstance();
+    }
+}
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
deleted file mode 100644 (file)
index 1f8f24f..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-/**
- * 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;
-
-import javax.net.ssl.X509ExtendedTrustManager;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.openhab.core.io.net.http.TlsTrustManagerProvider;
-import org.openhab.core.io.net.http.TrustAllTrustManager;
-import org.osgi.service.component.annotations.Component;
-
-/**
- * Provides a TrustManager to allow secure connections to any FRITZ!Box
- *
- * @author Christoph Weitkamp - Initial Contribution
- */
-@Component
-@NonNullByDefault
-public class AvmFritzTlsTrustManagerProvider implements TlsTrustManagerProvider {
-
-    @Override
-    public String getHostName() {
-        return "fritz.box";
-    }
-
-    @Override
-    public X509ExtendedTrustManager getTrustManager() {
-        return TrustAllTrustManager.getInstance();
-    }
-}
index 33bc639f27d069699abde3cdaf6bc7d4cab1b2f2..89dcaf9a4f337fd036e1187f90e8814f167be657 100644 (file)
  */
 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<ThingUID, PhonebookProvider> phonebookProviders;
     private final int matchCount;
+    private final int phoneNumberIndex;
 
     public PhonebookProfile(ProfileCallback callback, ProfileContext context,
             Map<ThingUID, PhonebookProvider> 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<String> 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<String> 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<String> 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<String> 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();
         }
     }
 
index 054e09c0cfc344100c7f91dbfb4e4ebfe6277b22..f70b51d95f92bbcc17a9a9bb12161628ca6dd645 100644 (file)
  */
 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<ThingUID, PhonebookProvider> phonebookProviders = new ConcurrentHashMap<>();
 
+    private final Map<LocalizedKey, ProfileType> 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<ProfileTypeUID> getSupportedProfileTypeUIDs() {
-        return Collections.singleton(PhonebookProfile.PHONEBOOK_PROFILE_TYPE_UID);
+        return Set.of(PhonebookProfile.PHONEBOOK_PROFILE_TYPE_UID);
     }
 
     @Override
     public Collection<ProfileType> 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<ParameterOption> createPhonebookList(Map.Entry<ThingUID, PhonebookProvider> entry) {
+    private List<ParameterOption> createPhonebookList(Map.Entry<ThingUID, PhonebookProvider> entry) {
         String thingUid = UIDUtils.encode(entry.getKey().toString());
         String thingName = entry.getValue().getFriendlyName();
 
-        Stream<ParameterOption> parameterOptions = entry.getValue().getPhonebooks().stream()
+        List<ParameterOption> 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<ParameterOption> parameterOptions = new ArrayList<>();
+            for (Map.Entry<ThingUID, PhonebookProvider> entry : phonebookProviders.entrySet()) {
+                parameterOptions.addAll(createPhonebookList(entry));
+            }
+            parameterOptions.sort(comparing(o -> o.getLabel()));
+            return parameterOptions;
         }
         return null;
     }
index bf105c313d36d97b2afac94c88fdc1984e554bc0..a020c72dce5c91a0b01f7e7c6cb4fc7cd17eb123 100644 (file)
@@ -88,9 +88,12 @@ public class Tr064PhonebookImpl implements Phonebook {
 
     @Override
     public Optional<String> 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
index f603d7d211857173ce0a074d7f673e78643c3d7d..462e36cf96273a20c43db2b4a71a87e66b5fa25b 100644 (file)
@@ -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">
+
        <config-description uri="profile:transform:PHONEBOOK">
                <parameter name="phonebook" type="text" required="true">
-                       <label>Phonebook</label>
-                       <description>The name of the the phonebook</description>
+                       <label>Phone book</label>
+                       <description>The name of the the phone book</description>
                </parameter>
-               <parameter name="matchCount" type="text" required="false">
+               <parameter name="matchCount" type="integer" min="0" step="1">
                        <label>Match Count</label>
                        <description>The number of digits matching the incoming value, counted from far right (default is 0 = all matching)</description>
+                       <default>0</default>
+               </parameter>
+               <parameter name="phoneNumberIndex" type="integer" min="0" max="1" step="1">
+                       <label>Phone Number Index</label>
+                       <description>The index of the phone number to be resolved from a CallItem state (StringListType), 0 or 1 (default is
+                               0)</description>
+                       <default>0</default>
                </parameter>
        </config-description>
 </config-description:config-descriptions>
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 (file)
index 0000000..ba097de
--- /dev/null
@@ -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 (file)
index 0000000..b51f665
--- /dev/null
@@ -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<Object[]> 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<String> 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<String, Object> 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));
+    }
+}