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 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;
22 import java.util.Objects;
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;
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;
56 * A control of Loxone Miniserver.
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.
62 * @author Pawel Pieczul - initial contribution
65 public class LxControl {
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.
71 * @author Pawel Pieczul - initial contribution
74 public static class LxControlConfig {
75 private final LxServerHandlerApi thingHandler;
76 private final LxContainer room;
77 private final LxCategory category;
79 LxControlConfig(LxControlConfig config) {
80 this(config.thingHandler, config.room, config.category);
83 public LxControlConfig(LxServerHandlerApi thingHandler, LxContainer room, LxCategory category) {
85 this.category = category;
86 this.thingHandler = thingHandler;
91 * This class is used to instantiate a particular control object by the {@link LxControlFactory}
93 * @author Pawel Pieczul - initial contribution
96 abstract static class LxControlInstance {
98 * Creates an instance of a particular control class.
100 * @param uuid UUID of the control object to be created
101 * @return a newly created control object
103 abstract LxControl create(LxUuid uuid);
106 * Return a type name for this control.
108 * @return type name (as used on the Miniserver)
110 abstract String getType();
114 * This class describes additional parameters of a control received from the Miniserver and is used during JSON
117 * @author Pawel Pieczul - initial contribution
120 class LxControlDetails {
127 Boolean increaseOnly;
131 Map<String, String> outputs;
132 Boolean presenceConnected;
133 Integer connectedInputs;
134 Boolean hasVaporizer;
135 Boolean hasDoorSensor;
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.
142 * @author Pawel Pieczul - initial contribution
146 interface CommandCallback {
147 abstract void handleCommand(Command cmd) throws IOException;
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.
154 * @author Pawel Pieczul - initial contribution
158 interface StateCallback {
159 abstract State getChannelState();
163 * A set of callbacks registered per each channel by the child classes.
165 * @author Pawel Pieczul - initial contribution
168 private class Callbacks {
169 private CommandCallback commandCallback;
170 private StateCallback stateCallback;
172 private Callbacks(CommandCallback cC, StateCallback sC) {
173 commandCallback = cC;
179 * Parameters parsed from the JSON configuration file during deserialization
182 LxControlDetails details;
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;
191 * Parameters set when finalizing {@link LxConfig} object setup. They will be null right after constructing object.
193 transient String defaultChannelLabel;
194 private transient LxControlConfig config;
197 * Parameters set when object is connected to the openHAB by the binding handler
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<>();
203 private final transient Logger logger;
204 private int numberOfChannels = 0;
207 * JSON deserialization routine, called during parsing configuration by the GSON library
209 public static final JsonDeserializer<LxControl> DESERIALIZER = new JsonDeserializer<LxControl>() {
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.");
220 LxControl control = LxControlFactory.createControl(uuid, controlType);
221 if (control == null) {
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);
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();
243 String name = entry.getKey().toLowerCase();
244 control.states.put(name, new LxState(new LxUuid(value), name, control));
253 LxControl(LxUuid uuid) {
254 logger = LoggerFactory.getLogger(LxControl.class);
256 states = new HashMap<>();
260 * A method that executes commands by the control. It delegates command execution to a registered callback method.
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
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);
271 logger.debug("Control UUID={} has no command handler", getUuid());
276 * Provides actual state value for the specified channel. It delegates execution to a registered callback method.
278 * @param channelId channel ID to get state for
279 * @return state if the channel value or null if no value available
281 public final State getChannelState(ChannelUID channelId) {
282 Callbacks c = callbacks.get(channelId);
283 if (c != null && c.stateCallback != null) {
285 return c.stateCallback.getChannelState();
286 } catch (NumberFormatException e) {
287 return UnDefType.UNDEF;
294 * Obtain control's name
296 * @return Human readable name of control
298 public String getName() {
303 * Get control's UUID as defined on the Miniserver
305 * @return UUID of the control
307 public LxUuid getUuid() {
312 * Get subcontrols of this control
314 * @return subcontrols of the control
316 public Map<LxUuid, LxControl> getSubControls() {
321 * Get control's channels
325 public List<Channel> getChannels() {
330 * Get control's and its subcontrols' channels
334 public List<Channel> getChannelsWithSubcontrols() {
335 final List<Channel> list = new ArrayList<>(channels);
336 subControls.values().forEach(c -> list.addAll(c.getChannelsWithSubcontrols()));
341 * Get control's Miniserver states
343 * @return control's Miniserver states
345 public Map<String, LxState> getStates() {
350 * Gets information is password is required to operate on this control object
352 * @return true is control is secured
354 public Boolean isSecured() {
355 return isSecured != null && isSecured;
359 * Compare UUID's of two controls -
361 * @param object Object to compare with
362 * @return true if UUID of two objects are equal
365 public boolean equals(Object object) {
366 if (this == object) {
369 if (object == null) {
372 if (object.getClass() != getClass()) {
375 LxControl c = (LxControl) object;
376 return Objects.equals(c.uuid, uuid);
380 * Hash code of the control is equal to its UUID's hash code
383 public int hashCode() {
384 return uuid.hashCode();
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.
393 * @param configToSet control's configuration
395 public void initialize(LxControlConfig configToSet) {
396 logger.debug("Initializing LxControl: {}", uuid);
398 if (config != null) {
399 logger.error("Error, attempt to initialize control that is already initialized: {}", uuid);
402 config = configToSet;
404 if (subControls == null) {
405 subControls = new HashMap<>();
407 subControls.values().removeIf(Objects::isNull);
410 if (config.room != null) {
411 config.room.addControl(this);
414 if (config.category != null) {
415 config.category.addControl(this);
418 String label = getLabel();
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";
424 String roomName = config.room != null ? config.room.getName() : null;
425 if (roomName != null) {
426 label = roomName + " / " + label;
428 defaultChannelLabel = label;
430 // Propagate to all subcontrols of this control object
431 subControls.values().forEach(c -> c.initialize(config));
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.
440 * @param state changed Miniserver state or null if not specified (any/all)
442 public void onStateChange(LxState state) {
443 if (config == null) {
444 logger.error("Attempt to change state with not finalized configuration!: {}", state.getUuid());
446 channels.forEach(channel -> {
447 ChannelUID channelId = channel.getUID();
448 State channelState = getChannelState(channelId);
449 if (channelState != null) {
450 config.thingHandler.setChannelState(channelId, channelState);
457 * Gets room UUID after it was deserialized by GSON
461 public LxUuid getRoomUuid() {
466 * Gets category UUID after it was deserialized by GSON
468 * @return category UUID
470 public LxUuid getCategoryUuid() {
475 * Gets a GSON object for reuse
477 * @return GSON object
480 if (config == null) {
481 logger.error("Attempt to get GSON from not finalized configuration!");
484 return config.thingHandler.getGson();
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.
491 * @param control a new control to be created
493 static void addControl(LxControl control) {
494 control.config.thingHandler.addControl(control);
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.
501 * @param control a control to be removed
503 static void removeControl(LxControl control) {
504 control.config.thingHandler.removeControl(control);
509 * Gets control's configuration
511 * @return configuration
513 LxControlConfig getConfig() {
518 * Get control's room.
520 * @return control's room object
522 LxContainer getRoom() {
527 * Get control's category.
529 * @return control's category object
531 LxCategory getCategory() {
532 return config.category;
536 * Changes the channel state in the framework.
538 * @param id channel ID
539 * @param state new state value
541 void setChannelState(ChannelUID id, State state) {
542 if (config == null) {
543 logger.error("Attempt to set channel state with not finalized configuration!: {}", id);
546 config.thingHandler.setChannelState(id, state);
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.
555 * @return control channel label
562 * Gets value of a state object of given name, if exists
564 * @param name name of state object
565 * @return state object's value
567 Double getStateDoubleValue(String name) {
568 LxState state = states.get(name);
570 Object value = state.getStateValue();
571 if (value instanceof Double) {
572 return (Double) value;
579 * Gets value of a state object of given name, if exists, and converts it to decimal type value.
581 * @param name state name
582 * @return state value
584 State getStateDecimalValue(String name) {
585 Double value = getStateDoubleValue(name);
587 return new DecimalType(value);
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.
596 * @param name state name
597 * @return state value
599 State getStatePercentValue(String name) {
600 Double value = getStateDoubleValue(name);
604 if (value >= 0.0 && value <= 100.0) {
605 return new PercentType(value.intValue());
607 return UnDefType.UNDEF;
611 * Gets text value of a state object of given name, if exists
613 * @param name name of state object
614 * @return state object's text value
616 String getStateTextValue(String name) {
617 LxState state = states.get(name);
619 Object value = state.getStateValue();
620 if (value instanceof String str) {
628 * Gets text value of a state object of given name, if exists and converts it to string type
630 * @param name name of state object
631 * @return state object's text value
633 State getStateStringValue(String name) {
634 String value = getStateTextValue(name);
636 return new StringType(value);
642 * Gets double value of a state object of given name, if exists and converts it to switch type
644 * @param name name of state object
645 * @return state object's text value
647 State getStateOnOffValue(String name) {
648 Double value = getStateDoubleValue(name);
653 return OnOffType.OFF;
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}.
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)
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);
677 ChannelUID channelId = getChannelId(numberOfChannels++);
678 ChannelBuilder builder = ChannelBuilder.create(channelId, itemType).withType(typeId).withLabel(channelLabel)
679 .withDescription(channelDescription + " : " + channelLabel);
681 builder.withDefaultTags(tags);
683 channels.add(builder.build());
684 if (commandCallback != null || stateCallback != null) {
685 callbacks.put(channelId, new Callbacks(commandCallback, stateCallback));
691 * Adds a new {@link StateDescriptionFragment} for a channel that has multiple options to select from or a custom
694 * @param channelId channel ID to add the description for
695 * @param descriptionFragment channel state description fragment
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);
701 if (channelId != null && descriptionFragment != null) {
702 config.thingHandler.setChannelStateDescription(channelId, descriptionFragment.toStateDescription());
708 * Sends an action command to the Miniserver using active socket connection
710 * @param action string with action command
711 * @throws IOException when communication error with Miniserver occurs
713 void sendAction(String action) throws IOException {
714 if (config == null) {
715 logger.error("Attempt to send command with not finalized configuration!: {}", action);
717 config.thingHandler.sendAction(uuid, action);
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.
726 void removeAllChannels() {
732 * Call when control is no more needed - unlink it from containers
734 private void dispose() {
735 if (config.room != null) {
736 config.room.removeControl(this);
738 if (config.category != null) {
739 config.category.removeControl(this);
741 subControls.values().forEach(control -> control.dispose());
745 * Build channel ID for the control, based on control's UUID, thing's UUID and index of the channel for the control
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
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);
756 String controlId = uuid.toString();
758 controlId += "-" + index;
760 return new ChannelUID(config.thingHandler.getThingId(), controlId);