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

[homeconnect] Predefined temp / spin speeds options for unsupported washer programs #10953

Merged
merged 3 commits into from
Jul 25, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -19,11 +19,9 @@
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
Expand Down Expand Up @@ -79,7 +77,6 @@ public class HomeConnectApiClient {
private final Logger logger = LoggerFactory.getLogger(HomeConnectApiClient.class);
private final HttpClient client;
private final String apiUrl;
private final Map<String, List<AvailableProgram>> programsCache;
private final OAuthClientService oAuthClientService;
private final CircularQueue<ApiRequest> communicationQueue;
private final ApiBridgeConfiguration apiBridgeConfiguration;
Expand All @@ -90,7 +87,6 @@ public HomeConnectApiClient(HttpClient httpClient, OAuthClientService oAuthClien
this.oAuthClientService = oAuthClientService;
this.apiBridgeConfiguration = apiBridgeConfiguration;

programsCache = new ConcurrentHashMap<>();
apiUrl = simulated ? API_SIMULATOR_BASE_URL : API_BASE_URL;
communicationQueue = new CircularQueue<>(COMMUNICATION_QUEUE_SIZE);
if (apiRequestHistory != null) {
Expand Down Expand Up @@ -610,16 +606,7 @@ public void stopProgram(String haId)

public List<AvailableProgram> getPrograms(String haId)
throws CommunicationException, AuthorizationException, ApplianceOfflineException {
List<AvailableProgram> programs;
if (programsCache.containsKey(haId)) {
logger.debug("Returning cached programs for '{}'.", haId);
programs = programsCache.get(haId);
programs = programs != null ? programs : Collections.emptyList();
} else {
programs = getAvailablePrograms(haId, BASE_PATH + haId + "/programs");
programsCache.put(haId, programs);
}
return programs;
return getAvailablePrograms(haId, BASE_PATH + haId + "/programs");
}

public List<AvailableProgram> getAvailablePrograms(String haId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,39 @@
* AvailableProgram model
*
* @author Jonas Brüstel - Initial contribution
* @author Laurent Garnier - field "supported" added
*
*/
@NonNullByDefault
public class AvailableProgram {
private final String key;
private final boolean supported;
private final boolean available;
private final String execution;

public AvailableProgram(String key, boolean available, String execution) {
public AvailableProgram(String key, boolean supported, boolean available, String execution) {
this.key = key;
this.supported = supported;
this.available = available;
this.execution = execution;
}

public AvailableProgram(String key, boolean available, String execution) {
this(key, true, available, execution);
}

public AvailableProgram(String key, boolean supported) {
this(key, supported, true, "");
}

public String getKey() {
return key;
}

public boolean isSupported() {
return supported;
}

public boolean isAvailable() {
return available;
}
Expand All @@ -46,6 +61,7 @@ public String getExecution() {

@Override
public String toString() {
return "AvailableProgram [key=" + key + ", available=" + available + ", execution=" + execution + "]";
return "AvailableProgram [key=" + key + ", supported=" + supported + ", available=" + available + ", execution="
+ execution + "]";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
Expand All @@ -44,6 +45,7 @@
import org.openhab.binding.homeconnect.internal.client.exception.AuthorizationException;
import org.openhab.binding.homeconnect.internal.client.exception.CommunicationException;
import org.openhab.binding.homeconnect.internal.client.listener.HomeConnectEventListener;
import org.openhab.binding.homeconnect.internal.client.model.AvailableProgram;
import org.openhab.binding.homeconnect.internal.client.model.AvailableProgramOption;
import org.openhab.binding.homeconnect.internal.client.model.Data;
import org.openhab.binding.homeconnect.internal.client.model.Event;
Expand Down Expand Up @@ -83,6 +85,7 @@
* sent to one of the channels.
*
* @author Jonas Brüstel - Initial contribution
* @author Laurent Garnier - programs cache moved and enhanced to allow adding unsupported programs
*/
@NonNullByDefault
public abstract class AbstractHomeConnectThingHandler extends BaseThingHandler implements HomeConnectEventListener {
Expand All @@ -105,7 +108,9 @@ public abstract class AbstractHomeConnectThingHandler extends BaseThingHandler i
private final ExpiringStateMap expiringStateMap;
private final AtomicBoolean accessible;
private final Logger logger = LoggerFactory.getLogger(AbstractHomeConnectThingHandler.class);
private final List<AvailableProgram> programsCache;
private final Map<String, List<AvailableProgramOption>> availableProgramOptionsCache;
private final Map<String, List<AvailableProgramOption>> unsupportedProgramOptions;

public AbstractHomeConnectThingHandler(Thing thing,
HomeConnectDynamicStateDescriptionProvider dynamicStateDescriptionProvider) {
Expand All @@ -115,10 +120,13 @@ public AbstractHomeConnectThingHandler(Thing thing,
this.dynamicStateDescriptionProvider = dynamicStateDescriptionProvider;
expiringStateMap = new ExpiringStateMap(Duration.ofSeconds(CACHE_TTL_SEC));
accessible = new AtomicBoolean(false);
programsCache = new CopyOnWriteArrayList<>();
availableProgramOptionsCache = new ConcurrentHashMap<>();
unsupportedProgramOptions = new ConcurrentHashMap<>();

configureEventHandlers(eventHandlers);
configureChannelUpdateHandlers(channelUpdateHandlers);
configureUnsupportedProgramOptions(unsupportedProgramOptions);
}

@Override
Expand Down Expand Up @@ -207,7 +215,8 @@ && getBridgeHandler().isPresent()) {
logger.debug("Start custom program. command={} haId={}", command.toFullString(), getThingHaId());
apiClient.startCustomProgram(getThingHaId(), command.toFullString());
}
} else if (command instanceof StringType && CHANNEL_SELECTED_PROGRAM_STATE.equals(channelUID.getId())) {
} else if (command instanceof StringType && CHANNEL_SELECTED_PROGRAM_STATE.equals(channelUID.getId())
&& isProgramSupported(command.toFullString())) {
apiClient.setSelectedProgram(getThingHaId(), command.toFullString());
}
}
Expand Down Expand Up @@ -347,20 +356,15 @@ protected void updateSelectedProgramStateDescription() {
return;
}

Optional<HomeConnectApiClient> apiClient = getApiClient();
if (apiClient.isPresent()) {
try {
List<StateOption> stateOptions = apiClient.get().getPrograms(getThingHaId()).stream()
.map(p -> new StateOption(p.getKey(), mapStringType(p.getKey()))).collect(Collectors.toList());
try {
List<StateOption> stateOptions = getPrograms().stream()
.map(p -> new StateOption(p.getKey(), mapStringType(p.getKey()))).collect(Collectors.toList());

getThingChannel(CHANNEL_SELECTED_PROGRAM_STATE).ifPresent(
channel -> dynamicStateDescriptionProvider.setStateOptions(channel.getUID(), stateOptions));
} catch (CommunicationException | ApplianceOfflineException | AuthorizationException e) {
logger.debug("Could not fetch available programs. thing={}, haId={}, error={}", getThingLabel(),
getThingHaId(), e.getMessage());
removeSelectedProgramStateDescription();
}
} else {
getThingChannel(CHANNEL_SELECTED_PROGRAM_STATE).ifPresent(
channel -> dynamicStateDescriptionProvider.setStateOptions(channel.getUID(), stateOptions));
} catch (CommunicationException | ApplianceOfflineException | AuthorizationException e) {
logger.debug("Could not fetch available programs. thing={}, haId={}, error={}", getThingLabel(),
getThingHaId(), e.getMessage());
removeSelectedProgramStateDescription();
}
}
Expand Down Expand Up @@ -485,6 +489,9 @@ protected Optional<Channel> getThingChannel(String channelId) {
*/
protected abstract void configureEventHandlers(final Map<String, EventHandler> handlers);

protected void configureUnsupportedProgramOptions(final Map<String, List<AvailableProgramOption>> programOptions) {
}

protected boolean isChannelLinkedToProgramOptionNotFullySupportedByApi() {
return false;
}
Expand Down Expand Up @@ -1473,12 +1480,31 @@ protected void updateProgramOptionsStateDescriptions(String programKey)
try {
availableProgramOptions = apiClient.get().getProgramOptions(getThingHaId(), programKey);
if (availableProgramOptions == null) {
// Program is unsupported, save in cache an empty list of options to avoid calling again the API
// for this program
availableProgramOptions = emptyList();
logger.debug("Saving empty options in cache for unsupported program '{}'.", programKey);
// Program is unsupported, to avoid calling again the API for this program, save in cache either
// the predefined options provided by the binding if they exist, or an empty list of options
if (unsupportedProgramOptions.containsKey(programKey)) {
availableProgramOptions = unsupportedProgramOptions.get(programKey);
availableProgramOptions = availableProgramOptions != null ? availableProgramOptions
: emptyList();
logger.debug("Saving predefined options in cache for unsupported program '{}'.",
programKey);
} else {
availableProgramOptions = emptyList();
logger.debug("Saving empty options in cache for unsupported program '{}'.", programKey);
}
availableProgramOptionsCache.put(programKey, availableProgramOptions);

// Add the unsupported program in programs cache and refresh the dynamic state description
if (addUnsupportedProgramInCache(programKey)) {
updateSelectedProgramStateDescription();
}
} else {
// If no options are returned by the API, using predefined options if available
if (availableProgramOptions.isEmpty() && unsupportedProgramOptions.containsKey(programKey)) {
availableProgramOptions = unsupportedProgramOptions.get(programKey);
availableProgramOptions = availableProgramOptions != null ? availableProgramOptions
: emptyList();
}
cacheToSet = true;
}
} catch (CommunicationException e) {
Expand Down Expand Up @@ -1599,4 +1625,49 @@ private synchronized void stopRetryRegistering() {
this.reinitializationFuture3 = null;
}
}

protected List<AvailableProgram> getPrograms()
throws CommunicationException, AuthorizationException, ApplianceOfflineException {
if (!programsCache.isEmpty()) {
logger.debug("Returning cached programs for '{}'.", getThingHaId());
return programsCache;
} else {
Optional<HomeConnectApiClient> apiClient = getApiClient();
if (apiClient.isPresent()) {
programsCache.addAll(apiClient.get().getPrograms(getThingHaId()));
return programsCache;
} else {
throw new CommunicationException("API not initialized");
}
}
}

/**
* Add an entry in the programs cache and mark it as unsupported
*
* @param programKey program id
* @return true if an entry was added in the cache
*/
private boolean addUnsupportedProgramInCache(String programKey) {
Optional<AvailableProgram> prog = programsCache.stream().filter(program -> programKey.equals(program.getKey()))
bruestel marked this conversation as resolved.
Show resolved Hide resolved
.findFirst();
if (!prog.isPresent()) {
programsCache.add(new AvailableProgram(programKey, false));
logger.debug("{} added in programs cache as an unsupported program", programKey);
return true;
}
return false;
}

/**
* Check if a program is marked as supported in the programs cache
*
* @param programKey program id
* @return true if the program is in the cache and marked as supported
*/
protected boolean isProgramSupported(String programKey) {
Optional<AvailableProgram> prog = programsCache.stream().filter(program -> programKey.equals(program.getKey()))
.findFirst();
return prog.isPresent() && prog.get().isSupported();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ protected void updateSelectedProgramStateDescription() {
if (apiClient.isPresent()) {
try {
ArrayList<StateOption> stateOptions = new ArrayList<>();
apiClient.get().getPrograms(getThingHaId()).forEach(availableProgram -> {
getPrograms().forEach(availableProgram -> {
if (PROGRAM_HOOD_AUTOMATIC.equals(availableProgram.getKey())) {
stateOptions.add(new StateOption(COMMAND_AUTOMATIC, mapStringType(availableProgram.getKey())));
} else if (PROGRAM_HOOD_DELAYED_SHUT_OFF.equals(availableProgram.getKey())) {
Expand Down
Loading