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.loxone.internal.controls;
15 import static org.openhab.binding.loxone.internal.LxBindingConstants.*;
17 import java.io.IOException;
18 import java.math.BigDecimal;
19 import java.util.ArrayList;
20 import java.util.Arrays;
21 import java.util.HashMap;
22 import java.util.List;
24 import java.util.stream.Collectors;
26 import org.openhab.binding.loxone.internal.types.LxState;
27 import org.openhab.binding.loxone.internal.types.LxUuid;
28 import org.openhab.core.library.types.DecimalType;
29 import org.openhab.core.library.types.UpDownType;
30 import org.openhab.core.thing.ChannelUID;
31 import org.openhab.core.thing.type.ChannelTypeUID;
32 import org.openhab.core.types.Command;
33 import org.openhab.core.types.State;
34 import org.openhab.core.types.StateDescriptionFragmentBuilder;
35 import org.openhab.core.types.StateOption;
36 import org.openhab.core.types.UnDefType;
37 import org.slf4j.Logger;
38 import org.slf4j.LoggerFactory;
40 import com.google.gson.JsonSyntaxException;
43 * A Light Controller V2 type of control on Loxone Miniserver.
45 * This control has been introduced in Loxone Config 9 in 2017 and it makes the {@link LxControlLightController}
46 * obsolete. Both controls will exist for some time together.
48 * Light controller V2 can have N outputs named AQ1...AQN that can function as Switch, Dimmer, RGB, Lumitech or Smart
49 * Actuator functional blocks. Individual controls will be created for these outputs so they can be operated directly
50 * and independently from the controller.
52 * Controller can also have M moods configured. Each mood defines own subset of outputs and their settings, which will
53 * be engaged when the mood is active. A dedicated switch control object will be created for each mood.
54 * This effectively will allow for mixing various moods by individually enabling/disabling them.
56 * It seems there is no imposed limitation for the number of outputs and moods.
58 * @author Pawel Pieczul - initial contribution
61 class LxControlLightControllerV2 extends LxControl {
63 static class Factory extends LxControlInstance {
65 LxControl create(LxUuid uuid) {
66 return new LxControlLightControllerV2(uuid);
71 return "lightcontrollerv2";
76 * State with list of active moods
78 private static final String STATE_ACTIVE_MOODS_LIST = "activemoods";
80 * State with list of available moods
82 private static final String STATE_MOODS_LIST = "moodlist";
85 * Command string used to set a given mood
87 private static final String CMD_CHANGE_TO_MOOD = "changeTo";
89 * Command string used to change to the next mood
91 private static final String CMD_NEXT_MOOD = "plus";
93 * Command string used to change to the previous mood
95 private static final String CMD_PREVIOUS_MOOD = "minus";
97 * Command string used to add mood to the active moods (mix it in)
99 private static final String CMD_ADD_MOOD = "addMood";
101 * Command string used to remove mood from the active moods (mix it out)
103 private static final String CMD_REMOVE_MOOD = "removeMood";
105 private final transient Logger logger = LoggerFactory.getLogger(LxControlLightControllerV2.class);
107 // Following commands are not supported:
108 // moveFavoriteMood, moveAdditionalMood, moveMood, addToFavoriteMood, removeFromFavoriteMood, learn, delete
110 private Map<Integer, LxControlMood> moodList = new HashMap<>();
111 private List<Integer> activeMoods = new ArrayList<>();
112 private ChannelUID channelId;
114 private LxControlLightControllerV2(LxUuid uuid) {
119 public void initialize(LxControlConfig config) {
120 super.initialize(config);
122 // add only channel, state description will be added later when a control state update message is received
123 channelId = addChannel("Number", new ChannelTypeUID(BINDING_ID, MINISERVER_CHANNEL_TYPE_LIGHT_CTRL),
124 defaultChannelLabel, "Light controller V2", tags, this::handleCommands, this::getChannelState);
127 private void handleCommands(Command command) throws IOException {
128 if (command instanceof UpDownType) {
129 if ((UpDownType) command == UpDownType.UP) {
130 sendAction(CMD_NEXT_MOOD);
132 sendAction(CMD_PREVIOUS_MOOD);
134 } else if (command instanceof DecimalType) {
135 int moodId = ((DecimalType) command).intValue();
136 if (isMoodOk(moodId)) {
137 sendAction(CMD_CHANGE_TO_MOOD + "/" + moodId);
142 private State getChannelState() {
143 // update the single mood channel state
144 if (activeMoods.size() == 1) {
145 Integer id = activeMoods.get(0);
147 return new DecimalType(id);
150 return UnDefType.UNDEF;
154 * Get configured and active moods from a new state value received from the Miniserver
156 * @param state state update from the Miniserver
159 public void onStateChange(LxState state) {
160 String stateName = state.getName();
161 Object value = state.getStateValue();
163 if (STATE_MOODS_LIST.equals(stateName) && value instanceof String) {
164 onMoodsListChange((String) value);
165 } else if (STATE_ACTIVE_MOODS_LIST.equals(stateName) && value instanceof String) {
166 // this state can be received before list of moods, but it contains a valid list of IDs
167 Integer[] array = getGson().fromJson((String) value, Integer[].class);
168 activeMoods = Arrays.asList(array).stream().filter(id -> isMoodOk(id)).collect(Collectors.toList());
169 // update all moods states - this will force update of channels too
170 moodList.values().forEach(mood -> mood.onStateChange(null));
171 // finally we update controller's state based on the active moods list
172 super.onStateChange(state);
174 } catch (JsonSyntaxException e) {
175 logger.debug("Error parsing state {}: {}", stateName, e.getMessage());
180 * Mix a mood into currently active moods.
182 * @param moodId ID of the mood to add
183 * @throws IOException when something went wrong with communication
185 void addMood(Integer moodId) throws IOException {
186 if (isMoodOk(moodId)) {
187 sendAction(CMD_ADD_MOOD + "/" + moodId);
192 * Check if mood is currently active.
194 * @param moodId mood ID to check
195 * @return true if mood is currently active
197 boolean isMoodActive(Integer moodId) {
198 return activeMoods.contains(moodId);
202 * Check if mood ID is within allowed range
204 * @param moodId mood ID to check
205 * @return true if mood ID is within allowed range or range is not configured
207 boolean isMoodOk(Integer moodId) {
208 return moodId != null && moodList.containsKey(moodId);
212 * Mix a mood out of currently active moods.
214 * @param moodId ID of the mood to remove
215 * @throws IOException when something went wrong with communication
217 void removeMood(Integer moodId) throws IOException {
218 if (isMoodOk(moodId)) {
219 sendAction(CMD_REMOVE_MOOD + "/" + moodId);
224 * Handles a change in the list of configured moods
226 * @param text json structure with new moods
227 * @throws JsonSyntaxException error parsing json structure
229 private void onMoodsListChange(String text) throws JsonSyntaxException {
230 LxControlMood[] array = getGson().fromJson(text, LxControlMood[].class);
231 Map<Integer, LxControlMood> newMoodList = new HashMap<>();
232 Integer minMoodId = null;
233 Integer maxMoodId = null;
234 for (LxControlMood mood : array) {
235 Integer id = mood.getId();
236 if (id != null && mood.getName() != null) {
237 logger.debug("Adding mood (id={}, name={})", id, mood.getName());
238 // mood-UUID = <controller-UUID>-M<mood-ID>
239 LxUuid moodUuid = new LxUuid(getUuid().toString() + "-M" + id);
240 mood.initialize(getConfig(), this, moodUuid);
241 newMoodList.put(id, mood);
242 if (minMoodId == null || minMoodId > id) {
245 if (maxMoodId == null || maxMoodId < id) {
251 if (channelId != null && minMoodId != null && maxMoodId != null) {
252 // convert all moods to options list for state description
253 List<StateOption> optionsList = newMoodList.values().stream()
254 .map(mood -> new StateOption(mood.getId().toString(), mood.getName())).collect(Collectors.toList());
255 addChannelStateDescriptionFragment(channelId,
256 StateDescriptionFragmentBuilder.create().withMinimum(new BigDecimal(minMoodId))
257 .withMaximum(new BigDecimal(maxMoodId)).withStep(BigDecimal.ONE).withReadOnly(false)
258 .withOptions(optionsList).build());
261 moodList.values().forEach(m -> removeControl(m));
262 newMoodList.values().forEach(m -> addControl(m));
263 moodList = newMoodList;