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.mqtt.generic.values;
15 import java.io.ByteArrayInputStream;
16 import java.io.IOException;
17 import java.net.URLConnection;
18 import java.util.List;
20 import org.eclipse.jdt.annotation.NonNullByDefault;
21 import org.eclipse.jdt.annotation.Nullable;
22 import org.openhab.core.library.CoreItemFactory;
23 import org.openhab.core.library.types.DecimalType;
24 import org.openhab.core.library.types.PercentType;
25 import org.openhab.core.library.types.RawType;
26 import org.openhab.core.types.Command;
27 import org.openhab.core.types.CommandDescriptionBuilder;
28 import org.openhab.core.types.State;
29 import org.openhab.core.types.StateDescriptionFragmentBuilder;
30 import org.openhab.core.types.UnDefType;
33 * MQTT topics are not inherently typed.
36 * With this class users are able to map MQTT topic values to framework types,
37 * for example for numbers {@link NumberValue}, boolean values {@link OnOffValue}, percentage values
38 * {@link PercentageValue}, string values {@link TextValue} and more.
42 * This class and the encapsulated (cached) state are necessary, because MQTT can't be queried,
43 * but we still need to be able to respond to framework requests for a value.
47 * {@link #getCache()} is used to retrieve a topic state and a call to {@link #update(Command)} sets the value.
50 * @author David Graeff - Initial contribution
53 public abstract class Value {
54 protected State state = UnDefType.UNDEF;
55 protected final List<Class<? extends Command>> commandTypes;
56 private final String itemType;
58 protected Value(String itemType, List<Class<? extends Command>> commandTypes) {
59 this.itemType = itemType;
60 this.commandTypes = commandTypes;
64 * Return a list of supported command types. The order of the list is important.
66 * The framework will try to parse an incoming string into one of those command types,
67 * starting with the first and continue until it succeeds.
70 * Your {@link #update(Command)} method must accept all command types of this list.
71 * You may accept more command types. This allows you to restrict the parsing of incoming
72 * MQTT values to the listed types, but handle more user commands.
74 * A prominent example is the {@link NumberValue}, which does not return {@link PercentType},
75 * because that would interfere with {@link DecimalType} while parsing the MQTT value.
76 * It does however accept a {@link PercentType} for {@link #update(Command)}, because a
77 * linked Item could send that type of command.
79 public final List<Class<? extends Command>> getSupportedCommandTypes() {
84 * Returns the item-type (one of {@link CoreItemFactory}).
86 public final String getItemType() {
91 * Returns the current value state.
93 public final State getChannelState() {
97 public String getMQTTpublishValue(Command command, @Nullable String pattern) {
98 if (pattern == null) {
99 return command.format("%s");
101 return command.format(pattern);
105 * Returns true if this is a binary type.
107 public boolean isBinary() {
112 * If the MQTT connection is not yet initialised or no values have
113 * been received yet, the default value is {@link UnDefType#UNDEF}. To restore to the
114 * default value after a connection got lost etc, this method will be called.
116 public final void resetState() {
117 state = UnDefType.UNDEF;
121 * Updates the internal value state with the given state.
123 * @param newState The new state to update the internal value.
124 * @exception IllegalArgumentException Thrown if for example a text is assigned to a number type.
126 public void update(State newState) throws IllegalArgumentException {
131 * Parses a given command into the proper type for this Value type. This will usually be a State,
132 * but can be a Command.
134 * @param command The command to parse.
135 * @exception IllegalArgumentException Thrown if for example a text is assigned to a number type.
137 public abstract Command parseCommand(Command command) throws IllegalArgumentException;
140 * Parses a given command from MQTT into the proper type for this Value type. This will usually
141 * be a State, but can be a non-State Command, in which case the channel will be commanded instead
142 * of updated, regardless of postCommand setting. The default implementation just calls
143 * parseCommand, so that both directions have the same logic.
145 * @param command The command to parse.
146 * @exception IllegalArgumentException Thrown if for example a text is assigned to a number type.
148 public Command parseMessage(Command command) throws IllegalArgumentException {
149 return parseCommand(command);
153 * Updates the internal value state with the given binary payload.
155 * @param data The binary payload to update the internal value.
156 * @exception IllegalArgumentException Thrown if for example a text is assigned to a number type.
158 public void update(byte[] data) throws IllegalArgumentException {
159 String mimeType = null;
161 // URLConnection.guessContentTypeFromStream(input) is not sufficient to detect all JPEG files
162 if (data.length >= 2 && data[0] == (byte) 0xFF && data[1] == (byte) 0xD8 && data[data.length - 2] == (byte) 0xFF
163 && data[data.length - 1] == (byte) 0xD9) {
164 mimeType = "image/jpeg";
166 try (final ByteArrayInputStream input = new ByteArrayInputStream(data)) {
168 mimeType = URLConnection.guessContentTypeFromStream(input);
169 } catch (final IOException ignored) {
171 } catch (final IOException ignored) {
174 state = new RawType(data, mimeType == null ? RawType.DEFAULT_MIME_TYPE : mimeType);
178 * Return the state description fragment builder for this value state.
180 * @param readOnly True if this is a read-only value.
181 * @return A state description fragment builder
183 public StateDescriptionFragmentBuilder createStateDescription(boolean readOnly) {
184 return StateDescriptionFragmentBuilder.create().withReadOnly(readOnly).withPattern("%s");
188 * Return the command description builder for this value state.
190 * @return A command description builder
192 public CommandDescriptionBuilder createCommandDescription() {
193 return CommandDescriptionBuilder.create();