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