diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/HomeAssistantChannelTransformation.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/HomeAssistantChannelTransformation.java index 75e4e71a5a212..4bd53696772c6 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/HomeAssistantChannelTransformation.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/HomeAssistantChannelTransformation.java @@ -53,11 +53,11 @@ public UndefinedException(JinjavaInterpreter interpreter) { private final Logger logger = LoggerFactory.getLogger(HomeAssistantChannelTransformation.class); private final Jinjava jinjava; - private final AbstractComponent component; + private final AbstractComponent component; private final String template; private final ObjectMapper objectMapper = new ObjectMapper(); - public HomeAssistantChannelTransformation(Jinjava jinjava, AbstractComponent component, String template) { + public HomeAssistantChannelTransformation(Jinjava jinjava, AbstractComponent component, String template) { super((String) null); this.jinjava = jinjava; this.component = component; diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/HomeAssistantJinjaFunctionLibrary.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/HomeAssistantJinjaFunctionLibrary.java index e3dad0683785e..84d4d0acc1deb 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/HomeAssistantJinjaFunctionLibrary.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/HomeAssistantJinjaFunctionLibrary.java @@ -14,15 +14,24 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import java.util.Map; import java.util.stream.Stream; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import com.google.re2j.Matcher; +import com.google.re2j.Pattern; +import com.google.re2j.PatternSyntaxException; import com.hubspot.jinjava.interpret.Context; import com.hubspot.jinjava.interpret.InterpretException; +import com.hubspot.jinjava.interpret.InvalidArgumentException; +import com.hubspot.jinjava.interpret.InvalidReason; import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.interpret.TemplateSyntaxException; import com.hubspot.jinjava.lib.filter.Filter; import com.hubspot.jinjava.lib.fn.ELFunctionDefinition; import com.hubspot.jinjava.util.ObjectTruthValue; @@ -39,15 +48,17 @@ public static void register(Context context) { new ELFunctionDefinition("", "iif", Functions.class, "iif", Object.class, Object[].class)); context.registerFilter(new SimpleFilter("iif", Functions.class, "iif", Object.class, Object[].class)); context.registerFilter(new IsDefinedFilter()); + context.registerFilter(new RegexFindAllFilter()); + context.registerFilter(new RegexFindAllIndexFilter()); } @NonNullByDefault({}) private static class SimpleFilter implements Filter { private final String name; private final Method method; - private final Class klass; + private final Class klass; - public SimpleFilter(String name, Class klass, String methodName, Class... args) { + public SimpleFilter(String name, Class klass, String methodName, Class... args) { this.name = name; this.klass = klass; try { @@ -110,9 +121,135 @@ public Object filter(Object var, JinjavaInterpreter interpreter, String... args) } } + // https://www.home-assistant.io/docs/configuration/templating/#regular-expressions + // https://github.com/home-assistant/core/blob/2024.12.2/homeassistant/helpers/template.py#L2453 + @NonNullByDefault({}) + private static class RegexFindAllFilter implements Filter { + @Override + public String getName() { + return "regex_findall"; + } + + @Override + public Object filter(Object var, JinjavaInterpreter interpreter, String... args) { + if (args.length > 2) { + throw new TemplateSyntaxException(interpreter, getName(), + "requires at most 2 arguments (regex string, ignore case)"); + } + + String find = null; + if (args.length >= 1) { + find = args[0]; + } + String ignoreCase = null; + if (args.length == 2) { + ignoreCase = args[1]; + } + + Matcher m = regexFindAll(var, interpreter, find, ignoreCase); + + List result = new ArrayList<>(); + while (m.find()) { + result.add(resultForMatcher(m)); + } + + return result; + } + + protected Object resultForMatcher(Matcher m) { + if (m.groupCount() == 0) { + return m.group(); + } else if (m.groupCount() == 1) { + return m.group(1); + } else { + List groups = new ArrayList<>(m.groupCount()); + for (int i = 1; i <= m.groupCount(); ++i) { + groups.add(m.group(i)); + } + return groups; + } + } + + protected Matcher regexFindAll(Object var, JinjavaInterpreter interpreter, String find, String ignoreCaseStr) { + String s; + if (var == null) { + s = "None"; + } else { + s = var.toString(); + } + + boolean ignoreCase = ObjectTruthValue.evaluate(ignoreCaseStr); + int flags = 0; + if (ignoreCase) { + flags = Pattern.CASE_INSENSITIVE; + } + + Pattern p; + try { + if (find instanceof String findString) { + p = Pattern.compile(findString, flags); + } else if (find == null) { + p = Pattern.compile("", flags); + } else { + throw new InvalidArgumentException(interpreter, this, InvalidReason.REGEX, 0, find); + } + + return p.matcher(s); + } catch (PatternSyntaxException e) { + throw new InvalidArgumentException(interpreter, this, InvalidReason.REGEX, 0, find); + } + } + } + + // https://www.home-assistant.io/docs/configuration/templating/#regular-expressions + // https://github.com/home-assistant/core/blob/2024.12.2/homeassistant/helpers/template.py#L2448 + @NonNullByDefault({}) + private static class RegexFindAllIndexFilter extends RegexFindAllFilter { + @Override + public String getName() { + return "regex_findall_index"; + } + + @Override + public Object filter(Object var, JinjavaInterpreter interpreter, String... args) { + if (args.length > 3) { + throw new TemplateSyntaxException(interpreter, getName(), + "requires at most 3 arguments (regex string, index, ignore case)"); + } + + String find = null; + if (args.length >= 1) { + find = args[0]; + } + int index = 0; + if (args.length >= 2) { + index = Integer.valueOf(args[1]); + if (index < 0) { + throw new InvalidArgumentException(interpreter, this, InvalidReason.POSITIVE_NUMBER, 1, args[1]); + } + } + + String ignoreCase = null; + if (args.length == 3) { + ignoreCase = args[2]; + } + + Matcher m = regexFindAll(var, interpreter, find, ignoreCase); + int i = 0; + while (i <= index) { + if (!m.find()) { + break; + } + i += 1; + } + + return resultForMatcher(m); + } + } + private static class Functions { // https://www.home-assistant.io/docs/configuration/templating/#immediate-if-iif - public static Object iif(Object value, Object... results) { + public static @Nullable Object iif(@Nullable Object value, @Nullable Object... results) { if (results.length > 3) { throw new IllegalArgumentException("Parameters for function 'iff' do not match"); } diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/HomeAssistantChannelTransformationTests.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/HomeAssistantChannelTransformationTests.java index f8341fbb1c6c9..ea172e2c2a1bb 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/HomeAssistantChannelTransformationTests.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/HomeAssistantChannelTransformationTests.java @@ -100,6 +100,24 @@ public void testIsDefined() { assertThat(transform("{{ 'hi' | is_defined }}", "{}"), is("hi")); } + @Test + public void testRegexFindall() { + assertThat(transform("{{ 'Flight from JFK to LHR' | regex_findall('([A-Z]{3})') }}", ""), is("[JFK, LHR]")); + assertThat(transform( + "{{ 'button_up_press' | regex_findall('^(?P