Device, Property, Dummy, Events working
This commit is contained in:
commit
668c590306
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
/target/
|
||||||
|
/.idea/
|
||||||
|
/*.iml
|
||||||
|
/*.db
|
||||||
8
application.properties
Normal file
8
application.properties
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
logging.level.de.ph87=DEBUG
|
||||||
|
#-
|
||||||
|
spring.datasource.url=jdbc:h2:./database;AUTO_SERVER=TRUE
|
||||||
|
spring.datasource.driverClassName=org.h2.Driver
|
||||||
|
spring.datasource.username=sa
|
||||||
|
spring.datasource.password=password
|
||||||
|
#-
|
||||||
|
spring.jpa.hibernate.ddl-auto=create
|
||||||
53
pom.xml
Normal file
53
pom.xml
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<groupId>de.ph87</groupId>
|
||||||
|
<artifactId>Home4</artifactId>
|
||||||
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.source>21</maven.compiler.source>
|
||||||
|
<maven.compiler.target>21</maven.compiler.target>
|
||||||
|
<maven.compiler.release>21</maven.compiler.release>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<parent>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-parent</artifactId>
|
||||||
|
<version>3.3.5</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-websocket</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.h2database</groupId>
|
||||||
|
<artifactId>h2</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.postgresql</groupId>
|
||||||
|
<artifactId>postgresql</artifactId>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
</project>
|
||||||
13
src/main/java/de/ph87/home/Backend.java
Normal file
13
src/main/java/de/ph87/home/Backend.java
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package de.ph87.home;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
|
@SpringBootApplication
|
||||||
|
public class Backend {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(Backend.class, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
39
src/main/java/de/ph87/home/device/Device.java
Normal file
39
src/main/java/de/ph87/home/device/Device.java
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package de.ph87.home.device;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import lombok.*;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Getter
|
||||||
|
@ToString
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class Device {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@NonNull
|
||||||
|
private String uuid = UUID.randomUUID().toString();
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Column(nullable = false, unique = true)
|
||||||
|
private String slug;
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
@NonNull
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String stateProperty;
|
||||||
|
|
||||||
|
public Device(@NonNull final String name, @NonNull final String slug, @NonNull final String stateProperty) {
|
||||||
|
this.name = name;
|
||||||
|
this.slug = slug;
|
||||||
|
this.stateProperty = stateProperty;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
50
src/main/java/de/ph87/home/device/DeviceController.java
Normal file
50
src/main/java/de/ph87/home/device/DeviceController.java
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
package de.ph87.home.device;
|
||||||
|
|
||||||
|
import de.ph87.home.property.PropertyNotFound;
|
||||||
|
import de.ph87.home.property.PropertyNotWritable;
|
||||||
|
import de.ph87.home.property.PropertyTypeMismatch;
|
||||||
|
import jakarta.annotation.Nullable;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@RequestMapping("Device")
|
||||||
|
public class DeviceController {
|
||||||
|
|
||||||
|
private final DeviceService deviceService;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@RequestMapping(value = "list", method = {RequestMethod.GET, RequestMethod.POST})
|
||||||
|
private List<DeviceDto> list(@RequestBody(required = false) @Nullable final DeviceFilter filter, @NonNull final HttpServletRequest request) {
|
||||||
|
log.debug("list: path={} filter={}", request.getServletPath(), filter);
|
||||||
|
return deviceService.list(filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@GetMapping("get/{uuidOrSlug}")
|
||||||
|
private DeviceDto get(@PathVariable @NonNull final String uuidOrSlug, @NonNull final HttpServletRequest request) throws DeviceNotFound {
|
||||||
|
log.debug("get: path={}", request.getServletPath());
|
||||||
|
return deviceService.toDto(uuidOrSlug);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@GetMapping("getState/{uuidOrSlug}")
|
||||||
|
private Boolean getState(@PathVariable @NonNull final String uuidOrSlug, @NonNull final HttpServletRequest request) throws DeviceNotFound {
|
||||||
|
log.debug("getState: path={}", request.getServletPath());
|
||||||
|
return deviceService.toDto(uuidOrSlug).getStateValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("setState/{uuidOrSlug}/{state}")
|
||||||
|
private void setState(@PathVariable @NonNull final String uuidOrSlug, @PathVariable final boolean state, @NonNull final HttpServletRequest request) throws PropertyNotFound, DeviceNotFound, PropertyNotWritable, PropertyTypeMismatch {
|
||||||
|
log.debug("setState: path={}", request.getServletPath());
|
||||||
|
deviceService.setState(uuidOrSlug, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
44
src/main/java/de/ph87/home/device/DeviceDto.java
Normal file
44
src/main/java/de/ph87/home/device/DeviceDto.java
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
package de.ph87.home.device;
|
||||||
|
|
||||||
|
import de.ph87.home.property.State;
|
||||||
|
import jakarta.annotation.Nullable;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@ToString
|
||||||
|
public class DeviceDto {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private final String uuid;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private final String slug;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private final String stateProperty;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private final State<Boolean> state;
|
||||||
|
|
||||||
|
public DeviceDto(@NonNull final Device device, @Nullable final State<Boolean> state) {
|
||||||
|
this.uuid = device.getUuid();
|
||||||
|
this.name = device.getName();
|
||||||
|
this.slug = device.getSlug();
|
||||||
|
this.stateProperty = device.getStateProperty();
|
||||||
|
this.state = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public Boolean getStateValue() {
|
||||||
|
if (state == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return state.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
21
src/main/java/de/ph87/home/device/DeviceEvent.java
Normal file
21
src/main/java/de/ph87/home/device/DeviceEvent.java
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package de.ph87.home.device;
|
||||||
|
|
||||||
|
import de.ph87.home.property.PropertyDto;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@ToString
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DeviceEvent {
|
||||||
|
|
||||||
|
private final DeviceDto deviceDto;
|
||||||
|
|
||||||
|
private final PropertyDto<?> propertyDto;
|
||||||
|
|
||||||
|
public boolean isValueDifferent() {
|
||||||
|
return propertyDto.isValueDifferent();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
39
src/main/java/de/ph87/home/device/DeviceFilter.java
Normal file
39
src/main/java/de/ph87/home/device/DeviceFilter.java
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package de.ph87.home.device;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import jakarta.annotation.Nullable;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@ToString
|
||||||
|
public class DeviceFilter {
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@JsonProperty
|
||||||
|
private Boolean stateNull;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@JsonProperty
|
||||||
|
private Boolean stateTrue;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@JsonProperty
|
||||||
|
private Boolean stateFalse;
|
||||||
|
|
||||||
|
@SuppressWarnings("RedundantIfStatement")
|
||||||
|
public boolean filter(@NonNull final DeviceDto dto) {
|
||||||
|
if (stateNull != null && stateNull != (dto.getState() == null)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (stateTrue != null && (dto.getState() == null || stateTrue != dto.getState().getValue())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (stateFalse != null && (dto.getState() == null || stateFalse == dto.getState().getValue())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
14
src/main/java/de/ph87/home/device/DeviceNotFound.java
Normal file
14
src/main/java/de/ph87/home/device/DeviceNotFound.java
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package de.ph87.home.device;
|
||||||
|
|
||||||
|
import lombok.NonNull;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||||
|
|
||||||
|
@ResponseStatus(HttpStatus.NOT_FOUND)
|
||||||
|
public class DeviceNotFound extends Exception {
|
||||||
|
|
||||||
|
public DeviceNotFound(@NonNull final String key, final @NonNull String value) {
|
||||||
|
super("Device not found: %s=%s".formatted(key, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
15
src/main/java/de/ph87/home/device/DeviceRepository.java
Normal file
15
src/main/java/de/ph87/home/device/DeviceRepository.java
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
package de.ph87.home.device;
|
||||||
|
|
||||||
|
import lombok.NonNull;
|
||||||
|
import org.springframework.data.repository.ListCrudRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public interface DeviceRepository extends ListCrudRepository<Device, String> {
|
||||||
|
|
||||||
|
Optional<Device> findByUuidOrSlug(@NonNull String uuid, @NonNull String slug);
|
||||||
|
|
||||||
|
List<Device> findAllByStateProperty(@NonNull String propertyId);
|
||||||
|
|
||||||
|
}
|
||||||
78
src/main/java/de/ph87/home/device/DeviceService.java
Normal file
78
src/main/java/de/ph87/home/device/DeviceService.java
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
package de.ph87.home.device;
|
||||||
|
|
||||||
|
import de.ph87.home.property.*;
|
||||||
|
import jakarta.annotation.Nullable;
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
|
import org.springframework.context.event.EventListener;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@Transactional
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DeviceService {
|
||||||
|
|
||||||
|
private final PropertyService propertyService;
|
||||||
|
|
||||||
|
private final DeviceRepository deviceRepository;
|
||||||
|
|
||||||
|
private final ApplicationEventPublisher applicationEventPublisher;
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void postConstruct() {
|
||||||
|
deviceRepository.save(new Device("EG Ambiente", "eg_ambiente", "eg_ambiente"));
|
||||||
|
deviceRepository.save(new Device("Wohnzimmer Fernseher", "wohnzimmer_fernseher", "wohnzimmer_fernseher"));
|
||||||
|
deviceRepository.save(new Device("Wohnzimmer Verstärker", "wohnzimmer_verstaerker", "wohnzimmer_verstaerker"));
|
||||||
|
deviceRepository.save(new Device("Wohnzimmer Fensterdeko", "wohnzimmer_fensterdeko", "wohnzimmer_fensterdeko"));
|
||||||
|
deviceRepository.save(new Device("Wohnzimmer Hängelampe", "wohnzimmer_haengelampe", "wohnzimmer_haengelampe"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setState(@NonNull final String uuidOrSlug, final boolean state) throws DeviceNotFound, PropertyNotFound, PropertyNotWritable, PropertyTypeMismatch {
|
||||||
|
log.debug("setState: uuidOrSlug={}, state={}", uuidOrSlug, state);
|
||||||
|
final Device device = byUuidOrSlug(uuidOrSlug);
|
||||||
|
log.debug("setState: device={}", device);
|
||||||
|
propertyService.write(device.getStateProperty(), state);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public DeviceDto toDto(final @NonNull String uuidOrSlug) throws DeviceNotFound {
|
||||||
|
return toDto(byUuidOrSlug(uuidOrSlug));
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public DeviceDto toDto(@NonNull final Device device) {
|
||||||
|
final State<Boolean> state = propertyService.readSafe(device.getStateProperty(), Boolean.class);
|
||||||
|
return new DeviceDto(device, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private Device byUuidOrSlug(@NonNull final String uuidOrSlug) throws DeviceNotFound {
|
||||||
|
return deviceRepository.findByUuidOrSlug(uuidOrSlug, uuidOrSlug).orElseThrow(() -> new DeviceNotFound("uuidOrSlug", uuidOrSlug));
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public List<DeviceDto> list(@Nullable final DeviceFilter filter) {
|
||||||
|
return deviceRepository.findAll().stream().map(this::toDto).filter(device -> filter == null || filter.filter(device)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventListener(PropertyDto.class)
|
||||||
|
public void onPropertyChange(@NonNull final PropertyDto<?> dto) {
|
||||||
|
deviceRepository.findAllByStateProperty(dto.getId())
|
||||||
|
.forEach(device -> {
|
||||||
|
final DeviceEvent deviceEvent = new DeviceEvent(toDto(device), dto);
|
||||||
|
log.debug("Device updated: {}", deviceEvent.getDeviceDto());
|
||||||
|
if (deviceEvent.isValueDifferent()) {
|
||||||
|
log.info("Device changed: {}", deviceEvent.getDeviceDto());
|
||||||
|
}
|
||||||
|
applicationEventPublisher.publishEvent(deviceEvent);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
30
src/main/java/de/ph87/home/dummy/DummyService.java
Normal file
30
src/main/java/de/ph87/home/dummy/DummyService.java
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package de.ph87.home.dummy;
|
||||||
|
|
||||||
|
import de.ph87.home.property.PropertyService;
|
||||||
|
import de.ph87.home.property.State;
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DummyService {
|
||||||
|
|
||||||
|
private final PropertyService propertyService;
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void postConstruct() {
|
||||||
|
register("eg_ambiente");
|
||||||
|
register("wohnzimmer_fernseher");
|
||||||
|
register("wohnzimmer_verstaerker");
|
||||||
|
register("wohnzimmer_fensterdeko");
|
||||||
|
register("wohnzimmer_haengelampe");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void register(final String id) {
|
||||||
|
propertyService.register(id, Boolean.class, (property, value) -> property.setState(new State<>(value)));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
55
src/main/java/de/ph87/home/property/Property.java
Normal file
55
src/main/java/de/ph87/home/property/Property.java
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
package de.ph87.home.property;
|
||||||
|
|
||||||
|
import jakarta.annotation.Nullable;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.function.BiConsumer;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@ToString
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class Property<T> {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private final String id;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private final Class<T> type;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private final BiConsumer<Property<T>, T> write;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private final Consumer<Property<T>> onStateSet;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private State<T> lastState = null;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private State<T> state = null;
|
||||||
|
|
||||||
|
private boolean valueDifferent = false;
|
||||||
|
|
||||||
|
public void setState(@Nullable final State<T> state) {
|
||||||
|
this.lastState = this.state;
|
||||||
|
this.state = state;
|
||||||
|
this.valueDifferent = (lastState == null) == (state == null) && (lastState == null || Objects.equals(lastState.getValue(), state.getValue()));
|
||||||
|
this.onStateSet.accept(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void write(@NonNull final Object value) throws PropertyNotWritable, PropertyTypeMismatch {
|
||||||
|
if (!type.isInstance(value)) {
|
||||||
|
throw new PropertyTypeMismatch(this, value);
|
||||||
|
}
|
||||||
|
if (write == null) {
|
||||||
|
throw new PropertyNotWritable(this);
|
||||||
|
}
|
||||||
|
write.accept(this, type.cast(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
34
src/main/java/de/ph87/home/property/PropertyDto.java
Normal file
34
src/main/java/de/ph87/home/property/PropertyDto.java
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package de.ph87.home.property;
|
||||||
|
|
||||||
|
import jakarta.annotation.Nullable;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@ToString
|
||||||
|
public class PropertyDto<T> {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private final String id;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private final Class<T> type;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private final State<T> lastState;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private final State<T> state;
|
||||||
|
|
||||||
|
private final boolean valueDifferent;
|
||||||
|
|
||||||
|
public PropertyDto(@NonNull final Property<T> property) {
|
||||||
|
this.id = property.getId();
|
||||||
|
this.type = property.getType();
|
||||||
|
this.state = property.getState();
|
||||||
|
this.lastState = property.getLastState();
|
||||||
|
this.valueDifferent = property.isValueDifferent();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
14
src/main/java/de/ph87/home/property/PropertyNotFound.java
Normal file
14
src/main/java/de/ph87/home/property/PropertyNotFound.java
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package de.ph87.home.property;
|
||||||
|
|
||||||
|
import lombok.NonNull;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||||
|
|
||||||
|
@ResponseStatus(HttpStatus.NOT_FOUND)
|
||||||
|
public class PropertyNotFound extends Exception {
|
||||||
|
|
||||||
|
public PropertyNotFound(@NonNull final String id) {
|
||||||
|
super("Property not found: id=" + id);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
14
src/main/java/de/ph87/home/property/PropertyNotWritable.java
Normal file
14
src/main/java/de/ph87/home/property/PropertyNotWritable.java
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package de.ph87.home.property;
|
||||||
|
|
||||||
|
import lombok.NonNull;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||||
|
|
||||||
|
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||||
|
public class PropertyNotWritable extends Exception {
|
||||||
|
|
||||||
|
public PropertyNotWritable(@NonNull final Property<?> property) {
|
||||||
|
super("Property not writable: id=" + property.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
98
src/main/java/de/ph87/home/property/PropertyService.java
Normal file
98
src/main/java/de/ph87/home/property/PropertyService.java
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
package de.ph87.home.property;
|
||||||
|
|
||||||
|
import jakarta.annotation.Nullable;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.function.BiConsumer;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class PropertyService {
|
||||||
|
|
||||||
|
private final ApplicationEventPublisher applicationEventPublisher;
|
||||||
|
|
||||||
|
private final List<Property<?>> propertyList = new ArrayList<>();
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public <TYPE> State<TYPE> readSafe(final @NonNull String id, @NonNull final Class<TYPE> type) {
|
||||||
|
try {
|
||||||
|
return this.read(id, type);
|
||||||
|
} catch (PropertyTypeMismatch | PropertyNotFound e) {
|
||||||
|
log.error(e.getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public <TYPE> State<TYPE> read(@NonNull final String id, @NonNull final Class<TYPE> type) throws PropertyNotFound, PropertyTypeMismatch {
|
||||||
|
log.debug("read: id={}", id);
|
||||||
|
final Property<?> property = byIdAndType(id, type);
|
||||||
|
if (property.getState() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (type.isInstance(property.getState().getValue())) {
|
||||||
|
//noinspection unchecked
|
||||||
|
return (State<TYPE>) property.getState();
|
||||||
|
}
|
||||||
|
throw new PropertyTypeMismatch(property, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void write(@NonNull final String id, @NonNull final Object value) throws PropertyNotFound, PropertyTypeMismatch, PropertyNotWritable {
|
||||||
|
log.debug("write: id={}, type={}, value={}", id, value.getClass().getSimpleName(), value);
|
||||||
|
final Property<?> property = byIdAndType(id, value.getClass());
|
||||||
|
property.write(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private <TYPE> Property<?> byIdAndType(final String id, final Class<TYPE> type) throws PropertyNotFound, PropertyTypeMismatch {
|
||||||
|
final Property<?> property = findById(id).orElseThrow(() -> new PropertyNotFound(id));
|
||||||
|
if (type != property.getType()) {
|
||||||
|
throw new PropertyTypeMismatch(property, type);
|
||||||
|
}
|
||||||
|
return property;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private Optional<Property<?>> findById(final @NonNull String id) {
|
||||||
|
final Optional<Property<?>> optional;
|
||||||
|
synchronized (propertyList) {
|
||||||
|
optional = propertyList.stream().filter(p -> p.getId().equals(id)).findFirst();
|
||||||
|
}
|
||||||
|
return optional;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("UnusedReturnValue")
|
||||||
|
public <TYPE> Property<TYPE> register(@NonNull final String id, final Class<TYPE> type, final BiConsumer<Property<TYPE>, TYPE> write) {
|
||||||
|
if (id.isEmpty()) {
|
||||||
|
throw new RuntimeException();
|
||||||
|
}
|
||||||
|
final Property<TYPE> property = new Property<>(id, type, write, this::onStateSet);
|
||||||
|
synchronized (propertyList) {
|
||||||
|
propertyList.add(property);
|
||||||
|
}
|
||||||
|
return property;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onStateSet(@NonNull final Property<?> property) {
|
||||||
|
final PropertyDto<?> dto = toDto(property);
|
||||||
|
log.debug("Property updated: {}", dto);
|
||||||
|
if (dto.isValueDifferent()) {
|
||||||
|
log.info("Property changed: {}", dto);
|
||||||
|
}
|
||||||
|
applicationEventPublisher.publishEvent(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private <T> PropertyDto<T> toDto(@NonNull final Property<T> property) {
|
||||||
|
return new PropertyDto<>(property);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
package de.ph87.home.property;
|
||||||
|
|
||||||
|
import lombok.NonNull;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||||
|
|
||||||
|
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||||
|
public class PropertyTypeMismatch extends Exception {
|
||||||
|
|
||||||
|
public PropertyTypeMismatch(@NonNull final Property<?> property, @NonNull final Class<?> type) {
|
||||||
|
super("Property type mismatch: id=%s, expected=%s, given=%s".formatted(property.getId(), property.getType().getSimpleName(), type.getSimpleName()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public PropertyTypeMismatch(@NonNull final Property<?> property, @NonNull final Object value) {
|
||||||
|
super("Property type mismatch: id=%s, expected=%s, given=%s, value=%s".formatted(property.getId(), property.getType().getSimpleName(), value.getClass().getSimpleName(), value));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
25
src/main/java/de/ph87/home/property/State.java
Normal file
25
src/main/java/de/ph87/home/property/State.java
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package de.ph87.home.property;
|
||||||
|
|
||||||
|
import jakarta.annotation.Nullable;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@ToString
|
||||||
|
public class State<T> {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private final ZonedDateTime timestamp;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private final T value;
|
||||||
|
|
||||||
|
public State(@Nullable final T value) {
|
||||||
|
this.timestamp = ZonedDateTime.now();
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
10
src/main/resources/application.properties
Normal file
10
src/main/resources/application.properties
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
logging.level.root=WARN
|
||||||
|
logging.level.de.ph87=INFO
|
||||||
|
#-
|
||||||
|
spring.jpa.hibernate.naming.implicit-strategy=org.hibernate.boot.model.naming.ImplicitNamingStrategyComponentPathImpl
|
||||||
|
spring.jpa.hibernate.ddl-auto=update
|
||||||
|
spring.jpa.open-in-view=false
|
||||||
|
#-
|
||||||
|
spring.jackson.serialization.indent_output=true
|
||||||
|
#-
|
||||||
|
spring.main.banner-mode=off
|
||||||
Loading…
Reference in New Issue
Block a user