]> git.basschouten.com Git - openhab-addons.git/blob
fe255eb8d64328836f83a889b5736dcb3550dde7
[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), OnOffType.from(recipe.isEnabled()));
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), OnOffType.from(scenario.isConfigured()));
245         }
246     }
247
248     /**
249      * Refresh the scenario status.
250      *
251      * @param scenarioKey the non-null scenario key
252      */
253     public void refreshScenarioStatus(String scenarioKey) {
254         NeeoUtil.requireNotEmpty(scenarioKey, "scenarioKey cannot be empty");
255
256         final NeeoScenario scenario = neeoRoom.getScenarios().getScenario(scenarioKey);
257         if (scenario != null) {
258             final boolean isActive = activeScenarios.get().contains(scenarioKey);
259             callback.stateChanged(UidUtils.createChannelId(NeeoConstants.ROOM_GROUP_SCENARIO_ID,
260                     NeeoConstants.ROOM_CHANNEL_STATUS, scenarioKey), OnOffType.from(isActive));
261         }
262     }
263
264     /**
265      * Refresh active scenarios
266      */
267     private void refreshActiveScenarios() {
268         final NeeoBrainApi api = callback.getApi();
269         if (api == null) {
270             logger.debug("API is null [likely bridge is offline]");
271         } else {
272             try {
273                 final List<String> activeScenarios = api.getActiveScenarios();
274                 final List<String> oldScenarios = this.activeScenarios.getAndSet(activeScenarios);
275
276                 if (!activeScenarios.equals(oldScenarios)) {
277                     activeScenarios.forEach(this::refreshScenarioStatus);
278                     oldScenarios.removeIf(activeScenarios::contains);
279                     oldScenarios.forEach(this::refreshScenarioStatus);
280                 }
281             } catch (IOException e) {
282                 logger.debug("Exception requesting active scenarios: {}", e.getMessage(), e);
283                 // callback.statusChanged(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
284             }
285         }
286     }
287
288     /**
289      * Sends the trigger for the current step
290      *
291      * @param step a possibly null, possibly empty step to send
292      */
293     private void sendCurrentStepTrigger(@Nullable String step) {
294         callback.triggerEvent(
295                 UidUtils.createChannelId(NeeoConstants.ROOM_GROUP_STATE_ID, NeeoConstants.ROOM_CHANNEL_CURRENTSTEP),
296                 step == null || step.isEmpty() ? "" : step);
297     }
298
299     /**
300      * Starts the given recipe key
301      *
302      * @param recipeKey the non-null recipe key
303      */
304     public void startRecipe(String recipeKey) {
305         NeeoUtil.requireNotEmpty(recipeKey, "recipeKey cannot be empty");
306
307         final NeeoBrainApi api = callback.getApi();
308         if (api == null) {
309             logger.debug("API is null [likely bridge is offline] - cannot start recipe: {}", recipeKey);
310         } else {
311             final NeeoRecipe recipe = neeoRoom.getRecipes().getRecipe(recipeKey);
312             final String scenarioKey = recipe == null ? null : recipe.getScenarioKey();
313
314             if (recipe != null) {
315                 if (recipe.isEnabled()) {
316                     final boolean isLaunch = NeeoRecipe.LAUNCH.equalsIgnoreCase(recipe.getType());
317
318                     try {
319                         if (isLaunch || scenarioKey == null || scenarioKey.isEmpty()) {
320                             handleExecuteResult(scenarioKey, recipeKey, true, api.executeRecipe(roomKey, recipeKey));
321                         } else {
322                             handleExecuteResult(scenarioKey, recipeKey, false, api.stopScenario(roomKey, scenarioKey));
323                         }
324                     } catch (IOException e) {
325                         logger.debug("Exception occurred during execution: {}", e.getMessage(), e);
326                     }
327                 } else {
328                     logger.debug("recipe for key {} was not enabled, cannot start or stop", recipeKey);
329                 }
330             } else {
331                 logger.debug("recipe key {} was not found", recipeKey);
332             }
333         }
334     }
335
336     /**
337      * Sets the scenario status.
338      *
339      * @param scenarioKey the non-null scenario key
340      * @param start whether to start (true) or stop (false) the scenario
341      */
342     public void setScenarioStatus(String scenarioKey, boolean start) {
343         NeeoUtil.requireNotEmpty(scenarioKey, "scenarioKey cannot be empty");
344
345         final NeeoRecipe recipe = neeoRoom.getRecipes().getRecipeByScenarioKey(scenarioKey,
346                 start ? NeeoRecipe.LAUNCH : NeeoRecipe.POWEROFF);
347         final String recipeKey = recipe == null ? null : recipe.getKey();
348
349         if (recipe != null && recipeKey != null && !recipeKey.isEmpty()) {
350             if (recipe.isEnabled()) {
351                 startRecipe(recipeKey);
352             } else {
353                 logger.debug("Recipe ({}) found for scenario {} but was not enabled", recipe.getKey(), scenarioKey);
354             }
355         } else {
356             logger.debug("No recipe found for scenario {} to start ({})", scenarioKey, start);
357         }
358     }
359
360     /**
361      * Handle the {@link ExecuteResult} from a call
362      *
363      * @param scenarioKey the possibly null scenario key being changed
364      * @param recipeKey the non-null recipe key being used
365      * @param launch whether the recipe launches the scenario (true) or not (false)
366      * @param result the non-null result (null will do nothing)
367      */
368     private void handleExecuteResult(@Nullable String scenarioKey, String recipeKey, boolean launch,
369             ExecuteResult result) {
370         Objects.requireNonNull(result, "result cannot be empty");
371         NeeoUtil.requireNotEmpty(recipeKey, "recipeKey cannot be empty");
372
373         int nextStep = 0;
374         if (scenarioKey != null && !scenarioKey.isEmpty()) {
375             callback.scheduleTask(() -> {
376                 processScenarioChange(scenarioKey, launch);
377             }, 1);
378         }
379
380         for (final ExecuteStep step : result.getSteps()) {
381             callback.scheduleTask(() -> {
382                 sendCurrentStepTrigger(step.getText());
383             }, nextStep);
384             nextStep += step.getDuration();
385         }
386
387         callback.scheduleTask(() -> {
388             sendCurrentStepTrigger(null);
389             refreshRecipeStatus(recipeKey);
390         }, nextStep);
391     }
392 }