added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.binding.xmltv</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

View File

@@ -0,0 +1,13 @@
This content is produced and maintained by the openHAB project.
* Project home: https://www.openhab.org
== Declared Project Licenses
This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab-addons

View File

@@ -0,0 +1,88 @@
# XmlTV Binding
XMLTV is an XML based file format for describing TV listings.
This format is often used by Home Theater software to produce their Electronic Program Guide (http://wiki.xmltv.org/index.php/Main_Page)
The norm allows to separate program display from its building.
The building of the XMLTV file itself is taken in charge by so called "grabbers" (http://wiki.xmltv.org/index.php/HowtoUseGrabbers).
Some websites provides updated XMLTV files than can be directly downloaded.
Here is a sample for France : https://www.xmltv.fr/
This binding takes an XMLTV file as input and creates a thing for each channel contained in it.
XmlTV channels are called Media Channels in this binding in order to avoid messing with openHAB Channels.
For each thing, you will be able to get information regarding the current program and the next to come.
## Supported Things
## Discovery
Once the XmlTV bridge to a file is created, you can add all known channels by searching new things.
## Binding Configuration
| Configuration Parameter | Required | Description | Default |
|-------------------------|----------|-----------------------------------------------------|---------|
| filePath | X | Full path (including filename) to an Xml TV file | |
| refresh | X | XMLTV file reload interval in hours | 24h |
| encoding | X | XMLTV file encoding | UTF8 |
## Thing Configuration
| Configuration Parameter | Required | Description | Default |
|-------------------------|----------|----------------------------------------------------------------|---------|
| channelId | X | Id of the channel as presented in the XmlTV file | |
| offset | X | Offset applied to program times (forward or backward (minutes) | 0 |
| refresh | X | Refresh interval in seconds | 60 |
## Channels
| Channel Type ID | Item Type | Description |
|-----------------|----------------------|-------------------------------------|
| iconUrl | String | Channel Icon URL |
| icon | Image | Icon of the channel |
### Current program (currentprog) Channels Group
| Channel Type ID | Item Type | Description |
|-----------------|----------------------|---------------------------------------------|
| progStart | DateTime | Program Start Time |
| progEnd | DateTime | Program End Time |
| progTitle | String | Program Title |
| progCategory | String | Program Category |
| progIconUrl | String | URL to an image of the program |
| icon | Image | Icon of the program |
| elapsedTime | Number:Time | Current time of currently playing program |
| remainingTime | Number:Time | Time remaining until end of the program |
| progress | Number:Dimensionless | Relative progression of the current program |
### Next program (nextprog) Channels Group
| Channel Type ID | Item Type | Description |
|-----------------|----------------------|---------------------------------------------|
| progStart | DateTime | Program Start Time |
| timeLeft | Number:Time | Time left before program start |
| progEnd | DateTime | Program End Time |
| progTitle | String | Program Title |
| progCategory | String | Program Category |
| progIconUrl | String | URL to an image of the program |
| icon | Image | Icon of the program |
## Full Example
### xmltv.things
```
Bridge xmltv:xmltvfile:france "XmlTV" @ "TV" [filePath="/etc/openhab2/scripts/tvguide.xml"]
{
Thing channel france2 "France 2" @ "TV" [channelId="C4.api.telerama.fr", offset=0, refresh=60]
}
```
### xmltv.items
```
String france2_title "Titre" {channel="xmltv:channel:france:france2:currentprog#progTitle"}
```

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>3.0.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.xmltv</artifactId>
<name>openHAB Add-ons :: Bundles :: XMLTV Binding</name>
</project>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.xmltv-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-xmltv" description="XMLTV Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<feature dependency="true">openhab.tp-jaxb</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.xmltv/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,61 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.xmltv.internal;
import java.util.Collections;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link XmlTVBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class XmlTVBindingConstants {
private static final String BINDING_ID = "xmltv";
// List of all Bridge Type UIDs
public static final ThingTypeUID XMLTV_FILE_BRIDGE_TYPE = new ThingTypeUID(BINDING_ID, "xmltvfile");
public static final ThingTypeUID XMLTV_CHANNEL_THING_TYPE = new ThingTypeUID(BINDING_ID, "channel");
// Channel groups
public static final String GROUP_CURRENT_PROGRAMME = "currentprog";
public static final String GROUP_NEXT_PROGRAMME = "nextprog";
public static final String GROUP_CHANNEL_PROPERTIES = "channelprops";
// List of all Channel ids
public static final String CHANNEL_CHANNEL_URL = "iconUrl";
public static final String CHANNEL_ICON = "icon";
public static final String CHANNEL_PROGRAMME_START = "progStart";
public static final String CHANNEL_PROGRAMME_END = "progEnd";
public static final String CHANNEL_PROGRAMME_TITLE = "progTitle";
public static final String CHANNEL_PROGRAMME_CATEGORY = "progCategory";
public static final String CHANNEL_PROGRAMME_ICON = "progIconUrl";
public static final String CHANNEL_PROGRAMME_ELAPSED = "elapsedTime";
public static final String CHANNEL_PROGRAMME_REMAINING = "remainingTime";
public static final String CHANNEL_PROGRAMME_PROGRESS = "progress";
public static final String CHANNEL_PROGRAMME_TIMELEFT = "timeLeft";
// Supported Thing types
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections
.unmodifiableSet(Stream.of(XMLTV_FILE_BRIDGE_TYPE, XMLTV_CHANNEL_THING_TYPE).collect(Collectors.toSet()));
}

View File

@@ -0,0 +1,115 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.xmltv.internal;
import static org.openhab.binding.xmltv.internal.XmlTVBindingConstants.*;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import javax.xml.bind.JAXBException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.xmltv.internal.discovery.XmlTVDiscoveryService;
import org.openhab.binding.xmltv.internal.handler.ChannelHandler;
import org.openhab.binding.xmltv.internal.handler.XmlTVHandler;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link XmlTVHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
@Component(configurationPid = "binding.xmltv", service = ThingHandlerFactory.class)
public class XmlTVHandlerFactory extends BaseThingHandlerFactory {
private final Logger logger = LoggerFactory.getLogger(XmlTVHandlerFactory.class);
private final Map<ThingUID, ServiceRegistration<?>> discoveryServiceRegs = new HashMap<>();
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
public @Nullable Thing createThing(ThingTypeUID thingTypeUID, Configuration configuration,
@Nullable ThingUID thingUID, @Nullable ThingUID bridgeUID) {
if (XMLTV_FILE_BRIDGE_TYPE.equals(thingTypeUID)) {
return super.createThing(thingTypeUID, configuration, thingUID, null);
} else if (XMLTV_CHANNEL_THING_TYPE.equals(thingTypeUID)) {
ThingUID newThingUID;
if (bridgeUID != null && thingUID != null) {
newThingUID = new ThingUID(thingTypeUID, bridgeUID, thingUID.getId());
} else {
newThingUID = thingUID;
}
return super.createThing(thingTypeUID, configuration, newThingUID, bridgeUID);
}
throw new IllegalArgumentException(
"The thing type " + thingTypeUID + " is not supported by the XmlTV binding.");
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (XMLTV_FILE_BRIDGE_TYPE.equals(thingTypeUID)) {
try {
XmlTVHandler bridgeHandler = new XmlTVHandler((Bridge) thing);
registerDeviceDiscoveryService(bridgeHandler);
return bridgeHandler;
} catch (JAXBException e) {
logger.error("Unable to create XmlTVHandler : {}", e.getMessage());
}
} else if (XMLTV_CHANNEL_THING_TYPE.equals(thingTypeUID)) {
return new ChannelHandler(thing);
}
return null;
}
@Override
protected void removeHandler(ThingHandler thingHandler) {
if (thingHandler instanceof XmlTVHandler) {
Thing thing = thingHandler.getThing();
unregisterDeviceDiscoveryService(thing);
}
super.removeHandler(thingHandler);
}
private synchronized void registerDeviceDiscoveryService(XmlTVHandler bridgeHandler) {
XmlTVDiscoveryService discoveryService = new XmlTVDiscoveryService(bridgeHandler);
discoveryServiceRegs.put(bridgeHandler.getThing().getUID(),
bundleContext.registerService(DiscoveryService.class.getName(), discoveryService, new Hashtable<>()));
}
private synchronized void unregisterDeviceDiscoveryService(Thing thing) {
ServiceRegistration<?> serviceReg = discoveryServiceRegs.remove(thing.getUID());
serviceReg.unregister();
}
}

View File

@@ -0,0 +1,27 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.xmltv.internal.configuration;
/**
* The {@link XmlChannelConfiguration} class contains fields mapping
* Channel thing configuration parameters.
*
* @author Gaël L'hopital - Initial contribution
*/
public class XmlChannelConfiguration {
public static final String CHANNEL_ID = "channelId";
public String channelId;
public Integer offset;
public Integer refresh;
}

View File

@@ -0,0 +1,25 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.xmltv.internal.configuration;
/**
* The {@link XmlTVConfiguration} class contains fields mapping TV bridge
* configuration parameters.
*
* @author Gaël L'hopital - Initial contribution
*/
public class XmlTVConfiguration {
public String filePath;
public Integer refresh;
public String encoding;
}

View File

@@ -0,0 +1,73 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.xmltv.internal.discovery;
import static org.openhab.binding.xmltv.internal.XmlTVBindingConstants.XMLTV_CHANNEL_THING_TYPE;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.xmltv.internal.XmlTVBindingConstants;
import org.openhab.binding.xmltv.internal.configuration.XmlChannelConfiguration;
import org.openhab.binding.xmltv.internal.handler.XmlTVHandler;
import org.openhab.binding.xmltv.internal.jaxb.Tv;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link XmlTVDiscoveryService} is responsible for discovering all channels
* declared in the XmlTV file
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class XmlTVDiscoveryService extends AbstractDiscoveryService {
private final Logger logger = LoggerFactory.getLogger(XmlTVDiscoveryService.class);
private static final int SEARCH_TIME = 10;
private XmlTVHandler bridgeHandler;
/**
* Creates a XmlTVDiscoveryService with background discovery disabled.
*/
public XmlTVDiscoveryService(XmlTVHandler bridgeHandler) {
super(XmlTVBindingConstants.SUPPORTED_THING_TYPES_UIDS, SEARCH_TIME);
this.bridgeHandler = bridgeHandler;
}
@Override
protected void startScan() {
logger.debug("Starting XmlTV discovery scan");
if (bridgeHandler.getThing().getStatus() == ThingStatus.ONLINE) {
Tv tv = bridgeHandler.getXmlFile();
if (tv != null) {
tv.getMediaChannels().stream().forEach(channel -> {
String channelId = channel.getId();
String uid = channelId.replaceAll("[^A-Za-z0-9_]", "_");
ThingUID thingUID = new ThingUID(XMLTV_CHANNEL_THING_TYPE, bridgeHandler.getThing().getUID(), uid);
DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID)
.withBridge(bridgeHandler.getThing().getUID())
.withLabel(channel.getDisplayNames().get(0).getValue()).withRepresentationProperty(uid)
.withProperty(XmlChannelConfiguration.CHANNEL_ID, channelId).build();
thingDiscovered(discoveryResult);
});
}
}
}
}

View File

@@ -0,0 +1,249 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.xmltv.internal.handler;
import static org.openhab.binding.xmltv.internal.XmlTVBindingConstants.*;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.xmltv.internal.configuration.XmlChannelConfiguration;
import org.openhab.binding.xmltv.internal.jaxb.Icon;
import org.openhab.binding.xmltv.internal.jaxb.MediaChannel;
import org.openhab.binding.xmltv.internal.jaxb.Programme;
import org.openhab.binding.xmltv.internal.jaxb.Tv;
import org.openhab.binding.xmltv.internal.jaxb.WithLangType;
import org.openhab.core.io.net.http.HttpUtil;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.RawType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.unit.SmartHomeUnits;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link ChannelHandler} is responsible for handling information
* made available in regard of the channel and current program
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class ChannelHandler extends BaseThingHandler {
private final Logger logger = LoggerFactory.getLogger(ChannelHandler.class);
private @NonNullByDefault({}) ScheduledFuture<?> globalJob;
private @Nullable MediaChannel mediaChannel;
private @Nullable RawType mediaIcon = new RawType(new byte[0], RawType.DEFAULT_MIME_TYPE);
public final List<Programme> programmes = new ArrayList<>();
public ChannelHandler(Thing thing) {
super(thing);
}
@Override
public void initialize() {
XmlChannelConfiguration config = getConfigAs(XmlChannelConfiguration.class);
logger.debug("Initializing Broadcast Channel handler for uid '{}'", getThing().getUID());
if (globalJob == null || globalJob.isCancelled()) {
globalJob = scheduler.scheduleWithFixedDelay(() -> {
if (programmes.size() < 2) {
refreshProgramList();
}
if (programmes.isEmpty()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
"No programmes to come in the current XML file for this channel");
} else if (Instant.now().isAfter(programmes.get(0).getProgrammeStop())) {
programmes.remove(0);
}
getThing().getChannels().forEach(channel -> updateChannel(channel.getUID()));
}, 3, config.refresh, TimeUnit.SECONDS);
}
}
private void refreshProgramList() {
Bridge bridge = getBridge();
if (bridge != null && bridge.getStatus() == ThingStatus.ONLINE) {
XmlTVHandler handler = (XmlTVHandler) bridge.getHandler();
if (handler != null) {
Tv tv = handler.getXmlFile();
if (tv != null) {
String channelId = (String) getConfig().get(XmlChannelConfiguration.CHANNEL_ID);
if (mediaChannel == null) {
Optional<MediaChannel> channel = tv.getMediaChannels().stream()
.filter(mediaChannel -> mediaChannel.getId().equals(channelId)).findFirst();
if (channel.isPresent()) {
mediaChannel = channel.get();
mediaIcon = downloadIcon(mediaChannel.getIcons());
}
}
programmes.clear();
tv.getProgrammes().stream().filter(
p -> p.getChannel().equals(channelId) && p.getProgrammeStop().isAfter(Instant.now()))
.forEach(p -> programmes.add(p));
updateStatus(ThingStatus.ONLINE);
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "No file available");
}
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
}
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
}
}
@Override
public void dispose() {
if (globalJob != null && !globalJob.isCancelled()) {
globalJob.cancel(true);
globalJob = null;
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
logger.debug("handleCommand {} for {}", command, channelUID);
if (command == RefreshType.REFRESH) {
refreshProgramList();
}
}
/**
* Update the channel from the last OpenUV data retrieved
*
* @param channelUID the id identifying the channel to be updated
*
*/
private void updateChannel(ChannelUID channelUID) {
String[] uidElements = channelUID.getId().split("#");
if (uidElements.length == 2) {
int target = GROUP_NEXT_PROGRAMME.equals(uidElements[0]) ? 1 : 0;
if (programmes.size() > target) {
Programme programme = programmes.get(target);
switch (uidElements[1]) {
case CHANNEL_ICON:
State icon = null;
if (GROUP_CHANNEL_PROPERTIES.equals(uidElements[0])) {
icon = mediaIcon;
} else {
icon = downloadIcon(programme.getIcons());
}
updateState(channelUID, icon != null ? icon : UnDefType.UNDEF);
break;
case CHANNEL_CHANNEL_URL:
updateState(channelUID,
mediaChannel != null ? !mediaChannel.getIcons().isEmpty()
? new StringType(mediaChannel.getIcons().get(0).getSrc())
: UnDefType.UNDEF : UnDefType.UNDEF);
break;
case CHANNEL_PROGRAMME_START:
Instant is = programme.getProgrammeStart();
ZonedDateTime zds = ZonedDateTime.ofInstant(is, ZoneId.systemDefault());
updateState(channelUID, new DateTimeType(zds));
break;
case CHANNEL_PROGRAMME_END:
ZonedDateTime zde = ZonedDateTime.ofInstant(programme.getProgrammeStop(),
ZoneId.systemDefault());
updateState(channelUID, new DateTimeType(zde));
break;
case CHANNEL_PROGRAMME_TITLE:
List<WithLangType> titles = programme.getTitles();
updateState(channelUID,
!titles.isEmpty() ? new StringType(programme.getTitles().get(0).getValue())
: UnDefType.UNDEF);
break;
case CHANNEL_PROGRAMME_CATEGORY:
List<WithLangType> categories = programme.getCategories();
updateState(channelUID,
!categories.isEmpty() ? new StringType(programme.getCategories().get(0).getValue())
: UnDefType.UNDEF);
break;
case CHANNEL_PROGRAMME_ICON:
List<Icon> icons = programme.getIcons();
updateState(channelUID,
!icons.isEmpty() ? new StringType(icons.get(0).getSrc()) : UnDefType.UNDEF);
break;
case CHANNEL_PROGRAMME_ELAPSED:
updateState(channelUID, getDurationInSeconds(programme.getProgrammeStart(), Instant.now()));
break;
case CHANNEL_PROGRAMME_REMAINING:
updateState(channelUID, getDurationInSeconds(Instant.now(), programme.getProgrammeStop()));
break;
case CHANNEL_PROGRAMME_TIMELEFT:
updateState(channelUID, getDurationInSeconds(Instant.now(), programme.getProgrammeStart()));
break;
case CHANNEL_PROGRAMME_PROGRESS:
Duration totalLength = Duration.between(programme.getProgrammeStart(),
programme.getProgrammeStop());
Duration elapsed1 = Duration.between(programme.getProgrammeStart(), Instant.now());
long secondsElapsed1 = elapsed1.toMillis() / 1000;
long secondsLength = totalLength.toMillis() / 1000;
double progress = 100.0 * secondsElapsed1 / secondsLength;
if (progress > 100 || progress < 0) {
logger.debug("Outstanding process");
}
updateState(channelUID, new QuantityType<>(progress, SmartHomeUnits.PERCENT));
break;
}
} else {
logger.warn("Not enough programs in XML file, think to refresh it");
}
}
}
private QuantityType<?> getDurationInSeconds(Instant from, Instant to) {
Duration elapsed = Duration.between(from, to);
long secondsElapsed = TimeUnit.MILLISECONDS.toSeconds(elapsed.toMillis());
return new QuantityType<>(secondsElapsed, SmartHomeUnits.SECOND);
}
private @Nullable RawType downloadIcon(List<Icon> icons) {
if (!icons.isEmpty()) {
String url = icons.get(0).getSrc();
return HttpUtil.downloadImage(url);
}
return null;
}
}

