diff --git a/application.properties b/application.properties index 29f3977..ded2b8e 100644 --- a/application.properties +++ b/application.properties @@ -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.driverClassName=org.h2.Driver diff --git a/pom.xml b/pom.xml index 81b2651..810c67c 100644 --- a/pom.xml +++ b/pom.xml @@ -40,6 +40,11 @@ lombok + + org.apache.httpcomponents.client5 + httpclient5 + + com.h2database h2 diff --git a/src/main/java/de/ph87/home/common/EpochSecondsToZonedDateTime.java b/src/main/java/de/ph87/home/common/EpochSecondsToZonedDateTime.java new file mode 100644 index 0000000..35f3935 --- /dev/null +++ b/src/main/java/de/ph87/home/common/EpochSecondsToZonedDateTime.java @@ -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 { + + @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()); + } + +} diff --git a/src/main/java/de/ph87/home/common/SecondsToDuration.java b/src/main/java/de/ph87/home/common/SecondsToDuration.java new file mode 100644 index 0000000..c68e64c --- /dev/null +++ b/src/main/java/de/ph87/home/common/SecondsToDuration.java @@ -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 { + + @Override + public Duration deserialize(final JsonParser jsonParser, final DeserializationContext deserializationContext) throws IOException { + return Duration.ofSeconds(jsonParser.getLongValue()); + } + +} diff --git a/src/main/java/de/ph87/home/device/DeviceEvent.java b/src/main/java/de/ph87/home/device/DeviceEvent.java index 92eb71e..544c024 100644 --- a/src/main/java/de/ph87/home/device/DeviceEvent.java +++ b/src/main/java/de/ph87/home/device/DeviceEvent.java @@ -15,7 +15,7 @@ public class DeviceEvent { private final PropertyDto propertyDto; public boolean isValueDifferent() { - return propertyDto.isValueDifferent(); + return propertyDto.isValueChanged(); } } diff --git a/src/main/java/de/ph87/home/device/DeviceService.java b/src/main/java/de/ph87/home/device/DeviceService.java index 1527507..850c2c5 100644 --- a/src/main/java/de/ph87/home/device/DeviceService.java +++ b/src/main/java/de/ph87/home/device/DeviceService.java @@ -38,7 +38,7 @@ public class DeviceService { log.debug("setState: uuidOrSlug={}, state={}", uuidOrSlug, state); final Device device = byUuidOrSlug(uuidOrSlug); log.debug("setState: device={}", device); - propertyService.write(device.getStateProperty(), state); + propertyService.write(device.getStateProperty(), state, Boolean.class); } @NonNull diff --git a/src/main/java/de/ph87/home/dummy/DummyService.java b/src/main/java/de/ph87/home/dummy/DummyService.java index 4b021ac..1b978aa 100644 --- a/src/main/java/de/ph87/home/dummy/DummyService.java +++ b/src/main/java/de/ph87/home/dummy/DummyService.java @@ -21,6 +21,7 @@ public class DummyService { register("wohnzimmer_verstaerker"); register("wohnzimmer_fensterdeko"); register("wohnzimmer_haengelampe"); + register("receiver"); } private void register(final String id) { diff --git a/src/main/java/de/ph87/home/property/Property.java b/src/main/java/de/ph87/home/property/Property.java index 78c27df..4e2ae72 100644 --- a/src/main/java/de/ph87/home/property/Property.java +++ b/src/main/java/de/ph87/home/property/Property.java @@ -33,23 +33,43 @@ public class Property { @Nullable private State state = null; - private boolean valueDifferent = false; + private boolean valueChanged = false; public void setState(@Nullable final State state) { this.lastState = this.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); } - public void write(@NonNull final Object value) throws PropertyNotWritable, PropertyTypeMismatch { - if (!type.isInstance(value)) { - throw new PropertyTypeMismatch(this, value); + @Nullable + public T getStateValue() { + if (state == null) { + return null; } + return state.getValue(); + } + + @Nullable + public R getStateValueAs(@NonNull final Class type) throws PropertyTypeMismatch { + if (this.type != type) { + throw new PropertyTypeMismatch(this, type); + } + //noinspection unchecked + return (R) getStateValue(); + } + + @NonNull + public R getStateValueAs(@NonNull final Class 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) { throw new PropertyNotWritable(this); } - write.accept(this, type.cast(value)); + write.accept(this, value); } } diff --git a/src/main/java/de/ph87/home/property/PropertyDto.java b/src/main/java/de/ph87/home/property/PropertyDto.java index 798d959..30e1973 100644 --- a/src/main/java/de/ph87/home/property/PropertyDto.java +++ b/src/main/java/de/ph87/home/property/PropertyDto.java @@ -21,14 +21,14 @@ public class PropertyDto { @Nullable private final State state; - private final boolean valueDifferent; + private final boolean valueChanged; public PropertyDto(@NonNull final Property property) { this.id = property.getId(); this.type = property.getType(); this.state = property.getState(); this.lastState = property.getLastState(); - this.valueDifferent = property.isValueDifferent(); + this.valueChanged = property.isValueChanged(); } } diff --git a/src/main/java/de/ph87/home/property/PropertyService.java b/src/main/java/de/ph87/home/property/PropertyService.java index 72d92b8..167bca6 100644 --- a/src/main/java/de/ph87/home/property/PropertyService.java +++ b/src/main/java/de/ph87/home/property/PropertyService.java @@ -34,30 +34,22 @@ public class PropertyService { @Nullable public State read(@NonNull final String id, @NonNull final Class 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) property.getState(); - } - throw new PropertyTypeMismatch(property, type); + return byIdAndType(id, type).getState(); } - public void write(@NonNull final String id, @NonNull final Object value) throws PropertyNotFound, PropertyTypeMismatch, PropertyNotWritable { + public void write(@NonNull final String id, @NonNull final TYPE value, final Class type) 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); + byIdAndType(id, type).write(value); } @NonNull - private Property byIdAndType(final String id, final Class type) throws PropertyNotFound, PropertyTypeMismatch { + public Property byIdAndType(final String id, final Class type) throws PropertyNotFound, PropertyTypeMismatch { final Property property = findById(id).orElseThrow(() -> new PropertyNotFound(id)); if (type != property.getType()) { throw new PropertyTypeMismatch(property, type); } - return property; + //noinspection unchecked + return (Property) property; } @NonNull @@ -84,7 +76,7 @@ public class PropertyService { private void onStateSet(@NonNull final Property property) { final PropertyDto dto = toDto(property); log.debug("Property updated: {}", dto); - if (dto.isValueDifferent()) { + if (dto.isValueChanged()) { log.info("Property changed: {}", dto); } applicationEventPublisher.publishEvent(dto); diff --git a/src/main/java/de/ph87/home/property/PropertyTypeMismatch.java b/src/main/java/de/ph87/home/property/PropertyTypeMismatch.java index a5e04a2..93f5a23 100644 --- a/src/main/java/de/ph87/home/property/PropertyTypeMismatch.java +++ b/src/main/java/de/ph87/home/property/PropertyTypeMismatch.java @@ -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())); } - 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)); - } - } diff --git a/src/main/java/de/ph87/home/tvheadend/TvheadendConfig.java b/src/main/java/de/ph87/home/tvheadend/TvheadendConfig.java new file mode 100644 index 0000000..fae60aa --- /dev/null +++ b/src/main/java/de/ph87/home/tvheadend/TvheadendConfig.java @@ -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); + +} diff --git a/src/main/java/de/ph87/home/tvheadend/TvheadendService.java b/src/main/java/de/ph87/home/tvheadend/TvheadendService.java new file mode 100644 index 0000000..4d35b9e --- /dev/null +++ b/src/main/java/de/ph87/home/tvheadend/TvheadendService.java @@ -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 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 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 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()); + } + } + } + +} diff --git a/src/main/java/de/ph87/home/tvheadend/api/TvheadendApiService.java b/src/main/java/de/ph87/home/tvheadend/api/TvheadendApiService.java new file mode 100644 index 0000000..195c450 --- /dev/null +++ b/src/main/java/de/ph87/home/tvheadend/api/TvheadendApiService.java @@ -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; + } + +} \ No newline at end of file diff --git a/src/main/java/de/ph87/home/tvheadend/api/TvheadendHttpHelper.java b/src/main/java/de/ph87/home/tvheadend/api/TvheadendHttpHelper.java new file mode 100644 index 0000000..4ae4b1e --- /dev/null +++ b/src/main/java/de/ph87/home/tvheadend/api/TvheadendHttpHelper.java @@ -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 get(@NonNull final String path, @NonNull final Class 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 post(@NonNull final String path, @NonNull final Object request, @NonNull final Class 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 parse(@NonNull final Class 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; + } + +} diff --git a/src/main/java/de/ph87/home/tvheadend/api/TvheadendStatus.java b/src/main/java/de/ph87/home/tvheadend/api/TvheadendStatus.java new file mode 100644 index 0000000..28d0474 --- /dev/null +++ b/src/main/java/de/ph87/home/tvheadend/api/TvheadendStatus.java @@ -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 connectionList; + + @NonNull + public final List subscriptionList; + + @NonNull + public final List inputList; + + @NonNull + public final List 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 inputList, + @NonNull final List subscriptionList, + @NonNull final List connectionList, + @NonNull final List 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; + } + +} diff --git a/src/main/java/de/ph87/home/tvheadend/api/dto/DvrEntryGridUpcoming.java b/src/main/java/de/ph87/home/tvheadend/api/dto/DvrEntryGridUpcoming.java new file mode 100644 index 0000000..0c1b2f4 --- /dev/null +++ b/src/main/java/de/ph87/home/tvheadend/api/dto/DvrEntryGridUpcoming.java @@ -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 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 title; + + @JsonProperty + private String disp_title; + + @JsonProperty + private String disp_subtitle; + + @JsonProperty + private Map summary; + + @JsonProperty + private String disp_summary; + + @JsonProperty + @ToString.Exclude + private Map 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 category; + +// @JsonProperty +// private List credits; + + @JsonProperty + private List keyword; + + @JsonProperty + private List 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); + } + + } + +} diff --git a/src/main/java/de/ph87/home/tvheadend/api/dto/StatusConnections.java b/src/main/java/de/ph87/home/tvheadend/api/dto/StatusConnections.java new file mode 100644 index 0000000..52888c0 --- /dev/null +++ b/src/main/java/de/ph87/home/tvheadend/api/dto/StatusConnections.java @@ -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 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; + + } + +} diff --git a/src/main/java/de/ph87/home/tvheadend/api/dto/StatusInputs.java b/src/main/java/de/ph87/home/tvheadend/api/dto/StatusInputs.java new file mode 100644 index 0000000..95e6d2e --- /dev/null +++ b/src/main/java/de/ph87/home/tvheadend/api/dto/StatusInputs.java @@ -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 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 pids; + + @JsonProperty + private String stream; + + @JsonProperty + private int snr; + + @JsonProperty + private int signal; + + @JsonProperty + private int snr_scale; + + } + +} diff --git a/src/main/java/de/ph87/home/tvheadend/api/dto/StatusSubscriptions.java b/src/main/java/de/ph87/home/tvheadend/api/dto/StatusSubscriptions.java new file mode 100644 index 0000000..bcd62eb --- /dev/null +++ b/src/main/java/de/ph87/home/tvheadend/api/dto/StatusSubscriptions.java @@ -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 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 pids; + + @JsonProperty + private String profile; + + @JsonProperty + private long in; + + @JsonProperty + private long out; + + @JsonProperty + private long total_in; + + @JsonProperty + private long total_out; + + } + +}