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 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 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 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;
+ }
+
+}
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