]> git.basschouten.com Git - openhab-addons.git/blob
e4abd30f59afe90d859b22d4f27c6b31cb8612c5
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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.neeo.internal;
14
15 import java.io.IOException;
16 import java.util.Objects;
17 import java.util.concurrent.atomic.AtomicReference;
18
19 import org.apache.commons.lang.ArrayUtils;
20 import org.apache.commons.lang.StringUtils;
21 import org.eclipse.jdt.annotation.NonNullByDefault;
22 import org.eclipse.jdt.annotation.Nullable;
23 import org.openhab.binding.neeo.internal.models.ExecuteResult;
24 import org.openhab.binding.neeo.internal.models.ExecuteStep;
25 import org.openhab.binding.neeo.internal.models.NeeoAction;
26 import org.openhab.binding.neeo.internal.models.NeeoRecipe;
27 import org.openhab.binding.neeo.internal.models.NeeoRecipes;
28 import org.openhab.binding.neeo.internal.models.NeeoRoom;
29 import org.openhab.binding.neeo.internal.models.NeeoScenario;
30 import org.openhab.core.library.types.OnOffType;
31 import org.openhab.core.library.types.StringType;
32 import org.slf4j.Logger;
33 import org.slf4j.LoggerFactory;
34
35 /**
36  * This protocol class for a Neeo Room
37  *
38  * @author Tim Roberts - Initial contribution
39  */
40 @NonNullByDefault
41 public class NeeoRoomProtocol {
42
43     /** The logger */
44     private final Logger logger = LoggerFactory.getLogger(NeeoRoomProtocol.class);
45
46     /** The {@link NeeoHandlerCallback} */
47     private final NeeoHandlerCallback callback;
48
49     /** The room key */
50     private final String roomKey;
51
52     /** The {@link NeeoRoom} */
53     private final NeeoRoom neeoRoom;
54
55     /** The currently active scenarios */
56     private final AtomicReference<String[]> activeScenarios = new AtomicReference<>(new String[0]);
57
58     /**
59      * Instantiates a new neeo room protocol.
60      *
61      * @param callback the non-null callback
62      * @param roomKey the non-empty room key
63      * @throws IOException Signals that an I/O exception has occurred.
64      */
65     public NeeoRoomProtocol(NeeoHandlerCallback callback, String roomKey) throws IOException {
66         Objects.requireNonNull(callback, "callback cannot be null");
67         NeeoUtil.requireNotEmpty(roomKey, "roomKey cannot be empty");
68
69         this.callback = callback;
70         this.roomKey = roomKey;
71
72         final NeeoBrainApi api = callback.getApi();
73         if (api == null) {
74             throw new IllegalArgumentException("NeeoBrainApi cannot be null");
75         }
76
77         neeoRoom = api.getRoom(roomKey);
78     }
79
80     /**
81      * Returns the callback being used
82      *
83      * @return the non-null callback
84      */
85     public NeeoHandlerCallback getCallback() {
86         return callback;
87     }
88
89     /**
90      * Processes the action if it applies to this room
91      *
92      * @param action a non-null action to process
93      */
94     public void processAction(NeeoAction action) {
95         Objects.requireNonNull(action, "action cannot be null");
96
97         final NeeoRecipes recipes = neeoRoom.getRecipes();
98         final boolean launch = StringUtils.equalsIgnoreCase(NeeoRecipe.LAUNCH, action.getAction());
99         final boolean poweroff = StringUtils.equalsIgnoreCase(NeeoRecipe.POWEROFF, action.getAction());
100
101         // Can't be both true but if both false - it's neither one
102         if (launch == poweroff) {
103             return;
104         }
105
106         final String recipeName = action.getRecipe();
107         final NeeoRecipe recipe = recipeName == null ? null : recipes.getRecipeByName(recipeName);
108         final String scenarioKey = recipe == null ? null : recipe.getScenarioKey();
109
110         if (scenarioKey != null && StringUtils.isNotEmpty(scenarioKey)) {
111             processScenarioChange(scenarioKey, launch);
112         } else {
113             logger.debug("Could not find a recipe named '{}' for the action {}", recipeName, action);
114         }
115     }
116
117     /**
118      * Processes a change to the scenario (whether it's been launched or not)
119      *
120      * @param scenarioKey a non-null, non-empty scenario key
121      * @param launch true if the scenario was launched, false otherwise
122      */
123     private void processScenarioChange(String scenarioKey, boolean launch) {
124         NeeoUtil.requireNotEmpty(scenarioKey, "scenarioKey cannot be empty");
125
126         final String[] activeScenarios = this.activeScenarios.get();
127         final int idx = ArrayUtils.indexOf(activeScenarios, scenarioKey);
128
129         // already set that way
130         if ((idx < 0 && !launch) || (idx >= 0 && launch)) {
131             return;
132         }
133
134         final String[] newScenarios = idx >= 0 ? (String[]) ArrayUtils.remove(activeScenarios, idx)
135                 : (String[]) ArrayUtils.add(activeScenarios, scenarioKey);
136
137         this.activeScenarios.set(newScenarios);
138
139         refreshScenarioStatus(scenarioKey);
140     }
141
142     /**
143      * Refresh state of the room - currently only refreshes the active scenarios via {@link #refreshActiveScenarios()}
144      */
145     public void refreshState() {
146         refreshActiveScenarios();
147     }
148
149     /**
150      * Refresh the recipe name
151      *
152      * @param recipeKey the non-empty recipe key
153      */
154     public void refreshRecipeName(String recipeKey) {
155         NeeoUtil.requireNotEmpty(recipeKey, "recipeKey cannot be empty");
156
157         final NeeoRecipe recipe = neeoRoom.getRecipes().getRecipe(recipeKey);
158         if (recipe != null) {
159             callback.stateChanged(UidUtils.createChannelId(NeeoConstants.ROOM_GROUP_RECIPE_ID,
160                     NeeoConstants.ROOM_CHANNEL_NAME, recipeKey), new StringType(recipe.getName()));
161         }
162     }
163
164     /**
165      * Refresh the recipe type
166      *
167      * @param recipeKey the non-empty recipe key
168      */
169     public void refreshRecipeType(String recipeKey) {
170         NeeoUtil.requireNotEmpty(recipeKey, "recipeKey cannot be empty");
171
172         final NeeoRecipe recipe = neeoRoom.getRecipes().getRecipe(recipeKey);
173         if (recipe != null) {
174             callback.stateChanged(UidUtils.createChannelId(NeeoConstants.ROOM_GROUP_RECIPE_ID,
175                     NeeoConstants.ROOM_CHANNEL_TYPE, recipeKey), new StringType(recipe.getType()));
176         }
177     }
178
179     /**
180      * Refresh whether the recipe is enabled
181      *
182      * @param recipeKey the non-null recipe key
183      */
184     public void refreshRecipeEnabled(String recipeKey) {
185         NeeoUtil.requireNotEmpty(recipeKey, "recipeKey cannot be empty");
186
187         final NeeoRecipe recipe = neeoRoom.getRecipes().getRecipe(recipeKey);
188         if (recipe != null) {
189             callback.stateChanged(UidUtils.createChannelId(NeeoConstants.ROOM_GROUP_RECIPE_ID,
190                     NeeoConstants.ROOM_CHANNEL_ENABLED, recipeKey), recipe.isEnabled() ? OnOffType.ON : OnOffType.OFF);
191         }
192     }
193
194     /**
195      * Refresh the recipe status.
196      *
197      * @param recipeKey the non-null recipe key
198      */
199     public void refreshRecipeStatus(String recipeKey) {
200         NeeoUtil.requireNotEmpty(recipeKey, "recipeKey cannot be empty");
201
202         final NeeoRecipe recipe = neeoRoom.getRecipes().getRecipe(recipeKey);
203         if (recipe != null) {
204             callback.stateChanged(UidUtils.createChannelId(NeeoConstants.ROOM_GROUP_RECIPE_ID,
205                     NeeoConstants.ROOM_CHANNEL_STATUS, recipeKey), OnOffType.OFF);
206         }
207     }
208
209     /**
210      * Refresh the scenario name.
211      *
212      * @param scenarioKey the non-null scenario key
213      */
214     public void refreshScenarioName(String scenarioKey) {
215         NeeoUtil.requireNotEmpty(scenarioKey, "scenarioKey cannot be empty");
216
217         final NeeoScenario scenario = neeoRoom.getScenarios().getScenario(scenarioKey);
218         if (scenario != null) {
219             callback.stateChanged(UidUtils.createChannelId(NeeoConstants.ROOM_GROUP_RECIPE_ID,
220                     NeeoConstants.ROOM_CHANNEL_NAME, scenarioKey), new StringType(scenario.getName()));
221         }
222     }
223
224     /**
225      * Refresh whether the scenario is configured.
226      *
227      * @param scenarioKey the non-null scenario key
228      */
229     public void refreshScenarioConfigured(String scenarioKey) {
230         NeeoUtil.requireNotEmpty(scenarioKey, "scenarioKey cannot be empty");
231
232         final NeeoScenario scenario = neeoRoom.getScenarios().getScenario(scenarioKey);
233         if (scenario != null) {
234             callback.stateChanged(UidUtils.createChannelId(NeeoConstants.ROOM_GROUP_SCENARIO_ID,
235                     NeeoConstants.ROOM_CHANNEL_ENABLED, scenarioKey),
236                     scenario.isConfigured() ? OnOffType.ON : OnOffType.OFF);
237         }
238     }
239
240     /**
241      * Refresh the scenario status.
242      *
243      * @param scenarioKey the non-null scenario key
244      */
245     public void refreshScenarioStatus(String scenarioKey) {
246         NeeoUtil.requireNotEmpty(scenarioKey, "scenarioKey cannot be empty");
247
248         final NeeoScenario scenario = neeoRoom.getScenarios().getScenario(scenarioKey);
249         if (scenario != null) {
250             final String[] active = activeScenarios.get();
251             final boolean isActive = ArrayUtils.contains(active, scenarioKey);
252             callback.stateChanged(UidUtils.createChannelId(NeeoConstants.ROOM_GROUP_SCENARIO_ID,
253                     NeeoConstants.ROOM_CHANNEL_STATUS, scenarioKey), isActive ? OnOffType.ON : OnOffType.OFF);
254         }
255     }
256
257     /**
258      * Refresh active scenarios
259      */
260     private void refreshActiveScenarios() {
261         final NeeoBrainApi api = callback.getApi();
262         if (api == null) {
263             logger.debug("API is null [likely bridge is offline]");
264         } else {
265             try {
266                 final String[] activeScenarios = api.getActiveScenarios();
267                 final String[] oldScenarios = this.activeScenarios.getAndSet(activeScenarios);
268
269                 if (!ArrayUtils.isEquals(activeScenarios, oldScenarios)) {
270                     for (String scenario : activeScenarios) {
271                         refreshScenarioStatus(scenario);
272                     }
273
274                     for (String oldScenario : oldScenarios) {
275                         if (!ArrayUtils.contains(activeScenarios, oldScenario)) {
276                             refreshScenarioStatus(oldScenario);
277                         }
278                     }
279                 }
280             } catch (IOException e) {
281                 logger.debug("Exception requesting active scenarios: {}", e.getMessage(), e);
282                 // callback.statusChanged(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
283             }
284         }
285     }
286
287     /**
288      * Sends the trigger for the current step
289      *
290      * @param step a possibly null, possibly empty step to send
291      */
292     private void sendCurrentStepTrigger(@Nullable String step) {
293         callback.triggerEvent(
294                 UidUtils.createChannelId(NeeoConstants.ROOM_GROUP_STATE_ID, NeeoConstants.ROOM_CHANNEL_CURRENTSTEP),
295                 step == null || StringUtils.isEmpty(step) ? "" : step);
296     }
297
298     /**
299      * Starts the given recipe key
300      *
301      * @param recipeKey the non-null recipe key
302      */
303     public void startRecipe(String recipeKey) {
304         NeeoUtil.requireNotEmpty(recipeKey, "recipeKey cannot be empty");
305
306         final NeeoBrainApi api = callback.getApi();
307         if (api == null) {
308             logger.debug("API is null [likely bridge is offline] - cannot start recipe: {}", recipeKey);
309         } else {
310             final NeeoRecipe recipe = neeoRoom.getRecipes().getRecipe(recipeKey);
311             final String scenarioKey = recipe == null ? null : recipe.getScenarioKey();
312
313             if (recipe != null) {
314                 if (recipe.isEnabled()) {
315                     final boolean isLaunch = StringUtils.equalsIgnoreCase(NeeoRecipe.LAUNCH, recipe.getType());
316
317                     try {
318                         if (isLaunch || scenarioKey == null || StringUtils.isEmpty(scenarioKey)) {
319                             handleExecuteResult(scenarioKey, recipeKey, true, api.executeRecipe(roomKey, recipeKey));
320                         } else {
321                             handleExecuteResult(scenarioKey, recipeKey, false, api.stopScenario(roomKey, scenarioKey));
322                         }
323                     } catch (IOException e) {
324                         logger.debug("Exception occurred during execution: {}", e.getMessage(), e);
325                     }
326                 } else {
327                     logger.debug("recipe for key {} was not enabled, cannot start or stop", recipeKey);
328                 }
329             } else {
330                 logger.debug("recipe key {} was not found", recipeKey);
331             }
332         }
333     }
334
335     /**
336      * Sets the scenario status.
337      *
338      * @param scenarioKey the non-null scenario key
339      * @param start whether to start (true) or stop (false) the scenario
340      */
341     public void setScenarioStatus(String scenarioKey, boolean start) {
342         NeeoUtil.requireNotEmpty(scenarioKey, "scenarioKey cannot be empty");
343
344         final NeeoRecipe recipe = neeoRoom.getRecipes().getRecipeByScenarioKey(scenarioKey,
345                 start ? NeeoRecipe.LAUNCH : NeeoRecipe.POWEROFF);
346         final String recipeKey = recipe == null ? null : recipe.getKey();
347
348         if (recipe != null && recipeKey != null && StringUtils.isNotEmpty(recipeKey)) {
349             if (recipe.isEnabled()) {
350                 startRecipe(recipeKey);
351             } else {
352                 logger.debug("Recipe ({}) found for scenario {} but was not enabled", recipe.getKey(), scenarioKey);
353             }
354         } else {
355             logger.debug("No recipe found for scenario {} to start ({})", scenarioKey, start);
356         }
357     }
358
359     /**
360      * Handle the {@link ExecuteResult} from a call
361      *
362      * @param scenarioKey the possibly null scenario key being changed
363      * @param recipeKey the non-null recipe key being used
364      * @param launch whether the recipe launches the scenario (true) or not (false)
365      * @param result the non-null result (null will do nothing)
366      */
367     private void handleExecuteResult(@Nullable String scenarioKey, String recipeKey, boolean launch,
368             ExecuteResult result) {
369         Objects.requireNonNull(result, "result cannot be empty");
370         NeeoUtil.requireNotEmpty(recipeKey, "recipeKey cannot be empty");
371
372         int nextStep = 0;
373         if (scenarioKey != null && StringUtils.isNotEmpty(scenarioKey)) {
374             callback.scheduleTask(() -> {
375                 processScenarioChange(scenarioKey, launch);
376             }, 1);
377         }
378
379         for (final ExecuteStep step : result.getSteps()) {
380             callback.scheduleTask(() -> {
381                 sendCurrentStepTrigger(step.getText());
382             }, nextStep);
383             nextStep += step.getDuration();
384         }
385
386         callback.scheduleTask(() -> {
387             sendCurrentStepTrigger(null);
388             refreshRecipeStatus(recipeKey);
389         }, nextStep);
390     }
391 }