[miio] i18n translation handling for basic channels (#11576)

Signed-off-by: Marcel Verpaalen <marcel@verpaalen.com>
This commit is contained in:
Marcel 2021-12-02 09:08:17 +01:00 committed by GitHub
parent 7c8a197e17
commit 82ac5ee2d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 2640 additions and 51 deletions

View File

@ -123,4 +123,8 @@ public final class MiIoBindingConstants {
+ File.separator + BINDING_ID;
public static final String BINDING_USERDATA_PATH = OpenHAB.getUserDataFolder() + File.separator
+ MiIoBindingConstants.BINDING_ID;
public static final String I18N_THING_PREFIX = "thing.";
public static final String I18N_CHANNEL_PREFIX = "ch.";
public static final String I18N_OPTION_PREFIX = "option.";
}

View File

@ -29,6 +29,8 @@ import org.openhab.binding.miio.internal.handler.MiIoGenericHandler;
import org.openhab.binding.miio.internal.handler.MiIoUnsupportedHandler;
import org.openhab.binding.miio.internal.handler.MiIoVacuumHandler;
import org.openhab.core.common.ThreadPoolManager;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TranslationProvider;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
@ -60,6 +62,8 @@ public class MiIoHandlerFactory extends BaseThingHandlerFactory {
private CloudConnector cloudConnector;
private ChannelTypeRegistry channelTypeRegistry;
private BasicChannelTypeProvider basicChannelTypeProvider;
private final TranslationProvider i18nProvider;
private final LocaleProvider localeProvider;
private @Nullable Future<Boolean> scheduledTask;
private final Logger logger = LoggerFactory.getLogger(MiIoHandlerFactory.class);
@ -67,11 +71,14 @@ public class MiIoHandlerFactory extends BaseThingHandlerFactory {
public MiIoHandlerFactory(@Reference HttpClientFactory httpClientFactory,
@Reference ChannelTypeRegistry channelTypeRegistry,
@Reference MiIoDatabaseWatchService miIoDatabaseWatchService, @Reference CloudConnector cloudConnector,
@Reference BasicChannelTypeProvider basicChannelTypeProvider, Map<String, Object> properties) {
@Reference BasicChannelTypeProvider basicChannelTypeProvider, @Reference TranslationProvider i18nProvider,
@Reference LocaleProvider localeProvider, Map<String, Object> properties) {
this.httpClientFactory = httpClientFactory;
this.miIoDatabaseWatchService = miIoDatabaseWatchService;
this.channelTypeRegistry = channelTypeRegistry;
this.basicChannelTypeProvider = basicChannelTypeProvider;
this.i18nProvider = i18nProvider;
this.localeProvider = localeProvider;
this.cloudConnector = cloudConnector;
@Nullable
String username = (String) properties.get("username");
@ -108,16 +115,18 @@ public class MiIoHandlerFactory extends BaseThingHandlerFactory {
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (thingTypeUID.equals(THING_TYPE_MIIO)) {
return new MiIoGenericHandler(thing, miIoDatabaseWatchService, cloudConnector);
return new MiIoGenericHandler(thing, miIoDatabaseWatchService, cloudConnector, i18nProvider,
localeProvider);
}
if (thingTypeUID.equals(THING_TYPE_BASIC)) {
return new MiIoBasicHandler(thing, miIoDatabaseWatchService, cloudConnector, channelTypeRegistry,
basicChannelTypeProvider);
basicChannelTypeProvider, i18nProvider, localeProvider);
}
if (thingTypeUID.equals(THING_TYPE_VACUUM)) {
return new MiIoVacuumHandler(thing, miIoDatabaseWatchService, cloudConnector, channelTypeRegistry);
return new MiIoVacuumHandler(thing, miIoDatabaseWatchService, cloudConnector, channelTypeRegistry,
i18nProvider, localeProvider);
}
return new MiIoUnsupportedHandler(thing, miIoDatabaseWatchService, cloudConnector,
httpClientFactory.getCommonHttpClient());
httpClientFactory.getCommonHttpClient(), i18nProvider, localeProvider);
}
}

View File

@ -50,6 +50,8 @@ import org.openhab.binding.miio.internal.transport.MiIoAsyncCommunication;
import org.openhab.core.cache.ExpiringCache;
import org.openhab.core.common.NamedThreadFactory;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TranslationProvider;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.ChannelUID;
@ -60,6 +62,8 @@ import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.builder.ThingBuilder;
import org.openhab.core.types.Command;
import org.osgi.framework.Bundle;
import org.osgi.framework.FrameworkUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -81,6 +85,9 @@ public abstract class MiIoAbstractHandler extends BaseThingHandler implements Mi
protected static final int MAX_QUEUE = 5;
protected static final Gson GSON = new GsonBuilder().create();
protected static final String TIMESTAMP = "timestamp";
protected final Bundle bundle;
protected final TranslationProvider i18nProvider;
protected final LocaleProvider localeProvider;
protected ScheduledExecutorService miIoScheduler = new ScheduledThreadPoolExecutor(3,
new NamedThreadFactory("binding-" + getThing().getUID().getAsString(), true));
@ -114,10 +121,13 @@ public abstract class MiIoAbstractHandler extends BaseThingHandler implements Mi
protected MiIoDatabaseWatchService miIoDatabaseWatchService;
public MiIoAbstractHandler(Thing thing, MiIoDatabaseWatchService miIoDatabaseWatchService,
CloudConnector cloudConnector) {
CloudConnector cloudConnector, TranslationProvider i18nProvider, LocaleProvider localeProvider) {
super(thing);
this.miIoDatabaseWatchService = miIoDatabaseWatchService;
this.cloudConnector = cloudConnector;
this.i18nProvider = i18nProvider;
this.localeProvider = localeProvider;
this.bundle = FrameworkUtil.getBundle(this.getClass());
}
@Override
@ -590,7 +600,8 @@ public abstract class MiIoAbstractHandler extends BaseThingHandler implements Mi
String label = getThing().getLabel();
if (label == null || label.startsWith("Xiaomi Mi Device")) {
ThingBuilder thingBuilder = editThing();
thingBuilder.withLabel(miDevice.getDescription());
label = getLocalText(I18N_THING_PREFIX + modelId, miDevice.getDescription());
thingBuilder.withLabel(label);
updateThing(thingBuilder.build());
}
logger.info("Mi Device model {} identified as: {}. Does not match thingtype {}. Changing thingtype to {}",
@ -644,4 +655,13 @@ public abstract class MiIoAbstractHandler extends BaseThingHandler implements Mi
logger.debug("Error while handing message {}", response.getResponse(), e);
}
}
protected String getLocalText(String key, String defaultText) {
try {
String text = i18nProvider.getText(bundle, key, defaultText, localeProvider.getLocale());
return text != null ? text : defaultText;
} catch (IllegalArgumentException e) {
return defaultText;
}
}
}