View File

@@ -0,0 +1,129 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.xmltv.internal.handler;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.time.Instant;
import java.util.Collections;
import java.util.Comparator;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.xmltv.internal.configuration.XmlTVConfiguration;
import org.openhab.binding.xmltv.internal.jaxb.Programme;
import org.openhab.binding.xmltv.internal.jaxb.Tv;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link XmlTVHandler} is responsible for handling XMLTV file and dispatch
* information made available to according Media Channels
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class XmlTVHandler extends BaseBridgeHandler {
private final Logger logger = LoggerFactory.getLogger(XmlTVHandler.class);
private final XMLInputFactory xif = XMLInputFactory.newFactory();
private final JAXBContext jc;
private @Nullable Tv currentXmlFile;
private @NonNullByDefault({}) ScheduledFuture<?> reloadJob;
public XmlTVHandler(Bridge thing) throws JAXBException {
super(thing);
xif.setProperty(XMLInputFactory.SUPPORT_DTD, false);
jc = JAXBContext.newInstance(Tv.class);
}
@Override
public void initialize() {
XmlTVConfiguration config = getConfigAs(XmlTVConfiguration.class);
logger.debug("Initializing {} for input file '{}'", getClass(), config.filePath);
reloadJob = scheduler.scheduleWithFixedDelay(() -> {
currentXmlFile = null;
XMLStreamReader xsr = null;
try {
// This can take some seconds depending upon weight of the XmlTV source file
xsr = xif.createXMLStreamReader(new FileInputStream(new File(config.filePath)), config.encoding);
try {
Unmarshaller unmarshaller = jc.createUnmarshaller();
Tv xmlFile = (Tv) unmarshaller.unmarshal(xsr);
// Remove all finished programmes
xmlFile.getProgrammes().removeIf(programme -> Instant.now().isAfter(programme.getProgrammeStop()));
if (!xmlFile.getProgrammes().isEmpty()) {
// Sort programmes by starting instant
Collections.sort(xmlFile.getProgrammes(), Comparator.comparing(Programme::getProgrammeStart));
// Ready to deliver data to ChannelHandlers
currentXmlFile = xmlFile;
updateStatus(ThingStatus.ONLINE);
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.DISABLED, "XMLTV file seems outdated");
}
xsr.close();
} catch (JAXBException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR, e.getMessage());
}
} catch (XMLStreamException | FileNotFoundException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
} finally {
try {
if (xsr != null) {
xsr.close();
}
} catch (XMLStreamException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
}
}
}, 0, config.refresh, TimeUnit.HOURS);
}
@Override
public void dispose() {
logger.debug("Running dispose");
if (reloadJob != null && !reloadJob.isCancelled()) {
reloadJob.cancel(true);
reloadJob = null;
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
// nothing to do
}
@Nullable
public Tv getXmlFile() {
return currentXmlFile;
}
}

