YAML things provider: create things even if binding is not yet installed (#4753)

Makes it consistent with managed thing provider.

Removes the OSGi reference between YamlThingProvider and YamlModelRepository and as consequence removes the circular reference.

Signed-off-by: Laurent Garnier <lg.hc@free.fr>
pull/4776/head
lolodomo 2025-05-11 12:57:21 +02:00 committed by GitHub
parent fd171e26d7
commit 3df33758d4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 107 additions and 112 deletions

View File

@ -21,7 +21,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
* The {@link YamlModelRepository} defines methods to update elements in a YAML model. * The {@link YamlModelRepository} defines methods to update elements in a YAML model.
* *
* @author Jan N. Klug - Initial contribution * @author Jan N. Klug - Initial contribution
* @author Laurent Garnier - Added methods refreshModelElements and generateSyntaxFromElements * @author Laurent Garnier - Added method generateSyntaxFromElements
*/ */
@NonNullByDefault @NonNullByDefault
public interface YamlModelRepository { public interface YamlModelRepository {
@ -31,14 +31,6 @@ public interface YamlModelRepository {
void updateElementInModel(String modelName, YamlElement element); void updateElementInModel(String modelName, YamlElement element);
/**
* Triggers the refresh of a certain type of elements in a given model.
*
* @param modelName the model name
* @param elementName the type of elements to refresh
*/
void refreshModelElements(String modelName, String elementName);
/** /**
* Generate the YAML syntax from a provided list of elements. * Generate the YAML syntax from a provided list of elements.
* *

View File

@ -77,7 +77,7 @@ import com.fasterxml.jackson.dataformat.yaml.YAMLParser;
* @author Jan N. Klug - Refactored for multiple types per file and add modifying possibility * @author Jan N. Klug - Refactored for multiple types per file and add modifying possibility
* @author Laurent Garnier - Introduce version 2 using map instead of table * @author Laurent Garnier - Introduce version 2 using map instead of table
* @author Laurent Garnier - Added basic version management * @author Laurent Garnier - Added basic version management
* @author Laurent Garnier - Added methods refreshModelElements and generateSyntaxFromElements + new parameters * @author Laurent Garnier - Added method generateSyntaxFromElements + new parameters
* for method isValid * for method isValid
*/ */
@NonNullByDefault @NonNullByDefault
@ -564,35 +564,6 @@ public class YamlModelRepositoryImpl implements WatchService.WatchEventListener,
writeModel(modelName); writeModel(modelName);
} }
@Override
@SuppressWarnings({ "rawtypes", "unchecked" })
public void refreshModelElements(String modelName, String elementName) {
logger.info("Refreshing {} from YAML model {}", elementName, modelName);
YamlModelWrapper model = modelCache.get(modelName);
if (model == null) {
logger.warn("Failed to refresh model {} because it is not known.", modelName);
return;
}
List<JsonNode> modelNodes = model.getNodesV1().get(elementName);
JsonNode modelMapNode = model.getNodes().get(elementName);
if (modelNodes == null && modelMapNode == null) {
logger.warn("Failed to refresh model {} because type {} is not known in the model.", modelName,
elementName);
return;
}
getElementListeners(elementName, model.getVersion()).forEach(listener -> {
Class<? extends YamlElement> elementClass = listener.getElementClass();
List elements = parseJsonNodes(modelNodes != null ? modelNodes : List.of(), modelMapNode, elementClass,
null, null);
if (!elements.isEmpty()) {
listener.updatedModel(modelName, elements);
}
});
}
private void writeModel(String modelName) { private void writeModel(String modelName) {
YamlModelWrapper model = modelCache.get(modelName); YamlModelWrapper model = modelCache.get(modelName);
if (model == null) { if (model == null) {

View File

@ -32,9 +32,7 @@ import org.openhab.core.config.core.ConfigDescriptionRegistry;
import org.openhab.core.config.core.ConfigUtil; import org.openhab.core.config.core.ConfigUtil;
import org.openhab.core.config.core.Configuration; import org.openhab.core.config.core.Configuration;
import org.openhab.core.i18n.LocaleProvider; import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.model.yaml.YamlElementName;
import org.openhab.core.model.yaml.YamlModelListener; import org.openhab.core.model.yaml.YamlModelListener;
import org.openhab.core.model.yaml.YamlModelRepository;
import org.openhab.core.service.ReadyMarker; import org.openhab.core.service.ReadyMarker;
import org.openhab.core.service.ReadyService; import org.openhab.core.service.ReadyService;
import org.openhab.core.service.StartLevelService; import org.openhab.core.service.StartLevelService;
@ -84,7 +82,6 @@ public class YamlThingProvider extends AbstractProvider<Thing>
private final Logger logger = LoggerFactory.getLogger(YamlThingProvider.class); private final Logger logger = LoggerFactory.getLogger(YamlThingProvider.class);
private final YamlModelRepository modelRepository;
private final BundleResolver bundleResolver; private final BundleResolver bundleResolver;
private final ThingTypeRegistry thingTypeRegistry; private final ThingTypeRegistry thingTypeRegistry;
private final ChannelTypeRegistry channelTypeRegistry; private final ChannelTypeRegistry channelTypeRegistry;
@ -104,32 +101,8 @@ public class YamlThingProvider extends AbstractProvider<Thing>
logger.debug("Starting lazy retry thread"); logger.debug("Starting lazy retry thread");
while (!queue.isEmpty()) { while (!queue.isEmpty()) {
for (QueueContent qc : queue) { for (QueueContent qc : queue) {
logger.trace("Retry creating thing {}", qc.thingUID); if (retryCreateThing(qc.thingHandlerFactory, qc.thingTypeUID, qc.configuration, qc.thingUID,
Thing newThing = qc.thingHandlerFactory.createThing(qc.thingTypeUID, qc.configuration, qc.thingUID, qc.bridgeUID)) {
qc.bridgeUID);
if (newThing != null) {
logger.debug("Successfully loaded thing \'{}\' during retry", qc.thingUID);
Thing oldThing = null;
for (Map.Entry<String, Collection<Thing>> entry : thingsMap.entrySet()) {
oldThing = entry.getValue().stream().filter(t -> t.getUID().equals(newThing.getUID()))
.findFirst().orElse(null);
if (oldThing != null) {
mergeThing(newThing, oldThing);
Collection<Thing> thingsForModel = Objects
.requireNonNull(thingsMap.get(entry.getKey()));
thingsForModel.remove(oldThing);
thingsForModel.add(newThing);
logger.debug("Refreshing thing \'{}\' after successful retry", newThing.getUID());
if (!ThingHelper.equals(oldThing, newThing)) {
notifyListenersAboutUpdatedElement(oldThing, newThing);
}
break;
}
}
if (oldThing == null) {
logger.debug("Refreshing thing \'{}\' after retry failed because thing is not found",
newThing.getUID());
}
queue.remove(qc); queue.remove(qc);
} }
} }
@ -153,12 +126,11 @@ public class YamlThingProvider extends AbstractProvider<Thing>
} }
@Activate @Activate
public YamlThingProvider(final @Reference YamlModelRepository modelRepository, public YamlThingProvider(final @Reference BundleResolver bundleResolver,
final @Reference BundleResolver bundleResolver, final @Reference ThingTypeRegistry thingTypeRegistry, final @Reference ThingTypeRegistry thingTypeRegistry,
final @Reference ChannelTypeRegistry channelTypeRegistry, final @Reference ChannelTypeRegistry channelTypeRegistry,
final @Reference ConfigDescriptionRegistry configDescriptionRegistry, final @Reference ConfigDescriptionRegistry configDescriptionRegistry,
final @Reference LocaleProvider localeProvider) { final @Reference LocaleProvider localeProvider) {
this.modelRepository = modelRepository;
this.bundleResolver = bundleResolver; this.bundleResolver = bundleResolver;
this.thingTypeRegistry = thingTypeRegistry; this.thingTypeRegistry = thingTypeRegistry;
this.channelTypeRegistry = channelTypeRegistry; this.channelTypeRegistry = channelTypeRegistry;
@ -242,6 +214,7 @@ public class YamlThingProvider extends AbstractProvider<Thing>
@Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC)
protected void addThingHandlerFactory(final ThingHandlerFactory thingHandlerFactory) { protected void addThingHandlerFactory(final ThingHandlerFactory thingHandlerFactory) {
logger.debug("addThingHandlerFactory {}", thingHandlerFactory.getClass().getSimpleName());
thingHandlerFactories.add(thingHandlerFactory); thingHandlerFactories.add(thingHandlerFactory);
thingHandlerFactoryAdded(thingHandlerFactory); thingHandlerFactoryAdded(thingHandlerFactory);
} }
@ -278,18 +251,75 @@ public class YamlThingProvider extends AbstractProvider<Thing>
loadedXmlThingTypes.remove(readyMarker.getIdentifier()); loadedXmlThingTypes.remove(readyMarker.getIdentifier());
} }
private void thingHandlerFactoryAdded(ThingHandlerFactory thingHandlerFactory) { private void thingHandlerFactoryAdded(ThingHandlerFactory handlerFactory) {
String bundleName = getBundleName(thingHandlerFactory); logger.debug("thingHandlerFactoryAdded {} isThingHandlerFactoryReady={}",
if (bundleName != null && loadedXmlThingTypes.contains(bundleName)) { handlerFactory.getClass().getSimpleName(), isThingHandlerFactoryReady(handlerFactory));
logger.debug("Refreshing models due to new thing handler factory {}", if (isThingHandlerFactoryReady(handlerFactory)) {
thingHandlerFactory.getClass().getSimpleName()); if (!thingsMap.isEmpty()) {
thingsMap.keySet().forEach(modelName -> { logger.debug("Refreshing models due to new thing handler factory {}",
modelRepository.refreshModelElements(modelName, handlerFactory.getClass().getSimpleName());
getElementClass().getAnnotation(YamlElementName.class).value()); thingsMap.keySet().forEach(modelName -> {
}); List<Thing> things = thingsMap.getOrDefault(modelName, List.of()).stream()
.filter(th -> handlerFactory.supportsThingType(th.getThingTypeUID())).toList();
if (!things.isEmpty()) {
logger.info("Refreshing YAML model {} ({} things with {})", modelName, things.size(),
handlerFactory.getClass().getSimpleName());
things.forEach(thing -> {
if (!retryCreateThing(handlerFactory, thing.getThingTypeUID(), thing.getConfiguration(),
thing.getUID(), thing.getBridgeUID())) {
// Possible cause: Asynchronous loading of the XML files
// Add the data to the queue in order to retry it later
logger.debug(
"ThingHandlerFactory \'{}\' claimed it can handle \'{}\' type but actually did not. Queued for later refresh.",
handlerFactory.getClass().getSimpleName(), thing.getThingTypeUID());
queueRetryThingCreation(handlerFactory, thing.getThingTypeUID(),
thing.getConfiguration(), thing.getUID(), thing.getBridgeUID());
}
});
} else {
logger.debug("No refresh needed from YAML model {}", modelName);
}
});
} else {
logger.debug("No things yet loaded; no need to trigger a refresh due to new thing handler factory");
}
} }
} }
private boolean retryCreateThing(ThingHandlerFactory handlerFactory, ThingTypeUID thingTypeUID,
Configuration configuration, ThingUID thingUID, @Nullable ThingUID bridgeUID) {
logger.trace("Retry creating thing {}", thingUID);
Thing newThing = handlerFactory.createThing(thingTypeUID, configuration, thingUID, bridgeUID);
if (newThing != null) {
logger.debug("Successfully loaded thing \'{}\' during retry", thingUID);
Thing oldThing = null;
for (Collection<Thing> modelThings : thingsMap.values()) {
oldThing = modelThings.stream().filter(t -> t.getUID().equals(newThing.getUID())).findFirst()
.orElse(null);
if (oldThing != null) {
mergeThing(newThing, oldThing);
modelThings.remove(oldThing);
modelThings.add(newThing);
logger.debug("Refreshing thing \'{}\' after successful retry", newThing.getUID());
if (!ThingHelper.equals(oldThing, newThing)) {
notifyListenersAboutUpdatedElement(oldThing, newThing);
}
break;
}
}
if (oldThing == null) {
logger.debug("Refreshing thing \'{}\' after retry failed because thing is not found",
newThing.getUID());
}
}
return newThing != null;
}
private boolean isThingHandlerFactoryReady(ThingHandlerFactory thingHandlerFactory) {
String bundleName = getBundleName(thingHandlerFactory);
return bundleName != null && loadedXmlThingTypes.contains(bundleName);
}
private @Nullable String getBundleName(ThingHandlerFactory thingHandlerFactory) { private @Nullable String getBundleName(ThingHandlerFactory thingHandlerFactory) {
Bundle bundle = bundleResolver.resolveBundle(thingHandlerFactory.getClass()); Bundle bundle = bundleResolver.resolveBundle(thingHandlerFactory.getClass());
return bundle == null ? null : bundle.getSymbolicName(); return bundle == null ? null : bundle.getSymbolicName();
@ -300,20 +330,6 @@ public class YamlThingProvider extends AbstractProvider<Thing>
String[] segments = thingUID.getAsString().split(AbstractUID.SEPARATOR); String[] segments = thingUID.getAsString().split(AbstractUID.SEPARATOR);
ThingTypeUID thingTypeUID = new ThingTypeUID(thingUID.getBindingId(), segments[1]); ThingTypeUID thingTypeUID = new ThingTypeUID(thingUID.getBindingId(), segments[1]);
ThingHandlerFactory handlerFactory = thingHandlerFactories.stream()
.filter(thf -> thf.supportsThingType(thingTypeUID)).findFirst().orElse(null);
if (handlerFactory == null) {
if (modelLoaded) {
logger.info("No ThingHandlerFactory found for thing {} (thing-type is {}). Deferring initialization.",
thingUID, thingTypeUID);
}
return null;
}
String bundleName = getBundleName(handlerFactory);
if (bundleName == null || !loadedXmlThingTypes.contains(bundleName)) {
return null;
}
ThingType thingType = thingTypeRegistry.getThingType(thingTypeUID, localeProvider.getLocale()); ThingType thingType = thingTypeRegistry.getThingType(thingTypeUID, localeProvider.getLocale());
ThingUID bridgeUID = thingDto.bridge != null ? new ThingUID(thingDto.bridge) : null; ThingUID bridgeUID = thingDto.bridge != null ? new ThingUID(thingDto.bridge) : null;
Configuration configuration = new Configuration(thingDto.config); Configuration configuration = new Configuration(thingDto.config);
@ -333,22 +349,22 @@ public class YamlThingProvider extends AbstractProvider<Thing>
Thing thing = thingBuilder.build(); Thing thing = thingBuilder.build();
Thing thingFromHandler = handlerFactory.createThing(thingTypeUID, configuration, thingUID, bridgeUID); Thing thingFromHandler = null;
if (thingFromHandler != null) { ThingHandlerFactory handlerFactory = thingHandlerFactories.stream()
mergeThing(thingFromHandler, thing); .filter(thf -> isThingHandlerFactoryReady(thf) && thf.supportsThingType(thingTypeUID)).findFirst()
logger.debug("Successfully loaded thing \'{}\'", thingUID); .orElse(null);
} else { if (handlerFactory != null) {
// Possible cause: Asynchronous loading of the XML files thingFromHandler = handlerFactory.createThing(thingTypeUID, configuration, thingUID, bridgeUID);
// Add the data to the queue in order to retry it later if (thingFromHandler != null) {
logger.debug( mergeThing(thingFromHandler, thing);
"ThingHandlerFactory \'{}\' claimed it can handle \'{}\' type but actually did not. Queued for later refresh.", logger.debug("Successfully loaded thing \'{}\'", thingUID);
handlerFactory.getClass().getSimpleName(), thingTypeUID); } else {
queue.add(new QueueContent(handlerFactory, thingTypeUID, configuration, thingUID, bridgeUID)); // Possible cause: Asynchronous loading of the XML files
Thread thread = lazyRetryThread; // Add the data to the queue in order to retry it later
if (thread == null || !thread.isAlive()) { logger.debug(
thread = new Thread(lazyRetryRunnable); "ThingHandlerFactory \'{}\' claimed it can handle \'{}\' type but actually did not. Queued for later refresh.",
lazyRetryThread = thread; handlerFactory.getClass().getSimpleName(), thingTypeUID);
thread.start(); queueRetryThingCreation(handlerFactory, thingTypeUID, configuration, thingUID, bridgeUID);
} }
} }
@ -383,7 +399,7 @@ public class YamlThingProvider extends AbstractProvider<Thing>
configDescriptionRegistry.getConfigDescription(descUriO)); configDescriptionRegistry.getConfigDescription(descUriO));
} }
} else { } else {
logger.warn("Channel type {} could not be found.", channelTypeUID); logger.warn("Channel type {} could not be found for thing '{}'.", channelTypeUID, thingUID);
} }
} }
@ -415,7 +431,12 @@ public class YamlThingProvider extends AbstractProvider<Thing>
} }
private void mergeThing(Thing target, Thing source) { private void mergeThing(Thing target, Thing source) {
target.setLabel(source.getLabel()); String label = source.getLabel();
if (label == null) {
ThingType thingType = thingTypeRegistry.getThingType(target.getThingTypeUID(), localeProvider.getLocale());
label = thingType != null ? thingType.getLabel() : null;
}
target.setLabel(label);
target.setLocation(source.getLocation()); target.setLocation(source.getLocation());
target.setBridgeUID(source.getBridgeUID()); target.setBridgeUID(source.getBridgeUID());
@ -439,4 +460,15 @@ public class YamlThingProvider extends AbstractProvider<Thing>
// add the channels only defined in source list to the target list // add the channels only defined in source list to the target list
ThingHelper.addChannelsToThing(target, channelsToAdd); ThingHelper.addChannelsToThing(target, channelsToAdd);
} }
private void queueRetryThingCreation(ThingHandlerFactory handlerFactory, ThingTypeUID thingTypeUID,
Configuration configuration, ThingUID thingUID, @Nullable ThingUID bridgeUID) {
queue.add(new QueueContent(handlerFactory, thingTypeUID, configuration, thingUID, bridgeUID));
Thread thread = lazyRetryThread;
if (thread == null || !thread.isAlive()) {
thread = new Thread(lazyRetryRunnable);
lazyRetryThread = thread;
thread.start();
}
}
} }