tvheadend
This commit is contained in:
parent
668c590306
commit
3e487c10b5
@ -1,4 +1,4 @@
|
|||||||
logging.level.de.ph87=DEBUG
|
logging.level.de.ph87.home.tvheadend.TvheadendService=DEBUG
|
||||||
#-
|
#-
|
||||||
spring.datasource.url=jdbc:h2:./database;AUTO_SERVER=TRUE
|
spring.datasource.url=jdbc:h2:./database;AUTO_SERVER=TRUE
|
||||||
spring.datasource.driverClassName=org.h2.Driver
|
spring.datasource.driverClassName=org.h2.Driver
|
||||||
|
|||||||
5
pom.xml
5
pom.xml
@ -40,6 +40,11 @@
|
|||||||
<artifactId>lombok</artifactId>
|
<artifactId>lombok</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.httpcomponents.client5</groupId>
|
||||||
|
<artifactId>httpclient5</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.h2database</groupId>
|
<groupId>com.h2database</groupId>
|
||||||
<artifactId>h2</artifactId>
|
<artifactId>h2</artifactId>
|
||||||
|
|||||||
@ -0,0 +1,26 @@
|
|||||||
|
package de.ph87.home.common;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonParser;
|
||||||
|
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||||
|
import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
|
||||||
|
public class EpochSecondsToZonedDateTime extends JsonDeserializer<ZonedDateTime> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ZonedDateTime deserialize(final JsonParser jsonParser, final DeserializationContext deserializationContext) throws IOException {
|
||||||
|
final long epochSeconds = jsonParser.getLongValue();
|
||||||
|
if (epochSeconds < 0) {
|
||||||
|
throw new RuntimeException();
|
||||||
|
}
|
||||||
|
if (epochSeconds == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return ZonedDateTime.ofInstant(Instant.ofEpochSecond(epochSeconds), ZoneId.systemDefault());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
17
src/main/java/de/ph87/home/common/SecondsToDuration.java
Normal file
17
src/main/java/de/ph87/home/common/SecondsToDuration.java
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package de.ph87.home.common;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonParser;
|
||||||
|
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||||
|
import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
public class SecondsToDuration extends JsonDeserializer<Duration> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Duration deserialize(final JsonParser jsonParser, final DeserializationContext deserializationContext) throws IOException {
|
||||||
|
return Duration.ofSeconds(jsonParser.getLongValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -15,7 +15,7 @@ public class DeviceEvent {
|
|||||||
private final PropertyDto<?> propertyDto;
|
private final PropertyDto<?> propertyDto;
|
||||||
|
|
||||||
public boolean isValueDifferent() {
|
public boolean isValueDifferent() {
|
||||||
return propertyDto.isValueDifferent();
|
return propertyDto.isValueChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,7 +38,7 @@ public class DeviceService {
|
|||||||
log.debug("setState: uuidOrSlug={}, state={}", uuidOrSlug, state);
|
log.debug("setState: uuidOrSlug={}, state={}", uuidOrSlug, state);
|
||||||
final Device device = byUuidOrSlug(uuidOrSlug);
|
final Device device = byUuidOrSlug(uuidOrSlug);
|
||||||
log.debug("setState: device={}", device);
|
log.debug("setState: device={}", device);
|
||||||
propertyService.write(device.getStateProperty(), state);
|
propertyService.write(device.getStateProperty(), state, Boolean.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
|
|||||||
@ -21,6 +21,7 @@ public class DummyService {
|
|||||||
register("wohnzimmer_verstaerker");
|
register("wohnzimmer_verstaerker");
|
||||||
register("wohnzimmer_fensterdeko");
|
register("wohnzimmer_fensterdeko");
|
||||||
register("wohnzimmer_haengelampe");
|
register("wohnzimmer_haengelampe");
|
||||||
|
register("receiver");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void register(final String id) {
|
private void register(final String id) {
|
||||||
|
|||||||
@ -33,23 +33,43 @@ public class Property<T> {
|
|||||||
@Nullable
|
@Nullable
|
||||||
private State<T> state = null;
|
private State<T> state = null;
|
||||||
|
|
||||||
private boolean valueDifferent = false;
|
private boolean valueChanged = false;
|
||||||
|
|
||||||
public void setState(@Nullable final State<T> state) {
|
public void setState(@Nullable final State<T> state) {
|
||||||
this.lastState = this.state;
|
this.lastState = this.state;
|
||||||
this.state = state;
|
this.state = state;
|
||||||
this.valueDifferent = (lastState == null) == (state == null) && (lastState == null || Objects.equals(lastState.getValue(), state.getValue()));
|
this.valueChanged = (lastState == null) == (state == null) && (lastState == null || Objects.equals(lastState.getValue(), state.getValue()));
|
||||||
this.onStateSet.accept(this);
|
this.onStateSet.accept(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void write(@NonNull final Object value) throws PropertyNotWritable, PropertyTypeMismatch {
|
@Nullable
|
||||||
if (!type.isInstance(value)) {
|
public T getStateValue() {
|
||||||
throw new PropertyTypeMismatch(this, value);
|
if (state == null) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
return state.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public <R extends T> R getStateValueAs(@NonNull final Class<R> type) throws PropertyTypeMismatch {
|
||||||
|
if (this.type != type) {
|
||||||
|
throw new PropertyTypeMismatch(this, type);
|
||||||
|
}
|
||||||
|
//noinspection unchecked
|
||||||
|
return (R) getStateValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public <R extends T> R getStateValueAs(@NonNull final Class<R> type, @NonNull final R fallbackIfNull) throws PropertyTypeMismatch {
|
||||||
|
final R value = getStateValueAs(type);
|
||||||
|
return Objects.requireNonNullElse(value, fallbackIfNull);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void write(@NonNull final T value) throws PropertyNotWritable {
|
||||||
if (write == null) {
|
if (write == null) {
|
||||||
throw new PropertyNotWritable(this);
|
throw new PropertyNotWritable(this);
|
||||||
}
|
}
|
||||||
write.accept(this, type.cast(value));
|
write.accept(this, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,14 +21,14 @@ public class PropertyDto<T> {
|
|||||||
@Nullable
|
@Nullable
|
||||||
private final State<T> state;
|
private final State<T> state;
|
||||||
|
|
||||||
private final boolean valueDifferent;
|
private final boolean valueChanged;
|
||||||
|
|
||||||
public PropertyDto(@NonNull final Property<T> property) {
|
public PropertyDto(@NonNull final Property<T> property) {
|
||||||
this.id = property.getId();
|
this.id = property.getId();
|
||||||
this.type = property.getType();
|
this.type = property.getType();
|
||||||
this.state = property.getState();
|
this.state = property.getState();
|
||||||
this.lastState = property.getLastState();
|
this.lastState = property.getLastState();
|
||||||
this.valueDifferent = property.isValueDifferent();
|
this.valueChanged = property.isValueChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,30 +34,22 @@ public class PropertyService {
|
|||||||
@Nullable
|
@Nullable
|
||||||
public <TYPE> State<TYPE> read(@NonNull final String id, @NonNull final Class<TYPE> type) throws PropertyNotFound, PropertyTypeMismatch {
|
public <TYPE> State<TYPE> read(@NonNull final String id, @NonNull final Class<TYPE> type) throws PropertyNotFound, PropertyTypeMismatch {
|
||||||
log.debug("read: id={}", id);
|
log.debug("read: id={}", id);
|
||||||
final Property<?> property = byIdAndType(id, type);
|
return byIdAndType(id, type).getState();
|
||||||
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 {
|
public <TYPE> void write(@NonNull final String id, @NonNull final TYPE value, final Class<TYPE> type) throws PropertyNotFound, PropertyTypeMismatch, PropertyNotWritable {
|
||||||
log.debug("write: id={}, type={}, value={}", id, value.getClass().getSimpleName(), value);
|
log.debug("write: id={}, type={}, value={}", id, value.getClass().getSimpleName(), value);
|
||||||
final Property<?> property = byIdAndType(id, value.getClass());
|
byIdAndType(id, type).write(value);
|
||||||
property.write(value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
private <TYPE> Property<?> byIdAndType(final String id, final Class<TYPE> type) throws PropertyNotFound, PropertyTypeMismatch {
|
public <TYPE> Property<TYPE> byIdAndType(final String id, final Class<TYPE> type) throws PropertyNotFound, PropertyTypeMismatch {
|
||||||
final Property<?> property = findById(id).orElseThrow(() -> new PropertyNotFound(id));
|
final Property<?> property = findById(id).orElseThrow(() -> new PropertyNotFound(id));
|
||||||
if (type != property.getType()) {
|
if (type != property.getType()) {
|
||||||
throw new PropertyTypeMismatch(property, type);
|
throw new PropertyTypeMismatch(property, type);
|
||||||
}
|
}
|
||||||
return property;
|
//noinspection unchecked
|
||||||
|
return (Property<TYPE>) property;
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@ -84,7 +76,7 @@ public class PropertyService {
|
|||||||
private void onStateSet(@NonNull final Property<?> property) {
|
private void onStateSet(@NonNull final Property<?> property) {
|
||||||
final PropertyDto<?> dto = toDto(property);
|
final PropertyDto<?> dto = toDto(property);
|
||||||
log.debug("Property updated: {}", dto);
|
log.debug("Property updated: {}", dto);
|
||||||
if (dto.isValueDifferent()) {
|
if (dto.isValueChanged()) {
|
||||||
log.info("Property changed: {}", dto);
|
log.info("Property changed: {}", dto);
|
||||||
}
|
}
|
||||||
applicationEventPublisher.publishEvent(dto);
|
applicationEventPublisher.publishEvent(dto);
|
||||||
|
|||||||
@ -11,8 +11,4 @@ public class PropertyTypeMismatch extends Exception {
|
|||||||
super("Property type mismatch: id=%s, expected=%s, given=%s".formatted(property.getId(), property.getType().getSimpleName(), type.getSimpleName()));
|
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
24
src/main/java/de/ph87/home/tvheadend/TvheadendConfig.java
Normal file
24
src/main/java/de/ph87/home/tvheadend/TvheadendConfig.java
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package de.ph87.home.tvheadend;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Component
|
||||||
|
@ConfigurationProperties(prefix = "de.ph87.home.tvheadend")
|
||||||
|
public class TvheadendConfig {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private String receiver = "receiver";
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private Duration BEFORE_RECORDING = Duration.ofMinutes(10);
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private Duration AFTER_NEED = Duration.ofMinutes(10);
|
||||||
|
|
||||||
|
}
|
||||||
148
src/main/java/de/ph87/home/tvheadend/TvheadendService.java
Normal file
148
src/main/java/de/ph87/home/tvheadend/TvheadendService.java
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
package de.ph87.home.tvheadend;
|
||||||
|
|
||||||
|
import de.ph87.home.property.*;
|
||||||
|
import de.ph87.home.tvheadend.api.TvheadendApiService;
|
||||||
|
import de.ph87.home.tvheadend.api.TvheadendStatus;
|
||||||
|
import jakarta.annotation.Nullable;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@EnableScheduling
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class TvheadendService {
|
||||||
|
|
||||||
|
private final TvheadendApiService tvheadendApiService;
|
||||||
|
|
||||||
|
private final TvheadendConfig tvheadendConfig;
|
||||||
|
|
||||||
|
private final PropertyService propertyService;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private Boolean lastNeeded = null;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private ZonedDateTime lastNeededChanged = null;
|
||||||
|
|
||||||
|
@Scheduled(initialDelay = 0, fixedDelay = 5, timeUnit = TimeUnit.SECONDS)
|
||||||
|
public void update() {
|
||||||
|
final TvheadendStatus status;
|
||||||
|
try {
|
||||||
|
status = tvheadendApiService.fetch();
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Failed to get tvheadend status: {}", e.getMessage());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Property<Boolean> receiver;
|
||||||
|
try {
|
||||||
|
receiver = propertyService.byIdAndType(tvheadendConfig.getReceiver(), Boolean.class);
|
||||||
|
} catch (PropertyNotFound | PropertyTypeMismatch e) {
|
||||||
|
log.warn("Failed to retrieve receiver Property: id={}, error={}", tvheadendConfig.getReceiver(), e.getMessage());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
updateUnsafe(status, receiver);
|
||||||
|
} catch (PropertyNotWritable | PropertyNotFound | PropertyTypeMismatch e) {
|
||||||
|
log.error("Cannot update receiver: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateUnsafe(@NonNull final TvheadendStatus status, @NonNull final Property<Boolean> receiver) throws PropertyNotFound, PropertyTypeMismatch, PropertyNotWritable {
|
||||||
|
final ZonedDateTime now = ZonedDateTime.now();
|
||||||
|
final String shouldBeMessage;
|
||||||
|
if (status.receiverNeeded) {
|
||||||
|
shouldBeMessage = "Receiver is needed.";
|
||||||
|
} else if (status.recordingNext != null) {
|
||||||
|
shouldBeMessage = "Receiver is needed in %s".formatted(durationToString(Duration.between(now, status.recordingNext.getStart_real().minus(tvheadendConfig.getBEFORE_RECORDING()))));
|
||||||
|
}else{
|
||||||
|
shouldBeMessage = "Receiver is NOT needed.";
|
||||||
|
}
|
||||||
|
final boolean justChanged = lastNeededChanged == null || !Objects.equals(lastNeeded, status.receiverNeeded);
|
||||||
|
if (justChanged) {
|
||||||
|
log.info(shouldBeMessage);
|
||||||
|
lastNeeded = status.receiverNeeded;
|
||||||
|
lastNeededChanged = now;
|
||||||
|
} else {
|
||||||
|
log.debug(shouldBeMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
final boolean isOn = receiver.getStateValueAs(Boolean.class, false);
|
||||||
|
if (status.receiverNeeded) {
|
||||||
|
if (!isOn) {
|
||||||
|
doSwitch(receiver, true);
|
||||||
|
} else if (justChanged) {
|
||||||
|
log.info("Receiver is already ON");
|
||||||
|
}
|
||||||
|
} else if (isOn) {
|
||||||
|
final Duration neededOffFor = Duration.between(lastNeededChanged, now);
|
||||||
|
final Duration rest = tvheadendConfig.getAFTER_NEED().minus(neededOffFor);
|
||||||
|
if (rest.toNanos() <= 0) {
|
||||||
|
doSwitch(receiver, false);
|
||||||
|
} else {
|
||||||
|
final String delayedMessage = "Receiver shutdown in %s".formatted(durationToString(rest));
|
||||||
|
if (justChanged) {
|
||||||
|
log.info(delayedMessage);
|
||||||
|
} else {
|
||||||
|
log.debug(delayedMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (justChanged) {
|
||||||
|
log.info("Receiver is already OFF");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private String durationToString(@NonNull final Duration duration) {
|
||||||
|
if (duration.toDays() > 1) {
|
||||||
|
return "%dd".formatted(duration.toDays());
|
||||||
|
} else if (duration.toHours() > 1) {
|
||||||
|
return "%dh".formatted(duration.toHours());
|
||||||
|
} else if (duration.toMinutes() > 1) {
|
||||||
|
return "%dm".formatted(duration.toMinutes());
|
||||||
|
} else if (duration.toSeconds() > 1) {
|
||||||
|
return "%ds".formatted(duration.toSeconds());
|
||||||
|
} else if (duration.toMillis() > 1) {
|
||||||
|
return "%dms".formatted(duration.toMillis());
|
||||||
|
}
|
||||||
|
return "now";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void doSwitch(@NonNull final Property<Boolean> receiver, final boolean state) throws PropertyNotWritable {
|
||||||
|
log.info("Switching receiver {}", state ? "ON" : "OFF");
|
||||||
|
receiver.write(state);
|
||||||
|
if (!state) {
|
||||||
|
log.info("Restarting tvheadend...");
|
||||||
|
try {
|
||||||
|
final Process process = Runtime.getRuntime().exec(new String[]{"sudo", "/bin/systemctl", "restart", "tvheadend"});
|
||||||
|
final String output = new String(process.getInputStream().readAllBytes()).replace("\n", "\\n").replace("\r", "\\r");
|
||||||
|
final int code = process.waitFor();
|
||||||
|
if (code != 0) {
|
||||||
|
log.error("return: {}", code);
|
||||||
|
log.error("output: {}", output);
|
||||||
|
throw new IOException("Process returned: %d".formatted(code));
|
||||||
|
} else {
|
||||||
|
log.debug("return: {}", code);
|
||||||
|
log.debug("output: {}", output);
|
||||||
|
}
|
||||||
|
log.info("tvheadend restarted");
|
||||||
|
} catch (IOException | InterruptedException e) {
|
||||||
|
log.error("Failed to restart tvheadend: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,43 @@
|
|||||||
|
package de.ph87.home.tvheadend.api;
|
||||||
|
|
||||||
|
import de.ph87.home.tvheadend.TvheadendConfig;
|
||||||
|
import de.ph87.home.tvheadend.api.dto.DvrEntryGridUpcoming;
|
||||||
|
import de.ph87.home.tvheadend.api.dto.StatusConnections;
|
||||||
|
import de.ph87.home.tvheadend.api.dto.StatusInputs;
|
||||||
|
import de.ph87.home.tvheadend.api.dto.StatusSubscriptions;
|
||||||
|
import jakarta.annotation.Nullable;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Getter
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class TvheadendApiService {
|
||||||
|
|
||||||
|
private final TvheadendHttpHelper tvheadendHttpHelper;
|
||||||
|
|
||||||
|
private final TvheadendConfig tvheadendConfig;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private TvheadendStatus status = null;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public TvheadendStatus fetch() throws IOException {
|
||||||
|
status = null;
|
||||||
|
status = new TvheadendStatus(
|
||||||
|
tvheadendConfig.getBEFORE_RECORDING(),
|
||||||
|
tvheadendHttpHelper.get("status/inputs", StatusInputs.class).getEntries(),
|
||||||
|
tvheadendHttpHelper.get("status/subscriptions", StatusSubscriptions.class).getEntries(),
|
||||||
|
tvheadendHttpHelper.get("status/connections", StatusConnections.class).getEntries(),
|
||||||
|
tvheadendHttpHelper.get("dvr/entry/grid_upcoming", DvrEntryGridUpcoming.class).getEntries()
|
||||||
|
);
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,75 @@
|
|||||||
|
package de.ph87.home.tvheadend.api;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonMappingException;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.hc.client5.http.utils.Base64;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public class TvheadendHttpHelper {
|
||||||
|
|
||||||
|
private static final String URL = "http://10.0.0.50:9981/api/%s";
|
||||||
|
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public <T> T get(@NonNull final String path, @NonNull final Class<? extends T> clazz) throws IOException {
|
||||||
|
return parse(clazz, open(false, path));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void post(@NonNull final String path, @NonNull final Object request) throws IOException {
|
||||||
|
final HttpURLConnection connection = openPost(path, request);
|
||||||
|
if (connection.getResponseCode() != 200) {
|
||||||
|
throw new IOException("Response code: " + connection.getResponseCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public <T> T post(@NonNull final String path, @NonNull final Object request, @NonNull final Class<? extends T> clazz) throws IOException {
|
||||||
|
return parse(clazz, openPost(path, request));
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private static HttpURLConnection openPost(@NonNull final String path, @NonNull final Object request) throws IOException {
|
||||||
|
final HttpURLConnection connection = open(true, path);
|
||||||
|
connection.getOutputStream().write(request.toString().getBytes());
|
||||||
|
return connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private <T> T parse(@NonNull final Class<? extends T> clazz, @NonNull final HttpURLConnection connection) throws IOException {
|
||||||
|
final String response = new String(connection.getInputStream().readAllBytes());
|
||||||
|
log.debug(response);
|
||||||
|
try {
|
||||||
|
return objectMapper.readValue(response, clazz);
|
||||||
|
} catch (JsonMappingException e) {
|
||||||
|
log.error("Failed to deserialize json: {}", e.getMessage());
|
||||||
|
log.error(response);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private static HttpURLConnection open(final boolean post, @NonNull final String path) throws IOException {
|
||||||
|
final String url = URL.formatted(path);
|
||||||
|
final String method = post ? "POST" : "GET";
|
||||||
|
log.debug("open {} {}", method, url);
|
||||||
|
final HttpURLConnection connection = (HttpURLConnection) URI.create(url).toURL().openConnection();
|
||||||
|
connection.setRequestMethod(method);
|
||||||
|
connection.setDoOutput(post);
|
||||||
|
connection.setRequestProperty("Authorization", "Basic " + new String(Base64.encodeBase64("api:api".getBytes(StandardCharsets.UTF_8))));
|
||||||
|
return connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
package de.ph87.home.tvheadend.api;
|
||||||
|
|
||||||
|
import de.ph87.home.tvheadend.api.dto.DvrEntryGridUpcoming;
|
||||||
|
import de.ph87.home.tvheadend.api.dto.StatusConnections;
|
||||||
|
import de.ph87.home.tvheadend.api.dto.StatusInputs;
|
||||||
|
import de.ph87.home.tvheadend.api.dto.StatusSubscriptions;
|
||||||
|
import jakarta.annotation.Nullable;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@ToString
|
||||||
|
public class TvheadendStatus {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final List<StatusConnections.Entry> connectionList;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final List<StatusSubscriptions.Entry> subscriptionList;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final List<StatusInputs.Entry> inputList;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final List<DvrEntryGridUpcoming.Entry> recordingList;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public final DvrEntryGridUpcoming.Entry recordingNext;
|
||||||
|
|
||||||
|
public final boolean receiverNeededForConnected;
|
||||||
|
|
||||||
|
public final boolean receiverNeededForSubscribed;
|
||||||
|
|
||||||
|
public final boolean receiverNeededForRecording;
|
||||||
|
|
||||||
|
public final boolean receiverNeeded;
|
||||||
|
|
||||||
|
public TvheadendStatus(
|
||||||
|
@NonNull final Duration beforeRecording,
|
||||||
|
@NonNull final List<StatusInputs.Entry> inputList,
|
||||||
|
@NonNull final List<StatusSubscriptions.Entry> subscriptionList,
|
||||||
|
@NonNull final List<StatusConnections.Entry> connectionList,
|
||||||
|
@NonNull final List<DvrEntryGridUpcoming.Entry> recordingList
|
||||||
|
) {
|
||||||
|
this.connectionList = connectionList;
|
||||||
|
this.subscriptionList = subscriptionList;
|
||||||
|
this.inputList = inputList;
|
||||||
|
this.recordingList = recordingList;
|
||||||
|
this.recordingNext = recordingList.stream().min(Comparator.comparing(DvrEntryGridUpcoming.Entry::getStart_real)).orElse(null);
|
||||||
|
this.receiverNeededForConnected = !connectionList.isEmpty();
|
||||||
|
this.receiverNeededForSubscribed = !subscriptionList.isEmpty();
|
||||||
|
this.receiverNeededForRecording = recordingNext != null && recordingNext.shouldBeOnForRecording(beforeRecording);
|
||||||
|
this.receiverNeeded = this.receiverNeededForConnected || this.receiverNeededForSubscribed || this.receiverNeededForRecording;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,259 @@
|
|||||||
|
package de.ph87.home.tvheadend.api.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||||
|
import de.ph87.home.common.EpochSecondsToZonedDateTime;
|
||||||
|
import de.ph87.home.common.SecondsToDuration;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import java.time.temporal.TemporalAmount;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@ToString
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public class DvrEntryGridUpcoming {
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private long total;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private List<Entry> entries;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@ToString
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public static class Entry {
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String uuid;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private boolean enabled;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@JsonDeserialize(using = EpochSecondsToZonedDateTime.class)
|
||||||
|
private ZonedDateTime create;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private long watched;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@JsonDeserialize(using = EpochSecondsToZonedDateTime.class)
|
||||||
|
private ZonedDateTime start;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@JsonDeserialize(using = SecondsToDuration.class)
|
||||||
|
private Duration start_extra;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@JsonDeserialize(using = EpochSecondsToZonedDateTime.class)
|
||||||
|
private ZonedDateTime start_real;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@JsonDeserialize(using = EpochSecondsToZonedDateTime.class)
|
||||||
|
private ZonedDateTime stop;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@JsonDeserialize(using = SecondsToDuration.class)
|
||||||
|
private Duration stop_extra;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@JsonDeserialize(using = EpochSecondsToZonedDateTime.class)
|
||||||
|
private ZonedDateTime stop_real;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@JsonDeserialize(using = SecondsToDuration.class)
|
||||||
|
private Duration duration;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String channel;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String channelname;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String image;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String fanart_image;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private Map<String, String> title;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String disp_title;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String disp_subtitle;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private Map<String, String> summary;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String disp_summary;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@ToString.Exclude
|
||||||
|
private Map<String, String> description;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@ToString.Exclude
|
||||||
|
private String disp_description;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String disp_extratext;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private int pri;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private int retention;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private int removal;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private int playposition;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private int playcount;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String config_name;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String owner;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String creator;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String filename;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private int errorcode;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private int errors;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private int data_errors;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private long dvb_eid;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private boolean noresched;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private boolean norerecord;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private long fileremoved;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String uri;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String autorec;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String autorec_caption;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String timerec;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String timerec_caption;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String parent;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String child;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private long content_type;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private int copyright_year;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private long broadcast;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String episode_disp;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String url;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private long filesize;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String sched_status;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private long duplicate;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@JsonDeserialize(using = EpochSecondsToZonedDateTime.class)
|
||||||
|
private ZonedDateTime first_aired;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String comment;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private List<String> category;
|
||||||
|
|
||||||
|
// @JsonProperty
|
||||||
|
// private List<Object> credits;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private List<String> keyword;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private List<Integer> genre;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private int age_rating;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String rating_label_saved;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String rating_icon_saved;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String rating_country_saved;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String rating_authority_saved;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String rating_label_uuid;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String rating_icon;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String rating_label;
|
||||||
|
|
||||||
|
public boolean shouldBeOnForRecording(@NonNull final TemporalAmount tolerance) {
|
||||||
|
final ZonedDateTime now = ZonedDateTime.now();
|
||||||
|
return !start_real.minus(tolerance).isAfter(now) && !stop_real.plus(tolerance).isBefore(now);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
package de.ph87.home.tvheadend.api.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||||
|
import de.ph87.home.common.EpochSecondsToZonedDateTime;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@ToString
|
||||||
|
public class StatusConnections {
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private long totalCount;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private List<Entry> entries;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@ToString
|
||||||
|
public static class Entry {
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private long id;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String server;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private int server_port;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String peer;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private int peer_port;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@JsonDeserialize(using = EpochSecondsToZonedDateTime.class)
|
||||||
|
private ZonedDateTime started;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private long streaming;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String type;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String user;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,82 @@
|
|||||||
|
package de.ph87.home.tvheadend.api.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@ToString
|
||||||
|
public class StatusInputs {
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private long totalCount;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private List<Entry> entries;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@ToString
|
||||||
|
public static class Entry {
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private int ec_block;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String uuid;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private int signal_scale;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private int weight;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private int cc;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private int ber;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private int tc_block;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String input;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private int tc_bit;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private int unc;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private int te;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private int bps;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private int subs;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private int ec_bit;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private List<Integer> pids;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String stream;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private int snr;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private int signal;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private int snr_scale;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,77 @@
|
|||||||
|
package de.ph87.home.tvheadend.api.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||||
|
import de.ph87.home.common.EpochSecondsToZonedDateTime;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@ToString
|
||||||
|
public class StatusSubscriptions {
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private long totalCount;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private List<Entry> entries;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@ToString
|
||||||
|
public static class Entry {
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private long id;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@JsonDeserialize(using = EpochSecondsToZonedDateTime.class)
|
||||||
|
private ZonedDateTime start;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private long errors;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String state;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String hostname;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String client;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String channel;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String service;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private List<Integer> pids;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private String profile;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private long in;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private long out;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private long total_in;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
private long total_out;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user