View File

@@ -0,0 +1,56 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.xmltv.internal.jaxb;
import java.math.BigInteger;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlSchemaType;
import javax.xml.bind.annotation.XmlType;
/**
* Java class for an icon XML element
*
* @author Gaël L'hopital - Initial contribution
*/
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType
@XmlRootElement(name = "icon")
public class Icon {
@XmlAttribute(required = true)
protected String src;
@XmlAttribute
@XmlSchemaType(name = "positiveInteger")
protected BigInteger width;
@XmlAttribute
@XmlSchemaType(name = "positiveInteger")
protected BigInteger height;
public String getSrc() {
return src;
}
public BigInteger getWidth() {
return width;
}
public BigInteger getHeight() {
return height;
}
}

View File

@@ -0,0 +1,62 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.xmltv.internal.jaxb;
import java.util.ArrayList;
import java.util.List;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlSchemaType;
import javax.xml.bind.annotation.XmlType;
import javax.xml.bind.annotation.adapters.CollapsedStringAdapter;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Java class for an channel XML element
* Renamed to MediaChannel in order to avoid confusion with ESH Channels
*
* @author Gaël L'hopital - Initial contribution
*/
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType
@NonNullByDefault
public class MediaChannel {
@XmlElement(name = "display-name", required = true)
protected List<WithLangType> displayNames = new ArrayList<>();
@XmlElement(name = "icon")
protected List<Icon> icons = new ArrayList<>();
@XmlAttribute(required = true)
@XmlJavaTypeAdapter(CollapsedStringAdapter.class)
@XmlSchemaType(name = "token")
protected String id = "";
public List<WithLangType> getDisplayNames() {
return this.displayNames;
}
public List<Icon> getIcons() {
return this.icons;
}
public String getId() {
return id;
}
}

