[tr064] Improvements of Phonebook Profile (#9054)

* Improvements of Phonebook Profile

Signed-off-by: Christoph Weitkamp <github@christophweitkamp.de>
This commit is contained in:
Christoph Weitkamp 2020-11-23 01:22:22 +01:00 committed by GitHub
parent 4cdbe65662
commit 0c30d90757
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 324 additions and 46 deletions

View File

@ -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() {

View File

@ -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<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();
}
}

View File

@ -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<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;
}

View 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

View 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>

View File

@ -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).

View File

@ -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));
}
}