]> git.basschouten.com Git - openhab-addons.git/blob
a7efb5dd66668f957fc33902d26f422b9f0dd846
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.homeconnect.internal.handler;
14
15 import static java.lang.String.format;
16 import static java.util.Collections.emptyList;
17 import static org.openhab.binding.homeconnect.internal.HomeConnectBindingConstants.*;
18
19 import java.util.ArrayList;
20 import java.util.List;
21 import java.util.Map;
22 import java.util.Optional;
23
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;
42
43 /**
44  * The {@link HomeConnectHoodHandler} is responsible for handling commands, which are
45  * sent to one of the channels of a hood.
46  *
47  * @author Jonas BrĂ¼stel - Initial contribution
48  */
49 @NonNullByDefault
50 public class HomeConnectHoodHandler extends AbstractHomeConnectThingHandler {
51
52     private static final String START_VENTING_INTENSIVE_STAGE_PAYLOAD_TEMPLATE = """
53
54             {
55                 "data": {
56                     "key": "Cooking.Common.Program.Hood.Venting",
57                     "options": [
58                         {
59                             "key": "Cooking.Common.Option.Hood.IntensiveLevel",
60                             "value": "%s"
61                         }
62                     ]
63                 }
64             }\
65             """;
66
67     private static final String START_VENTING_STAGE_PAYLOAD_TEMPLATE = """
68
69             {
70                 "data": {
71                     "key": "Cooking.Common.Program.Hood.Venting",
72                     "options": [
73                         {
74                             "key": "Cooking.Common.Option.Hood.VentingLevel",
75                             "value": "%s"
76                         }
77                     ]
78                 }
79             }\
80             """;
81
82     private final Logger logger = LoggerFactory.getLogger(HomeConnectHoodHandler.class);
83
84     public HomeConnectHoodHandler(Thing thing,
85             HomeConnectDynamicStateDescriptionProvider dynamicStateDescriptionProvider) {
86         super(thing, dynamicStateDescriptionProvider);
87     }
88
89     @Override
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();
106                             if (enabled) {
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())));
111                             }
112                             return OnOffType.from(enabled);
113                         } else {
114                             return UnDefType.UNDEF;
115                         }
116                     } else {
117                         return UnDefType.UNDEF;
118                     }
119                 })));
120     }
121
122     @Override
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());
140
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)));
147                     } else {
148                         updateState(channel.getUID(), UnDefType.UNDEF);
149                     }
150                 }));
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)));
156                     } else {
157                         updateState(channel.getUID(), UnDefType.UNDEF);
158                     }
159                 }));
160     }
161
162     @Override
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);
167
168         handlePowerCommand(channelUID, command, apiClient, STATE_POWER_OFF);
169
170         // light commands
171         handleLightCommands(channelUID, command, apiClient);
172
173         // program options
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());
179                 }
180             } else {
181                 logger.debug("Device can not handle command {} in current operation state ({}). thing={}, haId={}",
182                         command, operationState, getThingLabel(), getThingHaId());
183             }
184
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));
211             } else {
212                 logger.info("Start custom program. command={} haId={}", command.toFullString(), getThingHaId());
213                 apiClient.startCustomProgram(getThingHaId(), command.toFullString());
214             }
215         }
216     }
217
218     @Override
219     protected void updateSelectedProgramStateDescription() {
220         // update hood program actions
221         if (isBridgeOffline() || !isThingAccessibleViaServerSentEvents()) {
222             return;
223         }
224
225         Optional<HomeConnectApiClient> apiClient = getApiClient();
226         if (apiClient.isPresent()) {
227             try {
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())) {
233                         stateOptions.add(
234                                 new StateOption(COMMAND_DELAYED_SHUT_OFF, mapStringType(availableProgram.getKey())));
235                     } else if (PROGRAM_HOOD_VENTING.equals(availableProgram.getKey())) {
236                         try {
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");
241                             }
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)));
250                                 }
251                             });
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));
261                         }
262                     }
263                 });
264                 stateOptions.add(new StateOption(COMMAND_STOP, "Stop"));
265
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();
272             }
273         } else {
274             removeSelectedProgramStateDescription();
275         }
276     }
277
278     @Override
279     protected void removeSelectedProgramStateDescription() {
280         getThingChannel(CHANNEL_HOOD_ACTIONS_STATE).ifPresent(
281                 channel -> getDynamicStateDescriptionProvider().setStateOptions(channel.getUID(), emptyList()));
282     }
283
284     @Override
285     public String toString() {
286         return "HomeConnectHoodHandler [haId: " + getThingHaId() + "]";
287     }
288
289     @Override
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));
295     }
296
297     private StateOption createVentingStateOption(String optionKey) {
298         String label = mapStringType(PROGRAM_HOOD_VENTING);
299
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)));
318         } else {
319             return new StateOption(COMMAND_VENTING_INTENSIVE_2,
320                     format("%s (Intensive level %s)", label, mapStageStringType(STAGE_INTENSIVE_STAGE_2)));
321         }
322     }
323 }