View File

@@ -0,0 +1,69 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.xmltv.internal.jaxb;
import javax.xml.bind.annotation.XmlRegistry;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* This object contains factory methods for each Java content
* interface and Java element interface generated in the
* org.openhab.binding.xmltv.internal.jaxb package.
*
* @author Gaël L'hopital - Initial contribution
*/
@XmlRegistry
@NonNullByDefault
public class ObjectFactory {
/**
* Create an instance of {@link Tv }
*
*/
public Tv createTv() {
return new Tv();
}
/**
* Create an instance of {@link Programme }
*
*/
public Programme createProgramme() {
return new Programme();
}
/**
* Create an instance of {@link MediaChannel }
*
*/
public MediaChannel createChannel() {
return new MediaChannel();
}
/**
* Create an instance of {@link Icon }
*
*/
public Icon createIcon() {
return new Icon();
}
/**
* Create an instance of {@link WithLangType }
*
*/
public WithLangType createWithLangType() {
return new WithLangType();
}
}

View File

@@ -0,0 +1,91 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.xmltv.internal.jaxb;
import java.time.Instant;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlType;
import javax.xml.bind.annotation.adapters.CollapsedStringAdapter;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Java class for a programme XML element
*
* @author Gaël L'hopital - Initial contribution
*/
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType
@NonNullByDefault
public class Programme {
private static final DateTimeFormatter XMLTV_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyyMMddHHmmss Z");
@XmlElement(name = "title", required = true)
protected List<WithLangType> titles = new ArrayList<>();
@XmlElement(name = "category")
protected List<WithLangType> categories = new ArrayList<>();
@XmlElement(name = "icon")
protected List<Icon> icons = new ArrayList<>();
@XmlAttribute(required = true)
private String start = "";
@XmlAttribute
private String stop = "";
@XmlAttribute(required = true)
@XmlJavaTypeAdapter(CollapsedStringAdapter.class)
protected String channel = "";
public List<WithLangType> getTitles() {
return titles;
}
public List<WithLangType> getCategories() {
return categories;
}
public Instant getProgrammeStart() {
long epoch = iso860DateToEpoch(start);
return Instant.ofEpochMilli(epoch);
}
public Instant getProgrammeStop() {
long epoch = iso860DateToEpoch(stop);
return Instant.ofEpochMilli(epoch);
}
private long iso860DateToEpoch(String date) {
long epoch = ZonedDateTime.parse(date, XMLTV_DATE_FORMAT).toInstant().toEpochMilli();
return epoch;
}
public List<Icon> getIcons() {
return icons;
}
public String getChannel() {
return channel;
}
}

