2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.homeconnect.internal.handler;
15 import static java.lang.String.format;
16 import static java.util.Collections.emptyList;
17 import static org.openhab.binding.homeconnect.internal.HomeConnectBindingConstants.*;
19 import java.util.ArrayList;
20 import java.util.List;
22 import java.util.Optional;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.openhab.binding.homeconnect.internal.client.HomeConnectApiClient;
26 import org.openhab.binding.homeconnect.internal.client.exception.ApplianceOfflineException;
27 import org.openhab.binding.homeconnect.internal.client.exception.AuthorizationException;
28 import org.openhab.binding.homeconnect.internal.client.exception.CommunicationException;
29 import org.openhab.binding.homeconnect.internal.client.model.AvailableProgramOption;
30 import org.openhab.binding.homeconnect.internal.client.model.Data;
31 import org.openhab.binding.homeconnect.internal.type.HomeConnectDynamicStateDescriptionProvider;
32 import org.openhab.core.library.types.OnOffType;
33 import org.openhab.core.library.types.PercentType;
34 import org.openhab.core.library.types.StringType;
35 import org.openhab.core.thing.ChannelUID;
36 import org.openhab.core.thing.Thing;
37 import org.openhab.core.types.Command;
38 import org.openhab.core.types.StateOption;
39 import org.openhab.core.types.UnDefType;
40 import org.slf4j.Logger;
41 import org.slf4j.LoggerFactory;
44 * The {@link HomeConnectHoodHandler} is responsible for handling commands, which are
45 * sent to one of the channels of a hood.
47 * @author Jonas BrĂ¼stel - Initial contribution
50 public class HomeConnectHoodHandler extends AbstractHomeConnectThingHandler {
52 private static final String START_VENTING_INTENSIVE_STAGE_PAYLOAD_TEMPLATE = """
56 "key": "Cooking.Common.Program.Hood.Venting",
59 "key": "Cooking.Common.Option.Hood.IntensiveLevel",
67 private static final String START_VENTING_STAGE_PAYLOAD_TEMPLATE = """
71 "key": "Cooking.Common.Program.Hood.Venting",
74 "key": "Cooking.Common.Option.Hood.VentingLevel",
82 private final Logger logger = LoggerFactory.getLogger(HomeConnectHoodHandler.class);
84 public HomeConnectHoodHandler(Thing thing,
85 HomeConnectDynamicStateDescriptionProvider dynamicStateDescriptionProvider) {
86 super(thing, dynamicStateDescriptionProvider);
90 protected void configureChannelUpdateHandlers(Map<String, ChannelUpdateHandler> handlers) {
91 // register default update handlers
92 handlers.put(CHANNEL_OPERATION_STATE, defaultOperationStateChannelUpdateHandler());
93 handlers.put(CHANNEL_POWER_STATE, defaultPowerStateChannelUpdateHandler());
94 handlers.put(CHANNEL_REMOTE_START_ALLOWANCE_STATE, defaultRemoteStartAllowanceChannelUpdateHandler());
95 handlers.put(CHANNEL_REMOTE_CONTROL_ACTIVE_STATE, defaultRemoteControlActiveStateChannelUpdateHandler());
96 handlers.put(CHANNEL_LOCAL_CONTROL_ACTIVE_STATE, defaultLocalControlActiveStateChannelUpdateHandler());
97 handlers.put(CHANNEL_ACTIVE_PROGRAM_STATE, defaultActiveProgramStateUpdateHandler());
98 handlers.put(CHANNEL_AMBIENT_LIGHT_STATE, defaultAmbientLightChannelUpdateHandler());
99 handlers.put(CHANNEL_FUNCTIONAL_LIGHT_STATE,
100 (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
101 Optional<HomeConnectApiClient> apiClient = getApiClient();
102 if (apiClient.isPresent()) {
103 Data data = apiClient.get().getFunctionalLightState(getThingHaId());
104 if (data.getValue() != null) {
105 boolean enabled = data.getValueAsBoolean();
107 Data brightnessData = apiClient.get().getFunctionalLightBrightnessState(getThingHaId());
108 getLinkedChannel(CHANNEL_FUNCTIONAL_LIGHT_BRIGHTNESS_STATE)
109 .ifPresent(channel -> updateState(channel.getUID(),
110 new PercentType(brightnessData.getValueAsInt())));
112 return OnOffType.from(enabled);
114 return UnDefType.UNDEF;
117 return UnDefType.UNDEF;
123 protected void configureEventHandlers(Map<String, EventHandler> handlers) {
124 // register default SSE event handlers
125 handlers.put(EVENT_REMOTE_CONTROL_START_ALLOWED,
126 defaultBooleanEventHandler(CHANNEL_REMOTE_START_ALLOWANCE_STATE));
127 handlers.put(EVENT_REMOTE_CONTROL_ACTIVE, defaultBooleanEventHandler(CHANNEL_REMOTE_CONTROL_ACTIVE_STATE));
128 handlers.put(EVENT_LOCAL_CONTROL_ACTIVE, defaultBooleanEventHandler(CHANNEL_LOCAL_CONTROL_ACTIVE_STATE));
129 handlers.put(EVENT_OPERATION_STATE, defaultOperationStateEventHandler());
130 handlers.put(EVENT_ACTIVE_PROGRAM, defaultActiveProgramEventHandler());
131 handlers.put(EVENT_POWER_STATE, defaultPowerStateEventHandler());
132 handlers.put(EVENT_FUNCTIONAL_LIGHT_STATE, defaultBooleanEventHandler(CHANNEL_FUNCTIONAL_LIGHT_STATE));
133 handlers.put(EVENT_FUNCTIONAL_LIGHT_BRIGHTNESS_STATE,
134 defaultPercentHandler(CHANNEL_FUNCTIONAL_LIGHT_BRIGHTNESS_STATE));
135 handlers.put(EVENT_AMBIENT_LIGHT_STATE, defaultBooleanEventHandler(CHANNEL_AMBIENT_LIGHT_STATE));
136 handlers.put(EVENT_AMBIENT_LIGHT_BRIGHTNESS_STATE,
137 defaultPercentHandler(CHANNEL_AMBIENT_LIGHT_BRIGHTNESS_STATE));
138 handlers.put(EVENT_AMBIENT_LIGHT_COLOR_STATE, defaultAmbientLightColorStateEventHandler());
139 handlers.put(EVENT_AMBIENT_LIGHT_CUSTOM_COLOR_STATE, defaultAmbientLightCustomColorStateEventHandler());
141 // register hood specific SSE event handlers
142 handlers.put(EVENT_HOOD_INTENSIVE_LEVEL,
143 event -> getLinkedChannel(CHANNEL_HOOD_INTENSIVE_LEVEL).ifPresent(channel -> {
144 String hoodIntensiveLevel = event.getValue();
145 if (hoodIntensiveLevel != null) {
146 updateState(channel.getUID(), new StringType(mapStageStringType(hoodIntensiveLevel)));
148 updateState(channel.getUID(), UnDefType.UNDEF);
151 handlers.put(EVENT_HOOD_VENTING_LEVEL,
152 event -> getLinkedChannel(CHANNEL_HOOD_VENTING_LEVEL).ifPresent(channel -> {
153 String hoodVentingLevel = event.getValue();
154 if (hoodVentingLevel != null) {
155 updateState(channel.getUID(), new StringType(mapStageStringType(hoodVentingLevel)));
157 updateState(channel.getUID(), UnDefType.UNDEF);
163 protected void handleCommand(final ChannelUID channelUID, final Command command,
164 final HomeConnectApiClient apiClient)
165 throws CommunicationException, AuthorizationException, ApplianceOfflineException {
166 super.handleCommand(channelUID, command, apiClient);
168 handlePowerCommand(channelUID, command, apiClient, STATE_POWER_OFF);
171 handleLightCommands(channelUID, command, apiClient);
174 if (command instanceof StringType && CHANNEL_HOOD_ACTIONS_STATE.equals(channelUID.getId())) {
175 String operationState = getOperationState();
176 if (OPERATION_STATE_INACTIVE.equals(operationState) || OPERATION_STATE_RUN.equals(operationState)) {
177 if (COMMAND_STOP.equalsIgnoreCase(command.toFullString())) {
178 apiClient.stopProgram(getThingHaId());
181 logger.debug("Device can not handle command {} in current operation state ({}). thing={}, haId={}",
182 command, operationState, getThingLabel(), getThingHaId());
185 // These command always start the hood - even if appliance is turned off
186 if (COMMAND_AUTOMATIC.equalsIgnoreCase(command.toFullString())) {
187 apiClient.startProgram(getThingHaId(), PROGRAM_HOOD_AUTOMATIC);
188 } else if (COMMAND_DELAYED_SHUT_OFF.equalsIgnoreCase(command.toFullString())) {
189 apiClient.startProgram(getThingHaId(), PROGRAM_HOOD_DELAYED_SHUT_OFF);
190 } else if (COMMAND_VENTING_1.equalsIgnoreCase(command.toFullString())) {
191 apiClient.startCustomProgram(getThingHaId(),
192 format(START_VENTING_STAGE_PAYLOAD_TEMPLATE, STAGE_FAN_STAGE_01));
193 } else if (COMMAND_VENTING_2.equalsIgnoreCase(command.toFullString())) {
194 apiClient.startCustomProgram(getThingHaId(),
195 format(START_VENTING_STAGE_PAYLOAD_TEMPLATE, STAGE_FAN_STAGE_02));
196 } else if (COMMAND_VENTING_3.equalsIgnoreCase(command.toFullString())) {
197 apiClient.startCustomProgram(getThingHaId(),
198 format(START_VENTING_STAGE_PAYLOAD_TEMPLATE, STAGE_FAN_STAGE_03));
199 } else if (COMMAND_VENTING_4.equalsIgnoreCase(command.toFullString())) {
200 apiClient.startCustomProgram(getThingHaId(),
201 format(START_VENTING_STAGE_PAYLOAD_TEMPLATE, STAGE_FAN_STAGE_04));
202 } else if (COMMAND_VENTING_5.equalsIgnoreCase(command.toFullString())) {
203 apiClient.startCustomProgram(getThingHaId(),
204 format(START_VENTING_STAGE_PAYLOAD_TEMPLATE, STAGE_FAN_STAGE_05));
205 } else if (COMMAND_VENTING_INTENSIVE_1.equalsIgnoreCase(command.toFullString())) {
206 apiClient.startCustomProgram(getThingHaId(),
207 format(START_VENTING_INTENSIVE_STAGE_PAYLOAD_TEMPLATE, STAGE_INTENSIVE_STAGE_1));
208 } else if (COMMAND_VENTING_INTENSIVE_2.equalsIgnoreCase(command.toFullString())) {
209 apiClient.startCustomProgram(getThingHaId(),
210 format(START_VENTING_INTENSIVE_STAGE_PAYLOAD_TEMPLATE, STAGE_INTENSIVE_STAGE_2));
212 logger.info("Start custom program. command={} haId={}", command.toFullString(), getThingHaId());
213 apiClient.startCustomProgram(getThingHaId(), command.toFullString());
219 protected void updateSelectedProgramStateDescription() {
220 // update hood program actions
221 if (isBridgeOffline() || !isThingAccessibleViaServerSentEvents()) {
225 Optional<HomeConnectApiClient> apiClient = getApiClient();
226 if (apiClient.isPresent()) {
228 ArrayList<StateOption> stateOptions = new ArrayList<>();
229 getPrograms().forEach(availableProgram -> {
230 if (PROGRAM_HOOD_AUTOMATIC.equals(availableProgram.getKey())) {
231 stateOptions.add(new StateOption(COMMAND_AUTOMATIC, mapStringType(availableProgram.getKey())));
232 } else if (PROGRAM_HOOD_DELAYED_SHUT_OFF.equals(availableProgram.getKey())) {
234 new StateOption(COMMAND_DELAYED_SHUT_OFF, mapStringType(availableProgram.getKey())));
235 } else if (PROGRAM_HOOD_VENTING.equals(availableProgram.getKey())) {
237 List<AvailableProgramOption> availableProgramOptions = apiClient.get()
238 .getProgramOptions(getThingHaId(), PROGRAM_HOOD_VENTING);
239 if (availableProgramOptions == null || availableProgramOptions.isEmpty()) {
240 throw new CommunicationException("Program " + PROGRAM_HOOD_VENTING + " is unsupported");
242 availableProgramOptions.forEach(option -> {
243 if (OPTION_HOOD_VENTING_LEVEL.equalsIgnoreCase(option.getKey())) {
244 option.getAllowedValues().stream().filter(s -> !STAGE_FAN_OFF.equalsIgnoreCase(s))
245 .forEach(s -> stateOptions.add(createVentingStateOption(s)));
246 } else if (OPTION_HOOD_INTENSIVE_LEVEL.equalsIgnoreCase(option.getKey())) {
247 option.getAllowedValues().stream()
248 .filter(s -> !STAGE_INTENSIVE_STAGE_OFF.equalsIgnoreCase(s))
249 .forEach(s -> stateOptions.add(createVentingStateOption(s)));
252 } catch (CommunicationException | ApplianceOfflineException | AuthorizationException e) {
253 logger.warn("Could not fetch hood program options. error={}", e.getMessage());
254 stateOptions.add(createVentingStateOption(STAGE_FAN_STAGE_01));
255 stateOptions.add(createVentingStateOption(STAGE_FAN_STAGE_02));
256 stateOptions.add(createVentingStateOption(STAGE_FAN_STAGE_03));
257 stateOptions.add(createVentingStateOption(STAGE_FAN_STAGE_04));
258 stateOptions.add(createVentingStateOption(STAGE_FAN_STAGE_05));
259 stateOptions.add(createVentingStateOption(STAGE_INTENSIVE_STAGE_1));
260 stateOptions.add(createVentingStateOption(STAGE_INTENSIVE_STAGE_2));
264 stateOptions.add(new StateOption(COMMAND_STOP, "Stop"));
266 getThingChannel(CHANNEL_HOOD_ACTIONS_STATE).ifPresent(channel -> getDynamicStateDescriptionProvider()
267 .setStateOptions(channel.getUID(), stateOptions));
268 } catch (CommunicationException | ApplianceOfflineException | AuthorizationException e) {
269 logger.debug("Could not fetch available programs. thing={}, haId={}, error={}", getThingLabel(),
270 getThingHaId(), e.getMessage());
271 removeSelectedProgramStateDescription();
274 removeSelectedProgramStateDescription();
279 protected void removeSelectedProgramStateDescription() {
280 getThingChannel(CHANNEL_HOOD_ACTIONS_STATE).ifPresent(
281 channel -> getDynamicStateDescriptionProvider().setStateOptions(channel.getUID(), emptyList()));
285 public String toString() {
286 return "HomeConnectHoodHandler [haId: " + getThingHaId() + "]";
290 protected void resetProgramStateChannels(boolean offline) {
291 super.resetProgramStateChannels(offline);
292 getLinkedChannel(CHANNEL_ACTIVE_PROGRAM_STATE).ifPresent(c -> updateState(c.getUID(), UnDefType.UNDEF));
293 getLinkedChannel(CHANNEL_HOOD_INTENSIVE_LEVEL).ifPresent(c -> updateState(c.getUID(), UnDefType.UNDEF));
294 getLinkedChannel(CHANNEL_HOOD_VENTING_LEVEL).ifPresent(c -> updateState(c.getUID(), UnDefType.UNDEF));
297 private StateOption createVentingStateOption(String optionKey) {
298 String label = mapStringType(PROGRAM_HOOD_VENTING);
300 if (STAGE_FAN_STAGE_01.equalsIgnoreCase(optionKey)) {
301 return new StateOption(COMMAND_VENTING_1,
302 format("%s (Level %s)", label, mapStageStringType(STAGE_FAN_STAGE_01)));
303 } else if (STAGE_FAN_STAGE_02.equalsIgnoreCase(optionKey)) {
304 return new StateOption(COMMAND_VENTING_2,
305 format("%s (Level %s)", label, mapStageStringType(STAGE_FAN_STAGE_02)));
306 } else if (STAGE_FAN_STAGE_03.equalsIgnoreCase(optionKey)) {
307 return new StateOption(COMMAND_VENTING_3,
308 format("%s (Level %s)", label, mapStageStringType(STAGE_FAN_STAGE_03)));
309 } else if (STAGE_FAN_STAGE_04.equalsIgnoreCase(optionKey)) {
310 return new StateOption(COMMAND_VENTING_4,
311 format("%s (Level %s)", label, mapStageStringType(STAGE_FAN_STAGE_04)));
312 } else if (STAGE_FAN_STAGE_05.equalsIgnoreCase(optionKey)) {
313 return new StateOption(COMMAND_VENTING_5,
314 format("%s (Level %s)", label, mapStageStringType(STAGE_FAN_STAGE_05)));
315 } else if (STAGE_INTENSIVE_STAGE_1.equalsIgnoreCase(optionKey)) {
316 return new StateOption(COMMAND_VENTING_INTENSIVE_1,
317 format("%s (Intensive level %s)", label, mapStageStringType(STAGE_INTENSIVE_STAGE_1)));
319 return new StateOption(COMMAND_VENTING_INTENSIVE_2,
320 format("%s (Intensive level %s)", label, mapStageStringType(STAGE_INTENSIVE_STAGE_2)));