View File

@ -49,6 +49,8 @@ import org.openhab.binding.miio.internal.basic.MiIoDeviceActionCondition;
import org.openhab.binding.miio.internal.cloud.CloudConnector;
import org.openhab.binding.miio.internal.transport.MiIoAsyncCommunication;
import org.openhab.core.cache.ExpiringCache;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TranslationProvider;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.OnOffType;
@ -106,8 +108,9 @@ public class MiIoBasicHandler extends MiIoAbstractHandler {
public MiIoBasicHandler(Thing thing, MiIoDatabaseWatchService miIoDatabaseWatchService,
CloudConnector cloudConnector, ChannelTypeRegistry channelTypeRegistry,
BasicChannelTypeProvider basicChannelTypeProvider) {
super(thing, miIoDatabaseWatchService, cloudConnector);
BasicChannelTypeProvider basicChannelTypeProvider, TranslationProvider i18nProvider,
LocaleProvider localeProvider) {
super(thing, miIoDatabaseWatchService, cloudConnector, i18nProvider, localeProvider);
this.channelTypeRegistry = channelTypeRegistry;
this.basicChannelTypeProvider = basicChannelTypeProvider;
}
@ -452,6 +455,7 @@ public class MiIoBasicHandler extends MiIoAbstractHandler {
try {
JsonObject deviceMapping = Utils.convertFileToJSON(fn);
logger.debug("Using device database: {} for device {}", fn.getFile(), deviceName);
String key = fn.getFile().replaceFirst("/database/", "").split("json")[0];
Gson gson = new GsonBuilder().serializeNulls().create();
miioDevice = gson.fromJson(deviceMapping, MiIoBasicDevice.class);
for (Channel ch : getThing().getChannels()) {
@ -475,7 +479,7 @@ public class MiIoBasicHandler extends MiIoAbstractHandler {
logger.debug("properties {}", miChannel);
if (!miChannel.getType().isEmpty()) {
basicChannelTypeProvider.addChannelType(miChannel, deviceName);
ChannelUID channelUID = addChannel(thingBuilder, miChannel, deviceName);
ChannelUID channelUID = addChannel(thingBuilder, miChannel, deviceName, key);
if (channelUID != null) {
actions.put(channelUID, miChannel);
channelsAdded++;
@ -505,7 +509,8 @@ public class MiIoBasicHandler extends MiIoAbstractHandler {
return false;
}
private @Nullable ChannelUID addChannel(ThingBuilder thingBuilder, MiIoBasicChannel miChannel, String model) {
private @Nullable ChannelUID addChannel(ThingBuilder thingBuilder, MiIoBasicChannel miChannel, String model,
String key) {
String channel = miChannel.getChannel();
String dataType = miChannel.getType();
if (channel.isEmpty() || dataType.isEmpty()) {
@ -514,7 +519,8 @@ public class MiIoBasicHandler extends MiIoAbstractHandler {
return null;
}
ChannelUID channelUID = new ChannelUID(getThing().getUID(), channel);
ChannelBuilder newChannel = ChannelBuilder.create(channelUID, dataType).withLabel(miChannel.getFriendlyName());
String label = getLocalText(I18N_CHANNEL_PREFIX + key + channel, miChannel.getFriendlyName());
ChannelBuilder newChannel = ChannelBuilder.create(channelUID, dataType).withLabel(label);
boolean useGeneratedChannelType = false;
if (!miChannel.getChannelType().isBlank()) {
ChannelTypeUID channelTypeUID = new ChannelTypeUID(miChannel.getChannelType());

View File

@ -15,6 +15,8 @@ package org.openhab.binding.miio.internal.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.miio.internal.basic.MiIoDatabaseWatchService;
import org.openhab.binding.miio.internal.cloud.CloudConnector;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TranslationProvider;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
@ -33,8 +35,8 @@ public class MiIoGenericHandler extends MiIoAbstractHandler {
private final Logger logger = LoggerFactory.getLogger(MiIoGenericHandler.class);
public MiIoGenericHandler(Thing thing, MiIoDatabaseWatchService miIoDatabaseWatchService,
CloudConnector cloudConnector) {
super(thing, miIoDatabaseWatchService, cloudConnector);
CloudConnector cloudConnector, TranslationProvider i18nProvider, LocaleProvider localeProvider) {
super(thing, miIoDatabaseWatchService, cloudConnector, i18nProvider, localeProvider);
}
@Override

View File

@ -44,6 +44,8 @@ import org.openhab.binding.miio.internal.basic.MiIoDatabaseWatchService;
import org.openhab.binding.miio.internal.cloud.CloudConnector;
import org.openhab.binding.miio.internal.miot.MiotParser;
import org.openhab.core.cache.ExpiringCache;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TranslationProvider;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
@ -87,8 +89,9 @@ public class MiIoUnsupportedHandler extends MiIoAbstractHandler {
});
public MiIoUnsupportedHandler(Thing thing, MiIoDatabaseWatchService miIoDatabaseWatchService,
CloudConnector cloudConnector, HttpClient httpClientFactory) {
super(thing, miIoDatabaseWatchService, cloudConnector);
CloudConnector cloudConnector, HttpClient httpClientFactory, TranslationProvider i18nProvider,
LocaleProvider localeProvider) {
super(thing, miIoDatabaseWatchService, cloudConnector, i18nProvider, localeProvider);
this.httpClient = httpClientFactory;
}

View File

@ -52,6 +52,8 @@ import org.openhab.binding.miio.internal.robot.StatusType;
import org.openhab.binding.miio.internal.robot.VacuumErrorType;
import org.openhab.binding.miio.internal.transport.MiIoAsyncCommunication;
import org.openhab.core.cache.ExpiringCache;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TranslationProvider;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
@ -113,8 +115,9 @@ public class MiIoVacuumHandler extends MiIoAbstractHandler {
private RRMapDrawOptions mapDrawOptions = new RRMapDrawOptions();
public MiIoVacuumHandler(Thing thing, MiIoDatabaseWatchService miIoDatabaseWatchService,
CloudConnector cloudConnector, ChannelTypeRegistry channelTypeRegistry) {
super(thing, miIoDatabaseWatchService, cloudConnector);
CloudConnector cloudConnector, ChannelTypeRegistry channelTypeRegistry, TranslationProvider i18nProvider,
LocaleProvider localeProvider) {
super(thing, miIoDatabaseWatchService, cloudConnector, i18nProvider, localeProvider);
this.channelTypeRegistry = channelTypeRegistry;
mapChannelUid = new ChannelUID(thing.getUID(), CHANNEL_VACUUM_MAP);
status = new ExpiringCache<>(CACHE_EXPIRY, () -> {

File diff suppressed because it is too large Load Diff

View File

@ -12,7 +12,10 @@
*/
package org.openhab.binding.miio.internal;
import static org.openhab.binding.miio.internal.MiIoBindingConstants.*;
import java.io.File;
import java.io.FileFilter;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
@ -24,6 +27,9 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
@ -59,6 +65,7 @@ public class ReadmeHelper {
private static final String BASEFILE = "./README.base.md";
private static final String OUTPUTFILE = "./README.md";
private static final String DEVICE_NAMES_FILE = "./src/main/resources/misc/device_names.json";
private static final String I18N_CHANNEL_FILE = "./src/main/resources/OH-INF/i18n/basic.properties";
private static final boolean UPDATE_OPTION_MAPPING_README_COMMENTS = true;
@Disabled
@ -71,7 +78,6 @@ public class ReadmeHelper {
StringWriter channelList = rm.channelList();
LOGGER.info("## Creating Item Files for miio:basic devices");
StringWriter itemFileExamples = rm.itemFileExamples();
LOGGER.info("## Done");
try {
String baseDoc = new String(Files.readAllBytes(Paths.get(BASEFILE)), StandardCharsets.UTF_8);
String newDoc = baseDoc.replaceAll("!!!devices", deviceList.toString())
@ -79,8 +85,34 @@ public class ReadmeHelper {
.replaceAll("!!!itemFileExamples", itemFileExamples.toString());
Files.write(Paths.get(OUTPUTFILE), newDoc.getBytes(StandardCharsets.UTF_8));
} catch (IOException e) {
LOGGER.warn("IO exception", e);
LOGGER.warn("IO exception writing readme", e);
}
LOGGER.info("## Creating i18n entries for devices and miio:basic channels");
StringBuilder sb = new StringBuilder();
sb.append("# Automatic created list by miio readme maker for miio devices & database channels\n\n");
sb.append("# Devices\n\n");
for (MiIoDevices d : Arrays.asList(MiIoDevices.values())) {
sb.append(I18N_THING_PREFIX);
sb.append(d.getModel());
sb.append(" = ");
sb.append(d.getDescription());
sb.append("\n");
}
sb.append("\n# Channels\n\n");
for (Entry<String, String> e : sortByKeys(rm.createI18nEntries()).entrySet()) {
sb.append(e.getKey());
sb.append(" = ");
sb.append(e.getValue());
sb.append("\n");
}
sb.append("\n");
try {
Files.write(Paths.get(I18N_CHANNEL_FILE), sb.toString().getBytes(StandardCharsets.UTF_8));
} catch (IOException e) {
LOGGER.warn("IO exception creating i18n file", e);
}
LOGGER.info("## Done");
}
private StringWriter deviceList() {
@ -163,26 +195,22 @@ public class ReadmeHelper {
}
public static String readmeOptionMapping(MiIoBasicChannel channel, String model) {
StateDescriptionDTO stateDescription = channel.getStateDescription();
if (stateDescription != null && stateDescription.getOptions() != null) {
final List<OptionsValueListDTO> options = stateDescription.getOptions();
if (options != null && !options.isEmpty()) {
final List<OptionsValueListDTO> options = getChannelOptions(channel);
if (!options.isEmpty()) {
StringBuilder mapping = new StringBuilder();
mapping.append("Value mapping `[");
options.forEach((option) -> {
mapping.append(String.format("\"%s\"=\"%s\",", String.valueOf(option.value),
String.valueOf(option.label)));
mapping.append(
String.format("\"%s\"=\"%s\",", String.valueOf(option.value), String.valueOf(option.label)));
});
mapping.deleteCharAt(mapping.length() - 1);
mapping.append("]`");
String newComment = mapping.toString();
if (!channel.getReadmeComment().contentEquals(newComment)) {
LOGGER.info("Channel {} - {} readme comment updated to '{}'", model, channel.getChannel(),
newComment);
LOGGER.info("Channel {} - {} readme comment updated to '{}'", model, channel.getChannel(), newComment);
}
return newComment;
}
}
return channel.getReadmeComment();
}
@ -264,9 +292,12 @@ public class ReadmeHelper {
List<MiIoBasicDevice> arrayList = new ArrayList<>();
String path = "./src/main/resources/database/";
File dir = new File(path);
File[] filesList = dir.listFiles();
FileFilter fileFilter = file -> !file.isDirectory() && file.getName().toLowerCase().endsWith(".json");
File[] filesList = dir.listFiles(fileFilter);
if (filesList == null) {
return arrayList;
}
for (File file : filesList) {
if (file.isFile()) {
try {
JsonObject deviceMapping = convertFileToJSON(path + file.getName());
Gson gson = new GsonBuilder().serializeNulls().create();
@ -279,10 +310,62 @@ public class ReadmeHelper {
LOGGER.info("Error while searching in database '{}': {}", file.getName(), e.getMessage());
}
}
}
return arrayList;
}
public static List<OptionsValueListDTO> getChannelOptions(MiIoBasicChannel channel) {
StateDescriptionDTO state = channel.getStateDescription();
if (state != null) {
List<OptionsValueListDTO> options = state.getOptions();
if (options != null) {
return options;
}
}
return List.of();
}
private Map<String, String> createI18nEntries() {
Map<String, String> i18nEntries = new HashMap<>();
String path = "./src/main/resources/database/";
File dir = new File(path);
FileFilter fileFilter = file -> !file.isDirectory() && file.getName().toLowerCase().endsWith(".json");
File[] filesList = dir.listFiles(fileFilter);
if (filesList == null) {
return i18nEntries;
}
for (File file : filesList) {
try {
String key = file.getName().toLowerCase().split("json")[0];
JsonObject deviceMapping = convertFileToJSON(path + file.getName());
Gson gson = new GsonBuilder().serializeNulls().create();
@Nullable
MiIoBasicDevice devdb = gson.fromJson(deviceMapping, MiIoBasicDevice.class);
if (devdb == null) {
continue;
}
for (MiIoBasicChannel channel : devdb.getDevice().getChannels()) {
i18nEntries.put(I18N_CHANNEL_PREFIX + key + channel.getChannel(), channel.getFriendlyName());
List<OptionsValueListDTO> options = getChannelOptions(channel);
for (OptionsValueListDTO channelOption : options) {
String optionValue = channelOption.value;
String optionLabel = channelOption.label;
if (optionValue != null && optionLabel != null) {
i18nEntries.put(I18N_OPTION_PREFIX + key + channel.getChannel() + "-" + optionValue,
optionLabel);
}
}
}
} catch (Exception e) {
LOGGER.info("Error while searching in database '{}': {}", file.getName(), e.getMessage());
}
}
return i18nEntries;
}
public static <K extends Comparable, V> Map<K, V> sortByKeys(Map<K, V> map) {
return new TreeMap<>(map);
}
private static String minLengthString(String string, int length) {
return String.format("%-" + length + "s", string);
}
@ -290,7 +373,6 @@ public class ReadmeHelper {
JsonObject convertFileToJSON(String fileName) {
// Read from File to String
JsonObject jsonObject = new JsonObject();
try {
JsonElement jsonElement = JsonParser.parseReader(new FileReader(fileName));
jsonObject = jsonElement.getAsJsonObject();