2 * Copyright (c) 2010-2023 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.ArrayList;
17 import java.util.List;
18 import java.util.Objects;
19 import java.util.concurrent.atomic.AtomicReference;
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<List<String>> activeScenarios = new AtomicReference<>(new ArrayList<>());
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 = NeeoRecipe.LAUNCH.equalsIgnoreCase(action.getAction());
99 final boolean poweroff = NeeoRecipe.POWEROFF.equalsIgnoreCase(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 && !scenarioKey.isEmpty()) {
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 List<String> oldActiveScenarios;
127 List<String> newActiveScenarios;
130 oldActiveScenarios = this.activeScenarios.get();
131 newActiveScenarios = new ArrayList<>(oldActiveScenarios);
133 if (newActiveScenarios.contains(scenarioKey)) {
137 newActiveScenarios.remove(scenarioKey);
141 newActiveScenarios.add(scenarioKey);
146 } while (!this.activeScenarios.compareAndSet(oldActiveScenarios, newActiveScenarios));
148 refreshScenarioStatus(scenarioKey);
152 * Refresh state of the room - currently only refreshes the active scenarios via {@link #refreshActiveScenarios()}
154 public void refreshState() {
155 refreshActiveScenarios();
159 * Refresh the recipe name
161 * @param recipeKey the non-empty recipe key
163 public void refreshRecipeName(String recipeKey) {
164 NeeoUtil.requireNotEmpty(recipeKey, "recipeKey cannot be empty");
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()));
174 * Refresh the recipe type
176 * @param recipeKey the non-empty recipe key
178 public void refreshRecipeType(String recipeKey) {
179 NeeoUtil.requireNotEmpty(recipeKey, "recipeKey cannot be empty");
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()));
189 * Refresh whether the recipe is enabled
191 * @param recipeKey the non-null recipe key
193 public void refreshRecipeEnabled(String recipeKey) {
194 NeeoUtil.requireNotEmpty(recipeKey, "recipeKey cannot be empty");
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()));
204 * Refresh the recipe status.
206 * @param recipeKey the non-null recipe key
208 public void refreshRecipeStatus(String recipeKey) {
209 NeeoUtil.requireNotEmpty(recipeKey, "recipeKey cannot be empty");
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);
219 * Refresh the scenario name.
221 * @param scenarioKey the non-null scenario key
223 public void refreshScenarioName(String scenarioKey) {
224 NeeoUtil.requireNotEmpty(scenarioKey, "scenarioKey cannot be empty");
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()));
234 * Refresh whether the scenario is configured.
236 * @param scenarioKey the non-null scenario key
238 public void refreshScenarioConfigured(String scenarioKey) {
239 NeeoUtil.requireNotEmpty(scenarioKey, "scenarioKey cannot be empty");
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()));
249 * Refresh the scenario status.
251 * @param scenarioKey the non-null scenario key
253 public void refreshScenarioStatus(String scenarioKey) {
254 NeeoUtil.requireNotEmpty(scenarioKey, "scenarioKey cannot be empty");
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));
265 * Refresh active scenarios
267 private void refreshActiveScenarios() {
268 final NeeoBrainApi api = callback.getApi();
270 logger.debug("API is null [likely bridge is offline]");
273 final List<String> activeScenarios = api.getActiveScenarios();
274 final List<String> oldScenarios = this.activeScenarios.getAndSet(activeScenarios);
276 if (!activeScenarios.equals(oldScenarios)) {
277 activeScenarios.forEach(this::refreshScenarioStatus);
278 oldScenarios.removeIf(activeScenarios::contains);
279 oldScenarios.forEach(this::refreshScenarioStatus);
281 } catch (IOException e) {
282 logger.debug("Exception requesting active scenarios: {}", e.getMessage(), e);
283 // callback.statusChanged(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
289 * Sends the trigger for the current step
291 * @param step a possibly null, possibly empty step to send
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);
300 * Starts the given recipe key
302 * @param recipeKey the non-null recipe key
304 public void startRecipe(String recipeKey) {
305 NeeoUtil.requireNotEmpty(recipeKey, "recipeKey cannot be empty");
307 final NeeoBrainApi api = callback.getApi();
309 logger.debug("API is null [likely bridge is offline] - cannot start recipe: {}", recipeKey);
311 final NeeoRecipe recipe = neeoRoom.getRecipes().getRecipe(recipeKey);
312 final String scenarioKey = recipe == null ? null : recipe.getScenarioKey();
314 if (recipe != null) {
315 if (recipe.isEnabled()) {
316 final boolean isLaunch = NeeoRecipe.LAUNCH.equalsIgnoreCase(recipe.getType());
319 if (isLaunch || scenarioKey == null || scenarioKey.isEmpty()) {
320 handleExecuteResult(scenarioKey, recipeKey, true, api.executeRecipe(roomKey, recipeKey));
322 handleExecuteResult(scenarioKey, recipeKey, false, api.stopScenario(roomKey, scenarioKey));
324 } catch (IOException e) {
325 logger.debug("Exception occurred during execution: {}", e.getMessage(), e);
328 logger.debug("recipe for key {} was not enabled, cannot start or stop", recipeKey);
331 logger.debug("recipe key {} was not found", recipeKey);
337 * Sets the scenario status.
339 * @param scenarioKey the non-null scenario key
340 * @param start whether to start (true) or stop (false) the scenario
342 public void setScenarioStatus(String scenarioKey, boolean start) {
343 NeeoUtil.requireNotEmpty(scenarioKey, "scenarioKey cannot be empty");
345 final NeeoRecipe recipe = neeoRoom.getRecipes().getRecipeByScenarioKey(scenarioKey,
346 start ? NeeoRecipe.LAUNCH : NeeoRecipe.POWEROFF);
347 final String recipeKey = recipe == null ? null : recipe.getKey();
349 if (recipe != null && recipeKey != null && !recipeKey.isEmpty()) {
350 if (recipe.isEnabled()) {
351 startRecipe(recipeKey);
353 logger.debug("Recipe ({}) found for scenario {} but was not enabled", recipe.getKey(), scenarioKey);
356 logger.debug("No recipe found for scenario {} to start ({})", scenarioKey, start);
361 * Handle the {@link ExecuteResult} from a call
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)
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");
374 if (scenarioKey != null && !scenarioKey.isEmpty()) {
375 callback.scheduleTask(() -> {
376 processScenarioChange(scenarioKey, launch);
380 for (final ExecuteStep step : result.getSteps()) {
381 callback.scheduleTask(() -> {
382 sendCurrentStepTrigger(step.getText());
384 nextStep += step.getDuration();
387 callback.scheduleTask(() -> {
388 sendCurrentStepTrigger(null);
389 refreshRecipeStatus(recipeKey);