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), recipe.isEnabled() ? OnOffType.ON : OnOffType.OFF);
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),
245 scenario.isConfigured() ? OnOffType.ON : OnOffType.OFF);
250 * Refresh the scenario status.
252 * @param scenarioKey the non-null scenario key
254 public void refreshScenarioStatus(String scenarioKey) {
255 NeeoUtil.requireNotEmpty(scenarioKey, "scenarioKey cannot be empty");
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));
266 * Refresh active scenarios
268 private void refreshActiveScenarios() {
269 final NeeoBrainApi api = callback.getApi();
271 logger.debug("API is null [likely bridge is offline]");
274 final List<String> activeScenarios = api.getActiveScenarios();
275 final List<String> oldScenarios = this.activeScenarios.getAndSet(activeScenarios);
277 if (!activeScenarios.equals(oldScenarios)) {
278 activeScenarios.forEach(this::refreshScenarioStatus);
279 oldScenarios.removeIf(activeScenarios::contains);
280 oldScenarios.forEach(this::refreshScenarioStatus);
282 } catch (IOException e) {
283 logger.debug("Exception requesting active scenarios: {}", e.getMessage(), e);
284 // callback.statusChanged(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
290 * Sends the trigger for the current step
292 * @param step a possibly null, possibly empty step to send
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);
301 * Starts the given recipe key
303 * @param recipeKey the non-null recipe key
305 public void startRecipe(String recipeKey) {
306 NeeoUtil.requireNotEmpty(recipeKey, "recipeKey cannot be empty");
308 final NeeoBrainApi api = callback.getApi();
310 logger.debug("API is null [likely bridge is offline] - cannot start recipe: {}", recipeKey);
312 final NeeoRecipe recipe = neeoRoom.getRecipes().getRecipe(recipeKey);
313 final String scenarioKey = recipe == null ? null : recipe.getScenarioKey();
315 if (recipe != null) {
316 if (recipe.isEnabled()) {
317 final boolean isLaunch = NeeoRecipe.LAUNCH.equalsIgnoreCase(recipe.getType());
320 if (isLaunch || scenarioKey == null || scenarioKey.isEmpty()) {
321 handleExecuteResult(scenarioKey, recipeKey, true, api.executeRecipe(roomKey, recipeKey));
323 handleExecuteResult(scenarioKey, recipeKey, false, api.stopScenario(roomKey, scenarioKey));
325 } catch (IOException e) {
326 logger.debug("Exception occurred during execution: {}", e.getMessage(), e);
329 logger.debug("recipe for key {} was not enabled, cannot start or stop", recipeKey);
332 logger.debug("recipe key {} was not found", recipeKey);
338 * Sets the scenario status.
340 * @param scenarioKey the non-null scenario key
341 * @param start whether to start (true) or stop (false) the scenario
343 public void setScenarioStatus(String scenarioKey, boolean start) {
344 NeeoUtil.requireNotEmpty(scenarioKey, "scenarioKey cannot be empty");
346 final NeeoRecipe recipe = neeoRoom.getRecipes().getRecipeByScenarioKey(scenarioKey,
347 start ? NeeoRecipe.LAUNCH : NeeoRecipe.POWEROFF);
348 final String recipeKey = recipe == null ? null : recipe.getKey();
350 if (recipe != null && recipeKey != null && !recipeKey.isEmpty()) {
351 if (recipe.isEnabled()) {
352 startRecipe(recipeKey);
354 logger.debug("Recipe ({}) found for scenario {} but was not enabled", recipe.getKey(), scenarioKey);
357 logger.debug("No recipe found for scenario {} to start ({})", scenarioKey, start);
362 * Handle the {@link ExecuteResult} from a call
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)
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");
375 if (scenarioKey != null && !scenarioKey.isEmpty()) {
376 callback.scheduleTask(() -> {
377 processScenarioChange(scenarioKey, launch);
381 for (final ExecuteStep step : result.getSteps()) {
382 callback.scheduleTask(() -> {
383 sendCurrentStepTrigger(step.getText());
385 nextStep += step.getDuration();
388 callback.scheduleTask(() -> {
389 sendCurrentStepTrigger(null);
390 refreshRecipeStatus(recipeKey);