mirror of
https://github.com/danieldemus/openhab-addons
synced 2026-07-31 05:24:24 +02:00
[mqtt.awtrix3] Parse font tags for multi-colored app texts (#19466)
* Allow color tags in app text Signed-off-by: Thomas Lauterbach <2452988+DrRSatzteil@users.noreply.github.com>
This commit is contained in:
@@ -110,13 +110,45 @@ The button events can be used by rules to change the displayed app or perform an
|
||||
| `rainbow` | Switch | RW | Enable rainbow effect: Uses a rainbow effect for the displayed text. |
|
||||
| `reset`* | Switch | RW | Reset app to default state: All channels will be reset to their default values. |
|
||||
| `scroll-speed` | Number:Dimensionless | RW | Text scrolling speed: Provide as percentage value. The original speed is 100%. Values above 100% will increase the scrolling speed, values below 100% will decrease it. Setting this value to 0 will disable scrolling completely. |
|
||||
| `text` | String | RW | Text to display. |
|
||||
| `text` | String | RW | Text to display. Supports inline color formatting with font color tags (see Text Color Tags section below for details). |
|
||||
| `text-case` | Number:Dimensionless | RW | Set text case (0=normal, 1=uppercase, 2=lowercase). |
|
||||
| `text-offset` | Number:Dimensionless | RW | Text offset position: Horizontal offset of the text in pixels. |
|
||||
| `top-text` | String | RW | Draws the text on the top of the display. |
|
||||
|
||||
\* Cannot be used with notification Actions (see section Actions)
|
||||
|
||||
## Text Color Tags
|
||||
|
||||
The `text` channel supports inline color formatting using simple HTML-like font color tags. This allows you to display text with multiple colors in a single app.
|
||||
|
||||
### Syntax
|
||||
|
||||
Use the following format to apply colors to specific parts of your text:
|
||||
|
||||
```html
|
||||
<font color="#RRGGBB">colored text</font>
|
||||
```
|
||||
|
||||
Where `RRGGBB` is a 6-digit hexadecimal color code (e.g., `FF0000` for red, `00FF00` for green, `0000FF` for blue).
|
||||
|
||||
### Examples
|
||||
|
||||
```java
|
||||
// Multiple colored segments - "Hello" and "in" will use the color from the color channel, "World" and "Color" will use the specified colors
|
||||
Custom_Text.sendCommand('Hello <font color="#FF0000">World</font> in <font color="#00FF00">Color</font>')
|
||||
|
||||
// All text in custom color
|
||||
Custom_Text.sendCommand('<font color="#FF6600">Temperature: 25°C</font>')
|
||||
```
|
||||
|
||||
### Important Notes
|
||||
|
||||
- **Default color**: Text outside of `<font>` tags will be displayed in the color defined by the `color` channel.
|
||||
- **Text effects disabled**: When color tags are used, the `blink`, `fade`, and `rainbow` effects are automatically disabled, as each text segment has its own color. The `gradient-color` channel is also ignored.
|
||||
- **Tags cannot be nested**: `<font>` tags must not be placed inside other `<font>` tags. Nesting is not supported and will result in incorrect parsing.
|
||||
- **Case-insensitive hex values**: Both uppercase and lowercase hex values are supported (e.g., `#FF0000` or `#ff0000`).
|
||||
- **Malformed tags**: If a tag is malformed (e.g., missing closing tag), the parser will gracefully handle it by applying the default color.
|
||||
|
||||
## Actions
|
||||
|
||||
The binding supports various actions that can be used in rules to control the Awtrix display. To use these actions, you need to import them in your rules (see examples below).
|
||||
|
||||
+120
-2
@@ -12,8 +12,10 @@
|
||||
*/
|
||||
package org.openhab.binding.mqtt.awtrixlight.internal.app;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -21,6 +23,27 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.mqtt.awtrixlight.internal.Helper;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* The {@link TextSegment} is the representation of a text segment in an App.
|
||||
*
|
||||
* @author Thomas Lauterbach - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
class TextSegment {
|
||||
@SerializedName("t")
|
||||
public final String text;
|
||||
|
||||
@SerializedName("c")
|
||||
public final String color;
|
||||
|
||||
public TextSegment(String text, String color) {
|
||||
this.text = text;
|
||||
this.color = color;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link AwtrixApp} is the representation of the current app configuration and provides a method to create a config
|
||||
* string for the clock.
|
||||
@@ -30,6 +53,8 @@ import org.openhab.binding.mqtt.awtrixlight.internal.Helper;
|
||||
@NonNullByDefault
|
||||
public class AwtrixApp {
|
||||
|
||||
private static final int CLOSING_TAG_LENGTH = 7; // 7 = "</font>".length()
|
||||
|
||||
public static final String DEFAULT_TEXT = "New Awtrix App";
|
||||
public static final int DEFAULT_TEXTCASE = 0;
|
||||
public static final boolean DEFAULT_TOPTEXT = false;
|
||||
@@ -88,6 +113,9 @@ public class AwtrixApp {
|
||||
// effectSettings properties
|
||||
private Map<String, Object> effectSettings;
|
||||
|
||||
private static final java.util.regex.Pattern TEXT_COLOR_PATTERN = java.util.regex.Pattern
|
||||
.compile("color=\"#([0-9A-Fa-f]{6})\"");
|
||||
|
||||
public AwtrixApp() {
|
||||
this.effectSettings = new HashMap<String, Object>();
|
||||
this.effectSettings.put("speed", DEFAULT_EFFECTSPEED);
|
||||
@@ -362,7 +390,12 @@ public class AwtrixApp {
|
||||
|
||||
public Map<String, Object> getAppParams() {
|
||||
Map<String, Object> fields = new HashMap<String, Object>();
|
||||
fields.put("text", this.text);
|
||||
if (textHasColorTags(this.text)) {
|
||||
fields.put("text", this.parseTextSegments());
|
||||
} else {
|
||||
fields.put("text", this.text);
|
||||
fields.putAll(getTextEffectConfig());
|
||||
}
|
||||
fields.put("textCase", this.textCase);
|
||||
fields.put("topText", this.topText);
|
||||
fields.put("textOffset", this.textOffset);
|
||||
@@ -371,7 +404,6 @@ public class AwtrixApp {
|
||||
fields.put("lifetimeMode", this.lifetimeMode);
|
||||
fields.put("overlay", this.overlay);
|
||||
fields.putAll(getColorConfig());
|
||||
fields.putAll(getTextEffectConfig());
|
||||
fields.putAll(getBackgroundConfig());
|
||||
fields.putAll(getIconConfig());
|
||||
fields.put("duration", this.duration);
|
||||
@@ -515,6 +547,92 @@ public class AwtrixApp {
|
||||
return fields;
|
||||
}
|
||||
|
||||
private boolean textHasColorTags(String text) {
|
||||
if (text.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
// Check for the basic structure and use regex to validate color format
|
||||
// We need both opening and closing tags, and at least one valid color attribute
|
||||
return text.contains("<font") && text.contains("</font>") && TEXT_COLOR_PATTERN.matcher(text).find();
|
||||
}
|
||||
|
||||
private static String rgbToHex(int[] rgb) {
|
||||
// Ensure values are in 0-255 range
|
||||
int r = Math.min(255, Math.max(0, rgb[0]));
|
||||
int g = Math.min(255, Math.max(0, rgb[1]));
|
||||
int b = Math.min(255, Math.max(0, rgb[2]));
|
||||
|
||||
// Format as 6-digit hex string, padding with zeros if needed
|
||||
return String.format("%02x%02x%02x", r, g, b);
|
||||
}
|
||||
|
||||
private List<TextSegment> parseTextSegments() {
|
||||
List<TextSegment> segments = new ArrayList<>();
|
||||
if (this.text.isEmpty()) {
|
||||
return segments;
|
||||
}
|
||||
|
||||
String remaining = this.text;
|
||||
String defaultColor = rgbToHex(this.color);
|
||||
|
||||
while (true) {
|
||||
int startTag = remaining.indexOf("<font");
|
||||
if (startTag < 0) {
|
||||
// No more tags, add remaining text
|
||||
if (!remaining.isEmpty()) {
|
||||
segments.add(new TextSegment(remaining, defaultColor));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Add text before the tag
|
||||
if (startTag > 0) {
|
||||
segments.add(new TextSegment(remaining.substring(0, startTag), defaultColor));
|
||||
}
|
||||
|
||||
// Find the end of the opening tag
|
||||
int endTag = remaining.indexOf(">", startTag);
|
||||
if (endTag < 0) {
|
||||
// Malformed tag, add everything and stop
|
||||
segments.add(new TextSegment(remaining, defaultColor));
|
||||
break;
|
||||
}
|
||||
|
||||
// Extract color from tag (or use default)
|
||||
String tag = remaining.substring(startTag, endTag + 1);
|
||||
String color = extractColor(tag);
|
||||
if (color == null) {
|
||||
color = defaultColor;
|
||||
}
|
||||
|
||||
// Find the closing tag
|
||||
int closeTag = remaining.indexOf("</font>", endTag);
|
||||
if (closeTag < 0) {
|
||||
// No closing tag, add rest with color and stop
|
||||
segments.add(new TextSegment(remaining.substring(endTag + 1), color));
|
||||
break;
|
||||
}
|
||||
|
||||
// Add text between tags
|
||||
segments.add(new TextSegment(remaining.substring(endTag + 1, closeTag), color));
|
||||
|
||||
// Move past the closing tag
|
||||
remaining = remaining.substring(closeTag + CLOSING_TAG_LENGTH);
|
||||
}
|
||||
|
||||
return segments;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String extractColor(String tag) {
|
||||
java.util.regex.Matcher matcher = TEXT_COLOR_PATTERN.matcher(tag);
|
||||
if (matcher.find()) {
|
||||
// Group 1 contains the hex color value (without the #)
|
||||
return matcher.group(1).toLowerCase();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Map<String, Object> getTextEffectConfig() {
|
||||
Map<String, Object> fields = new HashMap<String, Object>();
|
||||
if (Arrays.equals(this.color, DEFAULT_COLOR) && Arrays.equals(this.gradient, DEFAULT_GRADIENT)) {
|
||||
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.mqtt.awtrixlight.internal.app;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
/**
|
||||
* Test cases for the {@link AwtrixApp} object.
|
||||
*
|
||||
* @author Thomas Lauterbach - Initial contribution
|
||||
*/
|
||||
class AwtrixAppTest {
|
||||
|
||||
@Test
|
||||
void testTextWithMixedContent() {
|
||||
AwtrixApp app = new AwtrixApp();
|
||||
app.setText("This is a <font color=\"#cc33cc\">Multi</font> Colored <font color=\"#34ab12\">Text</font>!");
|
||||
app.setColor(new int[] { 255, 255, 255 }); // Default white color
|
||||
|
||||
String config = app.getAppConfig();
|
||||
JsonObject json = JsonParser.parseString(config).getAsJsonObject();
|
||||
JsonArray text = json.get("text").getAsJsonArray();
|
||||
|
||||
// Should be 5 segments: "This is a ", "Multi", " Colored ", "Text", "!"
|
||||
assertEquals(5, text.size());
|
||||
|
||||
// Verify each segment's text
|
||||
assertEquals("This is a ", text.get(0).getAsJsonObject().get("t").getAsString());
|
||||
assertEquals("Multi", text.get(1).getAsJsonObject().get("t").getAsString());
|
||||
assertEquals(" Colored ", text.get(2).getAsJsonObject().get("t").getAsString());
|
||||
assertEquals("Text", text.get(3).getAsJsonObject().get("t").getAsString());
|
||||
assertEquals("!", text.get(4).getAsJsonObject().get("t").getAsString());
|
||||
|
||||
// Optionally, verify colors
|
||||
assertEquals("ffffff", text.get(0).getAsJsonObject().get("c").getAsString()); // Default color
|
||||
assertEquals("cc33cc", text.get(1).getAsJsonObject().get("c").getAsString());
|
||||
assertEquals("ffffff", text.get(2).getAsJsonObject().get("c").getAsString()); // Default color
|
||||
assertEquals("34ab12", text.get(3).getAsJsonObject().get("c").getAsString());
|
||||
assertEquals("ffffff", text.get(4).getAsJsonObject().get("c").getAsString()); // Default color
|
||||
}
|
||||
|
||||
@Test
|
||||
void testTextStartingWithColor() {
|
||||
AwtrixApp app = new AwtrixApp();
|
||||
app.setText("<font color=\"#ff0000\">Red</font> text at start");
|
||||
app.setColor(new int[] { 0, 0, 0 }); // Default black color
|
||||
|
||||
String config = app.getAppConfig();
|
||||
JsonObject json = JsonParser.parseString(config).getAsJsonObject();
|
||||
|
||||
assertTrue(json.has("text"));
|
||||
JsonArray text = json.get("text").getAsJsonArray();
|
||||
assertEquals(2, text.size());
|
||||
assertEquals("Red", text.get(0).getAsJsonObject().get("t").getAsString());
|
||||
assertEquals(" text at start", text.get(1).getAsJsonObject().get("t").getAsString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testTextWithBrackets() {
|
||||
AwtrixApp app = new AwtrixApp();
|
||||
app.setText("<font color=\"#ff0000\">Red <- Nice!</font>");
|
||||
app.setColor(new int[] { 0, 0, 0 }); // Default black color
|
||||
|
||||
String config = app.getAppConfig();
|
||||
JsonObject json = JsonParser.parseString(config).getAsJsonObject();
|
||||
|
||||
assertTrue(json.has("text"));
|
||||
JsonArray text = json.get("text").getAsJsonArray();
|
||||
assertEquals(1, text.size());
|
||||
assertEquals("Red <- Nice!", text.get(0).getAsJsonObject().get("t").getAsString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAdjacentColorSegments() {
|
||||
AwtrixApp app = new AwtrixApp();
|
||||
app.setText("<font color=\"#ff0000\">Red</font><font color=\"#0000ff\">Blue</font>");
|
||||
app.setColor(new int[] { 0, 0, 0 });
|
||||
|
||||
String config = app.getAppConfig();
|
||||
JsonObject json = JsonParser.parseString(config).getAsJsonObject();
|
||||
|
||||
assertTrue(json.has("text"));
|
||||
JsonArray text = json.get("text").getAsJsonArray();
|
||||
assertEquals(2, text.size());
|
||||
assertEquals("Red", text.get(0).getAsJsonObject().get("t").getAsString());
|
||||
assertEquals("Blue", text.get(1).getAsJsonObject().get("t").getAsString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAdjacentColorSegmentsWithSpace() {
|
||||
AwtrixApp app = new AwtrixApp();
|
||||
app.setText("<font color=\"#ff0000\">Red</font> <font color=\"#0000ff\">Blue</font>");
|
||||
app.setColor(new int[] { 0, 0, 0 });
|
||||
|
||||
String config = app.getAppConfig();
|
||||
JsonObject json = JsonParser.parseString(config).getAsJsonObject();
|
||||
|
||||
assertTrue(json.has("text"));
|
||||
JsonArray text = json.get("text").getAsJsonArray();
|
||||
assertEquals(3, text.size());
|
||||
assertEquals("Red", text.get(0).getAsJsonObject().get("t").getAsString());
|
||||
assertEquals(" ", text.get(1).getAsJsonObject().get("t").getAsString());
|
||||
assertEquals("Blue", text.get(2).getAsJsonObject().get("t").getAsString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPlainText() {
|
||||
AwtrixApp app = new AwtrixApp();
|
||||
app.setText("Just plain text with no colors");
|
||||
app.setColor(new int[] { 18, 52, 86 }); // Some default color
|
||||
|
||||
String config = app.getAppConfig();
|
||||
JsonObject json = JsonParser.parseString(config).getAsJsonObject();
|
||||
|
||||
assertTrue(json.has("text"));
|
||||
String text = json.get("text").getAsString();
|
||||
assertEquals("Just plain text with no colors", text);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEmptyString() {
|
||||
AwtrixApp app = new AwtrixApp();
|
||||
app.setText("");
|
||||
app.setColor(new int[] { 0, 0, 0 });
|
||||
|
||||
String config = app.getAppConfig();
|
||||
JsonObject json = JsonParser.parseString(config).getAsJsonObject();
|
||||
|
||||
// Depending on implementation, the text field might be empty or not present
|
||||
if (json.has("text")) {
|
||||
assertTrue(json.get("text").getAsString().isEmpty());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testOnlySpaces() {
|
||||
AwtrixApp app = new AwtrixApp();
|
||||
app.setText(" ");
|
||||
app.setColor(new int[] { 0, 0, 0 });
|
||||
|
||||
String config = app.getAppConfig();
|
||||
JsonObject json = JsonParser.parseString(config).getAsJsonObject();
|
||||
|
||||
assertTrue(json.has("text"));
|
||||
assertEquals(" ", json.get("text").getAsString());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user