mirror of
https://github.com/danieldemus/openhab-core.git
synced 2026-07-31 05:24:26 +02:00
Add file-based YAML support for UI pages and widgets (#5493)
Signed-off-by: Jimmy Tanagra <jcode@tanagra.id.au>
This commit is contained in:
+46
@@ -0,0 +1,46 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2010-2026 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.io.rest.ui;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import org.openhab.core.ui.components.RootUIComponent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link RootUIComponent} enriched with runtime-computed information for REST API responses.
|
||||||
|
*
|
||||||
|
* @author Jimmy Tanagra - Initial contribution
|
||||||
|
*/
|
||||||
|
public class EnrichedRootUIComponent extends RootUIComponent {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the component can be modified or deleted through the REST API.
|
||||||
|
* Components loaded from YAML configuration files are not editable.
|
||||||
|
*/
|
||||||
|
public boolean editable;
|
||||||
|
|
||||||
|
public EnrichedRootUIComponent(RootUIComponent component, boolean editable) {
|
||||||
|
super(component.getUID(), component.getType());
|
||||||
|
setConfig(component.getConfig());
|
||||||
|
setSlots(component.getSlots());
|
||||||
|
if (component.getTags() != null) {
|
||||||
|
addTags(component.getTags());
|
||||||
|
}
|
||||||
|
Date timestamp = component.getTimestamp();
|
||||||
|
if (timestamp != null) {
|
||||||
|
setTimestamp(timestamp);
|
||||||
|
}
|
||||||
|
setProps(component.getProps());
|
||||||
|
this.editable = editable;
|
||||||
|
}
|
||||||
|
}
|
||||||
+25
-12
@@ -44,6 +44,7 @@ import org.openhab.core.common.registry.RegistryChangeListener;
|
|||||||
import org.openhab.core.io.rest.RESTConstants;
|
import org.openhab.core.io.rest.RESTConstants;
|
||||||
import org.openhab.core.io.rest.RESTResource;
|
import org.openhab.core.io.rest.RESTResource;
|
||||||
import org.openhab.core.io.rest.Stream2JSONInputStream;
|
import org.openhab.core.io.rest.Stream2JSONInputStream;
|
||||||
|
import org.openhab.core.io.rest.ui.EnrichedRootUIComponent;
|
||||||
import org.openhab.core.io.rest.ui.TileDTO;
|
import org.openhab.core.io.rest.ui.TileDTO;
|
||||||
import org.openhab.core.ui.components.RootUIComponent;
|
import org.openhab.core.ui.components.RootUIComponent;
|
||||||
import org.openhab.core.ui.components.UIComponentRegistry;
|
import org.openhab.core.ui.components.UIComponentRegistry;
|
||||||
@@ -127,18 +128,20 @@ public class UIResource implements RESTResource {
|
|||||||
@Path("/components/{namespace}")
|
@Path("/components/{namespace}")
|
||||||
@Produces({ MediaType.APPLICATION_JSON })
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
@Operation(operationId = "getRegisteredUIComponentsInNamespace", summary = "Get all registered UI components in the specified namespace.", responses = {
|
@Operation(operationId = "getRegisteredUIComponentsInNamespace", summary = "Get all registered UI components in the specified namespace.", responses = {
|
||||||
@ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = RootUIComponent.class)))) })
|
@ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = EnrichedRootUIComponent.class)))) })
|
||||||
public Response getAllComponents(@Context Request request, @PathParam("namespace") String namespace,
|
public Response getAllComponents(@Context Request request, @PathParam("namespace") String namespace,
|
||||||
@QueryParam("summary") @Parameter(description = "summary fields only") @Nullable Boolean summary) {
|
@QueryParam("summary") @Parameter(description = "summary fields only") @Nullable Boolean summary) {
|
||||||
UIComponentRegistry registry = componentRegistryFactory.getRegistry(namespace);
|
UIComponentRegistry registry = componentRegistryFactory.getRegistry(namespace);
|
||||||
Stream<RootUIComponent> components = registry.getAll().stream();
|
Stream<EnrichedRootUIComponent> components = registry.getAll().stream()
|
||||||
|
.map(c -> new EnrichedRootUIComponent(c, registry.isEditable(c.getUID())));
|
||||||
if (summary != null && summary) {
|
if (summary != null && summary) {
|
||||||
components = components.map(c -> {
|
components = components.map(c -> {
|
||||||
RootUIComponent component = new RootUIComponent(c.getUID(), c.getType());
|
EnrichedRootUIComponent component = new EnrichedRootUIComponent(
|
||||||
|
new RootUIComponent(c.getUID(), c.getType()), c.editable);
|
||||||
@Nullable
|
@Nullable
|
||||||
Set<String> tags = c.getTags();
|
Set<String> tags = c.getTags();
|
||||||
if (tags != null) {
|
if (tags != null) {
|
||||||
component.addTags(c.getTags());
|
component.addTags(tags);
|
||||||
}
|
}
|
||||||
@Nullable
|
@Nullable
|
||||||
Date timestamp = c.getTimestamp();
|
Date timestamp = c.getTimestamp();
|
||||||
@@ -178,7 +181,7 @@ public class UIResource implements RESTResource {
|
|||||||
@Path("/components/{namespace}/{componentUID}")
|
@Path("/components/{namespace}/{componentUID}")
|
||||||
@Produces({ MediaType.APPLICATION_JSON })
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
@Operation(operationId = "getUIComponentInNamespace", summary = "Get a specific UI component in the specified namespace.", responses = {
|
@Operation(operationId = "getUIComponentInNamespace", summary = "Get a specific UI component in the specified namespace.", responses = {
|
||||||
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = RootUIComponent.class))),
|
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = EnrichedRootUIComponent.class))),
|
||||||
@ApiResponse(responseCode = "404", description = "Component not found") })
|
@ApiResponse(responseCode = "404", description = "Component not found") })
|
||||||
public Response getComponentByUID(@PathParam("namespace") String namespace,
|
public Response getComponentByUID(@PathParam("namespace") String namespace,
|
||||||
@PathParam("componentUID") String componentUID) {
|
@PathParam("componentUID") String componentUID) {
|
||||||
@@ -187,7 +190,7 @@ public class UIResource implements RESTResource {
|
|||||||
if (component == null) {
|
if (component == null) {
|
||||||
return Response.status(Status.NOT_FOUND).build();
|
return Response.status(Status.NOT_FOUND).build();
|
||||||
}
|
}
|
||||||
return Response.ok(component).build();
|
return Response.ok(new EnrichedRootUIComponent(component, registry.isEditable(componentUID))).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
@@ -197,12 +200,12 @@ public class UIResource implements RESTResource {
|
|||||||
@Produces({ MediaType.APPLICATION_JSON })
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
@Operation(operationId = "addUIComponentToNamespace", summary = "Add a UI component in the specified namespace.", security = {
|
@Operation(operationId = "addUIComponentToNamespace", summary = "Add a UI component in the specified namespace.", security = {
|
||||||
@SecurityRequirement(name = "oauth2", scopes = { "admin" }) }, responses = {
|
@SecurityRequirement(name = "oauth2", scopes = { "admin" }) }, responses = {
|
||||||
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = RootUIComponent.class))) })
|
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = EnrichedRootUIComponent.class))) })
|
||||||
public Response addComponent(@PathParam("namespace") String namespace, RootUIComponent component) {
|
public Response addComponent(@PathParam("namespace") String namespace, RootUIComponent component) {
|
||||||
UIComponentRegistry registry = componentRegistryFactory.getRegistry(namespace);
|
UIComponentRegistry registry = componentRegistryFactory.getRegistry(namespace);
|
||||||
component.updateTimestamp();
|
component.updateTimestamp();
|
||||||
RootUIComponent createdComponent = registry.add(component);
|
RootUIComponent createdComponent = registry.add(component);
|
||||||
return Response.ok(createdComponent).build();
|
return Response.ok(new EnrichedRootUIComponent(createdComponent, true)).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@PUT
|
@PUT
|
||||||
@@ -212,8 +215,9 @@ public class UIResource implements RESTResource {
|
|||||||
@Produces({ MediaType.APPLICATION_JSON })
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
@Operation(operationId = "updateUIComponentInNamespace", summary = "Update a specific UI component in the specified namespace.", security = {
|
@Operation(operationId = "updateUIComponentInNamespace", summary = "Update a specific UI component in the specified namespace.", security = {
|
||||||
@SecurityRequirement(name = "oauth2", scopes = { "admin" }) }, responses = {
|
@SecurityRequirement(name = "oauth2", scopes = { "admin" }) }, responses = {
|
||||||
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = RootUIComponent.class))),
|
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = EnrichedRootUIComponent.class))),
|
||||||
@ApiResponse(responseCode = "404", description = "Component not found") })
|
@ApiResponse(responseCode = "404", description = "Component not found"),
|
||||||
|
@ApiResponse(responseCode = "405", description = "Component is not editable.") })
|
||||||
public Response updateComponent(@PathParam("namespace") String namespace,
|
public Response updateComponent(@PathParam("namespace") String namespace,
|
||||||
@PathParam("componentUID") String componentUID, RootUIComponent component) {
|
@PathParam("componentUID") String componentUID, RootUIComponent component) {
|
||||||
UIComponentRegistry registry = componentRegistryFactory.getRegistry(namespace);
|
UIComponentRegistry registry = componentRegistryFactory.getRegistry(namespace);
|
||||||
@@ -221,13 +225,17 @@ public class UIResource implements RESTResource {
|
|||||||
if (existingComponent == null) {
|
if (existingComponent == null) {
|
||||||
return Response.status(Status.NOT_FOUND).build();
|
return Response.status(Status.NOT_FOUND).build();
|
||||||
}
|
}
|
||||||
|
if (!registry.isEditable(componentUID)) {
|
||||||
|
return Response.status(Status.METHOD_NOT_ALLOWED)
|
||||||
|
.entity("Cannot update component " + componentUID + " as it is not editable.").build();
|
||||||
|
}
|
||||||
if (!componentUID.equals(component.getUID())) {
|
if (!componentUID.equals(component.getUID())) {
|
||||||
throw new InvalidParameterException(
|
throw new InvalidParameterException(
|
||||||
"The component UID in the body of the request should match the UID in the URL");
|
"The component UID in the body of the request should match the UID in the URL");
|
||||||
}
|
}
|
||||||
component.updateTimestamp();
|
component.updateTimestamp();
|
||||||
registry.update(component);
|
registry.update(component);
|
||||||
return Response.ok(component).build();
|
return Response.ok(new EnrichedRootUIComponent(component, true)).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@DELETE
|
@DELETE
|
||||||
@@ -237,7 +245,8 @@ public class UIResource implements RESTResource {
|
|||||||
@Operation(operationId = "removeUIComponentFromNamespace", summary = "Remove a specific UI component in the specified namespace.", security = {
|
@Operation(operationId = "removeUIComponentFromNamespace", summary = "Remove a specific UI component in the specified namespace.", security = {
|
||||||
@SecurityRequirement(name = "oauth2", scopes = { "admin" }) }, responses = {
|
@SecurityRequirement(name = "oauth2", scopes = { "admin" }) }, responses = {
|
||||||
@ApiResponse(responseCode = "200", description = "OK"),
|
@ApiResponse(responseCode = "200", description = "OK"),
|
||||||
@ApiResponse(responseCode = "404", description = "Component not found") })
|
@ApiResponse(responseCode = "404", description = "Component not found"),
|
||||||
|
@ApiResponse(responseCode = "405", description = "Component is not editable.") })
|
||||||
public Response deleteComponent(@PathParam("namespace") String namespace,
|
public Response deleteComponent(@PathParam("namespace") String namespace,
|
||||||
@PathParam("componentUID") String componentUID) {
|
@PathParam("componentUID") String componentUID) {
|
||||||
UIComponentRegistry registry = componentRegistryFactory.getRegistry(namespace);
|
UIComponentRegistry registry = componentRegistryFactory.getRegistry(namespace);
|
||||||
@@ -245,6 +254,10 @@ public class UIResource implements RESTResource {
|
|||||||
if (component == null) {
|
if (component == null) {
|
||||||
return Response.status(Status.NOT_FOUND).build();
|
return Response.status(Status.NOT_FOUND).build();
|
||||||
}
|
}
|
||||||
|
if (!registry.isEditable(componentUID)) {
|
||||||
|
return Response.status(Status.METHOD_NOT_ALLOWED)
|
||||||
|
.entity("Cannot delete component " + componentUID + " as it is not editable.").build();
|
||||||
|
}
|
||||||
registry.remove(componentUID);
|
registry.remove(componentUID);
|
||||||
return Response.ok().build();
|
return Response.ok().build();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,5 +45,10 @@
|
|||||||
<artifactId>org.openhab.core.automation.module.script.rulesupport</artifactId>
|
<artifactId>org.openhab.core.automation.module.script.rulesupport</artifactId>
|
||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.openhab.core.bundles</groupId>
|
||||||
|
<artifactId>org.openhab.core.ui</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
+6
-1
@@ -45,10 +45,12 @@ 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.model.yaml.YamlModelRepository;
|
||||||
import org.openhab.core.model.yaml.internal.items.YamlItemDTO;
|
import org.openhab.core.model.yaml.internal.items.YamlItemDTO;
|
||||||
|
import org.openhab.core.model.yaml.internal.pages.YamlPageDTO;
|
||||||
import org.openhab.core.model.yaml.internal.rules.YamlRuleDTO;
|
import org.openhab.core.model.yaml.internal.rules.YamlRuleDTO;
|
||||||
import org.openhab.core.model.yaml.internal.rules.YamlRuleTemplateDTO;
|
import org.openhab.core.model.yaml.internal.rules.YamlRuleTemplateDTO;
|
||||||
import org.openhab.core.model.yaml.internal.semantics.YamlSemanticTagDTO;
|
import org.openhab.core.model.yaml.internal.semantics.YamlSemanticTagDTO;
|
||||||
import org.openhab.core.model.yaml.internal.things.YamlThingDTO;
|
import org.openhab.core.model.yaml.internal.things.YamlThingDTO;
|
||||||
|
import org.openhab.core.model.yaml.internal.widgets.YamlWidgetDTO;
|
||||||
import org.openhab.core.service.WatchService;
|
import org.openhab.core.service.WatchService;
|
||||||
import org.openhab.core.service.WatchService.Kind;
|
import org.openhab.core.service.WatchService.Kind;
|
||||||
import org.osgi.service.component.annotations.Activate;
|
import org.osgi.service.component.annotations.Activate;
|
||||||
@@ -85,6 +87,7 @@ import com.fasterxml.jackson.dataformat.yaml.YAMLParser;
|
|||||||
* @author Laurent Garnier - new parameters to retrieve errors and warnings when loading a file
|
* @author Laurent Garnier - new parameters to retrieve errors and warnings when loading a file
|
||||||
* @author Laurent Garnier - Added methods addElementsToBeGenerated, generateFileFormat, createIsolatedModel and
|
* @author Laurent Garnier - Added methods addElementsToBeGenerated, generateFileFormat, createIsolatedModel and
|
||||||
* removeIsolatedModel
|
* removeIsolatedModel
|
||||||
|
* @author Jimmy Tanagra - Add YamlPageDTO and YamlWidgetDTO
|
||||||
*/
|
*/
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
@Component(immediate = true)
|
@Component(immediate = true)
|
||||||
@@ -97,7 +100,9 @@ public class YamlModelRepositoryImpl implements WatchService.WatchEventListener,
|
|||||||
getElementName(YamlRuleTemplateDTO.class), // "ruleTemplates"
|
getElementName(YamlRuleTemplateDTO.class), // "ruleTemplates"
|
||||||
getElementName(YamlSemanticTagDTO.class), // "tags"
|
getElementName(YamlSemanticTagDTO.class), // "tags"
|
||||||
getElementName(YamlThingDTO.class), // "things"
|
getElementName(YamlThingDTO.class), // "things"
|
||||||
getElementName(YamlItemDTO.class) // "items"
|
getElementName(YamlItemDTO.class), // "items"
|
||||||
|
getElementName(YamlPageDTO.class), // "pages"
|
||||||
|
getElementName(YamlWidgetDTO.class) // "widgets"
|
||||||
);
|
);
|
||||||
|
|
||||||
private static final String UNWANTED_EXCEPTION_TEXT = "at [Source: UNKNOWN; byte offset: #UNKNOWN] ";
|
private static final String UNWANTED_EXCEPTION_TEXT = "at [Source: UNKNOWN; byte offset: #UNKNOWN] ";
|
||||||
|
|||||||
+111
@@ -0,0 +1,111 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2010-2026 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.model.yaml.internal.pages;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNull;
|
||||||
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
|
import org.openhab.core.config.core.dto.ConfigDescriptionDTO;
|
||||||
|
import org.openhab.core.model.yaml.YamlElement;
|
||||||
|
import org.openhab.core.model.yaml.YamlElementName;
|
||||||
|
import org.openhab.core.model.yaml.internal.util.FlexibleDateDeserializer;
|
||||||
|
import org.openhab.core.ui.components.UIComponent;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link YamlPageDTO} is a data transfer object used to serialize a UI page in a YAML configuration file.
|
||||||
|
* It maps to a {@link org.openhab.core.ui.components.RootUIComponent} in the {@code ui:page} namespace.
|
||||||
|
*
|
||||||
|
* @author Jimmy Tanagra - Initial contribution
|
||||||
|
*/
|
||||||
|
@YamlElementName("pages")
|
||||||
|
public class YamlPageDTO implements YamlElement, Cloneable {
|
||||||
|
|
||||||
|
public String uid;
|
||||||
|
public String component;
|
||||||
|
public Map<String, Object> config;
|
||||||
|
public Map<String, List<UIComponent>> slots;
|
||||||
|
public Set<String> tags;
|
||||||
|
public ConfigDescriptionDTO props;
|
||||||
|
@JsonDeserialize(using = FlexibleDateDeserializer.class)
|
||||||
|
public @Nullable Date timestamp;
|
||||||
|
|
||||||
|
public YamlPageDTO() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NonNull String getId() {
|
||||||
|
return uid == null ? "" : uid;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setId(@NonNull String id) {
|
||||||
|
uid = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public YamlElement cloneWithoutId() {
|
||||||
|
YamlPageDTO copy;
|
||||||
|
try {
|
||||||
|
copy = (YamlPageDTO) super.clone();
|
||||||
|
copy.uid = null;
|
||||||
|
return copy;
|
||||||
|
} catch (CloneNotSupportedException e) {
|
||||||
|
// Will never happen
|
||||||
|
return new YamlPageDTO();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isValid(@Nullable List<@NonNull String> errors, @Nullable List<@NonNull String> warnings) {
|
||||||
|
if (uid == null || uid.isBlank()) {
|
||||||
|
addToList(errors, "invalid page: uid is missing while mandatory");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (component == null || component.isBlank()) {
|
||||||
|
addToList(errors, "invalid page \"%s\": component is missing while mandatory".formatted(uid));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addToList(@Nullable List<@NonNull String> list, String value) {
|
||||||
|
if (list != null) {
|
||||||
|
list.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(uid, component, config, slots, tags, props);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(@Nullable Object obj) {
|
||||||
|
if (this == obj) {
|
||||||
|
return true;
|
||||||
|
} else if (obj == null || getClass() != obj.getClass()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
YamlPageDTO other = (YamlPageDTO) obj;
|
||||||
|
return Objects.equals(uid, other.uid) && Objects.equals(component, other.component)
|
||||||
|
&& Objects.equals(config, other.config) && Objects.equals(slots, other.slots)
|
||||||
|
&& Objects.equals(tags, other.tags) && Objects.equals(props, other.props);
|
||||||
|
}
|
||||||
|
}
|
||||||
+165
@@ -0,0 +1,165 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2010-2026 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.model.yaml.internal.pages;
|
||||||
|
|
||||||
|
import static org.openhab.core.model.yaml.YamlModelUtils.isIsolatedModel;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
|
import org.openhab.core.common.registry.AbstractProvider;
|
||||||
|
import org.openhab.core.model.yaml.YamlModelListener;
|
||||||
|
import org.openhab.core.ui.components.RootUIComponent;
|
||||||
|
import org.openhab.core.ui.components.UIComponentProvider;
|
||||||
|
import org.osgi.service.component.annotations.Component;
|
||||||
|
import org.osgi.service.component.annotations.Deactivate;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link YamlPageProvider} is an OSGi service that allows defining UI pages in YAML configuration files.
|
||||||
|
* These pages are automatically exposed to the {@link org.openhab.core.ui.components.UIComponentRegistry}
|
||||||
|
* under the {@code ui:page} namespace.
|
||||||
|
*
|
||||||
|
* @author Jimmy Tanagra - Initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
@Component(immediate = true, service = { UIComponentProvider.class, YamlPageProvider.class, YamlModelListener.class })
|
||||||
|
public class YamlPageProvider extends AbstractProvider<RootUIComponent>
|
||||||
|
implements UIComponentProvider, YamlModelListener<YamlPageDTO> {
|
||||||
|
|
||||||
|
public static final String PAGES_NAMESPACE = "ui:page";
|
||||||
|
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(YamlPageProvider.class);
|
||||||
|
|
||||||
|
private final Map<String, Collection<RootUIComponent>> pagesMap = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
@Deactivate
|
||||||
|
public void deactivate() {
|
||||||
|
pagesMap.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getNamespace() {
|
||||||
|
return PAGES_NAMESPACE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<RootUIComponent> getAll() {
|
||||||
|
// Ignore isolated models
|
||||||
|
return pagesMap.keySet().stream().filter(name -> !isIsolatedModel(name))
|
||||||
|
.map(name -> pagesMap.getOrDefault(name, List.of())).flatMap(Collection::stream).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<YamlPageDTO> getElementClass() {
|
||||||
|
return YamlPageDTO.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isVersionSupported(int version) {
|
||||||
|
return version >= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isDeprecated() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addedModel(String modelName, Collection<YamlPageDTO> elements) {
|
||||||
|
List<RootUIComponent> added = elements.stream().map(this::mapPage).filter(Objects::nonNull).toList();
|
||||||
|
Collection<RootUIComponent> modelPages = Objects
|
||||||
|
.requireNonNull(pagesMap.computeIfAbsent(modelName, k -> new ArrayList<>()));
|
||||||
|
modelPages.addAll(added);
|
||||||
|
added.forEach(page -> {
|
||||||
|
logger.debug("model {} added page {}", modelName, page.getUID());
|
||||||
|
if (!isIsolatedModel(modelName)) {
|
||||||
|
notifyListenersAboutAddedElement(page);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updatedModel(String modelName, Collection<YamlPageDTO> elements) {
|
||||||
|
List<RootUIComponent> updated = elements.stream().map(this::mapPage).filter(Objects::nonNull).toList();
|
||||||
|
Collection<RootUIComponent> modelPages = Objects
|
||||||
|
.requireNonNull(pagesMap.computeIfAbsent(modelName, k -> new ArrayList<>()));
|
||||||
|
updated.forEach(page -> {
|
||||||
|
String uid = page.getUID();
|
||||||
|
modelPages.stream().filter(p -> p.getUID().equals(uid)).findFirst().ifPresentOrElse(oldPage -> {
|
||||||
|
modelPages.remove(oldPage);
|
||||||
|
modelPages.add(page);
|
||||||
|
logger.debug("model {} updated page {}", modelName, uid);
|
||||||
|
if (!isIsolatedModel(modelName)) {
|
||||||
|
notifyListenersAboutUpdatedElement(oldPage, page);
|
||||||
|
}
|
||||||
|
}, () -> {
|
||||||
|
modelPages.add(page);
|
||||||
|
logger.debug("model {} added page {}", modelName, uid);
|
||||||
|
if (!isIsolatedModel(modelName)) {
|
||||||
|
notifyListenersAboutAddedElement(page);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removedModel(String modelName, Collection<YamlPageDTO> elements) {
|
||||||
|
Collection<RootUIComponent> modelPages = pagesMap.getOrDefault(modelName, List.of());
|
||||||
|
elements.stream().map(YamlPageDTO::getId).forEach(uid -> {
|
||||||
|
modelPages.stream().filter(p -> p.getUID().equals(uid)).findFirst().ifPresentOrElse(oldPage -> {
|
||||||
|
modelPages.remove(oldPage);
|
||||||
|
logger.debug("model {} removed page {}", modelName, uid);
|
||||||
|
if (!isIsolatedModel(modelName)) {
|
||||||
|
notifyListenersAboutRemovedElement(oldPage);
|
||||||
|
}
|
||||||
|
}, () -> logger.debug("model {} page {} not found", modelName, uid));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (modelPages.isEmpty()) {
|
||||||
|
pagesMap.remove(modelName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private @Nullable RootUIComponent mapPage(YamlPageDTO dto) {
|
||||||
|
String uid = dto.uid;
|
||||||
|
if (uid == null || uid.isBlank()) {
|
||||||
|
logger.warn("Skipping page with missing uid");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
RootUIComponent page = new RootUIComponent(uid, dto.component != null ? dto.component : "");
|
||||||
|
if (dto.config != null) {
|
||||||
|
page.setConfig(dto.config);
|
||||||
|
}
|
||||||
|
page.setSlots(dto.slots);
|
||||||
|
if (dto.tags != null) {
|
||||||
|
page.addTags(dto.tags);
|
||||||
|
}
|
||||||
|
if (dto.props != null) {
|
||||||
|
page.setProps(dto.props);
|
||||||
|
}
|
||||||
|
Date timestamp = dto.timestamp;
|
||||||
|
if (timestamp != null) {
|
||||||
|
page.setTimestamp(timestamp);
|
||||||
|
}
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
}
|
||||||
+89
@@ -0,0 +1,89 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2010-2026 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.model.yaml.internal.util;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.text.ParseException;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.format.DateTimeParseException;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonParser;
|
||||||
|
import com.fasterxml.jackson.core.JsonToken;
|
||||||
|
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||||
|
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Jackson deserializer for {@link Date} that accepts a wide range of formats:
|
||||||
|
* <ul>
|
||||||
|
* <li>Epoch milliseconds (numeric)</li>
|
||||||
|
* <li>ISO 8601 (e.g. {@code 2021-03-08T19:41:03Z})</li>
|
||||||
|
* <li>Legacy openHAB UI format (e.g. {@code Mar 8, 2021, 7:41:03 PM})</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @author Jimmy Tanagra - Initial contribution
|
||||||
|
*/
|
||||||
|
public class FlexibleDateDeserializer extends StdDeserializer<Date> {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
private static final List<String> STRING_PATTERNS = List.of( //
|
||||||
|
"MMM d, yyyy, h:mm:ss a", // legacy openHAB UI export format
|
||||||
|
"MMM d, yyyy, hh:mm:ss a", // zero-padded hour variant
|
||||||
|
"yyyy-MM-dd'T'HH:mm:ssXXX", // ISO 8601 with offset
|
||||||
|
"yyyy-MM-dd'T'HH:mm:ss", // ISO 8601 local
|
||||||
|
"yyyy-MM-dd HH:mm:ss", // common SQL-like format
|
||||||
|
"yyyy-MM-dd" // date only
|
||||||
|
);
|
||||||
|
|
||||||
|
public FlexibleDateDeserializer() {
|
||||||
|
super(Date.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Date deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
|
||||||
|
if (p.currentToken() == JsonToken.VALUE_NUMBER_INT) {
|
||||||
|
return new Date(p.getLongValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
String text = p.getText().trim();
|
||||||
|
|
||||||
|
// Try each string pattern
|
||||||
|
for (String pattern : STRING_PATTERNS) {
|
||||||
|
try {
|
||||||
|
SimpleDateFormat sdf = new SimpleDateFormat(pattern, Locale.US);
|
||||||
|
sdf.setLenient(false);
|
||||||
|
return sdf.parse(text);
|
||||||
|
} catch (ParseException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try ISO 8601 via Instant (handles Z, offsets, etc.)
|
||||||
|
try {
|
||||||
|
return Date.from(Instant.parse(text));
|
||||||
|
} catch (DateTimeParseException ignored) {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try parsing as epoch millis from string
|
||||||
|
try {
|
||||||
|
return new Date(Long.parseLong(text));
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
}
|
||||||
|
|
||||||
|
throw ctxt.weirdStringException(text, Date.class,
|
||||||
|
"Cannot parse date: unrecognized format. Accepted formats: epoch (ms), ISO 8601, or 'MMM d, yyyy, h:mm:ss a'");
|
||||||
|
}
|
||||||
|
}
|
||||||
+111
@@ -0,0 +1,111 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2010-2026 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.model.yaml.internal.widgets;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNull;
|
||||||
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
|
import org.openhab.core.config.core.dto.ConfigDescriptionDTO;
|
||||||
|
import org.openhab.core.model.yaml.YamlElement;
|
||||||
|
import org.openhab.core.model.yaml.YamlElementName;
|
||||||
|
import org.openhab.core.model.yaml.internal.util.FlexibleDateDeserializer;
|
||||||
|
import org.openhab.core.ui.components.UIComponent;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link YamlWidgetDTO} is a data transfer object used to serialize a UI widget in a YAML configuration file.
|
||||||
|
* It maps to a {@link org.openhab.core.ui.components.RootUIComponent} in the {@code ui:widget} namespace.
|
||||||
|
*
|
||||||
|
* @author Jimmy Tanagra - Initial contribution
|
||||||
|
*/
|
||||||
|
@YamlElementName("widgets")
|
||||||
|
public class YamlWidgetDTO implements YamlElement, Cloneable {
|
||||||
|
|
||||||
|
public String uid;
|
||||||
|
public String component;
|
||||||
|
public Map<String, Object> config;
|
||||||
|
public Map<String, List<UIComponent>> slots;
|
||||||
|
public Set<String> tags;
|
||||||
|
public ConfigDescriptionDTO props;
|
||||||
|
@JsonDeserialize(using = FlexibleDateDeserializer.class)
|
||||||
|
public @Nullable Date timestamp;
|
||||||
|
|
||||||
|
public YamlWidgetDTO() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NonNull String getId() {
|
||||||
|
return uid == null ? "" : uid;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setId(@NonNull String id) {
|
||||||
|
uid = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public YamlElement cloneWithoutId() {
|
||||||
|
YamlWidgetDTO copy;
|
||||||
|
try {
|
||||||
|
copy = (YamlWidgetDTO) super.clone();
|
||||||
|
copy.uid = null;
|
||||||
|
return copy;
|
||||||
|
} catch (CloneNotSupportedException e) {
|
||||||
|
// Will never happen
|
||||||
|
return new YamlWidgetDTO();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isValid(@Nullable List<@NonNull String> errors, @Nullable List<@NonNull String> warnings) {
|
||||||
|
if (uid == null || uid.isBlank()) {
|
||||||
|
addToList(errors, "invalid widget: uid is missing while mandatory");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (component == null || component.isBlank()) {
|
||||||
|
addToList(errors, "invalid widget \"%s\": component is missing while mandatory".formatted(uid));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addToList(@Nullable List<@NonNull String> list, String value) {
|
||||||
|
if (list != null) {
|
||||||
|
list.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(uid, component, config, slots, tags, props);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(@Nullable Object obj) {
|
||||||
|
if (this == obj) {
|
||||||
|
return true;
|
||||||
|
} else if (obj == null || getClass() != obj.getClass()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
YamlWidgetDTO other = (YamlWidgetDTO) obj;
|
||||||
|
return Objects.equals(uid, other.uid) && Objects.equals(component, other.component)
|
||||||
|
&& Objects.equals(config, other.config) && Objects.equals(slots, other.slots)
|
||||||
|
&& Objects.equals(tags, other.tags) && Objects.equals(props, other.props);
|
||||||
|
}
|
||||||
|
}
|
||||||
+167
@@ -0,0 +1,167 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2010-2026 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.model.yaml.internal.widgets;
|
||||||
|
|
||||||
|
import static org.openhab.core.model.yaml.YamlModelUtils.isIsolatedModel;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
|
import org.openhab.core.common.registry.AbstractProvider;
|
||||||
|
import org.openhab.core.model.yaml.YamlModelListener;
|
||||||
|
import org.openhab.core.ui.components.RootUIComponent;
|
||||||
|
import org.openhab.core.ui.components.UIComponentProvider;
|
||||||
|
import org.osgi.service.component.annotations.Component;
|
||||||
|
import org.osgi.service.component.annotations.Deactivate;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link YamlWidgetProvider} is an OSGi service that allows defining UI widgets in YAML configuration files.
|
||||||
|
* These widgets are automatically exposed to the {@link org.openhab.core.ui.components.UIComponentRegistry}
|
||||||
|
* under the {@code ui:widget} namespace.
|
||||||
|
*
|
||||||
|
* @author Jimmy Tanagra - Initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
@Component(immediate = true, service = { UIComponentProvider.class, YamlWidgetProvider.class, YamlModelListener.class })
|
||||||
|
public class YamlWidgetProvider extends AbstractProvider<RootUIComponent>
|
||||||
|
implements UIComponentProvider, YamlModelListener<YamlWidgetDTO> {
|
||||||
|
|
||||||
|
public static final String WIDGETS_NAMESPACE = "ui:widget";
|
||||||
|
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(YamlWidgetProvider.class);
|
||||||
|
|
||||||
|
private final Map<String, Collection<RootUIComponent>> widgetsMap = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
@Deactivate
|
||||||
|
public void deactivate() {
|
||||||
|
widgetsMap.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getNamespace() {
|
||||||
|
return WIDGETS_NAMESPACE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<RootUIComponent> getAll() {
|
||||||
|
// Ignore isolated models
|
||||||
|
return widgetsMap.keySet().stream().filter(name -> !isIsolatedModel(name))
|
||||||
|
.map(name -> widgetsMap.getOrDefault(name, List.of())).flatMap(Collection::stream).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<YamlWidgetDTO> getElementClass() {
|
||||||
|
return YamlWidgetDTO.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isVersionSupported(int version) {
|
||||||
|
return version >= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isDeprecated() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addedModel(String modelName, Collection<YamlWidgetDTO> elements) {
|
||||||
|
List<RootUIComponent> added = elements.stream().map(this::mapWidget).filter(Objects::nonNull).toList();
|
||||||
|
Collection<RootUIComponent> modelWidgets = Objects
|
||||||
|
.requireNonNull(widgetsMap.computeIfAbsent(modelName, k -> new ArrayList<>()));
|
||||||
|
modelWidgets.addAll(added);
|
||||||
|
added.forEach(widget -> {
|
||||||
|
logger.debug("model {} added widget {}", modelName, widget.getUID());
|
||||||
|
if (!isIsolatedModel(modelName)) {
|
||||||
|
notifyListenersAboutAddedElement(widget);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updatedModel(String modelName, Collection<YamlWidgetDTO> elements) {
|
||||||
|
List<RootUIComponent> updated = elements.stream().map(this::mapWidget).filter(Objects::nonNull).toList();
|
||||||
|
Collection<RootUIComponent> modelWidgets = Objects
|
||||||
|
.requireNonNull(widgetsMap.computeIfAbsent(modelName, k -> new ArrayList<>()));
|
||||||
|
updated.forEach(widget -> {
|
||||||
|
String uid = widget.getUID();
|
||||||
|
modelWidgets.stream().filter(w -> w.getUID().equals(uid)).findFirst().ifPresentOrElse(oldWidget -> {
|
||||||
|
modelWidgets.remove(oldWidget);
|
||||||
|
modelWidgets.add(widget);
|
||||||
|
logger.debug("model {} updated widget {}", modelName, uid);
|
||||||
|
if (!isIsolatedModel(modelName)) {
|
||||||
|
notifyListenersAboutUpdatedElement(oldWidget, widget);
|
||||||
|
}
|
||||||
|
}, () -> {
|
||||||
|
modelWidgets.add(widget);
|
||||||
|
logger.debug("model {} added widget {}", modelName, uid);
|
||||||
|
if (!isIsolatedModel(modelName)) {
|
||||||
|
notifyListenersAboutAddedElement(widget);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removedModel(String modelName, Collection<YamlWidgetDTO> elements) {
|
||||||
|
Collection<RootUIComponent> modelWidgets = widgetsMap.getOrDefault(modelName, List.of());
|
||||||
|
elements.stream().map(YamlWidgetDTO::getId).forEach(uid -> {
|
||||||
|
modelWidgets.stream().filter(w -> w.getUID().equals(uid)).findFirst().ifPresentOrElse(oldWidget -> {
|
||||||
|
modelWidgets.remove(oldWidget);
|
||||||
|
logger.debug("model {} removed widget {}", modelName, uid);
|
||||||
|
if (!isIsolatedModel(modelName)) {
|
||||||
|
notifyListenersAboutRemovedElement(oldWidget);
|
||||||
|
}
|
||||||
|
}, () -> logger.debug("model {} widget {} not found", modelName, uid));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (modelWidgets.isEmpty()) {
|
||||||
|
widgetsMap.remove(modelName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private @Nullable RootUIComponent mapWidget(YamlWidgetDTO dto) {
|
||||||
|
String uid = dto.uid;
|
||||||
|
if (uid == null || uid.isBlank()) {
|
||||||
|
logger.warn("Skipping widget with missing uid");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
RootUIComponent widget = new RootUIComponent(uid, dto.component != null ? dto.component : "");
|
||||||
|
if (dto.config != null) {
|
||||||
|
widget.setConfig(dto.config);
|
||||||
|
}
|
||||||
|
if (dto.slots != null) {
|
||||||
|
widget.setSlots(dto.slots);
|
||||||
|
}
|
||||||
|
if (dto.tags != null) {
|
||||||
|
widget.addTags(dto.tags);
|
||||||
|
}
|
||||||
|
if (dto.props != null) {
|
||||||
|
widget.setProps(dto.props);
|
||||||
|
}
|
||||||
|
Date timestamp = dto.timestamp;
|
||||||
|
if (timestamp != null) {
|
||||||
|
widget.setTimestamp(timestamp);
|
||||||
|
}
|
||||||
|
return widget;
|
||||||
|
}
|
||||||
|
}
|
||||||
+10
@@ -24,4 +24,14 @@ import org.openhab.core.common.registry.Registry;
|
|||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
public interface UIComponentRegistry extends Registry<RootUIComponent, String> {
|
public interface UIComponentRegistry extends Registry<RootUIComponent, String> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the component with the given UID is managed (i.e. editable via the REST API).
|
||||||
|
* File-based components (e.g. loaded from YAML configuration files) are not managed and therefore read-only.
|
||||||
|
*
|
||||||
|
* @param uid the UID of the component
|
||||||
|
* @return {@code true} if the component exists in the managed (jsondb) provider, {@code false} otherwise
|
||||||
|
*/
|
||||||
|
default boolean isEditable(String uid) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
@@ -63,4 +63,10 @@ public class UIComponentRegistryImpl extends AbstractRegistry<RootUIComponent, S
|
|||||||
}
|
}
|
||||||
super.removeProvider(provider);
|
super.removeProvider(provider);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isEditable(String uid) {
|
||||||
|
ManagedProvider<RootUIComponent, String> managed = getManagedProvider();
|
||||||
|
return managed != null && managed.get(uid) != null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -429,6 +429,7 @@
|
|||||||
|
|
||||||
<feature name="openhab-core-model-yaml" version="${project.version}">
|
<feature name="openhab-core-model-yaml" version="${project.version}">
|
||||||
<feature>openhab-core-base</feature>
|
<feature>openhab-core-base</feature>
|
||||||
|
<feature>openhab-core-ui</feature>
|
||||||
<bundle>mvn:org.openhab.core.bundles/org.openhab.core.model.yaml/${project.version}</bundle>
|
<bundle>mvn:org.openhab.core.bundles/org.openhab.core.model.yaml/${project.version}</bundle>
|
||||||
<requirement>openhab.tp;filter:="(feature=jackson)"</requirement>
|
<requirement>openhab.tp;filter:="(feature=jackson)"</requirement>
|
||||||
<feature dependency="true">openhab.tp-jackson</feature>
|
<feature dependency="true">openhab.tp-jackson</feature>
|
||||||
|
|||||||
Reference in New Issue
Block a user