View File

@@ -0,0 +1,49 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.xmltv.internal.jaxb;
import java.util.ArrayList;
import java.util.List;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Java class for a TV XML root element
*
* @author Gaël L'hopital - Initial contribution
*/
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType
@XmlRootElement(name = "tv")
@NonNullByDefault
public class Tv {
@XmlElement(name = "channel")
protected List<MediaChannel> channels = new ArrayList<>();
@XmlElement(name = "programme")
protected List<Programme> programmes = new ArrayList<>();
public List<MediaChannel> getMediaChannels() {
return this.channels;
}
public List<Programme> getProgrammes() {
return this.programmes;
}
}

View File

@@ -0,0 +1,48 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.xmltv.internal.jaxb;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlType;
import javax.xml.bind.annotation.XmlValue;
import javax.xml.bind.annotation.adapters.CollapsedStringAdapter;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
/**
* Java class for an XML element value including a language attribute
* information made available to according Media Channels
*
* @author Gaël L'hopital - Initial contribution
*/
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "withLangType")
public class WithLangType {
@XmlValue
@XmlJavaTypeAdapter(CollapsedStringAdapter.class)
protected String value;
@XmlAttribute
@XmlJavaTypeAdapter(CollapsedStringAdapter.class)
protected String lang;
public String getValue() {
return value;
}
public String getLang() {
return lang;
}
}

