mirror of
https://github.com/danieldemus/openhab-core.git
synced 2026-07-29 12:34:22 +02:00
[i18n] Allow generation of default translations for automation modules (#2966)
* Allow generation of default translations for automation modules Signed-off-by: Jan N. Klug <github@klug.nrw>
This commit is contained in:
@@ -68,6 +68,11 @@
|
||||
<artifactId>org.openhab.core.thing.xml</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.code.gson</groupId>
|
||||
<artifactId>gson</artifactId>
|
||||
<version>2.8.9</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.core.bom</groupId>
|
||||
<artifactId>org.openhab.core.bom.test</artifactId>
|
||||
|
||||
@@ -25,6 +25,8 @@ import org.openhab.core.thing.xml.internal.ChannelGroupTypeXmlResult;
|
||||
import org.openhab.core.thing.xml.internal.ChannelTypeXmlResult;
|
||||
import org.openhab.core.thing.xml.internal.ThingTypeXmlResult;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
/**
|
||||
* The bundle information provided by the openHAB XML files in the <code>OH-INF</code> directory.
|
||||
*
|
||||
@@ -39,6 +41,8 @@ public class BundleInfo {
|
||||
private List<ChannelGroupTypeXmlResult> channelGroupTypesXml = new ArrayList<>(5);
|
||||
private List<ChannelTypeXmlResult> channelTypesXml = new ArrayList<>(5);
|
||||
private List<ThingTypeXmlResult> thingTypesXml = new ArrayList<>(5);
|
||||
private List<JsonObject> moduleTypesJson = new ArrayList<>(5);
|
||||
private List<JsonObject> ruleTemplateJson = new ArrayList<>(5);
|
||||
|
||||
public String getBindingId() {
|
||||
return bindingId;
|
||||
@@ -80,6 +84,22 @@ public class BundleInfo {
|
||||
this.channelTypesXml = channelTypesXml;
|
||||
}
|
||||
|
||||
public List<JsonObject> getModuleTypesJson() {
|
||||
return moduleTypesJson;
|
||||
}
|
||||
|
||||
public void setModuleTypesJson(List<JsonObject> moduleTypesJson) {
|
||||
this.moduleTypesJson = moduleTypesJson;
|
||||
}
|
||||
|
||||
public List<JsonObject> getRuleTemplateJson() {
|
||||
return ruleTemplateJson;
|
||||
}
|
||||
|
||||
public void setRuleTemplateJson(List<JsonObject> ruleTemplateJson) {
|
||||
this.ruleTemplateJson = ruleTemplateJson;
|
||||
}
|
||||
|
||||
public List<ThingTypeXmlResult> getThingTypesXml() {
|
||||
return thingTypesXml;
|
||||
}
|
||||
|
||||
+51
@@ -17,7 +17,11 @@ import java.net.MalformedURLException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
import org.apache.maven.plugin.logging.Log;
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
@@ -30,6 +34,10 @@ import org.openhab.core.thing.xml.internal.ChannelTypeXmlResult;
|
||||
import org.openhab.core.thing.xml.internal.ThingDescriptionReader;
|
||||
import org.openhab.core.thing.xml.internal.ThingTypeXmlResult;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonNull;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.thoughtworks.xstream.converters.ConversionException;
|
||||
|
||||
/**
|
||||
@@ -42,6 +50,8 @@ public class BundleInfoReader {
|
||||
|
||||
private final Log log;
|
||||
|
||||
private final Predicate<Path> isJsonFile = path -> Files.isRegularFile(path) && path.toString().endsWith(".json");
|
||||
|
||||
public BundleInfoReader(Log log) {
|
||||
this.log = log;
|
||||
}
|
||||
@@ -51,6 +61,8 @@ public class BundleInfoReader {
|
||||
readBindingInfo(ohinfPath, bundleInfo);
|
||||
readConfigInfo(ohinfPath, bundleInfo);
|
||||
readThingInfo(ohinfPath, bundleInfo);
|
||||
readModuleTypeInfo(ohinfPath, bundleInfo);
|
||||
readRuleTemplateInfo(ohinfPath, bundleInfo);
|
||||
return bundleInfo;
|
||||
}
|
||||
|
||||
@@ -129,4 +141,43 @@ public class BundleInfoReader {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void readModuleTypeInfo(Path ohinfPath, BundleInfo bundleInfo) throws IOException {
|
||||
Path modulePath = ohinfPath.resolve("automation").resolve("moduletypes");
|
||||
if (Files.exists(modulePath)) {
|
||||
try (Stream<Path> files = Files.walk(modulePath)) {
|
||||
List<JsonObject> moduleTypes = files.filter(isJsonFile).flatMap(this::readJsonElementsFromFile)
|
||||
.map(JsonElement::getAsJsonObject).collect(Collectors.toList());
|
||||
if (!moduleTypes.isEmpty()) {
|
||||
bundleInfo.setModuleTypesJson(moduleTypes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void readRuleTemplateInfo(Path ohinfPath, BundleInfo bundleInfo) throws IOException {
|
||||
Path template = ohinfPath.resolve("automation").resolve("templates");
|
||||
if (Files.exists(template)) {
|
||||
try (Stream<Path> files = Files.walk(template)) {
|
||||
List<JsonObject> ruleTemplates = files.filter(isJsonFile).flatMap(this::readJsonElementsFromFile)
|
||||
.map(JsonElement::getAsJsonObject).collect(Collectors.toList());
|
||||
if (!ruleTemplates.isEmpty()) {
|
||||
bundleInfo.setRuleTemplateJson(ruleTemplates);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Stream<JsonElement> readJsonElementsFromFile(Path path) {
|
||||
try {
|
||||
JsonElement element = JsonParser.parseString(Files.readString(path));
|
||||
if (element.isJsonObject()) {
|
||||
return element.getAsJsonObject().entrySet().stream().map(Map.Entry::getValue)
|
||||
.filter(JsonElement::isJsonArray)
|
||||
.flatMap(a -> StreamSupport.stream(a.getAsJsonArray().spliterator(), false));
|
||||
}
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
return Stream.of(JsonNull.INSTANCE);
|
||||
}
|
||||
}
|
||||
|
||||
+3
@@ -111,6 +111,9 @@ public class GenerateDefaultTranslationsMojo extends AbstractI18nMojo {
|
||||
XmlToTranslationsConverter xmlConverter = new XmlToTranslationsConverter();
|
||||
Translations generatedTranslations = xmlConverter.convert(bundleInfo);
|
||||
|
||||
JsonToTranslationsConverter jsonConverter = new JsonToTranslationsConverter();
|
||||
jsonConverter.convert(bundleInfo).sections.forEach(generatedTranslations::addSection);
|
||||
|
||||
PropertiesToTranslationsConverter propertiesConverter = new PropertiesToTranslationsConverter(getLog());
|
||||
Translations existingTranslations = propertiesConverter.convert(defaultTranslationsPath);
|
||||
|
||||
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2022 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.core.tools.i18n.plugin;
|
||||
|
||||
import static org.openhab.core.tools.i18n.plugin.Translations.TranslationsEntry.entry;
|
||||
import static org.openhab.core.tools.i18n.plugin.Translations.TranslationsGroup.group;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.stream.Stream.Builder;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.tools.i18n.plugin.Translations.TranslationsEntry;
|
||||
import org.openhab.core.tools.i18n.plugin.Translations.TranslationsGroup;
|
||||
import org.openhab.core.tools.i18n.plugin.Translations.TranslationsSection;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
/**
|
||||
* Converts JSON based {@link BundleInfo} to {@link Translations}.
|
||||
*
|
||||
* @author Jan N. Klug - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class JsonToTranslationsConverter {
|
||||
|
||||
private static final Pattern OPTION_ESCAPE_PATTERN = Pattern.compile("([ :=])");
|
||||
|
||||
public Translations convert(BundleInfo bundleInfo) {
|
||||
return new Translations(Stream.of( //
|
||||
moduleTypeSection(bundleInfo), ruleTemplateSection(bundleInfo)//
|
||||
).flatMap(Function.identity()).collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
private Stream<TranslationsSection> moduleTypeSection(BundleInfo bundleInfo) {
|
||||
Builder<TranslationsSection> sectionBuilder = Stream.builder();
|
||||
|
||||
bundleInfo.getModuleTypesJson().stream().flatMap(this::getModuleType).forEach(sectionBuilder::add);
|
||||
return sectionBuilder.build().sorted(Comparator.comparing(s -> s.header));
|
||||
}
|
||||
|
||||
private Stream<TranslationsSection> ruleTemplateSection(BundleInfo bundleInfo) {
|
||||
Builder<TranslationsSection> sectionBuilder = Stream.builder();
|
||||
|
||||
bundleInfo.getRuleTemplateJson().stream().flatMap(this::getRuleTemplate).forEach(sectionBuilder::add);
|
||||
return sectionBuilder.build().sorted(Comparator.comparing(s -> s.header));
|
||||
}
|
||||
|
||||
private Stream<TranslationsSection> getModuleType(JsonObject moduleType) {
|
||||
String uid = getAsString(moduleType, "uid").orElse("");
|
||||
if (uid.isBlank()) {
|
||||
return Stream.of();
|
||||
}
|
||||
|
||||
String globalPrefix = "module-type." + uid + ".";
|
||||
Builder<TranslationsGroup> groupBuilder = Stream.builder();
|
||||
|
||||
// global entries
|
||||
Builder<TranslationsEntry> entriesBuilder = Stream.builder();
|
||||
getAsString(moduleType, "label").ifPresent(label -> entriesBuilder.add(entry(globalPrefix + "label", label)));
|
||||
getAsString(moduleType, "description")
|
||||
.ifPresent(description -> entriesBuilder.add(entry(globalPrefix + "description", description)));
|
||||
groupBuilder.add(group(entriesBuilder.build()));
|
||||
|
||||
// configDescriptions
|
||||
JsonElement configDescriptionsElement = moduleType.get("configDescriptions");
|
||||
if (configDescriptionsElement != null) {
|
||||
String prefix = globalPrefix + "config.";
|
||||
groupBuilder.add(getGroupFromArray(configDescriptionsElement, prefix));
|
||||
}
|
||||
|
||||
// inputs
|
||||
JsonElement inputsElement = moduleType.get("inputs");
|
||||
if (inputsElement != null) {
|
||||
String prefix = globalPrefix + "input.";
|
||||
groupBuilder.add(getGroupFromArray(inputsElement, prefix));
|
||||
}
|
||||
|
||||
// outputs
|
||||
JsonElement outputsElement = moduleType.get("outputs");
|
||||
if (outputsElement != null) {
|
||||
String prefix = globalPrefix + "output.";
|
||||
groupBuilder.add(getGroupFromArray(outputsElement, prefix));
|
||||
}
|
||||
|
||||
return Stream.of(new TranslationsSection(uid, groupBuilder.build().collect(Collectors.toList())));
|
||||
}
|
||||
|
||||
private Stream<TranslationsSection> getRuleTemplate(JsonObject ruleTemplate) {
|
||||
String uid = getAsString(ruleTemplate, "uid").orElse("");
|
||||
if (uid.isBlank()) {
|
||||
return Stream.of();
|
||||
}
|
||||
|
||||
String globalPrefix = "rule-template." + uid + ".";
|
||||
Builder<TranslationsGroup> groupBuilder = Stream.builder();
|
||||
|
||||
// global entries
|
||||
Builder<TranslationsEntry> entriesBuilder = Stream.builder();
|
||||
getAsString(ruleTemplate, "label").ifPresent(label -> entriesBuilder.add(entry(globalPrefix + "label", label)));
|
||||
getAsString(ruleTemplate, "description")
|
||||
.ifPresent(description -> entriesBuilder.add(entry(globalPrefix + "description", description)));
|
||||
groupBuilder.add(group(entriesBuilder.build()));
|
||||
|
||||
return Stream.of(new TranslationsSection(uid, groupBuilder.build().collect(Collectors.toList())));
|
||||
}
|
||||
|
||||
private TranslationsGroup getGroupFromArray(JsonElement parentElement, String prefix) {
|
||||
Builder<TranslationsEntry> entriesBuilder = Stream.builder();
|
||||
for (JsonElement jsonElement : parentElement.getAsJsonArray()) {
|
||||
JsonObject jsonObject = jsonElement.getAsJsonObject();
|
||||
getAsString(jsonObject, "name").ifPresent(name -> {
|
||||
String namePrefix = prefix + name + ".";
|
||||
getAsString(jsonObject, "label")
|
||||
.ifPresent(label -> entriesBuilder.add(entry(namePrefix + "label", label)));
|
||||
getAsString(jsonObject, "description")
|
||||
.ifPresent(description -> entriesBuilder.add(entry(namePrefix + "description", description)));
|
||||
JsonElement optionsElement = jsonObject.get("options");
|
||||
if (optionsElement != null) {
|
||||
for (JsonElement optionElement : optionsElement.getAsJsonArray()) {
|
||||
JsonObject optionObject = optionElement.getAsJsonObject();
|
||||
getAsString(optionObject, "value")
|
||||
.ifPresent(value -> getAsString(optionObject, "label").ifPresent(label -> {
|
||||
String optionKey = namePrefix + "option."
|
||||
+ OPTION_ESCAPE_PATTERN.matcher(value).replaceAll("\\\\$1");
|
||||
entriesBuilder.add(entry(optionKey, label));
|
||||
}));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return group(entriesBuilder.build());
|
||||
}
|
||||
|
||||
private Optional<String> getAsString(JsonObject object, String key) {
|
||||
JsonElement element = object.get(key);
|
||||
return element == null ? Optional.empty() : Optional.of(element.getAsString());
|
||||
}
|
||||
}
|
||||
+6
@@ -49,6 +49,8 @@ public class BundleInfoReaderTest {
|
||||
assertThat(bundleInfo.getChannelTypesXml().size(), is(2));
|
||||
assertThat(bundleInfo.getConfigDescriptions().size(), is(1));
|
||||
assertThat(bundleInfo.getThingTypesXml().size(), is(2));
|
||||
assertThat(bundleInfo.getModuleTypesJson().size(), is(2));
|
||||
assertThat(bundleInfo.getRuleTemplateJson().size(), is(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -62,6 +64,8 @@ public class BundleInfoReaderTest {
|
||||
assertThat(bundleInfo.getChannelTypesXml().size(), is(0));
|
||||
assertThat(bundleInfo.getConfigDescriptions().size(), is(1));
|
||||
assertThat(bundleInfo.getThingTypesXml().size(), is(0));
|
||||
assertThat(bundleInfo.getModuleTypesJson().size(), is(0));
|
||||
assertThat(bundleInfo.getRuleTemplateJson().size(), is(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -75,5 +79,7 @@ public class BundleInfoReaderTest {
|
||||
assertThat(bundleInfo.getChannelTypesXml().size(), is(0));
|
||||
assertThat(bundleInfo.getConfigDescriptions().size(), is(0));
|
||||
assertThat(bundleInfo.getThingTypesXml().size(), is(0));
|
||||
assertThat(bundleInfo.getModuleTypesJson().size(), is(0));
|
||||
assertThat(bundleInfo.getRuleTemplateJson().size(), is(0));
|
||||
}
|
||||
}
|
||||
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2022 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.core.tools.i18n.plugin;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.containsString;
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.maven.plugin.logging.SystemStreamLog;
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Tests {@link JsonToTranslationsConverter}.
|
||||
*
|
||||
* @author Jan N. Klug - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class JsonToTranslationsConverterTest {
|
||||
|
||||
@Test
|
||||
public void convertModuleTypeInfo() throws IOException {
|
||||
BundleInfoReader reader = new BundleInfoReader(new SystemStreamLog());
|
||||
BundleInfo bundleInfo = reader.readBundleInfo(Path.of("src/test/resources/acmeweather.bundle/OH-INF"));
|
||||
|
||||
JsonToTranslationsConverter converter = new JsonToTranslationsConverter();
|
||||
Translations translations = converter.convert(bundleInfo);
|
||||
|
||||
assertThat(translations.hasTranslations(), is(true));
|
||||
assertThat(translations.sections.size(), is(2));
|
||||
assertThat(translations.keysStream().count(), is(10L));
|
||||
|
||||
String lines = translations.linesStream().collect(Collectors.joining(System.lineSeparator()));
|
||||
assertThat(lines, containsString("acme.WeatherBadTrigger.label = Weather Bad Trigger"));
|
||||
assertThat(lines, containsString(
|
||||
"module-type.acme.WeatherBadTrigger.config.minimumLevel.description = The minimum level that results in a trigger"));
|
||||
assertThat(lines, containsString("module-type.acme.WeatherBadTrigger.output.level.label = Level"));
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -38,8 +38,8 @@ public class PropertiesToTranslationsConverterTest {
|
||||
.convert(Path.of("src/test/resources/acmeweather.bundle/OH-INF/i18n/acmeweather.properties"));
|
||||
|
||||
assertThat(translations.hasTranslations(), is(true));
|
||||
assertThat(translations.sections.size(), is(6));
|
||||
assertThat(translations.keysStream().count(), is(34L));
|
||||
assertThat(translations.sections.size(), is(8));
|
||||
assertThat(translations.keysStream().count(), is(44L));
|
||||
|
||||
String lines = translations.linesStream().collect(Collectors.joining(System.lineSeparator()));
|
||||
assertThat(lines, containsString("# binding"));
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"triggers": [
|
||||
{
|
||||
"uid": "acme.WeatherBadTrigger",
|
||||
"label": "Weather Bad Trigger",
|
||||
"description": "Triggers when weather is bad",
|
||||
"configDescriptions": [
|
||||
{
|
||||
"name": "minimumLevel",
|
||||
"type": "INTEGER",
|
||||
"label": "Minimum Level",
|
||||
"description": "The minimum level that results in a trigger",
|
||||
"required": true,
|
||||
"default": "5"
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "level",
|
||||
"type": "java.lang.Integer",
|
||||
"label": "Level",
|
||||
"description": "The level that triggered this event."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"conditions": [
|
||||
{
|
||||
"uid": "acme.WeatherCondition",
|
||||
"label": "the weather is good",
|
||||
"description": "Compares the weather to a weather level (0 - 10)",
|
||||
"configDescriptions": [
|
||||
{
|
||||
"name": "threshold",
|
||||
"type": "INTEGER",
|
||||
"label": "Threshold",
|
||||
"description": "the maximum weather level that is considered good",
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+16
@@ -46,3 +46,19 @@ channel-type.acmeweather.temperature.state.option.VALUE\=3 = My label 3
|
||||
channel-type.acmeweather.time-stamp.label = Observation Time
|
||||
channel-type.acmeweather.time-stamp.description = Time of data observation.
|
||||
channel-type.acmeweather.time-stamp.state.pattern = %1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS
|
||||
|
||||
# acme.WeatherBadTrigger
|
||||
|
||||
module-type.acme.WeatherBadTrigger.label = Weather Bad Trigger
|
||||
module-type.acme.WeatherBadTrigger.description = Triggers when weather is bad
|
||||
module-type.acme.WeatherBadTrigger.config.minimumLevel.label = Minimum Level
|
||||
module-type.acme.WeatherBadTrigger.config.minimumLevel.description = The minimum level that results in a trigger
|
||||
module-type.acme.WeatherBadTrigger.output.level.label = Level
|
||||
module-type.acme.WeatherBadTrigger.output.level.description = The level that triggered this event.
|
||||
|
||||
# acme.WeatherCondition
|
||||
|
||||
module-type.acme.WeatherCondition.label = the weather is good
|
||||
module-type.acme.WeatherCondition.description = Compares the weather to a weather level (0 - 10)
|
||||
module-type.acme.WeatherCondition.config.threshold.label = Threshold
|
||||
module-type.acme.WeatherCondition.config.threshold.description = the maximum weather level that is considered good
|
||||
|
||||
+16
@@ -47,6 +47,22 @@ channel-type.acmeweather.time-stamp.label = Observation Time
|
||||
channel-type.acmeweather.time-stamp.description = Time of data observation.
|
||||
channel-type.acmeweather.time-stamp.state.pattern = %1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS
|
||||
|
||||
# acme.WeatherBadTrigger
|
||||
|
||||
module-type.acme.WeatherBadTrigger.label = Weather Bad Trigger
|
||||
module-type.acme.WeatherBadTrigger.description = Triggers when weather is bad
|
||||
module-type.acme.WeatherBadTrigger.config.minimumLevel.label = Minimum Level
|
||||
module-type.acme.WeatherBadTrigger.config.minimumLevel.description = The minimum level that results in a trigger
|
||||
module-type.acme.WeatherBadTrigger.output.level.label = Level
|
||||
module-type.acme.WeatherBadTrigger.output.level.description = The level that triggered this event.
|
||||
|
||||
# acme.WeatherCondition
|
||||
|
||||
module-type.acme.WeatherCondition.label = the weather is good
|
||||
module-type.acme.WeatherCondition.description = Compares the weather to a weather level (0 - 10)
|
||||
module-type.acme.WeatherCondition.config.threshold.label = Threshold
|
||||
module-type.acme.WeatherCondition.config.threshold.description = the maximum weather level that is considered good
|
||||
|
||||
# custom
|
||||
|
||||
CUSTOM_KEY = Provides various weather data from the ACME weather service
|
||||
|
||||
Reference in New Issue
Block a user