]> git.basschouten.com Git - openhab-addons.git/blob
f16c04596fa3d8a8852311feec60c1fe1b851141
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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 java.io.IOException;
16 import java.lang.reflect.Type;
17 import java.util.ArrayList;
18 import java.util.HashMap;
19 import java.util.HashSet;
20 import java.util.List;
21 import java.util.Map;
22 import java.util.Objects;
23 import java.util.Set;
24
25 import org.openhab.binding.loxone.internal.LxServerHandlerApi;
26 import org.openhab.binding.loxone.internal.types.LxCategory;
27 import org.openhab.binding.loxone.internal.types.LxConfig;
28 import org.openhab.binding.loxone.internal.types.LxContainer;
29 import org.openhab.binding.loxone.internal.types.LxState;
30 import org.openhab.binding.loxone.internal.types.LxUuid;
31 import org.openhab.core.library.types.DecimalType;
32 import org.openhab.core.library.types.OnOffType;
33 import org.openhab.core.library.types.StringType;
34 import org.openhab.core.thing.Channel;
35 import org.openhab.core.thing.ChannelUID;
36 import org.openhab.core.thing.binding.builder.ChannelBuilder;
37 import org.openhab.core.thing.type.ChannelTypeUID;
38 import org.openhab.core.types.Command;
39 import org.openhab.core.types.State;
40 import org.openhab.core.types.StateDescriptionFragment;
41 import org.openhab.core.types.UnDefType;
42 import org.slf4j.Logger;
43 import org.slf4j.LoggerFactory;
44
45 import com.google.gson.Gson;
46 import com.google.gson.JsonArray;
47 import com.google.gson.JsonDeserializationContext;
48 import com.google.gson.JsonDeserializer;
49 import com.google.gson.JsonElement;
50 import com.google.gson.JsonObject;
51 import com.google.gson.JsonParseException;
52 import com.google.gson.reflect.TypeToken;
53
54 /**
55  * A control of Loxone Miniserver.
56  * <p>
57  * It represents a control object on the Miniserver. Controls can represent an input, functional block or an output of
58  * the Miniserver, that is marked as visible in the Loxone UI. Controls can belong to a {@link LxContainer} room and a
59  * {@link LxCategory} category.
60  *
61  * @author Pawel Pieczul - initial contribution
62  *
63  */
64 public class LxControl {
65
66     /**
67      * This class contains static configuration of the control and is used to make the fields transparent to the child
68      * classes that implement specific controls.
69      *
70      * @author Pawel Pieczul - initial contribution
71      *
72      */
73     public static class LxControlConfig {
74         private final LxServerHandlerApi thingHandler;
75         private final LxContainer room;
76         private final LxCategory category;
77
78         LxControlConfig(LxControlConfig config) {
79             this(config.thingHandler, config.room, config.category);
80         }
81
82         public LxControlConfig(LxServerHandlerApi thingHandler, LxContainer room, LxCategory category) {
83             this.room = room;
84             this.category = category;
85             this.thingHandler = thingHandler;
86         }
87     }
88
89     /**
90      * This class is used to instantiate a particular control object by the {@link LxControlFactory}
91      *
92      * @author Pawel Pieczul - initial contribution
93      *
94      */
95     abstract static class LxControlInstance {
96         /**
97          * Creates an instance of a particular control class.
98          *
99          * @param uuid UUID of the control object to be created
100          * @return a newly created control object
101          */
102         abstract LxControl create(LxUuid uuid);
103
104         /**
105          * Return a type name for this control.
106          *
107          * @return type name (as used on the Miniserver)
108          */
109         abstract String getType();
110     }
111
112     /**
113      * This class describes additional parameters of a control received from the Miniserver and is used during JSON
114      * deserialization.
115      *
116      * @author Pawel Pieczul - initial contribution
117      *
118      */
119     class LxControlDetails {
120         Double min;
121         Double max;
122         Double step;
123         String format;
124         String actualFormat;
125         String totalFormat;
126         Boolean increaseOnly;
127         String allOff;
128         String url;
129         String urlHd;
130         Map<String, String> outputs;
131         Boolean presenceConnected;
132         Integer connectedInputs;
133     }
134
135     /**
136      * A callback that should be implemented by child classes to process received commands. This callback can be
137      * provided for each channel created by the controls.
138      *
139      * @author Pawel Pieczul - initial contribution
140      *
141      */
142     @FunctionalInterface
143     interface CommandCallback {
144         abstract void handleCommand(Command cmd) throws IOException;
145     }
146
147     /**
148      * A callback that should be implemented by child classes to return current channel state. This callback can be
149      * provided for each channel created by the controls.
150      *
151      * @author Pawel Pieczul - initial contribution
152      *
153      */
154     @FunctionalInterface
155     interface StateCallback {
156         abstract State getChannelState();
157     }
158
159     /**
160      * A set of callbacks registered per each channel by the child classes.
161      *
162      * @author Pawel Pieczul - initial contribution
163      *
164      */
165     private class Callbacks {
166         private CommandCallback commandCallback;
167         private StateCallback stateCallback;
168
169         private Callbacks(CommandCallback cC, StateCallback sC) {
170             commandCallback = cC;
171             stateCallback = sC;
172         }
173     }
174
175     /*
176      * Parameters parsed from the JSON configuration file during deserialization
177      */
178     LxUuid uuid;
179     LxControlDetails details;
180     private String name;
181     private LxUuid roomUuid;
182     private Boolean isSecured;
183     private LxUuid categoryUuid;
184     private Map<LxUuid, LxControl> subControls;
185     private final Map<String, LxState> states;
186
187     /*
188      * Parameters set when finalizing {@link LxConfig} object setup. They will be null right after constructing object.
189      */
190     transient String defaultChannelLabel;
191     private transient LxControlConfig config;
192
193     /*
194      * Parameters set when object is connected to the openHAB by the binding handler
195      */
196     final transient Set<String> tags = new HashSet<>();
197     private final transient List<Channel> channels = new ArrayList<>();
198     private final transient Map<ChannelUID, Callbacks> callbacks = new HashMap<>();
199
200     private final transient Logger logger;
201     private int numberOfChannels = 0;
202
203     /*
204      * JSON deserialization routine, called during parsing configuration by the GSON library
205      */
206     public static final JsonDeserializer<LxControl> DESERIALIZER = new JsonDeserializer<LxControl>() {
207         @Override
208         public LxControl deserialize(JsonElement json, Type type, JsonDeserializationContext context)
209                 throws JsonParseException {
210             JsonObject parent = json.getAsJsonObject();
211             String controlName = LxConfig.deserializeString(parent, "name");
212             String controlType = LxConfig.deserializeString(parent, "type");
213             LxUuid uuid = LxConfig.deserializeObject(parent, "uuidAction", LxUuid.class, context);
214             if (controlName == null || controlType == null || uuid == null) {
215                 throw new JsonParseException("Control name/type/uuid is null.");
216             }
217             LxControl control = LxControlFactory.createControl(uuid, controlType);
218             if (control == null) {
219                 return null;
220             }
221             control.name = controlName;
222             control.isSecured = LxConfig.deserializeObject(parent, "isSecured", Boolean.class, context);
223             control.roomUuid = LxConfig.deserializeObject(parent, "room", LxUuid.class, context);
224             control.categoryUuid = LxConfig.deserializeObject(parent, "cat", LxUuid.class, context);
225             control.details = LxConfig.deserializeObject(parent, "details", LxControlDetails.class, context);
226             control.subControls = LxConfig.deserializeObject(parent, "subControls",
227                     new TypeToken<Map<LxUuid, LxControl>>() {
228                     }.getType(), context);
229
230             JsonObject states = parent.getAsJsonObject("states");
231             if (states != null) {
232                 states.entrySet().forEach(entry -> {
233                     // temperature state of intelligent home controller object is the only
234                     // one that has state represented as an array, as this is not implemented
235                     // yet, we will skip this state
236                     JsonElement element = entry.getValue();
237                     if (element != null && !(element instanceof JsonArray)) {
238                         String value = element.getAsString();
239                         if (value != null) {
240                             String name = entry.getKey().toLowerCase();
241                             control.states.put(name, new LxState(new LxUuid(value), name, control));
242                         }
243                     }
244                 });
245             }
246             return control;
247         }
248     };
249
250     LxControl(LxUuid uuid) {
251         logger = LoggerFactory.getLogger(LxControl.class);
252         this.uuid = uuid;
253         states = new HashMap<>();
254     }
255
256     /**
257      * A method that executes commands by the control. It delegates command execution to a registered callback method.
258      *
259      * @param channelId channel Id for the command
260      * @param command value of the command to perform
261      * @throws IOException in case of communication error with the Miniserver
262      */
263     public final void handleCommand(ChannelUID channelId, Command command) throws IOException {
264         Callbacks c = callbacks.get(channelId);
265         if (c != null && c.commandCallback != null) {
266             c.commandCallback.handleCommand(command);
267         } else {
268             logger.debug("Control UUID={} has no command handler", getUuid());
269         }
270     }
271
272     /**
273      * Provides actual state value for the specified channel. It delegates execution to a registered callback method.
274      *
275      * @param channelId channel ID to get state for
276      * @return state if the channel value or null if no value available
277      */
278     public final State getChannelState(ChannelUID channelId) {
279         Callbacks c = callbacks.get(channelId);
280         if (c != null && c.stateCallback != null) {
281             try {
282                 return c.stateCallback.getChannelState();
283             } catch (NumberFormatException e) {
284                 return UnDefType.UNDEF;
285             }
286         }
287         return null;
288     }
289
290     /**
291      * Obtain control's name
292      *
293      * @return Human readable name of control
294      */
295     public String getName() {
296         return name;
297     }
298
299     /**
300      * Get control's UUID as defined on the Miniserver
301      *
302      * @return UUID of the control
303      */
304     public LxUuid getUuid() {
305         return uuid;
306     }
307
308     /**
309      * Get subcontrols of this control
310      *
311      * @return subcontrols of the control
312      */
313     public Map<LxUuid, LxControl> getSubControls() {
314         return subControls;
315     }
316
317     /**
318      * Get control's channels
319      *
320      * @return channels
321      */
322     public List<Channel> getChannels() {
323         return channels;
324     }
325
326     /**
327      * Get control's and its subcontrols' channels
328      *
329      * @return channels
330      */
331     public List<Channel> getChannelsWithSubcontrols() {
332         final List<Channel> list = new ArrayList<>(channels);
333         subControls.values().forEach(c -> list.addAll(c.getChannelsWithSubcontrols()));
334         return list;
335     }
336
337     /**
338      * Get control's Miniserver states
339      *
340      * @return control's Miniserver states
341      */
342     public Map<String, LxState> getStates() {
343         return states;
344     }
345
346     /**
347      * Gets information is password is required to operate on this control object
348      *
349      * @return true is control is secured
350      */
351     public Boolean isSecured() {
352         return isSecured != null && isSecured;
353     }
354
355     /**
356      * Compare UUID's of two controls -
357      *
358      * @param object Object to compare with
359      * @return true if UUID of two objects are equal
360      */
361     @Override
362     public boolean equals(Object object) {
363         if (this == object) {
364             return true;
365         }
366         if (object == null) {
367             return false;
368         }
369         if (object.getClass() != getClass()) {
370             return false;
371         }
372         LxControl c = (LxControl) object;
373         return Objects.equals(c.uuid, uuid);
374     }
375
376     /**
377      * Hash code of the control is equal to its UUID's hash code
378      */
379     @Override
380     public int hashCode() {
381         return uuid.hashCode();
382     }
383
384     /**
385      * Initialize Miniserver's control in runtime. Each class that implements {@link LxControl} should override this
386      * method and call it as a first step in the overridden implementation. Then it should add all runtime data, like
387      * channels and any fields that derive their value from the parsed JSON configuration.
388      * Before this method is called during configuration parsing, the control object must not be used.
389      *
390      * @param configToSet control's configuration
391      */
392     public void initialize(LxControlConfig configToSet) {
393         logger.debug("Initializing LxControl: {}", uuid);
394
395         if (config != null) {
396             logger.error("Error, attempt to initialize control that is already initialized: {}", uuid);
397             return;
398         }
399         config = configToSet;
400
401         if (subControls == null) {
402             subControls = new HashMap<>();
403         } else {
404             subControls.values().removeIf(Objects::isNull);
405         }
406
407         if (config.room != null) {
408             config.room.addControl(this);
409         }
410
411         if (config.category != null) {
412             config.category.addControl(this);
413         }
414
415         String label = getLabel();
416         if (label == null) {
417             // Each control on a Miniserver must have a name defined, but in case this is a subject
418             // of some malicious data attack, we'll prevent null pointer exception
419             label = "Undefined name";
420         }
421         String roomName = config.room != null ? config.room.getName() : null;
422         if (roomName != null) {
423             label = roomName + " / " + label;
424         }
425         defaultChannelLabel = label;
426
427         // Propagate to all subcontrols of this control object
428         subControls.values().forEach(c -> c.initialize(config));
429     }
430
431     /**
432      * This method will be called from {@link LxState}, when Miniserver state value is updated.
433      * By default it will query all channels of the control and update their state accordingly.
434      * This method will not handle channel state descriptions, as they must be prepared individually.
435      * It can be overridden in child class to handle particular states differently.
436      *
437      * @param state changed Miniserver state or null if not specified (any/all)
438      */
439     public void onStateChange(LxState state) {
440         if (config == null) {
441             logger.error("Attempt to change state with not finalized configuration!: {}", state.getUuid());
442         } else {
443             channels.forEach(channel -> {
444                 ChannelUID channelId = channel.getUID();
445                 State channelState = getChannelState(channelId);
446                 if (channelState != null) {
447                     config.thingHandler.setChannelState(channelId, channelState);
448                 }
449             });
450         }
451     }
452
453     /**
454      * Gets room UUID after it was deserialized by GSON
455      *
456      * @return room UUID
457      */
458     public LxUuid getRoomUuid() {
459         return roomUuid;
460     }
461
462     /**
463      * Gets category UUID after it was deserialized by GSON
464      *
465      * @return category UUID
466      */
467     public LxUuid getCategoryUuid() {
468         return categoryUuid;
469     }
470
471     /**
472      * Gets a GSON object for reuse
473      *
474      * @return GSON object
475      */
476     Gson getGson() {
477         if (config == null) {
478             logger.error("Attempt to get GSON from not finalized configuration!");
479             return null;
480         }
481         return config.thingHandler.getGson();
482     }
483
484     /**
485      * Adds a new control in the framework. Called when a control is dynamically created based on some control's state
486      * changes from the Miniserver.
487      *
488      * @param control a new control to be created
489      */
490     static void addControl(LxControl control) {
491         control.config.thingHandler.addControl(control);
492     }
493
494     /**
495      * Removes a control from the framework. Called when a control is dynamically deleted based on some control's state
496      * changes from the Miniserver.
497      *
498      * @param control a control to be removed
499      */
500     static void removeControl(LxControl control) {
501         control.config.thingHandler.removeControl(control);
502         control.dispose();
503     }
504
505     /**
506      * Gets control's configuration
507      *
508      * @return configuration
509      */
510     LxControlConfig getConfig() {
511         return config;
512     }
513
514     /**
515      * Get control's room.
516      *
517      * @return control's room object
518      */
519     LxContainer getRoom() {
520         return config.room;
521     }
522
523     /**
524      * Get control's category.
525      *
526      * @return control's category object
527      */
528     LxCategory getCategory() {
529         return config.category;
530     }
531
532     /**
533      * Changes the channel state in the framework.
534      *
535      * @param id channel ID
536      * @param state new state value
537      */
538     void setChannelState(ChannelUID id, State state) {
539         if (config == null) {
540             logger.error("Attempt to set channel state with not finalized configuration!: {}", id);
541         } else {
542             if (state != null) {
543                 config.thingHandler.setChannelState(id, state);
544             }
545         }
546     }
547
548     /**
549      * Returns control label that will be used for building channel name. This allows for customizing the label per
550      * control by overriding this method, but keeping {@link LxControl#getName()} intact.
551      *
552      * @return control channel label
553      */
554     String getLabel() {
555         return name;
556     }
557
558     /**
559      * Gets value of a state object of given name, if exists
560      *
561      * @param name name of state object
562      * @return state object's value
563      */
564     Double getStateDoubleValue(String name) {
565         LxState state = states.get(name);
566         if (state != null) {
567             Object value = state.getStateValue();
568             if (value instanceof Double) {
569                 return (Double) value;
570             }
571         }
572         return null;
573     }
574
575     /**
576      * Gets value of a state object of given name, if exists, and converts it to decimal type value.
577      *
578      * @param name state name
579      * @return state value
580      */
581     State getStateDecimalValue(String name) {
582         Double value = getStateDoubleValue(name);
583         if (value != null) {
584             return new DecimalType(value);
585         }
586         return null;
587     }
588
589     /**
590      * Gets text value of a state object of given name, if exists
591      *
592      * @param name name of state object
593      * @return state object's text value
594      */
595     String getStateTextValue(String name) {
596         LxState state = states.get(name);
597         if (state != null) {
598             Object value = state.getStateValue();
599             if (value instanceof String) {
600                 return (String) value;
601             }
602         }
603         return null;
604     }
605
606     /**
607      * Gets text value of a state object of given name, if exists and converts it to string type
608      *
609      * @param name name of state object
610      * @return state object's text value
611      */
612     State getStateStringValue(String name) {
613         String value = getStateTextValue(name);
614         if (value != null) {
615             return new StringType(value);
616         }
617         return null;
618     }
619
620     /**
621      * Gets double value of a state object of given name, if exists and converts it to switch type
622      *
623      * @param name name of state object
624      * @return state object's text value
625      */
626     State getStateOnOffValue(String name) {
627         Double value = getStateDoubleValue(name);
628         if (value != null) {
629             if (value == 1.0) {
630                 return OnOffType.ON;
631             }
632             return OnOffType.OFF;
633         }
634         return null;
635     }
636
637     /**
638      * Create a new channel and add it to the control. Channel ID is assigned automatically in the order of calls to
639      * this method, see (@link LxControl#getChannelId}.
640      *
641      * @param itemType item type for the channel
642      * @param typeId channel type ID for the channel
643      * @param channelLabel channel label
644      * @param channelDescription channel description
645      * @param tags tags for the channel or null if no tags needed
646      * @param commandCallback {@link LxControl} child class method that will be called when command is received
647      * @param stateCallback {@link LxControl} child class method that will be called to get state value
648      * @return channel ID of the added channel (can be used to later set state description to it)
649      */
650     ChannelUID addChannel(String itemType, ChannelTypeUID typeId, String channelLabel, String channelDescription,
651             Set<String> tags, CommandCallback commandCallback, StateCallback stateCallback) {
652         if (channelLabel == null || channelDescription == null) {
653             logger.error("Attempt to add channel with not finalized configuration!: {}", channelLabel);
654             return null;
655         }
656         ChannelUID channelId = getChannelId(numberOfChannels++);
657         ChannelBuilder builder = ChannelBuilder.create(channelId, itemType).withType(typeId).withLabel(channelLabel)
658                 .withDescription(channelDescription + " : " + channelLabel);
659         if (tags != null) {
660             builder.withDefaultTags(tags);
661         }
662         channels.add(builder.build());
663         if (commandCallback != null || stateCallback != null) {
664             callbacks.put(channelId, new Callbacks(commandCallback, stateCallback));
665         }
666         return channelId;
667     }
668
669     /**
670      * Adds a new {@link StateDescriptionFragment} for a channel that has multiple options to select from or a custom
671      * format string.
672      *
673      * @param channelId channel ID to add the description for
674      * @param descriptionFragment channel state description fragment
675      */
676     void addChannelStateDescriptionFragment(ChannelUID channelId, StateDescriptionFragment descriptionFragment) {
677         if (config == null) {
678             logger.error("Attempt to set channel state description with not finalized configuration!: {}", channelId);
679         } else {
680             if (channelId != null && descriptionFragment != null) {
681                 config.thingHandler.setChannelStateDescription(channelId, descriptionFragment.toStateDescription());
682             }
683         }
684     }
685
686     /**
687      * Sends an action command to the Miniserver using active socket connection
688      *
689      * @param action string with action command
690      * @throws IOException when communication error with Miniserver occurs
691      */
692     void sendAction(String action) throws IOException {
693         if (config == null) {
694             logger.error("Attempt to send command with not finalized configuration!: {}", action);
695         } else {
696             config.thingHandler.sendAction(uuid, action);
697         }
698     }
699
700     /**
701      * Remove all channels from the control. This method is used by child classes that may decide to stop exposing any
702      * channels, for example by {@link LxControlMood}, which is based on {@link LxControlSwitch}, but sometime does not
703      * expose anything to the user.
704      */
705     void removeAllChannels() {
706         channels.clear();
707         callbacks.clear();
708     }
709
710     /**
711      * Call when control is no more needed - unlink it from containers
712      */
713     private void dispose() {
714         if (config.room != null) {
715             config.room.removeControl(this);
716         }
717         if (config.category != null) {
718             config.category.removeControl(this);
719         }
720         subControls.values().forEach(control -> control.dispose());
721     }
722
723     /**
724      * Build channel ID for the control, based on control's UUID, thing's UUID and index of the channel for the control
725      *
726      * @param index index of a channel within control (0 for primary channel) all indexes greater than 0 will have
727      *            -index added to the channel ID
728      * @return channel ID for the control and index
729      */
730     private ChannelUID getChannelId(int index) {
731         if (config == null) {
732             logger.error("Attempt to get control's channel ID with not finalized configuration!: {}", index);
733             return null;
734         }
735         String controlId = uuid.toString();
736         if (index > 0) {
737             controlId += "-" + index;
738         }
739         return new ChannelUID(config.thingHandler.getThingId(), controlId);
740     }
741 }