Skip to content

Commit

Permalink
[homeconnect] Predefined temp / spin speeds options for unsupported w…
Browse files Browse the repository at this point in the history
…asher programs (#10953)

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

Fix #10701

Also save in programs cache the unuspported program

Signed-off-by: Laurent Garnier <[email protected]>

* Use constants OPTION_WASHER_TEMPERATURE and OPTION_WASHER_SPIN_SPEED

Signed-off-by: Laurent Garnier <[email protected]>

* Review comment : using constants

Signed-off-by: Laurent Garnier <[email protected]>
  • Loading branch information
lolodomo authored Jul 25, 2021
1 parent 0f3289a commit 3d0c31b
Show file tree
Hide file tree
Showing 6 changed files with 179 additions and 43 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,26 @@ public class HomeConnectBindingConstants {
public static final String OPTION_HOOD_VENTING_LEVEL = "Cooking.Common.Option.Hood.VentingLevel";
public static final String OPTION_HOOD_INTENSIVE_LEVEL = "Cooking.Common.Option.Hood.IntensiveLevel";

// List of washer temperatures
public static final String TEMPERATURE_PREFIX = "LaundryCare.Washer.EnumType.Temperature.";
public static final String TEMPERATURE_AUTO = TEMPERATURE_PREFIX + "Auto";
public static final String TEMPERATURE_COLD = TEMPERATURE_PREFIX + "Cold";
public static final String TEMPERATURE_20 = TEMPERATURE_PREFIX + "GC20";
public static final String TEMPERATURE_30 = TEMPERATURE_PREFIX + "GC30";
public static final String TEMPERATURE_40 = TEMPERATURE_PREFIX + "GC40";
public static final String TEMPERATURE_60 = TEMPERATURE_PREFIX + "GC60";
public static final String TEMPERATURE_90 = TEMPERATURE_PREFIX + "GC90";

// List of spin speeds
public static final String SPIN_SPEED_PREFIX = "LaundryCare.Washer.EnumType.SpinSpeed.";
public static final String SPIN_SPEED_AUTO = SPIN_SPEED_PREFIX + "Auto";
public static final String SPIN_SPEED_OFF = SPIN_SPEED_PREFIX + "Off";
public static final String SPIN_SPEED_400 = SPIN_SPEED_PREFIX + "RPM400";
public static final String SPIN_SPEED_600 = SPIN_SPEED_PREFIX + "RPM600";
public static final String SPIN_SPEED_800 = SPIN_SPEED_PREFIX + "RPM800";
public static final String SPIN_SPEED_1200 = SPIN_SPEED_PREFIX + "RPM1200";
public static final String SPIN_SPEED_1400 = SPIN_SPEED_PREFIX + "RPM1400";

// List of stages
public static final String STAGE_FAN_OFF = "Cooking.Hood.EnumType.Stage.FanOff";
public static final String STAGE_FAN_STAGE_01 = "Cooking.Hood.EnumType.Stage.FanStage01";
Expand Down
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 @@ -1433,24 +1440,24 @@ protected void processProgramOptions(List<Option> options) {
}

protected String convertWasherTemperature(String value) {
if (value.startsWith("LaundryCare.Washer.EnumType.Temperature.GC")) {
return value.replace("LaundryCare.Washer.EnumType.Temperature.GC", "") + "°C";
if (value.startsWith(TEMPERATURE_PREFIX + "GC")) {
return value.replace(TEMPERATURE_PREFIX + "GC", "") + "°C";
}

if (value.startsWith("LaundryCare.Washer.EnumType.Temperature.Ul")) {
return mapStringType(value.replace("LaundryCare.Washer.EnumType.Temperature.Ul", ""));
if (value.startsWith(TEMPERATURE_PREFIX + "Ul")) {
return mapStringType(value.replace(TEMPERATURE_PREFIX + "Ul", ""));
}

return mapStringType(value);
}

protected String convertWasherSpinSpeed(String value) {
if (value.startsWith("LaundryCare.Washer.EnumType.SpinSpeed.RPM")) {
return value.replace("LaundryCare.Washer.EnumType.SpinSpeed.RPM", "") + " RPM";
if (value.startsWith(SPIN_SPEED_PREFIX + "RPM")) {
return value.replace(SPIN_SPEED_PREFIX + "RPM", "") + " RPM";
}

if (value.startsWith("LaundryCare.Washer.EnumType.SpinSpeed.Ul")) {
return value.replace("LaundryCare.Washer.EnumType.SpinSpeed.Ul", "");
if (value.startsWith(SPIN_SPEED_PREFIX + "Ul")) {
return value.replace(SPIN_SPEED_PREFIX + "Ul", "");
}

return mapStringType(value);
Expand All @@ -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()))
.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

0 comments on commit 3d0c31b

Please sign in to comment.