View File

@@ -0,0 +1,14 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
@javax.xml.bind.annotation.XmlSchema(namespace = "", elementFormDefault = javax.xml.bind.annotation.XmlNsForm.QUALIFIED)
package org.openhab.binding.xmltv.internal.jaxb;

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="xmltv" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>XmlTV Binding</name>
<description>This is the binding for reading and parsing XmlTV files</description>
<author>Gaël L'hopital</author>
</binding:binding>

View File

@@ -0,0 +1,180 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="xmltv"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<bridge-type id="xmltvfile">
<label>XmlTVFile</label>
<description>This is the interface to a XmlTV file</description>
<config-description>
<parameter name="filePath" type="text" required="true">
<label>XmlTV File Path</label>
<description>Path to an XmlTV file.</description>
</parameter>
<parameter name="refresh" type="integer" unit="h">
<label>Refresh Interval</label>
<description>Specifies the XMLTV file reload interval in hours</description>
<default>24</default>
</parameter>
<parameter name="encoding" type="text" required="true">
<label>File encoding</label>
<description>Specifies the XMLTV file encoding</description>
<default>UTF8</default>
</parameter>
</config-description>
</bridge-type>
<thing-type id="channel">
<supported-bridge-type-refs>
<bridge-type-ref id="xmltvfile"/>
</supported-bridge-type-refs>
<label>Channel</label>
<description>This represent a channel on a given TV file</description>
<channel-groups>
<channel-group id="channelprops" typeId="channelprops"/>
<channel-group id="currentprog" typeId="currentprog"/>
<channel-group id="nextprog" typeId="nextprog"/>
</channel-groups>
<config-description>
<parameter name="channelId" type="text" required="true">
<label>Channel Id</label>
<description>Id of the channel as presented in the XmlTV file.</description>
</parameter>
<parameter name="offset" type="integer" min="-1440" max="1440" unit="min">
<label>Offset</label>
<description>Moves an event or datetime value forward or backward (in minutes)</description>
<default>0</default>
</parameter>
<parameter name="refresh" type="integer" unit="s">
<label>Refresh Interval</label>
<description>Specifies the refresh interval in seconds</description>
<default>60</default>
</parameter>
</config-description>
</thing-type>
<channel-group-type id="channelprops">
<label>Channel Properties</label>
<description>Properties of the current channel</description>
<channels>
<channel id="iconUrl" typeId="iconUrl"/>
<channel id="icon" typeId="icon"/>
</channels>
</channel-group-type>
<channel-group-type id="currentprog">
<label>Current Program</label>
<description>Program currently on air on this channel</description>
<channels>
<channel id="progStart" typeId="progStart"/>
<channel id="progEnd" typeId="progEnd"/>
<channel id="progTitle" typeId="progTitle"/>
<channel id="progCategory" typeId="progCategory"/>
<channel id="progIconUrl" typeId="progIconUrl"/>
<channel id="icon" typeId="icon"/>
<channel id="elapsedTime" typeId="elapsedTime"/>
<channel id="remainingTime" typeId="remainingTime"/>
<channel id="progress" typeId="progress"/>
</channels>
</channel-group-type>
<channel-group-type id="nextprog">
<label>Next Program</label>
<description>Program which will follow current program on this channel</description>
<channels>
<channel id="progStart" typeId="progStart"/>
<channel id="timeLeft" typeId="timeLeft"/>
<channel id="progEnd" typeId="progEnd"/>
<channel id="progTitle" typeId="progTitle"/>
<channel id="progCategory" typeId="progCategory"/>
<channel id="progIconUrl" typeId="progIconUrl"/>
<channel id="icon" typeId="icon"/>
</channels>
</channel-group-type>
<channel-type id="iconUrl" advanced="true">
<item-type>String</item-type>
<label>Channel Icon URL</label>
<description>Icon URL of the TV channel.</description>
<state readOnly="true" pattern="%s"/>
</channel-type>
<channel-type id="progIconUrl" advanced="true">
<item-type>String</item-type>
<label>Program URL</label>
<description>URL to an image of the program.</description>
<state readOnly="true" pattern="%s"/>
</channel-type>
<channel-type id="progTitle">
<item-type>String</item-type>
<label>Title</label>
<description>Program Title.</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="progCategory">
<item-type>String</item-type>
<label>Category</label>
<description>Program Category.</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="progStart">
<item-type>DateTime</item-type>
<label>Start Time</label>
<description>Program Start Time</description>
<state readOnly="true" pattern="%1$tF %1$tR"/>
</channel-type>
<channel-type id="progEnd">
<item-type>DateTime</item-type>
<label>End Time</label>
<description>Program End Time</description>
<state readOnly="true" pattern="%1$tF %1$tR"/>
</channel-type>
<channel-type id="elapsedTime">
<item-type>Number:Time</item-type>
<label>Current Time</label>
<description>Current time of currently playing program.</description>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="remainingTime" advanced="true">
<item-type>Number:Time</item-type>
<label>Remaining Time</label>
<description>Time remaining until end of the program.</description>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="timeLeft" advanced="true">
<item-type>Number:Time</item-type>
<label>Time Left</label>
<description>Time left before program start</description>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="progress">
<item-type>Number:Dimensionless</item-type>
<label>Progress</label>
<description>Relative progression of the current program.</description>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="icon">
<item-type>Image</item-type>
<label>Icon</label>
<description>Icon of the channel / program.</description>
<state readOnly="true"></state>
</channel-type>
</thing:thing-descriptions>