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.Type;
31 import org.openhab.core.types.UnDefType;
34 * MQTT topics are not inherently typed.
37 * With this class users are able to map MQTT topic values to framework types,
38 * for example for numbers {@link NumberValue}, boolean values {@link OnOffValue}, percentage values
39 * {@link PercentageValue}, string values {@link TextValue} and more.
43 * This class and the encapsulated (cached) state are necessary, because MQTT can't be queried,
44 * but we still need to be able to respond to framework requests for a value.
48 * {@link #getCache()} is used to retrieve a topic state and a call to {@link #update(Command)} sets the value.
51 * @author David Graeff - Initial contribution
54 public abstract class Value {
55 protected State state = UnDefType.UNDEF;
56 protected final List<Class<? extends Command>> commandTypes;
57 private final String itemType;
59 protected Value(String itemType, List<Class<? extends Command>> commandTypes) {
60 this.itemType = itemType;
61 this.commandTypes = commandTypes;
65 * Return a list of supported command types. The order of the list is important.
67 * The framework will try to parse an incoming string into one of those command types,
68 * starting with the first and continue until it succeeds.
71 * Your {@link #update(Command)} method must accept all command types of this list.
72 * You may accept more command types. This allows you to restrict the parsing of incoming
73 * MQTT values to the listed types, but handle more user commands.
75 * A prominent example is the {@link NumberValue}, which does not return {@link PercentType},
76 * because that would interfere with {@link DecimalType} while parsing the MQTT value.
77 * It does however accept a {@link PercentType} for {@link #update(Command)}, because a
78 * linked Item could send that type of command.
80 public final List<Class<? extends Command>> getSupportedCommandTypes() {
85 * Returns the item-type (one of {@link CoreItemFactory}).
87 public final String getItemType() {
92 * Returns the current value state.
94 public final State getChannelState() {
98 public String getMQTTpublishValue(Command command, @Nullable String pattern) {
99 if (pattern == null) {
100 return command.format("%s");
102 return command.format(pattern);
106 * Returns true if this is a binary type.
108 public boolean isBinary() {
113 * If the MQTT connection is not yet initialised or no values have
114 * been received yet, the default value is {@link UnDefType#UNDEF}. To restore to the
115 * default value after a connection got lost etc, this method will be called.
117 public final void resetState() {
118 state = UnDefType.UNDEF;
122 * Updates the internal value state with the given state.
124 * @param newState The new state to update the internal value.
125 * @exception IllegalArgumentException Thrown if for example a text is assigned to a number type.
127 public void update(State newState) throws IllegalArgumentException {
132 * Parses a given command into the proper type for this Value type. This will usually be a State,
133 * but can be a Command.
135 * @param command The command to parse.
136 * @exception IllegalArgumentException Thrown if for example a text is assigned to a number type.
138 public abstract Command parseCommand(Command command) throws IllegalArgumentException;
141 * Parses a given command from MQTT into the proper type for this Value type. This will usually
142 * be a State, but can be a non-State Command, in which case the channel will be commanded instead
143 * of updated, regardless of postCommand setting. The default implementation just calls
144 * parseCommand, so that both directions have the same logic.
146 * @param command The command to parse.
147 * @exception IllegalArgumentException Thrown if for example a text is assigned to a number type.
149 public Type parseMessage(Command command) throws IllegalArgumentException {
150 return parseCommand(command);
154 * Updates the internal value state with the given binary payload.
156 * @param data The binary payload to update the internal value.
157 * @exception IllegalArgumentException Thrown if for example a text is assigned to a number type.
159 public void update(byte[] data) throws IllegalArgumentException {
160 String mimeType = null;
162 // URLConnection.guessContentTypeFromStream(input) is not sufficient to detect all JPEG files
163 if (data.length >= 2 && data[0] == (byte) 0xFF && data[1] == (byte) 0xD8 && data[data.length - 2] == (byte) 0xFF
164 && data[data.length - 1] == (byte) 0xD9) {
165 mimeType = "image/jpeg";
167 try (final ByteArrayInputStream input = new ByteArrayInputStream(data)) {
169 mimeType = URLConnection.guessContentTypeFromStream(input);
170 } catch (final IOException ignored) {
172 } catch (final IOException ignored) {
175 state = new RawType(data, mimeType == null ? RawType.DEFAULT_MIME_TYPE : mimeType);
179 * Return the state description fragment builder for this value state.
181 * @param readOnly True if this is a read-only value.
182 * @return A state description fragment builder
184 public StateDescriptionFragmentBuilder createStateDescription(boolean readOnly) {
185 return StateDescriptionFragmentBuilder.create().withReadOnly(readOnly).withPattern("%s");
189 * Return the command description builder for this value state.
191 * @return A command description builder
193 public CommandDescriptionBuilder createCommandDescription() {
194 return CommandDescriptionBuilder.create();