]> git.basschouten.com Git - openhab-addons.git/blob
f0f0beae8dcbe8fda69d9f8b37e9b7e5c533f74d
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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 = "\n" + "{\n" + "    \"data\": {\n"
53             + "        \"key\": \"Cooking.Common.Program.Hood.Venting\",\n" + "        \"options\": [\n"
54             + "            {\n" + "                \"key\": \"Cooking.Common.Option.Hood.IntensiveLevel\",\n"
55             + "                \"value\": \"%s\"\n" + "            }\n" + "        ]\n" + "    }\n" + "}";
56
57     private static final String START_VENTING_STAGE_PAYLOAD_TEMPLATE = "\n" + "{\n" + "    \"data\": {\n"
58             + "        \"key\": \"Cooking.Common.Program.Hood.Venting\",\n" + "        \"options\": [\n"
59             + "            {\n" + "                \"key\": \"Cooking.Common.Option.Hood.VentingLevel\",\n"
60             + "                \"value\": \"%s\"\n" + "            }\n" + "        ]\n" + "    }\n" + "}";
61
62     private final Logger logger = LoggerFactory.getLogger(HomeConnectHoodHandler.class);
63
64     public HomeConnectHoodHandler(Thing thing,
65             HomeConnectDynamicStateDescriptionProvider dynamicStateDescriptionProvider) {
66         super(thing, dynamicStateDescriptionProvider);
67     }
68
69     @Override
70     protected void configureChannelUpdateHandlers(Map<String, ChannelUpdateHandler> handlers) {
71         // register default update handlers
72         handlers.put(CHANNEL_OPERATION_STATE, defaultOperationStateChannelUpdateHandler());
73         handlers.put(CHANNEL_POWER_STATE, defaultPowerStateChannelUpdateHandler());
74         handlers.put(CHANNEL_REMOTE_START_ALLOWANCE_STATE, defaultRemoteStartAllowanceChannelUpdateHandler());
75         handlers.put(CHANNEL_REMOTE_CONTROL_ACTIVE_STATE, defaultRemoteControlActiveStateChannelUpdateHandler());
76         handlers.put(CHANNEL_LOCAL_CONTROL_ACTIVE_STATE, defaultLocalControlActiveStateChannelUpdateHandler());
77         handlers.put(CHANNEL_ACTIVE_PROGRAM_STATE, defaultActiveProgramStateUpdateHandler());
78         handlers.put(CHANNEL_AMBIENT_LIGHT_STATE, defaultAmbientLightChannelUpdateHandler());
79         handlers.put(CHANNEL_FUNCTIONAL_LIGHT_STATE,
80                 (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> {
81                     Optional<HomeConnectApiClient> apiClient = getApiClient();
82                     if (apiClient.isPresent()) {
83                         Data data = apiClient.get().getFunctionalLightState(getThingHaId());
84                         if (data.getValue() != null) {
85                             boolean enabled = data.getValueAsBoolean();
86                             if (enabled) {
87                                 Data brightnessData = apiClient.get().getFunctionalLightBrightnessState(getThingHaId());
88                                 getThingChannel(CHANNEL_FUNCTIONAL_LIGHT_BRIGHTNESS_STATE)
89                                         .ifPresent(channel -> updateState(channel.getUID(),
90                                                 new PercentType(brightnessData.getValueAsInt())));
91                             }
92                             return OnOffType.from(enabled);
93                         } else {
94                             return UnDefType.UNDEF;
95                         }
96                     } else {
97                         return UnDefType.UNDEF;
98                     }
99                 })));
100     }
101
102     @Override
103     protected void configureEventHandlers(Map<String, EventHandler> handlers) {
104         // register default SSE event handlers
105         handlers.put(EVENT_REMOTE_CONTROL_START_ALLOWED,
106                 defaultBooleanEventHandler(CHANNEL_REMOTE_START_ALLOWANCE_STATE));
107         handlers.put(EVENT_REMOTE_CONTROL_ACTIVE, defaultBooleanEventHandler(CHANNEL_REMOTE_CONTROL_ACTIVE_STATE));
108         handlers.put(EVENT_LOCAL_CONTROL_ACTIVE, defaultBooleanEventHandler(CHANNEL_LOCAL_CONTROL_ACTIVE_STATE));
109         handlers.put(EVENT_OPERATION_STATE, defaultOperationStateEventHandler());
110         handlers.put(EVENT_ACTIVE_PROGRAM, defaultActiveProgramEventHandler());
111         handlers.put(EVENT_POWER_STATE, defaultPowerStateEventHandler());
112         handlers.put(EVENT_FUNCTIONAL_LIGHT_STATE, defaultBooleanEventHandler(CHANNEL_FUNCTIONAL_LIGHT_STATE));
113         handlers.put(EVENT_FUNCTIONAL_LIGHT_BRIGHTNESS_STATE,
114                 defaultPercentHandler(CHANNEL_FUNCTIONAL_LIGHT_BRIGHTNESS_STATE));
115         handlers.put(EVENT_AMBIENT_LIGHT_STATE, defaultBooleanEventHandler(CHANNEL_AMBIENT_LIGHT_STATE));
116         handlers.put(EVENT_AMBIENT_LIGHT_BRIGHTNESS_STATE,
117                 defaultPercentHandler(CHANNEL_AMBIENT_LIGHT_BRIGHTNESS_STATE));
118         handlers.put(EVENT_AMBIENT_LIGHT_COLOR_STATE, defaultAmbientLightColorStateEventHandler());
119         handlers.put(EVENT_AMBIENT_LIGHT_CUSTOM_COLOR_STATE, defaultAmbientLightCustomColorStateEventHandler());
120
121         // register hood specific SSE event handlers
122         handlers.put(EVENT_HOOD_INTENSIVE_LEVEL,
123                 event -> getThingChannel(CHANNEL_HOOD_INTENSIVE_LEVEL).ifPresent(channel -> {
124                     String hoodIntensiveLevel = event.getValue();
125                     if (hoodIntensiveLevel != null) {
126                         updateState(channel.getUID(), new StringType(mapStageStringType(hoodIntensiveLevel)));
127                     } else {
128                         updateState(channel.getUID(), UnDefType.UNDEF);
129                     }
130                 }));
131         handlers.put(EVENT_HOOD_VENTING_LEVEL,
132                 event -> getThingChannel(CHANNEL_HOOD_VENTING_LEVEL).ifPresent(channel -> {
133                     String hoodVentingLevel = event.getValue();
134                     if (hoodVentingLevel != null) {
135                         updateState(channel.getUID(), new StringType(mapStageStringType(hoodVentingLevel)));
136                     } else {
137                         updateState(channel.getUID(), UnDefType.UNDEF);
138                     }
139                 }));
140     }
141
142     @Override
143     protected void handleCommand(final ChannelUID channelUID, final Command command,
144             final HomeConnectApiClient apiClient)
145             throws CommunicationException, AuthorizationException, ApplianceOfflineException {
146         super.handleCommand(channelUID, command, apiClient);
147
148         if (command instanceof OnOffType) {
149             if (CHANNEL_POWER_STATE.equals(channelUID.getId())) {
150                 apiClient.setPowerState(getThingHaId(),
151                         OnOffType.ON.equals(command) ? STATE_POWER_ON : STATE_POWER_OFF);
152             }
153         }
154
155         // light commands
156         handleLightCommands(channelUID, command, apiClient);
157
158         // program options
159         if (command instanceof StringType && CHANNEL_HOOD_ACTIONS_STATE.equals(channelUID.getId())) {
160             String operationState = getOperationState();
161             if (OPERATION_STATE_INACTIVE.equals(operationState) || OPERATION_STATE_RUN.equals(operationState)) {
162                 if (COMMAND_STOP.equalsIgnoreCase(command.toFullString())) {
163                     apiClient.stopProgram(getThingHaId());
164                 }
165             } else {
166                 logger.debug("Device can not handle command {} in current operation state ({}). thing={}, haId={}",
167                         command, operationState, getThingLabel(), getThingHaId());
168             }
169
170             // These command always start the hood - even if appliance is turned off
171             if (COMMAND_AUTOMATIC.equalsIgnoreCase(command.toFullString())) {
172                 apiClient.startProgram(getThingHaId(), PROGRAM_HOOD_AUTOMATIC);
173             } else if (COMMAND_DELAYED_SHUT_OFF.equalsIgnoreCase(command.toFullString())) {
174                 apiClient.startProgram(getThingHaId(), PROGRAM_HOOD_DELAYED_SHUT_OFF);
175             } else if (COMMAND_VENTING_1.equalsIgnoreCase(command.toFullString())) {
176                 apiClient.startCustomProgram(getThingHaId(),
177                         format(START_VENTING_STAGE_PAYLOAD_TEMPLATE, STAGE_FAN_STAGE_01));
178             } else if (COMMAND_VENTING_2.equalsIgnoreCase(command.toFullString())) {
179                 apiClient.startCustomProgram(getThingHaId(),
180                         format(START_VENTING_STAGE_PAYLOAD_TEMPLATE, STAGE_FAN_STAGE_02));
181             } else if (COMMAND_VENTING_3.equalsIgnoreCase(command.toFullString())) {
182                 apiClient.startCustomProgram(getThingHaId(),
183                         format(START_VENTING_STAGE_PAYLOAD_TEMPLATE, STAGE_FAN_STAGE_03));
184             } else if (COMMAND_VENTING_4.equalsIgnoreCase(command.toFullString())) {
185                 apiClient.startCustomProgram(getThingHaId(),
186                         format(START_VENTING_STAGE_PAYLOAD_TEMPLATE, STAGE_FAN_STAGE_04));
187             } else if (COMMAND_VENTING_5.equalsIgnoreCase(command.toFullString())) {
188                 apiClient.startCustomProgram(getThingHaId(),
189                         format(START_VENTING_STAGE_PAYLOAD_TEMPLATE, STAGE_FAN_STAGE_05));
190             } else if (COMMAND_VENTING_INTENSIVE_1.equalsIgnoreCase(command.toFullString())) {
191                 apiClient.startCustomProgram(getThingHaId(),
192                         format(START_VENTING_INTENSIVE_STAGE_PAYLOAD_TEMPLATE, STAGE_INTENSIVE_STAGE_1));
193             } else if (COMMAND_VENTING_INTENSIVE_2.equalsIgnoreCase(command.toFullString())) {
194                 apiClient.startCustomProgram(getThingHaId(),
195                         format(START_VENTING_INTENSIVE_STAGE_PAYLOAD_TEMPLATE, STAGE_INTENSIVE_STAGE_2));
196             } else {
197                 logger.info("Start custom program. command={} haId={}", command.toFullString(), getThingHaId());
198                 apiClient.startCustomProgram(getThingHaId(), command.toFullString());
199             }
200         }
201     }
202
203     @Override
204     protected void updateSelectedProgramStateDescription() {
205         // update hood program actions
206         if (isBridgeOffline() || !isThingAccessibleViaServerSentEvents()) {
207             return;
208         }
209
210         Optional<HomeConnectApiClient> apiClient = getApiClient();
211         if (apiClient.isPresent()) {
212             try {
213                 ArrayList<StateOption> stateOptions = new ArrayList<>();
214                 apiClient.get().getPrograms(getThingHaId()).forEach(availableProgram -> {
215                     if (PROGRAM_HOOD_AUTOMATIC.equals(availableProgram.getKey())) {
216                         stateOptions.add(new StateOption(COMMAND_AUTOMATIC, mapStringType(availableProgram.getKey())));
217                     } else if (PROGRAM_HOOD_DELAYED_SHUT_OFF.equals(availableProgram.getKey())) {
218                         stateOptions.add(
219                                 new StateOption(COMMAND_DELAYED_SHUT_OFF, mapStringType(availableProgram.getKey())));
220                     } else if (PROGRAM_HOOD_VENTING.equals(availableProgram.getKey())) {
221                         try {
222                             List<AvailableProgramOption> availableProgramOptions = apiClient.get()
223                                     .getProgramOptions(getThingHaId(), PROGRAM_HOOD_VENTING);
224                             if (availableProgramOptions.isEmpty()) {
225                                 throw new CommunicationException("Program " + PROGRAM_HOOD_VENTING + " is unsupported");
226                             }
227                             availableProgramOptions.forEach(option -> {
228                                 if (OPTION_HOOD_VENTING_LEVEL.equalsIgnoreCase(option.getKey())) {
229                                     option.getAllowedValues().stream().filter(s -> !STAGE_FAN_OFF.equalsIgnoreCase(s))
230                                             .forEach(s -> stateOptions.add(createVentingStateOption(s)));
231                                 } else if (OPTION_HOOD_INTENSIVE_LEVEL.equalsIgnoreCase(option.getKey())) {
232                                     option.getAllowedValues().stream()
233                                             .filter(s -> !STAGE_INTENSIVE_STAGE_OFF.equalsIgnoreCase(s))
234                                             .forEach(s -> stateOptions.add(createVentingStateOption(s)));
235                                 }
236                             });
237                         } catch (CommunicationException | ApplianceOfflineException | AuthorizationException e) {
238                             logger.warn("Could not fetch hood program options. error={}", e.getMessage());
239                             stateOptions.add(createVentingStateOption(STAGE_FAN_STAGE_01));
240                             stateOptions.add(createVentingStateOption(STAGE_FAN_STAGE_02));
241                             stateOptions.add(createVentingStateOption(STAGE_FAN_STAGE_03));
242                             stateOptions.add(createVentingStateOption(STAGE_FAN_STAGE_04));
243                             stateOptions.add(createVentingStateOption(STAGE_FAN_STAGE_05));
244                             stateOptions.add(createVentingStateOption(STAGE_INTENSIVE_STAGE_1));
245                             stateOptions.add(createVentingStateOption(STAGE_INTENSIVE_STAGE_2));
246                         }
247                     }
248                 });
249                 stateOptions.add(new StateOption(COMMAND_STOP, "Stop"));
250
251                 getThingChannel(CHANNEL_HOOD_ACTIONS_STATE).ifPresent(channel -> getDynamicStateDescriptionProvider()
252                         .setStateOptions(channel.getUID(), stateOptions));
253             } catch (CommunicationException | ApplianceOfflineException | AuthorizationException e) {
254                 logger.debug("Could not fetch available programs. thing={}, haId={}, error={}", getThingLabel(),
255                         getThingHaId(), e.getMessage());
256                 removeSelectedProgramStateDescription();
257             }
258         } else {
259             removeSelectedProgramStateDescription();
260         }
261     }
262
263     @Override
264     protected void removeSelectedProgramStateDescription() {
265         getThingChannel(CHANNEL_HOOD_ACTIONS_STATE).ifPresent(
266                 channel -> getDynamicStateDescriptionProvider().setStateOptions(channel.getUID(), emptyList()));
267     }
268
269     @Override
270     public String toString() {
271         return "HomeConnectHoodHandler [haId: " + getThingHaId() + "]";
272     }
273
274     @Override
275     protected void resetProgramStateChannels() {
276         super.resetProgramStateChannels();
277         getThingChannel(CHANNEL_ACTIVE_PROGRAM_STATE).ifPresent(c -> updateState(c.getUID(), UnDefType.UNDEF));
278         getThingChannel(CHANNEL_HOOD_INTENSIVE_LEVEL).ifPresent(c -> updateState(c.getUID(), UnDefType.UNDEF));
279         getThingChannel(CHANNEL_HOOD_VENTING_LEVEL).ifPresent(c -> updateState(c.getUID(), UnDefType.UNDEF));
280     }
281
282     private StateOption createVentingStateOption(String optionKey) {
283         String label = mapStringType(PROGRAM_HOOD_VENTING);
284
285         if (STAGE_FAN_STAGE_01.equalsIgnoreCase(optionKey)) {
286             return new StateOption(COMMAND_VENTING_1,
287                     format("%s (Level %s)", label, mapStageStringType(STAGE_FAN_STAGE_01)));
288         } else if (STAGE_FAN_STAGE_02.equalsIgnoreCase(optionKey)) {
289             return new StateOption(COMMAND_VENTING_2,
290                     format("%s (Level %s)", label, mapStageStringType(STAGE_FAN_STAGE_02)));
291         } else if (STAGE_FAN_STAGE_03.equalsIgnoreCase(optionKey)) {
292             return new StateOption(COMMAND_VENTING_3,
293                     format("%s (Level %s)", label, mapStageStringType(STAGE_FAN_STAGE_03)));
294         } else if (STAGE_FAN_STAGE_04.equalsIgnoreCase(optionKey)) {
295             return new StateOption(COMMAND_VENTING_4,
296                     format("%s (Level %s)", label, mapStageStringType(STAGE_FAN_STAGE_04)));
297         } else if (STAGE_FAN_STAGE_05.equalsIgnoreCase(optionKey)) {
298             return new StateOption(COMMAND_VENTING_5,
299                     format("%s (Level %s)", label, mapStageStringType(STAGE_FAN_STAGE_05)));
300         } else if (STAGE_INTENSIVE_STAGE_1.equalsIgnoreCase(optionKey)) {
301             return new StateOption(COMMAND_VENTING_INTENSIVE_1,
302                     format("%s (Intensive level %s)", label, mapStageStringType(STAGE_INTENSIVE_STAGE_1)));
303         } else {
304             return new StateOption(COMMAND_VENTING_INTENSIVE_2,
305                     format("%s (Intensive level %s)", label, mapStageStringType(STAGE_INTENSIVE_STAGE_2)));
306         }
307     }
308 }