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;
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;
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) {
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)
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;
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;
* 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 {
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) {
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
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());
}
}
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();
}
}
*/
protected abstract void configureEventHandlers(final Map<String, EventHandler> handlers);
+ protected void configureUnsupportedProgramOptions(final Map<String, List<AvailableProgramOption>> programOptions) {
+ }
+
protected boolean isChannelLinkedToProgramOptionNotFullySupportedByApi() {
return false;
}
}
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);
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) {
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();
+ }
}
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;
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))