2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.xmltv.internal.handler;
15 import static org.openhab.binding.xmltv.internal.XmlTVBindingConstants.*;
17 import java.time.Duration;
18 import java.time.Instant;
19 import java.time.ZoneId;
20 import java.time.ZonedDateTime;
21 import java.util.ArrayList;
22 import java.util.List;
23 import java.util.Optional;
24 import java.util.concurrent.ScheduledFuture;
25 import java.util.concurrent.TimeUnit;
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.openhab.binding.xmltv.internal.configuration.XmlChannelConfiguration;
30 import org.openhab.binding.xmltv.internal.jaxb.Icon;
31 import org.openhab.binding.xmltv.internal.jaxb.MediaChannel;
32 import org.openhab.binding.xmltv.internal.jaxb.Programme;
33 import org.openhab.binding.xmltv.internal.jaxb.WithLangType;
34 import org.openhab.core.io.net.http.HttpUtil;
35 import org.openhab.core.library.types.DateTimeType;
36 import org.openhab.core.library.types.QuantityType;
37 import org.openhab.core.library.types.RawType;
38 import org.openhab.core.library.types.StringType;
39 import org.openhab.core.library.unit.Units;
40 import org.openhab.core.thing.Bridge;
41 import org.openhab.core.thing.ChannelUID;
42 import org.openhab.core.thing.Thing;
43 import org.openhab.core.thing.ThingStatus;
44 import org.openhab.core.thing.ThingStatusDetail;
45 import org.openhab.core.thing.binding.BaseThingHandler;
46 import org.openhab.core.types.Command;
47 import org.openhab.core.types.RefreshType;
48 import org.openhab.core.types.State;
49 import org.openhab.core.types.UnDefType;
50 import org.slf4j.Logger;
51 import org.slf4j.LoggerFactory;
54 * The {@link ChannelHandler} is responsible for handling information
55 * made available in regard of the channel and current program
57 * @author Gaƫl L'hopital - Initial contribution
60 public class ChannelHandler extends BaseThingHandler {
61 private final Logger logger = LoggerFactory.getLogger(ChannelHandler.class);
63 private @Nullable ScheduledFuture<?> globalJob;
64 private @Nullable MediaChannel mediaChannel;
65 private State mediaIcon = UnDefType.UNDEF;
67 public final List<Programme> programmes = new ArrayList<>();
68 private final ZoneId zoneId;
70 public ChannelHandler(Thing thing, ZoneId zoneId) {
76 public void initialize() {
77 XmlChannelConfiguration config = getConfigAs(XmlChannelConfiguration.class);
79 logger.debug("Initializing Broadcast Channel handler for uid '{}'", getThing().getUID());
81 ScheduledFuture<?> job = globalJob;
82 if (job == null || job.isCancelled()) {
83 globalJob = scheduler.scheduleWithFixedDelay(() -> {
84 if (programmes.size() < 2) {
87 if (programmes.isEmpty()) {
88 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/no-more-programs");
89 } else if (Instant.now().isAfter(programmes.get(0).getProgrammeStop())) {
93 getThing().getChannels().forEach(channel -> updateChannel(channel.getUID()));
95 }, 3, config.refresh, TimeUnit.SECONDS);
99 private void refreshProgramList() {
100 Bridge bridge = getBridge();
101 if (bridge != null && bridge.getStatus() == ThingStatus.ONLINE) {
102 XmlTVHandler handler = (XmlTVHandler) bridge.getHandler();
103 if (handler != null) {
104 handler.getXmlFile().ifPresentOrElse(tv -> {
105 String channelId = (String) getConfig().get(XmlChannelConfiguration.CHANNEL_ID);
107 if (mediaChannel == null) {
108 Optional<MediaChannel> channel = tv.getMediaChannels().stream()
109 .filter(mediaChannel -> mediaChannel.getId().equals(channelId)).findFirst();
110 if (channel.isPresent()) {
111 mediaChannel = channel.get();
112 mediaIcon = downloadIcon(mediaChannel.getIcons());
117 tv.getProgrammes().stream().filter(
118 p -> p.getChannel().equals(channelId) && p.getProgrammeStop().isAfter(Instant.now()))
119 .forEach(p -> programmes.add(p));
121 updateStatus(ThingStatus.ONLINE);
122 }, () -> updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/no-file-available"));
124 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
127 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
132 public void dispose() {
133 ScheduledFuture<?> job = globalJob;
134 if (job != null && !job.isCancelled()) {
141 public void handleCommand(ChannelUID channelUID, Command command) {
142 logger.debug("handleCommand {} for {}", command, channelUID);
143 if (command == RefreshType.REFRESH) {
144 refreshProgramList();
149 * Update the channel from the last OpenUV data retrieved
151 * @param channelUID the id identifying the channel to be updated
154 private void updateChannel(ChannelUID channelUID) {
155 // TODO : usage extraction of groupname
156 String[] uidElements = channelUID.getId().split("#");
157 if (uidElements.length == 2) {
158 int target = GROUP_NEXT_PROGRAMME.equals(uidElements[0]) ? 1 : 0;
159 if (programmes.size() > target) {
160 Programme programme = programmes.get(target);
162 switch (uidElements[1]) {
164 State icon = GROUP_CHANNEL_PROPERTIES.equals(uidElements[0]) ? mediaIcon
165 : downloadIcon(programme.getIcons());
166 updateState(channelUID, icon);
168 case CHANNEL_CHANNEL_URL:
169 MediaChannel channel = mediaChannel;
170 updateState(channelUID,
171 channel != null ? !channel.getIcons().isEmpty()
172 ? new StringType(channel.getIcons().get(0).getSrc())
173 : UnDefType.UNDEF : UnDefType.UNDEF);
175 case CHANNEL_PROGRAMME_START:
176 updateDateTimeChannel(channelUID, programme.getProgrammeStart());
178 case CHANNEL_PROGRAMME_END:
179 updateDateTimeChannel(channelUID, programme.getProgrammeStop());
181 case CHANNEL_PROGRAMME_TITLE:
182 List<WithLangType> titles = programme.getTitles();
183 updateState(channelUID,
184 !titles.isEmpty() ? new StringType(programme.getTitles().get(0).getValue())
187 case CHANNEL_PROGRAMME_CATEGORY:
188 List<WithLangType> categories = programme.getCategories();
189 updateState(channelUID,
190 !categories.isEmpty() ? new StringType(programme.getCategories().get(0).getValue())
193 case CHANNEL_PROGRAMME_ICON:
194 List<Icon> icons = programme.getIcons();
195 updateState(channelUID,
196 !icons.isEmpty() ? new StringType(icons.get(0).getSrc()) : UnDefType.UNDEF);
198 case CHANNEL_PROGRAMME_ELAPSED:
199 updateState(channelUID, getDurationInSeconds(programme.getProgrammeStart(), Instant.now()));
201 case CHANNEL_PROGRAMME_REMAINING:
202 updateState(channelUID, getDurationInSeconds(Instant.now(), programme.getProgrammeStop()));
204 case CHANNEL_PROGRAMME_TIMELEFT:
205 updateState(channelUID, getDurationInSeconds(Instant.now(), programme.getProgrammeStart()));
207 case CHANNEL_PROGRAMME_PROGRESS:
208 long totalLength = Duration.between(programme.getProgrammeStart(), programme.getProgrammeStop())
210 long elapsed1 = Duration.between(programme.getProgrammeStart(), Instant.now()).toSeconds();
212 double progress = 100.0 * elapsed1 / totalLength;
213 if (progress > 100 || progress < 0) {
214 logger.debug("Outstanding process");
216 updateState(channelUID, new QuantityType<>((int) progress, Units.PERCENT));
221 logger.warn("Not enough programs in XML file, think to refresh it");
226 private void updateDateTimeChannel(ChannelUID channelUID, Instant instant) {
227 ZonedDateTime zds = ZonedDateTime.ofInstant(instant, zoneId);
228 updateState(channelUID, new DateTimeType(zds));
231 private QuantityType<?> getDurationInSeconds(Instant from, Instant to) {
232 long elapsed = Duration.between(from, to).toSeconds();
233 return new QuantityType<>(elapsed, Units.SECOND);
236 private State downloadIcon(List<Icon> icons) {
237 if (!icons.isEmpty()) {
238 String url = icons.get(0).getSrc();
239 RawType result = HttpUtil.downloadImage(url);
240 if (result != null) {
244 return UnDefType.NULL;