]> git.basschouten.com Git - openhab-addons.git/blob
2954b9a21c9002c47044cbf7bae31d0b099db8fc
[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.loxone.internal.controls;
14
15 import static org.openhab.binding.loxone.internal.LxBindingConstants.*;
16
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;
23 import java.util.Map;
24 import java.util.stream.Collectors;
25
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;
39
40 import com.google.gson.JsonSyntaxException;
41
42 /**
43  * A Light Controller V2 type of control on Loxone Miniserver.
44  * <p>
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.
47  * <p>
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.
51  * <p>
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.
55  * <p>
56  * It seems there is no imposed limitation for the number of outputs and moods.
57  *
58  * @author Pawel Pieczul - initial contribution
59  *
60  */
61 class LxControlLightControllerV2 extends LxControl {
62
63     static class Factory extends LxControlInstance {
64         @Override
65         LxControl create(LxUuid uuid) {
66             return new LxControlLightControllerV2(uuid);
67         }
68
69         @Override
70         String getType() {
71             return "lightcontrollerv2";
72         }
73     }
74
75     /**
76      * State with list of active moods
77      */
78     private static final String STATE_ACTIVE_MOODS_LIST = "activemoods";
79     /**
80      * State with list of available moods
81      */
82     private static final String STATE_MOODS_LIST = "moodlist";
83
84     /**
85      * Command string used to set a given mood
86      */
87     private static final String CMD_CHANGE_TO_MOOD = "changeTo";
88     /**
89      * Command string used to change to the next mood
90      */
91     private static final String CMD_NEXT_MOOD = "plus";
92     /**
93      * Command string used to change to the previous mood
94      */
95     private static final String CMD_PREVIOUS_MOOD = "minus";
96     /**
97      * Command string used to add mood to the active moods (mix it in)
98      */
99     private static final String CMD_ADD_MOOD = "addMood";
100     /**
101      * Command string used to remove mood from the active moods (mix it out)
102      */
103     private static final String CMD_REMOVE_MOOD = "removeMood";
104
105     private final transient Logger logger = LoggerFactory.getLogger(LxControlLightControllerV2.class);
106
107     // Following commands are not supported:
108     // moveFavoriteMood, moveAdditionalMood, moveMood, addToFavoriteMood, removeFromFavoriteMood, learn, delete
109
110     private Map<Integer, LxControlMood> moodList = new HashMap<>();
111     private List<Integer> activeMoods = new ArrayList<>();
112     private ChannelUID channelId;
113
114     private LxControlLightControllerV2(LxUuid uuid) {
115         super(uuid);
116     }
117
118     @Override
119     public void initialize(LxControlConfig config) {
120         super.initialize(config);
121         tags.add("Scene");
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);
125     }
126
127     private void handleCommands(Command command) throws IOException {
128         if (command instanceof UpDownType) {
129             if ((UpDownType) command == UpDownType.UP) {
130                 sendAction(CMD_NEXT_MOOD);
131             } else {
132                 sendAction(CMD_PREVIOUS_MOOD);
133             }
134         } else if (command instanceof DecimalType) {
135             int moodId = ((DecimalType) command).intValue();
136             if (isMoodOk(moodId)) {
137                 sendAction(CMD_CHANGE_TO_MOOD + "/" + moodId);
138             }
139         }
140     }
141
142     private State getChannelState() {
143         // update the single mood channel state
144         if (activeMoods.size() == 1) {
145             Integer id = activeMoods.get(0);
146             if (isMoodOk(id)) {
147                 return new DecimalType(id);
148             }
149         }
150         return UnDefType.UNDEF;
151     }
152
153     /**
154      * Get configured and active moods from a new state value received from the Miniserver
155      *
156      * @param state state update from the Miniserver
157      */
158     @Override
159     public void onStateChange(LxState state) {
160         String stateName = state.getName();
161         Object value = state.getStateValue();
162         try {
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);
173             }
174         } catch (JsonSyntaxException e) {
175             logger.debug("Error parsing state {}: {}", stateName, e.getMessage());
176         }
177     }
178
179     /**
180      * Mix a mood into currently active moods.
181      *
182      * @param moodId ID of the mood to add
183      * @throws IOException when something went wrong with communication
184      */
185     void addMood(Integer moodId) throws IOException {
186         if (isMoodOk(moodId)) {
187             sendAction(CMD_ADD_MOOD + "/" + moodId);
188         }
189     }
190
191     /**
192      * Check if mood is currently active.
193      *
194      * @param moodId mood ID to check
195      * @return true if mood is currently active
196      */
197     boolean isMoodActive(Integer moodId) {
198         return activeMoods.contains(moodId);
199     }
200
201     /**
202      * Check if mood ID is within allowed range
203      *
204      * @param moodId mood ID to check
205      * @return true if mood ID is within allowed range or range is not configured
206      */
207     boolean isMoodOk(Integer moodId) {
208         return moodId != null && moodList.containsKey(moodId);
209     }
210
211     /**
212      * Mix a mood out of currently active moods.
213      *
214      * @param moodId ID of the mood to remove
215      * @throws IOException when something went wrong with communication
216      */
217     void removeMood(Integer moodId) throws IOException {
218         if (isMoodOk(moodId)) {
219             sendAction(CMD_REMOVE_MOOD + "/" + moodId);
220         }
221     }
222
223     /**
224      * Handles a change in the list of configured moods
225      *
226      * @param text json structure with new moods
227      * @throws JsonSyntaxException error parsing json structure
228      */
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) {
243                     minMoodId = id;
244                 }
245                 if (maxMoodId == null || maxMoodId < id) {
246                     maxMoodId = id;
247                 }
248             }
249         }
250
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());
259         }
260
261         moodList.values().forEach(m -> removeControl(m));
262         newMoodList.values().forEach(m -> addControl(m));
263         moodList = newMoodList;
264     }
265 }