]> git.basschouten.com Git - openhab-addons.git/blob
a5a9d26b69e254b317f157f69199972ae844343f
[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.neeo.internal;
14
15 import java.io.IOException;
16 import java.util.ArrayList;
17 import java.util.List;
18 import java.util.Objects;
19 import java.util.concurrent.atomic.AtomicReference;
20
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<List<String>> activeScenarios = new AtomicReference<>(new ArrayList<>());
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 = NeeoRecipe.LAUNCH.equalsIgnoreCase(action.getAction());
99         final boolean poweroff = NeeoRecipe.POWEROFF.equalsIgnoreCase(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 && !scenarioKey.isEmpty()) {
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         List<String> oldActiveScenarios;
127         List<String> newActiveScenarios;
128
129         do {
130             oldActiveScenarios = this.activeScenarios.get();
131             newActiveScenarios = new ArrayList<>(oldActiveScenarios);
132
133             if (newActiveScenarios.contains(scenarioKey)) {
134                 if (launch) {
135                     return;
136                 } else {
137                     newActiveScenarios.remove(scenarioKey);
138                 }
139             } else {
140                 if (launch) {
141                     newActiveScenarios.add(scenarioKey);
142                 } else {
143                     return;
144                 }
145             }
146         } while (!this.activeScenarios.compareAndSet(oldActiveScenarios, newActiveScenarios));
147
148         refreshScenarioStatus(scenarioKey);
149     }
150
151     /**
152      * Refresh state of the room - currently only refreshes the active scenarios via {@link #refreshActiveScenarios()}
153      */
154     public void refreshState() {
155         refreshActiveScenarios();
156     }
157
158     /**
159      * Refresh the recipe name
160      *
161      * @param recipeKey the non-empty recipe key
162      */
163     public void refreshRecipeName(String recipeKey) {
164         NeeoUtil.requireNotEmpty(recipeKey, "recipeKey cannot be empty");
165
166         final NeeoRecipe recipe = neeoRoom.getRecipes().getRecipe(recipeKey);
167         if (recipe != null) {
168             callback.stateChanged(UidUtils.createChannelId(NeeoConstants.ROOM_GROUP_RECIPE_ID,
169                     NeeoConstants.ROOM_CHANNEL_NAME, recipeKey), new StringType(recipe.getName()));
170         }
171     }
172
173     /**
174      * Refresh the recipe type
175      *
176      * @param recipeKey the non-empty recipe key
177      */
178     public void refreshRecipeType(String recipeKey) {
179         NeeoUtil.requireNotEmpty(recipeKey, "recipeKey cannot be empty");
180
181         final NeeoRecipe recipe = neeoRoom.getRecipes().getRecipe(recipeKey);
182         if (recipe != null) {
183             callback.stateChanged(UidUtils.createChannelId(NeeoConstants.ROOM_GROUP_RECIPE_ID,
184                     NeeoConstants.ROOM_CHANNEL_TYPE, recipeKey), new StringType(recipe.getType()));
185         }
186     }
187
188     /**
189      * Refresh whether the recipe is enabled
190      *
191      * @param recipeKey the non-null recipe key
192      */
193     public void refreshRecipeEnabled(String recipeKey) {
194         NeeoUtil.requireNotEmpty(recipeKey, "recipeKey cannot be empty");
195
196         final NeeoRecipe recipe = neeoRoom.getRecipes().getRecipe(recipeKey);
197         if (recipe != null) {
198             callback.stateChanged(UidUtils.createChannelId(NeeoConstants.ROOM_GROUP_RECIPE_ID,
199                     NeeoConstants.ROOM_CHANNEL_ENABLED, recipeKey), recipe.isEnabled() ? OnOffType.ON : OnOffType.OFF);
200         }
201     }
202
203     /**
204      * Refresh the recipe status.
205      *
206      * @param recipeKey the non-null recipe key
207      */
208     public void refreshRecipeStatus(String recipeKey) {
209         NeeoUtil.requireNotEmpty(recipeKey, "recipeKey cannot be empty");
210
211         final NeeoRecipe recipe = neeoRoom.getRecipes().getRecipe(recipeKey);
212         if (recipe != null) {
213             callback.stateChanged(UidUtils.createChannelId(NeeoConstants.ROOM_GROUP_RECIPE_ID,
214                     NeeoConstants.ROOM_CHANNEL_STATUS, recipeKey), OnOffType.OFF);
215         }
216     }
217
218     /**
219      * Refresh the scenario name.
220      *
221      * @param scenarioKey the non-null scenario key
222      */
223     public void refreshScenarioName(String scenarioKey) {
224         NeeoUtil.requireNotEmpty(scenarioKey, "scenarioKey cannot be empty");
225
226         final NeeoScenario scenario = neeoRoom.getScenarios().getScenario(scenarioKey);
227         if (scenario != null) {
228             callback.stateChanged(UidUtils.createChannelId(NeeoConstants.ROOM_GROUP_RECIPE_ID,
229                     NeeoConstants.ROOM_CHANNEL_NAME, scenarioKey), new StringType(scenario.getName()));
230         }
231     }
232
233     /**
234      * Refresh whether the scenario is configured.
235      *
236      * @param scenarioKey the non-null scenario key
237      */
238     public void refreshScenarioConfigured(String scenarioKey) {
239         NeeoUtil.requireNotEmpty(scenarioKey, "scenarioKey cannot be empty");
240
241         final NeeoScenario scenario = neeoRoom.getScenarios().getScenario(scenarioKey);
242         if (scenario != null) {
243             callback.stateChanged(UidUtils.createChannelId(NeeoConstants.ROOM_GROUP_SCENARIO_ID,
244                     NeeoConstants.ROOM_CHANNEL_ENABLED, scenarioKey),
245                     scenario.isConfigured() ? OnOffType.ON : OnOffType.OFF);
246         }
247     }
248
249     /**
250      * Refresh the scenario status.
251      *
252      * @param scenarioKey the non-null scenario key
253      */
254     public void refreshScenarioStatus(String scenarioKey) {
255         NeeoUtil.requireNotEmpty(scenarioKey, "scenarioKey cannot be empty");
256
257         final NeeoScenario scenario = neeoRoom.getScenarios().getScenario(scenarioKey);
258         if (scenario != null) {
259             final boolean isActive = activeScenarios.get().contains(scenarioKey);
260             callback.stateChanged(UidUtils.createChannelId(NeeoConstants.ROOM_GROUP_SCENARIO_ID,
261                     NeeoConstants.ROOM_CHANNEL_STATUS, scenarioKey), OnOffType.from(isActive));
262         }
263     }
264
265     /**
266      * Refresh active scenarios
267      */
268     private void refreshActiveScenarios() {
269         final NeeoBrainApi api = callback.getApi();
270         if (api == null) {
271             logger.debug("API is null [likely bridge is offline]");
272         } else {
273             try {
274                 final List<String> activeScenarios = api.getActiveScenarios();
275                 final List<String> oldScenarios = this.activeScenarios.getAndSet(activeScenarios);
276
277                 if (!activeScenarios.equals(oldScenarios)) {
278                     activeScenarios.forEach(this::refreshScenarioStatus);
279                     oldScenarios.removeIf(activeScenarios::contains);
280                     oldScenarios.forEach(this::refreshScenarioStatus);
281                 }
282             } catch (IOException e) {
283                 logger.debug("Exception requesting active scenarios: {}", e.getMessage(), e);
284                 // callback.statusChanged(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
285             }
286         }
287     }
288
289     /**
290      * Sends the trigger for the current step
291      *
292      * @param step a possibly null, possibly empty step to send
293      */
294     private void sendCurrentStepTrigger(@Nullable String step) {
295         callback.triggerEvent(
296                 UidUtils.createChannelId(NeeoConstants.ROOM_GROUP_STATE_ID, NeeoConstants.ROOM_CHANNEL_CURRENTSTEP),
297                 step == null || step.isEmpty() ? "" : step);
298     }
299
300     /**
301      * Starts the given recipe key
302      *
303      * @param recipeKey the non-null recipe key
304      */
305     public void startRecipe(String recipeKey) {
306         NeeoUtil.requireNotEmpty(recipeKey, "recipeKey cannot be empty");
307
308         final NeeoBrainApi api = callback.getApi();
309         if (api == null) {
310             logger.debug("API is null [likely bridge is offline] - cannot start recipe: {}", recipeKey);
311         } else {
312             final NeeoRecipe recipe = neeoRoom.getRecipes().getRecipe(recipeKey);
313             final String scenarioKey = recipe == null ? null : recipe.getScenarioKey();
314
315             if (recipe != null) {
316                 if (recipe.isEnabled()) {
317                     final boolean isLaunch = NeeoRecipe.LAUNCH.equalsIgnoreCase(recipe.getType());
318
319                     try {
320                         if (isLaunch || scenarioKey == null || scenarioKey.isEmpty()) {
321                             handleExecuteResult(scenarioKey, recipeKey, true, api.executeRecipe(roomKey, recipeKey));
322                         } else {
323                             handleExecuteResult(scenarioKey, recipeKey, false, api.stopScenario(roomKey, scenarioKey));
324                         }
325                     } catch (IOException e) {
326                         logger.debug("Exception occurred during execution: {}", e.getMessage(), e);
327                     }
328                 } else {
329                     logger.debug("recipe for key {} was not enabled, cannot start or stop", recipeKey);
330                 }
331             } else {
332                 logger.debug("recipe key {} was not found", recipeKey);
333             }
334         }
335     }
336
337     /**
338      * Sets the scenario status.
339      *
340      * @param scenarioKey the non-null scenario key
341      * @param start whether to start (true) or stop (false) the scenario
342      */
343     public void setScenarioStatus(String scenarioKey, boolean start) {
344         NeeoUtil.requireNotEmpty(scenarioKey, "scenarioKey cannot be empty");
345
346         final NeeoRecipe recipe = neeoRoom.getRecipes().getRecipeByScenarioKey(scenarioKey,
347                 start ? NeeoRecipe.LAUNCH : NeeoRecipe.POWEROFF);
348         final String recipeKey = recipe == null ? null : recipe.getKey();
349
350         if (recipe != null && recipeKey != null && !recipeKey.isEmpty()) {
351             if (recipe.isEnabled()) {
352                 startRecipe(recipeKey);
353             } else {
354                 logger.debug("Recipe ({}) found for scenario {} but was not enabled", recipe.getKey(), scenarioKey);
355             }
356         } else {
357             logger.debug("No recipe found for scenario {} to start ({})", scenarioKey, start);
358         }
359     }
360
361     /**
362      * Handle the {@link ExecuteResult} from a call
363      *
364      * @param scenarioKey the possibly null scenario key being changed
365      * @param recipeKey the non-null recipe key being used
366      * @param launch whether the recipe launches the scenario (true) or not (false)
367      * @param result the non-null result (null will do nothing)
368      */
369     private void handleExecuteResult(@Nullable String scenarioKey, String recipeKey, boolean launch,
370             ExecuteResult result) {
371         Objects.requireNonNull(result, "result cannot be empty");
372         NeeoUtil.requireNotEmpty(recipeKey, "recipeKey cannot be empty");
373
374         int nextStep = 0;
375         if (scenarioKey != null && !scenarioKey.isEmpty()) {
376             callback.scheduleTask(() -> {
377                 processScenarioChange(scenarioKey, launch);
378             }, 1);
379         }
380
381         for (final ExecuteStep step : result.getSteps()) {
382             callback.scheduleTask(() -> {
383                 sendCurrentStepTrigger(step.getText());
384             }, nextStep);
385             nextStep += step.getDuration();
386         }
387
388         callback.scheduleTask(() -> {
389             sendCurrentStepTrigger(null);
390             refreshRecipeStatus(recipeKey);
391         }, nextStep);
392     }
393 }