diff --git a/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/HomeConnectBindingConstants.java b/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/HomeConnectBindingConstants.java index 4d2dd432c08..f07647d256c 100644 --- a/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/HomeConnectBindingConstants.java +++ b/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/HomeConnectBindingConstants.java @@ -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"; diff --git a/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/client/HomeConnectApiClient.java b/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/client/HomeConnectApiClient.java index 0b1e4473d0c..5c48d1f1c7d 100644 --- a/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/client/HomeConnectApiClient.java +++ b/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/client/HomeConnectApiClient.java @@ -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> programsCache; private final OAuthClientService oAuthClientService; private final CircularQueue 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 getPrograms(String haId) throws CommunicationException, AuthorizationException, ApplianceOfflineException { - List 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 getAvailablePrograms(String haId) diff --git a/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/client/model/AvailableProgram.java b/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/client/model/AvailableProgram.java index 54acbcc7381..80ee852c6ab 100644 --- a/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/client/model/AvailableProgram.java +++ b/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/client/model/AvailableProgram.java @@ -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 + "]"; } } diff --git a/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/handler/AbstractHomeConnectThingHandler.java b/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/handler/AbstractHomeConnectThingHandler.java index 906d9d721e7..836715ba5b2 100644 --- a/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/handler/AbstractHomeConnectThingHandler.java +++ b/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/handler/AbstractHomeConnectThingHandler.java @@ -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 programsCache; private final Map> availableProgramOptionsCache; + private final Map> 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 apiClient = getApiClient(); - if (apiClient.isPresent()) { - try { - List stateOptions = apiClient.get().getPrograms(getThingHaId()).stream() - .map(p -> new StateOption(p.getKey(), mapStringType(p.getKey()))).collect(Collectors.toList()); + try { + List 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 handlers); + protected void configureUnsupportedProgramOptions(final Map> 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 getPrograms() + throws CommunicationException, AuthorizationException, ApplianceOfflineException { + if (!programsCache.isEmpty()) { + logger.debug("Returning cached programs for '{}'.", getThingHaId()); + return programsCache; + } else { + Optional 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 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 prog = programsCache.stream().filter(program -> programKey.equals(program.getKey())) + .findFirst(); + return prog.isPresent() && prog.get().isSupported(); + } } diff --git a/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/handler/HomeConnectHoodHandler.java b/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/handler/HomeConnectHoodHandler.java index f9fc735eace..497e4a7ffdb 100644 --- a/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/handler/HomeConnectHoodHandler.java +++ b/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/handler/HomeConnectHoodHandler.java @@ -206,7 +206,7 @@ public class HomeConnectHoodHandler extends AbstractHomeConnectThingHandler { if (apiClient.isPresent()) { try { ArrayList 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())) { diff --git a/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/handler/HomeConnectWasherHandler.java b/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/handler/HomeConnectWasherHandler.java index 4d62ede7d87..59c47fbb0ad 100644 --- a/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/handler/HomeConnectWasherHandler.java +++ b/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/handler/HomeConnectWasherHandler.java @@ -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> 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))