2 * Copyright (c) 2010-2020 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.neeo.internal;
15 import java.io.IOException;
16 import java.util.Objects;
17 import java.util.concurrent.atomic.AtomicReference;
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;
36 * This protocol class for a Neeo Room
38 * @author Tim Roberts - Initial contribution
41 public class NeeoRoomProtocol {
44 private final Logger logger = LoggerFactory.getLogger(NeeoRoomProtocol.class);
46 /** The {@link NeeoHandlerCallback} */
47 private final NeeoHandlerCallback callback;
50 private final String roomKey;
52 /** The {@link NeeoRoom} */
53 private final NeeoRoom neeoRoom;
55 /** The currently active scenarios */
56 private final AtomicReference<String[]> activeScenarios = new AtomicReference<>(new String[0]);
59 * Instantiates a new neeo room protocol.
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.
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");
69 this.callback = callback;
70 this.roomKey = roomKey;
72 final NeeoBrainApi api = callback.getApi();
74 throw new IllegalArgumentException("NeeoBrainApi cannot be null");
77 neeoRoom = api.getRoom(roomKey);
81 * Returns the callback being used
83 * @return the non-null callback
85 public NeeoHandlerCallback getCallback() {
90 * Processes the action if it applies to this room
92 * @param action a non-null action to process
94 public void processAction(NeeoAction action) {
95 Objects.requireNonNull(action, "action cannot be null");
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());
101 // Can't be both true but if both false - it's neither one
102 if (launch == poweroff) {
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();
110 if (scenarioKey != null && StringUtils.isNotEmpty(scenarioKey)) {
111 processScenarioChange(scenarioKey, launch);
113 logger.debug("Could not find a recipe named '{}' for the action {}", recipeName, action);
118 * Processes a change to the scenario (whether it's been launched or not)
120 * @param scenarioKey a non-null, non-empty scenario key
121 * @param launch true if the scenario was launched, false otherwise
123 private void processScenarioChange(String scenarioKey, boolean launch) {
124 NeeoUtil.requireNotEmpty(scenarioKey, "scenarioKey cannot be empty");
126 final String[] activeScenarios = this.activeScenarios.get();
127 final int idx = ArrayUtils.indexOf(activeScenarios, scenarioKey);
129 // already set that way
130 if ((idx < 0 && !launch) || (idx >= 0 && launch)) {
134 final String[] newScenarios = idx >= 0 ? (String[]) ArrayUtils.remove(activeScenarios, idx)
135 : (String[]) ArrayUtils.add(activeScenarios, scenarioKey);
137 this.activeScenarios.set(newScenarios);
139 refreshScenarioStatus(scenarioKey);
143 * Refresh state of the room - currently only refreshes the active scenarios via {@link #refreshActiveScenarios()}
145 public void refreshState() {
146 refreshActiveScenarios();
150 * Refresh the recipe name
152 * @param recipeKey the non-empty recipe key
154 public void refreshRecipeName(String recipeKey) {
155 NeeoUtil.requireNotEmpty(recipeKey, "recipeKey cannot be empty");
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()));
165 * Refresh the recipe type
167 * @param recipeKey the non-empty recipe key
169 public void refreshRecipeType(String recipeKey) {
170 NeeoUtil.requireNotEmpty(recipeKey, "recipeKey cannot be empty");
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()));
180 * Refresh whether the recipe is enabled
182 * @param recipeKey the non-null recipe key
184 public void refreshRecipeEnabled(String recipeKey) {
185 NeeoUtil.requireNotEmpty(recipeKey, "recipeKey cannot be empty");
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);
195 * Refresh the recipe status.
197 * @param recipeKey the non-null recipe key
199 public void refreshRecipeStatus(String recipeKey) {
200 NeeoUtil.requireNotEmpty(recipeKey, "recipeKey cannot be empty");
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);
210 * Refresh the scenario name.
212 * @param scenarioKey the non-null scenario key
214 public void refreshScenarioName(String scenarioKey) {
215 NeeoUtil.requireNotEmpty(scenarioKey, "scenarioKey cannot be empty");
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()));
225 * Refresh whether the scenario is configured.
227 * @param scenarioKey the non-null scenario key
229 public void refreshScenarioConfigured(String scenarioKey) {
230 NeeoUtil.requireNotEmpty(scenarioKey, "scenarioKey cannot be empty");
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);
241 * Refresh the scenario status.
243 * @param scenarioKey the non-null scenario key
245 public void refreshScenarioStatus(String scenarioKey) {
246 NeeoUtil.requireNotEmpty(scenarioKey, "scenarioKey cannot be empty");
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);
258 * Refresh active scenarios
260 private void refreshActiveScenarios() {
261 final NeeoBrainApi api = callback.getApi();
263 logger.debug("API is null [likely bridge is offline]");
266 final String[] activeScenarios = api.getActiveScenarios();
267 final String[] oldScenarios = this.activeScenarios.getAndSet(activeScenarios);
269 if (!ArrayUtils.isEquals(activeScenarios, oldScenarios)) {
270 for (String scenario : activeScenarios) {
271 refreshScenarioStatus(scenario);
274 for (String oldScenario : oldScenarios) {
275 if (!ArrayUtils.contains(activeScenarios, oldScenario)) {
276 refreshScenarioStatus(oldScenario);
280 } catch (IOException e) {
281 logger.debug("Exception requesting active scenarios: {}", e.getMessage(), e);
282 // callback.statusChanged(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
288 * Sends the trigger for the current step
290 * @param step a possibly null, possibly empty step to send
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);
299 * Starts the given recipe key
301 * @param recipeKey the non-null recipe key
303 public void startRecipe(String recipeKey) {
304 NeeoUtil.requireNotEmpty(recipeKey, "recipeKey cannot be empty");
306 final NeeoBrainApi api = callback.getApi();
308 logger.debug("API is null [likely bridge is offline] - cannot start recipe: {}", recipeKey);
310 final NeeoRecipe recipe = neeoRoom.getRecipes().getRecipe(recipeKey);
311 final String scenarioKey = recipe == null ? null : recipe.getScenarioKey();
313 if (recipe != null) {
314 if (recipe.isEnabled()) {
315 final boolean isLaunch = StringUtils.equalsIgnoreCase(NeeoRecipe.LAUNCH, recipe.getType());
318 if (isLaunch || scenarioKey == null || StringUtils.isEmpty(scenarioKey)) {
319 handleExecuteResult(scenarioKey, recipeKey, true, api.executeRecipe(roomKey, recipeKey));
321 handleExecuteResult(scenarioKey, recipeKey, false, api.stopScenario(roomKey, scenarioKey));
323 } catch (IOException e) {
324 logger.debug("Exception occurred during execution: {}", e.getMessage(), e);
327 logger.debug("recipe for key {} was not enabled, cannot start or stop", recipeKey);
330 logger.debug("recipe key {} was not found", recipeKey);
336 * Sets the scenario status.
338 * @param scenarioKey the non-null scenario key
339 * @param start whether to start (true) or stop (false) the scenario
341 public void setScenarioStatus(String scenarioKey, boolean start) {
342 NeeoUtil.requireNotEmpty(scenarioKey, "scenarioKey cannot be empty");
344 final NeeoRecipe recipe = neeoRoom.getRecipes().getRecipeByScenarioKey(scenarioKey,
345 start ? NeeoRecipe.LAUNCH : NeeoRecipe.POWEROFF);
346 final String recipeKey = recipe == null ? null : recipe.getKey();
348 if (recipe != null && recipeKey != null && StringUtils.isNotEmpty(recipeKey)) {
349 if (recipe.isEnabled()) {
350 startRecipe(recipeKey);
352 logger.debug("Recipe ({}) found for scenario {} but was not enabled", recipe.getKey(), scenarioKey);
355 logger.debug("No recipe found for scenario {} to start ({})", scenarioKey, start);
360 * Handle the {@link ExecuteResult} from a call
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)
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");
373 if (scenarioKey != null && StringUtils.isNotEmpty(scenarioKey)) {
374 callback.scheduleTask(() -> {
375 processScenarioChange(scenarioKey, launch);
379 for (final ExecuteStep step : result.getSteps()) {
380 callback.scheduleTask(() -> {
381 sendCurrentStepTrigger(step.getText());
383 nextStep += step.getDuration();
386 callback.scheduleTask(() -> {
387 sendCurrentStepTrigger(null);
388 refreshRecipeStatus(recipeKey);