[homeconnect] Predefined temp / spin speeds options for unsupported washer 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 <lg.hc@free.fr>

* Use constants OPTION_WASHER_TEMPERATURE and OPTION_WASHER_SPIN_SPEED

Signed-off-by: Laurent Garnier <lg.hc@free.fr>

* Review comment : using constants

Signed-off-by: Laurent Garnier <lg.hc@free.fr>
This commit is contained in:
lolodomo 2021-07-25 11:49:19 +02:00 committed by GitHub
parent 0f3289a937
commit 3d0c31bd50
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 179 additions and 43 deletions

View File

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

View File

@ -19,11 +19,9 @@ import static org.openhab.binding.homeconnect.internal.client.HttpHelper.*;
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;
@ -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;
@ -90,7 +87,6 @@ public class HomeConnectApiClient {
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) {
@ -610,16 +606,7 @@ public class HomeConnectApiClient {
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)

View File

@ -18,24 +18,39 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
* 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;
}
@ -46,6 +61,7 @@ public class AvailableProgram {
@Override
public String toString() {
return "AvailableProgram [key=" + key + ", available=" + available + ", execution=" + execution + "]";
return "AvailableProgram [key=" + key + ", supported=" + supported + ", available=" + available + ", execution="
+ execution + "]";
}
}

View File

@ -26,6 +26,7 @@ import java.util.List;
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;
@ -44,6 +45,7 @@ import org.openhab.binding.homeconnect.internal.client.exception.ApplianceOfflin
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;
@ -83,6 +85,7 @@ import org.slf4j.LoggerFactory;
* 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 {
@ -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) {
@ -115,10 +120,13 @@ public abstract class AbstractHomeConnectThingHandler extends BaseThingHandler i
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
@ -207,7 +215,8 @@ public abstract class AbstractHomeConnectThingHandler extends BaseThingHandler i
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());
}
}
@ -347,20 +356,15 @@ public abstract class AbstractHomeConnectThingHandler extends BaseThingHandler i
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();
}
}
@ -485,6 +489,9 @@ public abstract class AbstractHomeConnectThingHandler extends BaseThingHandler i
*/
protected abstract void configureEventHandlers(final Map<String, EventHandler> handlers);
protected void configureUnsupportedProgramOptions(final Map<String, List<AvailableProgramOption>> programOptions) {
}
protected boolean isChannelLinkedToProgramOptionNotFullySupportedByApi() {
return false;
}
@ -1433,24 +1440,24 @@ public abstract class AbstractHomeConnectThingHandler extends BaseThingHandler i
}
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);
@ -1473,12 +1480,31 @@ public abstract class AbstractHomeConnectThingHandler extends BaseThingHandler i
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) {
@ -1599,4 +1625,49 @@ public abstract class AbstractHomeConnectThingHandler extends BaseThingHandler i
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();
}
}

View File

@ -206,7 +206,7 @@ public class HomeConnectHoodHandler extends AbstractHomeConnectThingHandler {
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())) {

View File

@ -23,6 +23,7 @@ import org.openhab.binding.homeconnect.internal.client.HomeConnectApiClient;
import org.openhab.binding.homeconnect.internal.client.exception.ApplianceOfflineException;
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.model.AvailableProgramOption;
import org.openhab.binding.homeconnect.internal.type.HomeConnectDynamicStateDescriptionProvider;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.ChannelUID;
@ -121,6 +122,47 @@ public class HomeConnectWasherHandler extends AbstractHomeConnectThingHandler {
event.getValue() == null ? UnDefType.UNDEF : new StringType(event.getValue()))));
}
@Override
protected void configureUnsupportedProgramOptions(Map<String, List<AvailableProgramOption>> programOptions) {
programOptions.put("LaundryCare.Washer.Program.Cotton.Eco4060", List.of(
new AvailableProgramOption(OPTION_WASHER_TEMPERATURE, List.of(TEMPERATURE_AUTO)),
new AvailableProgramOption(OPTION_WASHER_SPIN_SPEED,
List.of(SPIN_SPEED_400, SPIN_SPEED_600, SPIN_SPEED_800, SPIN_SPEED_1200, SPIN_SPEED_1400))));
programOptions.put("LaundryCare.Washer.Program.Cotton.Colour", List.of(
new AvailableProgramOption(OPTION_WASHER_TEMPERATURE,
List.of(TEMPERATURE_COLD, TEMPERATURE_20, TEMPERATURE_30, TEMPERATURE_40, TEMPERATURE_60,
TEMPERATURE_90)),
new AvailableProgramOption(OPTION_WASHER_SPIN_SPEED,
List.of(SPIN_SPEED_400, SPIN_SPEED_600, SPIN_SPEED_800, SPIN_SPEED_1200, SPIN_SPEED_1400))));
// Auto30 is a supported program provided by the API but the API returns empty options, so we defined predefined
// values for this program
programOptions.put("LaundryCare.Washer.Program.Auto30",
List.of(new AvailableProgramOption(OPTION_WASHER_TEMPERATURE, List.of(TEMPERATURE_AUTO)),
new AvailableProgramOption(OPTION_WASHER_SPIN_SPEED, List.of(SPIN_SPEED_AUTO))));
programOptions.put("LaundryCare.Washer.Program.Super153045.Super1530",
List.of(new AvailableProgramOption(OPTION_WASHER_TEMPERATURE,
List.of(TEMPERATURE_COLD, TEMPERATURE_20, TEMPERATURE_30, TEMPERATURE_40)),
new AvailableProgramOption(OPTION_WASHER_SPIN_SPEED,
List.of(SPIN_SPEED_400, SPIN_SPEED_600, SPIN_SPEED_800, SPIN_SPEED_1200))));
programOptions.put("LaundryCare.Washer.Program.Rinse",
List.of(new AvailableProgramOption(OPTION_WASHER_TEMPERATURE, List.of()),
new AvailableProgramOption(OPTION_WASHER_SPIN_SPEED, List.of(SPIN_SPEED_OFF, SPIN_SPEED_400,
SPIN_SPEED_600, SPIN_SPEED_800, SPIN_SPEED_1200, SPIN_SPEED_1400))));
programOptions.put("LaundryCare.Washer.Program.Spin.SpinDrain",
List.of(new AvailableProgramOption(OPTION_WASHER_TEMPERATURE, List.of()),
new AvailableProgramOption(OPTION_WASHER_SPIN_SPEED, List.of(SPIN_SPEED_OFF, SPIN_SPEED_400,
SPIN_SPEED_600, SPIN_SPEED_800, SPIN_SPEED_1200, SPIN_SPEED_1400))));
programOptions.put("LaundryCare.Washer.Program.DrumClean",
List.of(new AvailableProgramOption(OPTION_WASHER_TEMPERATURE, List.of()),
new AvailableProgramOption(OPTION_WASHER_SPIN_SPEED, List.of(SPIN_SPEED_1200))));
}
@Override
protected boolean isChannelLinkedToProgramOptionNotFullySupportedByApi() {
return (getThingChannel(CHANNEL_WASHER_IDOS1).isPresent() && isLinked(CHANNEL_WASHER_IDOS1))