Maybe theres someone else, who is interested in working with EPG-informations in his or her openHAB environment.
This soloution is based on my wifes wish, having a simple ad-free way to see the current and next-running tv-show on all the commonly used channels in our country (and switching on the TV with the current channel running / or setting a timer for recording shows in the background / etc.).
This is just a first simple method to implement the most relevant infos into openHAB with the help of xmltv (http://wiki.xmltv.org/index.php/Main_Page) which serves as a base for e.g. a kodi-/tv-binding implementation (which is not a part of this implementation, yet).
But first things first - a word of caution:
- I´m not fully aware of the current legal status for grabbing channel- and show-informations from multiple web-sources (this is what a xmltv-grabber do) - so please consider checking your countries law, before grabbing data with xmltv.
- I´m not a developer, nor a skilled scripter (is there even a difference?! - whatever) - so please use any of the provided infos/scrips with caution and feel free to review anything - I´m willed to learn and making things better in the future.
- I´m using openHABian v1.4.1 on a Rpi3b+ for myself and the shown install-script and OH-rule may not work with a different directory-structure than the default (â/etc/openhab2/â) - but it should be realy easy to rewrite the script to your own needs.
So - what can this one do for you?
- Showing Title and Subtitle for the currently running and next tv-show for all your selected channels
- Start / Stoptime for currently running and next tv-show
- There are a lot more informations available (like show description, cast-infos, channel-logos etc.) - but depending on the used xmltv-source, these informations may be incomplete - so I skipped them in this first version.
(EPG.sitemap - channel overview)
(EPG.sitemap - current show details)
(EPG.sitemap - next show details)
Installation steps
-
Install & configure xmltv
1.1. Automatic: Using my rudimentary bash install-script from here
1.2. Manual: Follow these steps:
# Update system and install XMLTV
sudo apt-get update
sudo apt-get install xmltv
# Get 'epg_parser.py' from github - to extract values from the xml-files
sudo -u openhab wget https://raw.githubusercontent.com/sparetimetherapist/xlmtv_for_openhab/master/epg_parser.py -O /etc/openhab2/scripts/epg_parser.py
# Creating the needed directories
sudo -u openhab mkdir -p /etc/openhab2/html/epg/{data/tmp,conf}
# Configuring xmltv-grabber (this one is for EU channels)
sudo -u openhab /usr/bin/tv_grab_eu_egon --configure --config-file /etc/openhab2/html/epg/conf/epg_eu.conf
# Only execute the following if your timezone is not UTC (your local timezone is CET in this example)
sudo sed -i -e 's/UTC/CET/g' /usr/bin/tv_grep
# Now edit your xmltv-settings, using your favourite editor (exchange '!' with '=' to add a channel)
sudo -u openhab nano /etc/openhab2/html/epg/conf/epg_eu.conf
-
Install EXEC-Binding and MAP / JS Transformation in openHAB
2.1. epg_duration.js -
Create stuff in OH
3.1. Reffer XMLTV-channelIDs to OH by creating aepg.map
in the following syntax (depending on the channels you choose in the âepg_eu.confâ)
NULL=none
-=none
1=hd.daserste.de
2=hd.zdf.de
3.2. Create EPG.items
(example for 2 channels / Create a new block for each channel you want to add)
Group gTVChannel
Group gTVStopTime
Number EPG_Update "EPG Status [%s]"
///////////////////////// Channel 1 /////////////////////////////
String TV_Channel_1_Name "Sender [%s]" (gTVChannel)
String TV_Channel_1_CurrentShow "Aktuell läuft [%s]" <live>
String TV_Channel_1_NextShow "Danach läuft [%s]"
DateTime TV_Channel_1_CurrentShowStartTime "Startzeit [%1$td.%1$tm.%1$tY - %1$tH:%1$tM Uhr]"
DateTime TV_Channel_1_NextShowStartTime "Startzeit [%1$td.%1$tm.%1$tY - %1$tH:%1$tM Uhr]"
DateTime TV_Channel_1_CurrentShowStopTime "Stopzeit [%1$td.%1$tm.%1$tY - %1$tH:%1$tM Uhr]" (gTVStopTime)
DateTime TV_Channel_1_NextShowStopTime "Stopzeit [%1$td.%1$tm.%1$tY - %1$tH:%1$tM Uhr]"
String TV_Channel_1_NextShowStartStop "Start / Ende [%s Uhr]"
String TV_Channel_1_CurrentShowDuration "Verbleibende Zeit [JS(epg_duration.js):%s]"
/////////////////////////////////////////////////////////////////
///////////////////////// Channel 2 /////////////////////////////
String TV_Channel_2_Name "Sender [%s]" (gTVChannel)
String TV_Channel_2_CurrentShow "Aktuell läuft [%s]" <live>
String TV_Channel_2_NextShow "Danach läuft [%s]"
DateTime TV_Channel_2_CurrentShowStartTime "Startzeit [%1$td.%1$tm.%1$tY - %1$tH:%1$tM Uhr]"
DateTime TV_Channel_2_NextShowStartTime "Startzeit [%1$td.%1$tm.%1$tY - %1$tH:%1$tM Uhr]"
DateTime TV_Channel_2_CurrentShowStopTime "Stopzeit [%1$td.%1$tm.%1$tY - %1$tH:%1$tM Uhr]" (gTVStopTime)
DateTime TV_Channel_2_NextShowStopTime "Stopzeit [%1$td.%1$tm.%1$tY - %1$tH:%1$tM Uhr]"
String TV_Channel_2_CurrentShowStartStop "Start / Ende [%s Uhr]"
String TV_Channel_2_NextShowStartStop "Start / Ende [%s Uhr]"
String TV_Channel_2_CurrentShowDuration "Verbleibende Zeit [JS(epg_duration.js):%s]"
/////////////////////////////////////////////////////////////////
3.3. Create your OH rule-file: EPG.rule
(works with as many channels as you want)
import java.io.File // You need to import this one for file-checking
// #############################################################
// Global EPG Settings
// #############################################################
//
// ----------------------- PREREQUISITES -----------------------
// 'Exec' Binding
// 'MAP' Transformation (including 'epg.map')
// 'xmltv' installed ('sudo apt-get install xmltv')
// 'epg_parser.py' in OH script-directory
//
// 'JS' Transformation (for easier duration-calculation) [OPTIONAL]
// -------------------------------------------------------------
//
val xml_grabber = "tv_grab_eu_egon" // There may be a need to change the used grabber, depending on your region (this one is for EU channels)
val xml_path = "/etc/openhab2/html/epg/"
val script_path = "/etc/openhab2/scripts/"
//
// #############################################################
// -------------------------------------------------------------> START
//
// EPG Rule #1
//
// This rule crawls all the public available EPG-Sources based
// on the selection in your xmltv-config.
//
// This one requires internet-connection to execute!
//
// -------------------------------------------------------------
rule "EPG - Update 2-day forecast XMLTV-file"
when
System started or // Execute on system startup...
Time cron "0 3 0 * * ?" or // ... every night at 3 o'clock...
Item EPG_Update received update 0 // ... and if another rule sends update '0' to 'EPG_Update'.
then
// Define variables
val epgConfig = new File(xml_path + "conf/epg_eu.conf")
val epgSource = new File(xml_path + "data/epgsource.xml")
// Abort if xmltv-config is missing
if (epgConfig.isFile() == false || epgConfig.canRead() == false) {
logError("EPG","EPG Error: Update/Initialization failed ('epg_eu.conf' is missing or can´t be accessed)")
return
}
// Check if epgsource.xml is available...
if (epgSource.isFile() && epgSource.canRead()) {
val newdate = new DateTime(now().minusHours(12))
val filedate = new DateTime(epgSource.lastModified())
// Refresh epgsource.xml if it´s lastModified date is older>12h
if(filedate.isBefore(newdate)) {
logInfo("EPG","EPG: 'epgsource.xml' is available, but older than 12 hours - Starting update... ")
executeCommandLine("sudo /usr/bin/" + xml_grabber + " --config-file " + xml_path + "conf/epg_eu.conf --days 2 --quiet --output " + xml_path + "data/epgsource.xml",5000)
sendCommand(EPG_Update, 1) // continue with rule step #2
return
} else { return }
} else {
// Execute if epgsource.xml can´t be found or accessed.
logInfo("EPG","EPG: 'epgsource.xml ' isn´t available or can´t be accessed - Fetching EPG-data...")
executeCommandLine("sudo /usr/bin/" + xml_grabber + " --config-file " + xml_path + "conf/epg_eu.conf --days 2 --quiet --output " + xml_path + "data/epgsource.xml",5000)
sendCommand(EPG_Update, 1) // continue with rule step #2
}
end // -------------------------------------------------------------> END
// -------------------------------------------------------------> START
//
// EPG Rule #2
//
// This one minimizes the 'epgsource.xml' hourly, by creating
// another file for all shows running until NOW.
//
// -------------------------------------------------------------
rule "EPG - Minimize XMLTV-file every hour"
when
Time cron "0 0 * * * ?" or // Execute every hour...
Item EPG_Update received update 1 // ... and if rule #1 sends a trigger
then
// Check for 'epgsource.xml'
val epgSource = new File(xml_path + "data/epgsource.xml")
if(epgSource.isFile() && epgSource.canRead()) {
// Extracts all shows running until NOW, depending on your local time
executeCommandLine("sudo /usr/bin/tv_grep --on-after now --output " + xml_path + "data/epgsource_bynow.xml " + xml_path + "data/epgsource.xml",5000)
sendCommand(EPG_Update, 2) // continue with rule #3
logInfo("EPG","EPG: 'epgsource.xml' successfully minimized.")
} else {
sendCommand(EPG_Update, 0) // if 'epgsource' wasn´t found - go back to rule #1...
return
}
end // -------------------------------------------------------------> END
// -------------------------------------------------------------> START
//
// EPG Rule #3
//
// Extract the current and next tv-shows for all selected
// channels from 'epgsource_bynow.xml'.
//
// -------------------------------------------------------------
rule "EPG - Get current and next tv-show from minimized XMLTV-file"
when
Item EPG_Update received update 2 // Execute on update '2'
then
// Check for 'epgsource_bynow.xml'
val epgFiltered = new File(xml_path + "data/epgsource_bynow.xml")
if(epgFiltered.isFile() && epgFiltered.canRead()) {
// Split minimized xmltv-file into one for each channel
executeCommandLine("sudo /usr/bin/tv_split --output " + xml_path + "data/%channel_bynow.xml " + xml_path + "data/epgsource_bynow.xml",5000)
gTVChannel.members.filter[ i | i.state != NULL ].forEach[ i |
// Extract and transform channelID from item
val chan = i.name.split("_").get(2).toString
val channelId = transform("MAP", "epg.map", chan)
// Query for current- and next-show infos
val stop = executeCommandLine("sudo python " + script_path + "epg_parser.py -t " + xml_path + "data/" + channelId + "_bynow.xml -c get_stopextra",5000)
executeCommandLine("sudo /usr/bin/tv_grep --on-after now --on-before now --output " + xml_path + "data/" + channelId + "_now.xml " + xml_path + "data/" + channelId + "_bynow.xml",5000)
executeCommandLine("sudo /usr/bin/tv_grep --on-after " + stop + " --on-before " + stop + " --output " + xml_path + "data/" + channelId + "_next.xml " + xml_path + "data/" + channelId + "_bynow.xml",5000)
]
// Continue to rule #4
sendCommand(EPG_Update, 3)
return
} else {
logError("EPG", "EPG Error: Update failed ('epgsource_bynow.xml' is missing or can´t be accessed)" )
sendCommand(EPG_Update, 1) // return to rule #2
return
}
end // -------------------------------------------------------------> END
// -------------------------------------------------------------> START
//
// EPG Rule #4
//
// Extract all values from the *.xml-files and send them to
// the corresponding openHAB-items.
//
// -------------------------------------------------------------
rule "EPG - Send updated infos to openHAB-items"
when
Item EPG_Update received update 3 // Execute on update #3
then
gTVChannel.members.forEach[ i |
val room = i.name.split("_").get(0).toString
val type = i.name.split("_").get(1).toString
val chan = i.name.split("_").get(2).toString
// Set variable item-names & transform values
val currentShowTitleItem = room + "_" + type + "_" + chan + "_CurrentShow"
val nextShowTitleItem = room + "_" + type + "_" + chan + "_NextShow"
val currentShowStartTimeItem = room + "_" + type + "_" + chan + "_CurrentShowStartTime"
val nextShowStartTimeItem = room + "_" + type + "_" + chan + "_NextShowStartTime"
val currentShowStopTimeItem = room + "_" + type + "_" + chan + "_CurrentShowStopTime"
val nextShowStopTimeItem = room + "_" + type + "_" + chan + "_NextShowStopTime"
// val currentShowStartStopItem = room + "_" + type + "_" + chan + "_CurrentShowStartStop"
val nextShowStartStopItem = room + "_" + type + "_" + chan + "_NextShowStartStop"
// val logoURLItem = room + "_" + type + "_" + chan + "_LogoURL"
val channelId = transform("MAP", "epg.map", chan)
// Execute python-script
val channelName = executeCommandLine("sudo python " + script_path + "epg_parser.py -t " + xml_path + "data/" + channelId + "_now.xml -c get_channelname",5000)
val currentShowTitle = executeCommandLine("sudo python " + script_path + "epg_parser.py -t " + xml_path + "data/" + channelId + "_now.xml -c get_title",5000)
val nextShowTitle = executeCommandLine("sudo python " + script_path + "epg_parser.py -t " + xml_path + "data/" + channelId + "_next.xml -c get_title",5000)
val currentShowStartTime = executeCommandLine("sudo python " + script_path + "epg_parser.py -t " + xml_path + "data/" + channelId + "_now.xml -c get_starttime",5000)
val nextShowStartTime = executeCommandLine("sudo python " + script_path + "epg_parser.py -t " + xml_path + "data/" + channelId + "_next.xml -c get_starttime",5000)
val currentShowStopTime = executeCommandLine("sudo python " + script_path + "epg_parser.py -t " + xml_path + "data/" + channelId + "_now.xml -c get_stoptime",5000)
val nextShowStopTime = executeCommandLine("sudo python " + script_path + "epg_parser.py -t " + xml_path + "data/" + channelId + "_next.xml -c get_stoptime",5000)
// val currentShowStartStop = executeCommandLine("sudo python " + script_path + "epg_parser.py -t " + xml_path + "data/" + channelId + "_now.xml -c get_startstop",5000)
val nextShowStartStop = executeCommandLine("sudo python " + script_path + "epg_parser.py -t " + xml_path + "data/" + channelId + "_next.xml -c get_startstop",5000)
// val logoURL = executeCommandLine("sudo python " + script_path + "epg_parser.py -t " + xml_path + "data/" + channelId + "_now.xml -c get_logourl",5000)
// Sending stuff to items
sendCommand(i.name, channelName)
sendCommand(currentShowTitleItem, currentShowTitle)
sendCommand(nextShowTitleItem, nextShowTitle)
if (currentShowStartTime != "" || currentShowStartTime != NULL) { sendCommand(currentShowStartTimeItem, currentShowStartTime.toString) }
sendCommand(nextShowStartTimeItem, nextShowStartTime.toString)
if (currentShowStopTime.toString != "" || currentShowStopTime.toString != NULL) { sendCommand(currentShowStopTimeItem, currentShowStopTime.toString) }
sendCommand(nextShowStopTimeItem, nextShowStopTime.toString)
// sendCommand(currentShowStartStopItem, currentShowStartStop.toString)
sendCommand(nextShowStartStopItem, nextShowStartStop.toString)
// sendCommand(logoURLItem, logoURL)
]
// Calculate remaining show time for current runnning show
gTVStopTime.members.forEach[ i |
val currentShowDuration = i.name.split("_").get(0).toString+"_"+i.name.split("_").get(1).toString+"_"+i.name.split("_").get(2).toString+"_CurrentShowDuration"
val oldDate = new DateTime((i.state as DateTimeType).calendar.timeInMillis).millis
val newDate = new DateTime(now()).millis
val duration = (((oldDate-newDate) / 1000) / 60)
sendCommand(currentShowDuration, duration.toString)
]
sendCommand(EPG_Update, 4) // Sending successfull value to update-item
logInfo("EPG","EPG: openHAB-items succesfully updated! \u2713")
end // -------------------------------------------------------------> END
// -------------------------------------------------------------> START
//
// EPG Rule #5
//
// This rule is very important to reduce the system calls to a
// minimum - it checks, if the stop-time of any of the current
// running tv-shows is reached.
//
// -------------------------------------------------------------
rule "EPG - Check if an EPG update is needed"
when
Time cron "0 0/1 * * * ?" // Execute every minute...
then
// Check for 'epgsource_bynow.xml'
val epgByNow = new File(xml_path + "data/epgsource_bynow.xml")
if(epgByNow.isFile() && epgByNow.canRead()) {
// Calculate remaining show time for current runnning show
gTVStopTime.members.forEach[ i |
val currentShowDuration = i.name.split("_").get(0).toString+"_"+i.name.split("_").get(1).toString+"_"+i.name.split("_").get(2).toString+"_CurrentShowDuration"
val oldDate = new DateTime((i.state as DateTimeType).calendar.timeInMillis).millis
val newDate = new DateTime(now()).millis
val duration = (((oldDate-newDate) / 1000) / 60)
sendCommand(currentShowDuration, duration.toString)
]
// Get timetsamp of the show with the earliest stoptime
val stopTime = executeCommandLine("sudo python " + script_path + "epg_parser.py -t " + xml_path + "data/epgsource_bynow.xml -c get_stopextra",5000)
val DateTimeType timestamp = DateTimeType.valueOf(stopTime) // Convert human readable time stamp to DateTimeType
// Compare current time with earliest stopDate of the current running shows
if (now.isAfter(new DateTime(timestamp.zonedDateTime.toInstant.toEpochMilli)) ) {
executeCommandLine("sudo /usr/bin/tv_grep --on-after now --output " + xml_path + "data/epgsource_bynow.xml " + xml_path + "data/epgsource.xml",5000) // Update 'epgsource_bynow.xml'
sendCommand(EPG_Update, 2) // execute rule #2
logInfo("EPG", "EPG: A TV-show has ended. Starting update...")
} else { return }
} else {
logError("EPG", "EPG Error: Update failed ('epgsource_bynow.xml' is missing or can´t be accessed)" )
sendCommand(EPG_Update, 1) // return to rule #2
return
}
end // -------------------------------------------------------------> END
3.4. And thats what your EPG.sitemap
could look like:
sitemap epg label="EPG"
{
Frame label="" {
Text item=TV_Channel_1_CurrentShow label="Das Erste [%s]" icon="tv_ard" {
Frame item=TV_Channel_1_Name label="" {
Text item=TV_Channel_1_CurrentShow icon="tv_ard"
Text item=TV_Channel_1_CurrentShowDuration icon="time"
}
}
Text item=TV_Channel_1_NextShow icon="next" {
Frame item=TV_Channel_1_Name label="" {
Text item=TV_Channel_1_NextShow label="Das Erste" icon="tv_ard"
Text item=TV_Channel_1_NextShowStartStop
}
}
}
Frame label="" {
Text item=TV_Channel_2_CurrentShow label="ZDF [%s]" icon="tv_zdf" {
Frame item=TV_Channel_2_Name label="" {
Text item=TV_Channel_2_CurrentShow icon="tv_zdf"
Text item=TV_Channel_2_CurrentShowDuration icon="time"
}
}
Text item=TV_Channel_2_NextShow icon="next" {
Frame item=TV_Channel_2_Name label="" {
Text item=TV_Channel_2_NextShow label="Das Erste" icon="tv_ard"
Text item=TV_Channel_2_NextShowStartStop
}
}
}
}
The rule should trigger an initial update automatically, after you created the above files.
If you find any errors or have improvements for anything, please tell me!